Add Task Kanban and States

Add Task Kanban and States

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
Andrey Sobolev 2021-12-02 20:12:16 +07:00
parent 32d559a07b
commit 6a31d8cf06
No known key found for this signature in database
GPG Key ID: BD80F68D68D8F7F2
16 changed files with 397 additions and 146 deletions

View File

@ -15,25 +15,26 @@
import type { Employee } from '@anticrm/contact' import type { Employee } from '@anticrm/contact'
import contact from '@anticrm/contact' import contact from '@anticrm/contact'
import type { Doc, Domain, FindOptions, Ref } from '@anticrm/core' import type { Doc, DocWithState, Domain, FindOptions, Ref } from '@anticrm/core'
import { Builder, Model, Prop, TypeString, UX } from '@anticrm/model' import { Builder, Model, Prop, TypeString, UX } from '@anticrm/model'
import chunter from '@anticrm/model-chunter' import chunter from '@anticrm/model-chunter'
import core, { TDoc, TSpace } from '@anticrm/model-core' import core, { TDoc, TSpaceWithStates } from '@anticrm/model-core'
import view from '@anticrm/model-view' import view from '@anticrm/model-view'
import workbench from '@anticrm/model-workbench' import workbench from '@anticrm/model-workbench'
import type { IntlString } from '@anticrm/platform' import type { IntlString } from '@anticrm/platform'
import type { Project, Task } from '@anticrm/task' import type { Project, Task } from '@anticrm/task'
import { createProjectKanban } from '@anticrm/task-resources'
import task from './plugin' import task from './plugin'
@Model(task.class.Project, core.class.Space) @Model(task.class.Project, core.class.SpaceWithStates)
@UX('Project' as IntlString, task.icon.Task) @UX('Project' as IntlString, task.icon.Task)
export class TProject extends TSpace implements Project {} export class TProject extends TSpaceWithStates implements Project {}
@Model(task.class.Task, core.class.Doc, 'task' as Domain) @Model(task.class.Task, core.class.Doc, 'task' as Domain, [core.interface.DocWithState])
@UX('Task' as IntlString, task.icon.Task, 'TASK' as IntlString) @UX('Task' as IntlString, task.icon.Task, 'TASK' as IntlString)
export class TTask extends TDoc implements Task { export class TTask extends TDoc implements Task {
@Prop(TypeString(), 'No.' as IntlString) declare number: DocWithState['number']
number!: number declare state: DocWithState['state']
@Prop(TypeString(), 'Name' as IntlString) @Prop(TypeString(), 'Name' as IntlString)
name!: string name!: string
@ -47,6 +48,9 @@ export class TTask extends TDoc implements Task {
@Prop(TypeString(), 'Comments' as IntlString) @Prop(TypeString(), 'Comments' as IntlString)
comments!: number comments!: number
@Prop(TypeString(), 'Attachments' as IntlString)
attachments!: number
@Prop(TypeString(), 'Labels' as IntlString) @Prop(TypeString(), 'Labels' as IntlString)
labels!: string labels!: string
} }
@ -104,6 +108,29 @@ export function createModel (builder: Builder): void {
editor: task.component.EditTask editor: task.component.EditTask
}) })
builder.createDoc(view.class.Sequence, view.space.Sequence, {
attachedTo: task.class.Task,
sequence: 0
})
builder.createDoc(view.class.Viewlet, core.space.Model, {
attachTo: task.class.Task,
descriptor: view.viewlet.Kanban,
open: task.component.EditTask,
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
options: {
lookup: {
assignee: contact.class.EmployeeAccount,
state: core.class.State
}
} as FindOptions<Doc>, // TODO: fix
config: ['$lookup.attachedTo', '$lookup.state']
})
builder.mixin(task.class.Task, core.class.Class, view.mixin.KanbanCard, {
card: task.component.KanbanCard
})
builder.createDoc(task.class.Project, core.space.Model, { builder.createDoc(task.class.Project, core.space.Model, {
name: 'public', name: 'public',
description: 'Public tasks', description: 'Public tasks',
@ -111,8 +138,8 @@ export function createModel (builder: Builder): void {
members: [] members: []
}, task.space.TasksPublic) }, task.space.TasksPublic)
builder.createDoc(view.class.Sequence, view.space.Sequence, { createProjectKanban(task.space.TasksPublic, async (_class, space, data, id) => {
attachedTo: task.class.Task, builder.createDoc(_class, space, data, id)
sequence: 0 return await Promise.resolve()
}) }).catch((err) => console.error(err))
} }

