Tasks MVP

1. Creation of projects
2. Creation of tasks
3. Task table display
4. Task Editing

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
Andrey Sobolev 2021-11-30 22:39:08 +07:00
parent 6b7a4b2474
commit af096a0f66
No known key found for this signature in database
GPG Key ID: BD80F68D68D8F7F2
20 changed files with 695 additions and 91 deletions

View File

@ -9864,7 +9864,7 @@ packages:
dev: false dev: false
file:projects/dev-storage.tgz_typescript@4.4.3: file:projects/dev-storage.tgz_typescript@4.4.3:
resolution: {integrity: sha512-SfTtSgH07SlBF5nTXqS/E5023gdeN5eFmaL4fdrvId5ixenWnSJ16Xcvj02R8nuQ7PAwkNkZMwO+707Ins3GlQ==, tarball: file:projects/dev-storage.tgz} resolution: {integrity: sha512-MI95DMgzOe43SqSrvtR+wgcws3voLsrjvQAS87DGkq15UUxobrixatDGfk5rxFUJsGNpw68s0SQNB+XsYmB4Dw==, tarball: file:projects/dev-storage.tgz}
id: file:projects/dev-storage.tgz id: file:projects/dev-storage.tgz
name: '@rush-temp/dev-storage' name: '@rush-temp/dev-storage'
version: 0.0.0 version: 0.0.0
@ -10267,7 +10267,7 @@ packages:
dev: false dev: false
file:projects/model-task.tgz_typescript@4.4.3: file:projects/model-task.tgz_typescript@4.4.3:
resolution: {integrity: sha512-ECVFsBQDPeXBhwde4kBvnJlE8sXbSuSPOVa76QAdw4poccNaXabQbDUMNKNIquq4kTPGp7GhUUbaDk316zWmcw==, tarball: file:projects/model-task.tgz} resolution: {integrity: sha512-BFTI2YyRTQa+3voYij2r8icUkp5QdKc69N3oPvir4XsfrRrlM90EiDYi0XKk2YCjRtnn+2DEa+pjT8q4CQke5g==, tarball: file:projects/model-task.tgz}
id: file:projects/model-task.tgz id: file:projects/model-task.tgz
name: '@rush-temp/model-task' name: '@rush-temp/model-task'
version: 0.0.0 version: 0.0.0
@ -10494,7 +10494,7 @@ packages:
dev: false dev: false
file:projects/prod.tgz_sass@1.42.1+typescript@4.4.3: file:projects/prod.tgz_sass@1.42.1+typescript@4.4.3:
resolution: {integrity: sha512-jYkFXSc3TaFdljpFFcRvEWTmtHKLA7nQUn+9kSDD9/J91/LmQEv3yEyJH89kb1ETNiTK6vnuLlp9br+RZipWVg==, tarball: file:projects/prod.tgz} resolution: {integrity: sha512-YXR+9ndU1ibNRBgBG2s//HcelDIJmxvXqSPkrZ1A9z8qhjtnYGi7mwqIKpxS+mYVr5nS6JRCNLXcblAp7aDlXA==, tarball: file:projects/prod.tgz}
id: file:projects/prod.tgz id: file:projects/prod.tgz
name: '@rush-temp/prod' name: '@rush-temp/prod'
version: 0.0.0 version: 0.0.0
@ -10868,13 +10868,24 @@ packages:
dev: false dev: false
file:projects/task-resources.tgz_e1367da94684b005adf08f025c517b1a: file:projects/task-resources.tgz_e1367da94684b005adf08f025c517b1a:
resolution: {integrity: sha512-x8gVBObHY9bs86rKxU8Vq0uKMNgjvz5LFQqux6+l8aECuCJjjj70UcQqV+5hBGaY9m5GnlaxAJZed4VntxS4XA==, tarball: file:projects/task-resources.tgz} resolution: {integrity: sha512-dxx9fOd472gS8ILbnRMpBhnd4KOT0xkpQRbSkGHn9sigBeRZ48ZdEXtfagTQSaVlgwhOoCbVm1QrFy67Ctpbcw==, tarball: file:projects/task-resources.tgz}
id: file:projects/task-resources.tgz id: file:projects/task-resources.tgz
name: '@rush-temp/task-resources' name: '@rush-temp/task-resources'
version: 0.0.0 version: 0.0.0
dependencies: dependencies:
'@typescript-eslint/eslint-plugin': 5.4.0_87dbf04088b125598d0271706532eaf3
'@typescript-eslint/parser': 5.4.0_eslint@7.32.0+typescript@4.4.3
eslint: 7.32.0
eslint-config-standard-with-typescript: 21.0.1_05a8ea1454e6ca4c9f98b94b8f3abf9c
eslint-plugin-import: 2.25.3_eslint@7.32.0
eslint-plugin-node: 11.1.0_eslint@7.32.0
eslint-plugin-promise: 5.1.1_eslint@7.32.0
eslint-plugin-svelte3: 3.2.1_eslint@7.32.0+svelte@3.43.1
prettier: 2.4.1
prettier-plugin-svelte: 2.5.0_prettier@2.4.1+svelte@3.43.1
sass: 1.42.1 sass: 1.42.1
svelte: 3.43.1 svelte: 3.43.1
svelte-check: 2.2.10_3708ed3db7329f2cbf76db19160094b1
svelte-loader: 3.1.2_svelte@3.43.1 svelte-loader: 3.1.2_svelte@3.43.1
svelte-preprocess: 4.9.8_b1ccfb371c7d68f75291f5a547be0b14 svelte-preprocess: 4.9.8_b1ccfb371c7d68f75291f5a547be0b14
transitivePeerDependencies: transitivePeerDependencies:
@ -10887,6 +10898,7 @@ packages:
- pug - pug
- stylus - stylus
- sugarss - sugarss
- supports-color
- typescript - typescript
dev: false dev: false
@ -11009,7 +11021,7 @@ packages:
dev: false dev: false
file:projects/tool.tgz_typescript@4.4.3: file:projects/tool.tgz_typescript@4.4.3:
resolution: {integrity: sha512-4mquFHT6NpGnSQiTikfIdCakmJbxLsSCVn7f8X4j8DWwx+GKYLxi8O+NGHuB82dZ76XJoonako1CsiVGvrNpPA==, tarball: file:projects/tool.tgz} resolution: {integrity: sha512-SwaepBCYg7nNx+Vb5vkowQ2QYwf6KLw8iKJGWmm8A261c9geGd8f2Sl2DYyxi9lhG946DRaMxxnQkSfzvNZkRg==, tarball: file:projects/tool.tgz}
id: file:projects/tool.tgz id: file:projects/tool.tgz
name: '@rush-temp/tool' name: '@rush-temp/tool'
version: 0.0.0 version: 0.0.0
@ -11189,7 +11201,7 @@ packages:
dev: false dev: false
file:projects/workspace.tgz_typescript@4.4.3: file:projects/workspace.tgz_typescript@4.4.3:
resolution: {integrity: sha512-Mey3vBH1eqNXrxrhsXI3d66AoIW4hqE34OWI7KN7hqY0nqVzUcex0GHBc78dw7vS+UDkW1Cc2J6rCiLvYQkzfw==, tarball: file:projects/workspace.tgz} resolution: {integrity: sha512-RqzmT9xyUKDthUVU6kMT/yarSmB0yPv2q4VI6cJ+O/Iw7HiuKhTUV7ObyA6AJPIYiHZIGOqM4EMrMvX2G8rWNA==, tarball: file:projects/workspace.tgz}
id: file:projects/workspace.tgz id: file:projects/workspace.tgz
name: '@rush-temp/workspace' name: '@rush-temp/workspace'
version: 0.0.0 version: 0.0.0

View File

@ -20,7 +20,7 @@ import type { AnyComponent } from '@anticrm/ui'
import type { Action } from '@anticrm/view' import type { Action } from '@anticrm/view'
import { recruitId } from '@anticrm/recruit' import { recruitId } from '@anticrm/recruit'
import recruit from '@anticrm/recruit-resources/src/plugin' import recruit from '@anticrm/recruit-resources/src/plugin'
import { TxViewlet } from '../../chunter/node_modules/@anticrm/activity/lib' import { TxViewlet } from '@anticrm/activity'
export default mergeIds(recruitId, recruit, { export default mergeIds(recruitId, recruit, {
action: { action: {

View File

@ -28,6 +28,8 @@
"@anticrm/model-workbench": "~0.6.1", "@anticrm/model-workbench": "~0.6.1",
"@anticrm/model-contact": "~0.6.1", "@anticrm/model-contact": "~0.6.1",
"@anticrm/task": "~0.6.0", "@anticrm/task": "~0.6.0",
"@anticrm/task-resources": "~0.6.0" "@anticrm/task-resources": "~0.6.0",
"@anticrm/model-chunter": "~0.6.0",
"@anticrm/workbench": "~0.6.1"
} }
} }

View File

@ -13,34 +13,42 @@
// limitations under the License. // limitations under the License.
// //
import type { IntlString } from '@anticrm/platform'
import { Builder, Model, TypeString, UX, Prop } from '@anticrm/model'
import type { Ref, Doc, FindOptions } from '@anticrm/core'
import core, { TSpace, TDoc } from '@anticrm/model-core'
import type { Project, Task } from '@anticrm/task'
import type { Employee } from '@anticrm/contact' import type { Employee } from '@anticrm/contact'
import type { AnyComponent } from '@anticrm/ui' import contact from '@anticrm/contact'
import type { Doc, Domain, FindOptions, Ref } from '@anticrm/core'
import workbench from '@anticrm/model-workbench' import { Builder, Model, Prop, TypeString, UX } from '@anticrm/model'
import chunter from '@anticrm/model-chunter'
import core, { TDoc, TSpace } from '@anticrm/model-core'
import view from '@anticrm/model-view' import view from '@anticrm/model-view'
import contact from '@anticrm/model-contact' import workbench from '@anticrm/model-workbench'
import type { IntlString } from '@anticrm/platform'
import type { Project, Task } from '@anticrm/task'
import task from './plugin' import task from './plugin'
@Model(task.class.Project, core.class.Space) @Model(task.class.Project, core.class.Space)
@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 TSpace implements Project {}
@Model(task.class.Task, core.class.Doc) @Model(task.class.Task, core.class.Doc, 'task' as Domain)
@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(), 'Title' as IntlString) @Prop(TypeString(), 'No.' as IntlString)
title!: string number!: number
@Prop(TypeString(), 'Name' as IntlString)
name!: string
@Prop(TypeString(), 'Description' as IntlString) @Prop(TypeString(), 'Description' as IntlString)
description!: string description!: string
@Prop(TypeString(), 'Assignee' as IntlString) @Prop(TypeString(), 'Assignee' as IntlString)
assignee!: Ref<Employee> assignee!: Ref<Employee>
@Prop(TypeString(), 'Comments' as IntlString)
comments!: number
@Prop(TypeString(), 'Labels' as IntlString)
labels!: string
} }
export function createModel (builder: Builder): void { export function createModel (builder: Builder): void {
@ -52,19 +60,6 @@ export function createModel (builder: Builder): void {
} }
}) })
builder.createDoc(view.class.Viewlet, core.space.Model, {
attachTo: task.class.Task,
descriptor: view.viewlet.Table,
open: 'ZX' as AnyComponent,
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
options: {
lookup: {
assignee: contact.class.Employee
}
} as FindOptions<Doc>,
config: ['title', '$lookup.assignee']
})
builder.createDoc(workbench.class.Application, core.space.Model, { builder.createDoc(workbench.class.Application, core.space.Model, {
label: task.string.ApplicationLabelTask, label: task.string.ApplicationLabelTask,
icon: task.icon.Task, icon: task.icon.Task,
@ -79,11 +74,45 @@ export function createModel (builder: Builder): void {
} }
] ]
} }
}, task.app.Tasks)
builder.createDoc(view.class.Viewlet, core.space.Model, {
attachTo: task.class.Task,
descriptor: view.viewlet.Table,
open: task.component.EditTask,
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
options: {
lookup: {
assignee: contact.class.Employee
}
} as FindOptions<Doc>,
config: [
'',
'name',
'$lookup.assignee',
{ presenter: chunter.component.AttachmentsPresenter, label: 'Files' },
{ presenter: chunter.component.CommentsPresenter, label: 'Comments' },
'modifiedOn'
]
}) })
builder.mixin(task.class.Task, core.class.Class, view.mixin.AttributePresenter, {
presenter: task.component.TaskPresenter
})
builder.mixin(task.class.Task, core.class.Class, view.mixin.ObjectEditor, {
editor: task.component.EditTask
})
builder.createDoc(task.class.Project, core.space.Model, { builder.createDoc(task.class.Project, core.space.Model, {
name: 'demo', name: 'public',
description: 'Demo Project', description: 'Public tasks',
private: false, private: false,
members: [] members: []
}, task.space.TasksPublic)
builder.createDoc(view.class.Sequence, view.space.Sequence, {
attachedTo: task.class.Task,
sequence: 0
}) })
} }

