Todos Support (#681)

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
Andrey Sobolev 2021-12-20 17:18:29 +07:00 committed by GitHub
parent ec738daf75
commit 56380c4958
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 564 additions and 74 deletions

View File

@ -14,13 +14,13 @@
// //
// To help typescript locate view plugin properly // To help typescript locate view plugin properly
import type {} from '@anticrm/view' import type { ActionTarget } from '@anticrm/view'
import attachment from '@anticrm/model-attachment' import attachment from '@anticrm/model-attachment'
import type { Employee } from '@anticrm/contact' import type { Employee } from '@anticrm/contact'
import contact from '@anticrm/contact' import contact from '@anticrm/contact'
import { Arr, Class, Doc, Domain, DOMAIN_MODEL, FindOptions, Ref, Space } from '@anticrm/core' import { Arr, Class, Doc, Domain, DOMAIN_MODEL, FindOptions, Ref, Space, Timestamp } from '@anticrm/core'
import { Builder, Collection, Mixin, Model, Prop, TypeRef, TypeString, UX } from '@anticrm/model' import { Builder, Collection, Mixin, Model, Prop, TypeBoolean, TypeDate, TypeRef, TypeString, UX } from '@anticrm/model'
import chunter from '@anticrm/model-chunter' import chunter from '@anticrm/model-chunter'
import core, { TAttachedDoc, TClass, TDoc, TSpace } from '@anticrm/model-core' import core, { TAttachedDoc, TClass, TDoc, TSpace } from '@anticrm/model-core'
import view from '@anticrm/model-view' import view from '@anticrm/model-view'
@ -42,7 +42,8 @@ import type {
WonStateTemplate, WonStateTemplate,
LostStateTemplate, LostStateTemplate,
KanbanTemplate, KanbanTemplate,
Task Task,
TodoItem
} from '@anticrm/task' } from '@anticrm/task'
import { createProjectKanban } from '@anticrm/task' import { createProjectKanban } from '@anticrm/task'
import task from './plugin' import task from './plugin'
@ -99,6 +100,22 @@ export class TTask extends TAttachedDoc implements Task {
assignee!: Ref<Employee> | null assignee!: Ref<Employee> | null
declare rank: string declare rank: string
@Prop(Collection(task.class.TodoItem), "Todo's" as IntlString)
todoItems!: number
}
@Model(task.class.TodoItem, core.class.AttachedDoc, DOMAIN_TASK)
@UX('Todo' as IntlString)
export class TTodoItem extends TAttachedDoc implements TodoItem {
@Prop(TypeString(), 'Name' as IntlString, task.icon.Task)
name!: string
@Prop(TypeBoolean(), 'Complete' as IntlString)
done!: boolean
@Prop(TypeDate(), 'Due date' as IntlString)
dueTo?: Timestamp
} }
@Model(task.class.SpaceWithStates, core.class.Space) @Model(task.class.SpaceWithStates, core.class.Space)
@ -212,7 +229,8 @@ export function createModel (builder: Builder): void {
TTask, TTask,
TSpaceWithStates, TSpaceWithStates,
TProject, TProject,
TIssue TIssue,
TTodoItem
) )
builder.mixin(task.class.Project, core.class.Class, workbench.mixin.SpaceView, { builder.mixin(task.class.Project, core.class.Class, workbench.mixin.SpaceView, {
view: { view: {
@ -389,6 +407,51 @@ export function createModel (builder: Builder): void {
builder.mixin(task.class.DoneState, core.class.Class, view.mixin.AttributePresenter, { builder.mixin(task.class.DoneState, core.class.Class, view.mixin.AttributePresenter, {
presenter: task.component.DoneStatePresenter presenter: task.component.DoneStatePresenter
}) })
builder.mixin(task.class.TodoItem, core.class.Class, view.mixin.AttributeEditor, {
editor: task.component.Todos
})
builder.mixin(task.class.TodoItem, core.class.Class, view.mixin.AttributePresenter, {
presenter: task.component.TodoItemPresenter
})
builder.createDoc(
view.class.Action,
core.space.Model,
{
label: 'Mark as done' as IntlString,
icon: task.icon.TodoCheck,
action: task.actionImpl.TodoItemMarkDone
},
task.action.TodoItemMarkDone
)
builder.createDoc(
view.class.Action,
core.space.Model,
{
label: 'Mark as undone' as IntlString,
icon: task.icon.TodoUnCheck,
action: task.actionImpl.TodoItemMarkUnDone
},
task.action.TodoItemMarkUnDone
)
builder.createDoc<ActionTarget<TodoItem>>(view.class.ActionTarget, core.space.Model, {
target: task.class.TodoItem,
action: task.action.TodoItemMarkDone,
query: {
done: false
}
})
builder.createDoc(view.class.ActionTarget, core.space.Model, {
target: task.class.TodoItem,
action: task.action.TodoItemMarkUnDone,
query: {
done: true
}
})
} }
export { taskOperation } from './migration' export { taskOperation } from './migration'

View File

@ -28,11 +28,15 @@ export default mergeIds(taskId, task, {
}, },
action: { action: {
CreateTask: '' as Ref<Action>, CreateTask: '' as Ref<Action>,
EditStatuses: '' as Ref<Action> EditStatuses: '' as Ref<Action>,
TodoItemMarkDone: '' as Ref<Action>,
TodoItemMarkUnDone: '' as Ref<Action>
}, },
actionImpl: { actionImpl: {
CreateTask: '' as Resource<(object: Doc) => Promise<void>>, CreateTask: '' as Resource<(object: Doc) => Promise<void>>,
EditStatuses: '' as Resource<(object: Doc) => Promise<void>> EditStatuses: '' as Resource<(object: Doc) => Promise<void>>,
TodoItemMarkDone: '' as Resource<(object: Doc) => Promise<void>>,
TodoItemMarkUnDone: '' as Resource<(object: Doc) => Promise<void>>
}, },
component: { component: {
ProjectView: '' as AnyComponent, ProjectView: '' as AnyComponent,
@ -46,7 +50,9 @@ export default mergeIds(taskId, task, {
StatePresenter: '' as AnyComponent, StatePresenter: '' as AnyComponent,
DoneStatePresenter: '' as AnyComponent, DoneStatePresenter: '' as AnyComponent,
StateEditor: '' as AnyComponent, StateEditor: '' as AnyComponent,
KanbanView: '' as AnyComponent KanbanView: '' as AnyComponent,
Todos: '' as AnyComponent,
TodoItemPresenter: '' as AnyComponent
}, },
string: { string: {
Task: '' as IntlString, Task: '' as IntlString,

View File

@ -18,7 +18,7 @@ import clone from 'just-clone'
import type { Class, Doc, Ref } from './classes' import type { Class, Doc, Ref } from './classes'
import core from './component' import core from './component'
import { Hierarchy } from './hierarchy' import { Hierarchy } from './hierarchy'
import { findProperty, resultSort } from './query' import { matchQuery, resultSort } from './query'
import type { DocumentQuery, FindOptions, FindResult, LookupData, Refs, Storage, TxResult, WithLookup } from './storage' import type { DocumentQuery, FindOptions, FindResult, LookupData, Refs, Storage, TxResult, WithLookup } from './storage'
import type { Tx, TxCreateDoc, TxMixin, TxPutBag, TxRemoveDoc, TxUpdateDoc } from './tx' import type { Tx, TxCreateDoc, TxMixin, TxPutBag, TxRemoveDoc, TxUpdateDoc } from './tx'
import { TxProcessor } from './tx' import { TxProcessor } from './tx'
@ -108,11 +108,7 @@ export abstract class MemDb extends TxProcessor {
result = this.getObjectsByClass(_class) result = this.getObjectsByClass(_class)
} }
for (const key in query) { result = matchQuery(result, query)
if (key === '_id' && ((query._id as any)?.$like === undefined || query._id === undefined)) continue
const value = (query as any)[key]
result = findProperty(result, key, value)
}
if (options?.lookup !== undefined) result = this.lookup(result as T[], options.lookup) if (options?.lookup !== undefined) result = this.lookup(result as T[], options.lookup)

View File

@ -1,3 +1,4 @@
import { DocumentQuery } from '.'
import { Doc } from './classes' import { Doc } from './classes'
import { createPredicates, isPredicate } from './predicate' import { createPredicates, isPredicate } from './predicate'
import { SortingQuery } from './storage' import { SortingQuery } from './storage'
@ -102,3 +103,18 @@ function getValue (key: string, obj: any): any {
} }
return value return value
} }
/**
* @public
*/
export function matchQuery<T extends Doc> (docs: Doc[], query: DocumentQuery<T>): Doc[] {
let result = [...docs]
for (const key in query) {
if (key === '_id' && ((query._id as any)?.$like === undefined || query._id === undefined)) continue
const value = (query as any)[key]
result = findProperty(result, key, value)
if (result.length === 0) {
break
}
}
return result
}

View File

@ -26,14 +26,17 @@
import SpaceSelect from './SpaceSelect.svelte' import SpaceSelect from './SpaceSelect.svelte'
import presentation from '..' import presentation from '..'
export let spaceClass: Ref<Class<Space>> export let spaceClass: Ref<Class<Space>> | undefined = undefined
export let space: Ref<Space> export let space: Ref<Space>
export let spaceLabel: IntlString export let spaceLabel: IntlString | undefined = undefined
export let spacePlaceholder: IntlString export let spacePlaceholder: IntlString | undefined = undefined
export let label: IntlString export let label: IntlString
export let okAction: () => void export let okAction: () => void
export let canSave: boolean = false export let canSave: boolean = false
export let okLabel: IntlString = presentation.string.Create
export let cancelLabel: IntlString = presentation.string.Cancel
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
</script> </script>
@ -48,13 +51,15 @@
{/if} {/if}
</div> </div>
<div class="content"><slot /></div> <div class="content"><slot /></div>
{#if spaceClass && spaceLabel && spacePlaceholder}
<div class="flex-col pool"> <div class="flex-col pool">
<div class="separator" /> <div class="separator" />
<SpaceSelect _class={spaceClass} label={spaceLabel} placeholder={spacePlaceholder} bind:value={space} /> <SpaceSelect _class={spaceClass} label={spaceLabel} placeholder={spacePlaceholder} bind:value={space} />
</div> </div>
{/if}
<div class="footer"> <div class="footer">
<Button disabled={!canSave} label={presentation.string.Create} size={'small'} transparent primary on:click={() => { okAction(); dispatch('close') }} /> <Button disabled={!canSave} label={okLabel} size={'small'} transparent primary on:click={() => { okAction(); dispatch('close') }} />
<Button label={presentation.string.Cancel} size={'small'} transparent on:click={() => { dispatch('close') }} /> <Button label={cancelLabel} size={'small'} transparent on:click={() => { dispatch('close') }} />
</div> </div>
</form> </form>

View File

@ -33,7 +33,7 @@
showPopup, showPopup,
TimeSince TimeSince
} from '@anticrm/ui' } from '@anticrm/ui'
import type { Action, AttributeModel } from '@anticrm/view' import type { AttributeModel } from '@anticrm/view'
import { buildModel, getActions, getObjectPresenter } from '@anticrm/view-resources' import { buildModel, getActions, getObjectPresenter } from '@anticrm/view-resources'
import { activityKey, ActivityKey, DisplayTx } from '../activity' import { activityKey, ActivityKey, DisplayTx } from '../activity'
import ShowMore from './ShowMore.svelte' import ShowMore from './ShowMore.svelte'
@ -53,7 +53,6 @@
let props: any let props: any
let employee: EmployeeAccount | undefined let employee: EmployeeAccount | undefined
let model: AttributeModel[] = [] let model: AttributeModel[] = []
let actions: Action[] = []
let edit = false let edit = false
@ -74,7 +73,7 @@
} }
const docClass: Class<Doc> = client.getModel().getObject(doc._class) const docClass: Class<Doc> = client.getModel().getObject(doc._class)
const presenter = await getObjectPresenter(client, doc._class, 'doc-presenter') const presenter = await getObjectPresenter(client, doc._class, { key: 'doc-presenter' })
if (presenter !== undefined) { if (presenter !== undefined) {
return { return {
display: 'inline', display: 'inline',
@ -125,10 +124,6 @@
}) })
} }
$: getActions(client, tx.tx.objectClass).then((result) => {
actions = result
})
async function getValue (m: AttributeModel, utx: TxUpdateDoc<Doc>): Promise<any> { async function getValue (m: AttributeModel, utx: TxUpdateDoc<Doc>): Promise<any> {
const val = (utx.operations as any)[m.key] const val = (utx.operations as any)[m.key]
console.log(m._class, m.key, val, typeof val) console.log(m._class, m.key, val, typeof val)
@ -140,6 +135,7 @@
return val return val
} }
const showMenu = async (ev: MouseEvent): Promise<void> => { const showMenu = async (ev: MouseEvent): Promise<void> => {
const actions = await getActions(client, tx.doc as Doc)
showPopup( showPopup(
Menu, Menu,
{ {

View File

@ -12,4 +12,11 @@
<path d="M14.4,8c0,3.5-2.9,6.4-6.4,6.4c-3.5,0-6.4-2.9-6.4-6.4c0-3.5,2.9-6.4,6.4-6.4c0.1-0.4,0.3-0.8,0.5-1.2c-0.2,0-0.3,0-0.5,0 C3.8,0.4,0.4,3.8,0.4,8c0,4.2,3.4,7.6,7.6,7.6s7.6-3.4,7.6-7.6c0-0.2,0-0.3,0-0.5C15.2,7.7,14.8,7.9,14.4,8z"/> <path d="M14.4,8c0,3.5-2.9,6.4-6.4,6.4c-3.5,0-6.4-2.9-6.4-6.4c0-3.5,2.9-6.4,6.4-6.4c0.1-0.4,0.3-0.8,0.5-1.2c-0.2,0-0.3,0-0.5,0 C3.8,0.4,0.4,3.8,0.4,8c0,4.2,3.4,7.6,7.6,7.6s7.6-3.4,7.6-7.6c0-0.2,0-0.3,0-0.5C15.2,7.7,14.8,7.9,14.4,8z"/>
<circle cx="13" cy="3" r="3"/> <circle cx="13" cy="3" r="3"/>
</symbol> </symbol>
<symbol id='todo-check' viewBox="0 0 16 16">
<circle cx="8" cy="8" r="6" stroke="white" fill="none"/>
<path d="M5.33268 8L7.33268 10L10.666 6" stroke="white"/>
</symbol>
<symbol id='todo-uncheck' viewBox="0 0 16 16">
<circle cx="8" cy="8" r="6" stroke="white" fill="none"/>
</symbol>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -17,6 +17,16 @@
"More": "Options", "More": "Options",
"TaskUnAssign": "Unassign", "TaskUnAssign": "Unassign",
"NoTaskForObject": "No tasks defined", "NoTaskForObject": "No tasks defined",
"Delete": "Delete" "Delete": "Delete",
"NoTodoItems": "No to do's defined",
"TodoName": "Name",
"TodoState": "State",
"DoneState": "done",
"UndoneState": "todo",
"TodoDueDate": "Due to",
"TodoDescription": "To do description *",
"TodoEdit": "Edit To Do",
"TodoSave": "Save",
"TodoCreate": "Create To Do"
} }
} }

View File

@ -20,7 +20,9 @@ const icons = require('../assets/icons.svg')
loadMetadata(task.icon, { loadMetadata(task.icon, {
Task: `${icons}#task`, Task: `${icons}#task`,
Kanban: `${icons}#kanban`, Kanban: `${icons}#kanban`,
Status: `${icons}#status` Status: `${icons}#status`,
TodoCheck: `${icons}#todo-check`,
TodoUnCheck: `${icons}#todo-uncheck`
}) })
addStringsLoader(taskId, async (lang: string) => await import(`../lang/${lang}.json`)) addStringsLoader(taskId, async (lang: string) => await import(`../lang/${lang}.json`))

View File

@ -0,0 +1,76 @@
<!--
// 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 { Class, Ref, Space } from '@anticrm/core'
import { Card, getClient } from '@anticrm/presentation'
import type { Task } from '@anticrm/task'
import { DatePicker, EditBox, Grid } from '@anticrm/ui'
import { createEventDispatcher } from 'svelte'
import task from '../../plugin'
export let objectId: Ref<Task>
export let _class: Ref<Class<Task>>
export let space: Ref<Space>
let name: string
const done = false
let dueTo: Date
$: _space = space
const dispatch = createEventDispatcher()
const client = getClient()
export function canClose (): boolean {
return objectId === undefined
}
async function createTodo () {
await client.addCollection(
task.class.TodoItem,
space,
objectId,
_class,
'todos',
{
name,
done,
dueTo: dueTo?.getTime() ?? undefined
}
)
}
</script>
<Card
label={task.string.TodoCreate}
okAction={createTodo}
canSave={name?.length > 0}
bind:space={_space}
on:close={() => {
dispatch('close')
}}
okLabel={task.string.TodoSave}>
<Grid column={1} rowGap={1.75}>
<EditBox
label={task.string.TodoDescription}
bind:value={name}
icon={task.icon.Task}
placeholder="todo..."
maxWidth="39rem"
focus
/>
<DatePicker title={task.string.TodoDueDate} bind:selected={dueTo} />
</Grid>
</Card>

View File

@ -0,0 +1,91 @@
<!--
// 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 { DocumentUpdate, Ref, Timestamp } from '@anticrm/core'
import { IntlString } from '@anticrm/platform'
import { Card, getClient } from '@anticrm/presentation'
import type { TodoItem } from '@anticrm/task'
import { DatePicker, EditBox, Grid } from '@anticrm/ui'
import { createEventDispatcher } from 'svelte'
import task from '../../plugin'
export let item: TodoItem
let name: string = ''
let dueTo: Date | undefined
let _itemId: Ref<TodoItem>
$: if (_itemId !== item._id) {
_itemId = item._id
name = item.name
dueTo = new Date(item.dueTo ?? 0)
console.log('AHTUNG', item, dueTo)
}
const dispatch = createEventDispatcher()
const client = getClient()
export function canClose (): boolean {
return true
}
async function editTodo () {
const ops: DocumentUpdate<TodoItem> = {}
if (item.name !== name) {
ops.name = name
}
if (item.dueTo !== dueTo) {
ops.dueTo = (dueTo?.getTime() ?? null) as unknown as Timestamp
}
console.log('AHTUNG', ops)
if (Object.keys(ops).length === 0) {
return
}
await client.updateCollection(
item._class,
item.space,
item._id,
item.attachedTo,
item.attachedToClass,
item.collection,
ops
)
}
</script>
<Card
label={task.string.TodoEdit}
okAction={editTodo}
canSave={name.length > 0}
space={item.space}
on:close={() => {
dispatch('close')
}}
okLabel={task.string.TodoSave}>
<Grid column={1} rowGap={1.75}>
<EditBox
label={task.string.TodoDescription}
bind:value={name}
icon={task.icon.Task}
placeholder="todo..."
maxWidth="39rem"
focus
/>
<DatePicker title={task.string.TodoDueDate} bind:selected={dueTo} />
</Grid>
</Card>

View File

@ -0,0 +1,33 @@
<!--
// 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 { TodoItem } from '@anticrm/task'
import { closeTooltip, Icon, showPopup } from '@anticrm/ui'
import task from '../../plugin'
import EditTodo from './EditTodo.svelte'
export let value: TodoItem
function show (elm: EventTarget | null) {
closeTooltip()
showPopup(EditTodo, { item: value }, elm as HTMLElement)
}
</script>
<div class="sm-tool-icon" on:click={(evt) => show(evt.target)}>
<span class="icon"><Icon icon={task.icon.Task} size={'small'} /></span>{value.name}
</div>

View File

@ -0,0 +1,47 @@
<!--
// 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 { TodoItem } from '@anticrm/task'
import { Label } from '@anticrm/ui'
import task from '../../plugin'
export let value: TodoItem
$: color = value.done ? '#60B96E' : '#6F7BC5'
$: text = value.done ? task.string.DoneState : task.string.UndoneState
</script>
{#if value }
<div class="overflow-label state-container" style="background-color: {color};">
<Label label={text}/>
</div>
{/if}
<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>

View File

@ -0,0 +1,86 @@
<!--
// 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 { TodoItem } from '@anticrm/task'
import { createQuery } from '@anticrm/presentation'
import { CircleButton, IconAdd, showPopup, Label } from '@anticrm/ui'
import CreateTodo from './CreateTodo.svelte'
import { Table } from '@anticrm/view-resources'
import task from '../../plugin'
export let objectId: Ref<Doc>
export let space: Ref<Space>
export let _class: Ref<Class<Doc>>
let todos: TodoItem[] = []
const query = createQuery()
$: query.query(task.class.TodoItem, { attachedTo: objectId }, result => { todos = result })
const createApp = (ev: MouseEvent): void => {
showPopup(CreateTodo, { objectId, _class, space }, ev.target as HTMLElement)
}
</script>
<div class="applications-container">
<div class="flex-row-center">
<div class="title">To Do's</div>
<CircleButton icon={IconAdd} size={'small'} selected on:click={createApp} />
</div>
{#if todos.length > 0}
<Table
_class={task.class.TodoItem}
config={[{ key: '', label: task.string.TodoName }, 'dueTo', { key: 'done', presenter: task.component.TodoStatePresenter, label: task.string.TodoState }]}
options={
{
// lookup: {
// }
}
}
query={ { attachedTo: objectId } }
/>
{:else}
<div class="flex-col-center mt-5 createapp-container">
<div class="small-text">
<a href={'#'} on:click={createApp}><Label label={task.string.NoTodoItems} /></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>

View File

@ -24,13 +24,17 @@ import TemplatesIcon from './components/TemplatesIcon.svelte'
import EditIssue from './components/EditIssue.svelte' import EditIssue from './components/EditIssue.svelte'
import { Doc } from '@anticrm/core' import { Doc } from '@anticrm/core'
import { showPopup } from '@anticrm/ui' import { showPopup } from '@anticrm/ui'
import { getClient } from '@anticrm/presentation'
import KanbanView from './components/kanban/KanbanView.svelte' import KanbanView from './components/kanban/KanbanView.svelte'
import StateEditor from './components/state/StateEditor.svelte' import StateEditor from './components/state/StateEditor.svelte'
import StatePresenter from './components/state/StatePresenter.svelte' import StatePresenter from './components/state/StatePresenter.svelte'
import DoneStatePresenter from './components/state/DoneStatePresenter.svelte' import DoneStatePresenter from './components/state/DoneStatePresenter.svelte'
import EditStatuses from './components/state/EditStatuses.svelte' import EditStatuses from './components/state/EditStatuses.svelte'
import { SpaceWithStates } from '@anticrm/task' import { SpaceWithStates, TodoItem } from '@anticrm/task'
import Todos from './components/todos/Todos.svelte'
import TodoItemPresenter from './components/todos/TodoItemPresenter.svelte'
import TodoStatePresenter from './components/todos/TodoStatePresenter.svelte'
export { default as KanbanTemplateEditor } from './components/kanban/KanbanTemplateEditor.svelte' export { default as KanbanTemplateEditor } from './components/kanban/KanbanTemplateEditor.svelte'
export { default as KanbanTemplateSelector } from './components/kanban/KanbanTemplateSelector.svelte' export { default as KanbanTemplateSelector } from './components/kanban/KanbanTemplateSelector.svelte'
@ -46,6 +50,19 @@ async function editStatuses (object: SpaceWithStates): Promise<void> {
showPopup(EditStatuses, { _id: object._id, spaceClass: object._class }, 'right') showPopup(EditStatuses, { _id: object._id, spaceClass: object._class }, 'right')
} }
async function toggleDone (value: boolean, object: TodoItem): Promise<void> {
await getClient().updateCollection(
object._class,
object.space,
object._id,
object.attachedTo,
object.attachedToClass,
object.collection, {
done: value
}
)
}
export default async (): Promise<Resources> => ({ export default async (): Promise<Resources> => ({
component: { component: {
CreateTask, CreateTask,
@ -57,10 +74,15 @@ export default async (): Promise<Resources> => ({
KanbanView, KanbanView,
StatePresenter, StatePresenter,
StateEditor, StateEditor,
DoneStatePresenter DoneStatePresenter,
Todos,
TodoItemPresenter,
TodoStatePresenter
}, },
actionImpl: { actionImpl: {
CreateTask: createTask, CreateTask: createTask,
EditStatuses: editStatuses EditStatuses: editStatuses,
TodoItemMarkDone: async (obj: TodoItem) => await toggleDone(true, obj),
TodoItemMarkUnDone: async (obj: TodoItem) => await toggleDone(false, obj)
} }
}) })

View File

@ -14,8 +14,8 @@
// //
import { IntlString, mergeIds } from '@anticrm/platform' import { IntlString, mergeIds } from '@anticrm/platform'
import task, { taskId } from '@anticrm/task' import task, { taskId } from '@anticrm/task'
import { AnyComponent } from '@anticrm/ui'
export default mergeIds(taskId, task, { export default mergeIds(taskId, task, {
string: { string: {
@ -35,9 +35,22 @@ export default mergeIds(taskId, task, {
More: '' as IntlString, More: '' as IntlString,
UploadDropFilesHere: '' as IntlString, UploadDropFilesHere: '' as IntlString,
NoTaskForObject: '' as IntlString, NoTaskForObject: '' as IntlString,
Delete: '' as IntlString Delete: '' as IntlString,
NoTodoItems: '' as IntlString,
TodoName: '' as IntlString,
TodoState: '' as IntlString,
DoneState: '' as IntlString,
UndoneState: '' as IntlString,
TodoDueDate: '' as IntlString,
TodoDescription: '' as IntlString,
TodoEdit: '' as IntlString,
TodoSave: '' as IntlString,
TodoCreate: '' as IntlString
}, },
status: { status: {
AssigneeRequired: '' as IntlString AssigneeRequired: '' as IntlString
},
component: {
TodoStatePresenter: '' as AnyComponent
} }
}) })

View File

@ -14,7 +14,7 @@
// //
import type { Employee } from '@anticrm/contact' import type { Employee } from '@anticrm/contact'
import { AttachedDoc, Class, Client, Data, Doc, DocWithRank, genRanks, Mixin, Ref, Space, TxOperations } from '@anticrm/core' import { AttachedDoc, Class, Client, Data, Doc, DocWithRank, genRanks, Mixin, Ref, Space, Timestamp, TxOperations } from '@anticrm/core'
import type { Asset, Plugin } from '@anticrm/platform' import type { Asset, Plugin } from '@anticrm/platform'
import { plugin } from '@anticrm/platform' import { plugin } from '@anticrm/platform'
import type { AnyComponent } from '@anticrm/ui' import type { AnyComponent } from '@anticrm/ui'
@ -55,6 +55,17 @@ export interface Task extends AttachedDoc, DocWithRank {
doneState: Ref<DoneState> | null doneState: Ref<DoneState> | null
number: number number: number
assignee: Ref<Employee> | null assignee: Ref<Employee> | null
todoItems?: number
}
/**
* @public
*/
export interface TodoItem extends AttachedDoc {
name: string
done: boolean
dueTo?: Timestamp
} }
/** /**
@ -168,7 +179,8 @@ const task = plugin(taskId, {
WonStateTemplate: '' as Ref<Class<WonStateTemplate>>, WonStateTemplate: '' as Ref<Class<WonStateTemplate>>,
LostStateTemplate: '' as Ref<Class<LostStateTemplate>>, LostStateTemplate: '' as Ref<Class<LostStateTemplate>>,
KanbanTemplate: '' as Ref<Class<KanbanTemplate>>, KanbanTemplate: '' as Ref<Class<KanbanTemplate>>,
KanbanTemplateSpace: '' as Ref<Class<KanbanTemplateSpace>> KanbanTemplateSpace: '' as Ref<Class<KanbanTemplateSpace>>,
TodoItem: '' as Ref<Class<TodoItem>>
}, },
viewlet: { viewlet: {
Kanban: '' as Ref<ViewletDescriptor> Kanban: '' as Ref<ViewletDescriptor>
@ -176,7 +188,9 @@ const task = plugin(taskId, {
icon: { icon: {
Task: '' as Asset, Task: '' as Asset,
Kanban: '' as Asset, Kanban: '' as Asset,
Status: '' as Asset Status: '' as Asset,
TodoCheck: '' as Asset,
TodoUnCheck: '' as Asset
}, },
global: { global: {
// Global task root, if not attached to some other object. // Global task root, if not attached to some other object.

View File

@ -19,7 +19,6 @@
import { getResource } from '@anticrm/platform' import { getResource } from '@anticrm/platform'
import { getClient } from '@anticrm/presentation' import { getClient } from '@anticrm/presentation'
import { Menu } from '@anticrm/ui' import { Menu } from '@anticrm/ui'
import { createEventDispatcher } from 'svelte'
import { getActions } from '../utils' import { getActions } from '../utils'
export let object: Doc export let object: Doc
@ -31,15 +30,13 @@
}[] = [] }[] = []
const client = getClient() const client = getClient()
const dispatch = createEventDispatcher()
async function invokeAction (action: Resource<(object: Doc) => Promise<void>>) { async function invokeAction (action: Resource<(object: Doc) => Promise<void>>) {
dispatch('close')
const impl = await getResource(action) const impl = await getResource(action)
await impl(object) await impl(object)
} }
getActions(client, object._class).then(result => { getActions(client, object).then(result => {
actions = result.map(a => ({ actions = result.map(a => ({
label: a.label, label: a.label,
icon: a.icon, icon: a.icon,

View File

@ -19,6 +19,7 @@
import { SortingOrder } from '@anticrm/core' import { SortingOrder } from '@anticrm/core'
import { createQuery, getClient } from '@anticrm/presentation' import { createQuery, getClient } from '@anticrm/presentation'
import { IconDown, IconUp, Label, Loading, showPopup } from '@anticrm/ui' import { IconDown, IconUp, Label, Loading, showPopup } from '@anticrm/ui'
import { BuildModelKey } from '@anticrm/view'
import { buildModel } from '../utils' import { buildModel } from '../utils'
import MoreV from './icons/MoreV.svelte' import MoreV from './icons/MoreV.svelte'
import Menu from './Menu.svelte' import Menu from './Menu.svelte'
@ -26,7 +27,7 @@
export let _class: Ref<Class<Doc>> export let _class: Ref<Class<Doc>>
export let query: DocumentQuery<Doc> export let query: DocumentQuery<Doc>
export let options: FindOptions<Doc> | undefined export let options: FindOptions<Doc> | undefined
export let config: string[] export let config: (BuildModelKey|string)[]
let sortKey = 'modifiedOn' let sortKey = 'modifiedOn'
let sortOrder = SortingOrder.Descending let sortOrder = SortingOrder.Descending

View File

@ -14,7 +14,7 @@
// limitations under the License. // limitations under the License.
// //
import core, { AttachedDoc, Class, Client, Collection, Doc, FindOptions, FindResult, Obj, Ref, TxOperations } from '@anticrm/core' import core, { AttachedDoc, Class, Client, Collection, Doc, FindOptions, FindResult, Obj, Ref, TxOperations, matchQuery } from '@anticrm/core'
import type { IntlString } from '@anticrm/platform' import type { IntlString } from '@anticrm/platform'
import { getResource } from '@anticrm/platform' import { getResource } from '@anticrm/platform'
import { getAttributePresenterClass } from '@anticrm/presentation' import { getAttributePresenterClass } from '@anticrm/presentation'
@ -41,9 +41,9 @@ export async function getObjectPresenter (client: Client, _class: Ref<Class<Obj>
(key.length > 0 ? key + '.' + clazz.sortingKey : clazz.sortingKey) (key.length > 0 ? key + '.' + clazz.sortingKey : clazz.sortingKey)
: key : key
return { return {
key, key: preserveKey.key,
_class, _class,
label: clazz.label, label: preserveKey.label ?? clazz.label,
presenter, presenter,
sortingKey sortingKey
} }
@ -68,16 +68,16 @@ async function getAttributePresenter (client: Client, _class: Ref<Class<Obj>>, k
const sortingKey = attribute.type._class === core.class.ArrOf ? resultKey + '.length' : resultKey const sortingKey = attribute.type._class === core.class.ArrOf ? resultKey + '.length' : resultKey
const presenter = await getResource(presenterMixin.presenter) const presenter = await getResource(presenterMixin.presenter)
return { return {
key: resultKey, key: preserveKey.key,
sortingKey, sortingKey,
_class: attrClass, _class: attrClass,
label: attribute.label, label: preserveKey.label ?? attribute.label,
presenter presenter
} }
} }
async function getPresenter (client: Client, _class: Ref<Class<Obj>>, key: BuildModelKey, preserveKey: BuildModelKey, options?: FindOptions<Doc>): Promise<AttributeModel> { async function getPresenter (client: Client, _class: Ref<Class<Obj>>, key: BuildModelKey, preserveKey: BuildModelKey, options?: FindOptions<Doc>): Promise<AttributeModel> {
if (typeof key === 'object') { if (key.presenter !== undefined) {
const { presenter, label, sortingKey } = key const { presenter, label, sortingKey } = key
return { return {
key: '', key: '',
@ -87,41 +87,41 @@ async function getPresenter (client: Client, _class: Ref<Class<Obj>>, key: Build
presenter: await getResource(presenter) presenter: await getResource(presenter)
} }
} }
if (key.length === 0) { if (key.key.length === 0) {
return await getObjectPresenter(client, _class, preserveKey) return await getObjectPresenter(client, _class, preserveKey)
} else { } else {
const split = key.split('.') const split = key.key.split('.')
if (split[0] === '$lookup') { if (split[0] === '$lookup') {
const lookupClass = (options?.lookup as any)[split[1]] as Ref<Class<Obj>> const lookupClass = (options?.lookup as any)[split[1]] as Ref<Class<Obj>>
if (lookupClass === undefined) { if (lookupClass === undefined) {
throw new Error('lookup class does not provided for ' + split[1]) throw new Error('lookup class does not provided for ' + split[1])
} }
const lookupKey = split[2] ?? '' const lookupKey = { ...key, key: split[2] ?? '' }
const model = await getPresenter(client, lookupClass, lookupKey, preserveKey) const model = await getPresenter(client, lookupClass, lookupKey, preserveKey)
if (lookupKey === '') { if (lookupKey.key === '') {
const attribute = client.getHierarchy().getAttribute(_class, split[1]) const attribute = client.getHierarchy().getAttribute(_class, split[1])
model.label = attribute.label model.label = attribute.label
} else { } else {
const attribute = client.getHierarchy().getAttribute(lookupClass, lookupKey) const attribute = client.getHierarchy().getAttribute(lookupClass, lookupKey.key)
model.label = attribute.label model.label = attribute.label
} }
return model return model
} }
return await getAttributePresenter(client, _class, key, preserveKey) return await getAttributePresenter(client, _class, key.key, preserveKey)
} }
} }
export async function buildModel (options: BuildModelOptions): Promise<AttributeModel[]> { export async function buildModel (options: BuildModelOptions): Promise<AttributeModel[]> {
console.log('building table model for', options) console.log('building table model for', options)
// eslint-disable-next-line array-callback-return // eslint-disable-next-line array-callback-return
const model = options.keys.map(async key => { const model = options.keys.map(key => typeof key === 'string' ? { key: key } : key).map(async key => {
try { try {
return await getPresenter(options.client, options._class, key, key, options.options) return await getPresenter(options.client, options._class, key, key, options.options)
} catch (err: any) { } catch (err: any) {
if ((options.ignoreMissing ?? false)) { if ((options.ignoreMissing ?? false)) {
return undefined return undefined
} }
const stringKey = (typeof key === 'string') ? key : key.label const stringKey = key.label ?? key.key
console.error('Failed to find presenter for', key, err) console.error('Failed to find presenter for', key, err)
const errorPresenter: AttributeModel = { const errorPresenter: AttributeModel = {
key: '', key: '',
@ -138,11 +138,17 @@ export async function buildModel (options: BuildModelOptions): Promise<Attribute
return (await Promise.all(model)).filter(a => a !== undefined) as AttributeModel[] return (await Promise.all(model)).filter(a => a !== undefined) as AttributeModel[]
} }
function filterActions (client: Client, _class: Ref<Class<Obj>>, targets: ActionTarget[], derived: Ref<Class<Doc>> = core.class.Doc): Array<Ref<Action>> { function filterActions (client: Client, doc: Doc, targets: ActionTarget[], derived: Ref<Class<Doc>> = core.class.Doc): Array<Ref<Action>> {
const result: Array<Ref<Action>> = [] const result: Array<Ref<Action>> = []
const hierarchy = client.getHierarchy() const hierarchy = client.getHierarchy()
for (const target of targets) { for (const target of targets) {
if (hierarchy.isDerived(_class, target.target) && client.getHierarchy().isDerived(target.target, derived)) { if (target.query !== undefined) {
const r = matchQuery([doc], target.query)
if (r.length === 0) {
continue
}
}
if (hierarchy.isDerived(doc._class, target.target) && client.getHierarchy().isDerived(target.target, derived)) {
result.push(target.action) result.push(target.action)
} }
} }
@ -157,9 +163,9 @@ function filterActions (client: Client, _class: Ref<Class<Obj>>, targets: Action
* So if we have contribution for Doc, Space and we ask for SpaceWithStates and derivedFrom=Space, * So if we have contribution for Doc, Space and we ask for SpaceWithStates and derivedFrom=Space,
* we won't recieve Doc contribution but recieve Space ones. * we won't recieve Doc contribution but recieve Space ones.
*/ */
export async function getActions (client: Client, _class: Ref<Class<Obj>>, derived: Ref<Class<Doc>> = core.class.Doc): Promise<FindResult<Action>> { export async function getActions (client: Client, doc: Doc, derived: Ref<Class<Doc>> = core.class.Doc): Promise<FindResult<Action>> {
const targets = await client.findAll(view.class.ActionTarget, {}) const targets = await client.findAll(view.class.ActionTarget, {})
return await client.findAll(view.class.Action, { _id: { $in: filterActions(client, _class, targets, derived) } }) return await client.findAll(view.class.Action, { _id: { $in: filterActions(client, doc, targets, derived) } })
} }
export async function deleteObject (client: Client & TxOperations, object: Doc): Promise<void> { export async function deleteObject (client: Client & TxOperations, object: Doc): Promise<void> {

View File

@ -14,7 +14,7 @@
// limitations under the License. // limitations under the License.
// //
import type { Class, Client, Doc, FindOptions, Mixin, Obj, Ref, Space, UXObject } from '@anticrm/core' import type { Class, Client, Doc, DocumentQuery, FindOptions, Mixin, Obj, Ref, Space, UXObject } from '@anticrm/core'
import type { Asset, IntlString, Plugin, Resource, Status } from '@anticrm/platform' import type { Asset, IntlString, Plugin, Resource, Status } from '@anticrm/platform'
import { plugin } from '@anticrm/platform' import { plugin } from '@anticrm/platform'
import type { AnyComponent, AnySvelteComponent } from '@anticrm/ui' import type { AnyComponent, AnySvelteComponent } from '@anticrm/ui'
@ -75,9 +75,11 @@ export interface Action extends Doc, UXObject {
/** /**
* @public * @public
*/ */
export interface ActionTarget extends Doc { export interface ActionTarget<T extends Doc=Doc> extends Doc {
target: Ref<Class<Doc>> target: Ref<Class<T>>
action: Ref<Action> action: Ref<Action>
query?: DocumentQuery<T>
} }
/** /**
@ -88,9 +90,10 @@ export const viewId = 'view' as Plugin
/** /**
* @public * @public
*/ */
export type BuildModelKey = string | { export interface BuildModelKey {
presenter: AnyComponent key: string
label: string presenter?: AnyComponent
label?: IntlString
sortingKey?: string sortingKey?: string
} }
@ -113,7 +116,7 @@ export interface AttributeModel {
export interface BuildModelOptions { export interface BuildModelOptions {
client: Client client: Client
_class: Ref<Class<Obj>> _class: Ref<Class<Obj>>
keys: BuildModelKey[] keys: (BuildModelKey | string)[]
options?: FindOptions<Doc> options?: FindOptions<Doc>
ignoreMissing?: boolean ignoreMissing?: boolean
} }

View File

@ -67,7 +67,7 @@
async function getActions (space: Space): Promise<Action[]> { async function getActions (space: Space): Promise<Action[]> {
const result = [editSpace] const result = [editSpace]
const extraActions = await getContributedActions(client, space._class, core.class.Space) const extraActions = await getContributedActions(client, space, core.class.Space)
for (const act of extraActions) { for (const act of extraActions) {
result.push({ result.push({
icon: act.icon ?? IconEdit, icon: act.icon ?? IconEdit,