View File

@ -30,7 +30,8 @@ export default mergeIds(taskId, task, {
CreateProject: '' as AnyComponent, CreateProject: '' as AnyComponent,
CreateTask: '' as AnyComponent, CreateTask: '' as AnyComponent,
EditTask: '' as AnyComponent, EditTask: '' as AnyComponent,
TaskPresenter: '' as AnyComponent TaskPresenter: '' as AnyComponent,
KanbanCard: '' as AnyComponent
}, },
string: { string: {
Task: '' as IntlString, Task: '' as IntlString,

View File

@ -15,6 +15,7 @@
"TaskDescription": "Description", "TaskDescription": "Description",
"UploadDropFilesHere": "Upload or drop files here", "UploadDropFilesHere": "Upload or drop files here",
"NoAttachmentsForTask": "There are no attachments for this task.", "NoAttachmentsForTask": "There are no attachments for this task.",
"AssigneeRequired": "Assignee is required" "AssigneeRequired": "Assignee is required",
"More": "Options"
} }
} }

View File

@ -41,6 +41,7 @@
"@anticrm/panel": "~0.6.0", "@anticrm/panel": "~0.6.0",
"@anticrm/view": "~0.6.0", "@anticrm/view": "~0.6.0",
"@anticrm/view-resources": "~0.6.0", "@anticrm/view-resources": "~0.6.0",
"@anticrm/login": "~0.6.1" "@anticrm/login": "~0.6.1",
"@anticrm/chunter-resources": "~0.6.0"
} }
} }

View File

