mirror of
https://github.com/hcengineering/platform.git
synced 2025-04-13 19:58:09 +00:00
TSK-360: Assignee selection enhancements (#2509)
Signed-off-by: Denis Maslennikov <denis.maslennikov@gmail.com>
This commit is contained in:
parent
44ae8f2e81
commit
e9cc9e7b47
@ -28,6 +28,12 @@
|
||||
"Edit": "Edit",
|
||||
"SelectAvatar": "Select avatar",
|
||||
"GravatarsManaged": "Gravatars are managed through",
|
||||
"CategoryCurrentUser": "Current user",
|
||||
"Assigned": "Assigned",
|
||||
"CategoryPreviousAssigned": "Previously assigned",
|
||||
"CategoryProjectLead": "Project lead",
|
||||
"CategoryProjectMembers": "Project members",
|
||||
"CategoryOther": "Other",
|
||||
"InltPropsValue": "{value}"
|
||||
}
|
||||
}
|
||||
|
@ -22,12 +22,18 @@
|
||||
"NumberSpaces": "{count, plural, =0 {В} =1 {В 1 месте} other {В # местах}}",
|
||||
"InThis": "В этом {space}",
|
||||
"NoMatchesInThis": "В этом {space} совпадения не обнаружены",
|
||||
"NoMatchesFound": "Не найдено соответсвий",
|
||||
"NoMatchesFound": "Не найдено соответствий",
|
||||
"NotInThis": "Не в этом {space}",
|
||||
"Add": "Добавить",
|
||||
"Edit": "Редактировать",
|
||||
"SelectAvatar": "Выбрать аватар",
|
||||
"GravatarsManaged": "Граватары управляются через",
|
||||
"CategoryCurrentUser": "Текущий пользователь",
|
||||
"Assigned": "Назначен",
|
||||
"CategoryPreviousAssigned": "Ранее назначенные",
|
||||
"CategoryProjectLead": "Руководитель проекта",
|
||||
"CategoryProjectMembers": "Участники проекта",
|
||||
"CategoryOther": "Прочие",
|
||||
"InltPropsValue": "{value}"
|
||||
}
|
||||
}
|
||||
|
177
packages/presentation/src/components/AssigneeBox.svelte
Normal file
177
packages/presentation/src/components/AssigneeBox.svelte
Normal file
@ -0,0 +1,177 @@
|
||||
<!--
|
||||
// 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, { Contact, Employee, formatName } from '@hcengineering/contact'
|
||||
import { Class, DocumentQuery, FindOptions, Ref } from '@hcengineering/core'
|
||||
import { getEmbeddedLabel, IntlString } from '@hcengineering/platform'
|
||||
import {
|
||||
ActionIcon,
|
||||
Button,
|
||||
ButtonKind,
|
||||
ButtonSize,
|
||||
getEventPositionElement,
|
||||
getFocusManager,
|
||||
Icon,
|
||||
IconOpen,
|
||||
Label,
|
||||
LabelAndProps,
|
||||
showPanel,
|
||||
showPopup,
|
||||
tooltip
|
||||
} from '@hcengineering/ui'
|
||||
import view from '@hcengineering/view'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import presentation, { IconPerson, UserInfo, getClient } from '..'
|
||||
import AssigneePopup from './AssigneePopup.svelte'
|
||||
|
||||
export let _class: Ref<Class<Employee>> = contact.class.Employee
|
||||
export let excluded: Ref<Contact>[] | undefined = undefined
|
||||
export let options: FindOptions<Employee> | undefined = undefined
|
||||
export let docQuery: DocumentQuery<Employee> | undefined = {
|
||||
active: true
|
||||
}
|
||||
export let label: IntlString
|
||||
export let placeholder: IntlString = presentation.string.Search
|
||||
export let value: Ref<Employee> | null | undefined
|
||||
export let prevAssigned: Ref<Employee>[] | undefined = []
|
||||
export let projectLead: Ref<Employee> | undefined = undefined
|
||||
export let projectMembers: Ref<Employee>[] | undefined = []
|
||||
export let titleDeselect: IntlString | undefined = undefined
|
||||
export let readonly = false
|
||||
export let kind: ButtonKind = 'no-border'
|
||||
export let size: ButtonSize = 'small'
|
||||
export let justify: 'left' | 'center' = 'center'
|
||||
export let width: string | undefined = undefined
|
||||
export let focusIndex = -1
|
||||
export let showTooltip: LabelAndProps | undefined = undefined
|
||||
export let showNavigate = true
|
||||
export let id: string | undefined = undefined
|
||||
|
||||
const icon = IconPerson
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
let selected: Employee | undefined
|
||||
let container: HTMLElement
|
||||
|
||||
const client = getClient()
|
||||
|
||||
async function updateSelected (value: Ref<Employee> | null | undefined) {
|
||||
selected = value ? await client.findOne(_class, { _id: value }) : undefined
|
||||
}
|
||||
|
||||
$: updateSelected(value)
|
||||
|
||||
function getName (obj: Contact): string {
|
||||
const isPerson = client.getHierarchy().isDerived(obj._class, contact.class.Person)
|
||||
return isPerson ? formatName(obj.name) : obj.name
|
||||
}
|
||||
const mgr = getFocusManager()
|
||||
|
||||
const _click = (ev: MouseEvent): void => {
|
||||
if (!readonly) {
|
||||
showPopup(
|
||||
AssigneePopup,
|
||||
{
|
||||
_class,
|
||||
options,
|
||||
docQuery,
|
||||
prevAssigned,
|
||||
projectLead,
|
||||
projectMembers,
|
||||
ignoreUsers: excluded ?? [],
|
||||
icon,
|
||||
selected: value,
|
||||
placeholder,
|
||||
titleDeselect
|
||||
},
|
||||
!$$slots.content ? container : getEventPositionElement(ev),
|
||||
(result) => {
|
||||
if (result === null) {
|
||||
value = null
|
||||
selected = undefined
|
||||
dispatch('change', null)
|
||||
} else if (result !== undefined && result._id !== value) {
|
||||
value = result._id
|
||||
dispatch('change', value)
|
||||
}
|
||||
mgr?.setFocusPos(focusIndex)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
$: hideIcon = size === 'x-large' || (size === 'large' && kind !== 'link')
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div {id} bind:this={container} class="min-w-0" class:w-full={width === '100%'} class:h-full={$$slots.content}>
|
||||
{#if $$slots.content}
|
||||
<div
|
||||
class="w-full h-full flex-streatch"
|
||||
on:click={_click}
|
||||
use:tooltip={selected !== undefined ? { label: getEmbeddedLabel(getName(selected)) } : undefined}
|
||||
>
|
||||
<slot name="content" />
|
||||
</div>
|
||||
{:else}
|
||||
<Button {focusIndex} width={width ?? 'min-content'} {size} {kind} {justify} {showTooltip} on:click={_click}>
|
||||
<span
|
||||
slot="content"
|
||||
class="overflow-label flex-grow"
|
||||
class:flex-between={showNavigate && selected}
|
||||
class:dark-color={value == null}
|
||||
>
|
||||
<div
|
||||
class="disabled"
|
||||
style:width={showNavigate && selected
|
||||
? `calc(${width ?? 'min-content'} - 1.5rem)`
|
||||
: `${width ?? 'min-content'}`}
|
||||
use:tooltip={selected !== undefined ? { label: getEmbeddedLabel(getName(selected)) } : undefined}
|
||||
>
|
||||
{#if selected}
|
||||
{#if hideIcon || selected}
|
||||
<UserInfo value={selected} size={kind === 'link' ? 'x-small' : 'tiny'} {icon} />
|
||||
{:else}
|
||||
{getName(selected)}
|
||||
{/if}
|
||||
{:else}
|
||||
<div class="flex-presenter">
|
||||
{#if icon}
|
||||
<div class="icon" class:small-gap={size === 'inline' || size === 'small'}>
|
||||
<Icon {icon} size={kind === 'link' ? 'small' : size} />
|
||||
</div>
|
||||
{/if}
|
||||
<div class="label no-underline">
|
||||
<Label {label} />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{#if selected && showNavigate}
|
||||
<ActionIcon
|
||||
icon={IconOpen}
|
||||
size={'small'}
|
||||
action={() => {
|
||||
if (selected) {
|
||||
showPanel(view.component.EditDoc, selected._id, selected._class, 'content')
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
</span>
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
285
packages/presentation/src/components/AssigneePopup.svelte
Normal file
285
packages/presentation/src/components/AssigneePopup.svelte
Normal file
@ -0,0 +1,285 @@
|
||||
<!--
|
||||
// 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, { Contact, Employee, EmployeeAccount, Person } from '@hcengineering/contact'
|
||||
import { Doc, DocumentQuery, FindOptions, getCurrentAccount, Ref } from '@hcengineering/core'
|
||||
import type { Asset, IntlString } from '@hcengineering/platform'
|
||||
import {
|
||||
createFocusManager,
|
||||
EditBox,
|
||||
FocusHandler,
|
||||
Icon,
|
||||
IconCheck,
|
||||
ListView,
|
||||
resizeObserver,
|
||||
AnySvelteComponent,
|
||||
Label,
|
||||
tooltip
|
||||
} from '@hcengineering/ui'
|
||||
import presentation, {
|
||||
AssigneeCategory,
|
||||
UserInfo,
|
||||
assigneeCategoryOrder,
|
||||
createQuery,
|
||||
getCategorytitle,
|
||||
getClient
|
||||
} from '..'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
|
||||
export let options: FindOptions<Contact> | undefined = undefined
|
||||
export let selected: Ref<Person> | undefined
|
||||
export let docQuery: DocumentQuery<Contact> | undefined = undefined
|
||||
export let prevAssigned: Ref<Employee>[] | undefined = []
|
||||
export let projectLead: Ref<Employee> | undefined = undefined
|
||||
export let projectMembers: Ref<Employee>[] | undefined = []
|
||||
export let titleDeselect: IntlString | undefined
|
||||
export let placeholder: IntlString = presentation.string.Search
|
||||
export let ignoreUsers: Ref<Person>[] = []
|
||||
export let shadows: boolean = true
|
||||
export let width: 'medium' | 'large' | 'full' = 'medium'
|
||||
export let size: 'small' | 'medium' | 'large' = 'small'
|
||||
export let searchField: string = 'name'
|
||||
export let showCategories: boolean = true
|
||||
export let icon: Asset | AnySvelteComponent | undefined = undefined
|
||||
|
||||
const client = getClient()
|
||||
const hierarchy = client.getHierarchy()
|
||||
const currentEmployee = (getCurrentAccount() as EmployeeAccount).employee
|
||||
|
||||
let search: string = ''
|
||||
let objects: Contact[] = []
|
||||
let contacts: Contact[] = []
|
||||
|
||||
let categorizedPersons: Map<Ref<Person>, AssigneeCategory>
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const query = createQuery()
|
||||
|
||||
$: query.query<Contact>(
|
||||
contact.class.Employee,
|
||||
{
|
||||
...(docQuery ?? {}),
|
||||
[searchField]: { $like: '%' + search + '%' },
|
||||
_id: { $nin: ignoreUsers }
|
||||
},
|
||||
(result) => {
|
||||
objects = result
|
||||
},
|
||||
{ ...(options ?? {}), limit: 200, sort: { name: 1 } }
|
||||
)
|
||||
|
||||
$: updateCategories(objects, currentEmployee, prevAssigned, projectLead, projectMembers)
|
||||
|
||||
function updateCategories (
|
||||
objects: Contact[],
|
||||
currentEmployee: Ref<Person>,
|
||||
prevAssigned: Ref<Person>[] | undefined,
|
||||
projectLead: Ref<Person> | undefined,
|
||||
projectMembers: Ref<Person>[] | undefined
|
||||
) {
|
||||
const persons = new Map<Ref<Person>, AssigneeCategory>(objects.map((t) => [t._id, 'Other']))
|
||||
if (projectLead) {
|
||||
persons.set(projectLead, 'ProjectLead')
|
||||
}
|
||||
projectMembers?.forEach((p) => persons.set(p, 'ProjectMembers'))
|
||||
prevAssigned?.forEach((p) => persons.set(p, 'PreviouslyAssigned'))
|
||||
if (selected) {
|
||||
persons.set(selected, 'Assigned')
|
||||
}
|
||||
persons.set(currentEmployee, 'CurrentUser')
|
||||
|
||||
categorizedPersons = new Map<Ref<Person>, AssigneeCategory>(
|
||||
[...persons].sort((a, b) => assigneeCategoryOrder.indexOf(a[1]) - assigneeCategoryOrder.indexOf(b[1]))
|
||||
)
|
||||
contacts = []
|
||||
categorizedPersons.forEach((p, k) => {
|
||||
const c = objects.find((e) => e._id === k)
|
||||
if (c) {
|
||||
contacts.push(c)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
let selection = 0
|
||||
let list: ListView
|
||||
|
||||
async function handleSelection (evt: Event | undefined, selection: number): Promise<void> {
|
||||
const person = contacts[selection]
|
||||
selected = person._id === selected ? undefined : person._id
|
||||
dispatch('close', selected !== undefined ? person : undefined)
|
||||
}
|
||||
|
||||
function onKeydown (key: KeyboardEvent): void {
|
||||
if (key.code === 'ArrowUp') {
|
||||
key.stopPropagation()
|
||||
key.preventDefault()
|
||||
list.select(selection - 1)
|
||||
}
|
||||
if (key.code === 'ArrowDown') {
|
||||
key.stopPropagation()
|
||||
key.preventDefault()
|
||||
list.select(selection + 1)
|
||||
}
|
||||
if (key.code === 'Enter') {
|
||||
key.preventDefault()
|
||||
key.stopPropagation()
|
||||
handleSelection(key, selection)
|
||||
}
|
||||
if (key.code === 'Escape') {
|
||||
key.preventDefault()
|
||||
key.stopPropagation()
|
||||
dispatch('close')
|
||||
}
|
||||
}
|
||||
const manager = createFocusManager()
|
||||
|
||||
function toAny (obj: any): any {
|
||||
return obj
|
||||
}
|
||||
|
||||
let selectedDiv: HTMLElement | undefined
|
||||
let scrollDiv: HTMLElement | undefined
|
||||
let cHeight = 0
|
||||
|
||||
const updateLocation = (scrollDiv?: HTMLElement, selectedDiv?: HTMLElement, objects?: Doc[], selected?: Ref<Doc>) => {
|
||||
const objIt = objects?.find((it) => it._id === selected)
|
||||
if (objIt === undefined) {
|
||||
cHeight = 0
|
||||
return
|
||||
}
|
||||
if (scrollDiv && selectedDiv) {
|
||||
const r = selectedDiv.getBoundingClientRect()
|
||||
const r2 = scrollDiv.getBoundingClientRect()
|
||||
|
||||
if (r && r2) {
|
||||
if (r.top > r2.top && r.bottom < r2.bottom) {
|
||||
cHeight = 0
|
||||
} else {
|
||||
if (r.bottom < r2.bottom) {
|
||||
cHeight = 1
|
||||
} else {
|
||||
cHeight = -1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$: updateLocation(scrollDiv, selectedDiv, contacts, selected)
|
||||
</script>
|
||||
|
||||
<FocusHandler {manager} />
|
||||
|
||||
<div
|
||||
class="selectPopup"
|
||||
class:full-width={width === 'full'}
|
||||
class:plainContainer={!shadows}
|
||||
class:width-40={width === 'large'}
|
||||
on:keydown={onKeydown}
|
||||
use:resizeObserver={() => {
|
||||
dispatch('changeContent')
|
||||
}}
|
||||
>
|
||||
<div class="header flex-between">
|
||||
<EditBox kind={'search-style'} focusIndex={1} focus bind:value={search} {placeholder} />
|
||||
</div>
|
||||
{#if cHeight === 1}
|
||||
<div class="background-theme-content-accent" style:height={'1px'} />
|
||||
{/if}
|
||||
<div
|
||||
class="scroll"
|
||||
on:scroll={() => updateLocation(scrollDiv, selectedDiv, contacts, selected)}
|
||||
bind:this={scrollDiv}
|
||||
>
|
||||
<div class="box">
|
||||
<ListView bind:this={list} count={contacts.length} bind:selection>
|
||||
<svelte:fragment slot="category" let:item>
|
||||
{#if showCategories}
|
||||
{@const obj = toAny(contacts[item])}
|
||||
{@const category = categorizedPersons.get(obj._id)}
|
||||
{@const cl = hierarchy.getClass(contacts[item]._class)}
|
||||
{#if item === 0 || (item > 0 && categorizedPersons.get(toAny(contacts[item - 1])._id) !== categorizedPersons.get(obj._id))}
|
||||
<!--Category for first item-->
|
||||
<div class="category-box">
|
||||
<div class="flex flex-grow overflow-label">
|
||||
<span class="fs-medium flex-center gap-2 mt-2 mb-2 ml-2">
|
||||
{#if cl.icon}
|
||||
<Icon icon={cl.icon} size={'small'} />
|
||||
{/if}
|
||||
<Label label={getCategorytitle(category)} />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="item" let:item>
|
||||
{@const obj = contacts[item]}
|
||||
<button
|
||||
class="menu-item w-full"
|
||||
class:background-bg-focused={obj._id === selected}
|
||||
class:border-radius-1={obj._id === selected}
|
||||
on:click={() => {
|
||||
handleSelection(undefined, item)
|
||||
}}
|
||||
>
|
||||
{#if selected}
|
||||
<div class="icon">
|
||||
{#if obj._id === selected}
|
||||
<div bind:this={selectedDiv}>
|
||||
{#if titleDeselect}
|
||||
<div class="clear-mins" use:tooltip={{ label: titleDeselect }}>
|
||||
<Icon icon={IconCheck} {size} />
|
||||
</div>
|
||||
{:else}
|
||||
<Icon icon={IconCheck} {size} />
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<span class="label">
|
||||
{#if obj._id === selected}
|
||||
<div bind:this={selectedDiv}>
|
||||
<div class="flex flex-grow overflow-label">
|
||||
<UserInfo size={'x-small'} value={obj} {icon} />
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex flex-grow overflow-label">
|
||||
<UserInfo size={'x-small'} value={obj} {icon} />
|
||||
</div>
|
||||
{/if}
|
||||
</span>
|
||||
</button>
|
||||
</svelte:fragment>
|
||||
</ListView>
|
||||
</div>
|
||||
</div>
|
||||
{#if cHeight === -1}
|
||||
<div class="background-theme-content-accent" style:height={'3px'} />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.plainContainer {
|
||||
color: var(--caption-color);
|
||||
background-color: var(--body-color);
|
||||
border: 1px solid var(--button-border-color);
|
||||
border-radius: 0.25rem;
|
||||
box-shadow: none;
|
||||
}
|
||||
</style>
|
@ -21,6 +21,7 @@ export { default as AttributeBarEditor } from './components/AttributeBarEditor.s
|
||||
export { default as AttributeEditor } from './components/AttributeEditor.svelte'
|
||||
export { default as AttributesBar } from './components/AttributesBar.svelte'
|
||||
export { default as Avatar } from './components/Avatar.svelte'
|
||||
export { default as AssigneeBox } from './components/AssigneeBox.svelte'
|
||||
export { default as Card } from './components/Card.svelte'
|
||||
export { default as CombineAvatars } from './components/CombineAvatars.svelte'
|
||||
export { default as EditableAvatar } from './components/EditableAvatar.svelte'
|
||||
@ -41,6 +42,7 @@ export { default as EmployeeBox } from './components/EmployeeBox.svelte'
|
||||
export { default as UsersPopup } from './components/UsersPopup.svelte'
|
||||
export { default as MembersBox } from './components/MembersBox.svelte'
|
||||
export { default as IconMembers } from './components/icons/Members.svelte'
|
||||
export { default as IconPerson } from './components/icons/Person.svelte'
|
||||
export { default as IconMembersOutline } from './components/icons/MembersOutline.svelte'
|
||||
export { default as ObjectSearchPopup } from './components/ObjectSearchPopup.svelte'
|
||||
export { default as IndexedDocumentPreview } from './components/IndexedDocumentPreview.svelte'
|
||||
|
@ -57,6 +57,12 @@ export default plugin(presentationId, {
|
||||
Edit: '' as IntlString,
|
||||
SelectAvatar: '' as IntlString,
|
||||
GravatarsManaged: '' as IntlString,
|
||||
CategoryCurrentUser: '' as IntlString,
|
||||
Assigned: '' as IntlString,
|
||||
CategoryPreviousAssigned: '' as IntlString,
|
||||
CategoryProjectLead: '' as IntlString,
|
||||
CategoryProjectMembers: '' as IntlString,
|
||||
CategoryOther: '' as IntlString,
|
||||
InltPropsValue: '' as IntlString
|
||||
},
|
||||
metadata: {
|
||||
|
@ -34,12 +34,13 @@ import core, {
|
||||
TxResult
|
||||
} from '@hcengineering/core'
|
||||
import login from '@hcengineering/login'
|
||||
import { getMetadata } from '@hcengineering/platform'
|
||||
import { getMetadata, IntlString } from '@hcengineering/platform'
|
||||
import { LiveQuery as LQ } from '@hcengineering/query'
|
||||
import { onDestroy } from 'svelte'
|
||||
import { deepEqual } from 'fast-equals'
|
||||
import { IconSize, DropdownIntlItem } from '@hcengineering/ui'
|
||||
import contact, { AvatarType, AvatarProvider } from '@hcengineering/contact'
|
||||
import presentation from '..'
|
||||
|
||||
let liveQuery: LQ
|
||||
let client: TxOperations
|
||||
@ -272,3 +273,43 @@ export function getAvatarProviderId (avatar?: string | null): Ref<AvatarProvider
|
||||
return contact.avatarProvider.Color
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export type AssigneeCategory =
|
||||
| 'CurrentUser'
|
||||
| 'Assigned'
|
||||
| 'PreviouslyAssigned'
|
||||
| 'ProjectLead'
|
||||
| 'ProjectMembers'
|
||||
| 'Other'
|
||||
|
||||
const assigneeCategoryTitleMap: Record<AssigneeCategory, IntlString> = Object.freeze({
|
||||
CurrentUser: presentation.string.CategoryCurrentUser,
|
||||
Assigned: presentation.string.Assigned,
|
||||
PreviouslyAssigned: presentation.string.CategoryPreviousAssigned,
|
||||
ProjectLead: presentation.string.CategoryProjectLead,
|
||||
ProjectMembers: presentation.string.CategoryProjectMembers,
|
||||
Other: presentation.string.CategoryOther
|
||||
})
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export const assigneeCategoryOrder: AssigneeCategory[] = [
|
||||
'CurrentUser',
|
||||
'Assigned',
|
||||
'PreviouslyAssigned',
|
||||
'ProjectLead',
|
||||
'ProjectMembers',
|
||||
'Other'
|
||||
]
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export function getCategorytitle (category: AssigneeCategory | undefined): IntlString {
|
||||
const cat: AssigneeCategory = category ?? 'Other'
|
||||
return assigneeCategoryTitleMap[cat]
|
||||
}
|
||||
|
@ -15,11 +15,12 @@
|
||||
<script lang="ts">
|
||||
import { Employee } from '@hcengineering/contact'
|
||||
import { AttachedData, Ref } from '@hcengineering/core'
|
||||
import { EmployeeBox, getClient } from '@hcengineering/presentation'
|
||||
import { AssigneeBox, getClient } from '@hcengineering/presentation'
|
||||
import { Issue, IssueTemplateData } from '@hcengineering/tracker'
|
||||
import { ButtonKind, ButtonSize, TooltipAlignment } from '@hcengineering/ui'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import tracker from '../../plugin'
|
||||
import { getPreviousAssignees } from '../../utils'
|
||||
|
||||
export let value: Issue | AttachedData<Issue> | IssueTemplateData
|
||||
export let size: ButtonSize = 'large'
|
||||
@ -30,6 +31,27 @@
|
||||
const client = getClient()
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
let prevAssigned: Ref<Employee>[] = []
|
||||
let projectLead: Ref<Employee> | undefined = undefined
|
||||
let projectMembers: Ref<Employee>[] = []
|
||||
|
||||
$: getPreviousAssignees(value).then((res) => {
|
||||
prevAssigned = res
|
||||
})
|
||||
|
||||
async function updateProjectMembers (issue: Issue | AttachedData<Issue> | IssueTemplateData) {
|
||||
if (issue.project) {
|
||||
const project = await client.findOne(tracker.class.Project, { _id: issue.project })
|
||||
projectLead = project?.lead || undefined
|
||||
projectMembers = project?.members || []
|
||||
} else {
|
||||
projectLead = undefined
|
||||
projectMembers = []
|
||||
}
|
||||
}
|
||||
|
||||
$: updateProjectMembers(value)
|
||||
|
||||
const handleAssigneeChanged = async (newAssignee: Ref<Employee> | undefined) => {
|
||||
if (newAssignee === undefined || value.assignee === newAssignee) {
|
||||
return
|
||||
@ -44,11 +66,13 @@
|
||||
</script>
|
||||
|
||||
{#if value}
|
||||
<EmployeeBox
|
||||
<AssigneeBox
|
||||
label={tracker.string.Assignee}
|
||||
placeholder={tracker.string.Assignee}
|
||||
value={value.assignee}
|
||||
allowDeselect
|
||||
{prevAssigned}
|
||||
{projectLead}
|
||||
{projectMembers}
|
||||
titleDeselect={tracker.string.Unassigned}
|
||||
{size}
|
||||
{kind}
|
||||
|
@ -15,7 +15,7 @@
|
||||
<script lang="ts">
|
||||
import contact from '@hcengineering/contact'
|
||||
import { Ref } from '@hcengineering/core'
|
||||
import { UserBox } from '@hcengineering/presentation'
|
||||
import { AssigneeBox } from '@hcengineering/presentation'
|
||||
import { Issue, Team } from '@hcengineering/tracker'
|
||||
import { deviceOptionsStore as deviceInfo, getEventPositionElement, ListView, showPopup } from '@hcengineering/ui'
|
||||
import { ContextMenu, FixedColumn, ListSelectionProvider } from '@hcengineering/view-resources'
|
||||
@ -60,21 +60,19 @@
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<FixedColumn key={'estimation_issue_assignee'} justify={'right'}>
|
||||
<AssigneeBox
|
||||
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>
|
||||
</svelte:fragment>
|
||||
</ListView>
|
||||
|
@ -13,7 +13,19 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import { Doc, DocumentQuery, Ref, SortingOrder, toIdMap, TxOperations, WithLookup } from '@hcengineering/core'
|
||||
import { Employee } from '@hcengineering/contact'
|
||||
import core, {
|
||||
AttachedData,
|
||||
Doc,
|
||||
DocumentQuery,
|
||||
Ref,
|
||||
SortingOrder,
|
||||
toIdMap,
|
||||
TxCollectionCUD,
|
||||
TxOperations,
|
||||
TxUpdateDoc,
|
||||
WithLookup
|
||||
} from '@hcengineering/core'
|
||||
import { TypeState } from '@hcengineering/kanban'
|
||||
import { Asset, IntlString, translate } from '@hcengineering/platform'
|
||||
import { createQuery } from '@hcengineering/presentation'
|
||||
@ -24,6 +36,7 @@ import {
|
||||
IssuesGrouping,
|
||||
IssuesOrdering,
|
||||
IssueStatus,
|
||||
IssueTemplateData,
|
||||
ProjectStatus,
|
||||
Sprint,
|
||||
SprintStatus,
|
||||
@ -592,3 +605,25 @@ export function subIssueListProvider (subIssues: Issue[], target: Ref<Issue>): v
|
||||
listProvider.updateFocus(selectedIssue)
|
||||
}
|
||||
}
|
||||
|
||||
export async function getPreviousAssignees (
|
||||
issue: Issue | AttachedData<Issue> | IssueTemplateData
|
||||
): Promise<Array<Ref<Employee>>> {
|
||||
return await new Promise((resolve) => {
|
||||
const query = createQuery(true)
|
||||
query.query(
|
||||
core.class.Tx,
|
||||
{
|
||||
'tx.objectId': (issue as Issue)._id,
|
||||
'tx.operations.assignee': { $exists: true }
|
||||
},
|
||||
(res) => {
|
||||
const prevAssignee = res
|
||||
.map((t) => ((t as TxCollectionCUD<Doc, Issue>).tx as TxUpdateDoc<Issue>).operations.assignee)
|
||||
.filter((p) => !(p == null)) as Array<Ref<Employee>>
|
||||
resolve(prevAssignee)
|
||||
query.unsubscribe()
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user