Universal UI for Tasks (#659)

Signed-off-by: Denis Bykhov <80476319+BykhovDenis@users.noreply.github.com>
This commit is contained in:
Denis Bykhov 2021-12-17 15:04:49 +06:00 committed by GitHub
parent b31202b34b
commit 531343d17b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 509 additions and 347 deletions

View File

@ -152,7 +152,7 @@ export function createModel (builder: Builder): void {
builder.createDoc(view.class.Viewlet, core.space.Model, {
attachTo: contact.class.Person,
descriptor: view.viewlet.Table,
open: contact.component.EditPerson,
open: contact.component.EditContact,
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
options: {},
config: [
@ -167,7 +167,7 @@ export function createModel (builder: Builder): void {
builder.createDoc(view.class.Viewlet, core.space.Model, {
attachTo: contact.class.Organization,
descriptor: view.viewlet.Table,
open: contact.component.EditOrganization,
open: contact.component.EditContact,
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
options: {},
config: ['', { presenter: attachment.component.AttachmentsPresenter, label: 'Files' }, 'modifiedOn', 'channels']

View File

@ -27,6 +27,7 @@ export const ids = mergeIds(contactId, contact, {
ChannelsPresenter: '' as AnyComponent,
CreatePerson: '' as AnyComponent,
EditPerson: '' as AnyComponent,
EditContact: '' as AnyComponent,
EditOrganization: '' as AnyComponent,
CreateOrganization: '' as AnyComponent,
CreatePersons: '' as AnyComponent,

View File

@ -28,7 +28,7 @@ import task, { TSpaceWithStates, TTask } from '@anticrm/model-task'
import view from '@anticrm/model-view'
import workbench from '@anticrm/model-workbench'
import type { IntlString } from '@anticrm/platform'
import type { } from '@anticrm/view'
import type {} from '@anticrm/view'
import lead from './plugin'
@Model(lead.class.Funnel, task.class.SpaceWithStates)
@ -36,7 +36,7 @@ import lead from './plugin'
export class TFunnel extends TSpaceWithStates implements Funnel {}
@Model(lead.class.Lead, task.class.Task)
@UX('Lead' as IntlString)
@UX('Lead' as IntlString, lead.icon.Lead)
export class TLead extends TTask implements Lead {
@Prop(TypeString(), 'Title' as IntlString)
title!: string
@ -61,32 +61,42 @@ export function createModel (builder: Builder): void {
}
})
builder.createDoc(workbench.class.Application, core.space.Model, {
label: lead.string.LeadApplication,
icon: lead.icon.LeadApplication,
hidden: false,
navigatorModel: {
spaces: [
{
label: lead.string.Funnels,
spaceClass: lead.class.Funnel,
addSpaceLabel: lead.string.CreateFunnel,
createComponent: lead.component.CreateFunnel
}
]
}
}, lead.app.Lead)
builder.createDoc(lead.class.Funnel, core.space.Model, {
name: 'Funnel',
description: 'Default funnel',
private: false,
members: []
}, lead.space.DefaultFunnel)
builder.createDoc(
workbench.class.Application,
core.space.Model,
{
label: lead.string.LeadApplication,
icon: lead.icon.LeadApplication,
hidden: false,
navigatorModel: {
spaces: [
{
label: lead.string.Funnels,
spaceClass: lead.class.Funnel,
addSpaceLabel: lead.string.CreateFunnel,
createComponent: lead.component.CreateFunnel
}
]
}
},
lead.app.Lead
)
builder.createDoc(
lead.class.Funnel,
core.space.Model,
{
name: 'Funnel',
description: 'Default funnel',
private: false,
members: []
},
lead.space.DefaultFunnel
)
builder.createDoc(view.class.Viewlet, core.space.Model, {
attachTo: lead.class.Lead,
descriptor: view.viewlet.Table,
open: lead.component.EditLead,
open: contact.component.EditContact,
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
options: {
lookup: {
@ -101,13 +111,14 @@ export function createModel (builder: Builder): void {
{ presenter: attachment.component.AttachmentsPresenter, label: 'Files' },
{ presenter: chunter.component.CommentsPresenter, label: 'Comments' },
'modifiedOn',
'$lookup.customer.channels']
'$lookup.customer.channels'
]
})
builder.createDoc(view.class.Viewlet, core.space.Model, {
attachTo: lead.class.Lead,
descriptor: task.viewlet.Kanban,
open: lead.component.EditLead,
open: contact.component.EditContact,
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
options: {
lookup: {
@ -135,13 +146,18 @@ export function createModel (builder: Builder): void {
sequence: 0
})
builder.createDoc(task.class.KanbanTemplateSpace, core.space.Model, {
name: 'Funnels',
description: 'Manage funnel statuses',
members: [],
private: false,
icon: lead.component.TemplatesIcon
}, lead.space.FunnelTemplates)
builder.createDoc(
task.class.KanbanTemplateSpace,
core.space.Model,
{
name: 'Funnels',
description: 'Manage funnel statuses',
members: [],
private: false,
icon: lead.component.TemplatesIcon
},
lead.space.FunnelTemplates
)
createKanban(lead.space.DefaultFunnel, async (_class, space, data, id) => {
builder.createDoc(_class, space, data, id)

View File

@ -70,7 +70,7 @@ export class TCandidate extends TPerson implements Candidate {
}
@Model(recruit.class.Applicant, task.class.Task)
@UX('Application' as IntlString, recruit.icon.RecruitApplication, 'APP' as IntlString)
@UX('Application' as IntlString, recruit.icon.Application, 'APP' as IntlString)
export class TApplicant extends TTask implements Applicant {
// We need to declare, to provide property with label
@Prop(TypeRef(recruit.class.Candidate), 'Candidate' as IntlString)
@ -107,28 +107,33 @@ export function createModel (builder: Builder): void {
editor: recruit.component.Applications
})
builder.createDoc(workbench.class.Application, core.space.Model, {
label: recruit.string.RecruitApplication,
icon: recruit.icon.RecruitApplication,
hidden: false,
navigatorModel: {
spaces: [
{
label: recruit.string.Vacancies,
spaceClass: recruit.class.Vacancy,
addSpaceLabel: recruit.string.CreateVacancy,
createComponent: recruit.component.CreateVacancy,
component: recruit.component.EditVacancy
},
{
label: recruit.string.Candidates,
spaceClass: recruit.class.Candidates,
addSpaceLabel: recruit.string.CreateCandidates,
createComponent: recruit.component.CreateCandidates
}
]
}
}, recruit.app.Recruit)
builder.createDoc(
workbench.class.Application,
core.space.Model,
{
label: recruit.string.RecruitApplication,
icon: recruit.icon.RecruitApplication,
hidden: false,
navigatorModel: {
spaces: [
{
label: recruit.string.Vacancies,
spaceClass: recruit.class.Vacancy,
addSpaceLabel: recruit.string.CreateVacancy,
createComponent: recruit.component.CreateVacancy,
component: recruit.component.EditVacancy
},
{
label: recruit.string.Candidates,
spaceClass: recruit.class.Candidates,
addSpaceLabel: recruit.string.CreateCandidates,
createComponent: recruit.component.CreateCandidates
}
]
}
},
recruit.app.Recruit
)
builder.createDoc(
recruit.class.Candidates,
core.space.Model,
@ -144,7 +149,7 @@ export function createModel (builder: Builder): void {
builder.createDoc(view.class.Viewlet, core.space.Model, {
attachTo: recruit.class.Candidate,
descriptor: view.viewlet.Table,
open: recruit.component.EditCandidate,
open: contact.component.EditContact,
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
options: {
// lookup: {
@ -166,7 +171,7 @@ export function createModel (builder: Builder): void {
builder.createDoc(view.class.Viewlet, core.space.Model, {
attachTo: recruit.class.Applicant,
descriptor: view.viewlet.Table,
open: recruit.component.EditCandidate,
open: contact.component.EditContact,
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
options: {
lookup: {
@ -189,7 +194,7 @@ export function createModel (builder: Builder): void {
builder.createDoc(view.class.Viewlet, core.space.Model, {
attachTo: recruit.class.Applicant,
descriptor: task.viewlet.Kanban,
open: recruit.component.EditCandidate,
open: contact.component.EditContact,
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
options: {
lookup: {
@ -208,6 +213,10 @@ export function createModel (builder: Builder): void {
editor: recruit.component.EditCandidate
})
builder.mixin(recruit.class.Applicant, core.class.Class, view.mixin.ObjectEditor, {
editor: recruit.component.EditApplication
})
builder.mixin(recruit.class.Applicant, core.class.Class, view.mixin.AttributePresenter, {
presenter: recruit.component.ApplicationPresenter
})
@ -238,13 +247,18 @@ export function createModel (builder: Builder): void {
sequence: 0
})
builder.createDoc(task.class.KanbanTemplateSpace, core.space.Model, {
name: 'Vacancies',
description: 'Manage vacancy statuses',
members: [],
private: false,
icon: recruit.component.TemplatesIcon
}, recruit.space.VacancyTemplates)
builder.createDoc(
task.class.KanbanTemplateSpace,
core.space.Model,
{
name: 'Vacancies',
description: 'Manage vacancy statuses',
members: [],
private: false,
icon: recruit.component.TemplatesIcon
},
recruit.space.VacancyTemplates
)
}
export { recruitOperation } from './migration'

View File

@ -50,6 +50,7 @@ export default mergeIds(recruitId, recruit, {
ApplicationPresenter: '' as AnyComponent,
ApplicationsPresenter: '' as AnyComponent,
EditVacancy: '' as AnyComponent,
EditApplication: '' as AnyComponent,
TemplatesIcon: '' as AnyComponent,
Applications: '' as AnyComponent
},

View File

@ -26,7 +26,24 @@ import core, { TAttachedDoc, TClass, TDoc, TSpace } from '@anticrm/model-core'
import view from '@anticrm/model-view'
import workbench from '@anticrm/model-workbench'
import type { IntlString } from '@anticrm/platform'
import type { Kanban, KanbanCard, Project, State, Issue, Sequence, DoneState, WonState, LostState, KanbanTemplateSpace, StateTemplate, DoneStateTemplate, WonStateTemplate, LostStateTemplate, KanbanTemplate, Task } from '@anticrm/task'
import type {
Kanban,
KanbanCard,
Project,
State,
Issue,
Sequence,
DoneState,
WonState,
LostState,
KanbanTemplateSpace,
StateTemplate,
DoneStateTemplate,
WonStateTemplate,
LostStateTemplate,
KanbanTemplate,
Task
} from '@anticrm/task'
import { createProjectKanban } from '@anticrm/task'
import task from './plugin'
import { AnyComponent } from '@anticrm/ui'
@ -77,8 +94,7 @@ export class TTask extends TAttachedDoc implements Task {
}
@Model(task.class.SpaceWithStates, core.class.Space)
export class TSpaceWithStates extends TSpace {
}
export class TSpaceWithStates extends TSpace {}
@Model(task.class.Project, task.class.SpaceWithStates)
@UX('Project' as IntlString, task.icon.Task)
@ -188,7 +204,8 @@ export function createModel (builder: Builder): void {
TTask,
TSpaceWithStates,
TProject,
TIssue)
TIssue
)
builder.mixin(task.class.Project, core.class.Class, workbench.mixin.SpaceView, {
view: {
class: task.class.Issue,
@ -196,21 +213,26 @@ export function createModel (builder: Builder): void {
}
})
builder.createDoc(workbench.class.Application, core.space.Model, {
label: task.string.ApplicationLabelTask,
icon: task.icon.Task,
hidden: false,
navigatorModel: {
spaces: [
{
label: task.string.Projects,
spaceClass: task.class.Project,
addSpaceLabel: task.string.CreateProject,
createComponent: task.component.CreateProject
}
]
}
}, task.app.Tasks)
builder.createDoc(
workbench.class.Application,
core.space.Model,
{
label: task.string.ApplicationLabelTask,
icon: task.icon.Task,
hidden: false,
navigatorModel: {
spaces: [
{
label: task.string.Projects,
spaceClass: task.class.Project,
addSpaceLabel: task.string.CreateProject,
createComponent: task.component.CreateProject
}
]
}
},
task.app.Tasks
)
builder.createDoc(view.class.Viewlet, core.space.Model, {
attachTo: task.class.Issue,
@ -237,7 +259,7 @@ export function createModel (builder: Builder): void {
})
builder.mixin(task.class.Issue, core.class.Class, view.mixin.ObjectEditor, {
editor: task.component.EditTask
editor: task.component.EditIssue
})
builder.createDoc(task.class.Sequence, task.space.Sequence, {
@ -260,44 +282,65 @@ export function createModel (builder: Builder): void {
config: [
// '$lookup.attachedTo',
'$lookup.state',
'$lookup.assignee']
'$lookup.assignee'
]
})
builder.mixin(task.class.Issue, core.class.Class, task.mixin.KanbanCard, {
card: task.component.KanbanCard
})
builder.createDoc(task.class.Project, core.space.Model, {
name: 'public',
description: 'Public tasks',
private: false,
members: []
}, task.space.TasksPublic)
builder.createDoc(
task.class.Project,
core.space.Model,
{
name: 'public',
description: 'Public tasks',
private: false,
members: []
},
task.space.TasksPublic
)
builder.createDoc(task.class.KanbanTemplateSpace, core.space.Model, {
name: 'Projects',
description: 'Manage project statuses',
members: [],
private: false,
icon: task.component.TemplatesIcon
}, task.space.ProjectTemplates)
builder.createDoc(
task.class.KanbanTemplateSpace,
core.space.Model,
{
name: 'Projects',
description: 'Manage project statuses',
members: [],
private: false,
icon: task.component.TemplatesIcon
},
task.space.ProjectTemplates
)
createProjectKanban(task.space.TasksPublic, async (_class, space, data, id) => {
builder.createDoc(_class, space, data, id)
return await Promise.resolve()
}).catch((err) => console.error(err))
builder.createDoc(view.class.Action, core.space.Model, {
label: 'Create task' as IntlString,
icon: task.icon.Task,
action: task.actionImpl.CreateTask
}, task.action.CreateTask)
builder.createDoc(
view.class.Action,
core.space.Model,
{
label: 'Create task' as IntlString,
icon: task.icon.Task,
action: task.actionImpl.CreateTask
},
task.action.CreateTask
)
builder.createDoc(view.class.Action, core.space.Model, {
label: 'Edit Statuses' as IntlString,
icon: view.icon.MoreH,
action: task.actionImpl.EditStatuses
}, task.action.EditStatuses)
builder.createDoc(
view.class.Action,
core.space.Model,
{
label: 'Edit Statuses' as IntlString,
icon: view.icon.MoreH,
action: task.actionImpl.EditStatuses
},
task.action.EditStatuses
)
builder.createDoc(view.class.ActionTarget, core.space.Model, {
target: task.class.SpaceWithStates,
@ -312,18 +355,28 @@ export function createModel (builder: Builder): void {
presenter: task.component.StatePresenter
})
builder.createDoc(view.class.ViewletDescriptor, core.space.Model, {
label: 'Kanban' as IntlString,
icon: task.icon.Kanban,
component: task.component.KanbanView
}, task.viewlet.Kanban)
builder.createDoc(
view.class.ViewletDescriptor,
core.space.Model,
{
label: 'Kanban' as IntlString,
icon: task.icon.Kanban,
component: task.component.KanbanView
},
task.viewlet.Kanban
)
builder.createDoc(core.class.Space, core.space.Model, {
name: 'Sequences',
description: 'Internal space to store sequence numbers',
members: [],
private: false
}, task.space.Sequence)
builder.createDoc(
core.class.Space,
core.space.Model,
{
name: 'Sequences',
description: 'Internal space to store sequence numbers',
members: [],
private: false
},
task.space.Sequence
)
}
export { taskOperation } from './migration'

View File

@ -39,6 +39,7 @@ export default mergeIds(taskId, task, {
CreateProject: '' as AnyComponent,
CreateTask: '' as AnyComponent,
EditTask: '' as AnyComponent,
EditIssue: '' as AnyComponent,
TaskPresenter: '' as AnyComponent,
KanbanCard: '' as AnyComponent,
TemplatesIcon: '' as AnyComponent,

View File

@ -1,7 +1,7 @@
{
"string": {
"UploadDropFilesHere": "Upload or drop files here",
"NoAttachments": "There are no attachments",
"NoAttachments": "There are no attachments for this",
"AddAttachment": "uploaded an attachment"
}
}

View File

@ -1,14 +1,15 @@
<!--
// Copyright © 2020 Anticrm Platform Contributors.
//
// 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.
-->
@ -16,7 +17,7 @@
import attachment from '../plugin'
import type { Attachment } from '@anticrm/attachment'
import type { Class, Doc, Ref, Space } from '@anticrm/core'
import { IntlString, setPlatformStatus, unknownError } from '@anticrm/platform'
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'
@ -26,7 +27,6 @@
export let objectId: Ref<Doc>
export let space: Ref<Space>
export let _class: Ref<Class<Doc>>
export let noLabel: IntlString = attachment.string.NoAttachments
let attachments: Attachment[] = []
@ -39,6 +39,7 @@
let loading = 0
const client = getClient()
const hierarchy = client.getHierarchy()
async function createAttachment (file: File) {
loading++
@ -78,6 +79,7 @@
}
let dragover = false
$: classLabel = hierarchy.getClass(_class).label
</script>
<div class="attachments-container">
@ -120,7 +122,10 @@
>
<UploadDuo size={'large'} />
<div class="small-text content-dark-color mt-2">
<Label label={noLabel} />
<Label label={attachment.string.NoAttachments} />
<span class="lower">
<Label label={classLabel} />
</span>
</div>
<div class="small-text">
<a href={'#'} on:click={() => inputFile.click()}><Label label={attachment.string.UploadDropFilesHere} /></a>
@ -156,4 +161,8 @@
border: 1px dashed var(--theme-zone-border-lite);
border-radius: 0.75rem;
}
.lower {
text-transform: lowercase;
}
</style>

View File

@ -14,23 +14,14 @@
// limitations under the License.
-->
<script lang="ts">
import type { Ref } from '@anticrm/core'
import { Panel } from '@anticrm/panel'
import { createQuery, getClient, UserBox } from '@anticrm/presentation'
import { Attachments } from '@anticrm/attachment-resources'
import { getClient, UserBox } from '@anticrm/presentation'
import type { Lead } from '@anticrm/lead'
import { EditBox, Grid } from '@anticrm/ui'
import contact from '@anticrm/contact'
import { createEventDispatcher } from 'svelte'
import { createEventDispatcher, onMount } from 'svelte'
import lead from '../plugin'
export let _id: Ref<Lead>
let object: Lead
const query = createQuery()
$: query.query(lead.class.Lead, { _id }, (result) => {
object = result[0]
})
export let object: Lead
const dispatch = createEventDispatcher()
const client = getClient()
@ -38,34 +29,31 @@
function change (field: string, value: any) {
client.updateDoc(object._class, object.space, object._id, { [field]: value })
}
onMount(() => {
dispatch('open', { ignoreKeys: ['comments', 'number', 'title', 'customer'] })
})
</script>
{#if object !== undefined}
<Panel
icon={lead.icon.Lead}
title={object.title}
{object}
on:close={() => {
dispatch('close')
}}
>
<Grid column={1} rowGap={1.5}>
<EditBox
label={lead.string.LeadName}
bind:value={object.title}
icon={lead.icon.Lead}
placeholder="The simple lead"
maxWidth="39rem"
focus
on:change={(evt) => change('title', object.title)}
/>
<UserBox _class={contact.class.Contact} title="Customer" caption="Select customer" bind:value={object.customer} on:change={() => {
<Grid column={1} rowGap={1.5}>
<EditBox
label={lead.string.LeadName}
bind:value={object.title}
icon={lead.icon.Lead}
placeholder="The simple lead"
maxWidth="39rem"
focus
on:change={(evt) => change('title', object.title)}
/>
<UserBox
_class={contact.class.Contact}
title="Customer"
caption="Select customer"
bind:value={object.customer}
on:change={() => {
change('customer', object.customer)
}} />
</Grid>
<div class="mt-14">
<Attachments objectId={object._id} _class={object._class} space={object.space} />
</div>
</Panel>
}}
/>
</Grid>
{/if}

View File

@ -22,7 +22,7 @@
import { ActionIcon, IconMoreH, showPopup } from '@anticrm/ui'
import { ContextMenu } from '@anticrm/view-resources'
import lead from '../plugin'
import EditLead from './EditLead.svelte'
import { EditTask } from '@anticrm/task-resources'
export let object: WithLookup<Lead>
export let draggable: boolean
@ -32,7 +32,7 @@
}
function showLead () {
showPopup(EditLead, { _id: object._id }, 'full')
showPopup(EditTask, { _id: object._id }, 'full')
}
</script>
@ -63,7 +63,7 @@
}}
icon={IconMoreH}
size={'small'}
/>
/>
</div>
</div>
</div>

View File

@ -16,14 +16,14 @@
<script lang="ts">
import type { Lead } from '@anticrm/lead'
import { closeTooltip, Icon, showPopup } from '@anticrm/ui'
import EditLead from './EditLead.svelte'
import lead from '../plugin'
import { EditTask } from '@anticrm/task-resources'
export let value: Lead
function show () {
closeTooltip()
showPopup(EditLead, { _id: value._id }, 'full')
showPopup(EditTask, { _id: value._id }, 'full')
}
</script>

View File

@ -1,8 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" style="display: none;">
<symbol id="chunter" viewBox="0 0 32 32">
<path d="M25.9,14.6C25.7,9.8,21.9,6,17,5.7h-0.5c0,0,0,0,0,0c-1.4,0-2.9,0.3-4.2,1c-3.2,1.6-5.2,4.8-5.2,8.4c0,1.4,0.3,2.7,0.9,4 l-1.9,5.7c-0.1,0.2,0,0.5,0.1,0.6c0.1,0.1,0.3,0.2,0.4,0.2c0.1,0,0.1,0,0.2,0l5.7-1.9c1.2,0.6,2.6,0.9,4,0.9c0,0,0,0,0,0 c3.6,0,6.8-2,8.4-5.2c0.7-1.3,1-2.8,1-4.2L25.9,14.6z M24.7,15.1C24.7,15.1,24.7,15.1,24.7,15.1c0,1.3-0.3,2.5-0.9,3.7 c-1.4,2.8-4.2,4.5-7.3,4.5c0,0,0,0,0,0c-1.3,0-2.5-0.3-3.6-0.9c-0.1-0.1-0.3-0.1-0.5,0l-4.8,1.6l1.6-4.8c0.1-0.2,0-0.3,0-0.5 c-0.6-1.1-0.9-2.4-0.9-3.7c0-3.1,1.7-5.9,4.5-7.3c1.1-0.6,2.4-0.9,3.6-0.9c0,0,0,0,0,0l0.5,0c4.2,0.2,7.5,3.6,7.7,7.7V15.1z"/>
</symbol>
<symbol id="recruitment" viewBox="0 0 24 24">
<g>
<path d="M9.6,14.4c-1.8,0-7.4,0-7.4,3.4c0,3,4.2,3.4,7.4,3.4c1.8,0,7.4,0,7.4-3.4C17.1,14.7,12.8,14.4,9.6,14.4z M9.6,20c-4.1,0-6.2-0.7-6.2-2.2c0-1.5,2.1-2.2,6.2-2.2s6.2,0.7,6.2,2.2C15.9,19.2,13.8,20,9.6,20z"/>
@ -35,4 +31,7 @@
<path d="M9.8,10.2H5.5c-0.3,0-0.6,0.3-0.6,0.6s0.3,0.6,0.6,0.6h4.4c0.3,0,0.6-0.3,0.6-0.6S10.1,10.2,9.8,10.2z"/>
<path d="M5.5,8.3h2.7c0.3,0,0.6-0.3,0.6-0.6S8.5,7.2,8.2,7.2H5.5c-0.3,0-0.6,0.3-0.6,0.6S5.1,8.3,5.5,8.3z"/>
</symbol>
<symbol id="application" viewBox="0 0 16 16">
<path d="M11.2,0C11.2,0,11.2,0,11.2,0H6C6,0,6,0,6,0s0,0-0.1,0H5.8C5.7,0,5.5,0.1,5.4,0.2L1.3,4.5C1.2,4.6,1.2,4.7,1.2,4.8v7.5 c0,2,1.5,3.6,3.5,3.7h6.5c0,0,0.1,0,0.1,0c2,0,3.6-1.7,3.5-3.7V3.5C14.8,1.6,13.2,0,11.2,0z M5.5,1.4v1.6c0,1-0.7,1.8-1.6,1.8H2.2 L5.5,1.4z M13.9,12.4c0,1.5-1.1,2.7-2.7,2.7H4.7c-1.5-0.1-2.6-1.2-2.6-2.7V5.8h1.8c1.4,0,2.6-1.2,2.6-2.8V1h4.8c0,0,0,0,0,0 c1.4,0,2.6,1.2,2.7,2.6V12.4z"/>
</symbol>
</svg>

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@ -14,8 +14,7 @@
"CandidatesName": "Pool name *",
"MakePrivateDescription": "Only members can see it",
"CreateAnApplication": "Create an application",
"NoApplicationsForCandidate": "There are no applications for this candidate.",
"NoAttachmentsForCandidate": "There are no attachments for this candidate."
"NoApplicationsForCandidate": "There are no applications for this candidate."
},
"status": {
"CandidateRequired": "Please select candidate"

View File

@ -22,7 +22,8 @@ loadMetadata(recruit.icon, {
Vacancy: `${icons}#vacancy`,
Location: `${icons}#location`,
Calendar: `${icons}#calendar`,
Create: `${icons}#create`
Create: `${icons}#create`,
Application: `${icons}#application`
})
addStringsLoader(recruitId, async (lang: string) => await import(`../lang/${lang}.json`))

View File

@ -1,55 +0,0 @@
<!--
// 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 { AttributeBarEditor, getClient, UserBox } from '@anticrm/presentation'
import { Applicant } from '@anticrm/recruit'
export let object: Applicant
const client = getClient()
function change() {
client.updateCollection(
object._class,
object.space,
object._id,
object.attachedTo,
object.attachedToClass,
object.collection,
{ assignee: object.assignee }
)
}
</script>
<div class="flex-between header">
<UserBox
_class={contact.class.Employee}
title="Assigned recruiter"
caption="Recruiters"
bind:value={object.assignee}
on:change={change}
allowDeselect
titleDeselect={'Unassign recruiter'}
/>
<AttributeBarEditor key={'state'} {object} showHeader={false} />
</div>
<style lang="scss">
.header {
width: 100%;
padding: 0 0.5rem;
}
</style>

View File

@ -13,26 +13,23 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import type { Applicant } from '@anticrm/recruit'
import { closeTooltip, IconFile, showPopup } from '@anticrm/ui'
import { getClient } from '@anticrm/presentation'
import { EditTask } from '@anticrm/task-resources'
import type { Applicant } from '@anticrm/recruit'
import { closeTooltip, IconFile, showPopup } from '@anticrm/ui'
import EditApplication from './EditApplication.svelte'
import { getClient } from '@anticrm/presentation'
export let value: Applicant
export let value: Applicant
const client = getClient()
const shortLabel = client.getHierarchy().getClass(value._class).shortLabel
function show () {
closeTooltip()
showPopup(EditApplication, { _id: value._id }, 'full')
}
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"><IconFile size={'small'}/></span>&nbsp;{shortLabel}-{value.number}
<span class="icon"><IconFile size={'small'} /></span>&nbsp;{shortLabel}-{value.number}
</div>

View File

@ -1,70 +1,58 @@
<!--
// Copyright © 2020 Anticrm Platform Contributors.
//
// Copyright © 2020, 2021 Anticrm Platform Contributors.
// Copyright © 2021 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import { createEventDispatcher } from 'svelte'
import type { Ref } from '@anticrm/core'
import { createEventDispatcher, onMount } from 'svelte'
import { createQuery } from '@anticrm/presentation'
import { Panel } from '@anticrm/panel'
import type { Candidate, Applicant, Vacancy } from '@anticrm/recruit'
import { Attachments } from '@anticrm/attachment-resources'
import Contact from './icons/Contact.svelte'
import CandidateCard from './CandidateCard.svelte'
import VacancyCard from './VacancyCard.svelte'
import recruit from '../plugin'
import { formatName } from '@anticrm/contact'
import ApplicantHeader from './ApplicantHeader.svelte'
export let _id: Ref<Applicant>
let object: Applicant
export let object: Applicant
let candidate: Candidate
let vacancy: Vacancy
const query = createQuery()
$: query.query(recruit.class.Applicant, { _id }, result => { object = result[0] })
const candidateQuery = createQuery()
$: if (object !== undefined) candidateQuery.query(recruit.class.Candidate, { _id: object.attachedTo }, result => { candidate = result[0] })
$: if (object !== undefined)
{candidateQuery.query(recruit.class.Candidate, { _id: object.attachedTo }, (result) => {
candidate = result[0]
})}
const vacancyQuery = createQuery()
$: if (object !== undefined) vacancyQuery.query(recruit.class.Vacancy, { _id: object.space }, result => { vacancy = result[0] })
$: if (object !== undefined)
{vacancyQuery.query(recruit.class.Vacancy, { _id: object.space }, (result) => {
vacancy = result[0]
})}
const dispatch = createEventDispatcher()
onMount(() => {
dispatch('open', { ignoreKeys: ['comments', 'number'] })
})
</script>
{#if object !== undefined && candidate !== undefined}
<Panel icon={Contact} title={formatName(candidate.name)} {object} on:close={() => { dispatch('close') }}>
<ApplicantHeader {object} slot="subtitle" />
<div class="grid-cards">
<CandidateCard {candidate}/>
<VacancyCard {vacancy}/>
<CandidateCard {candidate} />
<VacancyCard {vacancy} />
</div>
<div class="attachments">
<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;

View File

@ -26,6 +26,7 @@ import ApplicationPresenter from './components/ApplicationPresenter.svelte'
import ApplicationsPresenter from './components/ApplicationsPresenter.svelte'
import TemplatesIcon from './components/TemplatesIcon.svelte'
import Applications from './components/Applications.svelte'
import EditApplication from './components/EditApplication.svelte'
import { showPopup } from '@anticrm/ui'
import { Resources } from '@anticrm/platform'
@ -44,6 +45,7 @@ export default async (): Promise<Resources> => ({
CreateCandidate,
CreateApplication,
EditCandidate,
EditApplication,
KanbanCard,
ApplicationPresenter,
ApplicationsPresenter,

View File

@ -37,8 +37,6 @@ export default mergeIds(recruitId, recruit, {
CreateCandidate: '' as IntlString,
CreateAnApplication: '' as IntlString,
NoApplicationsForCandidate: '' as IntlString,
NoAttachmentsForCandidate: '' as IntlString,
FirstName: '' as IntlString,
LastName: '' as IntlString
},

View File

@ -71,6 +71,7 @@ export default plugin(recruitId, {
Vacancy: '' as Asset,
Location: '' as Asset,
Calendar: '' as Asset,
Create: '' as Asset
Create: '' as Asset,
Application: '' as Asset
}
})

View File

@ -13,7 +13,6 @@
"TaskName": "Task name *",
"TaskAssignee": "Assignee",
"TaskDescription": "Description",
"NoAttachmentsForTask": "There are no attachments for this task.",
"AssigneeRequired": "Assignee is required",
"More": "Options",
"TaskUnAssign": "Unassign",

View File

@ -0,0 +1,65 @@
<!--
// 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 { getClient } from '@anticrm/presentation'
import type { Issue } from '@anticrm/task'
import { EditBox, Grid } from '@anticrm/ui'
import { createEventDispatcher, onMount } from 'svelte'
import task from '../plugin'
export let object: Issue
const dispatch = createEventDispatcher()
const client = getClient()
function change (field: string, value: any) {
client.updateCollection(
object._class,
object.space,
object._id,
object.attachedTo,
object.attachedToClass,
object.collection,
{ [field]: value }
)
}
onMount(() => {
dispatch('open', { ignoreKeys: ['comments', 'name', 'description', 'number'] })
})
</script>
{#if object !== undefined}
<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>
{/if}

View File

@ -1,78 +1,118 @@
<!--
// Copyright © 2020 Anticrm Platform Contributors.
//
// Copyright © 2020, 2021 Anticrm Platform Contributors.
// Copyright © 2021 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import type { Ref } from '@anticrm/core'
import core, { Class, Doc, Ref } from '@anticrm/core'
import { Panel } from '@anticrm/panel'
import { createQuery, getClient } from '@anticrm/presentation'
import type { Issue } from '@anticrm/task'
import { EditBox, Grid } from '@anticrm/ui'
import { createQuery, getAttributePresenterClass, getClient } from '@anticrm/presentation'
import type { Task } from '@anticrm/task'
import { AnyComponent, Component } from '@anticrm/ui'
import view from '@anticrm/view'
import { createEventDispatcher } from 'svelte'
import task from '../plugin'
import { Attachments } from '@anticrm/attachment-resources'
import TaskHeader from './TaskHeader.svelte'
import { Asset } from '@anticrm/platform'
export let _id: Ref<Issue>
let object: Issue
export let _id: Ref<Task>
let object: Task
const client = getClient()
const hierarchy = client.getHierarchy()
const docKeys: Set<string> = new Set<string>(hierarchy.getAllAttributes(core.class.AttachedDoc).keys())
let keys: string[] = []
let collectionKeys: string[] = []
const query = createQuery()
$: query.query(task.class.Issue, { _id }, (result) => {
object = result[0]
})
$: _id &&
query.query(task.class.Task, { _id }, (result) => {
object = result[0]
})
const dispatch = createEventDispatcher()
const client = getClient()
function change (field: string, value: any) {
client.updateCollection(object._class, object.space, object._id, object.attachedTo, object.attachedToClass, object.collection, { [field]: value })
function getFiltredKeys (ignoreKeys: string[]): string[] {
let keys = Array.from(hierarchy.getAllAttributes(object._class).keys())
keys = keys.filter((k) => !docKeys.has(k))
keys = keys.filter((k) => !ignoreKeys.includes(k))
return keys
}
function getKeys (ignoreKeys: string[]): void {
const filtredKeys = getFiltredKeys(ignoreKeys)
keys = collectionsFilter(filtredKeys, false)
collectionKeys = collectionsFilter(filtredKeys, true)
}
function collectionsFilter (keys: string[], get: boolean): string[] {
const result: string[] = []
for (const key of keys) {
if (isCollectionAttr(key) === get) result.push(key)
}
return result
}
function isCollectionAttr (key: string): boolean {
const attribute = hierarchy.getAttribute(object._class, key)
return hierarchy.isDerived(attribute.type._class, core.class.Collection)
}
async function getEditor (_class: Ref<Class<Doc>>): Promise<AnyComponent> {
const clazz = hierarchy.getClass(_class)
const editorMixin = hierarchy.as(clazz, view.mixin.ObjectEditor)
if (editorMixin?.editor == null && clazz.extends != null) return getEditor(clazz.extends)
return editorMixin.editor
}
async function getCollectionEditor (key: string): Promise<AnyComponent> {
const attribute = hierarchy.getAttribute(object._class, key)
const attrClass = getAttributePresenterClass(attribute)
const clazz = client.getHierarchy().getClass(attrClass)
const editorMixin = client.getHierarchy().as(clazz, view.mixin.AttributeEditor)
return editorMixin.editor
}
$: icon = object && (hierarchy.getClass(object._class).icon as Asset)
$: title = object && hierarchy.getClass(object._class).label
</script>
{#if object !== undefined}
<Panel
icon={view.icon.Table}
title={object.name}
{icon}
{title}
{object}
on:close={() => {
dispatch('close')
}}
>
<TaskHeader {object} slot="subtitle" />
<TaskHeader {object} {keys} 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)}
{#await getEditor(object._class) then is}
<Component
{is}
props={{ object }}
on:open={(ev) => {
getKeys(ev.detail.ignoreKeys)
}}
/>
<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} noLabel={task.string.NoAttachmentsForTask} />
</div>
{/await}
{#each collectionKeys as collection}
<div class="mt-14">
{#await getCollectionEditor(collection) then is}
<Component {is} props={{ objectId: object._id, _class: object._class, space: object.space }} />
{/await}
</div>
{/each}
</Panel>
{/if}

View File

@ -1,42 +1,71 @@
<!--
// 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 { AttributeBarEditor, getClient, UserBox } from '@anticrm/presentation'
import { Issue } from '@anticrm/task'
import core, { Class, Doc, Ref, RefTo } from '@anticrm/core'
import { AttributeBarEditor, AttributesBar, getClient, UserBox } from '@anticrm/presentation'
import { Task } from '@anticrm/task'
import task from '../plugin'
export let object: Issue
export let object: Task
export let keys: string[]
const client = getClient()
const hierarchy = client.getHierarchy()
function change () {
client.updateCollection(object._class, object.space, object._id, object.attachedTo, object.attachedToClass, object.collection, { assignee: object.assignee })
client.updateCollection(
object._class,
object.space,
object._id,
object.attachedTo,
object.attachedToClass,
object.collection,
{ assignee: object.assignee }
)
}
$: assigneeTitle = hierarchy.getAttribute(object._class, 'assignee').label
function getAssigneeClass (object: Task): Ref<Class<Doc>> {
const attribute = hierarchy.getAttribute(object._class, 'assignee')
const attrClass = attribute.type._class
if (attrClass === core.class.RefTo) {
return (attribute.type as RefTo<Doc>).to
}
return contact.class.Employee
}
$: filtredKeys = keys.filter((p) => p !== 'state' && p !== 'assignee' && p !== 'doneState') // todo
</script>
<div class="flex-between header">
<UserBox
_class={contact.class.Employee}
title={task.string.TaskAssignee}
caption="Assignee"
bind:value={object.assignee}
on:change={change}
allowDeselect
titleDeselect={task.string.TaskUnAssign}
/>
<div class="flex-center">
<UserBox
_class={getAssigneeClass(object)}
title={assigneeTitle}
caption={assigneeTitle}
bind:value={object.assignee}
on:change={change}
allowDeselect
titleDeselect={task.string.TaskUnAssign}
/>
<div class="column">
<AttributesBar {object} keys={filtredKeys} />
</div>
</div>
<AttributeBarEditor key={'state'} {object} showHeader={false} />
</div>
@ -44,5 +73,19 @@
.header {
width: 100%;
padding: 0 0.5rem;
.column {
position: relative;
margin-left: 3rem;
&::before {
content: '';
position: absolute;
top: 0;
bottom: 0;
left: -1.5rem;
width: 1px;
background-color: var(--theme-bg-accent-hover);
}
}
}
</style>

View File

@ -21,6 +21,7 @@ import CreateProject from './components/CreateProject.svelte'
import TaskPresenter from './components/TaskPresenter.svelte'
import KanbanCard from './components/KanbanCard.svelte'
import TemplatesIcon from './components/TemplatesIcon.svelte'
import EditIssue from './components/EditIssue.svelte'
import { Doc } from '@anticrm/core'
import { showPopup } from '@anticrm/ui'
@ -34,6 +35,7 @@ export { default as KanbanTemplateEditor } from './components/kanban/KanbanTempl
export { default as KanbanTemplateSelector } from './components/kanban/KanbanTemplateSelector.svelte'
export { default as Tasks } from './components/Tasks.svelte'
export { default as EditTask } from './components/EditTask.svelte'
async function createTask (object: Doc): Promise<void> {
showPopup(CreateTask, { parent: object._id, space: object.space })
@ -48,6 +50,7 @@ export default async (): Promise<Resources> => ({
CreateTask,
CreateProject,
TaskPresenter,
EditIssue,
KanbanCard,
TemplatesIcon,
KanbanView,

View File

@ -32,7 +32,6 @@ export default mergeIds(taskId, task, {
TaskAssignee: '' as IntlString,
TaskUnAssign: '' as IntlString,
TaskDescription: '' as IntlString,
NoAttachmentsForTask: '' as IntlString,
More: '' as IntlString,
UploadDropFilesHere: '' as IntlString,
NoTaskForObject: '' as IntlString,