@ -12,7 +12,6 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
--> -->
<script lang="ts"> <script lang="ts">
import type { Attachment } from '@anticrm/chunter' import type { Attachment } from '@anticrm/chunter'
import chunter from '@anticrm/chunter' import chunter from '@anticrm/chunter'
@ -32,7 +31,9 @@
let attachments: Attachment[] = [] let attachments: Attachment[] = []
const query = createQuery() const query = createQuery()
$: query.query(chunter.class.Attachment, { attachedTo: objectId }, result => { attachments = result }) $: query.query(chunter.class.Attachment, { attachedTo: objectId }, (result) => {
attachments = result
})
let inputFile: HTMLInputElement let inputFile: HTMLInputElement
let loading = 0 let loading = 0
@ -84,17 +85,37 @@
<div class="flex-row-center"> <div class="flex-row-center">
<span class="title">Attachments</span> <span class="title">Attachments</span>
{#if loading} {#if loading}
<Spinner/> <Spinner />
{:else} {:else}
<CircleButton icon={IconAdd} size={'small'} on:click={ () => { inputFile.click() } } /> <CircleButton
icon={IconAdd}
size={'small'}
on:click={() => {
inputFile.click()
}}
/>
{/if} {/if}
<input bind:this={inputFile} multiple type="file" name="file" id="file" style="display: none" on:change={fileSelected}/> <input
bind:this={inputFile}
multiple
type="file"
name="file"
id="file"
style="display: none"
on:change={fileSelected}
/>
</div> </div>
{#if attachments.length === 0 && !loading} {#if attachments.length === 0 && !loading}
<div class="flex-col-center mt-5 zone-container" class:solid={dragover} <div
on:dragover|preventDefault={ () => { dragover = true } } class="flex-col-center mt-5 zone-container"
on:dragleave={ () => { dragover = false } } class:solid={dragover}
on:dragover|preventDefault={() => {
dragover = true
}}
on:dragleave={() => {
dragover = false
}}
on:drop|preventDefault|stopPropagation={fileDrop} on:drop|preventDefault|stopPropagation={fileDrop}
> >
<UploadDuo size={'large'} /> <UploadDuo size={'large'} />
@ -109,8 +130,8 @@
<Table <Table
_class={chunter.class.Attachment} _class={chunter.class.Attachment}
config={['', 'lastModified']} config={['', 'lastModified']}
options={ {} } options={{}}
query={ { attachedTo: objectId } } query={{ attachedTo: objectId }}
/> />
{/if} {/if}
</div> </div>
@ -121,7 +142,7 @@
flex-direction: column; flex-direction: column;
.title { .title {
margin-right: .75rem; margin-right: 0.75rem;
font-weight: 500; font-weight: 500;
font-size: 1.25rem; font-size: 1.25rem;
color: var(--theme-caption-color); color: var(--theme-caption-color);
@ -131,8 +152,8 @@
.zone-container { .zone-container {
padding: 1rem; padding: 1rem;
color: var(--theme-caption-color); color: var(--theme-caption-color);
background: rgba(255, 255, 255, .03); background: rgba(255, 255, 255, 0.03);
border: 1px dashed rgba(255, 255, 255, .16); border: 1px dashed rgba(255, 255, 255, 0.16);
border-radius: .75rem; border-radius: 0.75rem;
} }
</style> </style>

View File

@ -12,15 +12,14 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
--> -->
<script lang="ts"> <script lang="ts">
import { createEventDispatcher } from 'svelte' import core, { generateId, Ref } from '@anticrm/core'
import { IconFolder, EditBox, ToggleWithLabel, Grid } from '@anticrm/ui'
import { getClient, SpaceCreateCard } from '@anticrm/presentation' import { getClient, SpaceCreateCard } from '@anticrm/presentation'
import { Project } from '@anticrm/task'
import { EditBox, Grid, IconFolder, ToggleWithLabel } from '@anticrm/ui'
import { createEventDispatcher } from 'svelte'
import task from '../plugin' import task from '../plugin'
import core from '@anticrm/core' import { createProjectKanban } from '../utils'
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
@ -33,12 +32,22 @@
const client = getClient() const client = getClient()
function createProject () { async function createProject (): Promise<void> {
client.createDoc(task.class.Project, core.space.Model, { const id: Ref<Project> = generateId()
await client.createDoc(
task.class.Project,
core.space.Model,
{
name, name,
description, description,
private: false, private: false,
members: [] members: []
},
id
)
await createProjectKanban(id, async (_class, space, data, id) => {
await client.createDoc(_class, space, data, id)
}) })
} }
</script> </script>
@ -47,10 +56,12 @@
label={task.string.CreateProject} label={task.string.CreateProject}
okAction={createProject} okAction={createProject}
canSave={name.length > 0} canSave={name.length > 0}
on:close={() => { dispatch('close') }} on:close={() => {
dispatch('close')
}}
> >
<Grid column={1} rowGap={1.5}> <Grid column={1} rowGap={1.5}>
<EditBox label={task.string.ProjectName} icon={IconFolder} bind:value={name} placeholder={'Project name'} focus/> <EditBox label={task.string.ProjectName} icon={IconFolder} bind:value={name} placeholder={'Project name'} focus />
<ToggleWithLabel label={task.string.MakePrivate} description={task.string.MakePrivateDescription}/> <ToggleWithLabel label={task.string.MakePrivate} description={task.string.MakePrivateDescription} />
</Grid> </Grid>
</SpaceCreateCard> </SpaceCreateCard>

View File

@ -12,7 +12,6 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
--> -->
<script lang="ts"> <script lang="ts">
import contact, { Employee, EmployeeAccount } from '@anticrm/contact' import contact, { Employee, EmployeeAccount } from '@anticrm/contact'
import type { Data, Ref, Space } from '@anticrm/core' import type { Data, Ref, Space } from '@anticrm/core'
@ -53,9 +52,15 @@
throw new Error('sequence object not found') throw new Error('sequence object not found')
} }
const incResult = await client.updateDoc(view.class.Sequence, view.space.Sequence, sequence._id, { const incResult = await client.updateDoc(
view.class.Sequence,
view.space.Sequence,
sequence._id,
{
$inc: { sequence: 1 } $inc: { sequence: 1 }
}, true) },
true
)
const value: Data<Task> = { const value: Data<Task> = {
name: object.name, name: object.name,
@ -71,32 +76,50 @@
<!-- <DialogHeader {space} {object} {newValue} {resume} create={true} on:save={createCandidate}/> --> <!-- <DialogHeader {space} {object} {newValue} {resume} create={true} on:save={createCandidate}/> -->
<Card label={task.string.CreateTask} <Card
label={task.string.CreateTask}
okAction={createTask} okAction={createTask}
canSave={object.name.length > 0 && assignee !== undefined} canSave={object.name.length > 0 && assignee !== undefined}
spaceClass={task.class.Project} spaceClass={task.class.Project}
spaceLabel={task.string.ProjectName} spaceLabel={task.string.ProjectName}
spacePlaceholder={task.string.SelectProject} spacePlaceholder={task.string.SelectProject}
bind:space={_space} bind:space={_space}
on:close={() => { dispatch('close') }}> on:close={() => {
dispatch('close')
}}
>
<StatusControl slot="error" {status} /> <StatusControl slot="error" {status} />
<Grid column={1} rowGap={1.5}> <Grid column={1} rowGap={1.5}>
<EditBox label={task.string.TaskName} bind:value={object.name} icon={task.icon.Task} placeholder="The boring task" maxWidth="39rem" focus/> <EditBox
<UserBox _class={contact.class.EmployeeAccount} title='Assignee *' caption='Assign this task' bind:value={assignee} /> label={task.string.TaskName}
bind:value={object.name}
icon={task.icon.Task}
placeholder="The boring task"
maxWidth="39rem"
focus
/>
<UserBox
_class={contact.class.EmployeeAccount}
title="Assignee *"
caption="Assign this task"
bind:value={assignee}
/>
</Grid> </Grid>
</Card> </Card>
<style lang="scss"> <style lang="scss">
.channels { .channels {
margin-top: 1.25rem; margin-top: 1.25rem;
span { margin-left: .5rem; } span {
margin-left: 0.5rem;
}
} }
.locations { .locations {
span { span {
margin-bottom: .125rem; margin-bottom: 0.125rem;
font-weight: 500; font-weight: 500;
font-size: .75rem; font-size: 0.75rem;
color: var(--theme-content-accent-color); color: var(--theme-content-accent-color);
} }
@ -104,7 +127,7 @@
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
margin-top: .75rem; margin-top: 0.75rem;
color: var(--theme-caption-color); color: var(--theme-caption-color);
} }
} }
@ -117,12 +140,14 @@
.resume { .resume {
margin-top: 1rem; margin-top: 1rem;
padding: .75rem; padding: 0.75rem;
background: rgba(255, 255, 255, .05); background: rgba(255, 255, 255, 0.05);
border: 1px dashed rgba(255, 255, 255, .2); border: 1px dashed rgba(255, 255, 255, 0.2);
border-radius: .5rem; border-radius: 0.5rem;
backdrop-filter: blur(10px); backdrop-filter: blur(10px);
&.solid { border-style: solid; } &.solid {
border-style: solid;
}
} }
// .resume a { // .resume a {
// font-size: .75rem; // font-size: .75rem;

View File

@ -12,13 +12,12 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
--> -->
<script lang="ts"> <script lang="ts">
import type { Ref } from '@anticrm/core' import type { Ref } from '@anticrm/core'
import { Panel } from '@anticrm/panel' import { Panel } from '@anticrm/panel'
import { createQuery, getClient } from '@anticrm/presentation' import { createQuery, getClient } from '@anticrm/presentation'
import type { Task } from '@anticrm/task' import type { Task } from '@anticrm/task'
import { EditBox, Grid } from '@anticrm/ui' import { EditBox, Grid } from '@anticrm/ui'
import view from '@anticrm/view' import view from '@anticrm/view'
import { createEventDispatcher } from 'svelte' import { createEventDispatcher } from 'svelte'
import task from '../plugin' import task from '../plugin'
@ -29,7 +28,9 @@ import { EditBox, Grid } from '@anticrm/ui'
let object: Task let object: Task
const query = createQuery() const query = createQuery()
$: query.query(task.class.Task, { _id }, result => { object = result[0] }) $: query.query(task.class.Task, { _id }, (result) => {
object = result[0]
})
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
const client = getClient() const client = getClient()
@ -40,19 +41,40 @@ import { EditBox, Grid } from '@anticrm/ui'
</script> </script>
{#if object !== undefined} {#if object !== undefined}
<Panel icon={view.icon.Table} title={object.name} {object} on:close={() => { dispatch('close') }}> <Panel
icon={view.icon.Table}
title={object.name}
{object}
on:close={() => {
dispatch('close')
}}
>
<TaskHeader {object} slot="subtitle" /> <TaskHeader {object} slot="subtitle" />
<Grid column={1} rowGap={1.5}> <Grid column={1} rowGap={1.5}>
<EditBox label={task.string.TaskName} bind:value={object.name} icon={task.icon.Task} placeholder="The boring task" maxWidth="39rem" focus on:change={(evt) => change('name', object.name)}/> <EditBox
<EditBox label={task.string.TaskDescription} bind:value={object.description} icon={task.icon.Task} placeholder="Description" maxWidth="39rem" on:change={(evt) => change('description', object.description)}/> label={task.string.TaskName}
bind:value={object.name}
icon={task.icon.Task}
placeholder="The boring task"
maxWidth="39rem"
focus
on:change={(evt) => change('name', object.name)}
/>
<EditBox
label={task.string.TaskDescription}
bind:value={object.description}
icon={task.icon.Task}
placeholder="Description"
maxWidth="39rem"
on:change={(evt) => change('description', object.description)}
/>
</Grid> </Grid>
<div class="mt-14"> <div class="mt-14">
<Attachments objectId={object._id} _class={object._class} space={object.space} /> <Attachments objectId={object._id} _class={object._class} space={object.space} />
</div> </div>
</Panel> </Panel>
{/if} {/if}
<style lang="scss"> <style lang="scss">

View File

@ -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 { AttachmentsPresenter, CommentsPresenter } from '@anticrm/chunter-resources'
import { formatName } from '@anticrm/contact'
import type { WithLookup } from '@anticrm/core'
import { Avatar } from '@anticrm/presentation'
import type { Task } 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 draggable: boolean
const showMenu = (ev?: Event): void => {
showPopup(ContextMenu, { object }, (ev as MouseEvent).target as HTMLElement)
}
</script>
<div class="card-container" {draggable} class:draggable on:dragstart on:dragend>
<div class="content">
<div class="flex-row-center">
<div class="flex-col ml-2">
<div class="sm-tool-icon step-lr75">
<TaskPresenter value={object} />
</div>
<div class="fs-title">{object.name}</div>
<div class="small-text">{object.description}</div>
</div>
</div>
</div>
<div class="flex-between">
{#if object.$lookup?.assignee}
<div class="flex-center safari-gap-1">
<Avatar avatar={object.$lookup?.assignee?.avatar} size={'x-small'} />
<Label label={formatName(object.$lookup?.assignee?.name)} />
</div>
{/if}
<div class="flex-row-reverse">
<ActionIcon
label={task.string.More}
action={(evt) => {
showMenu(evt)
}}
icon={IconMoreH}
size={'small'}
/>
{#if (object.comments ?? 0) > 0}
<div class="step-lr75"><CommentsPresenter value={object} /></div>
{/if}
{#if (object.attachments ?? 0) > 0}
<div class="step-lr75"><AttachmentsPresenter value={object} /></div>
{/if}
</div>
</div>
</div>
<style lang="scss">
.card-container {
display: flex;
flex-direction: column;
padding: 1rem 1.25rem;
background-color: rgba(222, 222, 240, 0.06);
border-radius: 0.75rem;
user-select: none;
backdrop-filter: blur(10px);
.content {
display: flex;
justify-content: space-between;
margin-bottom: 1rem;
}
&.draggable {
cursor: grab;
}
}
</style>

View File

@ -13,10 +13,9 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
--> -->
<script lang="ts"> <script lang="ts">
import contact from '@anticrm/contact' import contact from '@anticrm/contact'
import { getClient, UserBox } from '@anticrm/presentation' import { AttributeBarEditor, getClient, UserBox } from '@anticrm/presentation'
import { Task } from '@anticrm/task' import { Task } from '@anticrm/task'
import task from '../plugin' import task from '../plugin'
@ -29,13 +28,19 @@
</script> </script>
<div class="flex-between header"> <div class="flex-between header">
<UserBox _class={contact.class.Employee} title={task.string.TaskAssignee} caption='Assignee' bind:value={object.assignee} on:change={change} /> <UserBox
<!-- <AttributeBarEditor key={'state'} {object} showHeader={false} /> --> _class={contact.class.Employee}
title={task.string.TaskAssignee}
caption="Assignee"
bind:value={object.assignee}
on:change={change}
/>
<AttributeBarEditor key={'state'} {object} showHeader={false} />
</div> </div>
<style lang="scss"> <style lang="scss">
.header { .header {
width: 100%; width: 100%;
padding: 0 .5rem; padding: 0 0.5rem;
} }
</style> </style>

View File

@ -13,27 +13,24 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
--> -->
<script lang="ts"> <script lang="ts">
import type { Task } from '@anticrm/task'
import { closeTooltip, Icon, showPopup } from '@anticrm/ui'
import EditTask from './EditTask.svelte'
import { getClient } from '@anticrm/presentation'
import task from '../plugin'
import type { Task } from '@anticrm/task' export let value: 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 const client = getClient()
const shortLabel = client.getHierarchy().getClass(value._class).shortLabel
const client = getClient() function show () {
const shortLabel = client.getHierarchy().getClass(value._class).shortLabel
function show () {
closeTooltip() closeTooltip()
showPopup(EditTask, { _id: value._id }, 'full') showPopup(EditTask, { _id: value._id }, 'full')
} }
</script> </script>
<div class="sm-tool-icon" on:click={show}> <div class="sm-tool-icon" on:click={show}>
<span class="icon"><Icon icon={task.icon.Task} size={'small'}/></span>{shortLabel}-{value.number} <span class="icon"><Icon icon={task.icon.Task} size={'small'} /></span>{shortLabel}-{value.number}
</div> </div>

View File

@ -13,16 +13,20 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
--> -->
<script lang="ts"> <script lang="ts">
export let size: 'small' | 'medium' | 'large' export let size: 'small' | 'medium' | 'large'
const fill: string = 'currentColor' const fill: string = 'currentColor'
</script> </script>
<svg class="svg-{size}" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> <svg class="svg-{size}" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path fill="var(--duotone-color)" d="M18,6.4c-0.1,0-0.2,0-0.3,0c-0.1,0-0.1,0-0.1,0c-0.1,0-0.1,0-0.1,0c0,0,0,0-0.1-0.1c0,0,0,0-0.1-0.1 c0-0.1-0.1-0.2-0.2-0.4c-0.9-1.9-2.8-3.3-5.1-3.3c-2.3,0-4.2,1.4-5.1,3.3C6.8,5.9,6.7,6,6.7,6.1c0,0.1-0.1,0.1-0.1,0.1 C6.6,6.3,6.6,6.3,6.5,6.3c0,0,0,0-0.1,0c0,0,0,0-0.1,0c-0.1,0-0.2,0-0.3,0C4,6.4,2.4,8,2.4,10S4,13.6,6,13.6h6h6 c2,0,3.6-1.6,3.6-3.6S20,6.4,18,6.4z"/> <path
fill="var(--duotone-color)"
d="M18,6.4c-0.1,0-0.2,0-0.3,0c-0.1,0-0.1,0-0.1,0c-0.1,0-0.1,0-0.1,0c0,0,0,0-0.1-0.1c0,0,0,0-0.1-0.1 c0-0.1-0.1-0.2-0.2-0.4c-0.9-1.9-2.8-3.3-5.1-3.3c-2.3,0-4.2,1.4-5.1,3.3C6.8,5.9,6.7,6,6.7,6.1c0,0.1-0.1,0.1-0.1,0.1 C6.6,6.3,6.6,6.3,6.5,6.3c0,0,0,0-0.1,0c0,0,0,0-0.1,0c-0.1,0-0.2,0-0.3,0C4,6.4,2.4,8,2.4,10S4,13.6,6,13.6h6h6 c2,0,3.6-1.6,3.6-3.6S20,6.4,18,6.4z"
/>
<g {fill}> <g {fill}>
<path d="M18,6.4c-0.1,0-0.2,0-0.3,0c-0.1,0-0.1,0-0.1,0c-0.1,0-0.1,0-0.1,0c0,0,0,0-0.1-0.1c0,0,0,0,0,0c0,0,0,0,0,0 c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0-0.1-0.1c0-0.1-0.1-0.2-0.2-0.4c-0.9-1.9-2.8-3.3-5.1-3.3c-2.3,0-4.2,1.4-5.1,3.3 C6.8,5.9,6.7,6,6.7,6.1c0,0.1-0.1,0.1-0.1,0.1c0,0,0,0,0,0C6.6,6.3,6.6,6.3,6.5,6.3c0,0,0,0-0.1,0c0,0,0,0-0.1,0 c-0.1,0-0.2,0-0.3,0C4,6.4,2.4,8,2.4,10S4,13.6,6,13.6h0.6l1.2-1.2H6c-1.3,0-2.4-1.1-2.4-2.4c0-1.3,1.1-2.4,2.4-2.4h0.1 c0.2,0,0.4,0,0.6,0c0.2,0,0.4-0.1,0.6-0.2c0.2-0.1,0.3-0.3,0.4-0.4c0.1-0.1,0.1-0.2,0.2-0.3C7.8,6.5,7.9,6.4,8,6.2l0,0 c0.7-1.5,2.2-2.6,4-2.6s3.3,1.1,4,2.6l0,0c0.1,0.2,0.1,0.3,0.2,0.4c0,0.1,0.1,0.2,0.2,0.3c0.1,0.2,0.2,0.3,0.4,0.4 c0.2,0.1,0.4,0.2,0.6,0.2c0.2,0,0.4,0,0.6,0H18c1.3,0,2.4,1.1,2.4,2.4c0,1.3-1.1,2.4-2.4,2.4h-1.8l1.2,1.2H18c2,0,3.6-1.6,3.6-3.6 S20,6.4,18,6.4z"/> <path
<path d="M12,11.2l-4.4,4.4l0.8,0.8l3-3V21c0,0.3,0.3,0.6,0.6,0.6s0.6-0.3,0.6-0.6v-7.6l3,3l0.8-0.8L12,11.2z"/> d="M18,6.4c-0.1,0-0.2,0-0.3,0c-0.1,0-0.1,0-0.1,0c-0.1,0-0.1,0-0.1,0c0,0,0,0-0.1-0.1c0,0,0,0,0,0c0,0,0,0,0,0 c0,0,0,0,0,0c0,0,0,0,0,0c0,0,0,0-0.1-0.1c0-0.1-0.1-0.2-0.2-0.4c-0.9-1.9-2.8-3.3-5.1-3.3c-2.3,0-4.2,1.4-5.1,3.3 C6.8,5.9,6.7,6,6.7,6.1c0,0.1-0.1,0.1-0.1,0.1c0,0,0,0,0,0C6.6,6.3,6.6,6.3,6.5,6.3c0,0,0,0-0.1,0c0,0,0,0-0.1,0 c-0.1,0-0.2,0-0.3,0C4,6.4,2.4,8,2.4,10S4,13.6,6,13.6h0.6l1.2-1.2H6c-1.3,0-2.4-1.1-2.4-2.4c0-1.3,1.1-2.4,2.4-2.4h0.1 c0.2,0,0.4,0,0.6,0c0.2,0,0.4-0.1,0.6-0.2c0.2-0.1,0.3-0.3,0.4-0.4c0.1-0.1,0.1-0.2,0.2-0.3C7.8,6.5,7.9,6.4,8,6.2l0,0 c0.7-1.5,2.2-2.6,4-2.6s3.3,1.1,4,2.6l0,0c0.1,0.2,0.1,0.3,0.2,0.4c0,0.1,0.1,0.2,0.2,0.3c0.1,0.2,0.2,0.3,0.4,0.4 c0.2,0.1,0.4,0.2,0.6,0.2c0.2,0,0.4,0,0.6,0H18c1.3,0,2.4,1.1,2.4,2.4c0,1.3-1.1,2.4-2.4,2.4h-1.8l1.2,1.2H18c2,0,3.6-1.6,3.6-3.6 S20,6.4,18,6.4z"
/>
<path d="M12,11.2l-4.4,4.4l0.8,0.8l3-3V21c0,0.3,0.3,0.6,0.6,0.6s0.6-0.3,0.6-0.6v-7.6l3,3l0.8-0.8L12,11.2z" />
</g> </g>
</svg> </svg>

View File

@ -19,11 +19,15 @@ import { Resources } from '@anticrm/platform'
import CreateTask from './components/CreateTask.svelte' import CreateTask from './components/CreateTask.svelte'
import CreateProject from './components/CreateProject.svelte' import CreateProject from './components/CreateProject.svelte'
import TaskPresenter from './components/TaskPresenter.svelte' import TaskPresenter from './components/TaskPresenter.svelte'
import KanbanCard from './components/KanbanCard.svelte'
export { createProjectKanban } from './utils'
export default async (): Promise<Resources> => ({ export default async (): Promise<Resources> => ({
component: { component: {
CreateTask, CreateTask,
CreateProject, CreateProject,
TaskPresenter TaskPresenter,
KanbanCard
} }
}) })

View File

@ -32,7 +32,8 @@ export default mergeIds(taskId, task, {
TaskAssignee: '' as IntlString, TaskAssignee: '' as IntlString,
TaskDescription: '' as IntlString, TaskDescription: '' as IntlString,
NoAttachmentsForTask: '' as IntlString, NoAttachmentsForTask: '' as IntlString,
UploadDropFilesHere: '' as IntlString UploadDropFilesHere: '' as IntlString,
More: '' as IntlString
}, },
status: { status: {
AssigneeRequired: '' as IntlString AssigneeRequired: '' as IntlString

View File

@ -14,9 +14,12 @@
// limitations under the License. // limitations under the License.
// //
import type { Doc, Ref, Space } from '@anticrm/core' import type { Class, Data, Doc, Ref, Space, State } from '@anticrm/core'
import login from '@anticrm/login' import login from '@anticrm/login'
import { getMetadata } from '@anticrm/platform' import { getMetadata } from '@anticrm/platform'
import { Project } from '@anticrm/task'
import core from '@anticrm/core'
import view, { Kanban } from '@anticrm/view'
export async function uploadFile (space: Ref<Space>, file: File, attachedTo: Ref<Doc>): Promise<string> { export async function uploadFile (space: Ref<Space>, file: File, attachedTo: Ref<Doc>): Promise<string> {
console.log(file) console.log(file)
@ -40,3 +43,41 @@ export async function uploadFile (space: Ref<Space>, file: File, attachedTo: Ref
console.log(uuid) console.log(uuid)
return uuid return uuid
} }
export async function createProjectKanban (
projectId: Ref<Project>,
factory: <T extends Doc>(_class: Ref<Class<T>>, space: Ref<Space>, data: Data<T>, id: Ref<T>) => Promise<void>
): Promise<void> {
const states = [
{ color: '#7C6FCD', name: 'Open' },
{ color: '#6F7BC5', name: 'In Progress' },
{ color: '#77C07B', name: 'Under review' },
{ color: '#A5D179', name: 'Done' },
{ color: '#F28469', name: 'Invalid' }
]
const ids: Array<Ref<State>> = []
for (const st of states) {
const sid = (projectId + '.state.' + st.name.toLowerCase().replace(' ', '_')) as Ref<State>
await factory(
core.class.State,
projectId,
{
title: st.name,
color: st.color
},
sid
)
ids.push(sid)
}
await factory(
view.class.Kanban,
projectId,
{
attachedTo: projectId,
states: ids,
order: []
},
(projectId + '.kanban.') as Ref<Kanban>
)
}

View File

@ -15,45 +15,44 @@
import type { AttachedDoc, Doc } from '@anticrm/core' import type { AttachedDoc, Doc } from '@anticrm/core'
import core from '@anticrm/core' import core from '@anticrm/core'
import { Resources } from '@anticrm/platform'
import StringEditor from './components/StringEditor.svelte'
import StringPresenter from './components/StringPresenter.svelte'
import BooleanEditor from './components/BooleanEditor.svelte'
import BooleanPresenter from './components/BooleanPresenter.svelte'
import StatePresenter from './components/StatePresenter.svelte'
import StateEditor from './components/StateEditor.svelte'
import TimestampPresenter from './components/TimestampPresenter.svelte'
import DateEditor from './components/DateEditor.svelte'
import DatePresenter from './components/DatePresenter.svelte'
import TableView from './components/TableView.svelte'
import Table from './components/Table.svelte'
import KanbanView from './components/KanbanView.svelte'
import { getClient, MessageBox } from '@anticrm/presentation' import { getClient, MessageBox } from '@anticrm/presentation'
import { showPopup } from '@anticrm/ui' import { showPopup } from '@anticrm/ui'
import {buildModel} from './utils' import BooleanEditor from './components/BooleanEditor.svelte'
import BooleanPresenter from './components/BooleanPresenter.svelte'
import DateEditor from './components/DateEditor.svelte'
import DatePresenter from './components/DatePresenter.svelte'
import KanbanView from './components/KanbanView.svelte'
import StateEditor from './components/StateEditor.svelte'
import StatePresenter from './components/StatePresenter.svelte'
import StringEditor from './components/StringEditor.svelte'
import StringPresenter from './components/StringPresenter.svelte'
import Table from './components/Table.svelte'
import TableView from './components/TableView.svelte'
import TimestampPresenter from './components/TimestampPresenter.svelte'
export { default as ContextMenu } from './components/Menu.svelte'
export { buildModel, getActions, getObjectPresenter } from './utils'
export { Table } export { Table }
export { buildModel, getObjectPresenter, getActions } from './utils'
function Delete(object: Doc): void { function Delete (object: Doc): void {
showPopup(MessageBox, { showPopup(MessageBox, {
label: 'Delete object', label: 'Delete object',
message: 'Do you want to delete this object?' message: 'Do you want to delete this object?'
}, undefined, (result) => { }, undefined, (result) => {
if (result) { if (result !== undefined) {
const client = getClient() const client = getClient()
if(client.getHierarchy().isDerived(object._class, core.class.AttachedDoc)) { if (client.getHierarchy().isDerived(object._class, core.class.AttachedDoc)) {
const adoc = object as AttachedDoc const adoc = object as AttachedDoc
client.removeCollection(object._class, object.space, adoc._id, adoc.attachedTo, adoc.attachedToClass, adoc.collection) client.removeCollection(object._class, object.space, adoc._id, adoc.attachedTo, adoc.attachedToClass, adoc.collection).catch(err => console.error(err))
} else { } else {
client.removeDoc(object._class, object.space, object._id) client.removeDoc(object._class, object.space, object._id).catch(err => console.error(err))
} }
} }
}) })
} }
export default async () => ({ export default async (): Promise<Resources> => ({
actionImpl: { actionImpl: {
Delete Delete
}, },