mirror of
https://github.com/hcengineering/platform.git
synced 2026-09-11 12:17:44 +02:00
Tracker: Rewrite AssigneePresenter (#1568)
This commit is contained in:
@@ -13,18 +13,19 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import contact, { Employee, formatName } from '@anticrm/contact'
|
||||
import { Ref, WithLookup } from '@anticrm/core'
|
||||
import contact, { Employee } from '@anticrm/contact'
|
||||
import { Class, Doc, Ref } from '@anticrm/core'
|
||||
import { Issue, Team } from '@anticrm/tracker'
|
||||
import { Avatar, UsersPopup, getClient } from '@anticrm/presentation'
|
||||
import { eventToHTMLElement, showPopup, Tooltip } from '@anticrm/ui'
|
||||
|
||||
import { UsersPopup, getClient } from '@anticrm/presentation'
|
||||
import { AttributeModel } from '@anticrm/view'
|
||||
import { eventToHTMLElement, showPopup } from '@anticrm/ui'
|
||||
import { getObjectPresenter } from '@anticrm/view-resources'
|
||||
import { IntlString } from '@anticrm/platform'
|
||||
import tracker from '../../plugin'
|
||||
import { IntlString, translate } from '@anticrm/platform'
|
||||
import { onMount } from 'svelte'
|
||||
|
||||
export let value: WithLookup<Issue>
|
||||
export let employees: (WithLookup<Employee> | undefined)[] = []
|
||||
export let value: Employee | null
|
||||
export let issueId: Ref<Issue>
|
||||
export let defaultClass: Ref<Class<Doc>> | undefined = undefined
|
||||
export let currentSpace: Ref<Team> | undefined = undefined
|
||||
export let isEditable: boolean = true
|
||||
export let shouldShowLabel: boolean = false
|
||||
@@ -32,37 +33,26 @@
|
||||
|
||||
const client = getClient()
|
||||
|
||||
let defaultNameString: string = ''
|
||||
let presenter: AttributeModel | undefined
|
||||
|
||||
$: employee = (value?.$lookup?.assignee ?? employees.find(x => x?._id === value?.assignee)) as Employee | undefined
|
||||
$: avatar = employee?.avatar
|
||||
$: formattedName = employee?.name ? formatName(employee.name) : defaultNameString
|
||||
$: label = employee ? tracker.string.AssignedTo : tracker.string.AssignTo
|
||||
|
||||
$: getDefaultNameString = async () => {
|
||||
if (!defaultName) {
|
||||
return
|
||||
$: if (value || defaultClass) {
|
||||
if (value) {
|
||||
getObjectPresenter(client, value._class, { key: '' }).then((p) => {
|
||||
presenter = p
|
||||
})
|
||||
} else if (defaultClass) {
|
||||
getObjectPresenter(client, defaultClass, { key: '' }).then((p) => {
|
||||
presenter = p
|
||||
})
|
||||
}
|
||||
|
||||
const result = await translate(defaultName, {})
|
||||
|
||||
if (!result) {
|
||||
return
|
||||
}
|
||||
|
||||
defaultNameString = result
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
getDefaultNameString()
|
||||
})
|
||||
|
||||
const handleAssigneeChanged = async (result: Employee | null | undefined) => {
|
||||
if (!isEditable || result === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
const currentIssue = await client.findOne(tracker.class.Issue, { space: currentSpace, _id: value._id })
|
||||
const currentIssue = await client.findOne(tracker.class.Issue, { space: currentSpace, _id: issueId })
|
||||
|
||||
if (currentIssue === undefined) {
|
||||
return
|
||||
@@ -81,7 +71,7 @@
|
||||
UsersPopup,
|
||||
{
|
||||
_class: contact.class.Employee,
|
||||
selected: employee?._id,
|
||||
selected: value?._id,
|
||||
allowDeselect: true,
|
||||
placeholder: tracker.string.AssignTo
|
||||
},
|
||||
@@ -91,36 +81,16 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if isEditable}
|
||||
<Tooltip {label} props={{ value: formattedName }}>
|
||||
<div class="flex-presenter" on:click={handleAssigneeEditorOpened}>
|
||||
<div class="icon">
|
||||
<Avatar size={'tiny'} {avatar} />
|
||||
</div>
|
||||
{#if shouldShowLabel}
|
||||
<div class="label nowrap ml-2">
|
||||
{formattedName}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</Tooltip>
|
||||
{:else}
|
||||
<div class="presenter">
|
||||
<div class="icon">
|
||||
<Avatar size={'tiny'} {avatar} />
|
||||
</div>
|
||||
{#if shouldShowLabel}
|
||||
<div class="label nowrap ml-2">
|
||||
{formattedName}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{#if presenter}
|
||||
<svelte:component
|
||||
this={presenter.presenter}
|
||||
{value}
|
||||
{defaultName}
|
||||
avatarSize={'tiny'}
|
||||
isInteractive={true}
|
||||
shouldShowPlaceholder={true}
|
||||
shouldShowName={shouldShowLabel}
|
||||
onEdit={handleAssigneeEditorOpened}
|
||||
tooltipLabels={{ personLabel: tracker.string.AssignedTo, placeholderLabel: tracker.string.AssignTo }}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
.presenter {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -43,16 +43,21 @@
|
||||
})
|
||||
|
||||
let states: TypeState[] | undefined
|
||||
$: statusesQuery.query(tracker.class.IssueStatus, { attachedTo: currentSpace }, (issueStatuses) => {
|
||||
states = issueStatuses.map((status) => ({
|
||||
_id: status._id,
|
||||
title: status.name,
|
||||
color: status.color ?? status.$lookup?.category?.color ?? 0
|
||||
}))
|
||||
}, {
|
||||
lookup: { category: tracker.class.IssueStatusCategory },
|
||||
sort: { rank: SortingOrder.Ascending }
|
||||
})
|
||||
$: statusesQuery.query(
|
||||
tracker.class.IssueStatus,
|
||||
{ attachedTo: currentSpace },
|
||||
(issueStatuses) => {
|
||||
states = issueStatuses.map((status) => ({
|
||||
_id: status._id,
|
||||
title: status.name,
|
||||
color: status.color ?? status.$lookup?.category?.color ?? 0
|
||||
}))
|
||||
},
|
||||
{
|
||||
lookup: { category: tracker.class.IssueStatusCategory },
|
||||
sort: { rank: SortingOrder.Ascending }
|
||||
}
|
||||
)
|
||||
|
||||
/* eslint-disable prefer-const */
|
||||
/* eslint-disable no-unused-vars */
|
||||
@@ -69,22 +74,25 @@
|
||||
}
|
||||
|
||||
let kanbanUI: Kanban
|
||||
const listProvider = new ListSelectionProvider(
|
||||
(offset: 1 | -1 | 0, of?: Doc, dir?: SelectDirection) => {
|
||||
kanbanUI.select(offset, of, dir)
|
||||
}
|
||||
)
|
||||
const listProvider = new ListSelectionProvider((offset: 1 | -1 | 0, of?: Doc, dir?: SelectDirection) => {
|
||||
kanbanUI.select(offset, of, dir)
|
||||
})
|
||||
onMount(() => {
|
||||
(document.activeElement as HTMLElement)?.blur()
|
||||
})
|
||||
|
||||
const showMenu = async (ev: MouseEvent, items: Doc[]): Promise<void> => {
|
||||
ev.preventDefault()
|
||||
showPopup(Menu, { object: items, baseMenuClass }, {
|
||||
getBoundingClientRect: () => DOMRect.fromRect({ width: 1, height: 1, x: ev.clientX, y: ev.clientY })
|
||||
}, () => {
|
||||
// selection = undefined
|
||||
})
|
||||
showPopup(
|
||||
Menu,
|
||||
{ object: items, baseMenuClass },
|
||||
{
|
||||
getBoundingClientRect: () => DOMRect.fromRect({ width: 1, height: 1, x: ev.clientX, y: ev.clientY })
|
||||
},
|
||||
() => {
|
||||
// selection = undefined
|
||||
}
|
||||
)
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -94,9 +102,7 @@
|
||||
mode: 'browser'
|
||||
}}
|
||||
/>
|
||||
<div class="flex-between label font-medium w-full p-4">
|
||||
Board
|
||||
</div>
|
||||
<div class="flex-between label font-medium w-full p-4">Board</div>
|
||||
<Kanban
|
||||
bind:this={kanbanUI}
|
||||
_class={tracker.class.Issue}
|
||||
@@ -114,7 +120,6 @@
|
||||
listProvider.updateFocus(evt.detail)
|
||||
}}
|
||||
selection={listProvider.current($focusStore)}
|
||||
|
||||
checked={$selectionStore ?? []}
|
||||
on:check={(evt) => {
|
||||
listProvider.updateSelection(evt.detail.docs, evt.detail.value)
|
||||
@@ -149,14 +154,14 @@
|
||||
<div class="flex-between mb-2">
|
||||
<IssuePresenter value={object} {currentTeam} />
|
||||
{#if issue.$lookup?.assignee}
|
||||
<AssigneePresenter value={issue} {currentSpace} isEditable={true}/>
|
||||
<AssigneePresenter value={issue.$lookup.assignee} issueId={issue._id} {currentSpace} isEditable={true} />
|
||||
{/if}
|
||||
</div>
|
||||
<span class="fs-bold title">
|
||||
{object.title}
|
||||
</span>
|
||||
<div class='flex gap-2 mt-2 mb-2'>
|
||||
<PriorityPresenter value={issue} {currentSpace} isEditable={true}/>
|
||||
<div class="flex gap-2 mt-2 mb-2">
|
||||
<PriorityPresenter value={issue} {currentSpace} isEditable={true} />
|
||||
</div>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import contact, { Employee } from '@anticrm/contact'
|
||||
import contact, { Employee, formatName } from '@anticrm/contact'
|
||||
import { DocumentQuery, FindOptions, Ref, SortingOrder, WithLookup } from '@anticrm/core'
|
||||
import { createQuery } from '@anticrm/presentation'
|
||||
import {
|
||||
@@ -27,9 +27,8 @@
|
||||
IssuePriority
|
||||
} from '@anticrm/tracker'
|
||||
import { Button, Label, ScrollBox, IconOptions, showPopup, eventToHTMLElement } from '@anticrm/ui'
|
||||
import tracker from '../../plugin'
|
||||
import { IntlString } from '@anticrm/platform'
|
||||
import ViewOptionsPopup from './ViewOptionsPopup.svelte'
|
||||
import tracker from '../../plugin'
|
||||
import {
|
||||
IssuesGroupByKeys,
|
||||
issuesGroupKeyMap,
|
||||
@@ -38,6 +37,7 @@
|
||||
groupBy,
|
||||
issuesSortOrderMap
|
||||
} from '../../utils'
|
||||
import ViewOptionsPopup from './ViewOptionsPopup.svelte'
|
||||
import IssuesListBrowser from './IssuesListBrowser.svelte'
|
||||
|
||||
export let currentSpace: Ref<Team>
|
||||
@@ -184,8 +184,11 @@
|
||||
|
||||
return Object.keys(unorderedIssues)
|
||||
.sort((o1, o2) => {
|
||||
const i1 = orderedCategories.findIndex((x) => x === o1)
|
||||
const i2 = orderedCategories.findIndex((x) => x === o2)
|
||||
const key1 = o1 === 'null' ? null : o1
|
||||
const key2 = o2 === 'null' ? null : o2
|
||||
|
||||
const i1 = orderedCategories.findIndex((x) => x === key1)
|
||||
const i2 = orderedCategories.findIndex((x) => x === key2)
|
||||
|
||||
return i1 - i2
|
||||
})
|
||||
@@ -245,6 +248,36 @@
|
||||
})
|
||||
}
|
||||
|
||||
if (key === 'assignee') {
|
||||
existingCategories.sort((a1, a2) => {
|
||||
const employeeId1 = a1 as Ref<Employee> | null
|
||||
const employeeId2 = a2 as Ref<Employee> | null
|
||||
|
||||
if (employeeId1 === null && employeeId2 !== null) {
|
||||
return 1
|
||||
}
|
||||
|
||||
if (employeeId1 !== null && employeeId2 === null) {
|
||||
return -1
|
||||
}
|
||||
|
||||
if (employeeId1 !== null && employeeId2 !== null) {
|
||||
const name1 = formatName(employees.find((x) => x?._id === employeeId1)?.name ?? '')
|
||||
const name2 = formatName(employees.find((x) => x?._id === employeeId2)?.name ?? '')
|
||||
|
||||
if (name1 > name2) {
|
||||
return 1
|
||||
} else if (name2 > name1) {
|
||||
return -1
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
return 0
|
||||
})
|
||||
}
|
||||
|
||||
return existingCategories
|
||||
}
|
||||
|
||||
@@ -322,7 +355,11 @@
|
||||
{ key: '', presenter: tracker.component.TitlePresenter, props: { shouldUseMargin: true } },
|
||||
{ key: '', presenter: tracker.component.DueDatePresenter, props: { currentSpace } },
|
||||
{ key: 'modifiedOn', presenter: tracker.component.ModificationDatePresenter },
|
||||
{ key: '', presenter: tracker.component.AssigneePresenter, props: { currentSpace } }
|
||||
{
|
||||
key: '$lookup.assignee',
|
||||
presenter: tracker.component.AssigneePresenter,
|
||||
props: { currentSpace, defaultClass: contact.class.Employee, shouldShowLabel: false }
|
||||
}
|
||||
]}
|
||||
{groupedIssues}
|
||||
/>
|
||||
|
||||
@@ -18,11 +18,18 @@
|
||||
import { getClient } from '@anticrm/presentation'
|
||||
import { Issue, IssueStatus, Team } from '@anticrm/tracker'
|
||||
import {
|
||||
Button, CheckBox, Component, eventToHTMLElement, IconAdd, IconMoreV, showPopup,
|
||||
Spinner, Tooltip
|
||||
Button,
|
||||
CheckBox,
|
||||
Component,
|
||||
eventToHTMLElement,
|
||||
IconAdd,
|
||||
IconMoreV,
|
||||
showPopup,
|
||||
Spinner,
|
||||
Tooltip
|
||||
} from '@anticrm/ui'
|
||||
import { BuildModelKey } from '@anticrm/view'
|
||||
import { buildModel, LoadingProps, Menu } from '@anticrm/view-resources'
|
||||
import { AttributeModel, BuildModelKey } from '@anticrm/view'
|
||||
import { buildModel, getObjectPresenter, LoadingProps, Menu } from '@anticrm/view-resources'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import tracker from '../../plugin'
|
||||
import { IssuesGroupByKeys, issuesGroupPresenterMap, IssuesOrderByKeys, issuesSortOrderMap } from '../../utils'
|
||||
@@ -53,12 +60,19 @@
|
||||
}
|
||||
}
|
||||
|
||||
let personPresenter: AttributeModel
|
||||
|
||||
$: combinedGroupedIssues = Object.values(groupedIssues).flat(1)
|
||||
$: options = { ...baseOptions, sort: { [orderBy]: issuesSortOrderMap[orderBy] } } as FindOptions<Issue>
|
||||
$: headerComponent = groupByKey === undefined ? null : issuesGroupPresenterMap[groupByKey]
|
||||
$: headerComponent =
|
||||
groupByKey === undefined || groupByKey === 'assignee' ? null : issuesGroupPresenterMap[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
|
||||
})
|
||||
|
||||
const handleMenuOpened = async (event: MouseEvent, object: Doc, rowIndex: number) => {
|
||||
selectedRowIndex = rowIndex
|
||||
|
||||
@@ -139,20 +153,30 @@
|
||||
|
||||
<div>
|
||||
{#each categories as category}
|
||||
{#if headerComponent}
|
||||
{#if headerComponent || groupByKey === 'assignee'}
|
||||
<div class="header categoryHeader flex-between label">
|
||||
<div class="flex-row-center gap-2">
|
||||
<Component
|
||||
is={headerComponent}
|
||||
props={{
|
||||
isEditable: false,
|
||||
shouldShowLabel: true,
|
||||
value: groupByKey ? { [groupByKey]: category } : {},
|
||||
defaultName: groupByKey === 'assignee' ? tracker.string.NoAssignee : undefined,
|
||||
statuses: groupByKey === 'status' ? statuses : undefined,
|
||||
employees: groupByKey === 'assignee' ? employees : undefined
|
||||
}}
|
||||
/>
|
||||
{#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={'tiny'}
|
||||
/>
|
||||
{:else if headerComponent}
|
||||
<Component
|
||||
is={headerComponent}
|
||||
props={{
|
||||
isEditable: false,
|
||||
shouldShowLabel: true,
|
||||
value: groupByKey ? { [groupByKey]: category } : {},
|
||||
statuses: groupByKey === 'status' ? statuses : undefined
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
<span class="eLabelCounter ml-2">{(groupedIssues[category] ?? []).length}</span>
|
||||
</div>
|
||||
<div class="flex">
|
||||
@@ -235,6 +259,7 @@
|
||||
<svelte:component
|
||||
this={attributeModel.presenter}
|
||||
value={getObjectValue(attributeModel.key, docObject) ?? ''}
|
||||
issueId={docObject._id}
|
||||
{...attributeModel.props}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
$: listProvider.update(Object.values(groupedIssues).flat(1))
|
||||
|
||||
onMount(() => {
|
||||
;(document.activeElement as HTMLElement)?.blur()
|
||||
(document.activeElement as HTMLElement)?.blur()
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user