View File

@ -14,26 +14,31 @@
// limitations under the License. // limitations under the License.
// //
import { taskId } from '@anticrm/task' import type { Ref, Space } from '@anticrm/core'
import task from '@anticrm/task-resources/src/plugin'
import type { IntlString } from '@anticrm/platform' import type { IntlString } from '@anticrm/platform'
import type { Ref, Class } from '@anticrm/core'
import type { Project } from '@anticrm/task'
import type { AnyComponent } from '@anticrm/ui'
import { mergeIds } from '@anticrm/platform' import { mergeIds } from '@anticrm/platform'
import task, { taskId } from '@anticrm/task'
import type { AnyComponent } from '@anticrm/ui'
import { Application } from '@anticrm/workbench'
export default mergeIds(taskId, task, { export default mergeIds(taskId, task, {
app: {
Tasks: '' as Ref<Application>
},
component: { component: {
ProjectView: '' as AnyComponent, ProjectView: '' as AnyComponent,
CreateProject: '' as AnyComponent, CreateProject: '' as AnyComponent,
CreateTask: '' as AnyComponent CreateTask: '' as AnyComponent,
}, EditTask: '' as AnyComponent,
class: { TaskPresenter: '' as AnyComponent
Project: '' as Ref<Class<Project>>
}, },
string: { string: {
Task: '' as IntlString,
ApplicationLabelTask: '' as IntlString, ApplicationLabelTask: '' as IntlString,
Projects: '' as IntlString, Projects: '' as IntlString,
CreateProject: '' as IntlString CreateProject: '' as IntlString
},
space: {
TasksPublic: '' as Ref<Space>
} }
}) })

