UBERF-4248: Task type (#4042)

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
Andrey Sobolev
2023-12-14 23:26:02 +07:00
committed by GitHub
parent 58b841518a
commit 19d6764250
196 changed files with 4690 additions and 2524 deletions
@@ -33,7 +33,8 @@
getClient
} from '@hcengineering/presentation'
import tags, { TagElement, TagReference } from '@hcengineering/tags'
import { calcRank } from '@hcengineering/task'
import { TaskType, calcRank } from '@hcengineering/task'
import { TaskKindSelector } from '@hcengineering/task-resources'
import {
Component as ComponentType,
Issue,
@@ -62,9 +63,8 @@
import { createEventDispatcher, onDestroy } from 'svelte'
import { activeComponent, activeMilestone, generateIssueShortLink, getIssueId, updateIssueRelation } from '../issues'
import tracker from '../plugin'
import ComponentSelector from './components/ComponentSelector.svelte'
import SetParentIssueActionPopup from './SetParentIssueActionPopup.svelte'
import SubIssues from './SubIssues.svelte'
import ComponentSelector from './components/ComponentSelector.svelte'
import AssigneeEditor from './issues/AssigneeEditor.svelte'
import IssueNotification from './issues/IssueNotification.svelte'
import ParentIssue from './issues/ParentIssue.svelte'
@@ -104,18 +104,24 @@
const parentQuery = createQuery()
let _space = draft?.space ?? space
let project: Project | undefined
let object = getDefaultObjectFromDraft() ?? getDefaultObject(id)
let isAssigneeTouched = false
let kind: Ref<TaskType> | undefined
function objectChange (object: IssueDraft, empty: any) {
let templateId: Ref<IssueTemplate> | undefined = draft?.template?.template
let appliedTemplateId: Ref<IssueTemplate> | undefined = draft?.template?.template
let template: IssueTemplate | undefined = undefined
const templateQuery = createQuery()
function objectChange (object: IssueDraft, empty: any): void {
if (shouldSaveDraft) {
draftController.save(object, empty)
}
}
$: objectChange(object, empty)
$: if (object.parentIssue) {
$: if (object.parentIssue !== undefined) {
parentQuery.query(
tracker.class.Issue,
{
@@ -131,7 +137,7 @@
}
function getDefaultObjectFromDraft (): IssueDraft | undefined {
if (!draft) {
if (draft == null) {
return
}
@@ -163,7 +169,7 @@
parentIssue: parentIssue?._id,
subIssues: []
}
if (originalIssue && !ignoreOriginal) {
if (originalIssue !== undefined && !ignoreOriginal) {
const res: IssueDraft = {
...base,
description: originalIssue.description,
@@ -176,11 +182,11 @@
parentIssue: originalIssue.parents[0]?.parentId,
title: `${originalIssue.title} (copy)`
}
client.findAll(tags.class.TagReference, { attachedTo: originalIssue._id }).then((p) => {
void client.findAll(tags.class.TagReference, { attachedTo: originalIssue._id }).then((p) => {
object.labels = p
})
if (originalIssue.relations?.[0]) {
client.findOne(tracker.class.Issue, { _id: originalIssue.relations[0]._id as Ref<Issue> }).then((p) => {
if (originalIssue.relations?.[0] !== undefined) {
void client.findOne(tracker.class.Issue, { _id: originalIssue.relations[0]._id as Ref<Issue> }).then((p) => {
relatedTo = p
})
}
@@ -191,13 +197,11 @@
}
fillDefaults(hierarchy, object, tracker.class.Issue)
let subIssuesComponent: SubIssues
let currentProject: Project | undefined
$: updateIssueStatusId(object, currentProject)
$: updateAssigneeId(object, currentProject)
$: canSave = getTitle(object.title ?? '').length > 0 && object.status !== undefined
$: canSave = getTitle(object.title ?? '').length > 0 && object.status !== undefined && kind !== undefined
$: empty = {
assignee: assignee ?? currentProject?.defaultAssignee,
@@ -210,6 +214,8 @@
space: _space
}
$: objectChange(object, empty)
$: if (_space !== undefined && object.space !== _space) {
object.space = _space
}
@@ -221,12 +227,6 @@
fillDefaults(hierarchy, object, tracker.class.Issue)
}
let templateId: Ref<IssueTemplate> | undefined = draft?.template?.template
let appliedTemplateId: Ref<IssueTemplate> | undefined = draft?.template?.template
let template: IssueTemplate | undefined = undefined
const templateQuery = createQuery()
$: if (templateId !== undefined) {
templateQuery.query(tracker.class.IssueTemplate, { _id: templateId }, (res) => {
template = res[0]
@@ -284,7 +284,9 @@
fillDefaults(hierarchy, object, tracker.class.Issue)
}
$: template && updateTemplate(template)
$: if (template !== undefined) {
void updateTemplate(template)
}
const dispatch = createEventDispatcher()
const spaceQuery = createQuery()
@@ -303,19 +305,19 @@
const docCreateManager = DocCreateExtensionManager.create(tracker.class.Issue)
async function updateIssueStatusId (object: IssueDraft, currentProject: Project | undefined) {
if (currentProject?.defaultIssueStatus && object.status === undefined) {
function updateIssueStatusId (object: IssueDraft, currentProject: Project | undefined): void {
if (currentProject?.defaultIssueStatus !== undefined && object.status === undefined) {
object.status = currentProject.defaultIssueStatus
}
}
function resetDefaultAssigneeId () {
if (!isAssigneeTouched && !!object.assignee && object.assignee === currentProject?.defaultAssignee) {
function resetDefaultAssigneeId (): void {
if (!isAssigneeTouched && !(object.assignee == null) && object.assignee === currentProject?.defaultAssignee) {
object = { ...object, assignee: assignee ?? null }
}
}
function updateAssigneeId (object: IssueDraft, currentProject: Project | undefined) {
function updateAssigneeId (object: IssueDraft, currentProject: Project | undefined): void {
if (!isAssigneeTouched && object.assignee == null && currentProject !== undefined) {
if (currentProject.defaultAssignee !== undefined) {
object.assignee = currentProject.defaultAssignee
@@ -324,13 +326,13 @@
}
}
}
function clearParentIssue () {
function clearParentIssue (): void {
object.parentIssue = undefined
parentQuery.unsubscribe()
parentIssue = undefined
}
function getTitle (value: string) {
function getTitle (value: string): string {
return value.trim()
}
@@ -338,7 +340,7 @@
return true
}
export async function onOutsideClick () {
export function onOutsideClick (): void {
if (shouldSaveDraft) {
draftController.save(object, empty)
}
@@ -346,7 +348,7 @@
async function createIssue (): Promise<void> {
const _id: Ref<Issue> = generateId()
if (!canSave || object.status === undefined || _space === undefined) {
if (!canSave || object.status === undefined || _space === undefined || kind === undefined) {
return
}
@@ -376,18 +378,20 @@
comments: 0,
subIssues: 0,
dueDate: object.dueDate,
parents: parentIssue
? [
{ parentId: parentIssue._id, parentTitle: parentIssue.title, space: parentIssue.space },
...parentIssue.parents
]
: [],
parents:
parentIssue != null
? [
{ parentId: parentIssue._id, parentTitle: parentIssue.title, space: parentIssue.space },
...parentIssue.parents
]
: [],
reportedTime: 0,
remainingTime: 0,
estimation: object.estimation,
reports: 0,
relations: relatedTo !== undefined ? [{ _id: relatedTo._id, _class: relatedTo._class }] : [],
childInfo: []
childInfo: [],
kind
}
await docCreateManager.commit(operations, _id, _space, value)
@@ -423,14 +427,6 @@
}
}
}
const parents = parentIssue
? [
{ parentId: _id, parentTitle: value.title, space: parentIssue.space },
{ parentId: parentIssue._id, parentTitle: parentIssue.title, space: parentIssue.space },
...parentIssue.parents
]
: [{ parentId: _id, parentTitle: value.title, space: _space }]
await subIssuesComponent.save(parents, _id)
addNotification(
await translate(tracker.string.IssueCreated, {}, $themeStore.language),
getTitle(object.title),
@@ -438,17 +434,16 @@
{
issueId: _id,
subTitlePostfix: (await translate(tracker.string.CreatedOne, {}, $themeStore.language)).toLowerCase(),
issueUrl: currentProject && generateIssueShortLink(getIssueId(currentProject, value as Issue))
issueUrl: currentProject != null && generateIssueShortLink(getIssueId(currentProject, value as Issue))
}
)
draftController.remove()
resetObject()
descriptionBox?.removeDraft(false)
isAssigneeTouched = false
}
async function setParentIssue () {
async function setParentIssue (): Promise<void> {
showPopup(
SetParentIssueActionPopup,
{ value: { ...object, space: _space, attachedTo: parentIssue?._id } },
@@ -462,7 +457,7 @@
)
}
const handleComponentIdChanged = (componentId: Ref<ComponentType> | null | undefined) => {
const handleComponentIdChanged = (componentId: Ref<ComponentType> | null | undefined): void => {
if (componentId === undefined) {
return
}
@@ -470,7 +465,7 @@
object.component = componentId
}
const handleMilestoneIdChanged = async (milestoneId: Ref<Milestone> | null | undefined) => {
const handleMilestoneIdChanged = (milestoneId: Ref<Milestone> | null | undefined): void => {
if (milestoneId === undefined) {
return
}
@@ -508,7 +503,7 @@
)
}
async function showConfirmationDialog () {
async function showConfirmationDialog (): Promise<void> {
draftController.save(object, empty)
const isFormEmpty = draft === undefined
@@ -602,7 +597,7 @@
on:close={() => dispatch('close')}
onCancel={showConfirmationDialog}
hideAttachments={attachments.size === 0}
hideSubheader={!parentIssue}
hideSubheader={parentIssue == null}
noFade={true}
on:changeContent
>
@@ -611,6 +606,9 @@
_class={tracker.class.Project}
label={tracker.string.Project}
bind:space={_space}
on:object={(evt) => {
project = evt.detail
}}
kind={'regular'}
size={'small'}
component={ProjectPresenter}
@@ -641,6 +639,7 @@
<div class="mr-2">
<Label {label} />
</div>
<TaskKindSelector projectType={project?.type} bind:taskType={kind} baseClass={tracker.class.Issue} />
{#if relatedTo}
<div class="lower mr-2">
<Label label={tracker.string.RelatedTo} />
@@ -700,7 +699,7 @@
}}
on:attachments={(ev) => {
if (ev.detail.size > 0) attachments = ev.detail.values
else if (ev.detail.size === 0 && ev.detail.values) {
else if (ev.detail.size === 0 && ev.detail.values != null) {
attachments.clear()
attachments = attachments
}
@@ -708,36 +707,28 @@
/>
{/key}
</div>
{#if _space}
<SubIssues
bind:this={subIssuesComponent}
projectId={_space}
project={currentProject}
milestone={object.milestone}
component={object.component}
bind:subIssues={object.subIssues}
/>
{/if}
<DocCreateExtComponent manager={docCreateManager} kind={'body'} space={currentProject} props={extraProps} />
<svelte:fragment slot="pool">
<div id="status-editor">
<StatusEditor
focusIndex={3}
value={object}
kind={'regular'}
size={'large'}
defaultIssueStatus={currentProject?.defaultIssueStatus}
shouldShowLabel={true}
short
on:refocus={() => {
manager.setFocusPos(3)
}}
on:change={({ detail }) => {
if (object.status !== detail) {
object.status = detail
}
}}
/>
{#if kind !== undefined}
<StatusEditor
focusIndex={3}
value={{ ...object, kind }}
kind={'regular'}
size={'large'}
defaultIssueStatus={currentProject?.defaultIssueStatus}
shouldShowLabel={true}
short
on:refocus={() => {
manager.setFocusPos(3)
}}
on:change={({ detail }) => {
if (object.status !== detail) {
object.status = detail
}
}}
/>
{/if}
</div>
<div id="priority-editor">
<PriorityEditor
@@ -821,11 +812,11 @@
<div id="parentissue-editor" class="new-line">
<Button
icon={tracker.icon.Parent}
label={object.parentIssue ? tracker.string.RemoveParent : tracker.string.SetParent}
label={object.parentIssue != null ? tracker.string.RemoveParent : tracker.string.SetParent}
kind={'regular'}
size={'large'}
notSelected={object.parentIssue === undefined}
on:click={object.parentIssue ? clearParentIssue : setParentIssue}
on:click={object.parentIssue != null ? clearParentIssue : setParentIssue}
/>
</div>
<DocCreateExtComponent manager={docCreateManager} kind={'pool'} space={currentProject} props={extraProps} />
@@ -72,7 +72,7 @@
allowDeselect
kind={'list'}
on:change={(evt) => {
client.update(target, { target: evt.detail || null })
void client.diffUpdate(target, { target: evt.detail || null })
}}
/>
</FixedColumn>
@@ -81,7 +81,7 @@
<Button
icon={IconDelete}
on:click={() => {
client.remove(target)
void client.remove(target)
}}
/>
</div>
@@ -113,7 +113,7 @@
allowDeselect
kind={'list'}
on:change={(evt) => {
client.createDoc(tracker.class.RelatedIssueTarget, space, {
void client.createDoc(tracker.class.RelatedIssueTarget, space, {
target: evt.detail || null,
rule: {
kind: 'spaceRule',
@@ -1,213 +0,0 @@
<!--
// 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 attachment, { Attachment } from '@hcengineering/attachment'
import { deleteFile } from '@hcengineering/attachment-resources/src/utils'
import core, { AttachedData, Doc, Ref, SortingOrder } from '@hcengineering/core'
import { DraftController, draftsStore, getClient } from '@hcengineering/presentation'
import tags from '@hcengineering/tags'
import { calcRank } from '@hcengineering/task'
import { Component, Issue, IssueDraft, IssueParentInfo, Milestone, Project } from '@hcengineering/tracker'
import { Button, ExpandCollapse, Scroller } from '@hcengineering/ui'
import { onDestroy } from 'svelte'
import tracker from '../plugin'
import Collapsed from './icons/Collapsed.svelte'
import Expanded from './icons/Expanded.svelte'
import DraftIssueChildList from './templates/DraftIssueChildList.svelte'
export let projectId: Ref<Project>
export let project: Project | undefined
export let milestone: Ref<Milestone> | null = null
export let component: Ref<Component> | null = null
export let subIssues: IssueDraft[] = []
let lastProject = project
let isCollapsed = false
async function handleIssueSwap (ev: CustomEvent<{ fromIndex: number, toIndex: number }>) {
if (subIssues) {
const { fromIndex, toIndex } = ev.detail
const [fromIssue] = subIssues.splice(fromIndex, 1)
const leftPart = subIssues.slice(0, toIndex)
const rightPart = subIssues.slice(toIndex)
subIssues = [...leftPart, fromIssue, ...rightPart]
}
}
$: onProjectChange(project)
function onProjectChange (project: Project | undefined) {
if (lastProject?._id === project?._id) return
lastProject = project
if (project === undefined) return
subIssues.forEach((p) => {
p.status = project.defaultIssueStatus
p.space = project._id
})
}
const client = getClient()
// TODO: move to utils
export async function save (parents: IssueParentInfo[], _id: Ref<Doc>) {
if (project === undefined) return
saved = true
for (const subIssue of subIssues) {
const lastOne = await client.findOne<Issue>(tracker.class.Issue, {}, { sort: { rank: SortingOrder.Descending } })
const incResult = await client.updateDoc(
tracker.class.Project,
core.space.Space,
project._id,
{
$inc: { sequence: 1 }
},
true
)
const childId = subIssue._id
const cvalue: AttachedData<Issue> = {
title: subIssue.title.trim(),
description: subIssue.description,
assignee: subIssue.assignee,
component: subIssue.component,
milestone: subIssue.milestone,
number: (incResult as any).object.sequence,
status: subIssue.status ?? project.defaultIssueStatus,
priority: subIssue.priority,
rank: calcRank(lastOne, undefined),
comments: 0,
subIssues: 0,
dueDate: null,
parents,
reportedTime: 0,
remainingTime: 0,
estimation: subIssue.estimation,
reports: 0,
relations: [],
childInfo: []
}
await client.addCollection(
tracker.class.Issue,
project._id,
_id,
tracker.class.Issue,
'subIssues',
cvalue,
childId
)
if ((subIssue.labels?.length ?? 0) > 0) {
for (const label of subIssue.labels) {
await client.addCollection(tags.class.TagReference, project._id, childId, tracker.class.Issue, 'labels', {
title: label.title,
color: label.color,
tag: label.tag
})
}
}
saveAttachments(childId)
}
}
async function saveAttachments (issue: Ref<Issue>) {
const draftAttachments = $draftsStore[`${issue}_attachments`]
if (draftAttachments) {
for (const key in draftAttachments) {
await saveAttachment(draftAttachments[key as Ref<Attachment>], issue)
}
}
removeDraft(issue)
}
async function saveAttachment (doc: Attachment, issue: Ref<Issue>): Promise<void> {
await client.addCollection(
attachment.class.Attachment,
projectId,
issue,
tracker.class.Issue,
'attachments',
doc,
doc._id
)
}
export function load (value: IssueDraft[]) {
subIssues = value
}
let saved = false
onDestroy(() => {
if (!saved) {
subIssues.forEach((st) => {
removeDraft(st._id, true)
})
}
})
// TODO: move to utils
export async function removeDraft (_id: string, removeFiles: boolean = false): Promise<void> {
const draftAttachments = $draftsStore[`${_id}_attachments`]
DraftController.remove(`${_id}_attachments`)
if (removeFiles && draftAttachments) {
for (const key in draftAttachments) {
const attachment = draftAttachments[key as Ref<Attachment>]
await deleteFile(attachment.file)
}
}
}
</script>
<!-- TODO: check if sub issues list is empty in a parent component -->
{#if subIssues.length > 0}
<div class="flex-between clear-mins">
<Button
width="min-content"
icon={isCollapsed ? Collapsed : Expanded}
size="small"
kind="ghost"
label={tracker.string.SubIssuesList}
labelParams={{ subIssues: subIssues.length }}
on:click={() => (isCollapsed = !isCollapsed)}
/>
</div>
<ExpandCollapse isExpanded={!isCollapsed} on:changeContent>
<div class="flex-col flex-no-shrink max-h-30 list clear-mins" class:collapsed={isCollapsed}>
<Scroller>
<DraftIssueChildList
{component}
{milestone}
bind:issues={subIssues}
project={projectId}
on:move={handleIssueSwap}
on:update-issue
/>
</Scroller>
</div>
</ExpandCollapse>
{/if}
<style lang="scss">
.list {
border-top: 1px solid var(--divider-color);
&.collapsed {
padding-top: 1px;
border-top: none;
}
}
</style>
@@ -47,7 +47,7 @@
let docWidth: number
let docSize = false
function showCreateDialog () {
function showCreateDialog (): void {
showPopup(NewComponent, { space, targetElement: null }, 'top')
}
@@ -3,6 +3,7 @@
import { ColorDefinition, IconSize, getPlatformColorDef, themeStore } from '@hcengineering/ui'
import { createEventDispatcher, onMount } from 'svelte'
import tracker from '../../plugin'
import task from '@hcengineering/task'
export let size: IconSize
@@ -37,7 +38,7 @@
viewBox="0 0 16 16"
xmlns="http://www.w3.org/2000/svg"
>
{#if category._id === tracker.issueStatusCategory.Backlog}
{#if category._id === tracker.issueStatusCategory.Backlog || category._id === task.statusCategory.UnStarted}
<path
fill-rule="evenodd"
clip-rule="evenodd"
@@ -22,6 +22,8 @@
import { DocNavLink } from '@hcengineering/view-resources'
import tracker from '../../plugin'
import { activeProjects } from '../../utils'
import { taskTypeStore } from '@hcengineering/task-resources'
import TaskTypeIcon from '@hcengineering/task-resources/src/components/taskTypes/TaskTypeIcon.svelte'
export let value: WithLookup<Issue>
export let disabled: boolean = false
@@ -36,17 +38,24 @@
let currentProject: Project | undefined = value?.$lookup?.space
$: if (value !== undefined) {
currentProject = $activeProjects.get(value?.space)
currentProject = $activeProjects.get(value?.space) as Project
}
$: title = currentProject ? `${currentProject.identifier}-${value?.number}` : `${value?.number}`
$: presenters =
value !== undefined ? getClient().getHierarchy().findMixinMixins(value, view.mixin.ObjectPresenter) : []
$: taskType = $taskTypeStore.get(value.kind)
</script>
{#if value}
<div class="flex-row-center flex-between">
<div class="flex-row-center">
{#if taskType !== undefined}
<div class="text-sm mr-1">
<TaskTypeIcon value={taskType} />
</div>
{/if}
<DocNavLink
object={value}
{onClick}
@@ -47,7 +47,7 @@
let currentProject: Project | undefined
$: currentProject = $activeProjects.get(space)
$: currentProject = $activeProjects.get(space) as Project
$: issueName = currentProject && issue && `${currentProject.identifier}-${issue.number}`
const limit: number = 350
@@ -14,18 +14,29 @@
-->
<script lang="ts">
import core, { Ref, StatusCategory, WithLookup } from '@hcengineering/core'
import { createQuery, getClient } from '@hcengineering/presentation'
import { ProjectType, getStates } from '@hcengineering/task'
import { typeStore } from '@hcengineering/task-resources'
import { getClient } from '@hcengineering/presentation'
import {
ProjectType,
TaskType,
getProjectTypeStates,
getStates,
getTaskTypeStates,
isTaskCategory
} from '@hcengineering/task'
import { taskTypeStore, typeStore } from '@hcengineering/task-resources'
import StatePresenter from '@hcengineering/task-resources/src/components/state/StatePresenter.svelte'
import { IssueStatus, Project } from '@hcengineering/tracker'
import { IconSize } from '@hcengineering/ui'
import { statusStore } from '@hcengineering/view-resources'
import tracker from '../../plugin'
import { activeProjects } from '../../utils'
import StatusIcon from '../icons/StatusIcon.svelte'
export let value: WithLookup<IssueStatus> | undefined
export let size: IconSize
export let space: Ref<Project> | undefined
export let space: Ref<Project> | undefined = undefined
export let projectType: Ref<ProjectType> | undefined = undefined
export let taskType: Ref<TaskType> | undefined = undefined
const dynamicFillCategories = [tracker.issueStatusCategory.Started]
@@ -38,22 +49,30 @@
count: number | undefined
} = { index: undefined, count: undefined }
const spaceQuery = createQuery()
let _space: Project | undefined = undefined
$: space
? spaceQuery.query(tracker.class.Project, { _id: space }, (res) => {
_space = res[0]
})
: (_space = undefined)
$: _space = space !== undefined ? ($activeProjects.get(space) as Project) : undefined
$: if (value?.category === tracker.issueStatusCategory.Started) {
statuses = getStates(_space, $typeStore, $statusStore.byId).filter(
(p) => p.category === tracker.issueStatusCategory.Started
)
if (taskType !== undefined) {
statuses = getTaskTypeStates(taskType, $taskTypeStore, $statusStore.byId).filter(
(p) => p.category === tracker.issueStatusCategory.Started
)
} else if (projectType !== undefined) {
statuses = getProjectTypeStates(projectType, $typeStore, $statusStore.byId).filter(
(p) => p.category === tracker.issueStatusCategory.Started
)
} else {
statuses = getStates(_space, $typeStore, $statusStore.byId).filter(
(p) => p.category === tracker.issueStatusCategory.Started
)
}
}
async function updateCategory (_space: Project | undefined, status: WithLookup<IssueStatus>, statuses: IssueStatus[]) {
if (status.$lookup?.category) {
async function updateCategory (
_space: Project | undefined,
status: WithLookup<IssueStatus>,
statuses: IssueStatus[]
): Promise<void> {
if (status.$lookup?.category !== undefined) {
category = status.$lookup.category
}
if (category === undefined || category._id !== value?.category) {
@@ -83,15 +102,27 @@
}
}
$: type = _space ? $typeStore.get(_space.type) : undefined
$: type = _space ? $typeStore.get(_space.type) : projectType !== undefined ? $typeStore.get(projectType) : undefined
$: viewState = value && getViewState(type, value)
$: value && updateCategory(_space, value, statuses)
$: if (value !== undefined) {
void updateCategory(_space, value, statuses)
}
$: icon = category?.icon
$: color = viewState?.color !== undefined ? viewState?.color : category !== undefined ? category.color : -1
</script>
{#if icon !== undefined && color !== undefined && category !== undefined}
{#if category !== undefined && isTaskCategory(category._id)}
<StatePresenter
{space}
projectType={_space?.type ?? projectType}
{taskType}
{value}
{size}
shouldShowName={false}
on:accent-color
/>
{:else if icon !== undefined && color !== undefined && category !== undefined}
<StatusIcon on:accent-color {category} {size} fill={color} {statusIcon} />
{/if}
@@ -17,10 +17,11 @@
import { IntlString } from '@hcengineering/platform'
import { createQuery } from '@hcengineering/presentation'
import { Issue, IssueStatus, Project } from '@hcengineering/tracker'
import { resolvedLocationStore } from '@hcengineering/ui'
import { IModeSelector, resolvedLocationStore } from '@hcengineering/ui'
import view, { Viewlet } from '@hcengineering/view'
import { createEventDispatcher } from 'svelte'
import { IModeSelector } from '@hcengineering/ui'
import { TypeSelector, selectedTaskTypeStore, selectedTypeStore, taskTypeStore } from '@hcengineering/task-resources'
import tracker from '../../plugin'
import IssuesView from './IssuesView.svelte'
@@ -28,8 +29,12 @@
export let baseQuery: DocumentQuery<Issue> = {}
export let title: IntlString
export let config: [string, IntlString, object][]
export let allProjectsTypes: boolean = false
export let baseClass = tracker.class.Issue
const dispatch = createEventDispatcher()
let query: DocumentQuery<Issue> | undefined = undefined
let modeSelectorProps: IModeSelector | undefined = undefined
@@ -89,8 +94,29 @@
onChange: (newMode: string) => dispatch('action', { mode: newMode })
}
}
$: allTypes = Array.from($taskTypeStore.values())
.filter((it) => it.parent === $selectedTypeStore)
.map((it) => it._id)
$: finalQuery = {
...query,
...(allProjectsTypes
? {}
: $selectedTaskTypeStore !== undefined
? { kind: $selectedTaskTypeStore }
: { kind: { $in: allTypes } })
}
const toVL = (data: any): Viewlet | undefined => data as Viewlet
</script>
{#if query !== undefined && modeSelectorProps !== undefined}
<IssuesView {query} space={currentSpace} {title} {modeSelectorProps} />
<IssuesView query={finalQuery} space={currentSpace} {title} {modeSelectorProps}>
<svelte:fragment slot="type_selector" let:viewlet>
{#if !allProjectsTypes}
<TypeSelector {baseClass} project={currentSpace} allTypes={toVL(viewlet)?.descriptor === view.viewlet.List} />
{/if}
</svelte:fragment>
</IssuesView>
{/if}
@@ -29,7 +29,7 @@
let resultQuery: DocumentQuery<Issue> = { ...searchQuery }
$: if (!label && title) {
translate(title, {}, $themeStore.language).then((res) => {
void translate(title, {}, $themeStore.language).then((res) => {
label = res
})
}
@@ -61,6 +61,11 @@
<svelte:fragment slot="label_selector">
<slot name="label_selector" />
</svelte:fragment>
<svelte:fragment slot="type_selector">
<slot name="type_selector" {viewlet} />
</svelte:fragment>
<svelte:fragment slot="extra">
<ComponentExtensions
extension={tracker.extensions.IssueListHeader}
@@ -103,7 +103,7 @@
$: currentSpace = space ?? tracker.project.DefaultProject
let currentProject: Project | undefined
$: currentProject = $activeProjects.get(currentSpace)
$: currentProject = $activeProjects.get(currentSpace) as Project
let resultQuery: DocumentQuery<any> = { ...query }
const client = getClient()
@@ -194,6 +194,7 @@
$: listProvider.update(tasks)
let categories: CategoryType[] = []
let loadCategories = true
const queryId = generateId()
@@ -211,6 +212,7 @@
queryId
).then((res) => {
categories = res
loadCategories = false
})
}
@@ -227,6 +229,7 @@
queryId
).then((res) => {
categories = res
loadCategories = false
})
const fullFilled: Record<string, boolean> = {}
@@ -305,7 +308,7 @@
}
</script>
{#if categories.length === 0}
{#if loadCategories}
<Loading />
{:else}
<ActionContext
@@ -20,7 +20,7 @@
let currentProject: Project | undefined = undefined
$: currentProject = $activeProjects.get(value.space)
$: currentProject = $activeProjects.get(value.space) as Project
$: title = currentProject ? `${currentProject.identifier}-${value?.number}` : `${value?.number}`
</script>
@@ -15,7 +15,8 @@
<script lang="ts">
import { AttachedData, Ref, WithLookup } from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation'
import { getStates } from '@hcengineering/task'
import { getTaskTypeStates } from '@hcengineering/task'
import { taskTypeStore } from '@hcengineering/task-resources'
import { Issue, IssueDraft, IssueStatus, Project } from '@hcengineering/tracker'
import {
Button,
@@ -30,10 +31,8 @@
import { statusStore } from '@hcengineering/view-resources'
import { createEventDispatcher } from 'svelte'
import tracker from '../../plugin'
import { activeProjects } from '../../utils'
import IssueStatusIcon from './IssueStatusIcon.svelte'
import StatusPresenter from './StatusPresenter.svelte'
import { typeStore } from '@hcengineering/task-resources'
type ValueType = Issue | (AttachedData<Issue> & { space: Ref<Project> }) | IssueDraft
export let value: ValueType
@@ -84,11 +83,7 @@
)
}
let space: Project | undefined = undefined
$: space = $activeProjects.get(value.space)
$: statuses = getStates(space, $typeStore, $statusStore.byId)
$: statuses = getTaskTypeStates(value.kind, $taskTypeStore, $statusStore.byId)
function getSelectedStatus (
statuses: WithLookup<IssueStatus>[] | undefined,
@@ -97,13 +92,19 @@
): WithLookup<IssueStatus> | undefined {
if (value.status !== undefined) {
const current = statuses?.find((status) => status._id === value.status)
if (current) return current
if (current != null) {
return current
}
}
if (defaultIssueStatus !== undefined) {
const res = statuses?.find((status) => status._id === defaultStatus)
changeStatus(res?._id, false)
void changeStatus(res?._id, false)
return res
}
// We need to choose first one, since it should not be case without status.
if (value.status === undefined) {
void changeStatus(statuses?.[0]?._id, false)
}
}
$: selectedStatus = getSelectedStatus(statuses, value, defaultIssueStatus)
@@ -16,9 +16,12 @@
import { Ref } from '@hcengineering/core'
import { IssueStatus, Project } from '@hcengineering/tracker'
import IssueStatusIcon from './IssueStatusIcon.svelte'
import { ProjectType, TaskType } from '@hcengineering/task'
export let value: IssueStatus | undefined
export let space: Ref<Project>
export let space: Ref<Project> | undefined = undefined
export let projectType: Ref<ProjectType> | undefined = undefined
export let taskType: Ref<TaskType> | undefined = undefined
export let size: 'small' | 'medium' = 'small'
export let kind: 'list-header' | undefined = undefined
export let colorInherit: boolean = false
@@ -29,8 +32,8 @@
{#if value}
<div class="flex-presenter" style:color={'inherit'}>
{#if !inline && shouldShowAvatar}
<IssueStatusIcon {value} {size} {space} on:accent-color />
{#if !inline}
<IssueStatusIcon {value} {size} {space} on:accent-color {projectType} {taskType} />
{/if}
<span
class="overflow-label"
@@ -17,6 +17,7 @@
import { Project } from '@hcengineering/tracker'
import { statusStore } from '@hcengineering/view-resources'
import StatusPresenter from './StatusPresenter.svelte'
import { selectedTaskTypeStore } from '@hcengineering/task-resources'
export let value: Ref<Status> | undefined
export let space: Ref<Project>
@@ -29,5 +30,14 @@
</script>
{#if value}
<StatusPresenter {space} value={statusValue} {size} {kind} {colorInherit} {accent} on:accent-color />
<StatusPresenter
{space}
value={statusValue}
{size}
{kind}
{colorInherit}
{accent}
on:accent-color
taskType={$selectedTaskTypeStore}
/>
{/if}
@@ -23,6 +23,7 @@
ButtonSize,
IconSize,
SelectPopup,
SelectPopupValueType,
TooltipAlignment,
eventToHTMLElement,
showPopup
@@ -59,19 +60,6 @@
dispatch('change', newStatus)
}
const handleStatusEditorOpened = (event: MouseEvent) => {
if (!isEditable) {
return
}
showPopup(
SelectPopup,
{ value: statusesInfo, placeholder: tracker.string.SetStatus },
eventToHTMLElement(event),
changeStatus
)
}
$: statuses = getStatuses($statusStore.byId, $typeStore, type)
function getStatuses (
@@ -100,16 +88,32 @@
return current
}
let statusesInfo: SelectPopupValueType[]
$: selectedStatus = getSelectedStatus(statuses, value)
$: selectedStatusLabel = selectedStatus?.name
$: statusesInfo = statuses?.map((s) => {
return {
id: s._id,
component: StatusPresenter,
props: { value: s, size: 'small' },
isSelected: selectedStatus?._id === s._id ?? false
$: statusesInfo =
statuses?.map((s) => {
return {
id: s._id,
component: StatusPresenter,
props: { value: s, size: 'small' },
isSelected: selectedStatus?._id === s._id ?? false
}
}) ?? []
const handleStatusEditorOpened = (event: MouseEvent) => {
if (!isEditable) {
return
}
})
showPopup(
SelectPopup,
{ value: statusesInfo, placeholder: tracker.string.SetStatus },
eventToHTMLElement(event),
changeStatus
)
}
$: smallgap = size === 'inline' || size === 'small'
</script>
@@ -22,9 +22,9 @@
eventToHTMLElement,
getEventPositionElement,
ListView,
showPopup
showPopup,
DatePresenter
} from '@hcengineering/ui'
import { DatePresenter } from '@hcengineering/ui'
import { ContextMenu, FixedColumn, ListSelectionProvider } from '@hcengineering/view-resources'
import { getIssueId } from '../../../issues'
import tracker from '../../../plugin'
@@ -18,10 +18,9 @@
import type { IntlString } from '@hcengineering/platform'
import { createQuery } from '@hcengineering/presentation'
import type { Issue, IssueStatus, Project } from '@hcengineering/tracker'
import { resolvedLocationStore } from '@hcengineering/ui'
import { resolvedLocationStore, IModeSelector } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
import { IModeSelector } from '@hcengineering/ui'
import tracker from '../../plugin'
import IssuesView from '../issues/IssuesView.svelte'
@@ -15,7 +15,7 @@
<script lang="ts">
import { Employee } from '@hcengineering/contact'
import { AccountArrayEditor, AssigneeBox } from '@hcengineering/contact-resources'
import core, { Account, DocumentUpdate, Ref, generateId, getCurrentAccount } from '@hcengineering/core'
import core, { Account, Data, DocumentUpdate, Ref, generateId, getCurrentAccount } from '@hcengineering/core'
import { Asset } from '@hcengineering/platform'
import presentation, { Card, createQuery, getClient } from '@hcengineering/presentation'
import task, { ProjectType } from '@hcengineering/task'
@@ -42,6 +42,7 @@
import tracker from '../../plugin'
import StatusSelector from '../issues/StatusSelector.svelte'
import ChangeIdentity from './ChangeIdentity.svelte'
import { typeStore } from '@hcengineering/task-resources'
export let project: Project | undefined = undefined
@@ -71,7 +72,7 @@
$: isNew = !project
async function handleSave () {
async function handleSave (): Promise<void> {
if (isNew) {
await createProject()
} else {
@@ -81,7 +82,7 @@
let identifier: string = project?.identifier ?? 'TSK'
function getProjectData () {
function getProjectData (): Omit<Data<Project>, 'type'> {
return {
name,
description,
@@ -98,7 +99,7 @@
}
}
async function updateProject () {
async function updateProject (): Promise<void> {
if (!project) {
return
}
@@ -154,10 +155,16 @@
let typeId: Ref<ProjectType> | undefined = project?.type
async function createProject () {
$: typeType = typeId !== undefined ? $typeStore.get(typeId) : undefined
$: if (defaultStatus === undefined && typeType !== undefined) {
defaultStatus = typeType.statuses[0]?._id
}
async function createProject (): Promise<void> {
const projectId = generateId<Project>()
const projectData = getProjectData()
if (typeId !== undefined) {
if (typeId !== undefined && typeType !== undefined) {
const ops = client
.apply(projectId)
.notMatch(tracker.class.Project, { identifier: projectData.identifier.toUpperCase() })
@@ -168,6 +175,9 @@
isSaving = false
if (succeeded) {
// Add vacancy mixin
await client.createMixin(projectId, tracker.class.Project, core.space.Space, typeType.targetClass, {})
close(projectId)
} else {
changeIdentity(changeIdentityRef)
@@ -175,17 +185,18 @@
}
}
function chooseIcon (ev: MouseEvent) {
function chooseIcon (ev: MouseEvent): void {
const icons = [tracker.icon.Home, tracker.icon.RedCircle]
showPopup(IconPicker, { icon, color, icons }, 'top', (result) => {
const update = (result: any) => {
if (result !== undefined && result !== null) {
icon = result.icon
color = result.color
isColorSelected = true
}
})
}
showPopup(IconPicker, { icon, color, icons }, 'top', update, update)
}
function changeIdentity (element: HTMLElement) {
function changeIdentity (element: HTMLElement): void {
showPopup(ChangeIdentity, { identifier, projectsIdentifiers }, element, (result) => {
if (result != null) {
identifier = result.toLocaleUpperCase()
@@ -193,7 +204,7 @@
})
}
function close (id?: Ref<Project>) {
function close (id?: Ref<Project>): void {
dispatch('close', id)
}
@@ -203,6 +214,7 @@
function handleTypeChange (evt: CustomEvent<Ref<ProjectType>>): void {
typeId = evt.detail
defaultStatus = undefined
}
$: identifier = identifier.toLocaleUpperCase().replaceAll('-', '_').replaceAll(' ', '_').substring(0, 5)
@@ -230,7 +242,7 @@
<Component
is={task.component.ProjectTypeSelector}
props={{
categories: [tracker.category.ProjectTypeCategory],
descriptors: [tracker.descriptors.ProjectType],
type: typeId,
disabled: !isNew,
focusIndex: 4,
@@ -0,0 +1,15 @@
<script lang="ts">
import { AccountArrayEditor } from '@hcengineering/contact-resources'
import contact from '@hcengineering/contact-resources/src/plugin'
import { getClient } from '@hcengineering/presentation'
import { Project } from '@hcengineering/tracker'
export let value: Project
</script>
<AccountArrayEditor
label={contact.string.Members}
value={value.members}
onChange={(evt) => {
void getClient().diffUpdate(value, { members: evt })
}}
/>
@@ -26,13 +26,12 @@
Milestone
} from '@hcengineering/tracker'
import { Button, Component, EditBox } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
import { createEventDispatcher, onDestroy } from 'svelte'
import tracker from '../../plugin'
import AssigneeEditor from '../issues/AssigneeEditor.svelte'
import PriorityEditor from '../issues/PriorityEditor.svelte'
import StatusEditor from '../issues/StatusEditor.svelte'
import EstimationEditor from './EstimationEditor.svelte'
import { onDestroy } from 'svelte'
export let parendIssueId: Ref<Issue>
export let project: Project