UBERF-6001: Roles management (#4994)

Signed-off-by: Alexey Zinoviev <alexey.zinoviev@xored.com>
This commit is contained in:
Alexey Zinoviev
2024-03-27 13:49:36 +05:00
committed by GitHub
parent bee1986299
commit 418b4b1bbd
112 changed files with 3448 additions and 874 deletions
@@ -14,72 +14,45 @@
-->
<script lang="ts">
import { Ref, generateId } from '@hcengineering/core'
import { Card, getClient, hasResource } from '@hcengineering/presentation'
import { SpaceTypeDescriptor, generateId } from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation'
import { ProjectTypeDescriptor, createProjectType } from '@hcengineering/task'
import { DropdownLabelsIntl, EditBox, ToggleWithLabel } from '@hcengineering/ui'
import { ToggleWithLabel } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
import task from '../../plugin'
import { Resource } from '@hcengineering/platform'
const client = getClient()
let name: string = ''
let classic: boolean = true
export let descriptor: SpaceTypeDescriptor
export let name: string = ''
export const handleTypeCreated: () => Promise<void> = createType
let descriptor: ProjectTypeDescriptor | undefined = undefined
let classic: boolean = true
$: projDescriptor = descriptor as ProjectTypeDescriptor
const dispatch = createEventDispatcher()
async function createType (): Promise<void> {
if (descriptor === undefined) {
if (projDescriptor === undefined) {
return
}
await createProjectType(
client,
{
name,
descriptor: descriptor._id,
descriptor: projDescriptor._id,
description: '',
tasks: [],
classic: descriptor.allowedClassic === true ? classic : false
roles: 0,
classic: projDescriptor.allowedClassic === true ? classic : false
},
[],
generateId()
)
dispatch('close')
}
const descriptors = client
.getModel()
.findAllSync(task.class.ProjectTypeDescriptor, {})
.filter((p) => hasResource(p._id as any as Resource<any>))
const items = descriptors.map((it) => ({
label: it.name,
id: it._id
}))
function selectType (evt: CustomEvent<Ref<ProjectTypeDescriptor>>): void {
descriptor = descriptors.find((it) => it._id === evt.detail)
}
descriptor = descriptors[0]
</script>
<Card
label={task.string.CreateProjectType}
canSave={name.trim().length > 0 && descriptor !== undefined}
okAction={createType}
on:close={() => {
dispatch('close')
}}
on:changeContent
>
<div class="flex-col flex-gap-2">
<EditBox bind:value={name} placeholder={task.string.ProjectType} />
<DropdownLabelsIntl {items} on:selected={selectType} />
{#if descriptor?.allowedClassic === true}
<ToggleWithLabel label={task.string.ClassicProject} bind:on={classic} />
{/if}
</div>
</Card>
{#if projDescriptor?.allowedClassic === true}
<ToggleWithLabel label={task.string.ClassicProject} bind:on={classic} />
{/if}
@@ -1,76 +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 { IdMap, Ref, WithLookup, toIdMap } from '@hcengineering/core'
import task, { ProjectType } from '@hcengineering/task'
import { Location, getCurrentResolvedLocation, navigate, resolvedLocationStore } from '@hcengineering/ui'
import { createQuery, hasResource } from '@hcengineering/presentation'
import { onDestroy } from 'svelte'
import Types from './Types.svelte'
import { Resource } from '@hcengineering/platform'
import { clearSettingsStore } from '@hcengineering/setting-resources'
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 {
clearSettingsStore()
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.filter((p) => hasResource(p.descriptor as any as Resource<any>))
},
{
lookup: {
descriptor: task.class.ProjectTypeDescriptor
}
}
)
$: typeMap = toIdMap(types)
$: type = typeId !== undefined ? typeMap.get(typeId) : undefined
</script>
<Types
{type}
{typeId}
{types}
on:change={(evt) => {
selectProjectType(evt.detail)
}}
/>
@@ -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, WithLookup } from '@hcengineering/core'
import { ProjectType } from '@hcengineering/task'
import { Location, resolvedLocationStore, resizeObserver } from '@hcengineering/ui'
import { onDestroy } from 'svelte'
import { typeStore } from '../../'
import ProjectEditor from './ProjectEditor.svelte'
export let visibleNav: boolean = true
let visibleSecondNav: boolean = true
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="hulyComponent"
use:resizeObserver={(element) => {
visibleSecondNav = element.clientWidth > 720
}}
>
{#if type !== undefined}
<ProjectEditor {type} descriptor={type.$lookup?.descriptor} {visibleNav} {visibleSecondNav} on:change />
{/if}
</div>
@@ -1,353 +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 { ComponentExtensions, createQuery, getClient } from '@hcengineering/presentation'
import task, { Project, ProjectType, ProjectTypeDescriptor, Task, TaskType } from '@hcengineering/task'
import { createEventDispatcher, onDestroy } from 'svelte'
import { Ref, SortingOrder } from '@hcengineering/core'
import type { IntlString } from '@hcengineering/platform'
import setting from '@hcengineering/setting'
import { ClassAttributes, clearSettingsStore, settingsStore } from '@hcengineering/setting-resources'
import {
Breadcrumbs,
ButtonIcon,
Component,
Header,
IconAdd,
IconCopy,
IconDelete,
IconDescription,
IconFolder,
IconMoreV,
IconSquareExpand,
Label,
Location,
ModernButton,
ModernEditbox,
NavItem,
Scroller,
Separator,
TextArea,
defineSeparators,
getCurrentResolvedLocation,
navigate,
resolvedLocationStore,
secondNavSeparators
} from '@hcengineering/ui'
import { showMenu } from '@hcengineering/view-resources'
import plugin from '../../plugin'
import IconLayers from '../icons/Layers.svelte'
import CreateTaskType from '../taskTypes/CreateTaskType.svelte'
import TaskTypeEditor from '../taskTypes/TaskTypeEditor.svelte'
import TaskTypeIcon from '../taskTypes/TaskTypeIcon.svelte'
import TaskTypeKindEditor from '../taskTypes/TaskTypeKindEditor.svelte'
export let type: ProjectType
export let descriptor: ProjectTypeDescriptor | undefined
export let visibleNav: boolean = true
export let visibleSecondNav: boolean = true
const dispatch = createEventDispatcher()
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
}
}
)
// $: 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>
clearSettingsStore()
navigate(loc)
}
$: items =
selectedTaskType !== undefined
? [
{ label: plugin.string.ProjectType, icon: descriptor?.icon },
{ title: selectedTaskType.name, icon: selectedTaskType.icon }
]
: [{ label: plugin.string.ProjectType, icon: descriptor?.icon }]
let scroller: Scroller
const navigator: {
id: string
label: IntlString
element?: HTMLElement
}[] = [
{ id: 'properties', label: setting.string.Properties },
{ id: 'tasktypes', label: setting.string.TaskTypes },
{ id: 'automations', label: setting.string.Automations },
{ id: 'collections', label: setting.string.Collections }
]
defineSeparators('typeSettings', secondNavSeparators)
</script>
{#if type !== undefined && descriptor !== undefined}
<Header minimize={!visibleNav} on:resize={(event) => dispatch('change', event.detail)}>
<ButtonIcon
icon={IconCopy}
size={'small'}
kind={'secondary'}
disabled
on:click={(ev) => {
// Do copy of type
}}
/>
<ButtonIcon
icon={IconDelete}
size={'small'}
kind={'secondary'}
disabled
on:click={(ev) => {
// Ask for delete
}}
/>
<ButtonIcon
icon={IconMoreV}
size={'small'}
kind={'secondary'}
on:click={(ev) => {
showMenu(ev, { object: type })
}}
/>
<Breadcrumbs
{items}
size={'large'}
selected={selectedTaskType !== undefined ? 1 : 0}
on:select={(event) => {
if (event.detail === 0) selectTaskType(undefined)
}}
>
<!-- afterLabel={plugin.string.Published} -->
<!-- <span slot="afterLabel">{dateStr}</span> -->
</Breadcrumbs>
<!-- <svelte:fragment slot="actions">
<div class="hulyHeader-buttonsGroup__label font-regular-12">
<Label label={plugin.string.LastSave} />
<span>{dateStr}</span>
</div>
<ModernButton kind={'secondary'} label={ui.string.SaveDraft} size={'small'} />
<ModernButton kind={'primary'} icon={IconSend} label={ui.string.Publish} size={'small'} />
</svelte:fragment> -->
</Header>
<div class="hulyComponent-content__container columns">
{#if selectedTaskTypeId === undefined && visibleSecondNav}
<div class="hulyComponent-content__column">
<div class="hulyComponent-content__navHeader">
<div class="hulyComponent-content__navHeader-menu">
<ButtonIcon kind={'tertiary'} icon={IconDescription} size={'small'} inheritColor />
</div>
</div>
{#each navigator as navItem, i (navItem.id)}
<NavItem
type={'type-anchor-link'}
label={navItem.label}
on:click={() => {
if (i === 0) scroller.scroll(0)
else navItem.element?.scrollIntoView()
}}
/>
{/each}
</div>
<Separator name={'typeSettings'} index={0} color={'transparent'} />
{/if}
<div class="hulyComponent-content__column content">
<Scroller bind:this={scroller} align={'center'} padding={'var(--spacing-3)'} bottomPadding={'var(--spacing-3)'}>
<div class="hulyComponent-content gap">
{#if selectedTaskType === undefined}
<div class="hulyComponent-content__column-group">
<div class="hulyComponent-content__header">
<ButtonIcon icon={descriptor.icon} size={'large'} kind={'secondary'} />
<ModernButton
icon={IconSquareExpand}
label={plugin.string.CountProjects}
labelParams={{ count: projects.length }}
disabled={projects.length === 0}
kind={'tertiary'}
size={'medium'}
hasMenu
/>
</div>
<ModernEditbox
kind={'ghost'}
size={'large'}
label={plugin.string.ProjectTypeTitle}
value={type?.name ?? ''}
on:blur={(evt) => {
if (type !== undefined) {
void client.diffUpdate(type, { name: evt.detail })
}
}}
/>
<TextArea
placeholder={plugin.string.Description}
width={'100%'}
height={'4.5rem'}
margin={'var(--spacing-1) var(--spacing-2)'}
noFocusBorder
bind:value={type.shortDescription}
on:change={() => onShortDescriptionChange(type?.shortDescription ?? '')}
/>
{#if descriptor?.editor}
<Component is={descriptor.editor} props={{ type }} />
{/if}
</div>
<ClassAttributes ofClass={descriptor.baseClass} _class={type.targetClass} showHierarchy />
<div bind:this={navigator[1].element} class="hulyTableAttr-container">
<div class="hulyTableAttr-header font-medium-12">
<IconLayers size={'small'} />
<span><Label label={setting.string.TaskTypes} /></span>
<ButtonIcon
kind={'primary'}
icon={IconAdd}
size={'small'}
on:click={(ev) => {
$settingsStore = { id: 'createTaskType', component: CreateTaskType, props: { type, descriptor } }
// showPopup(CreateTaskType, { type, descriptor }, 'top')
}}
/>
</div>
{#if taskTypes.length}
<div class="hulyTableAttr-content task">
{#each taskTypes as taskType}
<button
class="hulyTableAttr-content__row"
on:click|stopPropagation={() => {
selectTaskType(taskType._id)
}}
>
<div class="hulyTableAttr-content__row-icon-wrapper">
<TaskTypeIcon value={taskType} size={'small'} />
</div>
{#if taskType.name}
<div class="hulyTableAttr-content__row-label font-medium-14">
{taskType.name}
</div>
{/if}
<div class="hulyTableAttr-content__row-label grow dark font-regular-14">
<TaskTypeKindEditor readonly kind={taskType.kind} />
</div>
</button>
{/each}
</div>
{/if}
</div>
<div bind:this={navigator[2].element} class="flex-col gapV-8">
<ComponentExtensions extension={task.extensions.ProjectEditorExtension} props={{ type }} />
</div>
<div bind:this={navigator[3].element} class="hulyTableAttr-container">
<div class="hulyTableAttr-header font-medium-12">
<IconFolder size={'small'} />
<span><Label label={setting.string.Collections} /></span>
<ButtonIcon kind={'primary'} icon={IconAdd} size={'small'} on:click={() => {}} />
</div>
</div>
{:else}
<TaskTypeEditor taskType={selectedTaskType} projectType={type} {taskTypes} {taskTypeCounter} />
{/if}
</div>
</Scroller>
</div>
</div>
{/if}
<style lang="scss">
.gap {
gap: var(--spacing-4);
}
</style>
@@ -1,6 +1,5 @@
<!--
// Copyright © 2020, 2021 Anticrm Platform Contributors.
// Copyright © 2021, 2022, 2023 Hardcore Engineering Inc.
// Copyright © 2024 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
@@ -14,12 +13,14 @@
// limitations under the License.
-->
<script lang="ts">
import { Button, IconAdd, showPopup } from '@hcengineering/ui'
import CreateProjectType from './CreateProjectType.svelte'
import { ComponentExtensions } from '@hcengineering/presentation'
import { ProjectType, ProjectTypeDescriptor } from '@hcengineering/task'
import task from '../../plugin'
function open () {
showPopup(CreateProjectType, {}, 'top')
}
export let type: ProjectType | undefined
export let descriptor: ProjectTypeDescriptor | undefined
</script>
<Button id="new-project-type" icon={IconAdd} kind={'link'} size="small" on:click={open} />
{#if descriptor !== undefined && type !== undefined}
<ComponentExtensions extension={task.extensions.ProjectEditorExtension} props={{ type }} />
{/if}
@@ -0,0 +1,30 @@
<!--
// Copyright © 2024 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 { ButtonIcon, IconAdd, IconFolder, Label } from '@hcengineering/ui'
import { ProjectType, ProjectTypeDescriptor } from '@hcengineering/task'
import task from '../../plugin'
export let type: ProjectType | undefined
export let descriptor: ProjectTypeDescriptor | undefined
</script>
{#if descriptor !== undefined}
<div class="hulyTableAttr-header font-medium-12">
<IconFolder size="small" />
<span><Label label={task.string.Collections} /></span>
<ButtonIcon kind="primary" icon={IconAdd} size="small" on:click={() => {}} />
</div>
{/if}
@@ -0,0 +1,30 @@
<!--
// Copyright © 2024 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 { type ProjectType, type ProjectTypeDescriptor } from '@hcengineering/task'
import { SpaceTypeGeneralSectionEditor } from '@hcengineering/setting-resources'
import { Component } from '@hcengineering/ui'
export let type: ProjectType | undefined
export let descriptor: ProjectTypeDescriptor | undefined
</script>
<SpaceTypeGeneralSectionEditor {type} {descriptor}>
<svelte:fragment slot="extra">
{#if descriptor?.editor}
<Component is={descriptor.editor} props={{ type }} />
{/if}
</svelte:fragment>
</SpaceTypeGeneralSectionEditor>
@@ -31,14 +31,9 @@
let types: ProjectType[] = []
const typesQ = createQuery()
const query = disabled
? {
descriptor: { $in: descriptors }
}
: {
descriptor: { $in: descriptors },
archived: false
}
const query = {
descriptor: { $in: descriptors }
}
$: typesQ.query(task.class.ProjectType, query, (result) => {
types = result
})
@@ -0,0 +1,94 @@
<!--
// Copyright © 2024 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 { SortingOrder } from '@hcengineering/core'
import { ButtonIcon, IconAdd, Label, getCurrentResolvedLocation, navigate } from '@hcengineering/ui'
import { createQuery } from '@hcengineering/presentation'
import { ProjectType, ProjectTypeDescriptor, TaskType } from '@hcengineering/task'
import { clearSettingsStore, settingsStore } from '@hcengineering/setting-resources'
import IconLayers from '../icons/Layers.svelte'
import TaskTypeIcon from '../taskTypes/TaskTypeIcon.svelte'
import TaskTypeKindEditor from '../taskTypes/TaskTypeKindEditor.svelte'
import CreateTaskType from '../taskTypes/CreateTaskType.svelte'
import task from '../../plugin'
export let type: ProjectType | undefined
export let descriptor: ProjectTypeDescriptor | undefined
let taskTypes: TaskType[] = []
const taskTypesQuery = createQuery()
$: taskTypesQuery.query(
task.class.TaskType,
{ _id: { $in: type?.tasks ?? [] } },
(res) => {
taskTypes = res
},
{ sort: { _id: SortingOrder.Ascending } }
)
function handleTaskTypeSelected (id: string | undefined): void {
const loc = getCurrentResolvedLocation()
if (id !== undefined) {
loc.path[5] = 'taskTypes'
loc.path[6] = id
loc.path.length = 7
} else {
loc.path.length = 5
}
clearSettingsStore()
navigate(loc)
}
</script>
{#if descriptor !== undefined}
<div class="hulyTableAttr-header font-medium-12">
<IconLayers size={'small'} />
<span><Label label={task.string.TaskTypes} /></span>
<ButtonIcon
kind="primary"
icon={IconAdd}
size="small"
on:click={(ev) => {
$settingsStore = { id: 'createTaskType', component: CreateTaskType, props: { type, descriptor } }
}}
/>
</div>
{#if taskTypes.length}
<div class="hulyTableAttr-content task">
{#each taskTypes as taskType}
<button
class="hulyTableAttr-content__row"
on:click|stopPropagation={() => {
handleTaskTypeSelected(taskType._id)
}}
>
<div class="hulyTableAttr-content__row-icon-wrapper">
<TaskTypeIcon value={taskType} size={'small'} />
</div>
{#if taskType.name}
<div class="hulyTableAttr-content__row-label font-medium-14">
{taskType.name}
</div>
{/if}
<div class="hulyTableAttr-content__row-label grow dark font-regular-14">
<TaskTypeKindEditor readonly kind={taskType.kind} />
</div>
</button>
{/each}
</div>
{/if}
{/if}
@@ -1,149 +0,0 @@
<!--
// 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 { Icon, Label, IconOpenedArrow } 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)
}
</script>
{#each types as typeItem}
<button
class="hulyTaskNavLink-container font-regular-14"
class:selected={typeItem._id === typeId}
on:click={() => {
select(typeItem)
}}
>
<div class="hulyTaskNavLink-avatar">
{#if typeItem.$lookup?.descriptor?.icon}
<div class="hulyTaskNavLink-icon">
<Icon icon={typeItem.$lookup?.descriptor?.icon} size={'small'} fill={'currentColor'} />
</div>
{/if}
</div>
{#if typeItem.$lookup?.descriptor}
<div class="hulyTaskNavLink-content">
<span class="hulyTaskNavLink-content__title">{typeItem.name}</span>
<span class="hulyTaskNavLink-content__descriptor">
<Label label={typeItem.$lookup?.descriptor.name} />
</span>
</div>
{/if}
{#if typeItem._id === typeId}
<div class="hulyTaskNavLink-icon right">
<IconOpenedArrow size={'small'} />
</div>
{/if}
</button>
{/each}
<style lang="scss">
.hulyTaskNavLink-container {
display: flex;
align-items: center;
flex-shrink: 0;
gap: 0.75rem;
padding: 0 0.75rem 0 0.5rem;
height: 3.5rem;
min-width: 0;
border: none;
border-radius: 0.375rem;
outline: none;
&.selected {
cursor: auto;
}
.hulyTaskNavLink-avatar {
display: flex;
justify-content: center;
align-items: center;
flex-shrink: 0;
width: 2.5rem;
height: 2.5rem;
min-width: 0;
background-color: var(--global-ui-BackgroundColor);
border-radius: 0.375rem;
}
.hulyTaskNavLink-icon {
flex-shrink: 0;
width: 1rem;
height: 1rem;
color: var(--global-secondary-TextColor);
&.right {
visibility: hidden;
}
}
.hulyTaskNavLink-content {
display: flex;
flex-direction: column;
gap: 0.125rem;
flex-grow: 1;
min-width: 0;
min-height: 0;
&__title,
&__descriptor {
white-space: nowrap;
word-break: break-all;
text-overflow: ellipsis;
overflow: hidden;
text-align: left;
min-width: 0;
}
&__title {
color: var(--global-primary-TextColor);
}
&__descriptor {
color: var(--global-secondary-TextColor);
}
}
&:hover {
background-color: var(--global-ui-hover-highlight-BackgroundColor);
}
&.selected {
background-color: var(--global-ui-highlight-BackgroundColor);
.hulyTaskNavLink-icon {
color: var(--global-accent-TextColor);
&.right {
visibility: visible;
}
}
.hulyTaskNavLink-content .hulyTaskNavLink-content__title {
font-weight: 700;
color: var(--global-accent-TextColor);
}
.hulyTaskNavLink-content .hulyTaskNavLink-content__descriptor {
color: var(--global-primary-TextColor);
}
}
}
</style>
@@ -143,7 +143,7 @@
icon: ofClassClass.icon
})
await client.createDoc(task.class.TaskType, type._id, _taskType, taskTypeId)
await client.createDoc(task.class.TaskType, core.space.Model, _taskType, taskTypeId)
}
if (!type.tasks.includes(taskTypeId)) {
@@ -1,6 +1,6 @@
<!--
// Copyright © 2020, 2021 Anticrm Platform Contributors.
// Copyright © 2021, 2022, 2023 Hardcore Engineering Inc.
// Copyright © 2021, 2022, 2023, 2024 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
@@ -14,11 +14,20 @@
// limitations under the License.
-->
<script lang="ts">
import { AttributeEditor, getClient } from '@hcengineering/presentation'
import { AttributeEditor, createQuery, getClient } from '@hcengineering/presentation'
import task, { ProjectType, TaskType, calculateStatuses, findStatusAttr } from '@hcengineering/task'
import { Ref, Status } from '@hcengineering/core'
import { Ref, SortingOrder, Status } from '@hcengineering/core'
import { Asset, getEmbeddedLabel } from '@hcengineering/platform'
import { Label, showPopup, ButtonIcon, ModernButton, IconSquareExpand, IconAdd, Icon } from '@hcengineering/ui'
import {
Label,
showPopup,
ButtonIcon,
ModernButton,
IconSquareExpand,
IconAdd,
Icon,
Scroller
} from '@hcengineering/ui'
import { IconPicker, statusStore } from '@hcengineering/view-resources'
import { ClassAttributes, settingsStore } from '@hcengineering/setting-resources'
import { taskTypeStore } from '../..'
@@ -28,31 +37,67 @@
import TaskTypeIcon from './TaskTypeIcon.svelte'
import plugin from '../../plugin'
export let projectType: ProjectType
export let taskType: TaskType
export let taskTypeCounter: Map<Ref<TaskType>, number>
export let taskTypes: TaskType[]
export let spaceType: ProjectType
export let objectId: Ref<TaskType>
export let name: string | undefined
export let icon: Asset | undefined
const client = getClient()
$: descriptor = client.getModel().findAllSync(task.class.TaskTypeDescriptor, { _id: taskType?.descriptor })
let taskTypes: TaskType[] = []
const taskTypesQuery = createQuery()
$: taskTypesQuery.query(
task.class.TaskType,
{ _id: { $in: spaceType?.tasks ?? [] } },
(res) => {
taskTypes = res
},
{ sort: { _id: SortingOrder.Ascending } }
)
$: states = taskType.statuses.map((p) => $statusStore.byId.get(p)).filter((p) => p !== undefined) as Status[]
$: taskType = taskTypes.find((tt) => tt._id === objectId)
$: name = taskType?.name
$: icon = taskType?.icon
$: 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[]) ?? []
let tasksCounter: number = 0
const tasksCounterQuery = createQuery()
$: if (taskType !== undefined) {
tasksCounterQuery.query(
task.class.Task,
{ kind: taskType._id },
(res) => {
tasksCounter = res.length
},
{
projection: {
_id: 1
}
}
)
}
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) {
if (result !== undefined && result !== null && taskType !== undefined) {
await client.update(taskType, { color: result.color, icon: result.icon })
}
}
)
}
function handleAddStatus (): void {
if (taskType === undefined) {
return
}
const icons: Asset[] = []
const attr = findStatusAttr(getClient().getHierarchy(), taskType.ofClass)
$settingsStore = {
@@ -63,7 +108,7 @@
taskType,
_class: taskType.statusClass,
category: task.statusCategory.Active,
type: projectType,
type: spaceType,
ofAttribute: attr._id,
icon: undefined,
color: 0,
@@ -73,93 +118,120 @@
}
</script>
<div class="hulyComponent-content__column-group mt-4">
<div class="hulyComponent-content__header mb-6">
<div class="flex-row-center gap-1-5">
<TaskTypeKindEditor
kind={taskType.kind}
on:change={(evt) => {
void client.diffUpdate(taskType, { kind: evt.detail })
}}
/>
<ButtonIcon
icon={TaskTypeIcon}
iconProps={{ value: taskType }}
size={'large'}
kind={'secondary'}
on:click={selectIcon}
/>
{#if taskType !== undefined}
<div class="hulyComponent-content__container columns">
<div class="hulyComponent-content__column content">
<Scroller align={'center'} padding={'var(--spacing-3)'} bottomPadding={'var(--spacing-3)'}>
<div class="hulyComponent-content gap">
<div class="hulyComponent-content__column-group mt-4">
<div class="hulyComponent-content__header mb-6">
<div class="flex-row-center gap-1-5">
<TaskTypeKindEditor
kind={taskType.kind}
on:change={(evt) => {
if (taskType === undefined) {
return
}
void client.diffUpdate(taskType, { kind: evt.detail })
}}
/>
<ButtonIcon
icon={TaskTypeIcon}
iconProps={{ value: taskType }}
size={'large'}
kind={'secondary'}
on:click={selectIcon}
/>
</div>
<ModernButton
icon={IconSquareExpand}
label={plugin.string.CountTasks}
labelParams={{ count: tasksCounter }}
disabled={tasksCounter === 0}
kind={'tertiary'}
size={'medium'}
hasMenu
/>
</div>
<AttributeEditor
_class={task.class.TaskType}
object={taskType}
key="name"
editKind={'modern-ghost-large'}
/>
<div class="flex-row-center mt-4 ml-4 mr-4 gap-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) => {
if (taskType === undefined) {
return
}
void client.diffUpdate(taskType, { allowedAsChildOf: evt })
}}
/>
{/if}
</div>
</div>
<div class="hulyTableAttr-container">
<div class="hulyTableAttr-header font-medium-12">
<Icon icon={task.icon.ManageTemplates} size={'small'} />
<span><Label label={plugin.string.ProcessStates} /></span>
<ButtonIcon kind={'primary'} icon={IconAdd} size={'small'} on:click={handleAddStatus} />
</div>
<StatesProjectEditor
{taskType}
type={spaceType}
{states}
on:delete={async (evt) => {
if (taskType === undefined) {
return
}
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(spaceType, {
statuses: calculateStatuses(spaceType, $taskTypeStore, [
{ taskTypeId: taskType._id, statuses: taskType.statuses }
])
})
}}
on:move={async (evt) => {
if (taskType === undefined) {
return
}
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(spaceType, {
statuses: calculateStatuses(spaceType, $taskTypeStore, [{ taskTypeId: taskType._id, statuses }])
})
}}
/>
</div>
<ClassAttributes ofClass={taskType.ofClass} _class={taskType.targetClass} showHierarchy />
</div>
</Scroller>
</div>
<ModernButton
icon={IconSquareExpand}
label={plugin.string.CountTasks}
labelParams={{ count: taskTypeCounter.get(taskType._id) ?? 0 }}
disabled={taskTypeCounter.get(taskType._id) === undefined}
kind={'tertiary'}
size={'medium'}
hasMenu
/>
</div>
<AttributeEditor _class={task.class.TaskType} object={taskType} key="name" editKind={'modern-ghost-large'} />
<div class="flex-row-center mt-4 ml-4 mr-4 gap-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="hulyTableAttr-container">
<div class="hulyTableAttr-header font-medium-12">
<Icon icon={task.icon.ManageTemplates} size={'small'} />
<span><Label label={plugin.string.ProcessStates} /></span>
<ButtonIcon kind={'primary'} icon={IconAdd} size={'small'} on:click={handleAddStatus} />
</div>
<StatesProjectEditor
{taskType}
type={projectType}
{states}
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>
<ClassAttributes ofClass={taskType.ofClass} _class={taskType.targetClass} showHierarchy />
{/if}