View File

@ -2,6 +2,19 @@
"string": { "string": {
"ApplicationLabelTask": "Tasks", "ApplicationLabelTask": "Tasks",
"Projects": "Projects", "Projects": "Projects",
"CreateProject": "New Project" "CreateProject": "New Project",
"ProjectName": "Project",
"MakePrivate": "Make Private",
"MakePrivateDescription": "Only members can see it",
"CreateTask": "Create task",
"TaskProject": "selected project",
"SelectProject": "Select project",
"Task": "Task",
"TaskName": "Task name *",
"TaskAssignee": "Assignee",
"TaskDescription": "Description",
"UploadDropFilesHere": "Upload or drop files here",
"NoAttachmentsForTask": "There are no attachments for this task.",
"AssigneeRequired": "Assignee is required"
} }
} }

View File

@ -13,10 +13,12 @@
// limitations under the License. // limitations under the License.
// //
import { loadMetadata } from '@anticrm/platform' import { addStringsLoader, loadMetadata } from '@anticrm/platform'
import task from '@anticrm/task' import task, {taskId} from '@anticrm/task'
const icons = require('../assets/icons.svg') const icons = require('../assets/icons.svg')
loadMetadata(task.icon, { loadMetadata(task.icon, {
Task: `${icons}#task`, Task: `${icons}#task`,
}) })
addStringsLoader(taskId, async (lang: string) => await import(`../lang/${lang}.json`))

