mirror of
https://github.com/hcengineering/platform.git
synced 2024-12-23 03:22:19 +03:00
Refactor Issues (#2037)
Signed-off-by: Dvinyanin Alexandr <dvinyanin.alexandr@gmail.com>
This commit is contained in:
parent
a9392e1921
commit
6a7fa08fbd
@ -34,10 +34,10 @@ import {
|
||||
import attachment from '@anticrm/model-attachment'
|
||||
import chunter from '@anticrm/model-chunter'
|
||||
import core, { DOMAIN_SPACE, TAttachedDoc, TDoc, TSpace, TType } from '@anticrm/model-core'
|
||||
import { createAction } from '@anticrm/model-view'
|
||||
import view, { createAction } from '@anticrm/model-view'
|
||||
import { KeyBinding } from '@anticrm/view'
|
||||
import workbench, { createNavigateAction } from '@anticrm/model-workbench'
|
||||
import { Asset, IntlString } from '@anticrm/platform'
|
||||
import view, { KeyBinding } from '@anticrm/view'
|
||||
import setting from '@anticrm/setting'
|
||||
import {
|
||||
Document,
|
||||
@ -249,6 +249,40 @@ export class TProject extends TDoc implements Project {
|
||||
export function createModel (builder: Builder): void {
|
||||
builder.createModel(TTeam, TProject, TIssue, TIssueStatus, TIssueStatusCategory, TTypeIssuePriority)
|
||||
|
||||
builder.createDoc(view.class.Viewlet, core.space.Model, {
|
||||
attachTo: tracker.class.Issue,
|
||||
descriptor: tracker.viewlet.List,
|
||||
config: [
|
||||
{ key: '', presenter: tracker.component.PriorityEditor },
|
||||
{ key: '', presenter: tracker.component.IssuePresenter },
|
||||
{ key: '', presenter: tracker.component.StatusEditor },
|
||||
{ key: '', presenter: tracker.component.TitlePresenter, props: { shouldUseMargin: true } },
|
||||
{ key: '', presenter: tracker.component.DueDatePresenter },
|
||||
{
|
||||
key: '',
|
||||
presenter: tracker.component.ProjectEditor,
|
||||
props: { kind: 'secondary', size: 'small', shape: 'round', shouldShowPlaceholder: false }
|
||||
},
|
||||
{ key: 'modifiedOn', presenter: tracker.component.ModificationDatePresenter },
|
||||
{
|
||||
key: '$lookup.assignee',
|
||||
presenter: tracker.component.AssigneePresenter,
|
||||
props: { defaultClass: contact.class.Employee, shouldShowLabel: false }
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
builder.createDoc(
|
||||
view.class.ViewletDescriptor,
|
||||
core.space.Model,
|
||||
{
|
||||
label: view.string.Table,
|
||||
icon: view.icon.Table,
|
||||
component: tracker.component.ListView
|
||||
},
|
||||
tracker.viewlet.List
|
||||
)
|
||||
|
||||
builder.createDoc(
|
||||
tracker.class.IssueStatusCategory,
|
||||
core.space.Model,
|
||||
@ -387,7 +421,10 @@ export function createModel (builder: Builder): void {
|
||||
id: issuesId,
|
||||
label: tracker.string.Issues,
|
||||
icon: tracker.icon.Issues,
|
||||
component: tracker.component.Issues
|
||||
component: tracker.component.IssuesView,
|
||||
componentProps: {
|
||||
title: tracker.string.Issues
|
||||
}
|
||||
},
|
||||
{
|
||||
id: activeId,
|
||||
@ -504,4 +541,8 @@ export function createModel (builder: Builder): void {
|
||||
},
|
||||
tracker.action.SetParent
|
||||
)
|
||||
|
||||
builder.mixin(tracker.class.Issue, core.class.Class, view.mixin.ClassFilters, {
|
||||
filters: ['status', 'priority', 'project']
|
||||
})
|
||||
}
|
||||
|
@ -19,6 +19,7 @@ import { IntlString, mergeIds } from '@anticrm/platform'
|
||||
import { Team, trackerId } from '@anticrm/tracker'
|
||||
import tracker from '@anticrm/tracker-resources/src/plugin'
|
||||
import type { AnyComponent } from '@anticrm/ui'
|
||||
import { ViewletDescriptor } from '@anticrm/view'
|
||||
import { Application } from '@anticrm/workbench'
|
||||
|
||||
export default mergeIds(trackerId, tracker, {
|
||||
@ -41,5 +42,8 @@ export default mergeIds(trackerId, tracker, {
|
||||
},
|
||||
app: {
|
||||
Tracker: '' as Ref<Application>
|
||||
},
|
||||
viewlet: {
|
||||
List: '' as Ref<ViewletDescriptor>
|
||||
}
|
||||
})
|
||||
|
@ -0,0 +1,31 @@
|
||||
<script lang="ts">
|
||||
import type { Ref, WithLookup } from '@anticrm/core'
|
||||
import { Component } from '@anticrm/ui'
|
||||
import { BuildModelKey, Viewlet } from '@anticrm/view'
|
||||
import { IssuesDateModificationPeriod, IssuesGrouping, IssuesOrdering, Team } from '@anticrm/tracker'
|
||||
|
||||
export let currentSpace: Ref<Team>
|
||||
export let viewlet: WithLookup<Viewlet> | undefined
|
||||
export let config: (string | BuildModelKey)[] | undefined = undefined
|
||||
export let query = {}
|
||||
export let viewOptions: {
|
||||
groupBy: IssuesGrouping
|
||||
orderBy: IssuesOrdering
|
||||
completedIssuesPeriod: IssuesDateModificationPeriod
|
||||
shouldShowEmptyGroups: boolean
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if viewlet?.$lookup?.descriptor?.component}
|
||||
<Component
|
||||
is={viewlet.$lookup?.descriptor?.component}
|
||||
props={{
|
||||
currentSpace,
|
||||
config: config ?? viewlet.config,
|
||||
options: viewlet.options,
|
||||
viewlet,
|
||||
query,
|
||||
viewOptions
|
||||
}}
|
||||
/>
|
||||
{/if}
|
@ -0,0 +1,59 @@
|
||||
<script lang="ts">
|
||||
import { Ref, WithLookup } from '@anticrm/core'
|
||||
import { IssuesDateModificationPeriod, IssuesGrouping, IssuesOrdering, Team } from '@anticrm/tracker'
|
||||
import { Button, Icon, Tooltip, IconOptions, showPopup, eventToHTMLElement } from '@anticrm/ui'
|
||||
import { Filter, Viewlet } from '@anticrm/view'
|
||||
import { FilterButton } from '@anticrm/view-resources'
|
||||
import tracker from '../../plugin'
|
||||
import ViewOptionsPopup from './ViewOptionsPopup.svelte'
|
||||
|
||||
export let currentSpace: Ref<Team>
|
||||
export let viewlet: WithLookup<Viewlet> | undefined
|
||||
export let viewlets: WithLookup<Viewlet>[] = []
|
||||
export let label: string
|
||||
export let filters: Filter[] = []
|
||||
export let viewOptions: {
|
||||
groupBy: IssuesGrouping
|
||||
orderBy: IssuesOrdering
|
||||
completedIssuesPeriod: IssuesDateModificationPeriod
|
||||
shouldShowEmptyGroups: boolean
|
||||
}
|
||||
|
||||
const handleOptionsEditorOpened = (event: MouseEvent) => {
|
||||
if (!currentSpace) {
|
||||
return
|
||||
}
|
||||
|
||||
showPopup(ViewOptionsPopup, viewOptions, eventToHTMLElement(event), undefined, (result) => {
|
||||
if (result) viewOptions = { ...result }
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="ac-header full">
|
||||
<div class="ac-header__wrap-title">
|
||||
<div class="ac-header__icon"><Icon icon={tracker.icon.Issues} size={'small'} /></div>
|
||||
<span class="ac-header__title">{label}</span>
|
||||
<div class="ml-4"><FilterButton _class={tracker.class.Issue} bind:filters /></div>
|
||||
</div>
|
||||
{#if viewlets.length > 1}
|
||||
<div class="flex">
|
||||
{#each viewlets as v}
|
||||
<Tooltip label={v.$lookup?.descriptor?.label} direction={'top'}>
|
||||
<button
|
||||
class="ac-header__icon-button"
|
||||
class:selected={viewlet?._id === v._id}
|
||||
on:click={() => {
|
||||
viewlet = v
|
||||
}}
|
||||
>
|
||||
{#if v.$lookup?.descriptor?.icon}
|
||||
<Icon icon={v.$lookup?.descriptor?.icon} size={'small'} />
|
||||
{/if}
|
||||
</button>
|
||||
</Tooltip>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
<Button icon={IconOptions} kind={'link'} on:click={handleOptionsEditorOpened} />
|
||||
</div>
|
@ -0,0 +1,68 @@
|
||||
<script lang="ts">
|
||||
import core, { Ref, Space, WithLookup } from '@anticrm/core'
|
||||
import { getClient } from '@anticrm/presentation'
|
||||
import view, { Filter, Viewlet } from '@anticrm/view'
|
||||
import IssuesContent from './IssuesContent.svelte'
|
||||
import IssuesHeader from './IssuesHeader.svelte'
|
||||
import { IssuesDateModificationPeriod, IssuesGrouping, IssuesOrdering, Team } from '@anticrm/tracker'
|
||||
import tracker from '../../plugin'
|
||||
import { IntlString, translate } from '@anticrm/platform'
|
||||
import { FilterBar } from '@anticrm/view-resources'
|
||||
|
||||
export let currentSpace: Ref<Team> | undefined
|
||||
export let query = {}
|
||||
export let title: IntlString | undefined = undefined
|
||||
export let label: string = ''
|
||||
|
||||
let viewlet: WithLookup<Viewlet> | undefined = undefined
|
||||
let filters: Filter[]
|
||||
let viewOptions = {
|
||||
groupBy: IssuesGrouping.Status,
|
||||
orderBy: IssuesOrdering.Status,
|
||||
completedIssuesPeriod: IssuesDateModificationPeriod.All,
|
||||
shouldShowEmptyGroups: false
|
||||
}
|
||||
let resultQuery = {}
|
||||
|
||||
const client = getClient()
|
||||
|
||||
let viewlets: WithLookup<Viewlet>[] = []
|
||||
|
||||
$: update(currentSpace)
|
||||
|
||||
async function update (currentSpace?: Ref<Space>): Promise<void> {
|
||||
const space = await client.findOne(core.class.Space, { _id: currentSpace })
|
||||
if (space) {
|
||||
viewlets = await client.findAll(
|
||||
view.class.Viewlet,
|
||||
{ attachTo: tracker.class.Issue },
|
||||
{
|
||||
lookup: {
|
||||
descriptor: view.class.ViewletDescriptor
|
||||
}
|
||||
}
|
||||
)
|
||||
;[viewlet] = viewlets
|
||||
}
|
||||
}
|
||||
$: if (!label && title) {
|
||||
translate(title, {}).then((res) => {
|
||||
label = res
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if currentSpace}
|
||||
<IssuesHeader {currentSpace} {viewlets} {label} bind:viewlet bind:viewOptions bind:filters />
|
||||
<FilterBar _class={tracker.class.Issue} {query} bind:filters on:change={(e) => (resultQuery = e.detail)} />
|
||||
<div class="flex h-full">
|
||||
<div class="antiPanel-component">
|
||||
<IssuesContent {currentSpace} {viewlet} query={resultQuery} {viewOptions} />
|
||||
</div>
|
||||
{#if $$slots.aside !== undefined}
|
||||
<div class="antiPanel-component aside border-left">
|
||||
<slot name="aside" />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
@ -0,0 +1,85 @@
|
||||
<script lang="ts">
|
||||
import { ScrollBox } from '@anticrm/ui'
|
||||
import IssuesListBrowser from './IssuesListBrowser.svelte'
|
||||
import tracker from '../../plugin'
|
||||
import {
|
||||
Issue,
|
||||
IssuesDateModificationPeriod,
|
||||
IssuesGrouping,
|
||||
IssuesOrdering,
|
||||
IssueStatus,
|
||||
Team
|
||||
} from '@anticrm/tracker'
|
||||
import { Class, Doc, Ref, SortingOrder, WithLookup } from '@anticrm/core'
|
||||
import {
|
||||
getCategories,
|
||||
groupBy as groupByFunc,
|
||||
issuesGroupKeyMap,
|
||||
issuesOrderKeyMap,
|
||||
issuesSortOrderMap
|
||||
} from '../../utils'
|
||||
import { createQuery } from '@anticrm/presentation'
|
||||
import contact, { Employee } from '@anticrm/contact'
|
||||
import { BuildModelKey } from '@anticrm/view'
|
||||
|
||||
export let _class: Ref<Class<Doc>>
|
||||
export let currentSpace: Ref<Team>
|
||||
export let config: (string | BuildModelKey)[]
|
||||
export let query = {}
|
||||
export let viewOptions: {
|
||||
groupBy: IssuesGrouping
|
||||
orderBy: IssuesOrdering
|
||||
completedIssuesPeriod: IssuesDateModificationPeriod
|
||||
shouldShowEmptyGroups: boolean
|
||||
}
|
||||
|
||||
$: ({ groupBy, orderBy, shouldShowEmptyGroups } = viewOptions)
|
||||
$: groupByKey = issuesGroupKeyMap[groupBy]
|
||||
$: orderByKey = issuesOrderKeyMap[orderBy]
|
||||
|
||||
const statusesQuery = createQuery()
|
||||
let statuses: IssueStatus[] = []
|
||||
$: statusesQuery.query(
|
||||
tracker.class.IssueStatus,
|
||||
{ attachedTo: currentSpace },
|
||||
(result) => {
|
||||
statuses = result
|
||||
},
|
||||
{
|
||||
lookup: { category: tracker.class.IssueStatusCategory },
|
||||
sort: { rank: SortingOrder.Ascending }
|
||||
}
|
||||
)
|
||||
$: groupedIssues = groupByFunc(issues, groupBy)
|
||||
$: categories = getCategories(groupByKey, issues, !!shouldShowEmptyGroups, statuses, employees)
|
||||
$: employees = issues.map((x) => x.$lookup?.assignee).filter(Boolean) as Employee[]
|
||||
|
||||
const issuesQuery = createQuery()
|
||||
let issues: WithLookup<Issue>[] = []
|
||||
$: issuesQuery.query(
|
||||
tracker.class.Issue,
|
||||
query,
|
||||
(result) => {
|
||||
issues = result
|
||||
},
|
||||
{
|
||||
sort: { [orderByKey]: issuesSortOrderMap[orderByKey] },
|
||||
limit: 200,
|
||||
lookup: { assignee: contact.class.Employee, status: tracker.class.IssueStatus }
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
<ScrollBox vertical stretch>
|
||||
<IssuesListBrowser
|
||||
{_class}
|
||||
{currentSpace}
|
||||
{groupByKey}
|
||||
orderBy={orderByKey}
|
||||
{statuses}
|
||||
{employees}
|
||||
{categories}
|
||||
itemsConfig={config}
|
||||
{groupedIssues}
|
||||
/>
|
||||
</ScrollBox>
|
@ -14,7 +14,7 @@
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { AttachedData, Ref, WithLookup } from '@anticrm/core'
|
||||
import { AttachedData, Ref, SortingOrder, WithLookup } from '@anticrm/core'
|
||||
import { Issue, IssueStatus } from '@anticrm/tracker'
|
||||
import { getClient } from '@anticrm/presentation'
|
||||
import { Tooltip, TooltipAlignment } from '@anticrm/ui'
|
||||
@ -48,9 +48,18 @@
|
||||
await client.update(value, { status: newStatus })
|
||||
}
|
||||
}
|
||||
$: query = '_id' in value ? { atachedTo: value.space } : {}
|
||||
client
|
||||
.findAll(tracker.class.IssueStatus, query, {
|
||||
lookup: { category: tracker.class.IssueStatusCategory },
|
||||
sort: { order: SortingOrder.Ascending }
|
||||
})
|
||||
.then((result) => {
|
||||
if (!statuses) statuses = result
|
||||
})
|
||||
</script>
|
||||
|
||||
{#if value}
|
||||
{#if value && statuses}
|
||||
{#if isEditable}
|
||||
<Tooltip label={tracker.string.SetStatus} direction={tooltipAlignment} fill={tooltipFill}>
|
||||
<StatusSelector
|
||||
|
@ -5,7 +5,7 @@
|
||||
import { Project } from '@anticrm/tracker'
|
||||
import { EditBox, getCurrentLocation } from '@anticrm/ui'
|
||||
import { DocAttributeBar } from '@anticrm/view-resources'
|
||||
import Issues from '../issues/Issues.svelte'
|
||||
import IssuesView from '../issues/IssuesView.svelte'
|
||||
|
||||
import tracker from '../../plugin'
|
||||
|
||||
@ -26,7 +26,7 @@
|
||||
</script>
|
||||
|
||||
{#if object}
|
||||
<Issues currentSpace={object.space} query={{ project }} label={object.label}>
|
||||
<IssuesView currentSpace={object.space} query={{ project }} label={object.label}>
|
||||
<svelte:fragment slot="aside">
|
||||
<div class="flex-row p-4">
|
||||
<div class="fs-title text-xl">
|
||||
@ -44,5 +44,5 @@
|
||||
<DocAttributeBar {object} mixins={[]} ignoreKeys={['icon', 'label', 'description']} />
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</Issues>
|
||||
</IssuesView>
|
||||
{/if}
|
||||
|
@ -49,6 +49,8 @@ import EditProject from './components/projects/EditProject.svelte'
|
||||
import ModificationDatePresenter from './components/issues/ModificationDatePresenter.svelte'
|
||||
import EditIssue from './components/issues/edit/EditIssue.svelte'
|
||||
import NewIssueHeader from './components/NewIssueHeader.svelte'
|
||||
import ListView from './components/issues/ListView.svelte'
|
||||
import IssuesView from './components/issues/IssuesView.svelte'
|
||||
|
||||
export default async (): Promise<Resources> => ({
|
||||
component: {
|
||||
@ -83,7 +85,9 @@ export default async (): Promise<Resources> => ({
|
||||
ProjectStatusPresenter,
|
||||
SetDueDateActionPopup,
|
||||
SetParentIssueActionPopup,
|
||||
EditProject
|
||||
EditProject,
|
||||
IssuesView,
|
||||
ListView
|
||||
},
|
||||
function: {
|
||||
ProjectVisible: () => false
|
||||
|
@ -189,7 +189,9 @@ export default mergeIds(trackerId, tracker, {
|
||||
ProjectStatusPresenter: '' as AnyComponent,
|
||||
SetDueDateActionPopup: '' as AnyComponent,
|
||||
SetParentIssueActionPopup: '' as AnyComponent,
|
||||
EditProject: '' as AnyComponent
|
||||
EditProject: '' as AnyComponent,
|
||||
IssuesView: '' as AnyComponent,
|
||||
ListView: '' as AnyComponent
|
||||
},
|
||||
function: {
|
||||
ProjectVisible: '' as '' as Resource<(spaces: Space[]) => boolean>
|
||||
|
@ -13,6 +13,7 @@
|
||||
// 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 {
|
||||
@ -22,7 +23,8 @@ import {
|
||||
IssuesOrdering,
|
||||
Issue,
|
||||
IssuesDateModificationPeriod,
|
||||
ProjectStatus
|
||||
ProjectStatus,
|
||||
IssueStatus
|
||||
} from '@anticrm/tracker'
|
||||
import { AnyComponent, AnySvelteComponent, getMillisecondsInMonth, MILLISECONDS_IN_WEEK } from '@anticrm/ui'
|
||||
import tracker from './plugin'
|
||||
@ -294,3 +296,85 @@ export const projectsTitleMap: Record<ProjectsViewMode, IntlString> = Object.fre
|
||||
active: tracker.string.ActiveProjects,
|
||||
closed: tracker.string.ClosedProjects
|
||||
})
|
||||
|
||||
export function getCategories (
|
||||
key: IssuesGroupByKeys | undefined,
|
||||
elements: Issue[],
|
||||
shouldShowAll: boolean,
|
||||
statuses: IssueStatus[],
|
||||
employees: Employee[]
|
||||
): any[] {
|
||||
if (key === undefined) {
|
||||
return [undefined] // No grouping
|
||||
}
|
||||
|
||||
const defaultStatuses = Object.values(statuses).map((x) => x._id)
|
||||
|
||||
const existingCategories = Array.from(
|
||||
new Set(
|
||||
elements.map((x) => {
|
||||
return x[key]
|
||||
})
|
||||
)
|
||||
)
|
||||
|
||||
if (shouldShowAll) {
|
||||
if (key === 'status') {
|
||||
return defaultStatuses
|
||||
}
|
||||
|
||||
if (key === 'priority') {
|
||||
return defaultPriorities
|
||||
}
|
||||
}
|
||||
|
||||
if (key === 'status') {
|
||||
existingCategories.sort((s1, s2) => {
|
||||
const i1 = defaultStatuses.findIndex((x) => x === s1)
|
||||
const i2 = defaultStatuses.findIndex((x) => x === s2)
|
||||
|
||||
return i1 - i2
|
||||
})
|
||||
}
|
||||
|
||||
if (key === 'priority') {
|
||||
existingCategories.sort((p1, p2) => {
|
||||
const i1 = defaultPriorities.findIndex((x) => x === p1)
|
||||
const i2 = defaultPriorities.findIndex((x) => x === p2)
|
||||
|
||||
return i1 - i2
|
||||
})
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user