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
@@ -15,23 +15,25 @@
<script lang="ts">
import { Attribute, Class, Ref, Status, StatusCategory } from '@hcengineering/core'
import presentation, { Card, getClient } from '@hcengineering/presentation'
import { ProjectType, createState } from '@hcengineering/task'
import { EditBox } from '@hcengineering/ui'
import { ProjectType, TaskType, calculateStatuses, createState } from '@hcengineering/task'
import { EditBox, getColorNumberByText } from '@hcengineering/ui'
import { statusStore } from '@hcengineering/view-resources'
import { createEventDispatcher } from 'svelte'
import task from '../plugin'
import { taskTypeStore } from '..'
const dispatch = createEventDispatcher()
const client = getClient()
export let status: Status | undefined = undefined
export let _class: Ref<Class<Status>> | undefined = status?._class
export let taskType: TaskType
export let type: ProjectType
export let ofAttribute: Ref<Attribute<Status>>
export let category: Ref<StatusCategory> | undefined = status?.category
export let value = status?.name ?? ''
async function save () {
if (type === undefined || _class === undefined) return
async function save (): Promise<void> {
if (taskType === undefined || _class === undefined) return
if (status === undefined) {
const _id = await createState(client, _class, {
ofAttribute,
@@ -40,17 +42,14 @@
color: 9
})
const states = type.statuses.map((p) => $statusStore.byId.get(p._id)).filter((p) => p !== undefined) as Status[]
const states = taskType.statuses.map((p) => $statusStore.byId.get(p)).filter((p) => p !== undefined) as Status[]
const lastIndex = states.findLastIndex((p) => p.category === category)
const statuses = [
...type.statuses.slice(0, lastIndex + 1),
{
_id,
color: 9
},
...type.statuses.slice(lastIndex + 1)
]
const statuses = [...taskType.statuses.slice(0, lastIndex + 1), _id, ...taskType.statuses.slice(lastIndex + 1)]
await client.update(type, {
statuses: calculateStatuses(type, $taskTypeStore, [{ taskTypeId: taskType._id, statuses }])
})
await client.update(taskType, {
statuses
})
} else if (status.name.trim() !== value.trim()) {
@@ -58,29 +57,34 @@
ofAttribute,
name: value.trim(),
category,
color: 9
color: getColorNumberByText(value.trim())
})
const index = type.statuses.findIndex((p) => p._id === status?._id)
const oldStatus = type.statuses[index]
const statuses = [
...type.statuses.slice(0, index),
{
_id,
color: oldStatus.color
},
...type.statuses.slice(index + 1)
]
const projects = await client.findAll(task.class.Project, { type: type._id })
const docs = await client.findAll(task.class.Task, {
status: status._id,
space: { $in: projects.map((p) => p._id) }
const index = taskType.statuses.indexOf(status._id)
const statuses = [...taskType.statuses.slice(0, index), _id, ...taskType.statuses.slice(index + 1)]
await client.update(type, {
statuses: calculateStatuses(type, $taskTypeStore, [{ taskTypeId: taskType._id, statuses }])
})
const op = client.apply(_id)
await op.update(type, {
await client.update(taskType, {
statuses
})
docs.map((p) => op.update(p, { status: _id }))
await op.commit()
const projects = await client.findAll(task.class.Project, { type: type._id })
while (true) {
const docs = await client.findAll(
task.class.Task,
{
status: status._id,
space: { $in: projects.map((p) => p._id) }
},
{ limit: 1000 }
)
if (docs.length === 0) {
break
}
const op = client.apply(_id)
docs.map((p) => op.update(p, { status: _id }))
await op.commit()
}
}
dispatch('close')
}
@@ -16,7 +16,7 @@
import core, { Doc, FindResult, IdMap, Ref, RefTo, Space, Status, toIdMap } from '@hcengineering/core'
import { translate } from '@hcengineering/platform'
import presentation, { getClient } from '@hcengineering/presentation'
import { ProjectType } from '@hcengineering/task'
import { TaskType } from '@hcengineering/task'
import ui, {
EditWithIcon,
Icon,
@@ -39,7 +39,7 @@
import view from '@hcengineering/view-resources/src/plugin'
import { buildConfigLookup, getPresenter } from '@hcengineering/view-resources/src/utils'
import { createEventDispatcher } from 'svelte'
import { selectedTypeStore, typeStore } from '..'
import { selectedTaskTypeStore, taskTypeStore } from '..'
export let filter: Filter
export let space: Ref<Space> | undefined = undefined
@@ -65,21 +65,19 @@
async function getValues (
search: string,
selectedType: Ref<ProjectType> | undefined,
typeStore: IdMap<ProjectType>,
selectedType: Ref<TaskType> | undefined,
typeStore: IdMap<TaskType>,
statusStore: IdMap<Status>
): Promise<void> {
if (objectsPromise) {
await objectsPromise
}
await objectsPromise
targets.clear()
for (const object of filter.value) {
targets.add(object)
}
const type = selectedType ? typeStore.get(selectedType) : undefined
const type = selectedType !== undefined ? typeStore.get(selectedType) : undefined
if (type !== undefined) {
values = type?.statuses?.map((p) => statusStore.get(p._id))
values = type?.statuses?.map((p) => statusStore.get(p))
for (const value of values) {
targets.add(value?._id)
}
@@ -153,7 +151,7 @@
updateFilter()
}
function updateFilter () {
function updateFilter (): void {
clearTimeout(filterUpdateTimeout)
filterUpdateTimeout = setTimeout(() => {
@@ -165,7 +163,9 @@
const dispatch = createEventDispatcher()
$: if (targetClass) getValues(search, $selectedTypeStore, $typeStore, $statusStore.byId)
$: if (targetClass != null) {
void getValues(search, $selectedTaskTypeStore, $taskTypeStore, $statusStore.byId)
}
</script>
<div class="selectPopup" use:resizeObserver={() => dispatch('changeContent')}>
@@ -1,17 +1,16 @@
<script lang="ts">
import { Icon } from '@hcengineering/ui'
import { Icon, IconSize } from '@hcengineering/ui'
import IconFolder from './icons/Folder.svelte'
export let size: IconSize = 'small'
</script>
<div class="flex-center template-icon">
<Icon icon={IconFolder} size="small" />
<Icon icon={IconFolder} {size} />
</div>
<style lang="scss">
.template-icon {
width: 100%;
height: 100%;
color: #fff;
background-color: #7c6fcd;
}
</style>
@@ -0,0 +1,63 @@
<script lang="ts">
import { Class, Doc, Ref } from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation'
import { Project, ProjectType } from '@hcengineering/task'
import { DropdownLabels, resolvedLocationStore } from '@hcengineering/ui'
import { onDestroy } from 'svelte'
import { activeProjects, selectedTypeStore, selectedTaskTypeStore, taskTypeStore, typeStore } from '..'
import TaskKindSelector from './taskTypes/TaskKindSelector.svelte'
export let project: Ref<Project> | undefined = undefined
export let baseClass: Ref<Class<Doc>>
export let allTypes = false
let typeFixed: boolean = false
$: currentProject = project !== undefined ? $activeProjects.get(project) : undefined
const client = getClient()
$: availableProjectTypes = Array.from($typeStore.values()).filter((it) =>
it.tasks.some((tit) => {
const taskType = $taskTypeStore.get(tit)
if (taskType !== undefined) {
return client.getHierarchy().isDerived(taskType.ofClass, baseClass)
}
return false
})
)
$: if (
availableProjectTypes.length > 0 &&
($selectedTypeStore === undefined ||
availableProjectTypes.find((it) => it._id === $selectedTypeStore) === undefined)
) {
$selectedTypeStore = availableProjectTypes[0]._id
}
$: if ($resolvedLocationStore.query?.type !== undefined) {
$selectedTypeStore = $resolvedLocationStore.query?.type as Ref<ProjectType>
}
$: if (currentProject?.type !== undefined) {
$selectedTypeStore = currentProject?.type
typeFixed = true
} else {
typeFixed = false
}
onDestroy(() => {
selectedTypeStore.set(undefined)
})
</script>
{#if !typeFixed}
<DropdownLabels
kind={'link'}
items={availableProjectTypes.map((it) => ({ id: it._id, label: it.name }))}
bind:selected={$selectedTypeStore}
/>
{/if}
{#if !allTypes}
<TaskKindSelector {baseClass} projectType={$selectedTypeStore} bind:taskType={$selectedTaskTypeStore} />
{/if}
@@ -14,27 +14,15 @@
-->
<script lang="ts">
import { Class, Doc, DocumentQuery, mergeQueries, Ref, Space, WithLookup } from '@hcengineering/core'
import { IntlString, getEmbeddedLabel } from '@hcengineering/platform'
import { IntlString } from '@hcengineering/platform'
import { createQuery } from '@hcengineering/presentation'
import { Project, ProjectType, ProjectTypeCategory } from '@hcengineering/task'
import {
AnyComponent,
Button,
Component,
IModeSelector,
IconAdd,
Label,
Loading,
ModeSelector,
SearchEdit,
resolvedLocationStore,
showPopup
} from '@hcengineering/ui'
import { ViewOptions, Viewlet, ViewletDescriptor, ViewletPreference } from '@hcengineering/view'
import { Project, ProjectType, ProjectTypeDescriptor } from '@hcengineering/task'
import { AnyComponent, Button, Component, IconAdd, Loading, SearchEdit, showPopup } from '@hcengineering/ui'
import { Viewlet, ViewletDescriptor, ViewletPreference, ViewOptions } from '@hcengineering/view'
import { FilterBar, FilterButton, ViewletSelector, ViewletSettingButton } from '@hcengineering/view-resources'
import { selectedTaskTypeStore, selectedTypeStore, taskTypeStore } from '..'
import task from '../plugin'
import { onDestroy } from 'svelte'
import { selectedTypeStore } from '..'
import TypeSelector from './TypeSelector.svelte'
export let _class: Ref<Class<Doc>>
export let space: Ref<Space> | undefined = undefined
@@ -45,7 +33,7 @@
export let isCreationDisabled = false
export let descriptors: Ref<ViewletDescriptor>[] | undefined = undefined
export let baseQuery: DocumentQuery<Doc> | undefined = undefined
export let category: Ref<ProjectTypeCategory>
export let descriptor: Ref<ProjectTypeDescriptor>
let search = ''
let viewlet: WithLookup<Viewlet> | undefined
@@ -53,16 +41,10 @@
let preference: ViewletPreference | undefined
let viewlets: WithLookup<Viewlet>[] = []
let viewOptions: ViewOptions | undefined
let types: ProjectType[] = []
const typeQ = createQuery()
$: typeQ.query(task.class.ProjectType, { category, archived: false }, (result) => {
types = result
})
const spacesQ = createQuery()
let spaces: Project[] = []
$: spacesQ.query(task.class.Project, { type: mode as Ref<ProjectType> }, (result) => {
$: spacesQ.query(task.class.Project, { type: $selectedTypeStore as Ref<ProjectType> }, (result) => {
spaces = result
})
let resultQuery: DocumentQuery<Doc>
@@ -70,44 +52,30 @@
$: searchQuery = search === '' ? query : { ...query, $search: search }
$: resultQuery = searchQuery
function showCreateDialog () {
function showCreateDialog (): void {
if (createComponent === undefined) return
showPopup(createComponent, createComponentProps, 'top')
}
$: mode = $resolvedLocationStore.query?.mode ?? undefined
$: allTypes = Array.from($taskTypeStore.values())
.filter((it) => it.parent === $selectedTypeStore)
.map((it) => it._id)
let config: Array<[string, IntlString, object]> = []
$: config = types.map((p) => {
return [p._id, getEmbeddedLabel(p.name), {}]
})
let modeSelectorProps: IModeSelector | undefined = undefined
$: if (mode === undefined && config.length > 0) {
;[[mode]] = config
selectedTypeStore.set(mode as Ref<ProjectType>)
}
$: if (mode !== undefined) {
modeSelectorProps = {
mode,
config,
onChange: (_mode: string) => {
mode = _mode
selectedTypeStore.set(mode as Ref<ProjectType>)
}
}
$: finalQuery = {
...resultQuery,
...($selectedTaskTypeStore !== undefined ? { kind: $selectedTaskTypeStore } : { kind: { $in: allTypes } })
}
onDestroy(() => {
selectedTypeStore.set(undefined)
})
$: totalQuery = {
...query,
...($selectedTaskTypeStore !== undefined ? { kind: $selectedTaskTypeStore } : { kind: { $in: allTypes } })
}
</script>
<div class="ac-header full divide caption-height">
<div class="ac-header__wrap-title mr-3">
<span class="ac-header__title"><Label {label} /></span>
{#if modeSelectorProps !== undefined && modeSelectorProps.config.length > 1}
<ModeSelector props={modeSelectorProps} />
{/if}
<TypeSelector baseClass={_class} />
<!-- <span class="ac-header__title"><Label {label} /></span> -->
</div>
<div class="ac-header-full medium-gap mb-1">
@@ -121,7 +89,7 @@
...(descriptors !== undefined ? { descriptor: { $in: descriptors } } : {})
}}
/>
{#if createLabel && createComponent}
{#if createLabel !== undefined && createComponent}
<Button
icon={IconAdd}
label={createLabel}
@@ -149,7 +117,7 @@
{#if !viewlet?.$lookup?.descriptor?.component || viewlet?.attachTo !== _class || (preference !== undefined && viewlet?._id !== preference.attachedTo)}
<Loading />
{:else if viewOptions && viewlet}
{:else if viewOptions !== undefined && viewlet}
<FilterBar
{_class}
{space}
@@ -171,8 +139,8 @@
viewOptionsConfig: viewlet.viewOptions?.other,
createItemDialog: createComponent,
createItemLabel: createLabel,
query: resultQuery,
totalQuery: query
query: finalQuery,
totalQuery
}}
/>
{/if}
@@ -0,0 +1,33 @@
<script lang="ts">
import { ColorDefinition, IconSize, getPlatformColorDef, themeStore } from '@hcengineering/ui'
import { createEventDispatcher, onMount } from 'svelte'
export let size: IconSize
export let fill: number = -1
const dispatch = createEventDispatcher()
const dispatchAccentColor = (color?: ColorDefinition) => dispatch('accent-color', color)
$: color = getPlatformColorDef(fill, $themeStore.dark)
$: dispatchAccentColor(color)
onMount(() => {
dispatchAccentColor(color)
})
</script>
<svg
class="svg-{size}"
fill={color?.icon ?? 'currentColor'}
style:flex-shrink={0}
viewBox="0 0 16 16"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M6.97975 1.07389C3.92356 1.52014 1.50831 3.94118 1.0708 7.0002H2.08287C2.50337 4.49345 4.47822 2.51374 6.9825 2.08599L6.97975 1.07389ZM8.98249 2.08014L8.97974 1.06812C12.055 1.49885 14.4896 3.92773 14.9291 7.0002H13.917C13.4945 4.48183 11.5033 2.4954 8.98249 2.08014ZM9.01467 13.9146C11.5201 13.4879 13.4962 11.5077 13.917 9.00019H14.929C14.4913 12.06 12.0748 14.4815 9.01742 14.9267L9.01467 13.9146ZM2.0829 9.0002C2.50529 11.5176 4.49522 13.5034 7.01468 13.9196L7.01743 14.9317C3.94351 14.4999 1.51022 12.0717 1.07083 9.0002H2.0829Z"
/>
</svg>
@@ -0,0 +1,38 @@
<script lang="ts">
import { ColorDefinition, IconSize, getPlatformColorDef, themeStore } from '@hcengineering/ui'
import { createEventDispatcher, onMount } from 'svelte'
export let size: IconSize = 'small'
export let fill: number = -1
const dispatch = createEventDispatcher()
const dispatchAccentColor = (color?: ColorDefinition) => dispatch('accent-color', color)
$: color = getPlatformColorDef(fill, $themeStore.dark)
$: dispatchAccentColor(color)
onMount(() => {
dispatchAccentColor(color)
})
</script>
<svg
class="svg-{size}"
fill={color?.icon ?? 'currentColor'}
style:flex-shrink={0}
viewBox="0 0 16 16"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M8,1C4.1,1,1,4.1,1,8c0,3.9,3.1,7,7,7c3.9,0,7-3.1,7-7C15,4.1,11.9,1,8,1z M8,14c-3.3,0-6-2.7-6-6s2.7-6,6-6s6,2.7,6,6S11.3,14,8,14z"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M10.5,5.5c-0.2-0.2-0.6-0.2-0.8,0L8,7.2L6.3,5.5c-0.2-0.2-0.6-0.2-0.8,0s-0.2,0.6,0,0.8L7.2,8L5.5,9.7c-0.2,0.2-0.2,0.6,0,0.8s0.6,0.2,0.8,0L8,8.8l1.7,1.7c0.2,0.2,0.6,0.2,0.8,0c0.2-0.2,0.2-0.6,0-0.8L8.8,8l1.7-1.7C10.8,6.1,10.8,5.7,10.5,5.5z"
/>
</svg>
@@ -0,0 +1,34 @@
<script lang="ts">
import { ColorDefinition, IconSize, getPlatformColorDef, themeStore } from '@hcengineering/ui'
import { createEventDispatcher, onMount } from 'svelte'
export let size: IconSize = 'small'
export let fill: number = -1
const dispatch = createEventDispatcher()
const dispatchAccentColor = (color?: ColorDefinition) => dispatch('accent-color', color)
$: color = getPlatformColorDef(fill, $themeStore.dark)
$: dispatchAccentColor(color)
onMount(() => {
dispatchAccentColor(color)
})
</script>
<svg
class="svg-{size}"
fill={color?.icon ?? 'currentColor'}
style:flex-shrink={0}
viewBox="0 0 16 16"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M8,1C4.1,1,1,4.1,1,8c0,3.9,3.1,7,7,7c3.9,0,7-3.1,7-7C15,4.1,11.9,1,8,1z M8,14c-3.3,0-6-2.7-6-6s2.7-6,6-6s6,2.7,6,6S11.3,14,8,14z"
/>
<path
d="M10.6,6.1L7.5,9.2L5.9,7.6c-0.2-0.2-0.6-0.2-0.8,0s-0.2,0.6,0,0.8l2,2c0.1,0.1,0.3,0.2,0.4,0.2s0.3-0.1,0.4-0.2l3.5-3.5c0.2-0.2,0.2-0.6,0-0.8S10.8,5.8,10.6,6.1z"
/>
</svg>
@@ -0,0 +1,53 @@
<script lang="ts">
import { ColorDefinition, IconSize, getPlatformColorDef, themeStore } from '@hcengineering/ui'
import { createEventDispatcher, onMount } from 'svelte'
export let size: IconSize = 'small'
export let fill: number = -1
const dispatch = createEventDispatcher()
const dispatchAccentColor = (color?: ColorDefinition) => dispatch('accent-color', color)
export let count: number | undefined = undefined
export let index: number | undefined = undefined
$: color = getPlatformColorDef(fill, $themeStore.dark)
$: dispatchAccentColor(color)
onMount(() => {
dispatchAccentColor(color)
})
</script>
<svg
class="svg-{size}"
fill={color?.icon ?? 'currentColor'}
style:flex-shrink={0}
style:transform={'rotate(-90deg)'}
viewBox="0 0 16 16"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M8 14C11.3137 14 14 11.3137 14 8C14 4.68629 11.3137 2 8 2C4.68629 2 2 4.68629 2 8C2 11.3137 4.68629 14 8 14ZM8 15C11.866 15 15 11.866 15 8C15 4.13401 11.866 1 8 1C4.13401 1 1 4.13401 1 8C1 11.866 4.13401 15 8 15Z"
/>
{#if count !== undefined && index}
<path
d="M 4.5 4.5 L 9 4.5 A 4.5 4.5 0 {index > (count - 1) / 2 ? 1 : 0} 1 {Math.cos(
((2 * Math.PI) / count) * index - 0.01
) *
4.5 +
4.5} {Math.sin(((2 * Math.PI) / count) * index - 0.01) * 4.5 + 4.5} Z"
transform="translate(3.5,3.5)"
/>
{:else}
<path
d="M 4.5 4.5 L 9 4.5 A 4.5 4.5 0 1 1 {Math.cos(Math.PI - 0.01) * 4.5 + 4.5} {Math.sin(Math.PI - 0.01) * 4.5 +
4.5} Z"
transform="translate(3.5,3.5)"
/>
{/if}
</svg>
@@ -0,0 +1,33 @@
<script lang="ts">
import { ColorDefinition, IconSize, getPlatformColorDef, themeStore } from '@hcengineering/ui'
import { createEventDispatcher, onMount } from 'svelte'
export let size: IconSize = 'small'
export let fill: number = -1
const dispatch = createEventDispatcher()
const dispatchAccentColor = (color?: ColorDefinition) => dispatch('accent-color', color)
$: color = getPlatformColorDef(fill, $themeStore.dark)
$: dispatchAccentColor(color)
onMount(() => {
dispatchAccentColor(color)
})
</script>
<svg
class="svg-{size}"
fill={color?.icon ?? 'currentColor'}
style:flex-shrink={0}
viewBox="0 0 16 16"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M8 14C11.3137 14 14 11.3137 14 8C14 4.68629 11.3137 2 8 2C4.68629 2 2 4.68629 2 8C2 11.3137 4.68629 14 8 14ZM8 15C11.866 15 15 11.866 15 8C15 4.13401 11.866 1 8 1C4.13401 1 1 4.13401 1 8C1 11.866 4.13401 15 8 15Z"
/>
</svg>
@@ -1,51 +0,0 @@
<!--
// Copyright © 2020, 2021 Anticrm Platform Contributors.
// Copyright © 2021, 2022, 2023 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, Status } from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation'
import { ProjectType, ProjectTypeCategory } from '@hcengineering/task'
import { statusStore } from '@hcengineering/view-resources'
import StatesProjectEditor from '../state/StatesProjectEditor.svelte'
export let type: ProjectType
export let category: ProjectTypeCategory
const client = getClient()
let states: Status[] = []
$: states = type.statuses.map((p) => $statusStore.byId.get(p._id)).filter((p) => p !== undefined) as Status[]
async function onMove ({ detail: { stateID, position } }: { detail: { stateID: Ref<Status>, position: number } }) {
const index = type.statuses.findIndex((p) => p._id === stateID)
const state = type.statuses.splice(index, 1)[0]
const statuses = [...type.statuses.slice(0, position), state, ...type.statuses.slice(position)]
await client.update(type, {
statuses
})
}
async function onDelete ({ detail: { state } }: { detail: { state: Status } }) {
const index = type.statuses.findIndex((p) => p._id === state._id)
type.statuses.splice(index, 1)
await client.update(type, {
statuses: type.statuses
})
}
</script>
<StatesProjectEditor {type} {category} {states} on:delete={onDelete} on:move={onMove} />
@@ -0,0 +1,111 @@
<!--
// 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.
-->
<script lang="ts">
import { Ref } from '@hcengineering/core'
import { createQuery, hasResource } from '@hcengineering/presentation'
import setting from '@hcengineering/setting'
import task, { ProjectTypeDescriptor } from '@hcengineering/task'
import { Component, Icon, IconChevronDown, Label } from '@hcengineering/ui'
export let descriptor: ProjectTypeDescriptor | undefined
export let descriptorId: Ref<ProjectTypeDescriptor> | undefined
let categories: ProjectTypeDescriptor[] = []
const query = createQuery()
$: query.query(task.class.ProjectTypeDescriptor, {}, (result) => {
categories = result
if (descriptorId !== undefined) {
descriptor = categories.find((p) => p._id === descriptorId)
}
})
$: if (descriptor === undefined && categories.length > 0) {
descriptor = categories.filter((f) => hasResource(f.icon))[0]
}
function select (item: ProjectTypeDescriptor): void {
descriptor = item
}
</script>
<div class="flex-between trans-title header mb-3">
<Label label={setting.string.Categories} />
</div>
<div class="flex-col overflow-y-auto">
{#each categories as f (f._id)}
{#if hasResource(f.icon)}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
class="flex-between item"
class:selected={f._id === descriptor?._id}
on:click={() => {
select(f)
}}
>
<div class="icon flex-no-shrink mr-4">
<Component is={f.icon} />
</div>
<div class="flex-grow flex-col">
<div class="fs-title overflow-label">
<Label label={f.name} />
</div>
<div class="text-sm content-dark-color overflow-label">
<Label label={f.description} />
</div>
</div>
{#if f._id === descriptor?._id}
<div class="caption-color ml-4">
<slot name="tools">
<Icon icon={IconChevronDown} size={'small'} />
</slot>
</div>
{/if}
</div>
<slot name="content" categoryId={f._id} />
{/if}
{/each}
</div>
<style lang="scss">
.header {
min-height: 1.75rem;
}
.item {
padding: 0.25rem 0.25rem 0.25rem 0.25rem;
// background-color: var(--theme-button-default);
border: 1px solid var(--theme-button-border);
border-radius: 0.75rem;
cursor: pointer;
&:hover {
background-color: var(--theme-button-hovered);
}
&.selected {
background-color: var(--theme-button-pressed);
cursor: auto;
}
}
.item + .item {
margin-top: 0.75rem;
}
.icon {
width: 2.25rem;
height: 2.25rem;
border-radius: 50%;
overflow: hidden;
}
</style>
@@ -0,0 +1,75 @@
<!--
// Copyright © 2020, 2021 Anticrm Platform Contributors.
// Copyright © 2021, 2022, 2023 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 { IdMap, Ref, WithLookup, toIdMap } from '@hcengineering/core'
import task, { ProjectType } from '@hcengineering/task'
import { Location, getCurrentResolvedLocation, navigate, resolvedLocationStore } from '@hcengineering/ui'
import { createQuery } from '@hcengineering/presentation'
import { onDestroy } from 'svelte'
import Types from './Types.svelte'
export let kind: 'navigation' | 'tools' | undefined
export let categoryName: string
let type: WithLookup<ProjectType> | undefined
let typeId: Ref<ProjectType> | undefined
onDestroy(
resolvedLocationStore.subscribe((loc) => {
void (async (loc: Location): Promise<void> => {
typeId = loc.path[4] as Ref<ProjectType>
})(loc)
})
)
function selectProjectType (id: string): void {
const loc = getCurrentResolvedLocation()
loc.path[3] = categoryName
loc.path[4] = id
loc.path.length = 5
navigate(loc)
}
let types: WithLookup<ProjectType>[] = []
let typeMap: IdMap<WithLookup<ProjectType>> = new Map()
const query = createQuery()
$: query.query(
task.class.ProjectType,
{ archived: false },
(result) => {
types = result
},
{
lookup: {
descriptor: task.class.ProjectTypeDescriptor
}
}
)
$: typeMap = toIdMap(types)
$: type = typeId !== undefined ? typeMap.get(typeId) : undefined
</script>
<div class="ml-8 flex-row mt-1">
<Types
{type}
{typeId}
{types}
on:change={(evt) => {
selectProjectType(evt.detail)
}}
/>
</div>
@@ -0,0 +1,43 @@
<!--
// Copyright © 2020, 2021 Anticrm Platform Contributors.
// Copyright © 2021, 2022, 2023 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, WithLookup } from '@hcengineering/core'
import { ProjectType } from '@hcengineering/task'
import { Location, resolvedLocationStore } from '@hcengineering/ui'
import { onDestroy } from 'svelte'
import { typeStore } from '../../'
import ProjectEditor from './ProjectEditor.svelte'
let type: WithLookup<ProjectType> | undefined
let typeId: Ref<ProjectType> | undefined
onDestroy(
resolvedLocationStore.subscribe((loc) => {
void (async (loc: Location): Promise<void> => {
typeId = loc.path[4] as Ref<ProjectType>
})(loc)
})
)
$: type = typeId !== undefined ? $typeStore.get(typeId) : undefined
</script>
<div class="p-1 w-full h-full">
{#if type !== undefined}
<ProjectEditor {type} descriptor={type.$lookup?.descriptor} />
{/if}
</div>
@@ -0,0 +1,59 @@
<!--
// Copyright © 2020, 2021 Anticrm Platform Contributors.
// Copyright © 2021, 2022, 2023 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 { generateId } from '@hcengineering/core'
import { Button, IconAdd, Menu, getEventPopupPositionElement, showPopup } from '@hcengineering/ui'
import { translate } from '@hcengineering/platform'
import { getClient } from '@hcengineering/presentation'
import task, { ProjectTypeDescriptor, createProjectType } from '@hcengineering/task'
const client = getClient()
async function createType (descriptor: ProjectTypeDescriptor): Promise<void> {
const descriptorName = await translate(descriptor.name, {})
await createProjectType(
client,
{
name: `New ${descriptorName} project type`,
descriptor: descriptor._id,
description: '',
tasks: []
},
[],
generateId()
)
}
async function chooseProjectType (evt: MouseEvent): Promise<void> {
const descriptors = client.getModel().findAllSync(task.class.ProjectTypeDescriptor, {})
showPopup(
Menu,
{
actions: descriptors.map((it) => ({
label: it.name,
action: () => {
void createType(it)
}
}))
},
getEventPopupPositionElement(evt)
)
}
</script>
<Button id="new-project-type" icon={IconAdd} kind={'link'} size="small" on:click={chooseProjectType} />
@@ -0,0 +1,343 @@
<!--
// Copyright © 2020, 2021 Anticrm Platform Contributors.
// Copyright © 2021, 2022, 2023 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 { createQuery, getClient } from '@hcengineering/presentation'
import task, { Project, ProjectType, ProjectTypeDescriptor, Task, TaskType } from '@hcengineering/task'
import { Ref, SortingOrder, Status } from '@hcengineering/core'
import { getEmbeddedLabel } from '@hcengineering/platform'
import {
Button,
Component,
EditBox,
Icon,
IconAdd,
IconCopy,
IconDelete,
IconFile,
IconMoreV,
Label,
Location,
Scroller,
eventToHTMLElement,
getCurrentResolvedLocation,
navigate,
resolvedLocationStore,
showPopup
} from '@hcengineering/ui'
import { ContextMenu } from '@hcengineering/view-resources'
import { onDestroy } from 'svelte'
import plugin from '../../plugin'
import CreateTaskType from '../taskTypes/CreateTaskType.svelte'
import TaskTypeEditor from '../taskTypes/TaskTypeEditor.svelte'
import TaskTypeIcon from '../taskTypes/TaskTypeIcon.svelte'
import TaskTypeKindEditor from '../taskTypes/TaskTypeKindEditor.svelte'
import TypeClassEditor from '../taskTypes/TypeClassEditor.svelte'
export let type: ProjectType
export let descriptor: ProjectTypeDescriptor | undefined
const client = getClient()
const query = createQuery()
const projectQuery = createQuery()
$: if (descriptor === undefined) {
void client.findOne(task.class.ProjectTypeDescriptor, { _id: type.descriptor }).then((res) => {
descriptor = res
})
}
let taskTypes: TaskType[] = []
let projects: Project[] = []
$: query.query(
task.class.TaskType,
{ _id: { $in: type?.tasks ?? [] } },
(res) => {
taskTypes = res
},
{ sort: { _id: SortingOrder.Ascending } }
)
async function onShortDescriptionChange (value: string): Promise<void> {
if (type !== undefined) {
await client.diffUpdate(type, { shortDescription: value })
}
}
$: projectQuery.query(task.class.Project, { type: type._id }, (res) => {
projects = res
})
const statsQuery = createQuery()
let tasks: Task[] = []
$: statsQuery.query(
task.class.Task,
{ kind: { $in: taskTypes.map((it) => it._id) } },
(res) => {
tasks = res
},
{
projection: {
_id: 1,
_class: 1,
space: 1,
status: 1,
kind: 1
}
}
)
$: statusCounter = tasks.reduce(
(map, task) => map.set(task.status, (map.get(task.status) ?? 0) + 1),
new Map<Ref<Status>, number>()
)
// $: spaceCounter = tasks.reduce(
// (map, task) => map.set(task.space, (map.get(task.space) ?? 0) + 1),
// new Map<Ref<Space>, number>()
// )
$: taskTypeCounter = tasks.reduce(
(map, task) => map.set(task.kind, (map.get(task.kind) ?? 0) + 1),
new Map<Ref<TaskType>, number>()
)
let selectedTaskTypeId: Ref<TaskType> | undefined
$: selectedTaskType = taskTypes.find((it) => it._id === selectedTaskTypeId)
onDestroy(
resolvedLocationStore.subscribe((loc) => {
void (async (loc: Location): Promise<void> => {
selectedTaskTypeId = loc.path[5] as Ref<TaskType>
})(loc)
})
)
function selectTaskType (id: string | undefined): void {
const loc = getCurrentResolvedLocation()
if (id !== undefined) {
loc.path[5] = id
loc.path.length = 6
} else {
loc.path.length = 5
}
selectedTaskTypeId = id as Ref<TaskType>
navigate(loc)
}
</script>
<div class="h-full flex-col w-full">
{#if type !== undefined && descriptor !== undefined}
<div class="p-2 bottom-divider flex-row-center">
<div class="button-group flex-row-center right-divider pr-2">
<Button
icon={IconDelete}
kind={'regular'}
on:click={(ev) => {
// Ask for delete
}}
/>
<Button
icon={IconCopy}
kind={'regular'}
on:click={(ev) => {
// Do copy of type
}}
/>
<Button
icon={IconMoreV}
kind={'regular'}
on:click={(ev) => {
showPopup(ContextMenu, { object: type }, eventToHTMLElement(ev), () => {})
}}
/>
</div>
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div class="fs-title ml-2 flex-row-center">
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
class="hover-trans"
on:click={() => {
selectTaskType(undefined)
}}
>
<Label label={plugin.string.ProjectType} />
</div>
{#if selectedTaskType !== undefined}
<span class="p-1">/</span>
{selectedTaskType.name}
{/if}
</div>
</div>
<div class="h-full flex-row-top w-full justify-center">
<div class="h-full editorBox flex-col">
<Scroller padding={'0 1rem'} noStretch shrink>
{#if selectedTaskType === undefined}
<!-- <div class="flex-col">Navigation</div> -->
<div class="flex-grow h-full">
<div class="p-4 flex-col">
<div>
<div class="flex-row-center flex-between mb-2">
<div>
<Component is={descriptor.icon} props={{ size: 'large' }} />
</div>
<div class="flex-row-center no-word-wrap">
<Icon icon={IconFile} size={'small'} />
{projects.length} projects
</div>
</div>
<EditBox
kind={'large-style'}
value={type?.name ?? ''}
on:blur={(evt) => {
if (type !== undefined) {
void client.diffUpdate(type, { name: evt.detail })
}
}}
/>
<div class="p-2">
<EditBox
placeholder={getEmbeddedLabel('Description')}
kind={'small-style'}
bind:value={type.shortDescription}
on:change={() => onShortDescriptionChange(type?.shortDescription ?? '')}
/>
</div>
{#if descriptor?.editor}
<Component is={descriptor.editor} props={{ type }} />
{/if}
</div>
<div class="panelBox flex-col row">
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div class="fs-title flex flex-between bottom-divider">
<div class="trans-title">
<Label label={getEmbeddedLabel('Task types')} />
</div>
<div class="p-1">
<Button
icon={IconAdd}
kind={'primary'}
size={'small'}
on:click={(event) => {
showPopup(CreateTaskType, { type, descriptor }, 'top')
}}
/>
</div>
</div>
<div class="mt-1">
<!-- svelte-ignore a11y-no-static-element-interactions -->
{#each taskTypes as taskType}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
class="flex-grow p-2 antiButton sh-round flex-row-center"
class:regular={taskType._id === selectedTaskTypeId}
class:ghost={taskType._id !== selectedTaskTypeId}
on:click|stopPropagation={() => {
selectTaskType(taskType._id)
}}
>
<div class="p-2">
<TaskTypeIcon value={taskType} size={'small'} />
</div>
<div class="fs-title">
{taskType.name}
</div>
<div class="ml-2 text-sm">
<TaskTypeKindEditor readonly kind={taskType.kind} buttonKind={'link'} />
</div>
</div>
{/each}
</div>
</div>
<div class="panelBox flex-col row">
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div class="fs-title flex flex-between bottom-divider">
<div class="trans-title">
<Label label={getEmbeddedLabel('Automation')} />
</div>
<div class="p-1">
<Button icon={IconAdd} kind={'primary'} size={'small'} on:click={(event) => {}} />
</div>
</div>
<div class="mt-1">
<!-- svelte-ignore a11y-no-static-element-interactions -->
</div>
</div>
<div class="panelBox flex-col row">
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div class="fs-title flex flex-between bottom-divider">
<div class="trans-title">
<Label label={getEmbeddedLabel('Collections')} />
</div>
<div class="p-1">
<Button icon={IconAdd} kind={'primary'} size={'small'} on:click={(event) => {}} />
</div>
</div>
<div class="mt-1">
<!-- svelte-ignore a11y-no-static-element-interactions -->
</div>
</div>
<div class="panelBox flex-col row">
<div class="mt-1">
<TypeClassEditor ofClass={descriptor.baseClass} _class={type.targetClass} />
</div>
</div>
</div>
</div>
{:else}
<TaskTypeEditor
taskType={selectedTaskType}
projectType={type}
{taskTypes}
{taskTypeCounter}
{statusCounter}
/>
{/if}
</Scroller>
</div>
</div>
{/if}
</div>
<style lang="scss">
.row {
// border-bottom: 1px solid var(--theme-list-border-color);
// border-top: 1px solid var(--theme-list-border-color);
width: 100%;
}
.panelBox {
margin-top: 1rem;
padding: 0.5rem;
// TODO: Need to update it
border: 1px solid var(--theme-button-border);
background: var(--theme-button-default);
border-radius: 8px;
}
.editorBox {
max-width: 1024px;
min-width: 768px;
}
</style>
@@ -0,0 +1,38 @@
<!--
// Copyright © 2023 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 '@hcengineering/core'
import { getClient } from '@hcengineering/presentation'
import task, { type ProjectType } from '@hcengineering/task'
import { Component } from '@hcengineering/ui'
import { typeStore } from '../..'
export let value: ProjectType | Ref<ProjectType> | undefined
$: descriptor = getClient().getModel().findAllSync(task.class.ProjectTypeDescriptor, {}).shift()
$: _value = typeof value === 'string' ? $typeStore.get(value) : value
</script>
{#if _value !== undefined}
<span class="label flex-row-center gap-1 ml-3 no-word-wrap">
{#if descriptor?.icon}
<Component is={descriptor?.icon} props={{ size: 'small' }} />
{/if}
<span class:ml-1={descriptor?.icon !== undefined}>
{_value.name}
</span>
</span>
{/if}
@@ -15,14 +15,14 @@
<script lang="ts">
import type { Ref } from '@hcengineering/core'
import { createQuery } from '@hcengineering/presentation'
import type { ProjectType, ProjectTypeCategory } from '@hcengineering/task'
import type { ProjectType, ProjectTypeDescriptor } from '@hcengineering/task'
import task from '@hcengineering/task'
import type { DropdownTextItem } from '@hcengineering/ui'
import { ButtonKind, ButtonSize, DropdownLabels } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
import plugin from '../../plugin'
export let categories: Ref<ProjectTypeCategory>[]
export let descriptors: Ref<ProjectTypeDescriptor>[]
export let type: Ref<ProjectType> | undefined = undefined
export let kind: ButtonKind = 'no-border'
export let size: ButtonSize = 'small'
@@ -31,7 +31,14 @@
let types: ProjectType[] = []
const typesQ = createQuery()
const query = disabled ? { category: { $in: categories } } : { category: { $in: categories }, archived: false }
const query = disabled
? {
descriptor: { $in: descriptors }
}
: {
descriptor: { $in: descriptors },
archived: false
}
$: typesQ.query(task.class.ProjectType, query, (result) => {
types = result
})
@@ -0,0 +1,65 @@
<!--
// 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.
-->
<script lang="ts">
import { Ref, WithLookup } from '@hcengineering/core'
import { ProjectType } from '@hcengineering/task'
import { Component, Label, ListView } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
export let type: ProjectType | undefined
export let typeId: Ref<ProjectType> | undefined
export let types: WithLookup<ProjectType>[] = []
const dispatch = createEventDispatcher()
function select (item: ProjectType): void {
typeId = item._id
dispatch('change', typeId)
}
$: selection = types.findIndex((it) => it._id === typeId)
</script>
<div id="templates" class="flex-col w-full">
<ListView
count={types.length}
{selection}
on:click={(evt) => {
select(types[evt.detail])
}}
updateOnMouse={false}
>
<svelte:fragment slot="item" let:item>
{@const typeItem = types[item]}
<div class="flex-row flex-between p-1">
<div class="flex-col">
<div class="flex-row-center">
{#if typeItem.$lookup?.descriptor?.icon}
<div class="p-1">
<Component is={typeItem.$lookup?.descriptor?.icon} props={{ size: 'medium' }} />
</div>
{/if}
{typeItem.name}
</div>
{#if typeItem.$lookup?.descriptor}
<div class="text-sm">
<Label label={typeItem.$lookup?.descriptor.name} />
</div>
{/if}
</div>
</div>
</svelte:fragment>
</ListView>
</div>
@@ -21,6 +21,7 @@
import { statusStore } from '@hcengineering/view-resources'
import StatePresenter from './StatePresenter.svelte'
import StatesPopup from './StatesPopup.svelte'
import { selectedTaskTypeStore, selectedTypeStore } from '../../index'
export let value: Ref<Status>
export let space: Ref<Project>
@@ -51,7 +52,16 @@
</script>
{#if kind === 'list' || kind === 'list-header'}
<StatePresenter value={state} {space} {shouldShowName} {disabled} shouldShowTooltip on:click={handleClick} />
<StatePresenter
value={state}
{space}
{shouldShowName}
{disabled}
shouldShowTooltip
on:click={handleClick}
taskType={$selectedTaskTypeStore}
projectType={$selectedTypeStore}
/>
{:else}
<Button {kind} {size} {width} {justify} {shrink} on:click={handleClick}>
<svelte:fragment slot="content">
@@ -14,9 +14,26 @@
// limitations under the License.
-->
<script lang="ts">
import { Ref, Status } from '@hcengineering/core'
import { Project } from '@hcengineering/task'
import StateIconPresenter from './StateIconPresenter.svelte'
import core, { IdMap, Ref, Status, StatusCategory } from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation'
import task, { Project, ProjectType, TaskType } from '@hcengineering/task'
import {
ColorDefinition,
Icon,
IconSize,
IconWithEmoji,
getColorNumberByText,
getPlatformColorDef,
themeStore
} from '@hcengineering/ui'
import view from '@hcengineering/view'
import { statusStore } from '@hcengineering/view-resources'
import { createEventDispatcher, onMount } from 'svelte'
import { typeStore } from '../..'
import IconBacklog from '../icons/IconBacklog.svelte'
import IconCanceled from '../icons/IconCanceled.svelte'
import IconCompleted from '../icons/IconCompleted.svelte'
import IconStarted from '../icons/IconStarted.svelte'
export let value: Status | undefined
export let shouldShowAvatar = true
@@ -28,7 +45,82 @@
export let noUnderline: boolean = false
export let accent: boolean = false
export let shrink: number = 0
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: IconSize = 'small'
const dispatch = createEventDispatcher()
let type: ProjectType | undefined = undefined
let category: StatusCategory | undefined
$: void getType(space, projectType, $typeStore)
const client = getClient()
async function getType (
space: Ref<Project> | undefined,
projectType: Ref<ProjectType> | undefined,
types: IdMap<ProjectType>
): Promise<ProjectType | undefined> {
if (projectType !== undefined) {
type = types.get(projectType)
return
}
const _space = await client.findOne(task.class.Project, { _id: space })
if (_space === undefined) {
type = undefined
return
}
type = types.get(_space.type)
}
$: projectState = type?.statuses.find((p) => p._id === value?._id)
const dispatchAccentColor = (color?: ColorDefinition): void => {
dispatch('accent-color', color)
}
$: color =
projectState?.color !== undefined
? getPlatformColorDef(
projectState.color ?? category?.color ?? getColorNumberByText(value?.name ?? ''),
$themeStore.dark
)
: undefined
$: dispatchAccentColor(color)
onMount(() => {
dispatchAccentColor(color)
})
$: void updateCategory(value)
async function updateCategory (value: Status | undefined): Promise<void> {
if (value === undefined) {
category = undefined
return
}
category = await client.findOne(core.class.StatusCategory, { _id: value.category })
}
const categoryIcons = {
[task.statusCategory.UnStarted]: IconBacklog,
[task.statusCategory.Active]: IconStarted,
[task.statusCategory.Won]: IconCompleted,
[task.statusCategory.Lost]: IconCanceled
}
$: sameCategory = (type?.statuses ?? []).filter(
(it) =>
$statusStore.byId.get(it._id)?.category === value?.category &&
(taskType === undefined || it.taskType === taskType)
)
$: index = sameCategory.findIndex((it) => it._id === value?._id) + 1
$: icon = projectState?.icon === view.ids.IconWithEmoji ? IconWithEmoji : projectState?.icon
</script>
{#if value}
@@ -42,8 +134,27 @@
on:click
>
{#if shouldShowAvatar}
<div class:mr-2={shouldShowName}>
<StateIconPresenter {value} {shouldShowTooltip} {space} />
<div
class="state-container"
class:inline
class:mr-2={shouldShowName}
title={shouldShowTooltip ? value.name : undefined}
>
{#if icon != null}
<Icon {icon} {size} iconProps={{ icon: projectState?.color ?? category?.color }} />
{:else if category?._id === task.statusCategory.Active}
<Icon
icon={categoryIcons[category._id]}
{size}
iconProps={{ index, count: sameCategory.length + 1, fill: projectState?.color ?? category?.color }}
/>
{:else}
<Icon
icon={categoryIcons[category?._id ?? task.statusCategory.UnStarted]}
{size}
iconProps={{ fill: projectState?.color ?? category?.color }}
/>
{/if}
</div>
{/if}
{#if shouldShowName}
@@ -53,3 +164,13 @@
{/if}
</div>
{/if}
<style lang="scss">
.state-container {
flex-shrink: 0;
&.inline {
transform: translateY(0.125rem);
}
}
</style>
@@ -30,6 +30,8 @@
export let shrink: number = 0
export let disabled: boolean = false
import { selectedTaskTypeStore, selectedTypeStore } from '../../index'
$: state = $statusStore.byId.get(value)
</script>
@@ -37,6 +39,15 @@
{#if onChange !== undefined && state !== undefined}
<StateEditor value={state._id} {space} {onChange} {kind} {size} {shouldShowName} {shrink} {disabled} />
{:else}
<StatePresenter value={state} {space} {shouldShowName} {disabled} {shrink} on:accent-color />
<StatePresenter
value={state}
{space}
{shouldShowName}
{disabled}
{shrink}
on:accent-color
taskType={$selectedTaskTypeStore}
projectType={$selectedTypeStore}
/>
{/if}
{/if}
@@ -14,17 +14,11 @@
// limitations under the License.
-->
<script lang="ts">
import core, { IdMap, Ref, Status, StatusCategory, toIdMap } from '@hcengineering/core'
import { createQuery, getClient } from '@hcengineering/presentation'
import { IdMap, Ref, Status } from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation'
import task, { Project, ProjectType } from '@hcengineering/task'
import {
ColorDefinition,
getColorNumberByText,
getPlatformColorDef,
resizeObserver,
themeStore
} from '@hcengineering/ui'
import { statusStore } from '@hcengineering/view-resources'
import { resizeObserver } from '@hcengineering/ui'
import { ObjectPresenter, statusStore } from '@hcengineering/view-resources'
import { createEventDispatcher } from 'svelte'
import { typeStore } from '../..'
@@ -38,19 +32,6 @@
: []
const dispatch = createEventDispatcher()
function getColor (state: Status, type: ProjectType | undefined, categories: IdMap<StatusCategory>): ColorDefinition {
const category = state.category ? categories.get(state.category) : undefined
const targetColor = type?.statuses?.find((p) => p._id === state._id)?.color ?? state.color ?? category?.color
return getPlatformColorDef(targetColor ?? getColorNumberByText(state.name), $themeStore.dark)
}
let categories: IdMap<StatusCategory> = new Map()
const q = createQuery()
q.query(core.class.StatusCategory, {}, (res) => {
categories = toIdMap(res)
})
$: getType(space, $typeStore)
const client = getClient()
@@ -71,15 +52,20 @@
<div class="scroll">
<div class="box">
{#each states as state}
{@const color = getColor(state, type, categories)}
<button
class="menu-item"
on:click={() => {
dispatch('close', state)
}}
>
<div class="color" style:background-color={color.color} />
<span class="label">{state.name}</span>
<ObjectPresenter
_class={state._class}
objectId={state._id}
value={state}
props={{ projectType: type?._id }}
/>
<!-- <div class="color" style:background-color={color.color} />
<span class="label">{state.name}</span> -->
</button>
{/each}
</div>
@@ -1,6 +1,5 @@
<!--
// Copyright © 2020, 2021 Anticrm Platform Contributors.
// Copyright © 2021 Hardcore Engineering Inc.
// Copyright © 2023 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
@@ -15,12 +14,12 @@
-->
<script lang="ts">
import core, { Attribute, IdMap, Ref, Status, StatusCategory, toIdMap } from '@hcengineering/core'
import { ComponentExtensions, createQuery, getClient } from '@hcengineering/presentation'
import { ProjectType, ProjectTypeCategory } from '@hcengineering/task'
import { Asset } from '@hcengineering/platform'
import { createQuery, getClient } from '@hcengineering/presentation'
import { ProjectStatus, ProjectType, TaskType, findStatusAttr, isTaskCategory } from '@hcengineering/task'
import {
CircleButton,
ColorDefinition,
Component,
IconAdd,
IconCircles,
IconMoreH,
@@ -32,14 +31,15 @@
showPopup,
themeStore
} from '@hcengineering/ui'
import { ColorsPopup, StringPresenter } from '@hcengineering/view-resources'
import { ColorsPopup, IconPicker, ObjectPresenter } from '@hcengineering/view-resources'
import { createEventDispatcher } from 'svelte'
import task from '../../plugin'
import StatusesPopup from './StatusesPopup.svelte'
export let taskType: TaskType
export let type: ProjectType
export let category: ProjectTypeCategory
export let states: Status[] = []
export let statusCounter = new Map<Ref<Status>, number>()
const dispatch = createEventDispatcher()
const client = getClient()
@@ -58,7 +58,7 @@
return false
}
function dragover (ev: MouseEvent, i: number) {
function dragover (ev: MouseEvent, i: number): void {
const s = selected as number
if (dragswap(ev, i)) {
;[states[i], states[s]] = [states[s], states[i]]
@@ -66,7 +66,7 @@
}
}
async function onMove (to: number) {
function onMove (to: number): void {
dispatch('move', {
stateID: dragState,
position: to
@@ -82,39 +82,48 @@
if (targetColor !== undefined) {
targetColor.color = res
await client.update(type, { statuses: type.statuses })
type = type
}
})
}
const categoryEditor = category.editor
function add (ofAttribute: Ref<Attribute<Status>>, cat: Ref<StatusCategory>) {
function add (ofAttribute: Ref<Attribute<Status>> | undefined, cat: Ref<StatusCategory>): void {
if (ofAttribute === undefined) {
return
}
showPopup(task.component.CreateStatePopup, {
ofAttribute,
_class: category.statusClass,
_class: taskType.statusClass,
category: cat,
taskType,
type
})
}
function edit (status: Status) {
showPopup(task.component.CreateStatePopup, { status, type, ofAttribute: status.ofAttribute })
function edit (status: Status): void {
showPopup(task.component.CreateStatePopup, {
status,
taskType,
type,
ofAttribute: status.ofAttribute
})
}
let categories: StatusCategory[] = []
let categoriesMap: IdMap<StatusCategory> = new Map()
let groups = new Map<Ref<StatusCategory>, Status[]>()
const query = createQuery()
$: query.query(core.class.StatusCategory, { _id: { $in: category.statusCategories } }, (res) => {
$: query.query(core.class.StatusCategory, { _id: { $in: taskType.statusCategories } }, (res) => {
categories = res
categoriesMap = toIdMap(res)
})
function click (ev: MouseEvent, state: Status) {
function click (ev: MouseEvent, state: Status): void {
showPopup(
StatusesPopup,
{
onDelete: () => dispatch('delete', { state }),
showDelete: states.length > 1,
showDelete: states.filter((it) => it.category === state.category).length > 1,
onUpdate: () => {
edit(state)
}
@@ -124,9 +133,17 @@
)
}
function getColor (state: Status, categoriesMap: IdMap<StatusCategory>): ColorDefinition {
const category = state.category ? categoriesMap.get(state.category) : undefined
const targetColor = type.statuses.find((p) => p._id === state._id)?.color ?? state.color ?? category?.color
function getProjectStatus (
type: ProjectType,
state: Status,
categoriesMap: IdMap<StatusCategory>
): ProjectStatus | undefined {
return type.statuses.find((p) => p._id === state._id)
}
function getColor (type: ProjectType, state: Status, categoriesMap: IdMap<StatusCategory>): ColorDefinition {
const category = state.category !== undefined ? categoriesMap.get(state.category) : undefined
const targetColor = getProjectStatus(type, state, categoriesMap)?.color ?? state.color ?? category?.color
return getPlatformColorDef(targetColor ?? getColorNumberByText(state.name), $themeStore.dark)
}
@@ -153,71 +170,101 @@
}
return index
}
function selectIcon (el: HTMLElement, state: Status): void {
const icons: Asset[] = []
const projectStatus = getProjectStatus(type, state, categoriesMap)
showPopup(IconPicker, { icon: projectStatus?.icon, color: projectStatus?.color, icons }, el, async (result) => {
if (result !== undefined && result !== null) {
const targetColor = type.statuses.find((p) => p._id === state._id)
if (targetColor !== undefined) {
targetColor.color = result.color
targetColor.icon = result.icon
console.log(result.color)
await client.update(type, { statuses: type.statuses })
type = type
}
}
})
}
</script>
{#if categoryEditor}
<Component is={categoryEditor} props={{ type }} />
{/if}
<ComponentExtensions extension={task.extensions.ProjectEditorExtension} props={{ type }} />
{#each categories as cat, i}
{@const states = groups.get(cat._id) ?? []}
{@const prevIndex = getPrevIndex(groups, cat._id)}
<div class="flex-no-shrink flex-between trans-title uppercase" class:mt-4={i > 0}>
<Label label={cat.label} />
<CircleButton
icon={IconAdd}
size={'medium'}
on:click={() => {
add(cat.ofAttribute, cat._id)
}}
/>
</div>
<div class="flex-col flex-no-shrink mt-3">
{#each states as state, i}
{@const color = getColor(state, categoriesMap)}
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
bind:this={elements[prevIndex + i]}
class="flex-between states"
style:background={color.background ?? defaultBackground($themeStore.dark)}
draggable={true}
on:dragover|preventDefault={(ev) => {
dragover(ev, i + prevIndex)
<div class="flex-col p-2">
<div class="flex-no-shrink flex-between trans-title uppercase" class:mt-4={i > 0}>
<Label label={cat.label} />
<CircleButton
icon={IconAdd}
size={'medium'}
on:click={() => {
add(findStatusAttr(getClient().getHierarchy(), taskType.ofClass)?._id, cat._id)
}}
on:drop|preventDefault={() => {
onMove(prevIndex + i)
}}
on:dragstart={() => {
selected = i + prevIndex
dragState = states[i]._id
}}
on:dragend={() => {
selected = undefined
}}
>
<div class="bar"><IconCircles size={'small'} /></div>
<!-- svelte-ignore a11y-click-events-have-key-events -->
/>
</div>
<div class="flex-col flex-no-shrink mt-3">
{#each states as state, i}
{@const color = getColor(type, state, categoriesMap)}
{@const counter = statusCounter.get(state._id)}
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
class="color"
style:background-color={color.color}
on:click={() => {
onColor(state, color, elements[i + prevIndex])
bind:this={elements[prevIndex + i]}
class="flex-row-center flex-between states"
style:background={color.background ?? defaultBackground($themeStore.dark)}
draggable={true}
on:dragover|preventDefault={(ev) => {
dragover(ev, i + prevIndex)
}}
/>
<div class="flex-grow caption-color">
<StringPresenter value={state.name} oneLine />
</div>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
class="tool hover-trans"
on:click={(e) => {
click(e, state)
on:drop|preventDefault={() => {
onMove(prevIndex + i)
}}
on:dragstart={() => {
selected = i + prevIndex
dragState = states[i]._id
}}
on:dragend={() => {
selected = undefined
}}
>
<IconMoreH size={'medium'} />
<div class="bar"><IconCircles size={'small'} /></div>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
class="color"
on:click={(ev) => {
if (state.category !== undefined && isTaskCategory(state.category)) {
selectIcon(elements[i + prevIndex], state)
} else {
onColor(state, color, elements[i + prevIndex])
}
}}
>
<ObjectPresenter
_class={state._class}
objectId={state._id}
value={state}
props={{ projectType: type._id, taskType: taskType._id }}
/>
</div>
<div class="flex-grow caption-color no-word-wrap">
<!-- <StringPresenter value={state.name} oneLine /> -->
{#if counter !== undefined}
- {counter}
{/if}
</div>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
class="tool hover-trans"
on:click={(e) => {
click(e, state)
}}
>
<IconMoreH size={'medium'} />
</div>
</div>
</div>
{/each}
{/each}
</div>
</div>
{/each}
@@ -238,10 +285,6 @@
cursor: grabbing;
}
.color {
margin-right: 0.75rem;
width: 1rem;
height: 1rem;
border-radius: 0.25rem;
cursor: pointer;
}
.tool {
@@ -0,0 +1,163 @@
<!--
// Copyright © 2023 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 core, { Class, ClassifierKind, Data, Ref, RefTo, Status, generateId, toIdMap } from '@hcengineering/core'
import { getEmbeddedLabel } from '@hcengineering/platform'
import presentation, { Card, getClient } from '@hcengineering/presentation'
import {
ProjectType,
ProjectTypeDescriptor,
Task,
TaskType,
TaskTypeDescriptor,
createState,
findStatusAttr
} from '@hcengineering/task'
import { DropdownIntlItem, EditBox, getColorNumberByText } from '@hcengineering/ui'
import DropdownLabelsIntl from '@hcengineering/ui/src/components/DropdownLabelsIntl.svelte'
import { createEventDispatcher } from 'svelte'
import task from '../../plugin'
import TaskTypeKindEditor from './TaskTypeKindEditor.svelte'
const dispatch = createEventDispatcher()
const client = getClient()
export let type: ProjectType
export let descriptor: ProjectTypeDescriptor
export let taskType: TaskType
function defaultTaskType (type: ProjectType): Data<TaskType> {
return {
kind: 'task',
name: '',
parent: type._id,
descriptor: '' as Ref<TaskTypeDescriptor>,
ofClass: task.class.Task,
targetClass: task.class.Task,
statusCategories: [
task.statusCategory.UnStarted,
task.statusCategory.Active,
task.statusCategory.Won,
task.statusCategory.Lost
],
statusClass: core.class.Status,
statuses: [],
allowedAsChildOf: []
}
}
const taskTypeDescriptors = client.getModel().findAllSync(task.class.TaskTypeDescriptor, { allowCreate: true })
let { kind, name, targetClass, statusCategories, statuses, allowedAsChildOf } =
taskType !== undefined ? { ...taskType } : { ...defaultTaskType(type) }
function findStatusClass (_class: Ref<Class<Task>>): Ref<Class<Status>> | undefined {
const h = getClient().getHierarchy()
const attrs = h.getAllAttributes(_class)
for (const it of attrs.values()) {
if (it.type._class === core.class.RefTo && h.isDerived((it.type as RefTo<any>).to, core.class.Status)) {
return (it.type as RefTo<any>).to
}
}
}
let taskTypeDescriptor: Ref<TaskTypeDescriptor> = taskTypeDescriptors[0]._id
async function save (): Promise<void> {
if (type === undefined) return
const descr = taskTypeDescriptors.find((it) => it._id === taskTypeDescriptor)
if (descr === undefined) return
const ofClass = descr.baseClass
const _taskType = {
kind,
name,
ofClass,
descriptor: taskTypeDescriptor,
targetClass,
statusCategories,
statuses,
allowedAsChildOf,
statusClass: findStatusClass(ofClass) ?? core.class.Status,
parent: type._id,
icon: descr.icon
}
const taskTypeId: Ref<TaskType> = taskType?._id ?? generateId()
const categories = toIdMap(await client.findAll(core.class.StatusCategory, { _id: { $in: statusCategories } }))
const statusAttr =
findStatusAttr(client.getHierarchy(), ofClass) ?? client.getHierarchy().getAttribute(task.class.Task, 'status')
for (const st of statusCategories) {
const std = categories.get(st)
if (std !== undefined) {
const s = await createState(client, _taskType.statusClass, {
name: std.defaultStatusName,
ofAttribute: statusAttr._id,
category: std._id
})
_taskType.statuses.push(s)
if (type.statuses.find((it) => it._id === s) === undefined) {
await client.update(type, {
$push: { statuses: { _id: s, color: getColorNumberByText(std.defaultStatusName), taskType: taskTypeId } }
})
}
}
}
if (taskType !== undefined) {
await client.diffUpdate(taskType, _taskType)
} else {
const ofClassClass = client.getHierarchy().getClass(ofClass)
// Create target class for custom field.
_taskType.targetClass = await client.createDoc(core.class.Class, core.space.Model, {
extends: ofClass,
kind: ClassifierKind.MIXIN,
label: getEmbeddedLabel(name),
icon: ofClassClass.icon
})
await client.createDoc(task.class.TaskType, type._id, _taskType, taskTypeId)
}
if (!type.tasks.includes(taskTypeId)) {
await client.update(type, { $push: { tasks: taskTypeId } })
}
dispatch('close')
}
const descriptorItems: DropdownIntlItem[] = taskTypeDescriptors.map((it) => ({ id: it._id, label: it.name }))
</script>
<Card
label={task.string.TaskType}
okAction={save}
canSave
okLabel={taskType !== undefined ? presentation.string.Save : presentation.string.Create}
on:changeContent
onCancel={() => dispatch('close')}
>
<EditBox
focusIndex={1}
bind:value={name}
placeholder={task.string.TaskName}
kind={'large-style'}
autoFocus
fullSize
/>
<div class="p-1 flex-row-center mt-4">
<TaskTypeKindEditor bind:kind />
<DropdownLabelsIntl kind={'regular'} size={'large'} items={descriptorItems} bind:selected={taskTypeDescriptor} />
</div>
</Card>
@@ -0,0 +1,20 @@
<script lang="ts">
import { Class } from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation'
import { Project } from '@hcengineering/task'
import { typeStore } from '../..'
import task from '../../plugin'
export let value: Class<Project>
const client = getClient()
$: asValue = client.getHierarchy().hasMixin(value, task.mixin.ProjectTypeClass)
? client.getHierarchy().as(value, task.mixin.ProjectTypeClass)
: undefined
$: typeName = asValue !== undefined ? $typeStore.get(asValue.projectType) : undefined
</script>
{#if typeName}
({typeName.name})
{/if}
@@ -0,0 +1,43 @@
<script lang="ts">
import { Class, Doc, Ref, toIdMap } from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation'
import task, { ProjectType, TaskType } from '@hcengineering/task'
import { DropdownLabels } from '@hcengineering/ui'
import { onDestroy } from 'svelte'
import { selectedTaskTypeStore, taskTypeStore } from '../..'
export let projectType: Ref<ProjectType> | undefined
export let taskType: Ref<TaskType> | undefined
export let focusIndex: number = -1
export let baseClass: Ref<Class<Doc>> | undefined = undefined
export let allTypes = false
const client = getClient()
$: taskTypeDescriptors = toIdMap(client.getModel().findAllSync(task.class.TaskTypeDescriptor, {}))
$: items = Array.from($taskTypeStore.values())
.filter(
(it) =>
it.parent === projectType &&
(taskTypeDescriptors.get(it.descriptor)?.allowCreate ?? false) &&
(baseClass === undefined || client.getHierarchy().isDerived(it.targetClass, baseClass))
)
.map((it) => ({ id: it._id, label: it.name }))
$: if (
(taskType === undefined && items.length > 0) ||
(items.length > 0 && items.find((it) => it.id === taskType) === undefined)
) {
taskType = items[0].id
}
onDestroy(() => {
$selectedTaskTypeStore = undefined
})
</script>
{#if projectType !== undefined && items.length > 1}
<div class="p-1">
<DropdownLabels {focusIndex} kind={'regular'} {items} bind:selected={taskType} enableSearch={false} />
</div>
{/if}
@@ -0,0 +1,20 @@
<script lang="ts">
import { Class } from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation'
import { Task } from '@hcengineering/task'
import task from '../../plugin'
import { typeStore } from '../..'
export let value: Class<Task>
const client = getClient()
$: asValue = client.getHierarchy().hasMixin(value, task.mixin.TaskTypeClass)
? client.getHierarchy().as(value, task.mixin.TaskTypeClass)
: undefined
$: typeName = asValue !== undefined ? $typeStore.get(asValue.projectType) : undefined
</script>
{#if typeName}
({typeName.name})
{/if}
@@ -0,0 +1,178 @@
<!--
// Copyright © 2020, 2021 Anticrm Platform Contributors.
// Copyright © 2021, 2022, 2023 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 { AttributeEditor, getClient } from '@hcengineering/presentation'
import task, { ProjectType, TaskType, calculateStatuses } from '@hcengineering/task'
import { Ref, Status } from '@hcengineering/core'
import { Asset, getEmbeddedLabel } from '@hcengineering/platform'
import { Label, showPopup } from '@hcengineering/ui'
import { IconPicker, statusStore } from '@hcengineering/view-resources'
import { taskTypeStore } from '../..'
import StatesProjectEditor from '../state/StatesProjectEditor.svelte'
import TaskTypeKindEditor from '../taskTypes/TaskTypeKindEditor.svelte'
import TaskTypeRefEditor from '../taskTypes/TaskTypeRefEditor.svelte'
import TaskTypeIcon from './TaskTypeIcon.svelte'
import TypeClassEditor from './TypeClassEditor.svelte'
export let projectType: ProjectType
export let taskType: TaskType
export let taskTypeCounter: Map<Ref<TaskType>, number>
export let statusCounter: Map<Ref<Status>, number>
export let taskTypes: TaskType[]
const client = getClient()
$: descriptor = client.getModel().findAllSync(task.class.TaskTypeDescriptor, { _id: taskType?.descriptor })
$: states = taskType.statuses.map((p) => $statusStore.byId.get(p)).filter((p) => p !== undefined) as Status[]
function selectIcon (el: MouseEvent): void {
const icons: Asset[] = [descriptor[0].icon]
showPopup(
IconPicker,
{ icon: taskType?.icon, color: taskType?.color, icons, showColor: false },
el.target as HTMLElement,
async (result) => {
if (result !== undefined && result !== null) {
await client.update(taskType, { color: result.color, icon: result.icon })
}
}
)
}
</script>
<div class="p-4 flex-col">
<div class="flex-col">
<div class="flex-row-center flex-between">
<!-- svelte-ignore a11y-no-static-element-interactions -->
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div class="mb-1" on:click={selectIcon}>
<TaskTypeIcon value={taskType} size={'large'} />
</div>
<div class="ml-2 flex-row-center">
{taskTypeCounter.get(taskType._id) ?? 0}
</div>
</div>
<div class="flex-row-center">
<div class="fs-title mb-4">
<AttributeEditor
maxWidth={'10rem'}
_class={task.class.TaskType}
object={taskType}
key="name"
editKind={'large-style'}
/>
</div>
</div>
<TaskTypeKindEditor
kind={taskType.kind}
on:change={(evt) => {
void client.diffUpdate(taskType, { kind: evt.detail })
}}
/>
<div class="flex-row-center p-2 mt-4">
<div class="flex-no-shrink trans-title uppercase">
<Label label={getEmbeddedLabel('Parent type restrictions')} />
</div>
{#if taskType.kind === 'subtask' || taskType.kind === 'both'}
<TaskTypeRefEditor
label={getEmbeddedLabel('Allowed parents')}
value={taskType.allowedAsChildOf}
types={taskTypes.filter((it) => it.kind === 'task' || it.kind === 'both')}
onChange={(evt) => {
void client.diffUpdate(taskType, { allowedAsChildOf: evt })
}}
/>
{/if}
</div>
</div>
<div class="panelBox">
<div class="p-2 trans-title">
<Label label={getEmbeddedLabel('Process')} />
</div>
<div class="ml-2">
<StatesProjectEditor
{taskType}
type={projectType}
{states}
{statusCounter}
on:delete={async (evt) => {
const index = taskType.statuses.findIndex((p) => p === evt.detail.state._id)
taskType.statuses.splice(index, 1)
await client.update(taskType, {
statuses: taskType.statuses
})
await client.update(projectType, {
statuses: calculateStatuses(projectType, $taskTypeStore, [
{ taskTypeId: taskType._id, statuses: taskType.statuses }
])
})
}}
on:move={async (evt) => {
const index = taskType.statuses.findIndex((p) => p === evt.detail.stateID)
const state = taskType.statuses.splice(index, 1)[0]
const statuses = [
...taskType.statuses.slice(0, evt.detail.position),
state,
...taskType.statuses.slice(evt.detail.position)
]
await client.update(taskType, {
statuses
})
await client.update(projectType, {
statuses: calculateStatuses(projectType, $taskTypeStore, [{ taskTypeId: taskType._id, statuses }])
})
}}
/>
</div>
</div>
<div class="panelBox flex flex-col">
<div class="ml-2">
<TypeClassEditor ofClass={taskType.ofClass} _class={taskType.targetClass} />
</div>
</div>
</div>
<style lang="scss">
.row {
// border-bottom: 1px solid var(--theme-list-border-color);
// border-top: 1px solid var(--theme-list-border-color);
width: 100%;
}
.projects {
padding: 0.5rem 1rem 0.5rem 0.25rem;
color: var(--theme-caption-color);
background-color: var(--theme-button-default);
border: 1px solid var(--theme-button-border);
border-radius: 0.5rem;
user-select: none;
}
.panelBox {
padding: 0.5rem;
// TODO: Need to update it
border: 1px solid var(--theme-button-border);
background: var(--theme-button-default);
border-radius: 8px;
}
</style>
@@ -0,0 +1,71 @@
<!--
// 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.
-->
<script lang="ts">
import { TaskType } from '@hcengineering/task'
import {
ColorDefinition,
Icon,
IconSize,
IconWithEmoji,
getColorNumberByText,
getPlatformColorDef,
themeStore
} from '@hcengineering/ui'
import view from '@hcengineering/view'
import { createEventDispatcher, onMount } from 'svelte'
export let value: TaskType | undefined
export let size: IconSize = 'small'
export let inline: boolean = false
const dispatch = createEventDispatcher()
const dispatchAccentColor = (color?: ColorDefinition): void => {
dispatch('accent-color', color)
}
$: color = getPlatformColorDef(value?.color ?? getColorNumberByText(value?.name ?? ''), $themeStore.dark)
$: dispatchAccentColor(color)
onMount(() => {
dispatchAccentColor(color)
})
</script>
{#if value}
{@const icon = value?.icon === view.ids.IconWithEmoji ? IconWithEmoji : value?.icon}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div class="flex-presenter" on:click>
<div class="state-container" class:inline>
{#if icon != null}
<Icon {icon} {size} iconProps={{ icon: value?.color }} />
{:else}
<div class="border-divider-color svg-{size} border-radius-1" style:background={color?.color} />
{/if}
</div>
</div>
{/if}
<style lang="scss">
.state-container {
flex-shrink: 0;
&.inline {
transform: translateY(0.125rem);
}
}
</style>
@@ -0,0 +1,47 @@
<script lang="ts">
import { getEmbeddedLabel } from '@hcengineering/platform'
import { TaskTypeKind } from '@hcengineering/task'
import { ButtonKind, DropdownLabelsIntl, Label } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
export let kind: TaskTypeKind
export let readonly = false
export let buttonKind: ButtonKind = 'regular'
const dispatch = createEventDispatcher()
const items = [
{
id: 'task',
label: getEmbeddedLabel('Task')
},
{
id: 'subtask',
label: getEmbeddedLabel('Sub-task')
},
{
id: 'both',
label: getEmbeddedLabel('Task & Sub-task')
}
]
$: selected = items.find((it) => it.id === kind)
</script>
{#if readonly}
{#if selected}
<Label label={selected.label} />
{/if}
{:else}
<DropdownLabelsIntl
selected={kind}
{items}
justify={'left'}
size={'large'}
kind={buttonKind}
disabled={readonly}
on:selected={(evt) => {
if (evt.detail != null) {
kind = evt.detail
dispatch('change', kind)
}
}}
/>
{/if}
@@ -0,0 +1,42 @@
<!--
// Copyright © 2023 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 '@hcengineering/core'
import { getClient } from '@hcengineering/presentation'
import type { TaskType } from '@hcengineering/task'
import { taskTypeStore, typeStore, selectedTypeStore } from '../../'
import TaskTypeIcon from './TaskTypeIcon.svelte'
export let value: TaskType | Ref<TaskType> | undefined
$: _value = typeof value === 'string' ? $taskTypeStore.get(value) : value
$: _parent = _value !== undefined ? $typeStore.get(_value.parent) : undefined
$: _class = _value?.ofClass !== undefined ? getClient().getHierarchy().getClass(_value.ofClass) : undefined
</script>
{#if _class !== undefined && _value !== undefined}
<span class="label flex-row-center gap-1 ml-3">
<TaskTypeIcon value={_value} size={'small'} />
<span class="flex-row-center no-word-wrap" class:ml-1={_class.icon !== undefined}>
{#if $selectedTypeStore !== _parent?._id}
{_parent?.name}
<div>/</div>
{/if}
{_value.name}
</span>
</span>
{/if}
@@ -0,0 +1,48 @@
<!--
// 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 '@hcengineering/core'
import type { IntlString } from '@hcengineering/platform'
import { TaskType } from '@hcengineering/task'
import { DropdownLabels, DropdownTextItem } from '@hcengineering/ui'
export let label: IntlString
export let value: Ref<TaskType>[] = []
export let onChange: (value: Ref<TaskType>[]) => void
export let types: TaskType[]
let items: DropdownTextItem[] = []
$: items =
types.map((p) => {
return { id: p._id, label: p.name }
}) ?? []
</script>
<DropdownLabels
selected={[...(value ?? [])]}
{items}
{label}
useFlexGrow={true}
justify={'left'}
size={'large'}
kind={'link'}
width={'10rem'}
autoSelect={false}
on:selected={(e) => {
value = e.detail
onChange(e.detail)
}}
/>
@@ -0,0 +1,82 @@
<!--
// 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 core, { Class, Doc, Obj, Ref } from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation'
import { ClassAttributes } from '@hcengineering/setting-resources'
import { Button, Icon, IconAdd } from '@hcengineering/ui'
import { ObjectPresenter } from '@hcengineering/view-resources'
export let ofClass: Ref<Class<Obj>>
export let _class: Ref<Class<Obj>>
const client = getClient()
let classes: Class<Doc>[] = []
$: classes = client
.getHierarchy()
.getAncestors(_class)
.map((it) => client.getHierarchy().getClass(it))
.filter((it) => {
return (
!it.hidden &&
it.label !== undefined &&
it._id !== core.class.Doc &&
it._id !== core.class.AttachedDoc &&
it._id !== _class
)
})
$: clazz = client.getHierarchy().getClass(_class)
let mainAttributes: ClassAttributes
</script>
<div class="flex flex-between mb-4">
<div class="antiButton regular accent medium sh-round p-2 flex-row-center">
{#if clazz?.icon}
<div class="mr-2 flex">
<Icon icon={clazz.icon} size={'medium'} />
</div>
{/if}
{#if clazz}
<ObjectPresenter _class={clazz._class} objectId={clazz._id} value={clazz} />
{/if}
</div>
<Button icon={IconAdd} size={'small'} kind={'primary'} on:click={() => mainAttributes?.createAttribute()} />
</div>
<div class="ml-2 mr-2">
<table class="antiTable mx-2">
<tbody>
<ClassAttributes
bind:this={mainAttributes}
{_class}
{ofClass}
useOfClassAttributes={false}
showTitle={false}
showCreate={false}
/>
{#each classes as clazz2}
<ClassAttributes
_class={clazz2._id}
{ofClass}
useOfClassAttributes={false}
showTitle={false}
showCreate={false}
/>
{/each}
</tbody>
</table>
</div>