Tracker: Rewrite AssigneePresenter (#1568)

This commit is contained in:
Artyom Grigorovich
2022-04-28 19:03:10 +07:00
committed by GitHub
parent 155fca2649
commit d64c009366
38 changed files with 527 additions and 391 deletions
@@ -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>