View File

@ -0,0 +1,7 @@
module.exports = {
extends: ['./node_modules/@anticrm/platform-rig/profiles/ui/config/eslint.config.json'],
parserOptions: { tsconfigRootDir: __dirname },
settings: {
'svelte3/ignore-styles': () => true
}
};

View File

@ -14,7 +14,19 @@
"devDependencies": { "devDependencies": {
"svelte-loader":"^3.1.2", "svelte-loader":"^3.1.2",
"sass":"^1.37.5", "sass":"^1.37.5",
"svelte-preprocess":"^4.7.4" "svelte-preprocess":"^4.7.4",
"@anticrm/platform-rig":"~0.6.0",
"@typescript-eslint/eslint-plugin": "^5.4.0",
"@typescript-eslint/parser": "^5.4.0",
"eslint-config-standard-with-typescript": "^21.0.1",
"eslint-plugin-import": "^2.25.3",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^5.1.1",
"eslint-plugin-svelte3": "~3.2.1",
"prettier-plugin-svelte": "^2.2.0",
"eslint": "^7.32.0",
"prettier": "^2.4.1",
"svelte-check": "^2.2.10"
}, },
"dependencies": { "dependencies": {
"@anticrm/platform": "~0.6.5", "@anticrm/platform": "~0.6.5",
@ -24,6 +36,11 @@
"@anticrm/presentation": "~0.6.2", "@anticrm/presentation": "~0.6.2",
"@anticrm/text-editor": "~0.6.0", "@anticrm/text-editor": "~0.6.0",
"@anticrm/contact": "~0.6.2", "@anticrm/contact": "~0.6.2",
"@anticrm/core": "~0.6.11" "@anticrm/core": "~0.6.11",
"@anticrm/chunter": "~0.6.1",
"@anticrm/panel": "~0.6.0",
"@anticrm/view": "~0.6.0",
"@anticrm/view-resources": "~0.6.0",
"@anticrm/login": "~0.6.1"
} }
} }

