Custom done states (#1238)

This commit is contained in:
Denis Bykhov
2022-03-30 12:35:09 +07:00
committed by GitHub
parent 5f4edc43c0
commit 944cfafcba
11 changed files with 269 additions and 168 deletions
@@ -14,7 +14,7 @@
// limitations under the License.
-->
<script lang="ts">
import { Class, DocumentQuery, FindOptions, Ref } from '@anticrm/core'
import { Class, DocumentQuery, FindOptions, Ref, SortingOrder } from '@anticrm/core'
import { createQuery } from '@anticrm/presentation'
import { DoneState, SpaceWithStates, State, Task } from '@anticrm/task'
import { ScrollBox } from '@anticrm/ui'
@@ -57,7 +57,13 @@
{
space
},
(res) => (doneStates = res)
(res) => (doneStates = res),
{
sort: {
_class: SortingOrder.Descending,
rank: SortingOrder.Descending
}
}
)
async function updateQuery (search: string, selectedDoneStates: Set<Ref<DoneState>>): Promise<void> {
@@ -129,11 +135,20 @@
<Label label={task.string.DoneStates} />
</div>
</div>
<div class="flex-row-center caption-color states">
<div class="flex-row-center caption-color states" class:antiStatesBar={doneStatusesView}>
{#if doneStatusesView}
<div
class="doneState withoutDone flex-center whitespace-nowrap"
class:disable={!withoutDone}
on:click={() => {
noDoneClick()
}}
>
<Label label={task.string.NoDoneState}/>
</div>
{#each doneStates as state}
<div
class="doneState flex-row-center"
class="doneState flex-center whitespace-nowrap"
class:won={state._class === task.class.WonState}
class:lost={state._class === task.class.LostState}
class:disable={!selectedDoneStates.has(state._id)}
@@ -151,15 +166,6 @@
</span>
</div>
{/each}
<div
class="doneState withoutDone flex-row-center"
class:disable={!withoutDone}
on:click={() => {
noDoneClick()
}}
>
<Label label={task.string.NoDoneState}/>
</div>
{:else}
<StatesBar bind:state {space} on:change={() => updateQuery(search, selectedDoneStates)} />
{/if}
@@ -0,0 +1,129 @@
<!--
// 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 { AttachedDoc, Class, Doc, DocumentUpdate, FindOptions, Ref, SortingOrder } from '@anticrm/core'
import core from '@anticrm/core'
import { getResource } from '@anticrm/platform'
import { createQuery, getClient } from '@anticrm/presentation'
import type { Kanban, SpaceWithStates, State } from '@anticrm/task'
import task, { DoneState, LostState, WonState, DocWithRank, calcRank } from '@anticrm/task'
import { AnySvelteComponent, getPlatformColor, Grid } from '@anticrm/ui'
import { Loading, ScrollBox } from '@anticrm/ui'
import KanbanPanel from './KanbanPanel.svelte'
import { createEventDispatcher } from 'svelte'
export let kanban: Kanban
let wonStates: WonState[] = []
let lostStates: LostState[] = []
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)
})
}
let hoveredDoneState: Ref<DoneState> | undefined
const onDone = (state: DoneState) => async () => {
hoveredDoneState = undefined
dispatch('done', state)
}
</script>
<div class="done-panel overflow-y-auto whitespace-nowrap">
{#each wonStates as wonState}
<div
class="flex-grow flex-center done-item"
class:hovered={hoveredDoneState === wonState._id}
on:dragenter={() => {
hoveredDoneState = wonState._id
}}
on:dragleave={() => {
if (hoveredDoneState === wonState._id) {
hoveredDoneState = undefined
}
}}
on:dragover|preventDefault={() => {}}
on:drop={onDone(wonState)}>
<div class="done-icon won mr-2"/>
{wonState.title}
</div>
{/each}
{#each lostStates as lostState}
<div
class="flex-grow flex-center done-item"
class:hovered={hoveredDoneState === lostState._id}
on:dragenter={() => {
hoveredDoneState = lostState._id
}}
on:dragleave={() => {
if (hoveredDoneState === lostState._id) {
hoveredDoneState = undefined
}
}}
on:dragover|preventDefault={() => {}}
on:drop={onDone(lostState)}>
<div class="done-icon lost mr-2"/>
{lostState.title}
</div>
{/each}
</div>
<style lang="scss">
.done-panel {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
display: flex;
align-items: center;
justify-content: stretch;
padding: .5rem 2.5rem;
background-color: var(--theme-bg-color);
border-top: 1px solid var(--theme-dialog-divider);
border-radius: 0 0 1.25rem 1.25rem;
}
.done-item {
height: 3rem;
color: var(--theme-caption-color);
border: 1px dashed transparent;
border-radius: .75rem;
padding: 0.5rem;
&.hovered {
background-color: var(--theme-button-bg-enabled);
border-color: var(--theme-dialog-divider);
}
}
.done-icon {
width: .5rem;
height: .5rem;
border-radius: 50%;
&.won { background-color: #27B166; }
&.lost { background-color: #F96E50; }
}
</style>
@@ -1,24 +1,23 @@
<!--
// 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 '@anticrm/core'
import { createQuery, getClient } from '@anticrm/presentation'
import type { Kanban, State, DoneState } from '@anticrm/task'
import task, { calcRank } from '@anticrm/task'
import { Class,Ref,SortingOrder } from '@anticrm/core'
import { createQuery,getClient } from '@anticrm/presentation'
import type { DoneState,Kanban,State } from '@anticrm/task'
import task,{ calcRank } from '@anticrm/task'
import StatesEditor from '../state/StatesEditor.svelte'
export let kanban: Kanban
@@ -32,6 +31,7 @@
$: lostStates = doneStates.filter((x) => x._class === task.class.LostState)
const client = getClient()
const hierarchy = client.getHierarchy()
const statesQ = createQuery()
$: statesQ.query(task.class.State, { space: kanban.space }, result => { states = result}, {
@@ -65,19 +65,25 @@
)
}
async function onAdd () {
async function onAdd (_class: Ref<Class<State | DoneState>>) {
const lastOne = await client.findOne(
task.class.State,
_class,
{ space: kanban.space },
{ sort: { rank: SortingOrder.Descending } }
)
await client.createDoc(task.class.State, kanban.space, {
title: 'New State',
color: 9,
rank: calcRank(lastOne, undefined)
})
if (hierarchy.isDerived(_class, task.class.DoneState)) {
await client.createDoc(_class, kanban.space, {
title: 'New Done State',
rank: calcRank(lastOne, undefined)
})
} else {
await client.createDoc(task.class.State, kanban.space, {
title: 'New State',
color: 9,
rank: calcRank(lastOne, undefined)
})
}
}
</script>
<StatesEditor {states} {wonStates} {lostStates} on:add={onAdd} on:delete on:move={onMove}/>
<StatesEditor {states} {wonStates} {lostStates} on:add={(e) => { onAdd(e.detail) }} on:delete on:move={onMove}/>
@@ -15,10 +15,10 @@
-->
<script lang="ts">
import { createEventDispatcher } from 'svelte'
import { Ref, Space, SortingOrder } from '@anticrm/core'
import { Ref, Space, SortingOrder, Class } from '@anticrm/core'
import core from '@anticrm/core'
import { createQuery, getClient } from '@anticrm/presentation'
import type { State, DoneStateTemplate, KanbanTemplate, StateTemplate } from '@anticrm/task'
import type { State, DoneStateTemplate, KanbanTemplate, StateTemplate, DoneState } from '@anticrm/task'
import task, { calcRank } from '@anticrm/task'
import StatesEditor from '../state/StatesEditor.svelte'
@@ -34,6 +34,7 @@
const dispatch = createEventDispatcher()
const client = getClient()
const hierarchy = client.getHierarchy()
const statesQ = createQuery()
$: statesQ.query(task.class.StateTemplate, { attachedTo: kanban._id }, result => { states = result }, {
@@ -71,28 +72,44 @@
)
}
async function onAdd () {
async function onAdd (_class: Ref<Class<State | DoneState>>) {
const lastOne = await client.findOne(
task.class.StateTemplate,
{ attachedTo: kanban._id },
{ sort: { rank: SortingOrder.Descending } }
)
await client.addCollection(
task.class.StateTemplate,
kanban.space,
kanban._id,
kanban._class,
'statesC',
{
title: 'New State',
color: 9,
rank: calcRank(lastOne, undefined)
}
)
if (hierarchy.isDerived(_class, task.class.DoneState)) {
const targetClass = _class === task.class.WonState ? task.class.WonStateTemplate : task.class.LostStateTemplate
await client.addCollection(
targetClass,
kanban.space,
kanban._id,
kanban._class,
'doneStatesC',
{
title: 'New Done State',
rank: calcRank(lastOne, undefined)
}
)
} else {
await client.addCollection(
task.class.StateTemplate,
kanban.space,
kanban._id,
kanban._class,
'statesC',
{
title: 'New State',
color: 9,
rank: calcRank(lastOne, undefined)
}
)
}
}
function onDelete ({ detail: { state } }: { detail: { state: State }}) {
function onDelete ({ detail: { state } }: { detail: { state: State | DoneState }}) {
if (space === undefined) {
return
}
@@ -101,4 +118,4 @@
}
</script>
<StatesEditor {states} {wonStates} {lostStates} on:add={onAdd} on:delete={onDelete} on:move={onMove}/>
<StatesEditor {states} {wonStates} {lostStates} on:add={(e) => { onAdd(e.detail) }} on:delete={onDelete} on:move={onMove}/>
@@ -15,14 +15,13 @@
-->
<script lang="ts">
import { AttachedDoc, Class, Doc, DocumentUpdate, FindOptions, Ref, SortingOrder } from '@anticrm/core'
import core from '@anticrm/core'
import core,{ AttachedDoc,Class,Doc,DocumentUpdate,FindOptions,Ref,SortingOrder } from '@anticrm/core'
import { getResource } from '@anticrm/platform'
import { createQuery, getClient } from '@anticrm/presentation'
import type { Kanban, SpaceWithStates, State } from '@anticrm/task'
import task, { DoneState, LostState, WonState, DocWithRank, calcRank } from '@anticrm/task'
import { AnySvelteComponent, getPlatformColor } from '@anticrm/ui'
import { Loading, ScrollBox } from '@anticrm/ui'
import { createQuery,getClient } from '@anticrm/presentation'
import type { Kanban,SpaceWithStates,State } from '@anticrm/task'
import task,{ calcRank,DocWithRank,DoneState } from '@anticrm/task'
import { AnySvelteComponent,getPlatformColor,Loading,ScrollBox } from '@anticrm/ui'
import KanbanDragDone from './KanbanDragDone.svelte'
import KanbanPanel from './KanbanPanel.svelte'
// import KanbanPanelEmpty from './KanbanPanelEmpty.svelte'
@@ -39,8 +38,6 @@
let states: State[] = []
let objects: Item[] = []
let wonState: WonState | undefined
let lostState: LostState | undefined
const kanbanQuery = createQuery()
$: kanbanQuery.query(task.class.Kanban, { attachedTo: space }, result => { kanban = result[0] })
@@ -54,17 +51,6 @@
})
}
const doneStatesQ = createQuery()
$: if (kanban !== undefined) {
doneStatesQ.query(
task.class.DoneState,
{ space: kanban.space, ...search !== '' ? {$search: search} : {} },
(result) => {
wonState = result.find((x) => x._class === task.class.WonState)
lostState = result.find((x) => x._class === task.class.LostState)
})
}
const objsQ = createQuery()
$: objsQ.query(
_class,
@@ -164,14 +150,12 @@
return await getResource(presenterMixin.card)
}
const onDone = (state: DoneState) => async () => {
async function onDone (state: DoneState): Promise<void> {
isDragging = false
hoveredDoneState = undefined
await updateItem(dragCard, { doneState: state._id })
}
let isDragging = false
let hoveredDoneState: Ref<DoneState> | undefined
</script>
{#await cardPresenter(_class)}
@@ -227,37 +211,8 @@
</div>
</ScrollBox>
</div>
{#if isDragging && wonState !== undefined && lostState !== undefined}
<div class="done-panel">
<div
class="flex-grow flex-center done-item"
class:hovered={hoveredDoneState === wonState._id}
on:dragenter={() => {
hoveredDoneState = wonState?._id
}}
on:dragleave={() => {
hoveredDoneState = undefined
}}
on:dragover|preventDefault={() => {}}
on:drop={onDone(wonState)}>
<div class="done-icon won mr-2"/>
{wonState.title}
</div>
<div
class="flex-grow flex-center done-item"
class:hovered={hoveredDoneState === lostState._id}
on:dragenter={() => {
hoveredDoneState = lostState?._id
}}
on:dragleave={() => {
hoveredDoneState = undefined
}}
on:dragover|preventDefault={() => {}}
on:drop={onDone(lostState)}>
<div class="done-icon lost mr-2"/>
{lostState.title}
</div>
</div>
{#if isDragging}
<KanbanDragDone {kanban} on:done={(e) => { onDone(e.detail) }} />
{/if}
</div>
{/await}
@@ -273,42 +228,6 @@
height: 100%;
}
.done-panel {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
display: flex;
align-items: center;
justify-content: stretch;
padding: .5rem 2.5rem;
background-color: var(--theme-bg-color);
border-top: 1px solid var(--theme-dialog-divider);
border-radius: 0 0 1.25rem 1.25rem;
}
.done-item {
height: 3rem;
color: var(--theme-caption-color);
border: 1px dashed transparent;
border-radius: .75rem;
&.hovered {
background-color: var(--theme-button-bg-enabled);
border-color: var(--theme-dialog-divider);
}
}
.done-icon {
width: .5rem;
height: .5rem;
border-radius: 50%;
&.won { background-color: #27B166; }
&.lost { background-color: #F96E50; }
}
.scrollable {
height: 100%;
margin-bottom: .25rem;
@@ -33,6 +33,7 @@
},
{
sort: {
_class: SortingOrder.Descending,
rank: SortingOrder.Ascending
}
}
@@ -15,10 +15,10 @@
-->
<script lang="ts">
import type { Class, Obj, Ref } from '@anticrm/core'
import type { Class, Doc, DocumentQuery, Obj, Ref } from '@anticrm/core'
import core from '@anticrm/core'
import { createQuery, getClient, MessageBox } from '@anticrm/presentation'
import type { Kanban, SpaceWithStates, State } from '@anticrm/task'
import type { DoneState, Kanban, SpaceWithStates, State } from '@anticrm/task'
import task from '../../plugin'
import KanbanEditor from '../kanban/KanbanEditor.svelte'
import { Icon, IconClose, Label, showPopup, ActionIcon, ScrollBox } from '@anticrm/ui'
@@ -33,6 +33,7 @@
let spaceInstance: SpaceWithStates | undefined
const client = getClient()
const hierarchy = client.getHierarchy()
const dispatch = createEventDispatcher()
const kanbanQ = createQuery()
@@ -44,7 +45,7 @@
const spaceI = createQuery()
$: spaceI.query<SpaceWithStates>(spaceClass, { _id: _id }, result => { spaceInstance = result.shift() })
async function deleteState ({ state }: { state: State }) {
async function deleteState ({ state }: { state: State | DoneState }) {
if (spaceInstance === undefined) {
return
}
@@ -53,7 +54,14 @@
const spaceView = client.getHierarchy().as(spaceClassInstance, workbench.mixin.SpaceView)
const containingClass = spaceView.view.class
const objectsInThisState = await client.findAll(containingClass, { state: state._id })
let query: DocumentQuery<Doc>
if (hierarchy.isDerived(state._class, task.class.DoneState)) {
query = { doneState: state._id }
} else {
query = { state: state._id }
}
const objectsInThisState = await client.findAll(containingClass, query)
if (objectsInThisState.length > 0) {
showPopup(MessageBox, {
@@ -66,7 +74,6 @@
message: task.string.StatusDeleteConfirm
}, undefined, async (result) => {
if (result && kanban !== undefined) {
await client.updateDoc(kanban._class, kanban.space, kanban._id, { $pull: { states: state._id } })
client.removeDoc(state._class, state.space, state._id)
}
})
@@ -15,7 +15,7 @@
-->
<script lang="ts">
import { Ref } from '@anticrm/core'
import { Class, Ref } from '@anticrm/core'
import { AttributeEditor, getClient } from '@anticrm/presentation'
import type { DoneState, State } from '@anticrm/task'
import { CircleButton, IconAdd, IconMoreH, Label, showPopup, getPlatformColor } from '@anticrm/ui'
@@ -69,17 +69,17 @@
await client.updateDoc(state._class, state.space, state._id, { color })
}
async function onAdd () {
dispatch('add')
async function onAdd (_class: Ref<Class<State | DoneState>>) {
dispatch('add', _class)
}
</script>
<div class="flex-col">
<div>
<div class="flex-no-shrink flex-between trans-title uppercase">
<Label label={task.string.ActiveStates} />
<CircleButton icon={IconAdd} size={'medium'} on:click={onAdd}/>
<CircleButton icon={IconAdd} size={'medium'} on:click={() => { onAdd(task.class.State) }}/>
</div>
<div class="overflow-y-auto mt-3">
<div class="mt-3">
{#each states as state, i}
{#if state}
<div bind:this={elements[i]} class="flex-between states" draggable={true}
@@ -116,33 +116,53 @@
{/each}
</div>
</div>
<div class="flex-col mt-9">
<div class="flex-no-shrink trans-title uppercase">
<div class="mt-9">
<div class="flex-no-shrink flex-between trans-title uppercase">
<Label label={task.string.DoneStatesWon} />
<CircleButton icon={IconAdd} size={'medium'} on:click={() => { onAdd(task.class.WonState) }}/>
</div>
<div class="overflow-y-auto mt-4">
<div class="mt-4">
{#each wonStates as state}
{#if state}
<div class="states flex-row-center">
<div class="bar"/>
<div class="color" style="background-color: #a5d179"/>
<div class="flex-grow caption-color"><AttributeEditor maxWidth={'13rem'} _class={state._class} object={state} key="title"/></div>
{#if wonStates.length > 1}
<div class="tool hover-trans"
on:click={(ev) => {
showPopup(StatusesPopup, { onDelete: () => dispatch('delete', { state }) }, ev.target, () => {})
}}
>
<IconMoreH size={'medium'} />
</div>
{/if}
</div>
{/if}
{/each}
</div>
</div>
<div class="flex-col mt-9">
<div class="flex-no-shrink trans-title uppercase">
<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={() => { onAdd(task.class.LostState) }}/>
</div>
<div class="overflow-y-auto mt-4">
<div class="mt-4">
{#each lostStates as state}
{#if state}
<div class="states flex-row-center">
<div class="bar"/>
<div class="color" style="background-color: #f28469"/>
<div class="flex-grow caption-color"><AttributeEditor maxWidth={'13rem'} _class={state._class} object={state} key="title"/></div>
{#if lostStates.length > 1}
<div class="tool hover-trans"
on:click={(ev) => {
showPopup(StatusesPopup, { onDelete: () => dispatch('delete', { state }) }, ev.target, () => {})
}}
>
<IconMoreH size={'medium'} />
</div>
{/if}
</div>
{/if}
{/each}