mirror of
https://github.com/hcengineering/platform.git
synced 2026-09-10 03:37:43 +02:00
UBERF-4248: Task type (#4042)
Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
@@ -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}
|
||||
+10
-3
@@ -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>
|
||||
Reference in New Issue
Block a user