mirror of
https://github.com/hcengineering/platform.git
synced 2026-09-28 20:45:00 +02:00
Tasks as Attached Documents (#539)
Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
@@ -18,8 +18,8 @@
|
||||
import { EditBox, Grid, IconFolder, ToggleWithLabel } from '@anticrm/ui'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import task from '../plugin'
|
||||
import view, { createKanban, KanbanTemplate } from '@anticrm/view'
|
||||
import { KanbanTemplateSelector } from '@anticrm/view-resources'
|
||||
import { createKanban, KanbanTemplate } from '@anticrm/task'
|
||||
import KanbanTemplateSelector from './kanban/KanbanTemplateSelector.svelte'
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
const client = getClient()
|
||||
|
||||
async function createProject (): Promise<void> {
|
||||
if (templateId !== undefined && await client.findOne(view.class.KanbanTemplate, { _id: templateId }) === undefined) {
|
||||
if (templateId !== undefined && await client.findOne(task.class.KanbanTemplate, { _id: templateId }) === undefined) {
|
||||
throw Error(`Failed to find target kanban template: ${templateId}`)
|
||||
}
|
||||
|
||||
|
||||
@@ -14,53 +14,58 @@
|
||||
-->
|
||||
<script lang="ts">
|
||||
import contact, { Employee } from '@anticrm/contact'
|
||||
import type { Data, Ref, Space } from '@anticrm/core'
|
||||
import core from '@anticrm/core'
|
||||
import type { AttachedData, Data, Doc, Ref, Space } from '@anticrm/core'
|
||||
import { generateId } from '@anticrm/core'
|
||||
import { OK, Status } from '@anticrm/platform'
|
||||
import { Card, getClient, UserBox } from '@anticrm/presentation'
|
||||
import { Task } from '@anticrm/task'
|
||||
import { Issue, State } from '@anticrm/task'
|
||||
import { EditBox, Grid, Status as StatusControl } from '@anticrm/ui'
|
||||
import view from '@anticrm/view'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import task from '../plugin'
|
||||
|
||||
export let space: Ref<Space>
|
||||
export let parent: Pick<Doc, '_id' | '_class'> | undefined
|
||||
|
||||
let _space = space
|
||||
|
||||
$: _space = space
|
||||
const status: Status = OK
|
||||
|
||||
let assignee: Ref<Employee> | null = null
|
||||
|
||||
const object: Data<Task> = {
|
||||
const object: Data<Issue> = {
|
||||
name: '',
|
||||
description: '',
|
||||
assignee: null,
|
||||
number: 0
|
||||
number: 0,
|
||||
attachedTo: task.global.Task,
|
||||
attachedToClass: task.class.Issue,
|
||||
collection: 'tasks',
|
||||
state: '' as Ref<State>
|
||||
}
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const client = getClient()
|
||||
const taskId = generateId()
|
||||
const taskId: Ref<Issue> = generateId()
|
||||
|
||||
export function canClose (): boolean {
|
||||
return object.name !== ''
|
||||
}
|
||||
|
||||
async function createTask () {
|
||||
const state = await client.findOne(core.class.State, { space: _space })
|
||||
const state = await client.findOne(task.class.State, { space: _space })
|
||||
if (state === undefined) {
|
||||
throw new Error('create application: state not found')
|
||||
}
|
||||
|
||||
const sequence = await client.findOne(view.class.Sequence, { attachedTo: task.class.Task })
|
||||
const sequence = await client.findOne(task.class.Sequence, { attachedTo: task.class.Issue })
|
||||
if (sequence === undefined) {
|
||||
throw new Error('sequence object not found')
|
||||
}
|
||||
|
||||
const incResult = await client.updateDoc(
|
||||
view.class.Sequence,
|
||||
view.space.Sequence,
|
||||
task.class.Sequence,
|
||||
task.space.Sequence,
|
||||
sequence._id,
|
||||
{
|
||||
$inc: { sequence: 1 }
|
||||
@@ -68,7 +73,7 @@
|
||||
true
|
||||
)
|
||||
|
||||
const value: Data<Task> = {
|
||||
const value: AttachedData<Issue> = {
|
||||
name: object.name,
|
||||
description: object.description,
|
||||
assignee,
|
||||
@@ -77,8 +82,7 @@
|
||||
state: state._id
|
||||
}
|
||||
|
||||
await client.createDoc(task.class.Task, _space, value, taskId)
|
||||
dispatch('close')
|
||||
await client.addCollection(task.class.Issue, _space, parent?._id ?? task.global.Task, parent?._class ?? task.class.Issue, 'tasks', value, taskId)
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -87,7 +91,7 @@
|
||||
<Card
|
||||
label={task.string.CreateTask}
|
||||
okAction={createTask}
|
||||
canSave={object.name.length > 0}
|
||||
canSave={object.name.length > 0 && _space != null}
|
||||
spaceClass={task.class.Project}
|
||||
spaceLabel={task.string.ProjectName}
|
||||
spacePlaceholder={task.string.SelectProject}
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
import type { Ref } from '@anticrm/core'
|
||||
import { Panel } from '@anticrm/panel'
|
||||
import { createQuery, getClient } from '@anticrm/presentation'
|
||||
import type { Task } from '@anticrm/task'
|
||||
import type { Issue } from '@anticrm/task'
|
||||
import { EditBox, Grid } from '@anticrm/ui'
|
||||
import view from '@anticrm/view'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
@@ -24,11 +24,11 @@
|
||||
import { Attachments } from '@anticrm/attachment-resources'
|
||||
import TaskHeader from './TaskHeader.svelte'
|
||||
|
||||
export let _id: Ref<Task>
|
||||
let object: Task
|
||||
export let _id: Ref<Issue>
|
||||
let object: Issue
|
||||
|
||||
const query = createQuery()
|
||||
$: query.query(task.class.Task, { _id }, (result) => {
|
||||
$: query.query(task.class.Issue, { _id }, (result) => {
|
||||
object = result[0]
|
||||
})
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
const client = getClient()
|
||||
|
||||
function change (field: string, value: any) {
|
||||
client.updateDoc(task.class.Task, object.space, object._id, { [field]: value })
|
||||
client.updateCollection(object._class, object.space, object._id, object.attachedTo, object.attachedToClass, object.collection, { [field]: value })
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -18,13 +18,13 @@
|
||||
import { formatName } from '@anticrm/contact'
|
||||
import type { WithLookup } from '@anticrm/core'
|
||||
import { Avatar } from '@anticrm/presentation'
|
||||
import type { Task } from '@anticrm/task'
|
||||
import type { Issue } from '@anticrm/task'
|
||||
import { ActionIcon, IconMoreH, Label, showPopup } from '@anticrm/ui'
|
||||
import { ContextMenu } from '@anticrm/view-resources'
|
||||
import task from '../plugin'
|
||||
import TaskPresenter from './TaskPresenter.svelte'
|
||||
|
||||
export let object: WithLookup<Task>
|
||||
export let object: WithLookup<Issue>
|
||||
export let draggable: boolean
|
||||
|
||||
const showMenu = (ev?: Event): void => {
|
||||
|
||||
@@ -16,14 +16,14 @@
|
||||
<script lang="ts">
|
||||
import contact from '@anticrm/contact'
|
||||
import { AttributeBarEditor, getClient, UserBox } from '@anticrm/presentation'
|
||||
import { Task } from '@anticrm/task'
|
||||
import { Issue } from '@anticrm/task'
|
||||
import task from '../plugin'
|
||||
|
||||
export let object: Task
|
||||
export let object: Issue
|
||||
const client = getClient()
|
||||
|
||||
function change () {
|
||||
client.updateDoc(object._class, object.space, object._id, { assignee: object.assignee })
|
||||
client.updateCollection(object._class, object.space, object._id, object.attachedTo, object.attachedToClass, object.collection, { assignee: object.assignee })
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -14,13 +14,13 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import type { Task } from '@anticrm/task'
|
||||
import type { Issue } from '@anticrm/task'
|
||||
import { closeTooltip, Icon, showPopup } from '@anticrm/ui'
|
||||
import EditTask from './EditTask.svelte'
|
||||
import { getClient } from '@anticrm/presentation'
|
||||
import task from '../plugin'
|
||||
|
||||
export let value: Task
|
||||
export let value: Issue
|
||||
|
||||
const client = getClient()
|
||||
const shortLabel = client.getHierarchy().getClass(value._class).shortLabel
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
<!--
|
||||
// Copyright © 2020 Anticrm Platform Contributors.
|
||||
//
|
||||
// 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 { Ref, Space, Doc, Class } from '@anticrm/core'
|
||||
import type { Issue } from '@anticrm/task'
|
||||
import { createQuery } from '@anticrm/presentation'
|
||||
import { CircleButton, IconAdd, showPopup, Label } from '@anticrm/ui'
|
||||
import CreateTask from './CreateTask.svelte'
|
||||
// import FileDuo from "./icons/FileDuo.svelte"
|
||||
import { Table } from '@anticrm/view-resources'
|
||||
|
||||
import core from '@anticrm/core'
|
||||
import task from '../plugin'
|
||||
|
||||
export let objectId: Ref<Doc>
|
||||
export let space: Ref<Space>
|
||||
export let _class: Ref<Class<Doc>>
|
||||
|
||||
let tasks: Issue[] = []
|
||||
|
||||
const query = createQuery()
|
||||
$: query.query(task.class.Issue, { attachedTo: objectId }, result => { tasks = result })
|
||||
|
||||
const createApp = (ev: MouseEvent): void =>
|
||||
showPopup(CreateTask, { parent: { _id: objectId, _class, space } }, ev.target as HTMLElement, () => {})
|
||||
</script>
|
||||
|
||||
<div class="applications-container">
|
||||
<div class="flex-row-center">
|
||||
<div class="title">Tasks</div>
|
||||
<CircleButton icon={IconAdd} size={'small'} on:click={createApp} />
|
||||
</div>
|
||||
{#if tasks.length > 0}
|
||||
<Table
|
||||
_class={task.class.Issue}
|
||||
config={['', '$lookup.space.name', '$lookup.state']}
|
||||
options={
|
||||
{
|
||||
lookup: {
|
||||
state: task.class.State,
|
||||
space: core.class.Space
|
||||
}
|
||||
}
|
||||
}
|
||||
query={ { attachedTo: objectId } }
|
||||
/>
|
||||
{:else}
|
||||
<div class="flex-col-center mt-5 createapp-container">
|
||||
<!-- <FileDuo size={'large'} /> -->
|
||||
<div class="small-text content-dark-color mt-2">
|
||||
<Label label={task.string.NoTaskForObject} />
|
||||
</div>
|
||||
<div class="small-text">
|
||||
<a href={'#'} on:click={createApp}><Label label={task.string.CreateTask} /></a>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.applications-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.title {
|
||||
margin-right: .75rem;
|
||||
font-weight: 500;
|
||||
font-size: 1.25rem;
|
||||
color: var(--theme-caption-color);
|
||||
}
|
||||
}
|
||||
|
||||
.createapp-container {
|
||||
padding: 1rem;
|
||||
color: var(--theme-caption-color);
|
||||
background: var(--theme-bg-accent-color);
|
||||
border: 1px solid var(--theme-bg-accent-color);
|
||||
border-radius: .75rem;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,54 @@
|
||||
<!--
|
||||
// Copyright © 2020, 2021 Anticrm Platform Contributors.
|
||||
//
|
||||
// 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 { IntlString } from '@anticrm/platform'
|
||||
import { Label } from '@anticrm/ui'
|
||||
import { IconAdd } from '@anticrm/ui'
|
||||
|
||||
export let label: IntlString
|
||||
</script>
|
||||
|
||||
<div class="card-container">
|
||||
<div class="icon">
|
||||
<IconAdd size={'small'} />
|
||||
</div>
|
||||
<Label {label} />
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.card-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: .75rem 1.5rem;
|
||||
color: var(--theme-content-dark-color);
|
||||
background-color: var(--theme-button-bg-hovered);
|
||||
border: 1px dotted var(--theme-bg-accent-color);
|
||||
border-radius: .75rem;
|
||||
cursor: pointer;
|
||||
|
||||
.icon {
|
||||
margin-bottom: .25rem;
|
||||
opacity: .4;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: var(--theme-caption-color);
|
||||
.icon { opacity: 1; }
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,79 @@
|
||||
<!--
|
||||
// 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 type { Ref, Doc } from '@anticrm/core'
|
||||
import { createQuery, getClient } from '@anticrm/presentation'
|
||||
import type { Kanban, State, DoneState } from '@anticrm/task'
|
||||
import task from '@anticrm/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()
|
||||
|
||||
function sort <T extends Doc> (order: Ref<T>[], items: T[]): T[] {
|
||||
if (items.length === 0) {
|
||||
return []
|
||||
}
|
||||
|
||||
const itemMap = new Map(items.map(x => [x._id, x]))
|
||||
const x = order
|
||||
.map(id => itemMap.get(id))
|
||||
.filter((x): x is T => x !== undefined)
|
||||
|
||||
return x
|
||||
}
|
||||
|
||||
const statesQ = createQuery()
|
||||
$: statesQ.query(task.class.State, { _id: { $in: kanban.states ?? [] } }, result => { states = sort(kanban.states, result) })
|
||||
|
||||
const doneStatesQ = createQuery()
|
||||
$: doneStatesQ.query(task.class.DoneState, { _id: { $in: kanban.doneStates } }, (result) => { doneStates = sort(kanban.doneStates, result) })
|
||||
|
||||
async function onMove ({ detail: { stateID, position } }: { detail: { stateID: Ref<State>, position: number } }) {
|
||||
client.updateDoc(kanban._class, kanban.space, kanban._id, {
|
||||
$move: {
|
||||
states: {
|
||||
$value: stateID,
|
||||
$position: position
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function onAdd () {
|
||||
const state = await client.createDoc(task.class.State, kanban.space, {
|
||||
title: 'New State',
|
||||
color: '#7C6FCD'
|
||||
})
|
||||
await client.updateDoc(kanban._class, kanban.space, kanban._id, {
|
||||
$push: {
|
||||
states: state
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<StatesEditor {states} {wonStates} {lostStates} on:add={onAdd} on:delete on:move={onMove}/>
|
||||
@@ -0,0 +1,91 @@
|
||||
<!--
|
||||
// Copyright © 2020, 2021 Anticrm Platform Contributors.
|
||||
//
|
||||
// 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 { IntlString } from '@anticrm/platform'
|
||||
import { Label, ScrollBox, IconAdd } from '@anticrm/ui'
|
||||
|
||||
export let label: IntlString
|
||||
export let color: string
|
||||
// export let counter: number
|
||||
export let addAction: () => void | undefined
|
||||
</script>
|
||||
|
||||
<div class="panel-container step-lr75">
|
||||
<div class="header">
|
||||
<div class="bar" style="background-color: {color}" />
|
||||
<div class="flex-between label">
|
||||
<div>
|
||||
<Label {label} />
|
||||
<!-- <span>({counter})</span> -->
|
||||
</div>
|
||||
<div class="tool" on:click|preventDefault={addAction}>
|
||||
<IconAdd size={'small'} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="scroll" on:dragover on:drop>
|
||||
<ScrollBox vertical>
|
||||
<slot />
|
||||
</ScrollBox>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.panel-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
width: 20rem;
|
||||
height: 100%;
|
||||
background-color: var(--theme-button-bg-enabled);
|
||||
border: 1px solid var(--theme-bg-accent-color);
|
||||
border-radius: .75rem;
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 0 .75rem;
|
||||
height: 4rem;
|
||||
min-height: 4rem;
|
||||
user-select: none;
|
||||
|
||||
.bar {
|
||||
height: .375rem;
|
||||
border-radius: 1px;
|
||||
}
|
||||
.label {
|
||||
padding: 0 .5rem 0 1rem;
|
||||
height: 100%;
|
||||
font-weight: 500;
|
||||
color: var(--theme-caption-color);
|
||||
// span {
|
||||
// font-weight: 400;
|
||||
// color: var(--theme-content-dark-color);
|
||||
// }
|
||||
}
|
||||
.tool {
|
||||
opacity: .4;
|
||||
cursor: pointer;
|
||||
&:hover { opacity: 1; }
|
||||
}
|
||||
}
|
||||
|
||||
.scroll {
|
||||
margin: 0 .75rem .75rem;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,68 @@
|
||||
<!--
|
||||
// Copyright © 2020, 2021 Anticrm Platform Contributors.
|
||||
//
|
||||
// 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 { IntlString } from '@anticrm/platform'
|
||||
import { Label } from '@anticrm/ui'
|
||||
import { IconAdd } from '@anticrm/ui'
|
||||
|
||||
export let label: IntlString
|
||||
</script>
|
||||
|
||||
<div class="panel-container step-lr75">
|
||||
<div class="circle">
|
||||
<div class="icon">
|
||||
<IconAdd size={'large'} />
|
||||
</div>
|
||||
</div>
|
||||
<Label {label} />
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.panel-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 20rem;
|
||||
height: 100%;
|
||||
padding: .75rem;
|
||||
color: var(--theme-caption-color);
|
||||
background-color: var(--theme-button-bg-enabled);
|
||||
border: 1px dotted var(--theme-bg-accent-color);
|
||||
border-radius: .75rem;
|
||||
cursor: pointer;
|
||||
|
||||
.circle {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-bottom: 1.25rem;
|
||||
width: 5rem;
|
||||
height: 5rem;
|
||||
background-color: var(--theme-bg-accent-color);
|
||||
border-radius: 50%;
|
||||
|
||||
.icon { opacity: .6; }
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--theme-bg-accent-hover);
|
||||
box-shadow: 0 .75rem 2.5rem rgba(0, 0, 0, .15);
|
||||
|
||||
.circle .icon { opacity: 1; }
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,103 @@
|
||||
<!--
|
||||
// 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 { createEventDispatcher } from 'svelte'
|
||||
import { Ref, Space, Doc, generateId } from '@anticrm/core'
|
||||
import core from '@anticrm/core'
|
||||
import { createQuery, getClient } from '@anticrm/presentation'
|
||||
import type { State, DoneStateTemplate, KanbanTemplate, StateTemplate } from '@anticrm/task'
|
||||
import task from '@anticrm/task'
|
||||
|
||||
import StatesEditor from '../state/StatesEditor.svelte'
|
||||
|
||||
export let kanban: KanbanTemplate
|
||||
|
||||
let states: StateTemplate[] = []
|
||||
let doneStates: DoneStateTemplate[] = []
|
||||
let wonStates: DoneStateTemplate[] = []
|
||||
let lostStates: DoneStateTemplate[] = []
|
||||
$: wonStates = doneStates.filter((x) => x._class === task.class.WonStateTemplate)
|
||||
$: lostStates = doneStates.filter((x) => x._class === task.class.LostStateTemplate)
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const client = getClient()
|
||||
|
||||
function sort <T extends Doc>(order: Ref<T>[], items: T[]): T[] {
|
||||
if (items.length === 0) {
|
||||
return []
|
||||
}
|
||||
|
||||
const itemMap = new Map(items.map(x => [x._id, x]))
|
||||
const x = order
|
||||
.map(id => itemMap.get(id))
|
||||
.filter((x): x is T => x !== undefined)
|
||||
|
||||
return x
|
||||
}
|
||||
|
||||
const statesQ = createQuery()
|
||||
$: statesQ.query(task.class.StateTemplate, { attachedTo: kanban._id }, result => { states = sort(kanban.states, result) })
|
||||
|
||||
const doneStatesQ = createQuery()
|
||||
$: doneStatesQ.query(task.class.DoneStateTemplate, { attachedTo: kanban._id }, (result) => { doneStates = sort(kanban.doneStates, result) })
|
||||
|
||||
let space: Space | undefined
|
||||
const spaceQ = createQuery()
|
||||
$: spaceQ.query(core.class.Space, { _id: kanban.space }, (result) => { space = result[0] })
|
||||
|
||||
async function onMove ({ detail: { stateID, position } }: { detail: { stateID: Ref<StateTemplate>, position: number } }) {
|
||||
client.updateDoc(kanban._class, kanban.space, kanban._id, {
|
||||
$move: {
|
||||
states: {
|
||||
$value: stateID,
|
||||
$position: position
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function onAdd () {
|
||||
const stateID = generateId<StateTemplate>()
|
||||
await client.addCollection(
|
||||
task.class.StateTemplate,
|
||||
kanban.space,
|
||||
kanban._id,
|
||||
kanban._class,
|
||||
'statesC',
|
||||
{
|
||||
title: 'New State',
|
||||
color: '#7C6FCD'
|
||||
},
|
||||
stateID
|
||||
)
|
||||
|
||||
await client.updateDoc(kanban._class, kanban.space, kanban._id, {
|
||||
$push: {
|
||||
states: stateID
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function onDelete ({ detail: { state } }: { detail: { state: State }}) {
|
||||
if (space === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
dispatch('delete', { state })
|
||||
}
|
||||
</script>
|
||||
|
||||
<StatesEditor {states} {wonStates} {lostStates} on:add={onAdd} on:delete={onDelete} on:move={onMove}/>
|
||||
@@ -0,0 +1,39 @@
|
||||
<!--
|
||||
// Copyright © 2020, 2021 Anticrm Platform Contributors.
|
||||
//
|
||||
// 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 { Ref } from '@anticrm/core'
|
||||
import { createQuery } from '@anticrm/presentation'
|
||||
import { DumbDropdown } from '@anticrm/ui'
|
||||
import type { DumbDropdownItem } from '@anticrm/ui/src/types'
|
||||
import type { KanbanTemplate, KanbanTemplateSpace } from '@anticrm/task'
|
||||
import task from '@anticrm/task'
|
||||
|
||||
export let folders: Ref<KanbanTemplateSpace>[]
|
||||
export let template: Ref<KanbanTemplate> | undefined = undefined
|
||||
|
||||
let templates: KanbanTemplate[] = []
|
||||
const templatesQ = createQuery()
|
||||
$: templatesQ.query(task.class.KanbanTemplate, { space: { $in: folders } }, (result) => { templates = result })
|
||||
|
||||
let items: DumbDropdownItem[] = []
|
||||
$: items = [
|
||||
{ id: '', label: 'No template' },
|
||||
...templates.map(x => ({ id: x._id, label: x.title }))
|
||||
]
|
||||
let selectedItem: string = ''
|
||||
$: template = selectedItem === '' ? undefined : selectedItem as Ref<KanbanTemplate>
|
||||
</script>
|
||||
|
||||
<DumbDropdown {items} bind:selected={selectedItem} title="Statuses" />
|
||||
@@ -0,0 +1,295 @@
|
||||
<!--
|
||||
// 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 type { AttachedDoc, Class, Doc, FindOptions, Ref } from '@anticrm/core'
|
||||
import core from '@anticrm/core'
|
||||
import { getResource } from '@anticrm/platform'
|
||||
import { createQuery, getClient } from '@anticrm/presentation'
|
||||
import type { Kanban, SpaceWithStates, State, Task } from '@anticrm/task'
|
||||
import task, { DoneState, LostState, WonState } from '@anticrm/task'
|
||||
import type { AnySvelteComponent } from '@anticrm/ui'
|
||||
import { AnyComponent, Loading, ScrollBox } from '@anticrm/ui'
|
||||
import KanbanPanel from './KanbanPanel.svelte'
|
||||
import KanbanPanelEmpty from './KanbanPanelEmpty.svelte'
|
||||
|
||||
type Item = Doc & { state: Ref<State>, doneState: Ref<DoneState> | null }
|
||||
|
||||
export let _class: Ref<Class<Item>>
|
||||
export let space: Ref<SpaceWithStates>
|
||||
export let open: AnyComponent
|
||||
export let options: FindOptions<Item> | undefined
|
||||
export let config: string[]
|
||||
|
||||
let kanban: Kanban
|
||||
let states: State[] = []
|
||||
let rawStates: State[] = []
|
||||
|
||||
let objects: (Item | undefined)[] = []
|
||||
let rawObjects: Item[] = []
|
||||
let kanbanStates: Ref<State>[] = []
|
||||
let kanbanDoneStates: Ref<DoneState>[] = []
|
||||
let wonState: WonState | undefined
|
||||
let lostState: LostState | undefined
|
||||
|
||||
const kanbanQuery = createQuery()
|
||||
$: kanbanQuery.query(task.class.Kanban, { attachedTo: space }, result => { kanban = result[0] })
|
||||
|
||||
$: kanbanStates = kanban?.states ?? []
|
||||
$: kanbanDoneStates = kanban?.doneStates ?? []
|
||||
|
||||
function sort (kanban: Kanban, states: State[]): State[] {
|
||||
if (kanban === undefined || states.length === 0) { return [] }
|
||||
const map = states.reduce((map, state) => { map.set(state._id, state); return map }, new Map<Ref<State>, State>())
|
||||
return kanban.states.map(id => map.get(id) as State)
|
||||
}
|
||||
|
||||
function sortObjects<T extends Doc> (kanban: Kanban, objects: T[]): (T | undefined)[] {
|
||||
if (kanban === undefined || objects.length === 0) { return [] }
|
||||
const map = objects.reduce((map, doc) => { map.set(doc._id, doc); return map }, new Map<Ref<T>, T>())
|
||||
return kanban.order
|
||||
.map(id => map.get(id as Ref<T>))
|
||||
}
|
||||
|
||||
const statesQuery = createQuery()
|
||||
$: if (kanbanStates.length > 0) statesQuery.query(task.class.State, { _id: { $in: kanbanStates } }, result => { rawStates = result })
|
||||
$: states = sort(kanban, rawStates)
|
||||
|
||||
const doneStatesQ = createQuery()
|
||||
$: if (kanbanDoneStates.length > 0) {
|
||||
doneStatesQ.query(task.class.DoneState, { _id: { $in: kanbanDoneStates } }, (result) => {
|
||||
wonState = result.find((x) => x._class === task.class.WonState)
|
||||
lostState = result.find((x) => x._class === task.class.LostState)
|
||||
})
|
||||
}
|
||||
|
||||
const query = createQuery()
|
||||
$: query.query(_class, { space, doneState: null }, result => { rawObjects = result }, options)
|
||||
|
||||
$: objects = sortObjects(kanban, rawObjects)
|
||||
|
||||
function dragover (ev: MouseEvent, object: Item) {
|
||||
if (dragCard !== object) {
|
||||
const dragover = objects.indexOf(object)
|
||||
objects = objects.filter(x => x !== dragCard)
|
||||
objects = [...objects.slice(0, dragover), dragCard, ...objects.slice(dragover)]
|
||||
}
|
||||
}
|
||||
|
||||
async function move (state: Ref<State>) {
|
||||
const id = dragCard._id
|
||||
|
||||
if (dragCardInitialState !== state) {
|
||||
if (client.getHierarchy().isDerived(_class, core.class.AttachedDoc)) {
|
||||
const adoc: AttachedDoc = dragCard as Doc as AttachedDoc
|
||||
// We need to update using updateCollection
|
||||
client.updateCollection(_class, space, id as Ref<Doc> as Ref<AttachedDoc>, adoc.attachedTo, adoc.attachedToClass, adoc.collection, { state })
|
||||
} else {
|
||||
client.updateDoc<Task>(_class, space, id as Ref<Task>, { state })
|
||||
}
|
||||
}
|
||||
|
||||
if (dragCardInitialPosition !== dragCardEndPosition) {
|
||||
client.updateDoc(task.class.Kanban, space, kanban._id, {
|
||||
$move: {
|
||||
order: {
|
||||
$value: id,
|
||||
$position: dragCardEndPosition
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const client = getClient()
|
||||
|
||||
let dragCard: Item
|
||||
let dragCardInitialPosition: number
|
||||
let dragCardEndPosition: number
|
||||
let dragCardInitialState: Ref<State>
|
||||
|
||||
async function cardPresenter (_class: Ref<Class<Doc>>): Promise<AnySvelteComponent> {
|
||||
const clazz = client.getHierarchy().getClass(_class)
|
||||
const presenterMixin = client.getHierarchy().as(clazz, task.mixin.KanbanCard)
|
||||
return await getResource(presenterMixin.card)
|
||||
}
|
||||
|
||||
const onDone = (state: DoneState) => async () => {
|
||||
if (client.getHierarchy().isDerived(_class, core.class.AttachedDoc)) {
|
||||
const adoc: AttachedDoc = dragCard as Doc as AttachedDoc
|
||||
await client.updateCollection<Doc, Task>(
|
||||
_class,
|
||||
space,
|
||||
adoc._id as Ref<Task>,
|
||||
adoc.attachedTo,
|
||||
adoc.attachedToClass,
|
||||
adoc.collection,
|
||||
{ doneState: state._id }
|
||||
)
|
||||
} else {
|
||||
await client.updateDoc(dragCard._class, dragCard.space, dragCard._id, {
|
||||
doneState: state._id
|
||||
})
|
||||
}
|
||||
|
||||
isDragging = false
|
||||
hoveredDoneState = undefined
|
||||
}
|
||||
|
||||
let isDragging = false
|
||||
let hoveredDoneState: Ref<DoneState> | undefined
|
||||
</script>
|
||||
|
||||
{#await cardPresenter(_class)}
|
||||
<Loading/>
|
||||
{:then presenter}
|
||||
<div class="flex-col kanban-container">
|
||||
<div class="scrollable">
|
||||
<ScrollBox>
|
||||
<div class="kanban-content">
|
||||
{#each states as state (state)}
|
||||
<KanbanPanel label={state.title} color={state.color} counter={4}
|
||||
on:dragover={(event) => {
|
||||
event.preventDefault()
|
||||
if (dragCard.state !== state._id) {
|
||||
dragCard.state = state._id
|
||||
}
|
||||
}}
|
||||
on:drop={() => {
|
||||
move(state._id)
|
||||
isDragging = false
|
||||
}}>
|
||||
<!-- <KanbanCardEmpty label={'Create new application'} /> -->
|
||||
{#each objects as object, j (object)}
|
||||
{#if object !== undefined && object.state === state._id}
|
||||
<div
|
||||
class="step-tb75"
|
||||
on:dragover|preventDefault={(ev) => {
|
||||
dragover(ev, object)
|
||||
dragCardEndPosition = j
|
||||
}}
|
||||
on:drop|preventDefault={() => {
|
||||
dragCardEndPosition = j
|
||||
isDragging = false
|
||||
}}
|
||||
>
|
||||
<svelte:component this={presenter} {object} draggable={true}
|
||||
on:dragstart={() => {
|
||||
dragCardInitialState = state._id
|
||||
dragCardInitialPosition = j
|
||||
dragCardEndPosition = j
|
||||
dragCard = object
|
||||
isDragging = true
|
||||
}}
|
||||
on:dragend={() => {
|
||||
isDragging = false
|
||||
}}/>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
</KanbanPanel>
|
||||
{/each}
|
||||
<KanbanPanelEmpty label={'Add new column'} />
|
||||
</div>
|
||||
</ScrollBox>
|
||||
</div>
|
||||
{#if isDragging && wonState !== undefined && lostState !== undefined}
|
||||
<div class="done-panel">
|
||||
<div
|
||||
class="done-item flex-center w-full"
|
||||
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"/>
|
||||
{wonState.title}
|
||||
</div>
|
||||
<div
|
||||
class="done-item flex-center w-full"
|
||||
class:hovered={hoveredDoneState === lostState._id}
|
||||
on:dragenter={() => {
|
||||
console.log('enter')
|
||||
hoveredDoneState = lostState?._id
|
||||
}}
|
||||
on:dragleave={() => {
|
||||
console.log('leave')
|
||||
hoveredDoneState = undefined
|
||||
}}
|
||||
on:dragover|preventDefault={() => {}}
|
||||
on:drop={onDone(lostState)}>
|
||||
<div class="done-icon lost"/>
|
||||
{lostState.title}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/await}
|
||||
|
||||
<style lang="scss">
|
||||
.kanban-container {
|
||||
height: 100%;
|
||||
}
|
||||
.kanban-content {
|
||||
display: flex;
|
||||
margin: 0 2.5rem;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.done-panel {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
justify-content: stretch;
|
||||
height: 4rem;
|
||||
border-top: 1px solid var(--theme-bg-accent-hover);
|
||||
}
|
||||
|
||||
.done-item {
|
||||
gap: 0.5rem;
|
||||
color: var(--theme-content-accent-color);
|
||||
|
||||
margin: 0.5rem 2.5rem;
|
||||
|
||||
&.hovered {
|
||||
background-color: var(--theme-bg-selection);
|
||||
border-radius: 12px;
|
||||
border: 1px dashed var(--theme-bg-accent-hover);
|
||||
}
|
||||
}
|
||||
|
||||
.done-icon {
|
||||
width: 0.5rem;
|
||||
height: 0.5rem;
|
||||
|
||||
border-radius: 50%;
|
||||
|
||||
&.won {
|
||||
background-color: #a5d179;
|
||||
}
|
||||
|
||||
&.lost {
|
||||
background-color: #f28469;
|
||||
}
|
||||
}
|
||||
.scrollable {
|
||||
height: 100%;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,16 @@
|
||||
<script lang="ts">
|
||||
const fill: string = 'var(--theme-caption-color)'
|
||||
</script>
|
||||
|
||||
<svg {fill} viewBox="0 0 6 16" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="1" cy="1" r="1"/>
|
||||
<circle cx="4.5" cy="1" r="1"/>
|
||||
<circle cx="1" cy="4.5" r="1"/>
|
||||
<circle cx="4.5" cy="4.5" r="1"/>
|
||||
<circle cx="1" cy="8" r="1"/>
|
||||
<circle cx="4.5" cy="8" r="1"/>
|
||||
<circle cx="1" cy="11.5" r="1"/>
|
||||
<circle cx="4.5" cy="11.5" r="1"/>
|
||||
<circle cx="1" cy="15" r="1"/>
|
||||
<circle cx="4.5" cy="15" r="1"/>
|
||||
</svg>
|
||||
@@ -0,0 +1,50 @@
|
||||
<!--
|
||||
// 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 { createEventDispatcher } from 'svelte'
|
||||
import PopupDialog from './PopupDialog.svelte'
|
||||
|
||||
export let colors: string[] = ['#A5D179', '#77C07B', '#60B96E', '#45AEA3', '#46CBDE', '#47BDF6',
|
||||
'#5AADF6', '#73A6CD', '#B977CB', '#7C6FCD', '#6F7BC5', '#F28469']
|
||||
export let columns: number = 5
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
</script>
|
||||
|
||||
<PopupDialog label={'CHOOSE A COLOR'}>
|
||||
<div class="color-grid" style="grid-template-columns: repeat({columns}, 1.5rem)">
|
||||
{#each colors as color}
|
||||
<div class="color" style="background-color: {color}" on:click={() => { dispatch('close', color) }} />
|
||||
{/each}
|
||||
</div>
|
||||
</PopupDialog>
|
||||
|
||||
<style lang="scss">
|
||||
.color-grid {
|
||||
display: grid;
|
||||
grid-auto-rows: 1.5rem;
|
||||
gap: 1rem;
|
||||
|
||||
.color {
|
||||
border: 1px solid transparent;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover { border-color: var(--theme-button-border-focused); }
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,132 @@
|
||||
<!--
|
||||
// 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 type { Class, 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 task from '@anticrm/task'
|
||||
import KanbanEditor from '../kanban/KanbanEditor.svelte'
|
||||
import { Icon, IconClose, Label, showPopup } from '@anticrm/ui'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import workbench from '@anticrm/workbench'
|
||||
|
||||
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 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: _id }, result => { spaceInstance = result.shift() })
|
||||
|
||||
async function deleteState ({ state }: { state: State }) {
|
||||
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
|
||||
|
||||
const objectsInThisState = await client.findAll(containingClass, { state: state._id })
|
||||
|
||||
if (objectsInThisState.length > 0) {
|
||||
showPopup(MessageBox, {
|
||||
label: 'Can\'t delete status',
|
||||
message: `There are ${objectsInThisState.length} objects in the given state. Move or delete them first.`
|
||||
})
|
||||
} else {
|
||||
showPopup(MessageBox, {
|
||||
label: 'Delete status',
|
||||
message: 'Do you want to delete this status?'
|
||||
}, 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex-col floatdialog-container">
|
||||
<div class="flex-between header">
|
||||
<div class="flex-grow flex-col">
|
||||
<div class="flex-row-center">
|
||||
<div class="icon"><Icon icon={task.icon.Status} size={'small'} /></div>
|
||||
<span class="overflow-label title">Manage application statuses within <Label label={spaceClassInstance?.label}/></span>
|
||||
</div>
|
||||
<div class="overflow-label subtitle">{spaceInstance?.name}</div>
|
||||
</div>
|
||||
<div class="tool" on:click={() => dispatch('close')}><IconClose size={'small'} /></div>
|
||||
</div>
|
||||
<div class="flex-grow flex-col content">
|
||||
{#if kanban !== undefined}
|
||||
<KanbanEditor {kanban} on:delete={(e) => deleteState(e.detail)} />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.floatdialog-container {
|
||||
margin: 2rem 1rem 1.25rem 0;
|
||||
height: calc(100% - 3.25rem);
|
||||
background: var(--theme-dialog-bg-spec);
|
||||
border-radius: 1.25rem;
|
||||
box-shadow: var(--theme-dialog-shadow);
|
||||
backdrop-filter: blur(15px);
|
||||
|
||||
.header {
|
||||
padding: 0 2rem 0 2.5rem;
|
||||
height: 4.5rem;
|
||||
min-height: 4.5rem;
|
||||
|
||||
.icon {
|
||||
margin-right: .5rem;
|
||||
opacity: .6;
|
||||
}
|
||||
.title {
|
||||
font-weight: 500;
|
||||
font-size: 1rem;
|
||||
color: var(--theme-caption-color);
|
||||
}
|
||||
.subtitle {
|
||||
font-size: .75rem;
|
||||
color: var(--theme-content-dark-color);
|
||||
}
|
||||
.tool {
|
||||
margin-left: 2.5rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
.content {
|
||||
overflow: auto;
|
||||
margin: 1rem 2.5rem 1rem 2.5rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,66 @@
|
||||
<!--
|
||||
// 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 type { IntlString } from '@anticrm/platform'
|
||||
import { Label } from '@anticrm/ui'
|
||||
|
||||
export let label: IntlString
|
||||
</script>
|
||||
|
||||
<div class="card-container">
|
||||
<div class="card-bg" />
|
||||
<div class="overflow-label label"><Label {label} /></div>
|
||||
<div class="content"><slot /></div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.card-container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 32px 1.5rem 1.25rem;
|
||||
border-radius: .75rem;
|
||||
|
||||
.label {
|
||||
flex-shrink: 0;
|
||||
margin-bottom: 1.25rem;
|
||||
text-transform: uppercase;
|
||||
font-weight: 600;
|
||||
font-size: .75rem;
|
||||
color: var(--theme-content-trans-color);
|
||||
}
|
||||
|
||||
.content {
|
||||
flex-shrink: 0;
|
||||
flex-grow: 1;
|
||||
height: fit-content;
|
||||
}
|
||||
|
||||
.card-bg {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background-color: var(--theme-card-bg);
|
||||
border-radius: .75rem;
|
||||
backdrop-filter: blur(24px);
|
||||
box-shadow: var(--theme-card-shadow);
|
||||
z-index: -1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,53 @@
|
||||
<!--
|
||||
// Copyright © 2020, 2021 Anticrm Platform Contributors.
|
||||
// Copyright © 2021 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
|
||||
<script lang="ts">
|
||||
import { Ref } from '@anticrm/core'
|
||||
import task, { State } from '@anticrm/task'
|
||||
import { createQuery } from '@anticrm/presentation'
|
||||
import { showPopup } from '@anticrm/ui'
|
||||
import StatePresenter from './StatePresenter.svelte'
|
||||
import StatesPopup from './StatesPopup.svelte'
|
||||
|
||||
export let value: Ref<State>
|
||||
export let onChange: (value: any) => void
|
||||
let state: State
|
||||
let container: HTMLElement
|
||||
let opened: boolean = false
|
||||
|
||||
const query = createQuery()
|
||||
$: query.query(task.class.State, { _id: value }, (res) => {
|
||||
state = res[0]
|
||||
}, { limit: 1 })
|
||||
</script>
|
||||
|
||||
{#if state}
|
||||
<div class="flex-row-center cursor-pointer" bind:this={container}
|
||||
on:click|preventDefault={() => {
|
||||
if (!opened) {
|
||||
opened = true
|
||||
showPopup(StatesPopup, { space: state.space }, container, (result) => {
|
||||
if (result) {
|
||||
value = result._id
|
||||
onChange(value)
|
||||
}
|
||||
opened = false
|
||||
})
|
||||
}
|
||||
}} >
|
||||
<StatePresenter value={state} />
|
||||
</div>
|
||||
{/if}
|
||||
@@ -0,0 +1,41 @@
|
||||
<!--
|
||||
// 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 type { State } from '@anticrm/core'
|
||||
|
||||
export let value: State
|
||||
|
||||
</script>
|
||||
|
||||
<div class="overflow-label state-container" style="background-color: {value.color}">
|
||||
{value.title}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.state-container {
|
||||
padding: .25rem .5rem;
|
||||
width: 6.25rem;
|
||||
max-width: 6.25rem;
|
||||
text-transform: uppercase;
|
||||
text-align: center;
|
||||
letter-spacing: .5px;
|
||||
font-size: .625rem;
|
||||
color: #fff;
|
||||
border: 1px solid rgba(0, 0, 0, .1);
|
||||
border-radius: .25rem;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,196 @@
|
||||
<!--
|
||||
// Copyright © 2020, 2021 Anticrm Platform Contributors.
|
||||
// Copyright © 2021 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Ref } from '@anticrm/core'
|
||||
import { AttributeEditor, getClient } from '@anticrm/presentation'
|
||||
import type { DoneState, State } from '@anticrm/task'
|
||||
import { CircleButton, IconAdd, IconMoreH, Label, showPopup } from '@anticrm/ui'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import ColorsPopup from './ColorsPopup.svelte'
|
||||
import Circles from './Circles.svelte'
|
||||
import StatusesPopup from './StatusesPopup.svelte'
|
||||
|
||||
export let states: State[] = []
|
||||
export let wonStates: DoneState[] = []
|
||||
export let lostStates: DoneState[] = []
|
||||
|
||||
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: string | undefined): Promise<void> => {
|
||||
if (color === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
await client.updateDoc(state._class, state.space, state._id, { color })
|
||||
}
|
||||
|
||||
async function onAdd () {
|
||||
dispatch('add')
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="root w-full">
|
||||
<div class="flex-col w-full">
|
||||
<div class="flex-no-shrink flex-between states-header">
|
||||
<Label label={'ACTIVE STATUSES'} />
|
||||
<div on:click={onAdd}><CircleButton icon={IconAdd} size={'medium'} /></div>
|
||||
</div>
|
||||
<div class="content">
|
||||
{#each states as state, i}
|
||||
{#if state}
|
||||
<div bind:this={elements[i]} class="flex-between states" 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"><Circles /></div>
|
||||
<div class="color" style="background-color: {state.color}"
|
||||
on:click={() => {
|
||||
showPopup(ColorsPopup, {}, elements[i], onColorChange(state))
|
||||
}}
|
||||
/>
|
||||
<div class="flex-grow caption-color"><AttributeEditor maxWidth="20rem" _class={state._class} object={state} key="title"/></div>
|
||||
<div class="tool hover-trans"
|
||||
on:click={(ev) => {
|
||||
showPopup(StatusesPopup, { onDelete: () => dispatch('delete', { state }) }, ev.target, () => {})
|
||||
}}
|
||||
>
|
||||
<IconMoreH size={'medium'} />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-col w-full">
|
||||
<div class="flex-no-shrink states-header">
|
||||
<Label label={'DONE STATUS / WON'} />
|
||||
</div>
|
||||
<div class="content">
|
||||
{#each wonStates as state, i}
|
||||
{#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="20rem" _class={state._class} object={state} key="title"/></div>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-col w-full">
|
||||
<div class="flex-no-shrink states-header">
|
||||
<Label label={'DONE STATUS / LOST'} />
|
||||
</div>
|
||||
<div class="content">
|
||||
{#each lostStates as state, i}
|
||||
{#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="20rem" _class={state._class} object={state} key="title"/></div>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.root {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-rows: minmax(1px, max-content) min-content min-content;
|
||||
gap: 2rem;
|
||||
height: 100%;
|
||||
}
|
||||
.states {
|
||||
padding: .625rem 1rem;
|
||||
color: #fff;
|
||||
background-color: rgba(67, 67, 72, .3);
|
||||
border: 1px solid var(--theme-bg-accent-color);
|
||||
border-radius: .75rem;
|
||||
user-select: none;
|
||||
|
||||
&-header {
|
||||
margin-bottom: 1rem;
|
||||
font-weight: 600;
|
||||
font-size: .75rem;
|
||||
color: var(--theme-content-trans-color);
|
||||
}
|
||||
|
||||
.bar {
|
||||
margin-right: .375rem;
|
||||
width: .375rem;
|
||||
height: 1rem;
|
||||
opacity: .4;
|
||||
cursor: grabbing;
|
||||
}
|
||||
.color {
|
||||
margin-right: .75rem;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
border-radius: .25rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
.tool { margin-left: 1rem; }
|
||||
}
|
||||
.states + .states { margin-top: .5rem; }
|
||||
|
||||
.content {
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,66 @@
|
||||
<!--
|
||||
// Copyright © 2020, 2021 Anticrm Platform Contributors.
|
||||
// Copyright © 2021 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
|
||||
<script lang="ts">
|
||||
import { Ref } from '@anticrm/core'
|
||||
import { createQuery } from '@anticrm/presentation'
|
||||
import task, { SpaceWithStates, State } from '@anticrm/task'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
|
||||
export let space: Ref<SpaceWithStates>
|
||||
let states: State[] = []
|
||||
const dispatch = createEventDispatcher()
|
||||
const statesQuery = createQuery()
|
||||
statesQuery.query(task.class.State, { space }, (res) => { states = res })
|
||||
|
||||
</script>
|
||||
|
||||
<div class="flex-col statuses-container">
|
||||
{#each states as state}
|
||||
<div class="flex-row-center state" on:click={() => { dispatch('close', state) }}>
|
||||
<div class="color" style="background-color: {state.color}"></div>
|
||||
{state.title}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.statuses-container {
|
||||
padding: .5rem;
|
||||
max-height: 100%;
|
||||
min-width: 10rem;
|
||||
color: var(--theme-caption-color);
|
||||
background-color: var(--theme-button-bg-focused);
|
||||
border: 1px solid var(--theme-button-border-enabled);
|
||||
border-radius: .75rem;
|
||||
user-select: none;
|
||||
filter: drop-shadow(0 1.5rem 4rem rgba(0, 0, 0, .35));
|
||||
|
||||
.state {
|
||||
padding: .5rem;
|
||||
border-radius: .5rem;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover { background-color: var(--theme-button-bg-hovered); }
|
||||
.color {
|
||||
margin-right: .75rem;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
border-radius: .25rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,59 @@
|
||||
<!--
|
||||
// Copyright © 2020 Anticrm Platform Contributors.
|
||||
//
|
||||
// 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 { createEventDispatcher } from 'svelte'
|
||||
import { Label, IconDelete as Delete } from '@anticrm/ui'
|
||||
import task from '../../plugin'
|
||||
|
||||
export let onDelete: () => void
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
</script>
|
||||
|
||||
<div class="flex-col popup">
|
||||
<div class="flex-row-center red-color menu-item" on:click={() => { dispatch('close'); onDelete() }}>
|
||||
<div class="icon">
|
||||
<Delete size={'medium'} />
|
||||
</div>
|
||||
<div class="flex-grow"><Label label={task.string.Delete} /></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.popup {
|
||||
padding: .5rem;
|
||||
min-width: 12rem;
|
||||
background-color: var(--theme-button-bg-focused);
|
||||
border: 1px solid var(--theme-button-border-enabled);
|
||||
border-radius: .75rem;
|
||||
box-shadow: 0 .75rem 1.25rem rgba(0, 0, 0, .2);
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: .5rem;
|
||||
border-radius: .5rem;
|
||||
cursor: pointer;
|
||||
|
||||
.icon {
|
||||
margin-right: .75rem;
|
||||
transform-origin: center center;
|
||||
transform: scale(.75);
|
||||
}
|
||||
&:hover { background-color: var(--theme-button-bg-hovered); }
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user