Tasks as Attached Documents (#539)

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
Andrey Sobolev
2021-12-15 10:04:43 +01:00
committed by GitHub
parent 78f4916b6d
commit e958388e4c
113 changed files with 1238 additions and 898 deletions
@@ -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>