// // Copyright © 2020, 2021 Anticrm Platform Contributors. // Copyright © 2021 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. // import core, { getCurrentAccount, toIdMap, type Attribute, type Class, type Doc, type DocumentQuery, type IdMap, type Ref, type Status, type TxOperations } from '@hcengineering/core' import { type IntlString, type Resources } from '@hcengineering/platform' import { createQuery, getClient } from '@hcengineering/presentation' import task, { getStatusIndex, makeRank, type Project, type ProjectType, type Rank, type Task, type TaskType } from '@hcengineering/task' import { getCurrentLocation, navigate, showPopup } from '@hcengineering/ui' import { type ViewletDescriptor } from '@hcengineering/view' import { CategoryQuery, groupBy, statusStore } from '@hcengineering/view-resources' import { get, writable } from 'svelte/store' import { type Employee, type PersonAccount } from '@hcengineering/contact' import activity from '@hcengineering/activity' import chunter from '@hcengineering/chunter' import AssignedTasks from './components/AssignedTasks.svelte' import Dashboard from './components/Dashboard.svelte' import DueDateEditor from './components/DueDateEditor.svelte' import KanbanTemplatePresenter from './components/KanbanTemplatePresenter.svelte' import StatusFilter from './components/StatusFilter.svelte' import StatusSelector from './components/StatusSelector.svelte' import StatusTableView from './components/StatusTableView.svelte' import TaskHeader from './components/TaskHeader.svelte' import TaskPresenter from './components/TaskPresenter.svelte' import TemplatesIcon from './components/TemplatesIcon.svelte' import TypesView from './components/TypesView.svelte' import KanbanView from './components/kanban/KanbanView.svelte' import ProjectTypePresenter from './components/projectTypes/ProjectTypePresenter.svelte' import CreateStatePopup from './components/state/CreateStatePopup.svelte' import StateEditor from './components/state/StateEditor.svelte' import StateIconPresenter from './components/state/StateIconPresenter.svelte' import StatePresenter from './components/state/StatePresenter.svelte' import StateRefPresenter from './components/state/StateRefPresenter.svelte' import TypeStatesPopup from './components/state/TypeStatesPopup.svelte' import ProjectTypeClassPresenter from './components/taskTypes/ProjectTypeClassPresenter.svelte' import TaskKindSelector from './components/taskTypes/TaskKindSelector.svelte' import TaskTypeClassPresenter from './components/taskTypes/TaskTypeClassPresenter.svelte' import TaskTypePresenter from './components/taskTypes/TaskTypePresenter.svelte' import ProjectTypeSelector from './components/projectTypes/ProjectTypeSelector.svelte' import CreateProjectType from './components/projectTypes/CreateProjectType.svelte' import ProjectTypeGeneralSectionEditor from './components/projectTypes/ProjectTypeGeneralSectionEditor.svelte' import ProjectTypeTasksTypeSectionEditor from './components/projectTypes/ProjectTypeTasksTypeSectionEditor.svelte' import ProjectTypeAutomationsSectionEditor from './components/projectTypes/ProjectTypeAutomationsSectionEditor.svelte' import ProjectTypeCollectionsSectionEditor from './components/projectTypes/ProjectTypeCollectionsSectionEditor.svelte' import TaskTypeEditor from './components/taskTypes/TaskTypeEditor.svelte' import { employeeByIdStore, personAccountByIdStore, personByIdStore } from '@hcengineering/contact-resources' export { default as AssigneePresenter } from './components/AssigneePresenter.svelte' export { default as TypeSelector } from './components/TypeSelector.svelte' export * from './utils' export { StatePresenter, StateRefPresenter, TaskKindSelector, TypeStatesPopup } async function editStatuses (object: Project, ev: Event): Promise { const loc = getCurrentLocation() loc.path[2] = 'setting' loc.path[3] = 'spaceTypes' loc.path[4] = object.type navigate(loc) } async function exportTasks (docs: Task | Task[]): Promise { const client = getClient() const ddocs = Array.isArray(docs) ? docs : [docs] const docsStatuses = ddocs.map((doc) => doc.status) const statuses = await client.findAll(core.class.Status, { _id: { $in: docsStatuses } }) const personAccountById = get(personAccountByIdStore) const personById = get(personByIdStore) const employeeById = get(employeeByIdStore) const statusMap = toIdMap(statuses) const activityMessages = await client.findAll(activity.class.ActivityMessage, { _class: chunter.class.ChatMessage, attachedToClass: { $in: ddocs.map((d) => d._class) }, attachedTo: { $in: ddocs.map((d) => d._id) } }) const activityByDoc = groupBy(activityMessages, 'attachedTo') const toExport = ddocs.map((d) => { const statusName = statusMap.get(d.status)?.name ?? d.status const createdByAccount = personAccountById.get(d.createdBy as Ref)?.person const modeifedByAccount = personAccountById.get(d.modifiedBy as Ref)?.person const createdBy = personById.get(createdByAccount as Ref)?.name ?? d.createdBy const modifiedBy = personById.get(modeifedByAccount as Ref)?.name ?? d.modifiedBy const assignee = employeeById.get(d.assignee as Ref)?.name ?? d.assignee const collaborators = ((d as any)['notification:mixin:Collaborators']?.collaborators ?? []).map( (id: Ref) => { const personAccount = personAccountById.get(id)?.person return personAccount !== undefined ? personById.get(personAccount)?.name ?? id : id } ) const activityForDoc = (activityByDoc[d._id] ?? []).map((act) => { const activityCreatedByAccount = personAccountById.get(act.createdBy as Ref)?.person const activityModifiedByAccount = personAccountById.get(act.modifiedBy as Ref)?.person const activitycreatedBy = employeeById.get((activityCreatedByAccount as any as Ref) ?? ('' as Ref))?.name ?? act.createdBy const activitymodifiedBy = employeeById.get((activityModifiedByAccount as any as Ref) ?? ('' as Ref))?.name ?? act.modifiedBy return { ...act, createdBy: activitycreatedBy, modifiedBy: activitymodifiedBy } }) return { ...d, status: statusName, createdBy, modifiedBy, assignee, 'notification:mixin:Collaborators': { collaborators }, activity: activityForDoc } }) const filename = 'tasks' + new Date().toLocaleDateString() + '.json' const link = document.createElement('a') link.style.display = 'none' link.setAttribute('target', '_blank') link.setAttribute( 'href', 'data:application/json;charset=utf-8,%EF%BB%BF' + encodeURIComponent(JSON.stringify(toExport)) ) link.setAttribute('download', filename) document.body.appendChild(link) link.click() document.body.removeChild(link) } async function selectStatus ( doc: Task | Task[], ev: any, props: { ofAttribute: Ref> placeholder: IntlString _class: Ref> } ): Promise { showPopup( StatusSelector, { value: doc, ofAttribute: props.ofAttribute, _class: props._class, placeholder: props.placeholder }, 'top' ) } export type StatesBarPosition = 'start' | 'middle' | 'end' | undefined export default async (): Promise => ({ component: { TaskPresenter, KanbanTemplatePresenter, Dashboard, KanbanView, StatePresenter, StateEditor, StatusTableView, TaskHeader, AssignedTasks, StateRefPresenter, DueDateEditor, CreateStatePopup, StatusSelector, TemplatesIcon, TypesView, StateIconPresenter, StatusFilter, TaskTypePresenter, TaskTypeClassPresenter, ProjectTypeClassPresenter, ProjectTypePresenter, ProjectTypeSelector, CreateProjectType, ProjectTypeGeneralSectionEditor, ProjectTypeTasksTypeSectionEditor, ProjectTypeAutomationsSectionEditor, ProjectTypeCollectionsSectionEditor, TaskTypeEditor }, actionImpl: { EditStatuses: editStatuses, SelectStatus: selectStatus, ExportTasks: exportTasks }, function: { GetAllStates: getAllStates, StatusSort: statusSort } }) export async function getAllStates ( query: DocumentQuery | undefined, onUpdate: () => void, queryId: Ref, attr: Attribute, filterDone: boolean = true ): Promise { const typeId = get(selectedTypeStore) const type = typeId !== undefined ? get(typeStore).get(typeId) : undefined const taskTypeId = get(selectedTaskTypeStore) if (taskTypeId !== undefined) { const taskType = get(taskTypeStore).get(taskTypeId) if (taskType === undefined) { return [] } if (type !== undefined) { const statusMap = get(statusStore).byId const statuses = (taskType.statuses.map((p) => statusMap.get(p)) as Status[]) ?? [] if (filterDone) { return statuses .filter((p) => p?.category !== task.statusCategory.Lost && p?.category !== task.statusCategory.Won) .map((p) => p?._id) } else { return statuses.map((p) => p?._id) } } const _space = query?.space if (_space !== undefined) { const promise = new Promise>>((resolve, reject) => { let refresh: boolean = false const lq = CategoryQuery.getLiveQuery(queryId) refresh = lq.query(task.class.Project, { _id: _space as Ref }, (res) => { const statusMap = get(statusStore).byId const statuses = (taskType.statuses.map((p) => statusMap.get(p)) as Status[]) ?? [] let result: Array> = [] if (filterDone) { result = statuses .filter((p) => p?.category !== task.statusCategory.Lost && p?.category !== task.statusCategory.Won) .map((p) => p?._id) } else { result = statuses.map((p) => p?._id) } CategoryQuery.results.set(queryId, result) resolve(result) onUpdate() }) if (!refresh) { resolve(CategoryQuery.results.get(queryId) ?? []) } }) return await promise } } else if (type !== undefined) { const statusMap = get(statusStore).byId const statuses = (type.statuses.map((p) => statusMap.get(p._id)) as Status[]) ?? [] if (filterDone) { return statuses .filter((p) => p?.category !== task.statusCategory.Lost && p?.category !== task.statusCategory.Won) .map((p) => p?._id) } else { return statuses.map((p) => p?._id) } } const joinedProjectsTypes = get(typesOfJoinedProjectsStore) ?? [] const includedStatuses = Array.from(get(taskTypeStore).values()) .filter((taskType) => joinedProjectsTypes.includes(taskType.parent)) .flatMap((taskType) => taskType.statuses) const allStates = get(statusStore).array.filter((p) => p.ofAttribute === attr._id && includedStatuses.includes(p._id)) if (filterDone) { return allStates .filter((p) => p?.category !== task.statusCategory.Lost && p?.category !== task.statusCategory.Won) .map((p) => p?._id) } else { return allStates.map((p) => p?._id) } } async function statusSort ( client: TxOperations, value: Array>, space: Ref | undefined, viewletDescriptorId?: Ref ): Promise>> { const typeId = get(selectedTypeStore) const type = typeId !== undefined ? get(typeStore).get(typeId) : undefined const statuses = get(statusStore).byId const taskTypes = get(taskTypeStore) if (type !== undefined) { value.sort((a, b) => { const aVal = statuses.get(a) as Status const bVal = statuses.get(b) as Status if (type != null) { const aIndex = getStatusIndex(type, taskTypes, a) const bIndex = getStatusIndex(type, taskTypes, b) return aIndex - bIndex } else if (aVal != null && bVal != null) { return aVal.name.localeCompare(bVal.name) } else { return 0 } }) } else { const res = new Map, Rank>() let prevRank: Rank | undefined const types = await client.findAll(task.class.ProjectType, {}) for (const state of value) { if (res.has(state)) continue const index = types.findIndex((p) => p.tasks.some((q) => taskTypes.get(q)?.statuses.includes(state))) if (index === -1) continue const type = types.splice(index, 1)[0] const statuses = type.tasks.map((it) => taskTypes.get(it)).find((it) => it?.statuses.includes(state))?.statuses ?? [] // TODO: Check correctness for (let index = 0; index < statuses.length; index++) { const st = statuses[index] const prev = index > 0 ? res.get(statuses[index - 1]) : prevRank const next = index < statuses.length - 1 ? res.get(statuses[index + 1]) : undefined const rank = makeRank(prev, next) res.set(st, rank) prevRank = rank } } const result: Array<{ _id: Ref rank: Rank }> = [] for (const [key, value] of res.entries()) { result.push({ _id: key, rank: value }) } result.sort((a, b) => a.rank.localeCompare(b.rank)) return result.filter((p) => value.includes(p._id)).map((p) => p._id) } return value } export const typeStore = writable>(new Map()) export const taskTypeStore = writable>(new Map()) export const typesOfJoinedProjectsStore = writable>>() export const joinedProjectsStore = writable() function fillStores (): void { const client = getClient() if (client !== undefined) { const query = createQuery(true) query.query(task.class.ProjectType, {}, (res) => { typeStore.set(toIdMap(res)) }) const taskQuery = createQuery(true) taskQuery.query(task.class.TaskType, {}, (res) => { taskTypeStore.set(toIdMap(res)) }) const projectQuery = createQuery(true) projectQuery.query(task.class.Project, { members: getCurrentAccount()._id }, (res) => { typesOfJoinedProjectsStore.set(res.map((r) => r.type).filter((it, idx, arr) => arr.indexOf(it) === idx)) joinedProjectsStore.set(res) }) } else { setTimeout(() => { fillStores() }, 50) } } fillStores() export const selectedTypeStore = writable | undefined>(undefined) export const selectedTaskTypeStore = writable | undefined>(undefined) export const activeProjects = writable>(new Map()) const activeProjectsQuery = createQuery(true) activeProjectsQuery.query(task.class.Project, { archived: false }, (projects) => { activeProjects.set(toIdMap(projects)) })