mirror of
https://github.com/hcengineering/platform.git
synced 2026-09-23 01:55:00 +02:00
Merge branch 'main' of github.com:hcengineering/anticrm into show-filter-items-current-space
Signed-off-by: Denis Bunakalya <denis.bunakalya@xored.com>
This commit is contained in:
@@ -15,17 +15,16 @@
|
||||
<script lang="ts">
|
||||
import contact, { Employee } from '@hcengineering/contact'
|
||||
import { Class, Doc, Ref } from '@hcengineering/core'
|
||||
import { Issue, IssueTemplate } from '@hcengineering/tracker'
|
||||
import { UsersPopup, getClient } from '@hcengineering/presentation'
|
||||
import { AttributeModel } from '@hcengineering/view'
|
||||
import { eventToHTMLElement, showPopup } from '@hcengineering/ui'
|
||||
import { getObjectPresenter } from '@hcengineering/view-resources'
|
||||
import { IntlString } from '@hcengineering/platform'
|
||||
import { getClient, UsersPopup } from '@hcengineering/presentation'
|
||||
import { Issue, IssueTemplate } from '@hcengineering/tracker'
|
||||
import { eventToHTMLElement, showPopup } from '@hcengineering/ui'
|
||||
import { AttributeModel } from '@hcengineering/view'
|
||||
import { getObjectPresenter } from '@hcengineering/view-resources'
|
||||
import tracker from '../../plugin'
|
||||
|
||||
export let value: Employee | null | undefined
|
||||
export let issueId: Ref<Issue>
|
||||
export let issueClass: Ref<Class<Issue | IssueTemplate>> = tracker.class.Issue
|
||||
export let object: Issue | IssueTemplate
|
||||
export let defaultClass: Ref<Class<Doc>> | undefined = undefined
|
||||
export let isEditable: boolean = true
|
||||
export let shouldShowLabel: boolean = false
|
||||
@@ -52,15 +51,9 @@
|
||||
return
|
||||
}
|
||||
|
||||
const currentIssue = await client.findOne(issueClass, { _id: issueId })
|
||||
|
||||
if (currentIssue === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
const newAssignee = result === null ? null : result._id
|
||||
|
||||
await client.update(currentIssue, { assignee: newAssignee })
|
||||
await client.update(object, { assignee: newAssignee })
|
||||
}
|
||||
|
||||
const handleAssigneeEditorOpened = async (event: MouseEvent) => {
|
||||
|
||||
@@ -44,14 +44,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
$: tooltipValue = new Date(value).toLocaleString('default', {
|
||||
minute: '2-digit',
|
||||
hour: 'numeric',
|
||||
day: '2-digit',
|
||||
month: 'short',
|
||||
year: 'numeric'
|
||||
})
|
||||
|
||||
$: formatTime(value)
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
<span class="root" />
|
||||
|
||||
<style lang="scss">
|
||||
.root {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
min-width: 0;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
flex-shrink: 10;
|
||||
}
|
||||
</style>
|
||||
@@ -1,22 +1,22 @@
|
||||
<script lang="ts">
|
||||
import { fade } from 'svelte/transition'
|
||||
import {
|
||||
NotificationSeverity,
|
||||
Notification,
|
||||
Button,
|
||||
Icon,
|
||||
IconClose,
|
||||
IconInfo,
|
||||
IconCheckCircle,
|
||||
Label,
|
||||
showPanel
|
||||
} from '@hcengineering/ui'
|
||||
import { copyTextToClipboard, createQuery } from '@hcengineering/presentation'
|
||||
import { Issue, IssueStatus } from '@hcengineering/tracker'
|
||||
import {
|
||||
AnySvelteComponent,
|
||||
Button,
|
||||
Icon,
|
||||
IconCheckCircle,
|
||||
IconClose,
|
||||
IconInfo,
|
||||
Notification,
|
||||
NotificationSeverity,
|
||||
showPanel
|
||||
} from '@hcengineering/ui'
|
||||
import { fade } from 'svelte/transition'
|
||||
|
||||
import IssueStatusIcon from './IssueStatusIcon.svelte'
|
||||
import IssuePresenter from './IssuePresenter.svelte'
|
||||
import tracker from '../../plugin'
|
||||
import IssuePresenter from './IssuePresenter.svelte'
|
||||
import IssueStatusIcon from './IssueStatusIcon.svelte'
|
||||
|
||||
export let notification: Notification
|
||||
export let onRemove: () => void
|
||||
@@ -31,7 +31,7 @@
|
||||
|
||||
$: issueQuery.query(
|
||||
tracker.class.Issue,
|
||||
{ _id: params.issueId },
|
||||
{ _id: params?.issueId },
|
||||
(res) => {
|
||||
issue = res[0]
|
||||
},
|
||||
@@ -49,7 +49,7 @@
|
||||
)
|
||||
}
|
||||
|
||||
const getIcon = () => {
|
||||
const getIcon = (): AnySvelteComponent | undefined => {
|
||||
switch (severity) {
|
||||
case NotificationSeverity.Success:
|
||||
return IconCheckCircle
|
||||
@@ -84,14 +84,17 @@
|
||||
copyTextToClipboard(params?.issueUrl)
|
||||
}
|
||||
}
|
||||
$: icon = getIcon()
|
||||
</script>
|
||||
|
||||
<div class="root" in:fade out:fade>
|
||||
<Icon icon={getIcon()} size="medium" fill={getIconColor()} />
|
||||
{#if icon}
|
||||
<Icon {icon} size="medium" fill={getIconColor()} />
|
||||
{/if}
|
||||
|
||||
<div class="content">
|
||||
<div class="title">
|
||||
<Label label={title} />
|
||||
{title}
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="issue">
|
||||
@@ -105,7 +108,7 @@
|
||||
{subTitle}
|
||||
</div>
|
||||
<div class="postfix">
|
||||
{params.subTitlePostfix}
|
||||
{params?.subTitlePostfix}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { WithLookup } from '@hcengineering/core'
|
||||
import { Ref, WithLookup } from '@hcengineering/core'
|
||||
import { createQuery } from '@hcengineering/presentation'
|
||||
import type { Issue, Team } from '@hcengineering/tracker'
|
||||
import { showPanel } from '@hcengineering/ui'
|
||||
@@ -23,6 +23,9 @@
|
||||
export let disableClick = false
|
||||
export let onClick: (() => void) | undefined = undefined
|
||||
|
||||
// Extra properties
|
||||
export let teams: Map<Ref<Team>, Team> | undefined = undefined
|
||||
|
||||
function handleIssueEditorOpened () {
|
||||
if (disableClick) {
|
||||
return
|
||||
@@ -38,16 +41,21 @@
|
||||
const spaceQuery = createQuery()
|
||||
let currentTeam: Team | undefined = value?.$lookup?.space
|
||||
|
||||
$: if (value && value?.$lookup?.space === undefined) {
|
||||
spaceQuery.query(tracker.class.Team, { _id: value.space }, (res) => ([currentTeam] = res))
|
||||
$: if (teams === undefined) {
|
||||
if (value && value?.$lookup?.space === undefined) {
|
||||
spaceQuery.query(tracker.class.Team, { _id: value.space }, (res) => ([currentTeam] = res))
|
||||
} else {
|
||||
spaceQuery.unsubscribe()
|
||||
}
|
||||
} else {
|
||||
spaceQuery.unsubscribe()
|
||||
currentTeam = teams.get(value.space)
|
||||
}
|
||||
|
||||
$: title = currentTeam ? `${currentTeam.identifier}-${value?.number}` : `${value?.number}`
|
||||
</script>
|
||||
|
||||
{#if value}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<span
|
||||
class="issuePresenterRoot"
|
||||
class:noPointer={disableClick}
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
async function updateStatus (
|
||||
txes: Tx[],
|
||||
statuses: Map<Ref<IssueStatus>, WithLookup<IssueStatus>>,
|
||||
now: number
|
||||
_: number
|
||||
): Promise<void> {
|
||||
const result: WithTime[] = []
|
||||
|
||||
|
||||
@@ -24,6 +24,8 @@
|
||||
export let size: IconSize
|
||||
export let fill: string | undefined = undefined
|
||||
|
||||
export let issueStatuses: IssueStatus[] | undefined = undefined
|
||||
|
||||
const dynamicFillCategories = [tracker.issueStatusCategory.Started]
|
||||
|
||||
const client = getClient()
|
||||
@@ -36,12 +38,19 @@
|
||||
} = { index: undefined, count: undefined }
|
||||
|
||||
const categoriesQuery = createQuery()
|
||||
categoriesQuery.query(
|
||||
tracker.class.IssueStatus,
|
||||
{ category: tracker.issueStatusCategory.Started },
|
||||
(res) => (statuses = res),
|
||||
{ sort: { rank: SortingOrder.Ascending } }
|
||||
)
|
||||
|
||||
$: if (issueStatuses === undefined) {
|
||||
categoriesQuery.query(
|
||||
tracker.class.IssueStatus,
|
||||
{ category: tracker.issueStatusCategory.Started },
|
||||
(res) => (statuses = res),
|
||||
{ sort: { rank: SortingOrder.Ascending } }
|
||||
)
|
||||
} else {
|
||||
const _s = [...issueStatuses.filter((it) => it.category === tracker.issueStatusCategory.Started)]
|
||||
_s.sort((a, b) => a.rank.localeCompare(b.rank))
|
||||
categoriesQuery.unsubscribe()
|
||||
}
|
||||
|
||||
async function updateCategory (status: WithLookup<IssueStatus>, statuses: IssueStatus[]) {
|
||||
if (status.$lookup?.category) {
|
||||
|
||||
@@ -1,23 +1,39 @@
|
||||
<script lang="ts">
|
||||
import { DocumentQuery, WithLookup } from '@hcengineering/core'
|
||||
import { DocumentQuery, Ref, Space, WithLookup } from '@hcengineering/core'
|
||||
import { Issue, IssueStatus, Team } from '@hcengineering/tracker'
|
||||
import { Component } from '@hcengineering/ui'
|
||||
import { Viewlet } from '@hcengineering/view'
|
||||
import { Issue } from '@hcengineering/tracker'
|
||||
import { viewOptionsStore } from '@hcengineering/view-resources'
|
||||
import { Viewlet, ViewOptions } from '@hcengineering/view'
|
||||
import tracker from '../../plugin'
|
||||
import CreateIssue from '../CreateIssue.svelte'
|
||||
|
||||
export let viewlet: WithLookup<Viewlet>
|
||||
export let query: DocumentQuery<Issue> = {}
|
||||
export let space: Ref<Space> | undefined
|
||||
|
||||
// Extra properties
|
||||
export let teams: Map<Ref<Team>, Team> | undefined
|
||||
export let issueStatuses: Map<Ref<Team>, WithLookup<IssueStatus>[]>
|
||||
export let viewOptions: ViewOptions
|
||||
|
||||
const createItemDialog = CreateIssue
|
||||
const createItemLabel = tracker.string.AddIssueTooltip
|
||||
</script>
|
||||
|
||||
{#if viewlet?.$lookup?.descriptor?.component}
|
||||
<Component
|
||||
is={viewlet.$lookup.descriptor.component}
|
||||
props={{
|
||||
_class: tracker.class.Issue,
|
||||
config: viewlet.config,
|
||||
options: viewlet.options,
|
||||
createItemDialog,
|
||||
createItemLabel,
|
||||
viewlet,
|
||||
viewOptions,
|
||||
viewOptionsConfig: viewlet.viewOptions?.other,
|
||||
space,
|
||||
query,
|
||||
viewOptions: $viewOptionsStore
|
||||
props: { teams, issueStatuses }
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
$: groupedByProject = getGroupedIssues('project', issues)
|
||||
$: groupedBySprint = getGroupedIssues('sprint', issues)
|
||||
|
||||
const handleStatusFilterMenuSectionOpened = (event: MouseEvent | KeyboardEvent) => {
|
||||
const handleStatusFilterMenuSectionOpened = () => {
|
||||
const statusGroups: { [key: string]: number } = {}
|
||||
|
||||
for (const status of defaultStatuses) {
|
||||
@@ -66,7 +66,7 @@
|
||||
)
|
||||
}
|
||||
|
||||
const handlePriorityFilterMenuSectionOpened = (event: MouseEvent | KeyboardEvent) => {
|
||||
const handlePriorityFilterMenuSectionOpened = () => {
|
||||
const priorityGroups: { [key: string]: number } = {}
|
||||
|
||||
for (const priority of defaultPriorities) {
|
||||
@@ -86,7 +86,7 @@
|
||||
)
|
||||
}
|
||||
|
||||
const handleProjectFilterMenuSectionOpened = (event: MouseEvent | KeyboardEvent) => {
|
||||
const handleProjectFilterMenuSectionOpened = () => {
|
||||
const projectGroups: { [key: string]: number } = {}
|
||||
|
||||
for (const [project, value] of Object.entries(groupedByProject)) {
|
||||
@@ -105,7 +105,7 @@
|
||||
)
|
||||
}
|
||||
|
||||
const handleSprintFilterMenuSectionOpened = (event: MouseEvent | KeyboardEvent) => {
|
||||
const handleSprintFilterMenuSectionOpened = () => {
|
||||
const sprintGroups: { [key: string]: number } = {}
|
||||
|
||||
for (const [project, value] of Object.entries(groupedBySprint)) {
|
||||
|
||||
@@ -1,355 +0,0 @@
|
||||
<!--
|
||||
// Copyright © 2022 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import contact, { Employee } from '@hcengineering/contact'
|
||||
import { Class, Doc, FindOptions, Ref, WithLookup } from '@hcengineering/core'
|
||||
import { createQuery, getClient } from '@hcengineering/presentation'
|
||||
import { Issue, IssueStatus, Team } from '@hcengineering/tracker'
|
||||
import ui, {
|
||||
ActionIcon,
|
||||
Button,
|
||||
CheckBox,
|
||||
Component,
|
||||
eventToHTMLElement,
|
||||
ExpandCollapse,
|
||||
getEventPositionElement,
|
||||
IconAdd,
|
||||
IconMoreH,
|
||||
Label,
|
||||
showPopup,
|
||||
Spinner
|
||||
} from '@hcengineering/ui'
|
||||
import { AttributeModel, BuildModelKey } from '@hcengineering/view'
|
||||
import {
|
||||
buildModel,
|
||||
filterStore,
|
||||
FixedColumn,
|
||||
getObjectPresenter,
|
||||
LoadingProps,
|
||||
Menu
|
||||
} from '@hcengineering/view-resources'
|
||||
import { onDestroy } from 'svelte'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import tracker from '../../plugin'
|
||||
import { IssuesGroupByKeys, issuesGroupEditorMap, IssuesOrderByKeys, issuesSortOrderMap } from '../../utils'
|
||||
import CreateIssue from '../CreateIssue.svelte'
|
||||
import IssueStatistics from '../sprints/IssueStatistics.svelte'
|
||||
import IssuesListItem from './IssuesListItem.svelte'
|
||||
|
||||
export let _class: Ref<Class<Doc>>
|
||||
export let currentSpace: Ref<Team> | undefined = undefined
|
||||
export let groupByKey: IssuesGroupByKeys | undefined = undefined
|
||||
export let orderBy: IssuesOrderByKeys
|
||||
export let statuses: WithLookup<IssueStatus>[]
|
||||
export let employees: (WithLookup<Employee> | undefined)[] = []
|
||||
export let categories: any[] = []
|
||||
export let baseMenuClass: Ref<Class<Doc>> | undefined = undefined
|
||||
export let itemsConfig: (BuildModelKey | string)[]
|
||||
export let selectedObjectIds: Doc[] = []
|
||||
export let selectedRowIndex: number | undefined = undefined
|
||||
export let groupedIssues: { [key: string | number | symbol]: Issue[] } = {}
|
||||
export let loadingProps: LoadingProps | undefined = undefined
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
const client = getClient()
|
||||
const objectRefs: HTMLElement[] = []
|
||||
const baseOptions: FindOptions<Issue> = {
|
||||
lookup: {
|
||||
assignee: contact.class.Employee,
|
||||
status: tracker.class.IssueStatus,
|
||||
space: tracker.class.Team,
|
||||
_id: {
|
||||
subIssues: tracker.class.Issue
|
||||
}
|
||||
}
|
||||
}
|
||||
const categoryLimit: Record<any, number> = {}
|
||||
const spaceQuery = createQuery()
|
||||
const defaultLimit = 20
|
||||
const autoFoldLimit = 20
|
||||
const singleCategoryLimit = 200
|
||||
const noCategory = '#no_category'
|
||||
|
||||
let currentTeam: Team | undefined
|
||||
let personPresenter: AttributeModel
|
||||
let isCollapsedMap: Record<any, boolean> = {}
|
||||
let itemModels: AttributeModel[]
|
||||
let isFilterUpdate = false
|
||||
let groupedIssuesBeforeFilter = groupedIssues
|
||||
|
||||
const handleMenuOpened = async (event: MouseEvent, object: Doc, rowIndex: number) => {
|
||||
event.preventDefault()
|
||||
selectedRowIndex = rowIndex
|
||||
|
||||
if (!selectedObjectIdsSet.has(object._id)) {
|
||||
onObjectChecked(combinedGroupedIssues, false)
|
||||
|
||||
selectedObjectIds = []
|
||||
}
|
||||
|
||||
const items = selectedObjectIds.length > 0 ? selectedObjectIds : object
|
||||
|
||||
showPopup(Menu, { object: items, baseMenuClass }, getEventPositionElement(event), () => {
|
||||
selectedRowIndex = undefined
|
||||
})
|
||||
}
|
||||
|
||||
export const onObjectChecked = (docs: Doc[], value: boolean) => {
|
||||
dispatch('check', { docs, value })
|
||||
}
|
||||
|
||||
const handleRowFocused = (object: Doc) => {
|
||||
dispatch('row-focus', object)
|
||||
}
|
||||
|
||||
const handleNewIssueAdded = (event: MouseEvent, category: any) => {
|
||||
if (!currentSpace) {
|
||||
return
|
||||
}
|
||||
|
||||
showPopup(
|
||||
CreateIssue,
|
||||
{ space: currentSpace, ...(groupByKey ? { [groupByKey]: category } : {}) },
|
||||
eventToHTMLElement(event)
|
||||
)
|
||||
}
|
||||
|
||||
function toCat (category: any): any {
|
||||
return 'cat-' + (category ?? noCategory)
|
||||
}
|
||||
|
||||
const handleCollapseCategory = (category: any) => {
|
||||
isCollapsedMap[category] = !isCollapsedMap[category]
|
||||
}
|
||||
|
||||
const getLoadingElementsLength = (props: LoadingProps, options?: FindOptions<Doc>) => {
|
||||
if (options?.limit && options?.limit > 0) {
|
||||
return Math.min(options.limit, props.length)
|
||||
}
|
||||
|
||||
return props.length
|
||||
}
|
||||
|
||||
function limitGroup (
|
||||
category: any,
|
||||
groupes: { [key: string | number | symbol]: Issue[] },
|
||||
categoryLimit: Record<any, number>
|
||||
): Issue[] {
|
||||
const issues = groupes[category] ?? []
|
||||
const initialLimit = Object.keys(groupes).length === 1 ? singleCategoryLimit : defaultLimit
|
||||
const limit = categoryLimit[toCat(category)] ?? initialLimit
|
||||
return issues.slice(0, limit)
|
||||
}
|
||||
|
||||
const getInitCollapseValue = (category: any) =>
|
||||
categories.length === 1 ? false : (groupedIssues[category]?.length ?? 0) > autoFoldLimit
|
||||
|
||||
const unsubscribeFilter = filterStore.subscribe(() => (isFilterUpdate = true))
|
||||
onDestroy(unsubscribeFilter)
|
||||
|
||||
$: {
|
||||
if (isFilterUpdate && groupedIssuesBeforeFilter !== groupedIssues && groupByKey) {
|
||||
isCollapsedMap = {}
|
||||
|
||||
categories.forEach((category) => (isCollapsedMap[toCat(category)] = getInitCollapseValue(category)))
|
||||
|
||||
isFilterUpdate = false
|
||||
groupedIssuesBeforeFilter = groupedIssues
|
||||
}
|
||||
}
|
||||
|
||||
$: spaceQuery.query(tracker.class.Team, { _id: currentSpace }, (res) => {
|
||||
currentTeam = res.shift()
|
||||
})
|
||||
$: {
|
||||
const exkeys = new Set(Object.keys(isCollapsedMap))
|
||||
for (const c of categories) {
|
||||
if (!exkeys.delete(toCat(c))) {
|
||||
isCollapsedMap[toCat(c)] = getInitCollapseValue(c)
|
||||
}
|
||||
}
|
||||
for (const k of exkeys) {
|
||||
delete isCollapsedMap[k]
|
||||
}
|
||||
}
|
||||
$: combinedGroupedIssues = Object.values(groupedIssues).flat(1)
|
||||
$: options = { ...baseOptions, sort: { [orderBy]: issuesSortOrderMap[orderBy] } } as FindOptions<Issue>
|
||||
$: headerComponent = groupByKey === undefined || groupByKey === 'assignee' ? null : issuesGroupEditorMap[groupByKey]
|
||||
$: selectedObjectIdsSet = new Set<Ref<Doc>>(selectedObjectIds.map((it) => it._id))
|
||||
$: objectRefs.length = combinedGroupedIssues.length
|
||||
$: getObjectPresenter(client, contact.class.Person, { key: '' }).then((p) => {
|
||||
personPresenter = p
|
||||
})
|
||||
$: buildModel({ client, _class, keys: itemsConfig, lookup: options.lookup }).then((res) => (itemModels = res))
|
||||
</script>
|
||||
|
||||
<div class="issueslist-container">
|
||||
{#each categories as category}
|
||||
{@const items = groupedIssues[category] ?? []}
|
||||
{@const limited = limitGroup(category, groupedIssues, categoryLimit) ?? []}
|
||||
{#if headerComponent || groupByKey === 'assignee' || category === undefined}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div class="flex-between categoryHeader row" on:click={() => handleCollapseCategory(toCat(category))}>
|
||||
<div class="flex-row-center gap-2 clear-mins">
|
||||
<FixedColumn key={'issuelist_groupBy'} justify={'left'}>
|
||||
{#if groupByKey === 'assignee' && personPresenter}
|
||||
<svelte:component
|
||||
this={personPresenter.presenter}
|
||||
shouldShowLabel={true}
|
||||
value={employees.find((x) => x?._id === category)}
|
||||
defaultName={tracker.string.NoAssignee}
|
||||
shouldShowPlaceholder={true}
|
||||
isInteractive={false}
|
||||
avatarSize={'small'}
|
||||
enlargedText
|
||||
{currentSpace}
|
||||
/>
|
||||
{:else if !groupByKey}
|
||||
<span class="text-base fs-bold overflow-label content-accent-color pointer-events-none">
|
||||
<Label label={tracker.string.NoGrouping} />
|
||||
</span>
|
||||
{:else if headerComponent}
|
||||
<Component
|
||||
is={headerComponent}
|
||||
props={{
|
||||
isEditable: false,
|
||||
shouldShowLabel: true,
|
||||
value: groupByKey ? { [groupByKey]: category } : {},
|
||||
statuses: groupByKey === 'status' ? statuses : undefined,
|
||||
issues: groupedIssues[category],
|
||||
width: 'min-content',
|
||||
kind: 'list-header',
|
||||
enlargedText: true,
|
||||
currentSpace
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
</FixedColumn>
|
||||
<FixedColumn key={'issuelist_statistics'} justify={'left'}>
|
||||
<IssueStatistics issues={groupedIssues[category]} />
|
||||
</FixedColumn>
|
||||
{#if limited.length < items.length}
|
||||
<div class="counter">
|
||||
{limited.length}
|
||||
<div class="text-xs mx-1">/</div>
|
||||
{items.length}
|
||||
</div>
|
||||
<ActionIcon
|
||||
size={'small'}
|
||||
icon={IconMoreH}
|
||||
label={ui.string.ShowMore}
|
||||
action={() => {
|
||||
categoryLimit[toCat(category)] = limited.length + 20
|
||||
}}
|
||||
/>
|
||||
{:else}
|
||||
<span class="counter">{items.length}</span>
|
||||
{/if}
|
||||
</div>
|
||||
<Button
|
||||
icon={IconAdd}
|
||||
kind={'transparent'}
|
||||
showTooltip={{ label: tracker.string.AddIssueTooltip }}
|
||||
on:click={(event) => handleNewIssueAdded(event, category)}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
<ExpandCollapse isExpanded={!isCollapsedMap[toCat(category)]} duration={400}>
|
||||
{#if itemModels}
|
||||
{#if groupedIssues[category]}
|
||||
{#each limited as docObject (docObject._id)}
|
||||
<IssuesListItem
|
||||
bind:use={objectRefs[combinedGroupedIssues.findIndex((x) => x === docObject)]}
|
||||
{docObject}
|
||||
model={itemModels}
|
||||
{groupByKey}
|
||||
selected={selectedRowIndex === combinedGroupedIssues.findIndex((x) => x === docObject)}
|
||||
checked={selectedObjectIdsSet.has(docObject._id)}
|
||||
{statuses}
|
||||
{currentTeam}
|
||||
on:check={(ev) => dispatch('check', { docs: ev.detail.docs, value: ev.detail.value })}
|
||||
on:contextmenu={(event) =>
|
||||
handleMenuOpened(
|
||||
event,
|
||||
docObject,
|
||||
combinedGroupedIssues.findIndex((x) => x === docObject)
|
||||
)}
|
||||
on:focus={() => {}}
|
||||
on:mouseover={() => handleRowFocused(docObject)}
|
||||
/>
|
||||
{/each}
|
||||
{:else if loadingProps !== undefined}
|
||||
{#each Array(getLoadingElementsLength(loadingProps, options)) as _, rowIndex}
|
||||
<div class="listGrid row" class:fixed={rowIndex === selectedRowIndex}>
|
||||
<div class="flex-center clear-mins h-full">
|
||||
<div class="gridElement">
|
||||
<CheckBox checked={false} />
|
||||
<div class="ml-4">
|
||||
<Spinner size="small" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
{/if}
|
||||
</ExpandCollapse>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.issueslist-container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: max-content;
|
||||
min-width: auto;
|
||||
min-height: auto;
|
||||
}
|
||||
.categoryHeader {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
padding: 0 0.75rem 0 2.25rem;
|
||||
height: 3rem;
|
||||
min-height: 3rem;
|
||||
min-width: 0;
|
||||
background: var(--header-bg-color);
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
.row:not(:last-child) {
|
||||
border-bottom: 1px solid var(--accent-bg-color);
|
||||
}
|
||||
|
||||
.counter {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: nowrap;
|
||||
flex-shrink: 0;
|
||||
margin-left: 1rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
min-width: 1.325rem;
|
||||
text-align: center;
|
||||
font-weight: 500;
|
||||
font-size: 1rem;
|
||||
line-height: 1rem;
|
||||
color: var(--accent-color);
|
||||
background-color: var(--body-color);
|
||||
border: 1px solid var(--divider-color);
|
||||
border-radius: 1rem;
|
||||
}
|
||||
</style>
|
||||
@@ -1,82 +0,0 @@
|
||||
<!--
|
||||
// Copyright © 2022 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import type { Class, Doc, Ref, WithLookup } from '@hcengineering/core'
|
||||
import { BuildModelKey } from '@hcengineering/view'
|
||||
import {
|
||||
ActionContext,
|
||||
focusStore,
|
||||
ListSelectionProvider,
|
||||
SelectDirection,
|
||||
selectionStore,
|
||||
LoadingProps
|
||||
} from '@hcengineering/view-resources'
|
||||
import IssuesList from './IssuesList.svelte'
|
||||
import { Issue, IssueStatus, Team } from '@hcengineering/tracker'
|
||||
import { Employee } from '@hcengineering/contact'
|
||||
import { onMount } from 'svelte'
|
||||
import { IssuesGroupByKeys, IssuesOrderByKeys } from '../../utils'
|
||||
|
||||
export let _class: Ref<Class<Doc>>
|
||||
export let baseMenuClass: Ref<Class<Doc>> | undefined = undefined
|
||||
export let itemsConfig: (BuildModelKey | string)[]
|
||||
export let currentSpace: Ref<Team> | undefined = undefined
|
||||
export let groupByKey: IssuesGroupByKeys | undefined = undefined
|
||||
export let orderBy: IssuesOrderByKeys
|
||||
export let statuses: WithLookup<IssueStatus>[]
|
||||
export let employees: (WithLookup<Employee> | undefined)[] = []
|
||||
export let categories: any[] = []
|
||||
export let groupedIssues: { [key: string | number | symbol]: Issue[] } = {}
|
||||
export let loadingProps: LoadingProps | undefined = undefined
|
||||
|
||||
const listProvider = new ListSelectionProvider((offset: 1 | -1 | 0, of?: Doc, dir?: SelectDirection) => {})
|
||||
|
||||
let issuesList: IssuesList
|
||||
|
||||
$: if (issuesList !== undefined) listProvider.update(Object.values(groupedIssues).flat(1))
|
||||
|
||||
onMount(() => {
|
||||
;(document.activeElement as HTMLElement)?.blur()
|
||||
})
|
||||
</script>
|
||||
|
||||
<ActionContext
|
||||
context={{
|
||||
mode: 'browser'
|
||||
}}
|
||||
/>
|
||||
|
||||
<IssuesList
|
||||
bind:this={issuesList}
|
||||
{_class}
|
||||
{baseMenuClass}
|
||||
{currentSpace}
|
||||
{groupByKey}
|
||||
{orderBy}
|
||||
{statuses}
|
||||
{employees}
|
||||
{categories}
|
||||
{itemsConfig}
|
||||
{groupedIssues}
|
||||
{loadingProps}
|
||||
selectedObjectIds={$selectionStore ?? []}
|
||||
selectedRowIndex={listProvider.current($focusStore)}
|
||||
on:row-focus={(event) => {
|
||||
listProvider.updateFocus(event.detail ?? undefined)
|
||||
}}
|
||||
on:check={(event) => {
|
||||
listProvider.updateSelection(event.detail.docs, event.detail.value)
|
||||
}}
|
||||
/>
|
||||
@@ -1,23 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { Ref, Space } from '@hcengineering/core'
|
||||
import { DocumentQuery, WithLookup } from '@hcengineering/core'
|
||||
import { DocumentQuery, Ref, SortingOrder, Space, WithLookup } from '@hcengineering/core'
|
||||
import { IntlString, translate } from '@hcengineering/platform'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import { Issue } from '@hcengineering/tracker'
|
||||
import { Button, IconDetails, IconDetailsFilled, location } from '@hcengineering/ui'
|
||||
import { createQuery, getClient } from '@hcengineering/presentation'
|
||||
import { Issue, IssueStatus, Team } from '@hcengineering/tracker'
|
||||
import { Button, IconDetails, IconDetailsFilled } from '@hcengineering/ui'
|
||||
import view, { Viewlet } from '@hcengineering/view'
|
||||
import { FilterBar, ViewOptionModel, ViewOptionsButton, getActiveViewletId } from '@hcengineering/view-resources'
|
||||
import { FilterBar, getActiveViewletId, getViewOptions } from '@hcengineering/view-resources'
|
||||
import ViewletSettingButton from '@hcengineering/view-resources/src/components/ViewletSettingButton.svelte'
|
||||
import tracker from '../../plugin'
|
||||
import IssuesContent from './IssuesContent.svelte'
|
||||
import IssuesHeader from './IssuesHeader.svelte'
|
||||
import { getDefaultViewOptionsConfig } from '../../utils'
|
||||
import tracker from '../../plugin'
|
||||
import { onDestroy } from 'svelte'
|
||||
|
||||
export let space: Ref<Space> | undefined = undefined
|
||||
export let query: DocumentQuery<Issue> = {}
|
||||
export let title: IntlString | undefined = undefined
|
||||
export let label: string = ''
|
||||
export let viewOptionsConfig: ViewOptionModel[] = getDefaultViewOptionsConfig()
|
||||
|
||||
export let panelWidth: number = 0
|
||||
|
||||
@@ -39,7 +36,7 @@
|
||||
async function update (): Promise<void> {
|
||||
viewlets = await client.findAll(
|
||||
view.class.Viewlet,
|
||||
{ attachTo: tracker.class.Issue },
|
||||
{ attachTo: tracker.class.Issue, variant: { $ne: 'subissue' } },
|
||||
{
|
||||
lookup: {
|
||||
descriptor: view.class.ViewletDescriptor
|
||||
@@ -67,11 +64,40 @@
|
||||
$: if (docWidth <= 900 && !docSize) docSize = true
|
||||
$: if (docWidth > 900 && docSize) docSize = false
|
||||
|
||||
onDestroy(
|
||||
location.subscribe(() => {
|
||||
viewOptionsConfig = viewOptionsConfig
|
||||
})
|
||||
const teamQuery = createQuery()
|
||||
|
||||
let _teams: Map<Ref<Team>, Team> | undefined = undefined
|
||||
let _result: any
|
||||
$: teamQuery.query(tracker.class.Team, {}, (result) => {
|
||||
_result = JSON.stringify(result, undefined, 2)
|
||||
console.log('#RESULT 124', _result)
|
||||
const t = new Map<Ref<Team>, Team>()
|
||||
for (const r of result) {
|
||||
t.set(r._id, r)
|
||||
}
|
||||
_teams = t
|
||||
})
|
||||
|
||||
let issueStatuses: Map<Ref<Team>, WithLookup<IssueStatus>[]>
|
||||
|
||||
const statusesQuery = createQuery()
|
||||
statusesQuery.query(
|
||||
tracker.class.IssueStatus,
|
||||
{},
|
||||
(statuses) => {
|
||||
const st = new Map<Ref<Team>, WithLookup<IssueStatus>[]>()
|
||||
for (const s of statuses) {
|
||||
const id = s.attachedTo as Ref<Team>
|
||||
st.set(id, [...(st.get(id) ?? []), s])
|
||||
}
|
||||
issueStatuses = st
|
||||
},
|
||||
{
|
||||
lookup: { category: tracker.class.IssueStatusCategory },
|
||||
sort: { rank: SortingOrder.Ascending }
|
||||
}
|
||||
)
|
||||
$: viewOptions = getViewOptions(viewlet)
|
||||
</script>
|
||||
|
||||
<IssuesHeader {viewlets} {label} {space} bind:viewlet bind:search showLabelSelector={$$slots.label_selector}>
|
||||
@@ -80,7 +106,7 @@
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="extra">
|
||||
{#if viewlet}
|
||||
<ViewOptionsButton viewOptionsKey={viewlet._id} config={viewOptionsConfig} />
|
||||
<ViewletSettingButton bind:viewOptions {viewlet} />
|
||||
{/if}
|
||||
{#if asideFloat && $$slots.aside}
|
||||
<div class="buttons-divider" />
|
||||
@@ -99,8 +125,8 @@
|
||||
<slot name="afterHeader" />
|
||||
<FilterBar _class={tracker.class.Issue} query={searchQuery} on:change={(e) => (resultQuery = e.detail)} />
|
||||
<div class="flex w-full h-full clear-mins">
|
||||
{#if viewlet}
|
||||
<IssuesContent {viewlet} query={resultQuery} />
|
||||
{#if viewlet && _teams && issueStatuses}
|
||||
<IssuesContent {viewlet} query={resultQuery} {space} teams={_teams} {issueStatuses} {viewOptions} />
|
||||
{/if}
|
||||
{#if $$slots.aside !== undefined && asideShown}
|
||||
<div class="popupPanel-body__aside flex" class:float={asideFloat} class:shown={asideShown}>
|
||||
|
||||
@@ -17,7 +17,8 @@
|
||||
import { Class, Doc, DocumentQuery, Lookup, Ref, SortingOrder, WithLookup } from '@hcengineering/core'
|
||||
import { Kanban, TypeState } from '@hcengineering/kanban'
|
||||
import notification from '@hcengineering/notification'
|
||||
import { createQuery } from '@hcengineering/presentation'
|
||||
import { getResource } from '@hcengineering/platform'
|
||||
import { createQuery, getClient } from '@hcengineering/presentation'
|
||||
import tags from '@hcengineering/tags'
|
||||
import { Issue, IssuesGrouping, IssuesOrdering, IssueStatus, Team } from '@hcengineering/tracker'
|
||||
import {
|
||||
@@ -30,18 +31,19 @@
|
||||
showPopup,
|
||||
tooltip
|
||||
} from '@hcengineering/ui'
|
||||
import { focusStore, ListSelectionProvider, SelectDirection, selectionStore } from '@hcengineering/view-resources'
|
||||
import { ViewOptionModel, ViewOptions, ViewQueryOption } from '@hcengineering/view'
|
||||
import {
|
||||
focusStore,
|
||||
ListSelectionProvider,
|
||||
noCategory,
|
||||
SelectDirection,
|
||||
selectionStore
|
||||
} from '@hcengineering/view-resources'
|
||||
import ActionContext from '@hcengineering/view-resources/src/components/ActionContext.svelte'
|
||||
import Menu from '@hcengineering/view-resources/src/components/Menu.svelte'
|
||||
import { onMount } from 'svelte'
|
||||
import tracker from '../../plugin'
|
||||
import {
|
||||
getIssueStatusStates,
|
||||
getKanbanStatuses,
|
||||
getPriorityStates,
|
||||
issuesGroupBySorting,
|
||||
issuesSortOrderMap
|
||||
} from '../../utils'
|
||||
import { getIssueStatusStates, getKanbanStatuses, getPriorityStates, issuesGroupBySorting } from '../../utils'
|
||||
import CreateIssue from '../CreateIssue.svelte'
|
||||
import ProjectEditor from '../projects/ProjectEditor.svelte'
|
||||
import AssigneePresenter from './AssigneePresenter.svelte'
|
||||
@@ -53,24 +55,17 @@
|
||||
import StatusEditor from './StatusEditor.svelte'
|
||||
import EstimationEditor from './timereport/EstimationEditor.svelte'
|
||||
|
||||
export let currentSpace: Ref<Team> = tracker.team.DefaultTeam
|
||||
export let space: Ref<Team> | undefined = undefined
|
||||
export let baseMenuClass: Ref<Class<Doc>> | undefined = undefined
|
||||
export let query: DocumentQuery<Issue> = {}
|
||||
export let viewOptions: {
|
||||
groupBy: IssuesGrouping
|
||||
orderBy: IssuesOrdering
|
||||
shouldShowEmptyGroups: boolean
|
||||
shouldShowSubIssues: boolean
|
||||
}
|
||||
export let viewOptionsConfig: ViewOptionModel[] | undefined
|
||||
export let viewOptions: ViewOptions
|
||||
|
||||
$: currentSpace = typeof query.space === 'string' ? query.space : tracker.team.DefaultTeam
|
||||
$: ({ groupBy, orderBy, shouldShowEmptyGroups, shouldShowSubIssues } = viewOptions)
|
||||
$: sort = { [orderBy]: issuesSortOrderMap[orderBy] }
|
||||
$: rankFieldName = orderBy === IssuesOrdering.Manual ? orderBy : undefined
|
||||
$: resultQuery = {
|
||||
...(shouldShowSubIssues ? {} : { attachedTo: tracker.ids.NoParent }),
|
||||
...query
|
||||
} as any
|
||||
$: currentSpace = space || tracker.team.DefaultTeam
|
||||
$: groupBy = (viewOptions.groupBy ?? noCategory) as IssuesGrouping
|
||||
$: orderBy = viewOptions.orderBy
|
||||
$: sort = { [orderBy[0]]: orderBy[1] }
|
||||
$: dontUpdateRank = orderBy[0] !== IssuesOrdering.Manual
|
||||
|
||||
const spaceQuery = createQuery()
|
||||
const statusesQuery = createQuery()
|
||||
@@ -80,6 +75,28 @@
|
||||
currentTeam = res.shift()
|
||||
})
|
||||
|
||||
let resultQuery: DocumentQuery<any> = query
|
||||
$: getResultQuery(query, viewOptionsConfig, viewOptions).then((p) => (resultQuery = p))
|
||||
|
||||
const client = getClient()
|
||||
const hierarchy = client.getHierarchy()
|
||||
|
||||
async function getResultQuery (
|
||||
query: DocumentQuery<Issue>,
|
||||
viewOptions: ViewOptionModel[] | undefined,
|
||||
viewOptionsStore: ViewOptions
|
||||
): Promise<DocumentQuery<Issue>> {
|
||||
if (viewOptions === undefined) return query
|
||||
let result = hierarchy.clone(query)
|
||||
for (const viewOption of viewOptions) {
|
||||
if (viewOption.actionTartget !== 'query') continue
|
||||
const queryOption = viewOption as ViewQueryOption
|
||||
const f = await getResource(queryOption.action)
|
||||
result = f(viewOptionsStore[queryOption.key] ?? queryOption.defaultValue, query)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
let issueStatuses: WithLookup<IssueStatus>[] | undefined
|
||||
$: issueStatusStates = getIssueStatusStates(issueStatuses)
|
||||
$: statusesQuery.query(
|
||||
@@ -122,6 +139,12 @@
|
||||
}
|
||||
const issuesQuery = createQuery()
|
||||
let issueStates: TypeState[] = []
|
||||
const lookupIssue: Lookup<Issue> = {
|
||||
status: [tracker.class.IssueStatus, { category: tracker.class.IssueStatusCategory }],
|
||||
project: tracker.class.Project,
|
||||
sprint: tracker.class.Sprint,
|
||||
assignee: contact.class.Employee
|
||||
}
|
||||
$: issuesQuery.query(
|
||||
tracker.class.Issue,
|
||||
resultQuery,
|
||||
@@ -129,12 +152,7 @@
|
||||
issueStates = await getKanbanStatuses(groupBy, result)
|
||||
},
|
||||
{
|
||||
lookup: {
|
||||
status: [tracker.class.IssueStatus, { category: tracker.class.IssueStatusCategory }],
|
||||
project: tracker.class.Project,
|
||||
sprint: tracker.class.Sprint,
|
||||
assignee: contact.class.Employee
|
||||
},
|
||||
lookup: lookupIssue,
|
||||
sort: issuesGroupBySorting[groupBy]
|
||||
}
|
||||
)
|
||||
@@ -145,17 +163,16 @@
|
||||
})
|
||||
function getIssueStates (
|
||||
groupBy: IssuesGrouping,
|
||||
showEmptyGroups: boolean,
|
||||
states: TypeState[],
|
||||
statusStates: TypeState[],
|
||||
priorityStates: TypeState[]
|
||||
) {
|
||||
if (!showEmptyGroups && states.length > 0) return states
|
||||
if (states.length > 0) return states
|
||||
if (groupBy === IssuesGrouping.Status) return statusStates
|
||||
if (groupBy === IssuesGrouping.Priority) return priorityStates
|
||||
return []
|
||||
}
|
||||
$: states = getIssueStates(groupBy, shouldShowEmptyGroups, issueStates, issueStatusStates, priorityStates)
|
||||
$: states = getIssueStates(groupBy, issueStates, issueStatusStates, priorityStates)
|
||||
|
||||
const fullFilled: { [key: string]: boolean } = {}
|
||||
const getState = (state: any): WithLookup<IssueStatus> | undefined => {
|
||||
@@ -171,15 +188,15 @@
|
||||
mode: 'browser'
|
||||
}}
|
||||
/>
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<Kanban
|
||||
bind:this={kanbanUI}
|
||||
_class={tracker.class.Issue}
|
||||
search=""
|
||||
{states}
|
||||
{dontUpdateRank}
|
||||
options={{ sort, lookup }}
|
||||
query={resultQuery}
|
||||
fieldName={groupBy}
|
||||
{rankFieldName}
|
||||
on:content={(evt) => {
|
||||
listProvider.update(evt.detail)
|
||||
}}
|
||||
@@ -202,18 +219,16 @@
|
||||
<span class="lines-limit-2 ml-2">{state.title}</span>
|
||||
<span class="counter ml-2 text-md">{count}</span>
|
||||
</div>
|
||||
{#if groupBy === IssuesGrouping.Status}
|
||||
<div class="flex gap-1">
|
||||
<Button
|
||||
icon={IconAdd}
|
||||
kind={'transparent'}
|
||||
showTooltip={{ label: tracker.string.AddIssueTooltip, direction: 'left' }}
|
||||
on:click={() => {
|
||||
showPopup(CreateIssue, { space: currentSpace, status: state._id }, 'top')
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="flex gap-1">
|
||||
<Button
|
||||
icon={IconAdd}
|
||||
kind={'transparent'}
|
||||
showTooltip={{ label: tracker.string.AddIssueTooltip, direction: 'left' }}
|
||||
on:click={() => {
|
||||
showPopup(CreateIssue, { space: currentSpace, [groupBy]: state._id }, 'top')
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
@@ -244,7 +259,7 @@
|
||||
<AssigneePresenter
|
||||
value={issue.$lookup?.assignee}
|
||||
defaultClass={contact.class.Employee}
|
||||
issueId={issue._id}
|
||||
object={issue}
|
||||
isEditable={true}
|
||||
/>
|
||||
<div class="flex-center mt-2">
|
||||
@@ -252,8 +267,8 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="buttons-group xsmall-gap states-bar">
|
||||
{#if issue && issueStatuses && issue.subIssues > 0}
|
||||
<SubIssuesSelector value={issue} {currentTeam} statuses={issueStatuses} />
|
||||
{#if issue && issue.subIssues > 0}
|
||||
<SubIssuesSelector value={issue} {currentTeam} />
|
||||
{/if}
|
||||
<PriorityEditor value={issue} isEditable={true} kind={'link-bordered'} size={'inline'} justify={'center'} />
|
||||
<ProjectEditor
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
<script lang="ts">
|
||||
import contact, { Employee } from '@hcengineering/contact'
|
||||
import { Class, Doc, DocumentQuery, Ref, SortingOrder, WithLookup } from '@hcengineering/core'
|
||||
import { createQuery } from '@hcengineering/presentation'
|
||||
import { Issue, IssueStatus, ViewOptions } from '@hcengineering/tracker'
|
||||
import { issueSP, Scroller } from '@hcengineering/ui'
|
||||
import { BuildModelKey } from '@hcengineering/view'
|
||||
import tracker from '../../plugin'
|
||||
import {
|
||||
getCategories,
|
||||
groupBy as groupByFunc,
|
||||
issuesGroupKeyMap,
|
||||
issuesOrderKeyMap,
|
||||
issuesSortOrderMap
|
||||
} from '../../utils'
|
||||
import IssuesListBrowser from './IssuesListBrowser.svelte'
|
||||
|
||||
export let _class: Ref<Class<Doc>>
|
||||
export let config: (string | BuildModelKey)[]
|
||||
export let query: DocumentQuery<Issue> = {}
|
||||
export let viewOptions: ViewOptions
|
||||
|
||||
$: currentSpace = typeof query.space === 'string' ? query.space : tracker.team.DefaultTeam
|
||||
$: ({ groupBy, orderBy, shouldShowEmptyGroups, shouldShowSubIssues } = viewOptions)
|
||||
$: groupByKey = issuesGroupKeyMap[groupBy]
|
||||
$: orderByKey = issuesOrderKeyMap[orderBy]
|
||||
$: subIssuesQuery = shouldShowSubIssues ? {} : { attachedTo: tracker.ids.NoParent }
|
||||
|
||||
const statusesQuery = createQuery()
|
||||
let statuses: IssueStatus[] = []
|
||||
$: statusesQuery.query(
|
||||
tracker.class.IssueStatus,
|
||||
{ attachedTo: currentSpace },
|
||||
(result) => {
|
||||
statuses = [...result]
|
||||
},
|
||||
{
|
||||
lookup: { category: tracker.class.IssueStatusCategory },
|
||||
sort: { rank: SortingOrder.Ascending }
|
||||
}
|
||||
)
|
||||
$: groupedIssues = groupByFunc(issues, groupBy)
|
||||
$: categories = getCategories(groupByKey, issues, !!shouldShowEmptyGroups, statuses, employees)
|
||||
$: employees = issues.map((x) => x.$lookup?.assignee).filter(Boolean) as Employee[]
|
||||
|
||||
const issuesQuery = createQuery()
|
||||
let issues: WithLookup<Issue>[] = []
|
||||
$: issuesQuery.query(
|
||||
tracker.class.Issue,
|
||||
{ ...subIssuesQuery, ...query },
|
||||
(result) => {
|
||||
issues = result
|
||||
},
|
||||
{
|
||||
sort: { [orderByKey]: issuesSortOrderMap[orderByKey] },
|
||||
lookup: {
|
||||
assignee: contact.class.Employee,
|
||||
status: tracker.class.IssueStatus,
|
||||
space: tracker.class.Team,
|
||||
sprint: tracker.class.Sprint,
|
||||
_id: {
|
||||
subIssues: tracker.class.Issue
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
<div class="w-full h-full clear-mins">
|
||||
<Scroller fade={issueSP}>
|
||||
<IssuesListBrowser
|
||||
{_class}
|
||||
{currentSpace}
|
||||
{groupByKey}
|
||||
orderBy={orderByKey}
|
||||
{statuses}
|
||||
{employees}
|
||||
{categories}
|
||||
itemsConfig={config}
|
||||
{groupedIssues}
|
||||
/>
|
||||
</Scroller>
|
||||
</div>
|
||||
@@ -38,6 +38,7 @@
|
||||
<Spinner size="small" />
|
||||
{/if}
|
||||
<span class="overflow-label issue-title">{issue.title}</span>
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div
|
||||
class="button-close"
|
||||
use:tooltip={{ label: tracker.string.RemoveParent, direction: 'bottom' }}
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
<div class="root" style:max-width={maxWidth}>
|
||||
<span class="names">
|
||||
{#each value.parents as parentInfo}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<span class="name cursor-pointer" on:click={() => handleIssueEditorOpened(parentInfo)}
|
||||
>{parentInfo.parentTitle}</span
|
||||
>
|
||||
|
||||
@@ -73,6 +73,7 @@
|
||||
|
||||
{#if value}
|
||||
{#if kind === 'list' || kind === 'list-header'}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div class="priority-container" on:click={handlePriorityEditorOpened}>
|
||||
<div class="icon">
|
||||
{#if issuePriorities[value.priority]?.icon}<Icon icon={issuePriorities[value.priority]?.icon} {size} />{/if}
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
<!--
|
||||
// Copyright © 2023 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { IssuePriority } from '@hcengineering/tracker'
|
||||
import { Button, ButtonKind, ButtonSize, Icon, Label } from '@hcengineering/ui'
|
||||
import { issuePriorities } from '../../utils'
|
||||
|
||||
export let value: IssuePriority
|
||||
|
||||
export let kind: ButtonKind = 'link'
|
||||
export let size: ButtonSize = 'large'
|
||||
export let justify: 'left' | 'center' = 'left'
|
||||
export let width: string | undefined = undefined
|
||||
</script>
|
||||
|
||||
{#if kind === 'list' || kind === 'list-header'}
|
||||
<div class="priority-container">
|
||||
<div class="icon">
|
||||
{#if issuePriorities[value]?.icon}<Icon icon={issuePriorities[value]?.icon} {size} />{/if}
|
||||
</div>
|
||||
<span
|
||||
class="{kind === 'list' ? 'ml-2 text-md' : 'ml-3 text-base'} overflow-label disabled fs-bold content-accent-color"
|
||||
>
|
||||
<Label label={issuePriorities[value]?.label} />
|
||||
</span>
|
||||
</div>
|
||||
{:else}
|
||||
<Button
|
||||
label={issuePriorities[value]?.label}
|
||||
icon={issuePriorities[value]?.icon}
|
||||
{justify}
|
||||
{width}
|
||||
{size}
|
||||
{kind}
|
||||
disabled
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
.priority-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
min-width: 0;
|
||||
cursor: pointer;
|
||||
|
||||
.icon {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
color: var(--content-color);
|
||||
}
|
||||
&:hover {
|
||||
.icon {
|
||||
color: var(--caption-color) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -15,7 +15,7 @@
|
||||
<script lang="ts">
|
||||
import { AttachedData, Ref, SortingOrder, WithLookup } from '@hcengineering/core'
|
||||
import { createQuery, getClient } from '@hcengineering/presentation'
|
||||
import { Issue, IssueStatus } from '@hcengineering/tracker'
|
||||
import { Issue, IssueStatus, Team } from '@hcengineering/tracker'
|
||||
import type { ButtonKind, ButtonSize } from '@hcengineering/ui'
|
||||
import { Button, eventToHTMLElement, SelectPopup, showPopup, TooltipAlignment } from '@hcengineering/ui'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
@@ -34,6 +34,9 @@
|
||||
export let justify: 'left' | 'center' = 'left'
|
||||
export let width: string | undefined = undefined
|
||||
|
||||
// Extra properties
|
||||
export let issueStatuses: Map<Ref<Team>, WithLookup<IssueStatus>[]> | undefined = undefined
|
||||
|
||||
const client = getClient()
|
||||
const statusesQuery = createQuery()
|
||||
const dispatch = createEventDispatcher()
|
||||
@@ -65,7 +68,7 @@
|
||||
|
||||
$: selectedStatus = statuses?.find((status) => status._id === value.status) ?? statuses?.[0]
|
||||
$: selectedStatusLabel = shouldShowLabel ? selectedStatus?.name : undefined
|
||||
$: statusesInfo = statuses?.map((s, i) => {
|
||||
$: statusesInfo = statuses?.map((s) => {
|
||||
return {
|
||||
id: s._id,
|
||||
component: StatusPresenter,
|
||||
@@ -74,18 +77,23 @@
|
||||
}
|
||||
})
|
||||
$: if (!statuses) {
|
||||
const query = '_id' in value ? { attachedTo: value.space } : {}
|
||||
statusesQuery.query(
|
||||
tracker.class.IssueStatus,
|
||||
query,
|
||||
(result) => {
|
||||
statuses = result
|
||||
},
|
||||
{
|
||||
lookup: { category: tracker.class.IssueStatusCategory },
|
||||
sort: { rank: SortingOrder.Ascending }
|
||||
}
|
||||
)
|
||||
statuses = '_id' in value ? issueStatuses?.get(value.space) : undefined
|
||||
if (statuses === undefined) {
|
||||
const query = '_id' in value ? { attachedTo: value.space } : {}
|
||||
statusesQuery.query(
|
||||
tracker.class.IssueStatus,
|
||||
query,
|
||||
(result) => {
|
||||
statuses = result
|
||||
},
|
||||
{
|
||||
lookup: { category: tracker.class.IssueStatusCategory },
|
||||
sort: { rank: SortingOrder.Ascending }
|
||||
}
|
||||
)
|
||||
} else {
|
||||
statusesQuery.unsubscribe()
|
||||
}
|
||||
}
|
||||
$: smallgap = size === 'inline' || size === 'small'
|
||||
</script>
|
||||
@@ -95,7 +103,11 @@
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div class="flex-row-center flex-no-shrink" class:cursor-pointer={isEditable} on:click={handleStatusEditorOpened}>
|
||||
<div class="flex-center flex-no-shrink square-4">
|
||||
{#if selectedStatus}<IssueStatusIcon value={selectedStatus} size={kind === 'list' ? 'inline' : 'medium'} />{/if}
|
||||
{#if selectedStatus}<IssueStatusIcon
|
||||
value={selectedStatus}
|
||||
issueStatuses={statuses}
|
||||
size={kind === 'list' ? 'inline' : 'medium'}
|
||||
/>{/if}
|
||||
</div>
|
||||
{#if selectedStatusLabel}
|
||||
<span
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
<!--
|
||||
// Copyright © 2023 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Ref, WithLookup } from '@hcengineering/core'
|
||||
import { createQuery } from '@hcengineering/presentation'
|
||||
import tracker, { IssueStatus } from '@hcengineering/tracker'
|
||||
import StatusPresenter from './StatusPresenter.svelte'
|
||||
|
||||
export let value: Ref<IssueStatus> | undefined
|
||||
export let size: 'small' | 'medium' = 'medium'
|
||||
|
||||
let status: WithLookup<IssueStatus> | undefined
|
||||
|
||||
const query = createQuery()
|
||||
$: query.query(tracker.class.IssueStatus, { _id: value }, (res) => ([status] = res), {
|
||||
lookup: { category: tracker.class.IssueStatusCategory }
|
||||
})
|
||||
</script>
|
||||
|
||||
<StatusPresenter value={status} {size} />
|
||||
@@ -14,12 +14,13 @@
|
||||
-->
|
||||
<script lang="ts">
|
||||
import type { Issue } from '@hcengineering/tracker'
|
||||
import ParentNamesPresenter from './ParentNamesPresenter.svelte'
|
||||
import tracker from '../../plugin'
|
||||
import { showPanel } from '@hcengineering/ui'
|
||||
import tracker from '../../plugin'
|
||||
import ParentNamesPresenter from './ParentNamesPresenter.svelte'
|
||||
|
||||
export let value: Issue
|
||||
export let shouldUseMargin: boolean = false
|
||||
export let showParent = true
|
||||
|
||||
function handleIssueEditorOpened () {
|
||||
showPanel(tracker.component.EditIssue, value._id, value._class, 'content')
|
||||
@@ -28,12 +29,15 @@
|
||||
|
||||
{#if value}
|
||||
<span class="titlePresenter-container" class:with-margin={shouldUseMargin} title={value.title}>
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<span
|
||||
class="name overflow-label cursor-pointer"
|
||||
style={`max-width: ${value.parents.length !== 0 ? 95 : 100}%`}
|
||||
style:max-width={showParent ? `${value.parents.length !== 0 ? 95 : 100}%` : '100%'}
|
||||
on:click={handleIssueEditorOpened}>{value.title}</span
|
||||
>
|
||||
<ParentNamesPresenter {value} />
|
||||
{#if showParent}
|
||||
<ParentNamesPresenter {value} />
|
||||
{/if}
|
||||
</span>
|
||||
{/if}
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
import { getResource } from '@hcengineering/platform'
|
||||
import presentation, { createQuery, getClient, MessageViewer } from '@hcengineering/presentation'
|
||||
import setting, { settingId } from '@hcengineering/setting'
|
||||
import type { Issue, IssuesGrouping, IssuesOrdering, IssueStatus, Team } from '@hcengineering/tracker'
|
||||
import type { Issue, IssueStatus, Team } from '@hcengineering/tracker'
|
||||
import {
|
||||
Button,
|
||||
EditBox,
|
||||
@@ -33,14 +33,7 @@
|
||||
showPopup,
|
||||
Spinner
|
||||
} from '@hcengineering/ui'
|
||||
import {
|
||||
ContextMenu,
|
||||
focusStore,
|
||||
ListSelectionProvider,
|
||||
SelectDirection,
|
||||
UpDownNavigator,
|
||||
viewOptionsStore
|
||||
} from '@hcengineering/view-resources'
|
||||
import { ContextMenu, UpDownNavigator } from '@hcengineering/view-resources'
|
||||
import { createEventDispatcher, onDestroy, onMount } from 'svelte'
|
||||
import { generateIssueShortLink, getIssueId } from '../../../issues'
|
||||
import tracker from '../../../plugin'
|
||||
@@ -49,8 +42,6 @@
|
||||
import CopyToClipboard from './CopyToClipboard.svelte'
|
||||
import SubIssues from './SubIssues.svelte'
|
||||
import SubIssueSelector from './SubIssueSelector.svelte'
|
||||
import { groupBy as groupByFunc, issuesOrderKeyMap, issuesSortOrderMap } from '../../../utils'
|
||||
import contact from '@hcengineering/contact'
|
||||
|
||||
export let _id: Ref<Issue>
|
||||
export let _class: Ref<Class<Issue>>
|
||||
@@ -71,14 +62,6 @@
|
||||
let isEditing = false
|
||||
let descriptionBox: AttachmentStyledBox
|
||||
|
||||
let groupBy: IssuesGrouping
|
||||
let orderBy: IssuesOrdering
|
||||
let shouldShowSubIssues: boolean
|
||||
$: ({ groupBy, orderBy, shouldShowSubIssues } = $viewOptionsStore)
|
||||
$: orderByKey = issuesOrderKeyMap[orderBy]
|
||||
$: subIssuesQuery = shouldShowSubIssues ? {} : { attachedTo: tracker.ids.NoParent }
|
||||
$: query = { space: issue?.space }
|
||||
|
||||
const notificationClient = getResource(notification.function.GetNotificationClient).then((res) => res())
|
||||
|
||||
$: read(_id)
|
||||
@@ -126,72 +109,6 @@
|
||||
$: isDescriptionEmpty = !new DOMParser().parseFromString(description, 'text/html').documentElement.innerText?.trim()
|
||||
$: parentIssue = issue?.$lookup?.attachedTo
|
||||
|
||||
let issues: WithLookup<Issue>[] = []
|
||||
let neighbourIssues: Issue[] = []
|
||||
const issuesQuery = createQuery()
|
||||
const subIssuesQueryClient = createQuery()
|
||||
|
||||
$: if (parentIssue) {
|
||||
subIssuesQueryClient.query(
|
||||
tracker.class.Issue,
|
||||
{ attachedTo: parentIssue?._id },
|
||||
async (result) => (neighbourIssues = result ?? []),
|
||||
{
|
||||
sort: { rank: SortingOrder.Descending }
|
||||
}
|
||||
)
|
||||
} else {
|
||||
issuesQuery.query(
|
||||
tracker.class.Issue,
|
||||
{ ...subIssuesQuery, ...query },
|
||||
(result) => {
|
||||
issues = result
|
||||
},
|
||||
{
|
||||
sort: { [orderByKey]: issuesSortOrderMap[orderByKey] },
|
||||
lookup: {
|
||||
assignee: contact.class.Employee,
|
||||
status: tracker.class.IssueStatus,
|
||||
space: tracker.class.Team,
|
||||
sprint: tracker.class.Sprint,
|
||||
_id: {
|
||||
subIssues: tracker.class.Issue
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
$: groupedIssues = groupByFunc(issues, groupBy)
|
||||
$: flatGroupedIssues = Object.values(groupedIssues ?? {}).flat(1)
|
||||
$: issuesToNavigate = parentIssue ? neighbourIssues : flatGroupedIssues
|
||||
|
||||
const listProvider = new ListSelectionProvider((offset: 1 | -1 | 0, of?: Doc, dir?: SelectDirection) => {
|
||||
if (dir === 'vertical') {
|
||||
if (groupedIssues) {
|
||||
const selectedRowIndex = listProvider.current($focusStore)
|
||||
let position =
|
||||
(of !== undefined ? issuesToNavigate.findIndex((x) => x._id === of?._id) : selectedRowIndex) ?? -1
|
||||
|
||||
position -= offset
|
||||
|
||||
if (position < 0) {
|
||||
position = 0
|
||||
}
|
||||
|
||||
if (position >= issuesToNavigate.length) {
|
||||
position = issuesToNavigate.length - 1
|
||||
}
|
||||
|
||||
listProvider.updateFocus(issuesToNavigate[position])
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
$: if (issue) listProvider.updateFocus(issue)
|
||||
|
||||
$: listProvider.update(issuesToNavigate)
|
||||
|
||||
function edit (ev: MouseEvent) {
|
||||
ev.preventDefault()
|
||||
|
||||
@@ -324,6 +241,7 @@
|
||||
<span class="title select-text">{title}</span>
|
||||
<div class="mt-6 description-preview select-text">
|
||||
{#if isDescriptionEmpty}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div class="placeholder" on:click={edit}>
|
||||
<Label label={tracker.string.IssueDescriptionPlaceholder} />
|
||||
</div>
|
||||
|
||||
@@ -13,63 +13,20 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Ref, WithLookup } from '@hcengineering/core'
|
||||
import { DocumentQuery, Ref, WithLookup } from '@hcengineering/core'
|
||||
import { Issue, IssueStatus, Team } from '@hcengineering/tracker'
|
||||
import { getEventPositionElement, showPanel, showPopup } from '@hcengineering/ui'
|
||||
import { ActionContext, ContextMenu, FixedColumn } from '@hcengineering/view-resources'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { flip } from 'svelte/animate'
|
||||
import { getIssueId } from '../../../issues'
|
||||
import { Viewlet, ViewOptions } from '@hcengineering/view'
|
||||
import { ActionContext, List } from '@hcengineering/view-resources'
|
||||
import tracker from '../../../plugin'
|
||||
import Circles from '../../icons/Circles.svelte'
|
||||
import AssigneeEditor from '../AssigneeEditor.svelte'
|
||||
import DueDateEditor from '../DueDateEditor.svelte'
|
||||
import PriorityEditor from '../PriorityEditor.svelte'
|
||||
import StatusEditor from '../StatusEditor.svelte'
|
||||
import EstimationEditor from '../timereport/EstimationEditor.svelte'
|
||||
import SubIssuesSelector from './SubIssuesSelector.svelte'
|
||||
|
||||
export let issues: Issue[]
|
||||
export let query: DocumentQuery<Issue> | undefined = undefined
|
||||
export let issues: Issue[] | undefined = undefined
|
||||
export let viewlet: Viewlet
|
||||
export let viewOptions: ViewOptions
|
||||
|
||||
export let teams: Map<Ref<Team>, Team>
|
||||
// Extra properties
|
||||
export let teams: Map<Ref<Team>, Team> | undefined
|
||||
export let issueStatuses: Map<Ref<Team>, WithLookup<IssueStatus>[]>
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
let draggingIndex: number | null = null
|
||||
let hoveringIndex: number | null = null
|
||||
|
||||
function openIssue (target: Issue) {
|
||||
dispatch('issue-focus', target)
|
||||
showPanel(tracker.component.EditIssue, target._id, target._class, 'content')
|
||||
}
|
||||
|
||||
function resetDrag () {
|
||||
draggingIndex = null
|
||||
hoveringIndex = null
|
||||
}
|
||||
|
||||
function handleDragStart (ev: DragEvent, index: number) {
|
||||
if (ev.dataTransfer) {
|
||||
ev.dataTransfer.effectAllowed = 'move'
|
||||
ev.dataTransfer.dropEffect = 'move'
|
||||
draggingIndex = index
|
||||
}
|
||||
}
|
||||
|
||||
function handleDrop (ev: DragEvent, toIndex: number) {
|
||||
if (ev.dataTransfer && draggingIndex !== null && toIndex !== draggingIndex) {
|
||||
ev.dataTransfer.dropEffect = 'move'
|
||||
|
||||
dispatch('move', { fromIndex: draggingIndex, toIndex })
|
||||
}
|
||||
|
||||
resetDrag()
|
||||
}
|
||||
|
||||
function showContextMenu (ev: MouseEvent, object: Issue) {
|
||||
showPopup(ContextMenu, { object }, getEventPositionElement(ev))
|
||||
}
|
||||
</script>
|
||||
|
||||
<ActionContext
|
||||
@@ -78,136 +35,15 @@
|
||||
}}
|
||||
/>
|
||||
|
||||
{#each issues as issue, index (issue._id)}
|
||||
{@const currentTeam = teams.get(issue.space)}
|
||||
{@const openIssueCall = () => openIssue(issue)}
|
||||
<div
|
||||
class="flex-between row"
|
||||
class:is-dragging={index === draggingIndex}
|
||||
class:is-dragged-over-up={draggingIndex !== null && index < draggingIndex && index === hoveringIndex}
|
||||
class:is-dragged-over-down={draggingIndex !== null && index > draggingIndex && index === hoveringIndex}
|
||||
animate:flip={{ duration: 400 }}
|
||||
draggable={true}
|
||||
on:click|self={openIssueCall}
|
||||
on:contextmenu|preventDefault={(ev) => showContextMenu(ev, issue)}
|
||||
on:dragstart={(ev) => handleDragStart(ev, index)}
|
||||
on:dragover|preventDefault={() => false}
|
||||
on:dragenter={() => (hoveringIndex = index)}
|
||||
on:drop|preventDefault={(ev) => handleDrop(ev, index)}
|
||||
on:dragend={resetDrag}
|
||||
>
|
||||
<div class="draggable-container">
|
||||
<div class="draggable-mark"><Circles /></div>
|
||||
</div>
|
||||
<div class="flex-row-center ml-6 clear-mins gap-2">
|
||||
<PriorityEditor value={issue} isEditable kind={'list'} size={'small'} justify={'center'} />
|
||||
<span class="issuePresenter" on:click={openIssueCall}>
|
||||
<FixedColumn key={'subissue_issue'} justify={'left'}>
|
||||
{#if currentTeam}
|
||||
{getIssueId(currentTeam, issue)}
|
||||
{/if}
|
||||
</FixedColumn>
|
||||
</span>
|
||||
<StatusEditor
|
||||
value={issue}
|
||||
statuses={issueStatuses.get(issue.space)}
|
||||
justify="center"
|
||||
kind={'list'}
|
||||
size={'small'}
|
||||
tooltipAlignment="bottom"
|
||||
/>
|
||||
<span class="text name" title={issue.title} on:click={openIssueCall}>
|
||||
{issue.title}
|
||||
</span>
|
||||
{#if issue.subIssues > 0}
|
||||
<SubIssuesSelector value={issue} {currentTeam} statuses={issueStatuses.get(issue.space)} />
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex-center flex-no-shrink">
|
||||
<EstimationEditor value={issue} kind={'list'} />
|
||||
{#if issue.dueDate !== null}
|
||||
<DueDateEditor value={issue} />
|
||||
{/if}
|
||||
<AssigneeEditor value={issue} />
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
<style lang="scss">
|
||||
.row {
|
||||
position: relative;
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
|
||||
.text {
|
||||
font-weight: 500;
|
||||
color: var(--caption-color);
|
||||
}
|
||||
|
||||
.issuePresenter {
|
||||
flex-shrink: 0;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
font-weight: 500;
|
||||
color: var(--content-color);
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: var(--caption-color);
|
||||
text-decoration: underline;
|
||||
}
|
||||
&:active {
|
||||
color: var(--accent-color);
|
||||
}
|
||||
}
|
||||
|
||||
.name {
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.draggable-container {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
width: 1.5rem;
|
||||
cursor: grabbing;
|
||||
|
||||
.draggable-mark {
|
||||
opacity: 0;
|
||||
width: 0.375rem;
|
||||
height: 1rem;
|
||||
margin-left: 0.75rem;
|
||||
transition: opacity 0.1s;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.draggable-mark {
|
||||
opacity: 0.4;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-dragging::before {
|
||||
position: absolute;
|
||||
content: '';
|
||||
background-color: var(--theme-bg-color);
|
||||
opacity: 0.4;
|
||||
inset: 0;
|
||||
}
|
||||
|
||||
&.is-dragged-over-up::before {
|
||||
position: absolute;
|
||||
content: '';
|
||||
inset: 0;
|
||||
border-top: 1px solid var(--theme-bg-check);
|
||||
}
|
||||
&.is-dragged-over-down::before {
|
||||
position: absolute;
|
||||
content: '';
|
||||
inset: 0;
|
||||
border-bottom: 1px solid var(--theme-bg-check);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
{#if viewlet}
|
||||
<List
|
||||
_class={tracker.class.Issue}
|
||||
{viewOptions}
|
||||
viewOptionsConfig={viewlet.viewOptions?.other}
|
||||
config={viewlet.config}
|
||||
documents={issues}
|
||||
{query}
|
||||
flatHeaders={true}
|
||||
props={{ teams, issueStatuses }}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
import tracker from '../../../plugin'
|
||||
import { getIssueId } from '../../../issues'
|
||||
import IssueStatusIcon from '../IssueStatusIcon.svelte'
|
||||
import { ListSelectionProvider } from '@hcengineering/view-resources'
|
||||
|
||||
export let issue: WithLookup<Issue>
|
||||
|
||||
@@ -48,6 +49,7 @@
|
||||
function openParentIssue () {
|
||||
if (parentIssue) {
|
||||
closeTooltip()
|
||||
ListSelectionProvider.Pop()
|
||||
openIssue(parentIssue._id)
|
||||
}
|
||||
}
|
||||
@@ -90,7 +92,7 @@
|
||||
|
||||
$: areSubIssuesLoading = !subIssues
|
||||
$: parentIssue = issue.$lookup?.attachedTo ? (issue.$lookup?.attachedTo as Issue) : null
|
||||
$: if (parentIssue) {
|
||||
$: if (parentIssue && parentIssue.subIssues > 0) {
|
||||
subIssuesQeury.query(
|
||||
tracker.class.Issue,
|
||||
{ space: issue.space, attachedTo: parentIssue._id },
|
||||
@@ -111,6 +113,7 @@
|
||||
{#if parentIssue}
|
||||
<div class="flex root">
|
||||
<div class="item clear-mins">
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div
|
||||
class="flex-center parent-issue cursor-pointer"
|
||||
use:tooltip={{ label: tracker.string.OpenParent, direction: 'bottom' }}
|
||||
@@ -134,11 +137,12 @@
|
||||
<Spinner size="small" />
|
||||
</div>
|
||||
{:else}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div
|
||||
bind:this={subIssuesElement}
|
||||
class="flex-center sub-issues cursor-pointer"
|
||||
use:tooltip={{ label: tracker.string.OpenSubIssues, direction: 'bottom' }}
|
||||
on:click|preventDefault={areSubIssuesLoading ? undefined : showSubIssues}
|
||||
on:click|preventDefault={showSubIssues}
|
||||
>
|
||||
<span class="overflow-label">{subIssues?.length}</span>
|
||||
<div class="ml-2">
|
||||
|
||||
@@ -14,12 +14,12 @@
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Ref, SortingOrder, WithLookup } from '@hcengineering/core'
|
||||
import { createQuery, getClient } from '@hcengineering/presentation'
|
||||
import { calcRank, Issue, IssueStatus, Team } from '@hcengineering/tracker'
|
||||
import { Button, Spinner, ExpandCollapse, closeTooltip, IconAdd } from '@hcengineering/ui'
|
||||
import { createQuery } from '@hcengineering/presentation'
|
||||
import { Issue, IssueStatus, Team } from '@hcengineering/tracker'
|
||||
import { Button, Chevron, closeTooltip, ExpandCollapse, IconAdd, Label } from '@hcengineering/ui'
|
||||
import view, { Viewlet } from '@hcengineering/view'
|
||||
import { getViewOptions, ViewletSettingButton } from '@hcengineering/view-resources'
|
||||
import tracker from '../../../plugin'
|
||||
import Collapsed from '../../icons/Collapsed.svelte'
|
||||
import Expanded from '../../icons/Expanded.svelte'
|
||||
import CreateSubIssue from './CreateSubIssue.svelte'
|
||||
import SubIssueList from './SubIssueList.svelte'
|
||||
|
||||
@@ -27,84 +27,109 @@
|
||||
export let teams: Map<Ref<Team>, Team>
|
||||
export let issueStatuses: Map<Ref<Team>, WithLookup<IssueStatus>[]>
|
||||
|
||||
const subIssuesQuery = createQuery()
|
||||
const client = getClient()
|
||||
|
||||
let subIssues: Issue[] | undefined
|
||||
let isCollapsed = false
|
||||
let isCreating = false
|
||||
|
||||
async function handleIssueSwap (ev: CustomEvent<{ fromIndex: number; toIndex: number }>) {
|
||||
if (subIssues) {
|
||||
const { fromIndex, toIndex } = ev.detail
|
||||
const [prev, next] = [
|
||||
subIssues[fromIndex < toIndex ? toIndex : toIndex - 1],
|
||||
subIssues[fromIndex < toIndex ? toIndex + 1 : toIndex]
|
||||
]
|
||||
const issue = subIssues[fromIndex]
|
||||
$: hasSubIssues = issue.subIssues > 0
|
||||
|
||||
await client.update(issue, { rank: calcRank(prev, next) })
|
||||
}
|
||||
let viewlet: Viewlet | undefined
|
||||
|
||||
const query = createQuery()
|
||||
$: query.query(view.class.Viewlet, { _id: tracker.viewlet.SubIssues }, (res) => {
|
||||
;[viewlet] = res
|
||||
})
|
||||
|
||||
let _teams = teams
|
||||
let _issueStatuses = issueStatuses
|
||||
|
||||
const teamsQuery = createQuery()
|
||||
|
||||
$: if (teams === undefined) {
|
||||
teamsQuery.query(tracker.class.Team, {}, async (result) => {
|
||||
_teams = new Map(result.map((it) => [it._id, it]))
|
||||
})
|
||||
} else {
|
||||
teamsQuery.unsubscribe()
|
||||
}
|
||||
|
||||
$: hasSubIssues = issue.subIssues > 0
|
||||
$: subIssuesQuery.query(tracker.class.Issue, { attachedTo: issue._id }, async (result) => (subIssues = result), {
|
||||
sort: { rank: SortingOrder.Ascending },
|
||||
lookup: {
|
||||
_id: {
|
||||
subIssues: tracker.class.Issue
|
||||
const statusesQuery = createQuery()
|
||||
$: if (issueStatuses === undefined) {
|
||||
statusesQuery.query(
|
||||
tracker.class.IssueStatus,
|
||||
{},
|
||||
(statuses) => {
|
||||
const st = new Map<Ref<Team>, WithLookup<IssueStatus>[]>()
|
||||
for (const s of statuses) {
|
||||
const id = s.attachedTo as Ref<Team>
|
||||
st.set(id, [...(st.get(id) ?? []), s])
|
||||
}
|
||||
_issueStatuses = st
|
||||
},
|
||||
{
|
||||
lookup: { category: tracker.class.IssueStatusCategory },
|
||||
sort: { rank: SortingOrder.Ascending }
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
} else {
|
||||
statusesQuery.unsubscribe()
|
||||
}
|
||||
|
||||
$: viewOptions = viewlet !== undefined ? getViewOptions(viewlet) : undefined
|
||||
</script>
|
||||
|
||||
<div class="flex-between">
|
||||
{#if hasSubIssues}
|
||||
<Button
|
||||
width="min-content"
|
||||
icon={isCollapsed ? Collapsed : Expanded}
|
||||
size="small"
|
||||
kind="transparent"
|
||||
label={tracker.string.SubIssuesList}
|
||||
labelParams={{ subIssues: issue.subIssues }}
|
||||
on:click={() => {
|
||||
isCollapsed = !isCollapsed
|
||||
isCreating = false
|
||||
}}
|
||||
/>
|
||||
>
|
||||
<svelte:fragment slot="content">
|
||||
<Chevron size={'small'} expanded={!isCollapsed} outline fill={'var(--caption-color)'} marginRight={'.375rem'} />
|
||||
<Label label={tracker.string.SubIssuesList} params={{ subIssues: issue.subIssues }} />
|
||||
</svelte:fragment>
|
||||
</Button>
|
||||
{/if}
|
||||
|
||||
<Button
|
||||
id="add-sub-issue"
|
||||
width="min-content"
|
||||
icon={hasSubIssues ? IconAdd : undefined}
|
||||
label={hasSubIssues ? undefined : tracker.string.AddSubIssues}
|
||||
labelParams={{ subIssues: 0 }}
|
||||
kind={'transparent'}
|
||||
size={'small'}
|
||||
showTooltip={{ label: tracker.string.AddSubIssues, props: { subIssues: 1 }, direction: 'bottom' }}
|
||||
on:click={() => {
|
||||
closeTooltip()
|
||||
isCreating = true
|
||||
isCollapsed = false
|
||||
}}
|
||||
/>
|
||||
<div class="flex-row-center">
|
||||
{#if viewlet && hasSubIssues && viewOptions}
|
||||
<ViewletSettingButton bind:viewOptions {viewlet} kind={'transparent'} />
|
||||
{/if}
|
||||
<Button
|
||||
id="add-sub-issue"
|
||||
width="min-content"
|
||||
icon={hasSubIssues ? IconAdd : undefined}
|
||||
label={hasSubIssues ? undefined : tracker.string.AddSubIssues}
|
||||
labelParams={{ subIssues: 0 }}
|
||||
kind={'transparent'}
|
||||
size={'small'}
|
||||
showTooltip={{ label: tracker.string.AddSubIssues, props: { subIssues: 1 }, direction: 'bottom' }}
|
||||
on:click={() => {
|
||||
closeTooltip()
|
||||
isCreating = true
|
||||
isCollapsed = false
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-1">
|
||||
{#if subIssues && issueStatuses}
|
||||
<ExpandCollapse isExpanded={!isCollapsed} duration={400}>
|
||||
{#if hasSubIssues}
|
||||
{#if issueStatuses}
|
||||
{#if hasSubIssues && viewOptions && viewlet}
|
||||
<ExpandCollapse isExpanded={!isCollapsed} duration={400}>
|
||||
<div class="list" class:collapsed={isCollapsed}>
|
||||
<SubIssueList
|
||||
issues={subIssues}
|
||||
{issueStatuses}
|
||||
{teams}
|
||||
on:issue-focus={() => (isCreating = false)}
|
||||
on:move={handleIssueSwap}
|
||||
teams={_teams}
|
||||
{viewlet}
|
||||
{viewOptions}
|
||||
issueStatuses={_issueStatuses}
|
||||
query={{ attachedTo: issue._id }}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</ExpandCollapse>
|
||||
</ExpandCollapse>
|
||||
{/if}
|
||||
<ExpandCollapse isExpanded={!isCollapsed} duration={400}>
|
||||
{#if isCreating}
|
||||
{@const team = teams.get(issue.space)}
|
||||
@@ -121,10 +146,6 @@
|
||||
{/if}
|
||||
{/if}
|
||||
</ExpandCollapse>
|
||||
{:else}
|
||||
<div class="flex-center pt-3">
|
||||
<Spinner />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -13,17 +13,26 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Doc, Ref, WithLookup } from '@hcengineering/core'
|
||||
import { Ref, SortingOrder, WithLookup } from '@hcengineering/core'
|
||||
import { createQuery } from '@hcengineering/presentation'
|
||||
import { Issue, IssueStatus, Team } from '@hcengineering/tracker'
|
||||
import { ButtonKind, ButtonSize, getPlatformColor } from '@hcengineering/ui'
|
||||
import { Button, closeTooltip, ProgressCircle, SelectPopup, showPanel, showPopup } from '@hcengineering/ui'
|
||||
import { updateFocus } from '@hcengineering/view-resources'
|
||||
import tracker from '../../../plugin'
|
||||
import {
|
||||
Button,
|
||||
ButtonKind,
|
||||
ButtonSize,
|
||||
closeTooltip,
|
||||
getPlatformColor,
|
||||
ProgressCircle,
|
||||
SelectPopup,
|
||||
showPanel,
|
||||
showPopup
|
||||
} from '@hcengineering/ui'
|
||||
import { getIssueId } from '../../../issues'
|
||||
import tracker from '../../../plugin'
|
||||
import { subIssueListProvider } from '../../../utils'
|
||||
|
||||
export let value: WithLookup<Issue>
|
||||
export let currentTeam: Team | undefined
|
||||
export let statuses: WithLookup<IssueStatus>[] | undefined
|
||||
|
||||
export let kind: ButtonKind = 'link-bordered'
|
||||
export let size: ButtonSize = 'inline'
|
||||
@@ -32,21 +41,35 @@
|
||||
|
||||
let btn: HTMLElement
|
||||
|
||||
let subIssues: Issue[] | undefined
|
||||
let doneStatus: Ref<Doc> | undefined
|
||||
let subIssues: Issue[] = []
|
||||
let countComplate: number = 0
|
||||
|
||||
const query = createQuery()
|
||||
const statusesQuery = createQuery()
|
||||
|
||||
let statuses: WithLookup<IssueStatus>[] = []
|
||||
|
||||
$: if (value.$lookup?.subIssues !== undefined) {
|
||||
query.unsubscribe()
|
||||
subIssues = value.$lookup.subIssues as Issue[]
|
||||
subIssues.sort((a, b) => (a.rank ?? '').localeCompare(b.rank ?? ''))
|
||||
} else {
|
||||
query.query(tracker.class.Issue, { attachedTo: value._id }, (res) => (subIssues = res), {
|
||||
sort: { rank: SortingOrder.Ascending }
|
||||
})
|
||||
}
|
||||
|
||||
statusesQuery.query(tracker.class.IssueStatus, {}, (res) => (statuses = res), {
|
||||
lookup: { category: tracker.class.IssueStatusCategory }
|
||||
})
|
||||
|
||||
$: if (statuses && subIssues) {
|
||||
doneStatus = statuses.find((s) => s.category === tracker.issueStatusCategory.Completed)?._id ?? undefined
|
||||
if (doneStatus) countComplate = subIssues.filter((si) => si.status === doneStatus).length
|
||||
const doneStatuses = statuses.filter((s) => s.category === tracker.issueStatusCategory.Completed).map((p) => p._id)
|
||||
countComplate = subIssues.filter((si) => doneStatuses.includes(si.status)).length
|
||||
}
|
||||
$: hasSubIssues = (subIssues?.length ?? 0) > 0
|
||||
|
||||
function getIssueStatusIcon (issue: Issue) {
|
||||
function getIssueStatusIcon (issue: Issue, statuses: WithLookup<IssueStatus>[] | undefined) {
|
||||
const status = statuses?.find((s) => issue.status === s._id)
|
||||
const category = status?.$lookup?.category
|
||||
const color = status?.color ?? category?.color
|
||||
@@ -59,9 +82,11 @@
|
||||
|
||||
function openIssue (target: Ref<Issue>) {
|
||||
if (target !== value._id) {
|
||||
subIssueListProvider(subIssues, target)
|
||||
showPanel(tracker.component.EditIssue, target, value._class, 'content')
|
||||
}
|
||||
}
|
||||
|
||||
function showSubIssues () {
|
||||
if (subIssues) {
|
||||
closeTooltip()
|
||||
@@ -71,7 +96,7 @@
|
||||
value: subIssues.map((iss) => {
|
||||
const text = currentTeam ? `${getIssueId(currentTeam, iss)} ${iss.title}` : iss.title
|
||||
|
||||
return { id: iss._id, text, isSelected: iss._id === value._id, ...getIssueStatusIcon(iss) }
|
||||
return { id: iss._id, text, isSelected: iss._id === value._id, ...getIssueStatusIcon(iss, statuses) }
|
||||
}),
|
||||
width: 'large'
|
||||
},
|
||||
@@ -86,12 +111,6 @@
|
||||
},
|
||||
(selectedIssue) => {
|
||||
selectedIssue !== undefined && openIssue(selectedIssue)
|
||||
},
|
||||
(selectedIssue) => {
|
||||
const focus = subIssues?.find((it) => it._id === selectedIssue.id)
|
||||
if (focus !== undefined) {
|
||||
updateFocus({ focus })
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -17,10 +17,13 @@
|
||||
import presentation, { createQuery, getClient } from '@hcengineering/presentation'
|
||||
import { calcRank, Issue, IssueStatus, Team } from '@hcengineering/tracker'
|
||||
import { Label, Spinner } from '@hcengineering/ui'
|
||||
import { Viewlet, ViewOptions } from '@hcengineering/view'
|
||||
import tracker from '../../../plugin'
|
||||
import SubIssueList from '../edit/SubIssueList.svelte'
|
||||
|
||||
export let object: Doc
|
||||
export let viewlet: Viewlet
|
||||
export let viewOptions: ViewOptions
|
||||
|
||||
let query: DocumentQuery<Issue>
|
||||
$: query = { 'relations._id': object._id, 'relations._class': object._class }
|
||||
@@ -75,9 +78,9 @@
|
||||
</script>
|
||||
|
||||
<div class="mt-1">
|
||||
{#if subIssues !== undefined}
|
||||
{#if subIssues !== undefined && viewlet !== undefined}
|
||||
{#if issueStatuses.size > 0 && teams}
|
||||
<SubIssueList issues={subIssues} {teams} {issueStatuses} on:move={handleIssueSwap} />
|
||||
<SubIssueList bind:viewOptions {viewlet} issues={subIssues} {teams} {issueStatuses} on:move={handleIssueSwap} />
|
||||
{:else}
|
||||
<div class="p-1">
|
||||
<Label label={presentation.string.NoMatchesFound} />
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
<script lang="ts">
|
||||
import { Doc } from '@hcengineering/core'
|
||||
import { IntlString } from '@hcengineering/platform'
|
||||
import { createQuery } from '@hcengineering/presentation'
|
||||
import { Button, Icon, IconAdd, Label, showPopup } from '@hcengineering/ui'
|
||||
import view, { Viewlet } from '@hcengineering/view'
|
||||
import { getViewOptions, ViewletSettingButton } from '@hcengineering/view-resources'
|
||||
import tracker from '../../../plugin'
|
||||
import RelatedIssues from './RelatedIssues.svelte'
|
||||
export let object: Doc
|
||||
export let label: IntlString
|
||||
|
||||
let viewlet: Viewlet | undefined
|
||||
|
||||
const vquery = createQuery()
|
||||
$: vquery.query(view.class.Viewlet, { _id: tracker.viewlet.SubIssues }, (res) => {
|
||||
;[viewlet] = res
|
||||
})
|
||||
|
||||
let viewOptions = getViewOptions(viewlet)
|
||||
</script>
|
||||
|
||||
<div class="antiSection">
|
||||
<div class="antiSection-header">
|
||||
<div class="antiSection-header__icon">
|
||||
<Icon icon={tracker.icon.Issue} size={'small'} />
|
||||
</div>
|
||||
<span class="antiSection-header__title">
|
||||
<Label {label} />
|
||||
</span>
|
||||
<div class="buttons-group small-gap">
|
||||
{#if viewlet && viewOptions}
|
||||
<ViewletSettingButton bind:viewOptions {viewlet} kind={'transparent'} />
|
||||
{/if}
|
||||
<Button
|
||||
id="add-sub-issue"
|
||||
width="min-content"
|
||||
icon={IconAdd}
|
||||
label={undefined}
|
||||
labelParams={{ subIssues: 0 }}
|
||||
kind={'transparent'}
|
||||
size={'small'}
|
||||
on:click={() => {
|
||||
showPopup(tracker.component.CreateIssue, { relatedTo: object, space: object.space }, 'top')
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-row">
|
||||
{#if viewlet}
|
||||
<RelatedIssues {object} {viewOptions} {viewlet} />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
@@ -120,9 +120,6 @@
|
||||
<IssuePresenter value={object} disableClick />
|
||||
</svelte:fragment>
|
||||
|
||||
<div class="header no-border flex-col p-1">
|
||||
<div class="flex-row-center flex-between" />
|
||||
</div>
|
||||
{#if currentTeam && issueStatuses}
|
||||
<SubIssuesEstimations
|
||||
issue={object}
|
||||
@@ -142,14 +139,18 @@
|
||||
<Button
|
||||
icon={IconAdd}
|
||||
size={'small'}
|
||||
on:click={(event) => {
|
||||
showPopup(TimeSpendReportPopup, {
|
||||
issueId: object._id,
|
||||
issueClass: object._class,
|
||||
space: object.space,
|
||||
assignee: object.assignee,
|
||||
defaultTimeReportDay
|
||||
})
|
||||
on:click={() => {
|
||||
showPopup(
|
||||
TimeSpendReportPopup,
|
||||
{
|
||||
issueId: object._id,
|
||||
issueClass: object._class,
|
||||
space: object.space,
|
||||
assignee: object.assignee,
|
||||
defaultTimeReportDay
|
||||
},
|
||||
'top'
|
||||
)
|
||||
}}
|
||||
label={tracker.string.TimeSpendReportAdd}
|
||||
/>
|
||||
|
||||
+30
-52
@@ -14,11 +14,11 @@
|
||||
-->
|
||||
<script lang="ts">
|
||||
import contact from '@hcengineering/contact'
|
||||
import { Doc, Ref } from '@hcengineering/core'
|
||||
import { Ref } from '@hcengineering/core'
|
||||
import { UserBox } from '@hcengineering/presentation'
|
||||
import { Issue, Team } from '@hcengineering/tracker'
|
||||
import { getEventPositionElement, ListView, showPopup } from '@hcengineering/ui'
|
||||
import { ContextMenu, FixedColumn, ListSelectionProvider, SelectDirection } from '@hcengineering/view-resources'
|
||||
import { deviceOptionsStore as deviceInfo, getEventPositionElement, ListView, showPopup } from '@hcengineering/ui'
|
||||
import { ContextMenu, FixedColumn, ListSelectionProvider } from '@hcengineering/view-resources'
|
||||
import { getIssueId } from '../../../issues'
|
||||
import tracker from '../../../plugin'
|
||||
import EstimationEditor from './EstimationEditor.svelte'
|
||||
@@ -28,18 +28,19 @@
|
||||
export let teams: Map<Ref<Team>, Team>
|
||||
|
||||
function showContextMenu (ev: MouseEvent, object: Issue) {
|
||||
showPopup(ContextMenu, { object }, getEventPositionElement(ev))
|
||||
showPopup(ContextMenu, { object }, $deviceInfo.isMobile ? 'top' : getEventPositionElement(ev))
|
||||
}
|
||||
|
||||
const listProvider = new ListSelectionProvider((offset: 1 | -1 | 0, of?: Doc, dir?: SelectDirection) => {})
|
||||
const listProvider = new ListSelectionProvider(() => {})
|
||||
$: twoRows = $deviceInfo.twoRows
|
||||
</script>
|
||||
|
||||
<ListView count={issues.length}>
|
||||
<ListView count={issues.length} addClass={'step-tb-2-accent'}>
|
||||
<svelte:fragment slot="item" let:item>
|
||||
{@const issue = issues[item]}
|
||||
{@const currentTeam = teams.get(issue.space)}
|
||||
<div
|
||||
class="flex-between row"
|
||||
class="{twoRows ? 'flex-col' : 'flex-between'} p-text-2"
|
||||
on:contextmenu|preventDefault={(ev) => showContextMenu(ev, issue)}
|
||||
on:mouseover={() => {
|
||||
listProvider.updateFocus(issue)
|
||||
@@ -48,55 +49,32 @@
|
||||
listProvider.updateFocus(issue)
|
||||
}}
|
||||
>
|
||||
<div class="flex-row-center clear-mins gap-2 p-2 flex-grow">
|
||||
<span class="issuePresenter">
|
||||
<FixedColumn key={'estimation_issue'} justify={'left'}>
|
||||
{#if currentTeam}
|
||||
{getIssueId(currentTeam, issue)}
|
||||
{/if}
|
||||
</FixedColumn>
|
||||
</span>
|
||||
<span class="text name" title={issue.title}>
|
||||
<div class="flex-row-center clear-mins gap-2 flex-grow mr-4" class:p-text={twoRows}>
|
||||
<FixedColumn key={'estimation_issue'} justify={'left'} addClass={'fs-bold'}>
|
||||
{#if currentTeam}
|
||||
{getIssueId(currentTeam, issue)}
|
||||
{/if}
|
||||
</FixedColumn>
|
||||
<span class="overflow-label fs-bold caption-color" title={issue.title}>
|
||||
{issue.title}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<FixedColumn key={'estimation_issue_assignee'} justify={'right'}>
|
||||
<UserBox
|
||||
width={'100%'}
|
||||
label={tracker.string.Assignee}
|
||||
_class={contact.class.Employee}
|
||||
value={issue.assignee}
|
||||
readonly
|
||||
showNavigate={false}
|
||||
/>
|
||||
</FixedColumn>
|
||||
<FixedColumn key={'estimation'} justify={'left'}>
|
||||
<EstimationEditor value={issue} kind={'list'} />
|
||||
</FixedColumn>
|
||||
<div class="flex-row-center clear-mins gap-2 self-end" class:p-text={twoRows}>
|
||||
<FixedColumn key={'estimation_issue_assignee'} justify={'right'}>
|
||||
<UserBox
|
||||
width={'100%'}
|
||||
label={tracker.string.Assignee}
|
||||
_class={contact.class.Employee}
|
||||
value={issue.assignee}
|
||||
readonly
|
||||
showNavigate={false}
|
||||
/>
|
||||
</FixedColumn>
|
||||
<FixedColumn key={'estimation'} justify={'left'}>
|
||||
<EstimationEditor value={issue} kind={'list'} />
|
||||
</FixedColumn>
|
||||
</div>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</ListView>
|
||||
|
||||
<style lang="scss">
|
||||
.row {
|
||||
.text {
|
||||
font-weight: 500;
|
||||
color: var(--caption-color);
|
||||
}
|
||||
|
||||
.issuePresenter {
|
||||
flex-shrink: 0;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
font-weight: 500;
|
||||
color: var(--content-color);
|
||||
}
|
||||
|
||||
.name {
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -53,6 +53,7 @@
|
||||
</script>
|
||||
|
||||
{#if kind === 'link'}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div id="ReportedTimeEditor" class="link-container flex-between" on:click={showReports}>
|
||||
{#if value !== undefined}
|
||||
<span class="overflow-label">
|
||||
|
||||
@@ -70,7 +70,7 @@
|
||||
config={[
|
||||
'$lookup.attachedTo',
|
||||
'',
|
||||
'$lookup.employee',
|
||||
'employee',
|
||||
{
|
||||
key: '$lookup.attachedTo',
|
||||
presenter: ParentNamesPresenter,
|
||||
|
||||
+4
-10
@@ -16,7 +16,7 @@
|
||||
import { Ref, SortingOrder, WithLookup } from '@hcengineering/core'
|
||||
import { createQuery } from '@hcengineering/presentation'
|
||||
import { Issue, IssueStatus, Team } from '@hcengineering/tracker'
|
||||
import { Scroller, Spinner } from '@hcengineering/ui'
|
||||
import { Spinner } from '@hcengineering/ui'
|
||||
import Expandable from '@hcengineering/ui/src/components/Expandable.svelte'
|
||||
import tracker from '../../../plugin'
|
||||
import EstimationSubIssueList from './EstimationSubIssueList.svelte'
|
||||
@@ -38,15 +38,9 @@
|
||||
|
||||
{#if subIssues && issueStatuses}
|
||||
{#if hasSubIssues}
|
||||
<Expandable label={tracker.string.ChildEstimation}>
|
||||
<svelte:fragment slot="title">
|
||||
: {total}
|
||||
</svelte:fragment>
|
||||
<div class="h-50">
|
||||
<Scroller>
|
||||
<EstimationSubIssueList issues={subIssues} {teams} />
|
||||
</Scroller>
|
||||
</div>
|
||||
<Expandable label={tracker.string.ChildEstimation} contentColor bordered>
|
||||
<svelte:fragment slot="title">: <span class="caption-color">{total}</span></svelte:fragment>
|
||||
<EstimationSubIssueList issues={subIssues} {teams} />
|
||||
</Expandable>
|
||||
{/if}
|
||||
{:else}
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
export let value: number
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<span
|
||||
{id}
|
||||
class:link={kind === 'link'}
|
||||
|
||||
+3
-1
@@ -15,11 +15,13 @@
|
||||
<script lang="ts">
|
||||
import { TimeReportDayType } from '@hcengineering/tracker'
|
||||
import { DropdownIntlItem, DropdownLabelsIntl } from '@hcengineering/ui'
|
||||
import type { ButtonKind } from '@hcengineering/ui'
|
||||
import tracker from '../../../plugin'
|
||||
import TimeReportDayIcon from './TimeReportDayIcon.svelte'
|
||||
|
||||
export let label = tracker.string.TimeReportDayTypeLabel
|
||||
export let selected: TimeReportDayType | undefined
|
||||
export let kind: ButtonKind = 'link-bordered'
|
||||
|
||||
const workDaysDropdownItems: DropdownIntlItem[] = [
|
||||
{
|
||||
@@ -34,7 +36,7 @@
|
||||
</script>
|
||||
|
||||
<DropdownLabelsIntl
|
||||
kind="link-bordered"
|
||||
{kind}
|
||||
icon={TimeReportDayIcon}
|
||||
shouldUpdateUndefined={false}
|
||||
{label}
|
||||
|
||||
+13
-5
@@ -18,7 +18,7 @@
|
||||
import type { IntlString } from '@hcengineering/platform'
|
||||
import presentation, { Card, getClient, UserBox } from '@hcengineering/presentation'
|
||||
import { Issue, TimeReportDayType, TimeSpendReport } from '@hcengineering/tracker'
|
||||
import { DatePresenter, EditBox } from '@hcengineering/ui'
|
||||
import { DatePresenter, EditBox, Button } from '@hcengineering/ui'
|
||||
import tracker from '../../../plugin'
|
||||
import { getTimeReportDate, getTimeReportDayType } from '../../../utils'
|
||||
import TimeReportDayDropdown from './TimeReportDayDropdown.svelte'
|
||||
@@ -85,23 +85,31 @@
|
||||
>
|
||||
<div class="flex-row-center gap-2">
|
||||
<EditBox focus bind:value={data.value} {placeholder} format={'number'} maxDigitsAfterPoint={3} kind={'editbox'} />
|
||||
<Button kind={'link-bordered'} on:click={() => (data.value = 0.125)}><span slot="content">1/8</span></Button>
|
||||
<Button kind={'link-bordered'} on:click={() => (data.value = 0.25)}><span slot="content">1/4</span></Button>
|
||||
<Button kind={'link-bordered'} on:click={() => (data.value = 0.5)}><span slot="content">1/2</span></Button>
|
||||
<Button kind={'link-bordered'} on:click={() => (data.value = 0.75)}><span slot="content">3/4</span></Button>
|
||||
<div class="buttons-divider" />
|
||||
<Button kind={'link-bordered'} on:click={() => (data.value = 1)}><span slot="content">1</span></Button>
|
||||
</div>
|
||||
<EditBox bind:value={data.description} placeholder={tracker.string.TimeSpendReportDescription} kind={'editbox'} />
|
||||
<svelte:fragment slot="pool">
|
||||
<UserBox
|
||||
_class={contact.class.Employee}
|
||||
label={contact.string.Employee}
|
||||
kind={'link-bordered'}
|
||||
kind={'no-border'}
|
||||
bind:value={data.employee}
|
||||
showNavigate={false}
|
||||
/>
|
||||
<TimeReportDayDropdown
|
||||
kind={'no-border'}
|
||||
bind:selected={selectedTimeReportDay}
|
||||
on:selected={({ detail }) => (data.date = getTimeReportDate(detail))}
|
||||
/>
|
||||
<DatePresenter
|
||||
kind={'link'}
|
||||
bind:value={data.date}
|
||||
editable
|
||||
on:change={({ detail }) => (selectedTimeReportDay = getTimeReportDayType(detail))}
|
||||
/>
|
||||
</div>
|
||||
<EditBox bind:value={data.description} placeholder={tracker.string.TimeSpendReportDescription} kind={'editbox'} />
|
||||
</svelte:fragment>
|
||||
</Card>
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
import { DocumentQuery, Ref, SortingOrder } from '@hcengineering/core'
|
||||
import { createQuery } from '@hcengineering/presentation'
|
||||
import { Issue, Team, TimeSpendReport } from '@hcengineering/tracker'
|
||||
import { Expandable, floorFractionDigits, Label, Scroller, Spinner } from '@hcengineering/ui'
|
||||
import { Expandable, floorFractionDigits, Label, Spinner } from '@hcengineering/ui'
|
||||
import tracker from '../../../plugin'
|
||||
import TimePresenter from './TimePresenter.svelte'
|
||||
import TimeSpendReportsList from './TimeSpendReportsList.svelte'
|
||||
@@ -42,21 +42,19 @@
|
||||
</script>
|
||||
|
||||
{#if reports}
|
||||
<Expandable expanded={true}>
|
||||
<Expandable expanded={true} contentColor bordered>
|
||||
<svelte:fragment slot="title">
|
||||
<span class="overflow-label flex-nowrap">
|
||||
<Label label={tracker.string.ReportedTime} />: <TimePresenter value={reportedTime} {workDayLength} />
|
||||
<Label label={tracker.string.TimeSpendReports} />: <TimePresenter value={total} {workDayLength} />
|
||||
<Label label={tracker.string.ReportedTime} />:
|
||||
<span class="caption-color"><TimePresenter value={reportedTime} {workDayLength} /></span>.
|
||||
<Label label={tracker.string.TimeSpendReports} />:
|
||||
<span class="caption-color"><TimePresenter value={total} {workDayLength} /></span>
|
||||
</span>
|
||||
</svelte:fragment>
|
||||
<div class="h-50">
|
||||
<Scroller>
|
||||
<TimeSpendReportsList {reports} {teams} />
|
||||
</Scroller>
|
||||
</div>
|
||||
<TimeSpendReportsList {reports} {teams} />
|
||||
</Expandable>
|
||||
{:else}
|
||||
<div class="flex-center pt-3">
|
||||
<div class="flex-center">
|
||||
<Spinner />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
+40
-59
@@ -14,12 +14,18 @@
|
||||
-->
|
||||
<script lang="ts">
|
||||
import contact from '@hcengineering/contact'
|
||||
import { Doc, Ref, Space, WithLookup } from '@hcengineering/core'
|
||||
import { Ref, Space, WithLookup } from '@hcengineering/core'
|
||||
import UserBox from '@hcengineering/presentation/src/components/UserBox.svelte'
|
||||
import { Team, TimeReportDayType, TimeSpendReport } from '@hcengineering/tracker'
|
||||
import { eventToHTMLElement, getEventPositionElement, ListView, showPopup } from '@hcengineering/ui'
|
||||
import {
|
||||
deviceOptionsStore as deviceInfo,
|
||||
eventToHTMLElement,
|
||||
getEventPositionElement,
|
||||
ListView,
|
||||
showPopup
|
||||
} from '@hcengineering/ui'
|
||||
import DatePresenter from '@hcengineering/ui/src/components/calendar/DatePresenter.svelte'
|
||||
import { ContextMenu, FixedColumn, ListSelectionProvider, SelectDirection } from '@hcengineering/view-resources'
|
||||
import { ContextMenu, FixedColumn, ListSelectionProvider } from '@hcengineering/view-resources'
|
||||
import { getIssueId } from '../../../issues'
|
||||
import tracker from '../../../plugin'
|
||||
import TimePresenter from './TimePresenter.svelte'
|
||||
@@ -33,7 +39,7 @@
|
||||
showPopup(ContextMenu, { object }, getEventPositionElement(ev))
|
||||
}
|
||||
|
||||
const listProvider = new ListSelectionProvider((offset: 1 | -1 | 0, of?: Doc, dir?: SelectDirection) => {})
|
||||
const listProvider = new ListSelectionProvider(() => {})
|
||||
|
||||
const toTeamId = (ref: Ref<Space>) => ref as Ref<Team>
|
||||
|
||||
@@ -51,18 +57,19 @@
|
||||
assignee: value.employee,
|
||||
defaultTimeReportDay
|
||||
},
|
||||
eventToHTMLElement(event)
|
||||
$deviceInfo.isMobile ? 'top' : eventToHTMLElement(event)
|
||||
)
|
||||
}
|
||||
$: twoRows = $deviceInfo.twoRows
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<ListView count={reports.length}>
|
||||
<ListView count={reports.length} addClass={'step-tb-2-accent'}>
|
||||
<svelte:fragment slot="item" let:item>
|
||||
{@const report = reports[item]}
|
||||
{@const currentTeam = teams.get(toTeamId(report.space))}
|
||||
<div
|
||||
class="flex-between row"
|
||||
class="{twoRows ? 'flex-col' : 'flex-between'} p-text-2"
|
||||
on:contextmenu|preventDefault={(ev) => showContextMenu(ev, report)}
|
||||
on:mouseover={() => {
|
||||
listProvider.updateFocus(report)
|
||||
@@ -72,62 +79,36 @@
|
||||
}}
|
||||
on:click={(evt) => editSpendReport(evt, report, currentTeam?.defaultTimeReportDay)}
|
||||
>
|
||||
<div class="flex-row-center clear-mins gap-2 p-2 flex-grow">
|
||||
<span class="issuePresenter">
|
||||
<FixedColumn key={'tmiespend_issue'} justify={'left'}>
|
||||
{#if currentTeam && report.$lookup?.attachedTo}
|
||||
{getIssueId(currentTeam, report.$lookup?.attachedTo)}
|
||||
{/if}
|
||||
</FixedColumn>
|
||||
</span>
|
||||
<div class="flex-row-center clear-mins gap-2 flex-grow mr-4" class:p-text={twoRows}>
|
||||
<FixedColumn key={'tmiespend_issue'} justify={'left'} addClass={'fs-bold'}>
|
||||
{#if currentTeam && report.$lookup?.attachedTo}
|
||||
{getIssueId(currentTeam, report.$lookup?.attachedTo)}
|
||||
{/if}
|
||||
</FixedColumn>
|
||||
{#if report.$lookup?.attachedTo?.title}
|
||||
<span class="text name" title={report.$lookup?.attachedTo?.title}>
|
||||
<span class="overflow-label fs-bold caption-color" title={report.$lookup?.attachedTo?.title}>
|
||||
{report.$lookup?.attachedTo?.title}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
<FixedColumn key={'timespend_assignee'} justify={'left'}>
|
||||
<UserBox
|
||||
width={'100%'}
|
||||
label={tracker.string.Assignee}
|
||||
_class={contact.class.Employee}
|
||||
value={report.employee}
|
||||
readonly
|
||||
showNavigate={false}
|
||||
/>
|
||||
</FixedColumn>
|
||||
|
||||
<FixedColumn key={'timespend_reported'} justify={'center'}>
|
||||
<div class="p-1">
|
||||
<div class="flex-row-center clear-mins gap-2 self-end" class:p-text={twoRows}>
|
||||
<FixedColumn key={'timespend_assignee'} justify={'left'}>
|
||||
<UserBox
|
||||
width={'100%'}
|
||||
label={tracker.string.Assignee}
|
||||
_class={contact.class.Employee}
|
||||
value={report.employee}
|
||||
readonly
|
||||
showNavigate={false}
|
||||
/>
|
||||
</FixedColumn>
|
||||
<FixedColumn key={'timespend_reported'} justify={'center'}>
|
||||
<TimePresenter value={report.value} workDayLength={currentTeam?.workDayLength} />
|
||||
</div>
|
||||
</FixedColumn>
|
||||
<FixedColumn key={'timespend_date'} justify={'left'}>
|
||||
<DatePresenter value={report.date} />
|
||||
</FixedColumn>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</FixedColumn>
|
||||
<FixedColumn key={'timespend_date'} justify={'left'}>
|
||||
<DatePresenter value={report.date} />
|
||||
</FixedColumn>
|
||||
</div>
|
||||
</div></svelte:fragment
|
||||
>
|
||||
</ListView>
|
||||
|
||||
<style lang="scss">
|
||||
.row {
|
||||
.text {
|
||||
font-weight: 500;
|
||||
color: var(--caption-color);
|
||||
}
|
||||
|
||||
.issuePresenter {
|
||||
flex-shrink: 0;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
font-weight: 500;
|
||||
color: var(--content-color);
|
||||
}
|
||||
|
||||
.name {
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user