From 619801aaca1fc94538c2f58e0898f416d511b3d3 Mon Sep 17 00:00:00 2001 From: Alex <41288429+Dvinyanin@users.noreply.github.com> Date: Wed, 15 Jun 2022 11:59:43 +0700 Subject: [PATCH] Add kanban view (#2071) Signed-off-by: Dvinyanin Alexandr --- changelog.md | 1 + models/tracker/package.json | 3 +- models/tracker/src/index.ts | 24 ++- models/tracker/src/plugin.ts | 3 +- .../src/components/issues/KanbanView.svelte | 187 ++++++++++++++++++ .../components/issues/ViewOptionsPopup.svelte | 2 +- plugins/tracker-resources/src/index.ts | 3 +- plugins/tracker-resources/src/plugin.ts | 1 + plugins/tracker-resources/src/utils.ts | 116 ++++++++++- tests/sanity/tests/tracker.spec.ts | 20 +- 10 files changed, 346 insertions(+), 14 deletions(-) create mode 100644 plugins/tracker-resources/src/components/issues/KanbanView.svelte diff --git a/changelog.md b/changelog.md index cda2f87c7a..44312d9be9 100644 --- a/changelog.md +++ b/changelog.md @@ -13,6 +13,7 @@ Leads: Tracker: - Attachments support +- Board view ## 0.6.26 diff --git a/models/tracker/package.json b/models/tracker/package.json index 76db6f6477..2c046c9a99 100644 --- a/models/tracker/package.json +++ b/models/tracker/package.json @@ -42,6 +42,7 @@ "@anticrm/workbench": "~0.6.1", "@anticrm/view": "~0.6.0", "@anticrm/model-presentation": "~0.6.0", - "@anticrm/setting": "~0.6.1" + "@anticrm/setting": "~0.6.1", + "@anticrm/task": "~0.6.0" } } diff --git a/models/tracker/src/index.ts b/models/tracker/src/index.ts index 06700c9ce7..2af3329244 100644 --- a/models/tracker/src/index.ts +++ b/models/tracker/src/index.ts @@ -39,6 +39,7 @@ import workbench, { createNavigateAction } from '@anticrm/model-workbench' import notification from '@anticrm/notification' import { Asset, IntlString } from '@anticrm/platform' import setting from '@anticrm/setting' +import task from '@anticrm/task' import { Document, Issue, @@ -307,6 +308,23 @@ export function createModel (builder: Builder): void { tracker.viewlet.List ) + builder.createDoc(view.class.Viewlet, core.space.Model, { + attachTo: tracker.class.Issue, + descriptor: tracker.viewlet.Kanban, + config: [] + }) + + builder.createDoc( + view.class.ViewletDescriptor, + core.space.Model, + { + label: tracker.string.Board, + icon: task.icon.Kanban, + component: tracker.component.KanbanView + }, + tracker.viewlet.Kanban + ) + builder.createDoc( tracker.class.IssueStatusCategory, core.space.Model, @@ -469,12 +487,6 @@ export function createModel (builder: Builder): void { // icon: tracker.icon.TrackerApplication, component: tracker.component.Backlog }, - { - id: boardId, - label: tracker.string.Board, - // icon: tracker.icon.TrackerApplication, - component: tracker.component.Board - }, { id: projectsId, label: tracker.string.Projects, diff --git a/models/tracker/src/plugin.ts b/models/tracker/src/plugin.ts index f2555df9da..6b9aa2b618 100644 --- a/models/tracker/src/plugin.ts +++ b/models/tracker/src/plugin.ts @@ -46,7 +46,8 @@ export default mergeIds(trackerId, tracker, { Tracker: '' as Ref }, viewlet: { - List: '' as Ref + List: '' as Ref, + Kanban: '' as Ref }, completion: { IssueQuery: '' as Resource, diff --git a/plugins/tracker-resources/src/components/issues/KanbanView.svelte b/plugins/tracker-resources/src/components/issues/KanbanView.svelte new file mode 100644 index 0000000000..3cabd20b2d --- /dev/null +++ b/plugins/tracker-resources/src/components/issues/KanbanView.svelte @@ -0,0 +1,187 @@ + + + +{#await getKanbanStatuses(client, groupBy, resultQuery, shouldShowEmptyGroups) then states} + + { + listProvider.update(evt.detail) + }} + on:obj-focus={(evt) => { + listProvider.updateFocus(evt.detail) + }} + selection={listProvider.current($focusStore)} + checked={$selectionStore ?? []} + on:check={(evt) => { + listProvider.updateSelection(evt.detail.docs, evt.detail.value) + }} + on:contextmenu={(evt) => showMenu(evt.detail.evt, evt.detail.objects)} + > + +
+
+
+ + {state.title} + {count} +
+ {#if groupBy === IssuesGrouping.Status} +
+ +
+ {/if} +
+
+
+ + {@const issue = toIssue(object)} +
+
+ + + {object.title} + +
+
+ +
+
+ +
+
+
+
+{/await} + + diff --git a/plugins/tracker-resources/src/components/issues/ViewOptionsPopup.svelte b/plugins/tracker-resources/src/components/issues/ViewOptionsPopup.svelte index 2bc618b86c..331f0f3878 100644 --- a/plugins/tracker-resources/src/components/issues/ViewOptionsPopup.svelte +++ b/plugins/tracker-resources/src/components/issues/ViewOptionsPopup.svelte @@ -94,7 +94,7 @@
- {#if _groupBy === IssuesGrouping.Status || _groupBy === IssuesGrouping.Priority} + {#if _groupBy === IssuesGrouping.Status}
diff --git a/plugins/tracker-resources/src/index.ts b/plugins/tracker-resources/src/index.ts index 22f30dc46a..6f9744e50f 100644 --- a/plugins/tracker-resources/src/index.ts +++ b/plugins/tracker-resources/src/index.ts @@ -54,6 +54,7 @@ import TargetDatePresenter from './components/projects/TargetDatePresenter.svelt import SetDueDateActionPopup from './components/SetDueDateActionPopup.svelte' import SetParentIssueActionPopup from './components/SetParentIssueActionPopup.svelte' import Views from './components/views/Views.svelte' +import KanbanView from './components/issues/KanbanView.svelte' import tracker from './plugin' import { getIssueId } from './utils' @@ -76,7 +77,6 @@ export async function queryIssue ( ) ).map((e) => [e._id, e]) ) - for (const currentTeam of teams) { const nids: number[] = [] for (let n = 0; n < currentTeam.sequence; n++) { @@ -143,6 +143,7 @@ export default async (): Promise => ({ EditProject, IssuesView, ListView, + KanbanView, IssuePreview }, function: { diff --git a/plugins/tracker-resources/src/plugin.ts b/plugins/tracker-resources/src/plugin.ts index 62889e0e38..8a6cd6ae14 100644 --- a/plugins/tracker-resources/src/plugin.ts +++ b/plugins/tracker-resources/src/plugin.ts @@ -194,6 +194,7 @@ export default mergeIds(trackerId, tracker, { EditProject: '' as AnyComponent, IssuesView: '' as AnyComponent, ListView: '' as AnyComponent, + KanbanView: '' as AnyComponent, IssuePreview: '' as AnyComponent }, function: { diff --git a/plugins/tracker-resources/src/utils.ts b/plugins/tracker-resources/src/utils.ts index 7e9f3dd567..03b0fd04f7 100644 --- a/plugins/tracker-resources/src/utils.ts +++ b/plugins/tracker-resources/src/utils.ts @@ -13,9 +13,9 @@ // limitations under the License. // -import { Employee, formatName } from '@anticrm/contact' -import { DocumentQuery, Ref, SortingOrder } from '@anticrm/core' -import type { Asset, IntlString } from '@anticrm/platform' +import contact, { Employee, formatName } from '@anticrm/contact' +import { DocumentQuery, Ref, SortingOrder, TxOperations } from '@anticrm/core' +import { Asset, IntlString, translate } from '@anticrm/platform' import { IssuePriority, Team, @@ -27,6 +27,7 @@ import { IssueStatus } from '@anticrm/tracker' import { AnyComponent, AnySvelteComponent, getMillisecondsInMonth, MILLISECONDS_IN_WEEK } from '@anticrm/ui' +import { TypeState } from '@anticrm/kanban' import tracker from './plugin' export interface NavigationItem { @@ -392,3 +393,112 @@ export function getCategories ( export function getIssueId (team: Team, issue: Issue): string { return `${team.identifier}-${issue.number}` } + +export async function getKanbanStatuses ( + client: TxOperations, + groupBy: IssuesGrouping, + issueQuery: DocumentQuery, + shouldShowEmptyGroups: boolean +): Promise { + if (groupBy === IssuesGrouping.Status && shouldShowEmptyGroups) { + return ( + await client.findAll( + tracker.class.IssueStatus, + { attachedTo: issueQuery.space }, + { + lookup: { category: tracker.class.IssueStatusCategory }, + sort: { rank: SortingOrder.Ascending } + } + ) + ).map((status) => ({ + _id: status._id, + title: status.name, + color: status.color ?? status.$lookup?.category?.color ?? 0, + icon: status.$lookup?.category?.icon ?? undefined + })) + } + if (groupBy === IssuesGrouping.NoGrouping) return [] + if (groupBy === IssuesGrouping.Priority) { + const issues = await client.findAll(tracker.class.Issue, issueQuery, { + sort: { priority: SortingOrder.Ascending } + }) + const states = issues.reduce((result, issue) => { + const { priority } = issue + if (result.find(({ _id }) => _id === priority) !== undefined) return result + return [ + ...result, + { + _id: priority, + title: issuePriorities[priority].label, + color: 0, + icon: issuePriorities[priority].icon + } + ] + }, []) + await Promise.all( + states.map(async (state) => { + state.title = await translate(state.title as IntlString, {}) + }) + ) + return states + } + if (groupBy === IssuesGrouping.Status) { + const issues = await client.findAll(tracker.class.Issue, issueQuery, { + lookup: { status: [tracker.class.IssueStatus, { category: tracker.class.IssueStatusCategory }] }, + sort: { '$lookup.status.rank': SortingOrder.Ascending } + }) + return issues.reduce((result, issue) => { + const status = issue.$lookup?.status + if (status === undefined || result.find(({ _id }) => _id === status._id) !== undefined) return result + const icon = '$lookup' in status ? status.$lookup?.category?.icon : undefined + return [ + ...result, + { + _id: status._id, + title: status.name, + color: status.color ?? 0, + icon + } + ] + }, []) + } + if (groupBy === IssuesGrouping.Assignee) { + const issues = await client.findAll(tracker.class.Issue, issueQuery, { + lookup: { assignee: contact.class.Employee }, + sort: { '$lookup.assignee.name': SortingOrder.Ascending } + }) + const noAssignee = await translate(tracker.string.NoAssignee, {}) + return issues.reduce((result, issue) => { + if (result.find(({ _id }) => _id === issue.assignee) !== undefined) return result + return [ + ...result, + { + _id: issue.assignee, + title: issue.$lookup?.assignee?.name ?? noAssignee, + color: 0, + icon: undefined + } + ] + }, []) + } + if (groupBy === IssuesGrouping.Project) { + const issues = await client.findAll(tracker.class.Issue, issueQuery, { + lookup: { project: tracker.class.Project }, + sort: { '$lookup.project.label': SortingOrder.Ascending } + }) + const noProject = await translate(tracker.string.NoProject, {}) + return issues.reduce((result, issue) => { + if (result.find(({ _id }) => _id === issue.project) !== undefined) return result + return [ + ...result, + { + _id: issue.project, + title: issue.$lookup?.project?.label ?? noProject, + color: 0, + icon: undefined + } + ] + }, []) + } + return [] +} diff --git a/tests/sanity/tests/tracker.spec.ts b/tests/sanity/tests/tracker.spec.ts index 8104d0e5c9..bb28bd2f0e 100644 --- a/tests/sanity/tests/tracker.spec.ts +++ b/tests/sanity/tests/tracker.spec.ts @@ -1,5 +1,5 @@ import { test, expect } from '@playwright/test' -import { PlatformSetting, PlatformURI } from './utils' +import { generateId, PlatformSetting, PlatformURI } from './utils' test.use({ storageState: PlatformSetting }) @@ -32,3 +32,21 @@ test('create-issue-and-sub-issue', async ({ page }) => { await page.click('button:has-text("Save")') await page.click('span.name:text("sub-issue")') }) + +test('use-kanban', async ({ page }) => { + await page.goto(`${PlatformURI}/workbench%3Acomponent%3AWorkbenchApp`) + await page.click('[id="app-tracker\\:string\\:TrackerApplication"]') + await expect(page).toHaveURL(`${PlatformURI}/workbench%3Acomponent%3AWorkbenchApp/tracker%3Aapp%3ATracker`) + + const issueName = 'issue-' + generateId(5) + await page.click('button:has-text("New issue")') + await page.click('[placeholder="Issue\\ title"]') + await page.fill('[placeholder="Issue\\ title"]', issueName) + await page.click('button:has-text("Backlog")') + await page.click('button:has-text("In Progress")') + await page.click('button:has-text("Save issue")') + + await page.locator('text="Issues"').click() + await page.click('[name="tooltip-tracker:string:Board"]') + await expect(page.locator('.panel-container:has-text("In Progress")')).toContainText(issueName) +})