Allow unassign (#511)

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
Andrey Sobolev 2021-12-03 17:16:16 +07:00 committed by GitHub
parent 29371e128e
commit e4783e09f9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 220 additions and 168 deletions

View File

@ -62,8 +62,8 @@ export class TAttribute extends TDoc implements AnyAttribute {
label!: IntlString label!: IntlString
} }
@Model(core.class.Type, core.class.Doc) @Model(core.class.Type, core.class.Obj)
export class TType extends TDoc implements Type<any> { export class TType extends TObj implements Type<any> {
label!: IntlString label!: IntlString
} }

View File

@ -15,7 +15,7 @@
import type { Account, Arr, Domain, Ref, Space, State } from '@anticrm/core' import type { Account, Arr, Domain, Ref, Space, State } from '@anticrm/core'
import { DOMAIN_MODEL } from '@anticrm/core' import { DOMAIN_MODEL } from '@anticrm/core'
import { Implements, Model, Prop, TypeBoolean, TypeState, TypeString } from '@anticrm/model' import { Implements, Model, Prop, TypeBoolean, TypeRef, TypeString } from '@anticrm/model'
import type { IntlString } from '@anticrm/platform' import type { IntlString } from '@anticrm/platform'
import core from './component' import core from './component'
import { TDoc } from './core' import { TDoc } from './core'
@ -53,7 +53,7 @@ export class TState extends TDoc implements State {
@Implements(core.interface.DocWithState) @Implements(core.interface.DocWithState)
export class TDocWithState extends TDoc { export class TDocWithState extends TDoc {
@Prop(TypeState(), 'State' as IntlString) @Prop(TypeRef(core.class.State), 'State' as IntlString)
state!: Ref<State> state!: Ref<State>
@Prop(TypeString(), 'No.' as IntlString) @Prop(TypeString(), 'No.' as IntlString)

View File

@ -38,7 +38,6 @@
"@anticrm/recruit-resources": "~0.6.0", "@anticrm/recruit-resources": "~0.6.0",
"@anticrm/chunter": "~0.6.0", "@anticrm/chunter": "~0.6.0",
"@anticrm/model-chunter": "~0.6.0", "@anticrm/model-chunter": "~0.6.0",
"@anticrm/view": "~0.6.0", "@anticrm/view": "~0.6.0"
"@anticrm/activity": "~0.6.0"
} }
} }

View File

@ -13,10 +13,9 @@
// limitations under the License. // limitations under the License.
// //
import activity from '@anticrm/activity'
import type { Employee } from '@anticrm/contact' import type { Employee } from '@anticrm/contact'
import type { Doc, Domain, FindOptions, Ref, Timestamp } from '@anticrm/core' import type { Doc, Domain, FindOptions, Ref, Timestamp } from '@anticrm/core'
import { Builder, Model, Prop, TypeBoolean, TypeDate, TypeString, UX } from '@anticrm/model' import { Builder, Model, Prop, TypeBoolean, TypeDate, TypeRef, TypeString, UX } from '@anticrm/model'
import chunter from '@anticrm/model-chunter' import chunter from '@anticrm/model-chunter'
import contact, { TPerson } from '@anticrm/model-contact' import contact, { TPerson } from '@anticrm/model-contact'
import core, { TAttachedDoc, TDocWithState, TSpace, TSpaceWithStates } from '@anticrm/model-core' import core, { TAttachedDoc, TDocWithState, TSpace, TSpaceWithStates } from '@anticrm/model-core'
@ -80,7 +79,7 @@ export class TCandidate extends TPerson implements Candidate {
@UX('Application' as IntlString, recruit.icon.RecruitApplication, 'APP' as IntlString) @UX('Application' as IntlString, recruit.icon.RecruitApplication, 'APP' as IntlString)
export class TApplicant extends TAttachedDoc implements Applicant { export class TApplicant extends TAttachedDoc implements Applicant {
// We need to declare, to provide property with label // We need to declare, to provide property with label
@Prop(TypeString(), 'Candidate' as IntlString) @Prop(TypeRef(recruit.class.Candidate), 'Candidate' as IntlString)
declare attachedTo: Ref<Candidate> declare attachedTo: Ref<Candidate>
@Prop(TypeString(), 'Attachments' as IntlString) @Prop(TypeString(), 'Attachments' as IntlString)
@ -89,8 +88,8 @@ export class TApplicant extends TAttachedDoc implements Applicant {
@Prop(TypeString(), 'Comments' as IntlString) @Prop(TypeString(), 'Comments' as IntlString)
comments?: number comments?: number
@Prop(TypeString(), 'Assigned recruiter' as IntlString) @Prop(TypeRef(contact.class.Employee), 'Assigned recruiter' as IntlString)
employee!: Ref<Employee> employee!: Ref<Employee> | null
// We need this two to make typescript happy. // We need this two to make typescript happy.
declare state: TDocWithState['state'] declare state: TDocWithState['state']
@ -228,14 +227,6 @@ export function createModel (builder: Builder): void {
attachedTo: recruit.class.Applicant, attachedTo: recruit.class.Applicant,
sequence: 0 sequence: 0
}) })
builder.createDoc(activity.class.TxViewlet, core.space.Model, {
objectClass: recruit.class.Applicant,
icon: recruit.icon.RecruitApplication,
txClass: core.class.TxUpdateDoc,
component: recruit.activity.TxApplicantUpdate,
display: 'inline'
}, recruit.ids.TxApplicantUpdate)
} }
export { default } from './plugin' export { default } from './plugin'