View File

@ -0,0 +1,138 @@
<!--
// 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 { Attachment } from '@anticrm/chunter'
import chunter from '@anticrm/chunter'
import type { Class, Doc, Ref, Space } from '@anticrm/core'
import { setPlatformStatus, unknownError } from '@anticrm/platform'
import { createQuery, getClient } from '@anticrm/presentation'
import { CircleButton, IconAdd, Label, Spinner } from '@anticrm/ui'
import { Table } from '@anticrm/view-resources'
import { uploadFile } from '../utils'
import UploadDuo from './icons/UploadDuo.svelte'
import task from '../plugin'
export let objectId: Ref<Doc>
export let space: Ref<Space>
export let _class: Ref<Class<Doc>>
let attachments: Attachment[] = []
const query = createQuery()
$: query.query(chunter.class.Attachment, { attachedTo: objectId }, result => { attachments = result })
let inputFile: HTMLInputElement
let loading = 0
const client = getClient()
async function createAttachment (file: File) {
loading++
try {
const uuid = await uploadFile(space, file, objectId)
console.log('uploaded file uuid', uuid)
client.addCollection(chunter.class.Attachment, space, objectId, _class, 'attachments', {
name: file.name,
file: uuid,
type: file.type,
size: file.size,
lastModified: file.lastModified
})
} catch (err: any) {
setPlatformStatus(unknownError(err))
} finally {
loading--
}
}
function fileSelected () {
console.log(inputFile.files)
const list = inputFile.files
if (list === null || list.length === 0) return
for (let index = 0; index < list.length; index++) {
const file = list.item(index)
if (file !== null) createAttachment(file)
}
}
function fileDrop (e: DragEvent) {
const list = e.dataTransfer?.files
if (list === undefined || list.length === 0) return
for (let index = 0; index < list.length; index++) {
const file = list.item(index)
if (file !== null) createAttachment(file)
}
}
let dragover = false
</script>
<div class="attachments-container">
<div class="flex-row-center">
<span class="title">Attachments</span>
{#if loading}
<Spinner/>
{:else}
<CircleButton icon={IconAdd} size={'small'} on:click={ () => { inputFile.click() } } />
{/if}
<input bind:this={inputFile} multiple type="file" name="file" id="file" style="display: none" on:change={fileSelected}/>
</div>
{#if attachments.length === 0 && !loading}
<div class="flex-col-center mt-5 zone-container" class:solid={dragover}
on:dragover|preventDefault={ () => { dragover = true } }
on:dragleave={ () => { dragover = false } }
on:drop|preventDefault|stopPropagation={fileDrop}
>
<UploadDuo size={'large'} />
<div class="small-text content-dark-color mt-2">
<Label label={task.string.NoAttachmentsForTask} />
</div>
<div class="small-text">
<a href={'#'} on:click={() => inputFile.click()}><Label label={task.string.UploadDropFilesHere} /></a>
</div>
</div>
{:else}
<Table
_class={chunter.class.Attachment}
config={['', 'lastModified']}
options={ {} }
query={ { attachedTo: objectId } }
/>
{/if}
</div>
<style lang="scss">
.attachments-container {
display: flex;
flex-direction: column;
.title {
margin-right: .75rem;
font-weight: 500;
font-size: 1.25rem;
color: var(--theme-caption-color);
}
}
.zone-container {
padding: 1rem;
color: var(--theme-caption-color);
background: rgba(255, 255, 255, .03);
border: 1px dashed rgba(255, 255, 255, .16);
border-radius: .75rem;
}
</style>

View File

@ -0,0 +1,56 @@
<!--
// 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 { IconFolder, EditBox, ToggleWithLabel, Grid } from '@anticrm/ui'
import { getClient, SpaceCreateCard } from '@anticrm/presentation'
import task from '../plugin'
import core from '@anticrm/core'
const dispatch = createEventDispatcher()
let name: string = ''
let description: string = ''
export function canClose(): boolean {
return name === ''
}
const client = getClient()
function createProject() {
client.createDoc(task.class.Project, core.space.Model, {
name,
description,
private: false,
members: []
})
}
</script>
<SpaceCreateCard
label={task.string.CreateProject}
okAction={createProject}
canSave={name ? true : false}
on:close={() => { dispatch('close') }}
>
<Grid column={1} rowGap={1.5}>
<EditBox label={task.string.ProjectName} icon={IconFolder} bind:value={name} placeholder={'Project name'} focus/>
<ToggleWithLabel label={task.string.MakePrivate} description={task.string.MakePrivateDescription}/>
</Grid>
</SpaceCreateCard>

View File

@ -14,46 +14,119 @@
--> -->
<script lang="ts"> <script lang="ts">
import contact, { Employee, EmployeeAccount } from '@anticrm/contact'
import type { Data, Ref, Space } from '@anticrm/core'
import { generateId } from '@anticrm/core'
import { Card, getClient, UserBox } from '@anticrm/presentation'
import { Task } from '@anticrm/task'
import { EditBox, Grid, Status as StatusControl } from '@anticrm/ui'
import { Status, OK, Severity } from '@anticrm/platform'
import view from '@anticrm/view'
import { createEventDispatcher } from 'svelte' import { createEventDispatcher } from 'svelte'
import type { Ref, Space } from '@anticrm/core'
import { DatePicker, EditBox, Dialog, Tabs, Section, Grid, Row, TextArea, IconFile } from '@anticrm/ui'
import { UserBox } from '@anticrm/presentation'
import { ReferenceInput } from '@anticrm/text-editor'
import type { Person } from '@anticrm/contact'
import { getClient } from '@anticrm/presentation'
import contact from '@anticrm/contact'
import task from '../plugin' import task from '../plugin'
export let space: Ref<Space> export let space: Ref<Space>
let _space = space
const status: Status = OK
let assignee: Ref<EmployeeAccount> // | null = null
const object: Data<Task> = {
name: '',
description: '',
assignee: undefined as unknown as Ref<Employee>,
number: 0
}
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
let title: string
let assignee: Ref<Person>
const client = getClient() const client = getClient()
const taskId = generateId()
function createCandidate() { export function canClose (): boolean {
client.createDoc(task.class.Task, space, { return object.name !== ''
title, }
async function createTask () {
const sequence = await client.findOne(view.class.Sequence, { attachedTo: task.class.Task })
if (sequence === undefined) {
throw new Error('sequence object not found')
}
const incResult = await client.updateDoc(view.class.Sequence, view.space.Sequence, sequence._id, {
$inc: { sequence: 1 }
}, true)
const value: Data<Task> = {
name: object.name,
description: object.description,
assignee, assignee,
}) number: (incResult as any).object.sequence
}
await client.createDoc(task.class.Task, _space, value, taskId)
dispatch('close')
} }
</script> </script>
<Dialog label={'Create Task'} <!-- <DialogHeader {space} {object} {newValue} {resume} create={true} on:save={createCandidate}/> -->
okLabel={'Create Task'}
okAction={createCandidate} <Card label={task.string.CreateTask}
on:close={() => { dispatch('close') }}> okAction={createTask}
<Tabs/> canSave={object.name.length > 0 && assignee !== undefined}
<Section icon={IconFile} label={'General Information'}> spaceClass={task.class.Project}
<Grid> spaceLabel={task.string.ProjectName}
<Row><EditBox label={'Title *'} placeholder={'The Secret Project'} bind:value={title} focus /></Row> spacePlaceholder={task.string.SelectProject}
<UserBox _class={contact.class.Person} title='Assignee' caption='Employees' bind:value={assignee} /> bind:space={_space}
<DatePicker title={'Pick due date'} /> on:close={() => { dispatch('close') }}>
<Row><ReferenceInput /></Row> <StatusControl slot="error" {status} />
</Grid> <Grid column={1} rowGap={1.5}>
</Section> <EditBox label={task.string.TaskName} bind:value={object.name} icon={task.icon.Task} placeholder="The boring task" maxWidth="39rem" focus/>
</Dialog> <UserBox _class={contact.class.EmployeeAccount} title='Assignee *' caption='Assign this task' bind:value={assignee} />
</Grid>
</Card>
<style lang="scss">
.channels {
margin-top: 1.25rem;
span { margin-left: .5rem; }
}
.locations {
span {
margin-bottom: .125rem;
font-weight: 500;
font-size: .75rem;
color: var(--theme-content-accent-color);
}
.row {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: .75rem;
color: var(--theme-caption-color);
}
}
.separator {
margin: 1rem 0;
height: 1px;
background-color: var(--theme-card-divider);
}
.resume {
margin-top: 1rem;
padding: .75rem;
background: rgba(255, 255, 255, .05);
border: 1px dashed rgba(255, 255, 255, .2);
border-radius: .5rem;
backdrop-filter: blur(10px);
&.solid { border-style: solid; }
}
// .resume a {
// font-size: .75rem;
// color: var(--theme-content-dark-color);
// &:hover { color: var(--theme-content-color); }
// }
</style>

View File

@ -0,0 +1,68 @@
<!--
// 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 } from '@anticrm/core'
import { Panel } from '@anticrm/panel'
import { createQuery, getClient } from '@anticrm/presentation'
import type { Task } from '@anticrm/task'
import { EditBox, Grid } from '@anticrm/ui'
import view from '@anticrm/view'
import { createEventDispatcher } from 'svelte'
import task from '../plugin'
import Attachments from './Attachments.svelte'
import TaskHeader from './TaskHeader.svelte'
export let _id: Ref<Task>
let object: Task
const query = createQuery()
$: query.query(task.class.Task, { _id }, result => { object = result[0] })
const dispatch = createEventDispatcher()
const client = getClient()
function change (field: string, value: any) {
client.updateDoc(task.class.Task, object.space, object._id, { [field]: value })
}
</script>
{#if object !== undefined}
<Panel icon={view.icon.Table} title={object.name} {object} on:close={() => { dispatch('close') }}>
<TaskHeader {object} slot="subtitle" />
<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 label={task.string.TaskDescription} bind:value={object.description} icon={task.icon.Task} placeholder="Description" maxWidth="39rem" on:change={(evt) => change('description', object.description)}/>
</Grid>
<div class="mt-14">
<Attachments objectId={object._id} _class={object._class} space={object.space} />
</div>
</Panel>
{/if}
<style lang="scss">
.attachments {
margin-top: 3.5rem;
}
.grid-cards {
display: grid;
grid-template-columns: 1fr 1fr;
column-gap: 1.5rem;
}
</style>

View File

@ -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 contact from '@anticrm/contact'
import { getClient, UserBox } from '@anticrm/presentation'
import { Task } from '@anticrm/task'
import task from '../plugin'
export let object: Task
const client = getClient()
function change () {
client.updateDoc(object._class, object.space, object._id, { assignee: object.assignee })
}
</script>
<div class="flex-between header">
<UserBox _class={contact.class.Employee} title={task.string.TaskAssignee} caption='Assignee' bind:value={object.assignee} on:change={change} />
<!-- <AttributeBarEditor key={'state'} {object} showHeader={false} /> -->
</div>
<style lang="scss">
.header {
width: 100%;
padding: 0 .5rem;
}
</style>

View File

@ -0,0 +1,39 @@
<!--
// 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 { 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'
export let value: Task
const client = getClient()
const shortLabel = client.getHierarchy().getClass(value._class).shortLabel
function show () {
closeTooltip()
showPopup(EditTask, { _id: value._id }, 'full')
}
</script>
<div class="sm-tool-icon" on:click={show}>
<span class="icon"><Icon icon={task.icon.Task} size={'small'}/></span>{shortLabel}-{value.number}
</div>

View File

@ -0,0 +1,28 @@
<!--
// 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">
export let size: 'small' | 'medium' | 'large'
const fill: string = 'currentColor'
</script>
<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"/>
<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 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>
</svg>

View File

@ -14,10 +14,16 @@
// limitations under the License. // limitations under the License.
// //
import CreateTask from './components/CreateTask.svelte' import { Resources } from '@anticrm/platform'
export default async () => ({ import CreateTask from './components/CreateTask.svelte'
import CreateProject from './components/CreateProject.svelte'
import TaskPresenter from './components/TaskPresenter.svelte'
export default async (): Promise<Resources> => ({
component: { component: {
CreateTask CreateTask,
}, CreateProject,
TaskPresenter
}
}) })

View File

@ -13,9 +13,28 @@
// limitations under the License. // limitations under the License.
// //
import { mergeIds } from '@anticrm/platform' import { IntlString, mergeIds } from '@anticrm/platform'
import task, { taskId } from '@anticrm/task' import task, { taskId } from '@anticrm/task'
export default mergeIds(taskId, task, { export default mergeIds(taskId, task, {
string: {
ApplicationLabelTask: '' as IntlString,
Projects: '' as IntlString,
CreateProject: '' as IntlString,
ProjectName: '' as IntlString,
MakePrivate: '' as IntlString,
MakePrivateDescription: '' as IntlString,
CreateTask: '' as IntlString,
TaskProject: '' as IntlString,
SelectProject: '' as IntlString,
TaskName: '' as IntlString,
TaskAssignee: '' as IntlString,
TaskDescription: '' as IntlString,
NoAttachmentsForTask: '' as IntlString,
UploadDropFilesHere: '' as IntlString
},
status: {
AssigneeRequired: '' as IntlString
}
}) })

View File

@ -0,0 +1,42 @@
//
// 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.
//
import type { Doc, Ref, Space } from '@anticrm/core'
import login from '@anticrm/login'
import { getMetadata } from '@anticrm/platform'
export async function uploadFile (space: Ref<Space>, file: File, attachedTo: Ref<Doc>): Promise<string> {
console.log(file)
const uploadUrl = getMetadata(login.metadata.UploadUrl)
const data = new FormData()
data.append('file', file)
const url = `${uploadUrl as string}?space=${space}&name=${encodeURIComponent(file.name)}&attachedTo=${attachedTo}`
const resp = await fetch(url, {
method: 'POST',
headers: {
Authorization: 'Bearer ' + (getMetadata(login.metadata.LoginToken) as string)
},
body: data
})
if (resp.status !== 200) {
throw new Error('Can\'t upload file.')
}
const uuid = await resp.text()
console.log(uuid)
return uuid
}

View File

@ -13,10 +13,10 @@
// limitations under the License. // limitations under the License.
// //
import { plugin } from '@anticrm/platform'
import type { Plugin, Asset } from '@anticrm/platform'
import type { Space, Doc, Ref, Class } from '@anticrm/core'
import type { Employee } from '@anticrm/contact' import type { Employee } from '@anticrm/contact'
import type { Class, Doc, Ref, Space } from '@anticrm/core'
import type { Asset, Plugin } from '@anticrm/platform'
import { plugin } from '@anticrm/platform'
/** /**
* @public * @public
@ -27,9 +27,15 @@ export interface Project extends Space {}
* @public * @public
*/ */
export interface Task extends Doc { export interface Task extends Doc {
title: string number: number // Sequence number
name: string
description: string description: string
assignee: Ref<Employee> assignee: Ref<Employee>
comments?: number
attachments?: number
labels?: string
} }
/** /**
@ -39,7 +45,8 @@ export const taskId = 'task' as Plugin
export default plugin(taskId, { export default plugin(taskId, {
class: { class: {
Task: '' as Ref<Class<Task>> Task: '' as Ref<Class<Task>>,
Project: '' as Ref<Class<Project>>
}, },
icon: { icon: {
Task: '' as Asset Task: '' as Asset