Rank order (#2150)

Signed-off-by: Dvinyanin Alexandr <dvinyanin.alexandr@gmail.com>
This commit is contained in:
Alex 2022-06-28 13:53:39 +07:00 committed by GitHub
parent cade773b95
commit 1e21c85379
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 86 additions and 42 deletions

View File

@ -6,6 +6,9 @@ HR:
- Allow to change assignee in Kanban - Allow to change assignee in Kanban
Tracker:
- Manual issues ordering
## 0.6.29 ## 0.6.29
Platform: Platform:

View File

@ -494,7 +494,7 @@ export function createModel (builder: Builder): void {
id: issuesId, id: issuesId,
label: tracker.string.Issues, label: tracker.string.Issues,
icon: tracker.icon.Issues, icon: tracker.icon.Issues,
component: tracker.component.IssuesView, component: tracker.component.Issues,
componentProps: { componentProps: {
title: tracker.string.Issues title: tracker.string.Issues
} }

View File

@ -27,7 +27,7 @@
export let states: TypeState[] = [] export let states: TypeState[] = []
export let query: DocumentQuery<Item> = {} export let query: DocumentQuery<Item> = {}
export let fieldName: string export let fieldName: string
export let rankFieldName: string export let rankFieldName: string | undefined
export let selection: number | undefined = undefined export let selection: number | undefined = undefined
export let checked: Doc[] = [] export let checked: Doc[] = []
@ -57,7 +57,10 @@
dragItem?: Item // required for svelte to properly recalculate state. dragItem?: Item // required for svelte to properly recalculate state.
): ExtItem[] { ): ExtItem[] {
const stateCards = objects.filter((it) => (it as any)[fieldName] === state._id) const stateCards = objects.filter((it) => (it as any)[fieldName] === state._id)
stateCards.sort((a, b) => (a as any)[rankFieldName]?.localeCompare((b as any)[rankFieldName])) if (rankFieldName !== undefined) {
const sortField = rankFieldName
stateCards.sort((a, b) => (a as any)[sortField]?.localeCompare((b as any)[sortField]))
}
return stateCards.map((it, idx, arr) => ({ return stateCards.map((it, idx, arr) => ({
it, it,
prev: arr[idx - 1], prev: arr[idx - 1],
@ -96,14 +99,13 @@
} }
} }
const dragCardRank = (dragCard as any)[rankFieldName] if (rankFieldName !== undefined && dragCardInitialRank !== (dragCard as any)[rankFieldName]) {
if (dragCardInitialRank !== (dragCard as any)[rankFieldName]) { const dragCardRank = (dragCard as any)[rankFieldName]
updates = { updates = {
...updates, ...updates,
[rankFieldName]: dragCardRank [rankFieldName]: dragCardRank
} }
} }
if (Object.keys(updates).length > 0) { if (Object.keys(updates).length > 0) {
await updateItem(dragCard, updates) await updateItem(dragCard, updates)
} }
@ -113,7 +115,7 @@
const client = getClient() const client = getClient()
let dragCard: Item | undefined let dragCard: Item | undefined
let dragCardInitialRank: string let dragCardInitialRank: string | undefined
let dragCardInitialState: StateType let dragCardInitialState: StateType
let isDragging = false let isDragging = false
@ -144,23 +146,26 @@
if (card !== undefined && card[fieldName] !== state._id) { if (card !== undefined && card[fieldName] !== state._id) {
card[fieldName] = state._id card[fieldName] = state._id
const objs = getStateObjects(objects, state) const objs = getStateObjects(objects, state)
card[rankFieldName] = calcRank(objs[objs.length - 1]?.it, undefined) if (rankFieldName !== undefined) card[rankFieldName] = calcRank(objs[objs.length - 1]?.it, undefined)
} }
} }
function cardDragOver (evt: CardDragEvent, object: ExtItem): void { function cardDragOver (evt: CardDragEvent, object: ExtItem): void {
if (dragCard !== undefined) { if (dragCard !== undefined) {
;(dragCard as any)[rankFieldName] = doCalcRank(object, evt) ;(dragCard as any)[fieldName] = (dragCard as any)[fieldName]
if (rankFieldName !== undefined) {
;(dragCard as any)[rankFieldName] = doCalcRank(object, evt)
}
} }
} }
function cardDrop (evt: CardDragEvent, object: ExtItem): void { function cardDrop (evt: CardDragEvent, object: ExtItem): void {
if (dragCard !== undefined) { if (dragCard !== undefined && rankFieldName !== undefined) {
;(dragCard as any)[rankFieldName] = doCalcRank(object, evt) ;(dragCard as any)[rankFieldName] = doCalcRank(object, evt)
} }
isDragging = false isDragging = false
} }
function onDragStart (object: ExtItem, state: TypeState): void { function onDragStart (object: ExtItem, state: TypeState): void {
dragCardInitialState = state._id dragCardInitialState = state._id
dragCardInitialRank = (object.it as any)[rankFieldName] dragCardInitialRank = rankFieldName === undefined ? undefined : (object.it as any)[rankFieldName]
dragCard = object.it dragCard = object.it
isDragging = true isDragging = true
dispatch('obj-focus', object.it) dispatch('obj-focus', object.it)

View File

@ -106,6 +106,7 @@
"NoAssignee": "No assignee", "NoAssignee": "No assignee",
"LastUpdated": "Last updated", "LastUpdated": "Last updated",
"DueDate": "Due date", "DueDate": "Due date",
"Manual": "Manual",
"All": "All", "All": "All",
"PastWeek": "Past week", "PastWeek": "Past week",
"PastMonth": "Past month", "PastMonth": "Past month",

View File

@ -106,6 +106,7 @@
"NoAssignee": "Нет исполнителя", "NoAssignee": "Нет исполнителя",
"LastUpdated": "Последнее обновление", "LastUpdated": "Последнее обновление",
"DueDate": "Срок", "DueDate": "Срок",
"Manual": "Пользовательский",
"All": "Все", "All": "Все",
"PastWeek": "Предыдущая неделя", "PastWeek": "Предыдущая неделя",
"PastMonth": "Предыдущий месяц", "PastMonth": "Предыдущий месяц",

View File

@ -25,7 +25,10 @@
let query: DocumentQuery<Issue> let query: DocumentQuery<Issue>
$: statusQuery.query( $: statusQuery.query(
tracker.class.IssueStatus, tracker.class.IssueStatus,
{ category: { $in: [tracker.issueStatusCategory.Unstarted, tracker.issueStatusCategory.Started] } }, {
category: { $in: [tracker.issueStatusCategory.Unstarted, tracker.issueStatusCategory.Started] },
space: currentSpace
},
(result) => { (result) => {
query = { status: { $in: result.map(({ _id }) => _id) }, space: currentSpace } query = { status: { $in: result.map(({ _id }) => _id) }, space: currentSpace }
} }

View File

@ -23,9 +23,13 @@
const statusQuery = createQuery() const statusQuery = createQuery()
let query: DocumentQuery<Issue> = {} let query: DocumentQuery<Issue> = {}
$: statusQuery.query(tracker.class.IssueStatus, { category: tracker.issueStatusCategory.Backlog }, (result) => { $: statusQuery.query(
query = { status: { $in: result.map(({ _id }) => _id) }, space: currentSpace } tracker.class.IssueStatus,
}) { category: tracker.issueStatusCategory.Backlog, space: currentSpace },
(result) => {
query = { status: { $in: result.map(({ _id }) => _id) }, space: currentSpace }
}
)
</script> </script>
<IssuesView {query} title={tracker.string.BacklogIssues} /> <IssuesView {query} title={tracker.string.BacklogIssues} />

View File

@ -0,0 +1,26 @@
<!--
// 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 { Ref } from '@anticrm/core'
import { Team } from '@anticrm/tracker'
import tracker from '../../plugin'
import IssuesView from './IssuesView.svelte'
export let currentSpace: Ref<Team>
$: query = { space: currentSpace }
</script>
<IssuesView {query} title={tracker.string.ActiveIssues} />

View File

@ -14,18 +14,18 @@
--> -->
<script lang="ts"> <script lang="ts">
import contact from '@anticrm/contact' import contact from '@anticrm/contact'
import { Class, Doc, DocumentQuery, FindOptions, Ref, SortingOrder, WithLookup } from '@anticrm/core' import { Class, Doc, DocumentQuery, Lookup, Ref, SortingOrder, WithLookup } from '@anticrm/core'
import { Kanban, TypeState } from '@anticrm/kanban' import { Kanban } from '@anticrm/kanban'
import notification from '@anticrm/notification' import notification from '@anticrm/notification'
import { createQuery, getClient } from '@anticrm/presentation' import { createQuery, getClient } from '@anticrm/presentation'
import { Issue, IssuesGrouping, IssueStatus, Team, ViewOptions } from '@anticrm/tracker' import { Issue, IssuesGrouping, IssuesOrdering, IssueStatus, Team, ViewOptions } from '@anticrm/tracker'
import { Button, Component, Icon, IconAdd, showPanel, showPopup, Tooltip } from '@anticrm/ui' import { Button, Component, Icon, IconAdd, showPanel, showPopup, Tooltip } from '@anticrm/ui'
import { focusStore, ListSelectionProvider, SelectDirection, selectionStore } from '@anticrm/view-resources' import { focusStore, ListSelectionProvider, SelectDirection, selectionStore } from '@anticrm/view-resources'
import ActionContext from '@anticrm/view-resources/src/components/ActionContext.svelte' import ActionContext from '@anticrm/view-resources/src/components/ActionContext.svelte'
import Menu from '@anticrm/view-resources/src/components/Menu.svelte' import Menu from '@anticrm/view-resources/src/components/Menu.svelte'
import { onMount } from 'svelte' import { onMount } from 'svelte'
import tracker from '../../plugin' import tracker from '../../plugin'
import { getKanbanStatuses } from '../../utils' import { getKanbanStatuses, issuesSortOrderMap } from '../../utils'
import CreateIssue from '../CreateIssue.svelte' import CreateIssue from '../CreateIssue.svelte'
import ProjectEditor from '../projects/ProjectEditor.svelte' import ProjectEditor from '../projects/ProjectEditor.svelte'
import AssigneePresenter from './AssigneePresenter.svelte' import AssigneePresenter from './AssigneePresenter.svelte'
@ -40,7 +40,9 @@
export let query: DocumentQuery<Issue> = {} export let query: DocumentQuery<Issue> = {}
$: currentSpace = typeof query.space === 'string' ? query.space : tracker.team.DefaultTeam $: currentSpace = typeof query.space === 'string' ? query.space : tracker.team.DefaultTeam
$: ({ groupBy, shouldShowEmptyGroups, shouldShowSubIssues } = viewOptions) $: ({ groupBy, orderBy, shouldShowEmptyGroups, shouldShowSubIssues } = viewOptions)
$: sort = { [orderBy]: issuesSortOrderMap[orderBy] }
$: rankFieldName = orderBy === IssuesOrdering.Manual ? orderBy : undefined
$: resultQuery = { $: resultQuery = {
...(shouldShowSubIssues ? {} : { attachedTo: tracker.ids.NoParent }), ...(shouldShowSubIssues ? {} : { attachedTo: tracker.ids.NoParent }),
space: currentSpace, space: currentSpace,
@ -57,17 +59,10 @@
}) })
let issueStatuses: WithLookup<IssueStatus>[] | undefined let issueStatuses: WithLookup<IssueStatus>[] | undefined
let states: TypeState[] | undefined
$: statusesQuery.query( $: statusesQuery.query(
tracker.class.IssueStatus, tracker.class.IssueStatus,
{ attachedTo: currentSpace }, { attachedTo: currentSpace },
(is) => { (is) => {
states = is.map((status) => ({
_id: status._id,
title: status.name,
color: status.color ?? status.$lookup?.category?.color ?? 0,
icon: status.$lookup?.category?.icon ?? undefined
}))
issueStatuses = is issueStatuses = is
}, },
{ {
@ -80,13 +75,11 @@
return object as WithLookup<Issue> return object as WithLookup<Issue>
} }
const options: FindOptions<Issue> = { const lookup: Lookup<Issue> = {
lookup: { assignee: contact.class.Employee,
assignee: contact.class.Employee, space: tracker.class.Team,
space: tracker.class.Team, _id: {
_id: { subIssues: tracker.class.Issue
subIssues: tracker.class.Issue
}
} }
} }
@ -124,10 +117,10 @@
_class={tracker.class.Issue} _class={tracker.class.Issue}
search="" search=""
{states} {states}
{options} options={{ sort, lookup }}
query={resultQuery} query={resultQuery}
fieldName={groupBy} fieldName={groupBy}
rankFieldName={'rank'} {rankFieldName}
on:content={(evt) => { on:content={(evt) => {
listProvider.update(evt.detail) listProvider.update(evt.detail)
}} }}

View File

@ -18,6 +18,7 @@ import { Resources } from '@anticrm/platform'
import { ObjectSearchResult } from '@anticrm/presentation' import { ObjectSearchResult } from '@anticrm/presentation'
import { Issue, Team } from '@anticrm/tracker' import { Issue, Team } from '@anticrm/tracker'
import Inbox from './components/inbox/Inbox.svelte' import Inbox from './components/inbox/Inbox.svelte'
import Issues from './components/issues/Issues.svelte'
import Active from './components/issues/Active.svelte' import Active from './components/issues/Active.svelte'
import AssigneePresenter from './components/issues/AssigneePresenter.svelte' import AssigneePresenter from './components/issues/AssigneePresenter.svelte'
import Backlog from './components/issues/Backlog.svelte' import Backlog from './components/issues/Backlog.svelte'
@ -111,6 +112,7 @@ export async function queryIssue<D extends Issue> (
export default async (): Promise<Resources> => ({ export default async (): Promise<Resources> => ({
component: { component: {
NopeComponent, NopeComponent,
Issues,
Active, Active,
Backlog, Backlog,
Inbox, Inbox,

View File

@ -122,6 +122,7 @@ export default mergeIds(trackerId, tracker, {
NoAssignee: '' as IntlString, NoAssignee: '' as IntlString,
LastUpdated: '' as IntlString, LastUpdated: '' as IntlString,
DueDate: '' as IntlString, DueDate: '' as IntlString,
Manual: '' as IntlString,
All: '' as IntlString, All: '' as IntlString,
PastWeek: '' as IntlString, PastWeek: '' as IntlString,
PastMonth: '' as IntlString, PastMonth: '' as IntlString,
@ -179,6 +180,7 @@ export default mergeIds(trackerId, tracker, {
Inbox: '' as AnyComponent, Inbox: '' as AnyComponent,
MyIssues: '' as AnyComponent, MyIssues: '' as AnyComponent,
Views: '' as AnyComponent, Views: '' as AnyComponent,
Issues: '' as AnyComponent,
Active: '' as AnyComponent, Active: '' as AnyComponent,
Backlog: '' as AnyComponent, Backlog: '' as AnyComponent,
Projects: '' as AnyComponent, Projects: '' as AnyComponent,

View File

@ -43,7 +43,8 @@ export const issuesOrderByOptions: Record<IssuesOrdering, IntlString> = {
[IssuesOrdering.Status]: tracker.string.Status, [IssuesOrdering.Status]: tracker.string.Status,
[IssuesOrdering.Priority]: tracker.string.Priority, [IssuesOrdering.Priority]: tracker.string.Priority,
[IssuesOrdering.LastUpdated]: tracker.string.LastUpdated, [IssuesOrdering.LastUpdated]: tracker.string.LastUpdated,
[IssuesOrdering.DueDate]: tracker.string.DueDate [IssuesOrdering.DueDate]: tracker.string.DueDate,
[IssuesOrdering.Manual]: tracker.string.Manual
} }
export const issuesDateModificationPeriodOptions: Record<IssuesDateModificationPeriod, IntlString> = { export const issuesDateModificationPeriodOptions: Record<IssuesDateModificationPeriod, IntlString> = {

View File

@ -54,7 +54,7 @@ export interface Selection {
} }
export type IssuesGroupByKeys = keyof Pick<Issue, 'status' | 'priority' | 'assignee' | 'project'> export type IssuesGroupByKeys = keyof Pick<Issue, 'status' | 'priority' | 'assignee' | 'project'>
export type IssuesOrderByKeys = keyof Pick<Issue, 'status' | 'priority' | 'modifiedOn' | 'dueDate'> export type IssuesOrderByKeys = keyof Pick<Issue, 'status' | 'priority' | 'modifiedOn' | 'dueDate' | 'rank'>
export const issuesGroupKeyMap: Record<IssuesGrouping, IssuesGroupByKeys | undefined> = { export const issuesGroupKeyMap: Record<IssuesGrouping, IssuesGroupByKeys | undefined> = {
[IssuesGrouping.Status]: 'status', [IssuesGrouping.Status]: 'status',
@ -68,14 +68,16 @@ export const issuesOrderKeyMap: Record<IssuesOrdering, IssuesOrderByKeys> = {
[IssuesOrdering.Status]: 'status', [IssuesOrdering.Status]: 'status',
[IssuesOrdering.Priority]: 'priority', [IssuesOrdering.Priority]: 'priority',
[IssuesOrdering.LastUpdated]: 'modifiedOn', [IssuesOrdering.LastUpdated]: 'modifiedOn',
[IssuesOrdering.DueDate]: 'dueDate' [IssuesOrdering.DueDate]: 'dueDate',
[IssuesOrdering.Manual]: 'rank'
} }
export const issuesSortOrderMap: Record<IssuesOrderByKeys, SortingOrder> = { export const issuesSortOrderMap: Record<IssuesOrderByKeys, SortingOrder> = {
status: SortingOrder.Ascending, status: SortingOrder.Ascending,
priority: SortingOrder.Ascending, priority: SortingOrder.Ascending,
modifiedOn: SortingOrder.Descending, modifiedOn: SortingOrder.Descending,
dueDate: SortingOrder.Descending dueDate: SortingOrder.Descending,
rank: SortingOrder.Ascending
} }
export const issuesGroupEditorMap: Record<'status' | 'priority' | 'project', AnyComponent | undefined> = { export const issuesGroupEditorMap: Record<'status' | 'priority' | 'project', AnyComponent | undefined> = {

View File

@ -82,8 +82,9 @@ export enum IssuesGrouping {
export enum IssuesOrdering { export enum IssuesOrdering {
Status = 'status', Status = 'status',
Priority = 'priority', Priority = 'priority',
LastUpdated = 'lastUpdated', LastUpdated = 'modifiedOn',
DueDate = 'dueDate' DueDate = 'dueDate',
Manual = 'rank'
} }
/** /**