Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com>
This commit is contained in:
Denis Bykhov
2023-09-04 23:06:34 +06:00
committed by GitHub
parent a2388cd1d4
commit 2422baa108
97 changed files with 1678 additions and 1268 deletions
@@ -167,7 +167,7 @@
/>
<ViewletSettingButton bind:viewOptions bind:viewlet />
</div>
<FilterBar {_class} query={searchQuery} {viewOptions} on:change={(e) => (resultQuery = e.detail)} />
<FilterBar {_class} query={searchQuery} space={undefined} {viewOptions} on:change={(e) => (resultQuery = e.detail)} />
<Component is={tags.component.TagsCategoryBar} props={{ targetClass: _class, category }} on:change={handleChange} />
@@ -13,60 +13,66 @@
// limitations under the License.
-->
<script lang="ts">
import { Class, Data, Ref } from '@hcengineering/core'
import presentation, { Card, createQuery, getClient } from '@hcengineering/presentation'
import { DoneState, SpaceWithStates, State, createState } from '@hcengineering/task'
import { EditBox, Label } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
import task from '../plugin'
import presentation, { Card, getClient } from '@hcengineering/presentation'
import { calcRank, DoneState, Kanban, KanbanTemplate, KanbanTemplateSpace, State } from '@hcengineering/task'
import { Class, Data, generateId, Ref, SortingOrder } from '@hcengineering/core'
const dispatch = createEventDispatcher()
const client = getClient()
const hierarchy = client.getHierarchy()
export let status: State | undefined = undefined
export let _class: Ref<Class<State | DoneState>> | undefined = status?._class
export let template: KanbanTemplate | undefined = undefined
export let value = status?.name ?? ''
const isTemplate = template !== undefined
export let space: Kanban | KanbanTemplateSpace | undefined
export let space: Ref<SpaceWithStates>
let canSave = true
let _space: SpaceWithStates | undefined = undefined
const query = createQuery()
$: query.query(task.class.SpaceWithStates, { _id: space }, (res) => {
_space = res[0]
})
const canSave = true
async function save () {
if (space === undefined && template === undefined && status?.space === undefined) return
const attachedTo = isTemplate && template?._id ? { attachedTo: template._id } : {}
const kanban = space as Kanban
if (_class !== undefined && status === undefined) {
const query = isTemplate ? { ...attachedTo } : kanban?.attachedTo ? { space: kanban.attachedTo } : {}
const lastOne = await client.findOne(_class, query, { sort: { rank: SortingOrder.Descending } })
let newDoc: Data<State> = {
ofAttribute: task.attribute.State,
name: value.trim(),
rank: calcRank(lastOne, undefined),
...attachedTo
}
if (_space === undefined || _class === undefined) return
if (status === undefined) {
if (!hierarchy.isDerived(_class, task.class.DoneState)) {
newDoc = {
const newDoc: Data<State> = {
ofAttribute: task.attribute.State,
name: value.trim(),
color: 9,
rank: calcRank(lastOne, undefined),
...attachedTo
color: 9
}
const id = await createState(client, _class, newDoc)
await client.update(_space, { $push: { states: id } })
} else {
const newDoc: Data<DoneState> = {
ofAttribute: task.attribute.DoneState,
name: value.trim()
}
const id = await createState(client, _class, newDoc)
await client.update(_space, { $push: { states: id } })
}
} else {
const id = await createState(client, _class, { ...status, name: value.trim() })
if (!hierarchy.isDerived(_class, task.class.DoneState)) {
const states = _space.states
const index = states.findIndex((x) => x === status?._id)
if (index !== -1) {
states[index] = id
await client.update(_space, { states })
}
} else {
const states = _space.doneStates ?? []
const index = states.findIndex((x) => x === status?._id)
if (index !== -1) {
states[index] = id
await client.update(_space, { doneStates: states })
}
}
const ops = client.apply(template?.space ?? kanban?.attachedTo ?? generateId()).notMatch(_class, {
space: isTemplate && template ? template.space : kanban?.attachedTo,
name: value.trim(),
...attachedTo
})
await ops.createDoc(_class, isTemplate && template ? template.space : kanban?.attachedTo, newDoc)
canSave = await ops.commit()
}
if (status !== undefined && _class !== undefined) {
const ops = client.apply(status._id).notMatch(_class, { space: status.space, name: value.trim(), ...attachedTo })
await ops.update(status, { name: value.trim() })
canSave = await ops.commit()
}
if (canSave) dispatch('close')
dispatch('close')
}
</script>
@@ -0,0 +1,84 @@
<!--
// 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 { Class, Data, Ref, SortingOrder } from '@hcengineering/core'
import presentation, { Card, getClient } from '@hcengineering/presentation'
import { DoneStateTemplate, KanbanTemplate, KanbanTemplateSpace, StateTemplate, calcRank } from '@hcengineering/task'
import { EditBox, Label } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
import task from '../plugin'
const dispatch = createEventDispatcher()
const client = getClient()
const hierarchy = client.getHierarchy()
export let status: StateTemplate | undefined = undefined
export let _class: Ref<Class<StateTemplate | DoneStateTemplate>> | undefined = status?._class
export let template: KanbanTemplate
export let space: KanbanTemplateSpace
export let value = status?.name ?? ''
let canSave = true
async function save () {
if (space === undefined && template === undefined && status?.space === undefined) return
const attachedTo = { attachedTo: template._id }
if (_class !== undefined && status === undefined) {
const lastOne = await client.findOne(_class, attachedTo, { sort: { rank: SortingOrder.Descending } })
let newDoc: Data<StateTemplate> = {
ofAttribute: task.attribute.State,
name: value.trim(),
rank: calcRank(lastOne, undefined),
...attachedTo
}
if (!hierarchy.isDerived(_class, task.class.DoneState)) {
newDoc = {
ofAttribute: task.attribute.State,
name: value.trim(),
color: 9,
rank: calcRank(lastOne, undefined),
...attachedTo
}
}
const ops = client.apply(template.space).notMatch(_class, {
space: template.space,
name: value.trim(),
...attachedTo
})
await ops.createDoc(_class, template.space, newDoc)
canSave = await ops.commit()
}
if (status !== undefined && _class !== undefined) {
const ops = client.apply(status._id).notMatch(_class, { space: status.space, name: value.trim(), ...attachedTo })
await ops.update(status, { name: value.trim() })
canSave = await ops.commit()
}
if (canSave) dispatch('close')
}
</script>
<Card
label={task.string.StatusPopupTitle}
okAction={save}
canSave
okLabel={presentation.string.Save}
on:changeContent
onCancel={() => dispatch('close')}
>
<EditBox focusIndex={1} bind:value placeholder={task.string.StatusName} kind={'large-style'} autoFocus fullSize />
<svelte:fragment slot="error">
{#if !canSave}
<Label label={task.string.NameAlreadyExists} />
{/if}
</svelte:fragment>
</Card>
@@ -14,16 +14,16 @@
// limitations under the License.
-->
<script lang="ts">
import { Class, DocumentQuery, FindOptions, Ref, SortingOrder } from '@hcengineering/core'
import { Class, DocumentQuery, FindOptions, IdMap, Ref, Status } from '@hcengineering/core'
import { createQuery, getClient } from '@hcengineering/presentation'
import { DoneState, SpaceWithStates, State, Task } from '@hcengineering/task'
import type { TabItem } from '@hcengineering/ui'
import { TabList } from '@hcengineering/ui'
import { TableBrowser } from '@hcengineering/view-resources'
import { TableBrowser, statusStore } from '@hcengineering/view-resources'
import task from '../plugin'
import StatesBar from './state/StatesBar.svelte'
import Lost from './icons/Lost.svelte'
import Won from './icons/Won.svelte'
import StatesBar from './state/StatesBar.svelte'
export let _class: Ref<Class<Task>>
export let space: Ref<SpaceWithStates>
@@ -33,14 +33,28 @@
let doneStatusesView: boolean = false
let state: Ref<State> | undefined = undefined
let _space: SpaceWithStates | undefined = undefined
const selectedDoneStates: Set<Ref<DoneState>> = new Set<Ref<DoneState>>()
$: resConfig = updateConfig(config)
let doneStates: DoneState[] = []
let itemsDS: TabItem[] = []
$: doneStates = getDoneStates(_space, $statusStore)
$: itemsDS = getItems(doneStates)
let selectedDS: string[] = []
let withoutDone: boolean = false
let resultQuery: DocumentQuery<Task>
function getItems (doneStates: DoneState[]): TabItem[] {
const itemsDS: TabItem[] = doneStates.map((s) => {
return {
id: s._id,
label: s.name,
icon: s._class === task.class.WonState ? Won : Lost,
color: s._class === task.class.WonState ? 'var(--theme-won-color)' : 'var(--theme-lost-color)'
}
})
itemsDS.unshift({ id: 'NoDoneState', labelIntl: task.string.NoDoneState })
return itemsDS
}
function updateConfig (config: string[]): string[] {
if (state !== undefined) {
return config.filter((p) => p !== 'status')
@@ -51,34 +65,28 @@
return config
}
const doneStateQuery = createQuery()
doneStateQuery.query(
task.class.DoneState,
space != null
? {
space
}
: {},
(res) => {
doneStates = res
itemsDS = doneStates.map((s) => {
return {
id: s._id,
label: s.name,
icon: s._class === task.class.WonState ? Won : Lost,
color: s._class === task.class.WonState ? 'var(--theme-won-color)' : 'var(--theme-lost-color)'
}
})
itemsDS.unshift({ id: 'NoDoneState', labelIntl: task.string.NoDoneState })
},
const spaceQuery = createQuery()
$: spaceQuery.query(
task.class.SpaceWithStates,
{
sort: {
_class: SortingOrder.Descending,
rank: SortingOrder.Descending
}
_id: space
},
(res) => {
_space = res[0]
}
)
function getDoneStates (space: SpaceWithStates | undefined, statusStore: IdMap<Status>): DoneState[] {
if (space === undefined) {
return []
}
const doneStates = space.doneStates
? space.doneStates.map((x) => statusStore.get(x) as DoneState).filter((p) => p !== undefined)
: []
return doneStates
}
const client = getClient()
async function updateQuery (query: DocumentQuery<Task>, selectedDoneStates: Set<Ref<DoneState>>): Promise<void> {
@@ -13,29 +13,36 @@
// limitations under the License.
-->
<script lang="ts">
import { Ref } from '@hcengineering/core'
import { IdMap, Ref, Status } from '@hcengineering/core'
import { createQuery } from '@hcengineering/presentation'
import type { Kanban } from '@hcengineering/task'
import type { SpaceWithStates } from '@hcengineering/task'
import task, { DoneState, LostState, WonState } from '@hcengineering/task'
import { createEventDispatcher } from 'svelte'
import Won from '../icons/Won.svelte'
import Lost from '../icons/Lost.svelte'
import { statusStore } from '@hcengineering/view-resources'
export let kanban: Kanban
export let space: Ref<SpaceWithStates>
let wonStates: WonState[] = []
let lostStates: LostState[] = []
let _space: SpaceWithStates | undefined = undefined
const dispatch = createEventDispatcher()
const doneStatesQ = createQuery()
$: if (kanban !== undefined) {
doneStatesQ.query(task.class.DoneState, { space: kanban.space }, (result) => {
wonStates = result.filter((x) => x._class === task.class.WonState)
lostStates = result.filter((x) => x._class === task.class.LostState)
})
} else {
doneStatesQ.unsubscribe()
const query = createQuery()
query.query(task.class.SpaceWithStates, { _id: space }, (result) => {
_space = result[0]
})
function getStates (space: SpaceWithStates | undefined, statusStore: IdMap<Status>): void {
if (space === undefined) return
const result: Status[] =
(space.doneStates?.map((p) => statusStore.get(p))?.filter((p) => p !== undefined) as Status[]) ?? []
wonStates = result.filter((x) => x._class === task.class.WonState)
lostStates = result.filter((x) => x._class === task.class.LostState)
}
$: getStates(_space, $statusStore)
let hoveredDoneState: Ref<DoneState> | undefined
const onDone = (state: DoneState) => async () => {
@@ -1,77 +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, SortingOrder } from '@hcengineering/core'
import { createQuery, getClient } from '@hcengineering/presentation'
import type { DoneState, Kanban, State } from '@hcengineering/task'
import task, { calcRank } from '@hcengineering/task'
import StatesEditor from '../state/StatesEditor.svelte'
export let kanban: Kanban
let states: State[] = []
let doneStates: DoneState[] = []
let wonStates: DoneState[] = []
let lostStates: DoneState[] = []
$: wonStates = doneStates.filter((x) => x._class === task.class.WonState)
$: lostStates = doneStates.filter((x) => x._class === task.class.LostState)
const client = getClient()
const statesQ = createQuery()
$: statesQ.query(
task.class.State,
{ space: kanban.space },
(result) => {
states = result
},
{
sort: {
rank: SortingOrder.Ascending
}
}
)
const doneStatesQ = createQuery()
$: doneStatesQ.query(
task.class.DoneState,
{ space: kanban.space },
(result) => {
doneStates = result
},
{
sort: {
rank: SortingOrder.Ascending
}
}
)
async function onMove ({ detail: { stateID, position } }: { detail: { stateID: Ref<State>; position: number } }) {
const [prev, next] = [states[position - 1], states[position + 1]]
const state = states.find((x) => x._id === stateID)
if (state === undefined) {
return
}
await client.updateDoc(state._class, state.space, state._id, {
rank: calcRank(prev, next)
})
}
</script>
<StatesEditor {states} {wonStates} {lostStates} space={kanban} on:delete on:move={onMove} />
@@ -14,21 +14,20 @@
// limitations under the License.
-->
<script lang="ts">
import { createEventDispatcher } from 'svelte'
import { Ref, Space, SortingOrder } from '@hcengineering/core'
import core from '@hcengineering/core'
import core, { Ref, SortingOrder, Space } from '@hcengineering/core'
import { createQuery, getClient } from '@hcengineering/presentation'
import type {
State,
DoneState,
DoneStateTemplate,
KanbanTemplate,
StateTemplate,
DoneState,
KanbanTemplateSpace
KanbanTemplateSpace,
State,
StateTemplate
} from '@hcengineering/task'
import task, { calcRank } from '@hcengineering/task'
import { createEventDispatcher } from 'svelte'
import StatesEditor from '../state/StatesEditor.svelte'
import StatesTemplateEditor from '../state/StatesTemplateEditor.svelte'
export let kanban: KanbanTemplate
export let folder: KanbanTemplateSpace
@@ -103,7 +102,7 @@
}
</script>
<StatesEditor
<StatesTemplateEditor
template={kanban}
space={folder}
{states}
@@ -27,7 +27,7 @@
import { Item, Kanban as KanbanUI } from '@hcengineering/kanban'
import { getResource } from '@hcengineering/platform'
import { createQuery, getClient, ActionContext } from '@hcengineering/presentation'
import { Kanban, SpaceWithStates, Task, TaskGrouping, TaskOrdering } from '@hcengineering/task'
import { SpaceWithStates, Task, TaskGrouping, TaskOrdering } from '@hcengineering/task'
import {
ColorDefinition,
defaultBackground,
@@ -75,7 +75,6 @@
export let options: FindOptions<Task> | undefined
$: currentSpace = space
$: groupByKey = (viewOptions.groupBy[0] ?? noCategory) as TaskGrouping
$: orderBy = viewOptions.orderBy
$: sort = { [orderBy[0]]: orderBy[1] }
@@ -88,13 +87,6 @@
accentColors = accentColors
}
const spaceQuery = createQuery()
let currentProject: SpaceWithStates | undefined
$: spaceQuery.query(task.class.SpaceWithStates, { _id: currentSpace }, (res) => {
currentProject = res.shift()
})
let resultQuery: DocumentQuery<any> = { ...query }
$: getResultQuery(query, viewOptionsConfig, viewOptions).then((p) => (resultQuery = { ...p, ...query }))
@@ -160,20 +152,21 @@
const queryId = generateId()
$: updateCategories(_class, tasks, groupByKey, viewOptions, viewOptionsConfig)
$: updateCategories(_class, space, tasks, groupByKey, viewOptions, viewOptionsConfig)
function update () {
updateCategories(_class, tasks, groupByKey, viewOptions, viewOptionsConfig)
updateCategories(_class, space, tasks, groupByKey, viewOptions, viewOptionsConfig)
}
async function updateCategories (
_class: Ref<Class<Doc>>,
space: Ref<SpaceWithStates> | undefined,
docs: Doc[],
groupByKey: string,
viewOptions: ViewOptions,
viewOptionsModel: ViewOptionModel[] | undefined
) {
categories = await getCategories(client, _class, docs, groupByKey, viewlet.descriptor)
categories = await getCategories(client, _class, space, docs, groupByKey, viewlet.descriptor)
for (const viewOption of viewOptionsModel ?? []) {
if (viewOption.actionTarget !== 'category') continue
const categoryFunc = viewOption as CategoryOption
@@ -188,6 +181,7 @@
const res = await categoryAction(
_class,
spaces.length > 0 ? { space: { $in: Array.from(spaces.values()) } } : {},
space,
groupByKey,
update,
queryId,
@@ -231,13 +225,6 @@
$: presenterMixin = client.getHierarchy().as(clazz, task.mixin.KanbanCard)
$: cardPresenter = getResource(presenterMixin.card)
let kanban: Kanban
const kanbanQuery = createQuery()
$: kanbanQuery.query(task.class.Kanban, { attachedTo: space }, (result) => {
kanban = result[0]
})
const getDoneUpdate = (e: any) => ({ doneState: e.detail._id } as DocumentUpdate<Doc>)
</script>
@@ -301,13 +288,15 @@
</svelte:fragment>
<!-- eslint-disable-next-line no-undef -->
<svelte:fragment slot="doneBar" let:onDone>
<KanbanDragDone
{kanban}
on:done={(e) => {
// eslint-disable-next-line no-undef
onDone(getDoneUpdate(e))
}}
/>
{#if space}
<KanbanDragDone
{space}
on:done={(e) => {
// eslint-disable-next-line no-undef
onDone(getDoneUpdate(e))
}}
/>
{/if}
</svelte:fragment>
</KanbanUI>
{/await}
@@ -26,7 +26,7 @@
export let value: Ref<DoneState> | null | undefined
export let onChange: (value: any) => void
export let space: Ref<SpaceWithStates>
export let space: Ref<SpaceWithStates> | undefined
export let kind: ButtonKind = 'no-border'
export let size: ButtonSize = 'small'
export let justify: 'left' | 'center' = 'center'
@@ -16,11 +16,12 @@
<script lang="ts">
import { Ref, Status, StatusValue } from '@hcengineering/core'
import { statusStore } from '@hcengineering/view-resources'
import type { DoneState } from '@hcengineering/task'
import type { DoneState, SpaceWithStates } from '@hcengineering/task'
import DoneStatePresenter from './DoneStatePresenter.svelte'
import DoneStateEditor from './DoneStateEditor.svelte'
export let value: Ref<DoneState> | StatusValue
export let space: Ref<SpaceWithStates> | undefined
export let showTitle: boolean = true
export let onChange: ((value: Ref<DoneState>) => void) | undefined = undefined
@@ -29,7 +30,7 @@
{#if value}
{#if onChange !== undefined && state !== undefined}
<DoneStateEditor value={state._id} space={state.space} {onChange} kind="link" size="medium" />
<DoneStateEditor value={state._id} {space} {onChange} kind="link" size="medium" />
{:else}
<DoneStatePresenter value={state} {showTitle} />
{/if}
@@ -14,7 +14,7 @@
// limitations under the License.
-->
<script lang="ts">
import { Class, Doc, Ref, SortingOrder } from '@hcengineering/core'
import { Class, Doc, IdMap, Ref, Status } from '@hcengineering/core'
import { createQuery } from '@hcengineering/presentation'
import { DoneState, SpaceWithStates } from '@hcengineering/task'
import { Label, PaletteColorIndexes, getPlatformColor, resizeObserver, themeStore } from '@hcengineering/ui'
@@ -23,24 +23,29 @@
import Lost from '../icons/Lost.svelte'
import Unknown from '../icons/Unknown.svelte'
import Won from '../icons/Won.svelte'
import { statusStore } from '@hcengineering/view-resources'
export let space: Ref<SpaceWithStates>
let states: DoneState[] = []
const dispatch = createEventDispatcher()
const statesQuery = createQuery()
statesQuery.query(
task.class.DoneState,
{ space },
(res) => {
states = res
},
{
sort: {
_class: SortingOrder.Descending,
rank: SortingOrder.Ascending
}
}
)
let _space: SpaceWithStates
function getStates (space: SpaceWithStates | undefined, statesStore: IdMap<Status>): void {
if (space === undefined) return
const res: Status[] =
(space.doneStates?.map((p) => statesStore.get(p))?.filter((p) => p !== undefined) as Status[]) ?? []
res.sort((a, b) => a._class.localeCompare(b._class))
states = res
}
$: getStates(_space, $statusStore)
const spaceQuery = createQuery()
spaceQuery.query(task.class.SpaceWithStates, { _id: space }, (res) => {
_space = res[0]
})
function getColor (_class: Ref<Class<Doc>>): string {
return _class === task.class.WonState
? getPlatformColor(PaletteColorIndexes.Crocodile, $themeStore.dark)
@@ -14,64 +14,67 @@
// limitations under the License.
-->
<script lang="ts">
import type { Class, Doc, DocumentQuery, Obj, Ref } from '@hcengineering/core'
import core from '@hcengineering/core'
import { createQuery, getClient, MessageBox } from '@hcengineering/presentation'
import type { DoneState, Kanban, SpaceWithStates, State } from '@hcengineering/task'
import task from '../../plugin'
import KanbanEditor from '../kanban/KanbanEditor.svelte'
import { Icon, Label, showPopup, Panel, Scroller } from '@hcengineering/ui'
import type { Doc, DocumentQuery, IdMap, Ref, Status } from '@hcengineering/core'
import { MessageBox, createQuery, getClient } from '@hcengineering/presentation'
import type { DoneState, LostState, SpaceWithStates, State, WonState } from '@hcengineering/task'
import { Icon, Label, Panel, Scroller, showPopup } from '@hcengineering/ui'
import { statusStore } from '@hcengineering/view-resources'
import { createEventDispatcher } from 'svelte'
import workbench from '@hcengineering/workbench'
import task from '../../plugin'
import StatesEditor from './StatesEditor.svelte'
export let _id: Ref<SpaceWithStates>
export let spaceClass: Ref<Class<Obj>>
let kanban: Kanban | undefined
let spaceClassInstance: Class<SpaceWithStates> | undefined
let spaceInstance: SpaceWithStates | undefined
const client = getClient()
const hierarchy = client.getHierarchy()
const dispatch = createEventDispatcher()
const kanbanQ = createQuery()
$: kanbanQ.query(task.class.Kanban, { attachedTo: _id }, (result) => {
kanban = result[0]
})
const spaceQ = createQuery()
$: spaceQ.query<Class<SpaceWithStates>>(core.class.Class, { _id: spaceClass }, (result) => {
spaceClassInstance = result.shift()
})
const spaceI = createQuery()
$: spaceI.query<SpaceWithStates>(spaceClass, { _id }, (result) => {
spaceInstance = result.shift()
$: spaceI.query<SpaceWithStates>(task.class.SpaceWithStates, { _id }, (result) => {
spaceInstance = result[0]
})
$: spaceClass = spaceInstance ? hierarchy.getClass(spaceInstance._class) : undefined
$: [states, doneStates] = getStates(spaceInstance, $statusStore)
$: wonStates = doneStates.filter((x) => x._class === task.class.WonState) as WonState[]
$: lostStates = doneStates.filter((x) => x._class === task.class.LostState) as LostState[]
function getStates (space: SpaceWithStates | undefined, statusStore: IdMap<Status>): [Status[], DoneState[]] {
if (space === undefined) {
return [[], []]
}
const states = space.states.map((x) => statusStore.get(x) as Status).filter((p) => p !== undefined)
const doneStates = space.doneStates
? space.doneStates.map((x) => statusStore.get(x) as DoneState).filter((p) => p !== undefined)
: []
return [states, doneStates]
}
async function deleteState ({ state }: { state: State | DoneState }) {
if (spaceInstance === undefined) {
return
}
const spaceClassInstance = client.getHierarchy().getClass(spaceInstance._class)
const spaceView = client.getHierarchy().as(spaceClassInstance, workbench.mixin.SpaceView)
const containingClass = spaceView.view.class
let query: DocumentQuery<Doc>
if (hierarchy.isDerived(state._class, task.class.DoneState)) {
query = { doneState: state._id }
query = { doneState: state._id, space: _id }
} else {
query = { status: state._id }
query = { status: state._id, space: _id }
}
const objectsInThisState = await client.findAll(containingClass, query)
const objectsInThisState = await client.findAll(task.class.Task, query)
if (objectsInThisState.length > 0) {
showPopup(MessageBox, {
label: task.string.CantStatusDelete,
message: task.string.CantStatusDeleteError
message: task.string.CantStatusDeleteError,
canSubmit: false
})
} else {
showPopup(
@@ -82,13 +85,43 @@
},
undefined,
async (result) => {
if (result && kanban !== undefined) {
client.removeDoc(state._class, state.space, state._id)
if (result !== undefined) {
if (hierarchy.isDerived(state._class, task.class.DoneState)) {
const index = doneStates.findIndex((x) => x._id === state._id)
if (index === -1) {
return
}
states.splice(index, 1)
if (spaceInstance) {
await client.update(spaceInstance, { doneStates: states.map((x) => x._id) })
}
} else {
const index = states.findIndex((x) => x._id === state._id)
if (index === -1) {
return
}
states.splice(index, 1)
if (spaceInstance) {
await client.update(spaceInstance, { states: states.map((x) => x._id) })
}
}
}
}
)
}
}
async function onMove (stateID: Ref<State>, position: number) {
const index = states.findIndex((x) => x._id === stateID)
if (index === -1) {
return
}
const elem = states.splice(index, 1)
states = [...states.slice(0, position), elem[0], ...states.slice(position)]
if (spaceInstance) {
await client.update(spaceInstance, { states: states.map((x) => x._id) })
}
}
</script>
<Panel
@@ -108,7 +141,7 @@
<div class="title-wrapper">
<span class="wrapped-title">
<Label label={task.string.ManageStatusesWithin} />
{#if spaceClassInstance}<Label label={spaceClassInstance?.label} />{:else}...{/if}
{#if spaceClass}<Label label={spaceClass?.label} />{:else}...{/if}
</span>
{#if spaceInstance?.name}<span class="wrapped-subtitle">{spaceInstance?.name}</span>{/if}
</div>
@@ -117,9 +150,13 @@
<Scroller>
<div class="popupPanel-body__main-content py-10 clear-mins">
{#if kanban !== undefined}
<KanbanEditor {kanban} on:delete={(e) => deleteState(e.detail)} />
{/if}
<StatesEditor
{states}
{wonStates}
{lostStates}
on:delete={(e) => deleteState(e.detail)}
on:move={(e) => onMove(e.detail.stateID, e.detail.position)}
/>
</div>
</Scroller>
</Panel>
@@ -15,10 +15,10 @@
-->
<script lang="ts">
import { Ref, Space } from '@hcengineering/core'
import task, { State } from '@hcengineering/task'
import { createQuery } from '@hcengineering/presentation'
import { State } from '@hcengineering/task'
import type { ButtonKind, ButtonSize } from '@hcengineering/ui'
import { showPopup, Button, eventToHTMLElement } from '@hcengineering/ui'
import { Button, eventToHTMLElement, showPopup } from '@hcengineering/ui'
import { statusStore } from '@hcengineering/view-resources'
import StatePresenter from './StatePresenter.svelte'
import StatesPopup from './StatesPopup.svelte'
@@ -32,18 +32,9 @@
export let shouldShowName: boolean = true
export let shrink: number = 0
let state: State
$: state = $statusStore.get(value)
let opened: boolean = false
const query = createQuery()
$: query.query(
task.class.State,
{ _id: value },
(res) => {
state = res[0]
},
{ limit: 1 }
)
const handleClick = (ev: MouseEvent) => {
if (!opened) {
opened = true
@@ -14,14 +14,15 @@
// limitations under the License.
-->
<script lang="ts">
import { Ref, Status, StatusValue } from '@hcengineering/core'
import type { ButtonKind, ButtonSize } from '@hcengineering/ui'
import { Ref, Space, Status, StatusValue } from '@hcengineering/core'
import { State } from '@hcengineering/task'
import type { ButtonKind, ButtonSize } from '@hcengineering/ui'
import { statusStore } from '@hcengineering/view-resources'
import StateEditor from './StateEditor.svelte'
import StatePresenter from './StatePresenter.svelte'
export let value: Ref<State> | StatusValue
export let space: Ref<Space>
export let onChange: ((value: Ref<State>) => void) | undefined = undefined
export let kind: ButtonKind = 'link'
export let size: ButtonSize = 'medium'
@@ -33,7 +34,7 @@
{#if value}
{#if onChange !== undefined && state !== undefined}
<StateEditor value={state._id} space={state.space} {onChange} {kind} {size} {shouldShowName} {shrink} />
<StateEditor value={state._id} {space} {onChange} {kind} {size} {shouldShowName} {shrink} />
{:else}
<StatePresenter value={state} {shouldShowName} on:accent-color />
{/if}
@@ -15,8 +15,8 @@
-->
<script lang="ts">
import { Ref } from '@hcengineering/core'
import { BreadcrumbsElement } from '@hcengineering/presentation'
import task, { SpaceWithStates, State } from '@hcengineering/task'
import { BreadcrumbsElement, createQuery } from '@hcengineering/presentation'
import task, { SpaceWithStates, State, getStates } from '@hcengineering/task'
import { ScrollerBar, getColorNumberByText, getPlatformColor, themeStore } from '@hcengineering/ui'
import { statusStore } from '@hcengineering/view-resources'
import { createEventDispatcher } from 'svelte'
@@ -26,7 +26,20 @@
export let state: Ref<State> | undefined = undefined
export let gap: 'none' | 'small' | 'big' = 'small'
$: states = $statusStore.filter((it) => it.space === space && it.ofAttribute === task.attribute.State)
let _space: SpaceWithStates | undefined = undefined
const spaceQuery = createQuery()
spaceQuery.query(
task.class.SpaceWithStates,
{
_id: space
},
(res) => {
_space = res[0]
}
)
$: states = getStates(_space, $statusStore)
let divScroll: HTMLElement
const dispatch = createEventDispatcher()
@@ -16,7 +16,7 @@
<script lang="ts">
import { Ref } from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation'
import type { DoneState, Kanban, KanbanTemplate, KanbanTemplateSpace, State } from '@hcengineering/task'
import type { DoneState, KanbanTemplate, KanbanTemplateSpace, State } from '@hcengineering/task'
import {
CircleButton,
Component,
@@ -40,7 +40,7 @@
import StatusesPopup from './StatusesPopup.svelte'
export let template: KanbanTemplate | undefined = undefined
export let space: KanbanTemplateSpace | Kanban | undefined = undefined
export let space: KanbanTemplateSpace | undefined = undefined
export let states: State[] = []
export let wonStates: DoneState[] = []
export let lostStates: DoneState[] = []
@@ -14,28 +14,23 @@
// limitations under the License.
-->
<script lang="ts">
import { Ref, SortingOrder } from '@hcengineering/core'
import { Ref } from '@hcengineering/core'
import { createQuery } from '@hcengineering/presentation'
import task, { SpaceWithStates, State } from '@hcengineering/task'
import task, { SpaceWithStates, getStates } from '@hcengineering/task'
import { getColorNumberByText, getPlatformColorDef, resizeObserver, themeStore } from '@hcengineering/ui'
import { statusStore } from '@hcengineering/view-resources'
import { createEventDispatcher } from 'svelte'
export let space: Ref<SpaceWithStates>
let states: State[] = []
$: states = getStates(_space, $statusStore)
const dispatch = createEventDispatcher()
const statesQuery = createQuery()
statesQuery.query(
task.class.State,
{ space },
(res) => {
states = res
},
{
sort: {
rank: SortingOrder.Ascending
}
}
)
let _space: SpaceWithStates | undefined = undefined
const query = createQuery()
$: query.query(task.class.SpaceWithStates, { _id: space }, (res) => {
_space = res[0]
})
</script>
<div class="selectPopup" use:resizeObserver={() => dispatch('changeContent')}>
@@ -0,0 +1,313 @@
<!--
// 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 { Class, Ref } from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation'
import type {
DoneStateTemplate,
KanbanTemplate,
KanbanTemplateSpace,
State,
StateTemplate
} from '@hcengineering/task'
import {
CircleButton,
Component,
IconAdd,
IconCircles,
IconMoreH,
Label,
PaletteColorIndexes,
defaultBackground,
eventToHTMLElement,
getColorNumberByText,
getPlatformColorDef,
showPopup,
themeStore
} from '@hcengineering/ui'
import { ColorsPopup, StringPresenter } from '@hcengineering/view-resources'
import { createEventDispatcher } from 'svelte'
import task from '../../plugin'
import Lost from '../icons/Lost.svelte'
import Won from '../icons/Won.svelte'
import StatusesPopup from './StatusesPopup.svelte'
export let template: KanbanTemplate
export let space: KanbanTemplateSpace
export let states: StateTemplate[] = []
export let wonStates: DoneStateTemplate[] = []
export let lostStates: DoneStateTemplate[] = []
const dispatch = createEventDispatcher()
const client = getClient()
const elements: HTMLElement[] = []
let selected: number | undefined
let dragState: Ref<State>
function dragswap (ev: MouseEvent, i: number): boolean {
const s = selected as number
if (i < s) {
return ev.offsetY < elements[i].offsetHeight / 2
} else if (i > s) {
return ev.offsetY > elements[i].offsetHeight / 2
}
return false
}
function dragover (ev: MouseEvent, i: number) {
const s = selected as number
if (dragswap(ev, i)) {
;[states[i], states[s]] = [states[s], states[i]]
selected = i
}
}
async function onMove (to: number) {
dispatch('move', {
stateID: dragState,
position: to
})
}
const onColorChange =
(state: State) =>
async (color: number | undefined): Promise<void> => {
if (color == null) {
return
}
await client.updateDoc(state._class, state.space, state._id, { color })
}
const spaceEditor = space.editor
function add (_class: Ref<Class<StateTemplate | DoneStateTemplate>>) {
showPopup(task.component.CreateStateTemplatePopup, {
space,
template,
_class
})
}
function edit (status: StateTemplate) {
showPopup(task.component.CreateStateTemplatePopup, { status, template, space })
}
</script>
{#if spaceEditor}
<Component is={spaceEditor} props={{ template }} />
{/if}
<div class="flex-no-shrink flex-between trans-title uppercase">
<Label label={task.string.ActiveStates} />
<CircleButton
icon={IconAdd}
size={'medium'}
on:click={() => {
add(task.class.StateTemplate)
}}
/>
</div>
<div class="flex-col mt-3">
{#each states as state, i}
{@const color = getPlatformColorDef(state.color ?? getColorNumberByText(state.name), $themeStore.dark)}
{#if state}
<div
bind:this={elements[i]}
class="flex-between states"
style:background={color.background ?? defaultBackground($themeStore.dark)}
draggable={true}
on:dragover|preventDefault={(ev) => {
dragover(ev, i)
}}
on:drop|preventDefault={() => {
onMove(i)
}}
on:dragstart={() => {
selected = i
dragState = states[i]._id
}}
on:dragend={() => {
selected = undefined
}}
>
<div class="bar"><IconCircles size={'small'} /></div>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
class="color"
style:background-color={color.color}
on:click={() => {
showPopup(ColorsPopup, { selected: color.name }, elements[i], onColorChange(state))
}}
/>
<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={(ev) => {
showPopup(
StatusesPopup,
{
onDelete: () => dispatch('delete', { state }),
showDelete: states.length > 1,
onUpdate: () => edit(state)
},
eventToHTMLElement(ev),
() => {}
)
}}
>
<IconMoreH size={'medium'} />
</div>
</div>
{/if}
{/each}
</div>
<div class="flex-col mt-9">
<div class="flex-no-shrink flex-between trans-title uppercase">
<Label label={task.string.DoneStatesWon} />
<CircleButton
icon={IconAdd}
size={'medium'}
on:click={() => {
add(task.class.WonStateTemplate)
}}
/>
</div>
<div class="flex-col mt-4">
{#each wonStates as state}
{@const color = getPlatformColorDef(PaletteColorIndexes.Crocodile, $themeStore.dark)}
{#if state}
<div
class="states flex-row-center"
style:color={color.title}
style:background={color.background ?? defaultBackground($themeStore.dark)}
>
<div class="bar" />
<div class="mr-2">
<Won size={'medium'} />
</div>
<div class="flex-grow caption-color">
<StringPresenter value={state.name} oneLine />
<!-- <AttributeEditor maxWidth={'13rem'} _class={state._class} object={state} key="name" />-->
</div>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
class="tool hover-trans"
on:click={(ev) => {
showPopup(
StatusesPopup,
{
onDelete: () => dispatch('delete', { state }),
showDelete: wonStates.length > 1,
onUpdate: () => edit(state)
},
eventToHTMLElement(ev),
() => {}
)
}}
>
<IconMoreH size={'medium'} />
</div>
</div>
{/if}
{/each}
</div>
</div>
<div class="mt-9">
<div class="flex-no-shrink flex-between trans-title uppercase">
<Label label={task.string.DoneStatesLost} />
<CircleButton
icon={IconAdd}
size={'medium'}
on:click={() => {
add(task.class.LostStateTemplate)
}}
/>
</div>
<div class="mt-4 mb-10">
{#each lostStates as state}
{@const color = getPlatformColorDef(PaletteColorIndexes.Firework, $themeStore.dark)}
{#if state}
<div
class="states flex-row-center"
style:color={color.title}
style:background={color.background ?? defaultBackground($themeStore.dark)}
>
<div class="bar" />
<div class="mr-2">
<Lost size={'medium'} />
</div>
<div class="flex-grow caption-color">
<StringPresenter value={state.name} oneLine />
<!-- <AttributeEditor maxWidth={'13rem'} _class={state._class} object={state} key="name" />-->
</div>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
class="tool hover-trans"
on:click={(ev) => {
showPopup(
StatusesPopup,
{
onDelete: () => dispatch('delete', { state }),
showDelete: lostStates.length > 1,
onUpdate: () => edit(state)
},
eventToHTMLElement(ev),
() => {}
)
}}
>
<IconMoreH size={'medium'} />
</div>
</div>
{/if}
{/each}
</div>
</div>
<style lang="scss">
.states {
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;
.bar {
margin-right: 0.25rem;
width: 1rem;
height: 1rem;
opacity: 0.4;
cursor: grabbing;
}
.color {
margin-right: 0.75rem;
width: 1rem;
height: 1rem;
border-radius: 0.25rem;
cursor: pointer;
}
.tool {
margin-left: 1rem;
}
}
.states + .states {
margin-top: 0.5rem;
}
</style>