View File

@ -13,14 +13,13 @@
// limitations under the License. // limitations under the License.
// //
import { mergeIds } from '@anticrm/platform' import type { Doc, Ref, Space } from '@anticrm/core'
import type { IntlString, Resource } from '@anticrm/platform' import type { IntlString, Resource } from '@anticrm/platform'
import type { Ref, Space, Doc } from '@anticrm/core' import { mergeIds } from '@anticrm/platform'
import type { AnyComponent } from '@anticrm/ui'
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 '@anticrm/activity' import type { AnyComponent } from '@anticrm/ui'
import type { Action } from '@anticrm/view'
export default mergeIds(recruitId, recruit, { export default mergeIds(recruitId, recruit, {
action: { action: {
@ -48,11 +47,5 @@ export default mergeIds(recruitId, recruit, {
}, },
space: { space: {
CandidatesPublic: '' as Ref<Space> CandidatesPublic: '' as Ref<Space>
},
ids: {
TxApplicantUpdate: '' as Ref<TxViewlet>
},
activity: {
TxApplicantUpdate: '' as AnyComponent
} }
}) })

View File

@ -16,7 +16,7 @@
import type { Employee } from '@anticrm/contact' import type { Employee } from '@anticrm/contact'
import contact from '@anticrm/contact' import contact from '@anticrm/contact'
import type { Doc, DocWithState, Domain, FindOptions, Ref } from '@anticrm/core' import type { Doc, DocWithState, Domain, FindOptions, Ref } from '@anticrm/core'
import { Builder, Model, Prop, TypeString, UX } from '@anticrm/model' import { Builder, Model, Prop, TypeRef, TypeString, UX } from '@anticrm/model'
import chunter from '@anticrm/model-chunter' import chunter from '@anticrm/model-chunter'
import core, { TDoc, TSpaceWithStates } from '@anticrm/model-core' import core, { TDoc, TSpaceWithStates } from '@anticrm/model-core'
import view from '@anticrm/model-view' import view from '@anticrm/model-view'
@ -42,8 +42,8 @@ export class TTask extends TDoc implements Task {
@Prop(TypeString(), 'Description' as IntlString) @Prop(TypeString(), 'Description' as IntlString)
description!: string description!: string
@Prop(TypeString(), 'Assignee' as IntlString) @Prop(TypeRef(contact.class.Employee), 'Assignee' as IntlString)
assignee!: Ref<Employee> assignee!: Ref<Employee> | null
@Prop(TypeString(), 'Comments' as IntlString) @Prop(TypeString(), 'Comments' as IntlString)
comments!: number comments!: number

View File

@ -84,7 +84,7 @@ export abstract class MemDb extends TxProcessor {
const result: LookupData<T> = {} const result: LookupData<T> = {}
for (const key in lookup) { for (const key in lookup) {
const id = (doc as any)[key] as Ref<Doc> const id = (doc as any)[key] as Ref<Doc>
if (id !== undefined) { if (id != null) {
(result as any)[key] = this.getObject(id) (result as any)[key] = this.getObject(id)
} }
} }

View File

@ -308,8 +308,8 @@ export function TypeDate (): Type<string> {
/** /**
* @public * @public
*/ */
export function TypeState (): Type<string> { export function TypeRef (_class: Ref<Class<Doc>>): Type<string> {
return { _class: core.class.State, label: 'TypeState' as IntlString } return { _class: _class, label: 'TypeRef' as IntlString }
} }
/** /**

View File

@ -15,12 +15,13 @@
--> -->
<script lang="ts"> <script lang="ts">
import type { Ref, Class, Doc } from '@anticrm/core' import type { AttachedDoc, AttachedDoc, Doc } from '@anticrm/core'
import core from '@anticrm/core'
import { getResource } from '@anticrm/platform' import { getResource } from '@anticrm/platform'
import type { AnySvelteComponent } from '@anticrm/ui' import type { AnySvelteComponent } from '@anticrm/ui'
import { CircleButton, Label } from '@anticrm/ui' import { CircleButton, Label } from '@anticrm/ui'
import { getClient } from '../utils'
import view from '@anticrm/view' import view from '@anticrm/view'
import { getClient } from '../utils'
// export let _class: Ref<Class<Doc>> // export let _class: Ref<Class<Doc>>
export let key: string export let key: string
@ -45,8 +46,13 @@
editor = getResource(editorMixin.editor) editor = getResource(editorMixin.editor)
} }
function onChange(value: any) { function onChange (value: any) {
client.updateDoc(_class, object.space, object._id, { [key]: value }, true).then(result => console.log('UPDATE RESULT', result)) if (client.getHierarchy().isDerived(object._class, core.class.AttachedDoc)) {
const adoc = object as AttachedDoc
client.updateCollection(_class, object.space, adoc._id, adoc.attachedTo, adoc.attachedToClass, adoc.collection, { [key]: value })
} else {
client.updateDoc(_class, object.space, object._id, { [key]: value }, true)
}
} }
</script> </script>

View File

@ -29,8 +29,11 @@
export let _class: Ref<Class<Person>> export let _class: Ref<Class<Person>>
export let title: IntlString export let title: IntlString
export let caption: IntlString export let caption: IntlString
export let value: Ref<Person> export let value: Ref<Person> | null | undefined
export let show: boolean = false export let show: boolean = false
export let allowDeselect = false
export let titleDeselect: IntlString | undefined = undefined
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
let selected: Person | undefined let selected: Person | undefined
@ -40,11 +43,13 @@
const client = getClient() const client = getClient()
async function updateSelected(value: Ref<Person>) { async function updateSelected (value: Ref<Person>) {
selected = await client.findOne(_class, { _id: value }) selected = await client.findOne(_class, { _id: value })
} }
$: updateSelected(value) $: if (value != null) {
updateSelected(value)
}
onMount(() => { onMount(() => {
if (btn && show) { if (btn && show) {
@ -59,10 +64,19 @@
btn.focus() btn.focus()
if (!opened) { if (!opened) {
opened = true opened = true
showPopup(UsersPopup, { _class, title, caption }, container, (result) => { showPopup(UsersPopup, { _class, title, caption, allowDeselect, selected: value, titleDeselect }, container, (result) => {
if (result) { if (result === undefined) {
// Value is not changed.
opened = false
return
}
if (result != null) {
value = result._id value = result._id
dispatch('change', value) dispatch('change', value)
} else {
value = null
selected = undefined
dispatch('change', null)
} }
opened = false opened = false
}) })

View File

@ -22,17 +22,23 @@
import type { Ref, Class } from '@anticrm/core' import type { Ref, Class } from '@anticrm/core'
import type { Person } from '@anticrm/contact' import type { Person } from '@anticrm/contact'
import { createQuery } from '../utils' import { createQuery } from '../utils'
import { ActionIcon } from '@anticrm/ui'
import BlueCheck from './icons/BlueCheck.svelte'
export let _class: Ref<Class<Person>> export let _class: Ref<Class<Person>>
export let title: IntlString export let title: IntlString
export let caption: IntlString export let caption: IntlString
export let selected: Ref<Person> | undefined
export let allowDeselect: boolean = false
export let titleDeselect: IntlString | undefined = undefined
let search: string = '' let search: string = ''
let objects: Person[] = [] let objects: Person[] = []
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
const query = createQuery() const query = createQuery()
$: query.query(_class, { name: { $like: '%'+search+'%' } }, result => { objects = result }) $: query.query(_class, { name: { $like: '%' + search + '%' } }, result => { objects = result })
afterUpdate(() => { dispatch('update', Date.now()) }) afterUpdate(() => { dispatch('update', Date.now()) })
</script> </script>
@ -46,7 +52,12 @@
<div class="flex-col box"> <div class="flex-col box">
{#each objects as person} {#each objects as person}
<button class="menu-item" on:click={() => { dispatch('close', person) }}> <button class="menu-item" on:click={() => { dispatch('close', person) }}>
<UserInfo size={'medium'} value={person} /> <div class='flex-grow'>
<UserInfo size={'medium'} value={person} />
</div>
{#if allowDeselect && person._id === selected}
<ActionIcon direction={'top'} label={titleDeselect ?? 'Deselect'} icon={BlueCheck} action={() => { dispatch('close', null) }} size={'small'}/>
{/if}
</button> </button>
{/each} {/each}
</div> </div>

View File

@ -0,0 +1,22 @@
<script lang="ts">
export let size: 'small' | 'medium' | 'large'
const fill: string = 'currentColor'
</script>
<svg class="svg-{size}" {fill} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<svg viewBox="0 0 24 24" {fill} xmlns="http://www.w3.org/2000/svg">
<g filter="url(#filter0_b_991_16450)">
<rect width="24" height="24" rx="12" fill="#4474F6"/>
</g>
<path d="M8.25 12L10.9989 15L15.75 9" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<defs>
<filter id="filter0_b_991_16450" x="-100" y="-100" width="224" height="224" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feGaussianBlur in="BackgroundImage" stdDeviation="50"/>
<feComposite in2="SourceAlpha" operator="in" result="effect1_backgroundBlur_991_16450"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_backgroundBlur_991_16450" result="shape"/>
</filter>
</defs>
</svg>
</svg>

View File

@ -129,8 +129,15 @@
actions = result actions = result
}) })
function getValue (utx: TxUpdateDoc<Doc>, key: string): any { async function getValue (m: AttributeModel, utx: TxUpdateDoc<Doc>): Promise<any> {
return (utx.operations as any)[key] const val = (utx.operations as any)[m.key]
console.log(m._class, m.key, val, typeof val)
if (client.getHierarchy().isDerived(m._class, core.class.Doc) && typeof val === 'string') {
// We have an reference, we need to find a real object to pass for presenter
return await client.findOne(m._class, { _id: val as Ref<Doc> })
}
return val
} }
const showMenu = async (ev: MouseEvent): Promise<void> => { const showMenu = async (ev: MouseEvent): Promise<void> => {
showPopup( showPopup(
@ -207,8 +214,14 @@
{/if} {/if}
{#if viewlet === undefined && model.length > 0 && tx.updateTx} {#if viewlet === undefined && model.length > 0 && tx.updateTx}
{#each model as m} {#each model as m}
<span>changed {m.label} to</span> {#await getValue(m, tx.updateTx) then value}
<div class="strong"><svelte:component this={m.presenter} value={getValue(tx.updateTx, m.key)} /></div> {#if value === null}
<span>unset {m.label}</span>
{:else}
<span>changed {m.label} to</span>
<div class="strong"><svelte:component this={m.presenter} {value} /></div>
{/if}
{/await}
{/each} {/each}
{:else if viewlet && viewlet.display === 'inline' && viewlet.component} {:else if viewlet && viewlet.display === 'inline' && viewlet.component}
<div> <div>

View File

@ -13,28 +13,43 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
--> -->
<script lang="ts"> <script lang="ts">
import contact from '@anticrm/contact'
import { AttributeBarEditor, getClient, UserBox } from '@anticrm/presentation' import { AttributeBarEditor, getClient, UserBox } from '@anticrm/presentation'
import { Applicant } from '@anticrm/recruit' import { Applicant } from '@anticrm/recruit'
import contact from '@anticrm/contact'
export let object: Applicant export let object: Applicant
const client = getClient() const client = getClient()
function change () { function change() {
client.updateDoc(object._class, object.space, object._id, { employee: object.employee }) client.updateCollection(
object._class,
object.space,
object._id,
object.attachedTo,
object.attachedToClass,
object.collection,
{ employee: object.employee }
)
} }
</script> </script>
<div class="flex-between header"> <div class="flex-between header">
<UserBox _class={contact.class.Employee} title='Assigned recruiter' caption='Recruiters' bind:value={object.employee} on:change={change} /> <UserBox
_class={contact.class.Employee}
title="Assigned recruiter"
caption="Recruiters"
bind:value={object.employee}
on:change={change}
allowDeselect
titleDeselect={'Unassign recruiter'}
/>
<AttributeBarEditor key={'state'} {object} showHeader={false} /> <AttributeBarEditor key={'state'} {object} showHeader={false} />
</div> </div>
<style lang="scss"> <style lang="scss">
.header { .header {
width: 100%; width: 100%;
padding: 0 .5rem; padding: 0 0.5rem;
} }
</style> </style>

View File

@ -12,7 +12,6 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
--> -->
<script lang="ts"> <script lang="ts">
import { createEventDispatcher } from 'svelte' import { createEventDispatcher } from 'svelte'
import type { Ref, Space, SpaceWithStates } from '@anticrm/core' import type { Ref, Space, SpaceWithStates } from '@anticrm/core'
@ -33,7 +32,7 @@
export let space: Ref<SpaceWithStates> export let space: Ref<SpaceWithStates>
export let candidate: Ref<Candidate> // | null = null export let candidate: Ref<Candidate> // | null = null
export let employee: Ref<Employee> // | null = null export let employee: Ref<Employee> | null = null
export let preserveCandidate = false export let preserveCandidate = false
@ -56,18 +55,31 @@
if (sequence === undefined) { if (sequence === undefined) {
throw new Error('sequence object not found') throw new Error('sequence object not found')
} }
const incResult = await client.updateDoc(view.class.Sequence, view.space.Sequence, sequence._id, { const incResult = await client.updateDoc(
$inc: { sequence: 1 } view.class.Sequence,
}, true) view.space.Sequence,
const id = await client.addCollection(recruit.class.Applicant, _space, candidate, recruit.class.Candidate, 'applications', { sequence._id,
state: state._id, {
number: incResult.object.sequence, $inc: { sequence: 1 }
employee: employee },
}) true
)
const id = await client.addCollection(
recruit.class.Applicant,
_space,
candidate,
recruit.class.Candidate,
'applications',
{
state: state._id,
number: incResult.object.sequence,
employee: employee
}
)
} }
async function validate (candidate: Ref<Candidate>, space: Ref<Space>) { async function validate (candidate: Ref<Candidate>, space: Ref<Space>) {
if (candidate === undefined) { if (candidate == null) {
status = new Status(Severity.INFO, recruit.status.CandidateRequired, {}) status = new Status(Severity.INFO, recruit.status.CandidateRequired, {})
} else { } else {
if (space === undefined) { if (space === undefined) {
@ -84,22 +96,32 @@
} }
$: validate(candidate, _space) $: validate(candidate, _space)
</script> </script>
<Card label={'Create Application'} <Card
okAction={createApplication} label={'Create Application'}
canSave={status.severity === Severity.OK} okAction={createApplication}
spaceClass={recruit.class.Vacancy} canSave={status.severity === Severity.OK}
spaceLabel={'Vacancy'} spaceClass={recruit.class.Vacancy}
spacePlaceholder={'Select vacancy'} spaceLabel={'Vacancy'}
bind:space={_space} spacePlaceholder={'Select vacancy'}
on:close={() => { dispatch('close') }}> bind:space={_space}
on:close={() => {
dispatch('close')
}}
>
<StatusControl slot="error" {status} /> <StatusControl slot="error" {status} />
<Grid column={1} rowGap={1.75}> <Grid column={1} rowGap={1.75}>
{#if !preserveCandidate} {#if !preserveCandidate}
<UserBox _class={recruit.class.Candidate} title='Candidate' caption='Candidates' bind:value={candidate} /> <UserBox _class={recruit.class.Candidate} title="Candidate" caption="Candidates" bind:value={candidate} />
{/if} {/if}
<UserBox _class={contact.class.Employee} title='Assigned recruiter' caption='Recruiters' bind:value={employee} /> <UserBox
_class={contact.class.Employee}
title="Assigned recruiter"
caption="Recruiters"
bind:value={employee}
allowDeselect
titleDeselect={'Unassign recruiter'}
/>
</Grid> </Grid>
</Card> </Card>

View File

@ -1,48 +0,0 @@
<!--
// Copyright © 2020, 2021 Anticrm Platform Contributors.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import type { Class, State, TxUpdateDoc } from '@anticrm/core'
import core from '@anticrm/core'
import { getClient } from '@anticrm/presentation'
import type { Applicant } from '@anticrm/recruit'
import { Component } from '@anticrm/ui'
import view from '@anticrm/view'
export let tx: TxUpdateDoc<Applicant>
const client = getClient()
const stateClass = client.getModel().getObject(core.class.State) as Class<State>
const statePresenter = client.getHierarchy().as(stateClass, view.mixin.AttributePresenter)
</script>
{#if tx.operations.state}
<div class="flex-row-center update-container">
<span>updated State to</span>
{#if statePresenter?.presenter}
{#await client.findOne(core.class.State, { _id: tx.operations.state }) then st}
{#if st}
<Component is={statePresenter.presenter} props={{ value: st }}/>
{/if}
{/await}
{/if}
</div>
{/if}
<style lang="scss">
.update-container span { margin-right: .5rem; }
</style>

View File

@ -25,7 +25,6 @@ import KanbanCard from './components/KanbanCard.svelte'
import EditVacancy from './components/EditVacancy.svelte' import EditVacancy from './components/EditVacancy.svelte'
import ApplicationPresenter from './components/ApplicationPresenter.svelte' import ApplicationPresenter from './components/ApplicationPresenter.svelte'
import ApplicationsPresenter from './components/ApplicationsPresenter.svelte' import ApplicationsPresenter from './components/ApplicationsPresenter.svelte'
import TxApplicantUpdate from './components/activity/TxApplicantUpdate.svelte'
import { showPopup } from '@anticrm/ui' import { showPopup } from '@anticrm/ui'
import { Resources } from '@anticrm/platform' import { Resources } from '@anticrm/platform'
@ -49,8 +48,5 @@ export default async (): Promise<Resources> => ({
ApplicationPresenter, ApplicationPresenter,
ApplicationsPresenter, ApplicationsPresenter,
EditVacancy EditVacancy
},
activity: {
TxApplicantUpdate
} }
}) })

View File

@ -53,7 +53,7 @@ export interface Candidate extends Person {
export interface Applicant extends DocWithState, AttachedDoc { export interface Applicant extends DocWithState, AttachedDoc {
attachments?: number attachments?: number
comments?: number comments?: number
employee: Ref<Employee> employee: Ref<Employee> | null
} }
/** /**

View File

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

View File

@ -13,7 +13,7 @@
// limitations under the License. // limitations under the License.
--> -->
<script lang="ts"> <script lang="ts">
import contact, { Employee, EmployeeAccount } from '@anticrm/contact' import contact, { Employee } from '@anticrm/contact'
import type { Data, Ref, Space } from '@anticrm/core' import type { Data, Ref, Space } from '@anticrm/core'
import { generateId } from '@anticrm/core' import { generateId } from '@anticrm/core'
import { OK, Status } from '@anticrm/platform' import { OK, Status } from '@anticrm/platform'
@ -29,12 +29,12 @@
let _space = space let _space = space
const status: Status = OK const status: Status = OK
let assignee: Ref<EmployeeAccount> // | null = null let assignee: Ref<Employee> | null = null
const object: Data<Task> = { const object: Data<Task> = {
name: '', name: '',
description: '', description: '',
assignee: undefined as unknown as Ref<Employee>, assignee: null,
number: 0 number: 0
} }
@ -79,7 +79,7 @@
<Card <Card
label={task.string.CreateTask} label={task.string.CreateTask}
okAction={createTask} okAction={createTask}
canSave={object.name.length > 0 && assignee !== undefined} canSave={object.name.length > 0}
spaceClass={task.class.Project} spaceClass={task.class.Project}
spaceLabel={task.string.ProjectName} spaceLabel={task.string.ProjectName}
spacePlaceholder={task.string.SelectProject} spacePlaceholder={task.string.SelectProject}
@ -99,10 +99,12 @@
focus focus
/> />
<UserBox <UserBox
_class={contact.class.EmployeeAccount} _class={contact.class.Employee}
title="Assignee *" title="Assignee *"
caption="Assign this task" caption="Assign this task"
bind:value={assignee} bind:value={assignee}
allowDeselect
titleDeselect={task.string.TaskUnAssign}
/> />
</Grid> </Grid>
</Card> </Card>

View File

@ -34,6 +34,8 @@
caption="Assignee" caption="Assignee"
bind:value={object.assignee} bind:value={object.assignee}
on:change={change} on:change={change}
allowDeselect
titleDeselect={task.string.TaskUnAssign}
/> />
<AttributeBarEditor key={'state'} {object} showHeader={false} /> <AttributeBarEditor key={'state'} {object} showHeader={false} />
</div> </div>

View File

@ -30,6 +30,7 @@ export default mergeIds(taskId, task, {
SelectProject: '' as IntlString, SelectProject: '' as IntlString,
TaskName: '' as IntlString, TaskName: '' as IntlString,
TaskAssignee: '' as IntlString, TaskAssignee: '' as IntlString,
TaskUnAssign: '' as IntlString,
TaskDescription: '' as IntlString, TaskDescription: '' as IntlString,
NoAttachmentsForTask: '' as IntlString, NoAttachmentsForTask: '' as IntlString,
UploadDropFilesHere: '' as IntlString, UploadDropFilesHere: '' as IntlString,

View File

@ -33,7 +33,7 @@ export interface Task extends Doc {
name: string name: string
description: string description: string
assignee: Ref<Employee> assignee: Ref<Employee> | null
comments?: number comments?: number
attachments?: number attachments?: number

View File

@ -1,36 +1,35 @@
// //
// Copyright © 2020, 2021 Anticrm Platform Contributors. // Copyright © 2020, 2021 Anticrm Platform Contributors.
// Copyright © 2021 Hardcore Engineering Inc. // Copyright © 2021 Hardcore Engineering Inc.
// //
// Licensed under the Eclipse Public License, Version 2.0 (the "License"); // 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 // 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 // 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 // Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, // distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// //
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
// //
import type { Class, Client, Doc, FindOptions, FindResult, Obj, Ref } 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 type { Ref, Class, Obj, FindOptions, Doc, Client, FindResult } from '@anticrm/core' import type { AnyComponent } from '@anticrm/ui'
import type { AnyComponent, AnySvelteComponent } from '@anticrm/ui'
import type { Action, ActionTarget, BuildModelOptions } from '@anticrm/view' import type { Action, ActionTarget, BuildModelOptions } from '@anticrm/view'
import view, { AttributeModel } from '@anticrm/view' import view, { AttributeModel } from '@anticrm/view'
/** /**
* @public * @public
*/ */
export async function getObjectPresenter(client: Client, _class: Ref<Class<Obj>>, preserveKey: string): Promise<AttributeModel> { export async function getObjectPresenter (client: Client, _class: Ref<Class<Obj>>, preserveKey: string): Promise<AttributeModel> {
const clazz = client.getHierarchy().getClass(_class) const clazz = client.getHierarchy().getClass(_class)
const presenterMixin = client.getHierarchy().as(clazz, view.mixin.AttributePresenter) const presenterMixin = client.getHierarchy().as(clazz, view.mixin.AttributePresenter)
if (presenterMixin.presenter === undefined) { if (presenterMixin.presenter === undefined) {
if (clazz.extends !== undefined) { if (clazz.extends !== undefined) {
return getObjectPresenter(client, clazz.extends, preserveKey) return await getObjectPresenter(client, clazz.extends, preserveKey)
} else { } else {
throw new Error('object presenter not found for ' + preserveKey) throw new Error('object presenter not found for ' + preserveKey)
} }
@ -38,37 +37,48 @@ export async function getObjectPresenter(client: Client, _class: Ref<Class<Obj>>
const presenter = await getResource(presenterMixin.presenter) const presenter = await getResource(presenterMixin.presenter)
return { return {
key: preserveKey, key: preserveKey,
_class,
label: clazz.label, label: clazz.label,
presenter presenter
} as AttributeModel }
} }
async function getAttributePresenter(client: Client, _class: Ref<Class<Obj>>, key: string, preserveKey: string) { async function getAttributePresenter (client: Client, _class: Ref<Class<Obj>>, key: string, preserveKey: string): Promise<AttributeModel> {
const attribute = client.getHierarchy().getAttribute(_class, key) const attribute = client.getHierarchy().getAttribute(_class, key)
const clazz = client.getHierarchy().getClass(attribute.type._class) let attrClass = attribute.type._class
const presenterMixin = client.getHierarchy().as(clazz, view.mixin.AttributePresenter) const clazz = client.getHierarchy().getClass(attrClass)
let presenterMixin = client.getHierarchy().as(clazz, view.mixin.AttributePresenter)
let parent = clazz.extends
while (presenterMixin.presenter === undefined && parent !== undefined) {
const pclazz = client.getHierarchy().getClass(parent)
attrClass = parent
presenterMixin = client.getHierarchy().as(pclazz, view.mixin.AttributePresenter)
parent = pclazz.extends
}
if (presenterMixin.presenter === undefined) { if (presenterMixin.presenter === undefined) {
throw new Error('attribute presenter not found for ' + preserveKey) throw new Error('attribute presenter not found for ' + preserveKey)
} }
const presenter = await getResource(presenterMixin.presenter) const presenter = await getResource(presenterMixin.presenter)
return { return {
key: preserveKey, key: preserveKey,
_class: attrClass,
label: attribute.label, label: attribute.label,
presenter presenter
} as AttributeModel }
} }
async function getPresenter(client: Client, _class: Ref<Class<Obj>>, key: string, preserveKey: string, options?: FindOptions<Doc>): Promise<AttributeModel> { async function getPresenter (client: Client, _class: Ref<Class<Obj>>, key: string, preserveKey: string, options?: FindOptions<Doc>): Promise<AttributeModel> {
if (typeof key === 'object') { if (typeof key === 'object') {
const {presenter, label} = key const { presenter, label } = key
return { return {
key: '', key: '',
_class,
label: label as IntlString, label: label as IntlString,
presenter: await getResource(presenter as AnyComponent) presenter: await getResource(presenter as AnyComponent)
} }
} }
if (key.length === 0) { if (key.length === 0) {
return getObjectPresenter(client, _class, preserveKey) return await getObjectPresenter(client, _class, preserveKey)
} else { } else {
const split = key.split('.') const split = key.split('.')
if (split[0] === '$lookup') { if (split[0] === '$lookup') {
@ -80,36 +90,37 @@ async function getPresenter(client: Client, _class: Ref<Class<Obj>>, key: string
const model = await getPresenter(client, lookupClass, lookupKey, preserveKey) const model = await getPresenter(client, lookupClass, lookupKey, preserveKey)
if (lookupKey === '') { if (lookupKey === '') {
const attribute = client.getHierarchy().getAttribute(_class, split[1]) const attribute = client.getHierarchy().getAttribute(_class, split[1])
model.label = attribute.label as IntlString model.label = attribute.label
} else { } else {
const attribute = client.getHierarchy().getAttribute(lookupClass, lookupKey) const attribute = client.getHierarchy().getAttribute(lookupClass, lookupKey)
model.label = attribute.label as IntlString model.label = attribute.label
} }
return model return model
} }
return getAttributePresenter(client, _class, key, preserveKey) return await getAttributePresenter(client, _class, 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._class) console.log('building table model for', options._class)
// eslint-disable-next-line array-callback-return
const model = options.keys.map(key => { const model = options.keys.map(key => {
try { try {
const result = getPresenter(options.client, options._class, key, key, options.options) const result = getPresenter(options.client, options._class, key, key, options.options)
return result return result
} catch(err: any) { } catch (err: any) {
if (!(options.ignoreMissing ?? false)) { if (!(options.ignoreMissing ?? false)) {
throw err throw err
} }
} }
}) })
console.log(model) console.log(model)
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[]): Ref<Action>[] { function filterActions (client: Client, _class: Ref<Class<Obj>>, targets: ActionTarget[]): Array<Ref<Action>> {
const result: Ref<Action>[] = [] const result: Array<Ref<Action>> = []
for (const target of targets) { for (const target of targets) {
if (client.getHierarchy().isDerived(_class, target.target)) { if (client.getHierarchy().isDerived(_class, target.target)) {
result.push(target.action) result.push(target.action)
} }
@ -117,7 +128,7 @@ function filterActions(client: Client, _class: Ref<Class<Obj>>, targets: ActionT
return result return result
} }
export async function getActions(client: Client, _class: Ref<Class<Obj>>): Promise<FindResult<Action>> { export async function getActions (client: Client, _class: Ref<Class<Obj>>): 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) }}) return await client.findAll(view.class.Action, { _id: { $in: filterActions(client, _class, targets) } })
} }

View File

@ -109,6 +109,7 @@ export const viewId = 'view' as Plugin
export interface AttributeModel { export interface AttributeModel {
key: string key: string
label: IntlString label: IntlString
_class: Ref<Class<Doc>>
presenter: AnySvelteComponent presenter: AnySvelteComponent
} }