Move with state and validate (#680)

Signed-off-by: Denis Bykhov <80476319+BykhovDenis@users.noreply.github.com>
This commit is contained in:
Denis Bykhov 2021-12-20 15:37:15 +06:00 committed by GitHub
parent 9f2670964c
commit ec738daf75
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 182 additions and 75 deletions

View File

@ -224,6 +224,10 @@ export function createModel (builder: Builder): void {
presenter: recruit.component.ApplicationPresenter
})
builder.mixin(recruit.class.Applicant, core.class.Class, view.mixin.ObjectValidator, {
validator: recruit.validator.ApplicantValidator
})
builder.createDoc(
view.class.Action,
core.space.Model,

View File

@ -25,22 +25,35 @@ function logInfo (msg: string, result: MigrationResult): void {
}
export const recruitOperation: MigrateOperation = {
async migrate (client: MigrationClient): Promise<void> {
logInfo('done for Applicants', await client.update(DOMAIN_TASK, { _class: recruit.class.Applicant, doneState: { $exists: false } }, { doneState: null }))
logInfo(
'done for Applicants',
await client.update(
DOMAIN_TASK,
{ _class: recruit.class.Applicant, doneState: { $exists: false } },
{ doneState: null }
)
)
logInfo('$move employee => assignee', await client.update(
logInfo(
'$move employee => assignee',
await client.update(
DOMAIN_TASK,
{ _class: recruit.class.Applicant, employee: { $exists: true } },
{ $rename: { employee: 'assignee' } }
))
)
)
const employees = (await client.find(DOMAIN_CONTACT, { _class: contact.class.Employee })).map(emp => emp._id)
const employees = (await client.find(DOMAIN_CONTACT, { _class: contact.class.Employee })).map((emp) => emp._id)
// update assignee to unassigned if there is no employee exists.
logInfo('applicants wrong assignee', await client.update(
logInfo(
'applicants wrong assignee',
await client.update(
DOMAIN_TASK,
{ _class: recruit.class.Applicant, assignee: { $not: { $in: employees } } },
{ assignee: null }
))
)
)
},
async upgrade (client: MigrationUpgradeClient): Promise<void> {
console.log('Recruit: Performing model upgrades')

View File

@ -13,8 +13,8 @@
// limitations under the License.
//
import type { Doc, Ref, Space } from '@anticrm/core'
import type { IntlString, Resource } from '@anticrm/platform'
import type { Client, Doc, Ref, Space } from '@anticrm/core'
import type { IntlString, Resource, Status } from '@anticrm/platform'
import { mergeIds } from '@anticrm/platform'
import { recruitId } from '@anticrm/recruit'
import recruit from '@anticrm/recruit-resources/src/plugin'
@ -40,6 +40,9 @@ export default mergeIds(recruitId, recruit, {
Candidates: '' as IntlString,
Vacancy: '' as IntlString
},
validator: {
ApplicantValidator: '' as Resource<<T extends Doc>(doc: T, client: Client) => Promise<Status>>
},
component: {
CreateVacancy: '' as AnyComponent,
CreateCandidates: '' as AnyComponent,

View File

@ -13,13 +13,13 @@
// limitations under the License.
//
import type { Class, Doc, Ref, Space } from '@anticrm/core'
import type { Class, Client, Doc, Ref, Space } from '@anticrm/core'
import { DOMAIN_MODEL } from '@anticrm/core'
import { Builder, Mixin, Model } from '@anticrm/model'
import core, { TClass, TDoc } from '@anticrm/model-core'
import type { Asset, IntlString, Resource } from '@anticrm/platform'
import type { Asset, IntlString, Resource, Status } from '@anticrm/platform'
import type { AnyComponent } from '@anticrm/ui'
import type { Action, ActionTarget, AttributeEditor, AttributePresenter, ObjectEditor, Viewlet, ViewletDescriptor } from '@anticrm/view'
import type { Action, ActionTarget, AttributeEditor, AttributePresenter, ObjectEditor, ObjectValidator, Viewlet, ViewletDescriptor } from '@anticrm/view'
import view from './plugin'
@Mixin(view.mixin.AttributeEditor, core.class.Class)
@ -37,6 +37,11 @@ export class TObjectEditor extends TClass implements ObjectEditor {
editor!: AnyComponent
}
@Mixin(view.mixin.ObjectValidator, core.class.Class)
export class TObjectValidator extends TClass implements ObjectValidator {
validator!: Resource<(<T extends Doc>(doc: T, client: Client) => Promise<Status<{}>>)>
}
@Model(view.class.ViewletDescriptor, core.class.Doc, DOMAIN_MODEL)
export class TViewletDescriptor extends TDoc implements ViewletDescriptor {
component!: AnyComponent
@ -65,7 +70,7 @@ export class TActionTarget extends TDoc implements ActionTarget {
}
export function createModel (builder: Builder): void {
builder.createModel(TAttributeEditor, TAttributePresenter, TObjectEditor, TViewletDescriptor, TViewlet, TAction, TActionTarget)
builder.createModel(TAttributeEditor, TAttributePresenter, TObjectEditor, TViewletDescriptor, TViewlet, TAction, TActionTarget, TObjectValidator)
builder.mixin(core.class.TypeString, core.class.Class, view.mixin.AttributeEditor, {
editor: view.component.StringEditor

View File

@ -15,16 +15,17 @@
<script lang="ts">
import type { Employee } from '@anticrm/contact'
import contact from '@anticrm/contact'
import { Ref, SortingOrder, Space } from '@anticrm/core'
import { Account, Class, Client, Doc, generateId, Ref, SortingOrder } from '@anticrm/core'
import { calcRank } from '@anticrm/core'
import { OK, Severity, Status } from '@anticrm/platform'
import { getResource, OK, Resource, Severity, Status } from '@anticrm/platform'
import { Card, getClient, UserBox } from '@anticrm/presentation'
import type { Candidate } from '@anticrm/recruit'
import type { SpaceWithStates } from '@anticrm/task'
import type { Applicant, Candidate } from '@anticrm/recruit'
import type { SpaceWithStates, State } from '@anticrm/task'
import task from '@anticrm/task'
import { Grid, Status as StatusControl } from '@anticrm/ui'
import { createEventDispatcher } from 'svelte'
import recruit from '../plugin'
import view from '@anticrm/view'
export let space: Ref<SpaceWithStates>
export let candidate: Ref<Candidate>
@ -33,17 +34,33 @@
export let preserveCandidate = false
let status: Status = OK
let _space = space
const doc: Applicant = {
state: '' as Ref<State>,
doneState: null,
number: 0,
assignee: assignee,
rank: '',
attachedTo: candidate,
attachedToClass: recruit.class.Candidate,
_class: recruit.class.Applicant,
space: space,
_id: generateId(),
collection: 'applications',
modifiedOn: Date.now(),
modifiedBy: '' as Ref<Account>
}
const dispatch = createEventDispatcher()
const client = getClient()
const hierarchy = client.getHierarchy()
export function canClose (): boolean {
return candidate === undefined && assignee === undefined
}
async function createApplication () {
const state = await client.findOne(task.class.State, { space: _space })
const state = await client.findOne(task.class.State, { space: doc.space })
if (state === undefined) {
throw new Error('create application: state not found')
}
@ -68,38 +85,40 @@
)
await client.addCollection(
recruit.class.Applicant,
_space,
candidate,
doc.space,
doc.attachedTo,
recruit.class.Candidate,
'applications',
{
state: state._id,
doneState: null,
number: incResult.object.sequence,
assignee: assignee,
number: (incResult as any).object.sequence,
assignee: doc.assignee,
rank: calcRank(lastOne, undefined)
}
)
}
async function validate (candidate: Ref<Candidate>, space: Ref<Space>) {
if (candidate === undefined) {
status = new Status(Severity.INFO, recruit.status.CandidateRequired, {})
} else {
if (space === undefined) {
status = new Status(Severity.INFO, recruit.status.VacancyRequired, {})
} else {
const applicants = await client.findAll(recruit.class.Applicant, { space, attachedTo: candidate })
if (applicants.length > 0) {
status = new Status(Severity.ERROR, recruit.status.ApplicationExists, {})
async function invokeValidate (
action: Resource<<T extends Doc>(doc: T, client: Client) => Promise<Status>>
): Promise<Status> {
const impl = await getResource(action)
return await impl(doc, client)
}
async function validate (doc: Applicant, _class: Ref<Class<Doc>>): Promise<void> {
const clazz = hierarchy.getClass(_class)
const validatorMixin = hierarchy.as(clazz, view.mixin.ObjectValidator)
if (validatorMixin?.validator != null) {
status = await invokeValidate(validatorMixin.validator)
} else if (clazz.extends != null) {
await validate(doc, clazz.extends)
} else {
status = OK
}
}
}
}
$: validate(candidate, _space)
$: validate(doc, doc._class)
</script>
<Card
@ -109,7 +128,7 @@
spaceClass={recruit.class.Vacancy}
spaceLabel={'Vacancy'}
spacePlaceholder={'Select vacancy'}
bind:space={_space}
bind:space={doc.space}
on:close={() => {
dispatch('close')
}}
@ -117,13 +136,13 @@
<StatusControl slot="error" {status} />
<Grid column={1} rowGap={1.75}>
{#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={doc.attachedTo} />
{/if}
<UserBox
_class={contact.class.Employee}
title="Assigned recruiter"
caption="Recruiters"
bind:value={assignee}
bind:value={doc.assignee}
allowDeselect
titleDeselect={'Unassign recruiter'}
/>

View File

@ -13,7 +13,7 @@
// limitations under the License.
//
import type { Doc } from '@anticrm/core'
import type { Client, Doc } from '@anticrm/core'
import CreateVacancy from './components/CreateVacancy.svelte'
import CreateCandidates from './components/CreateCandidates.svelte'
@ -29,16 +29,38 @@ import Applications from './components/Applications.svelte'
import EditApplication from './components/EditApplication.svelte'
import { showPopup } from '@anticrm/ui'
import { Resources } from '@anticrm/platform'
import { OK, Resources, Severity, Status } from '@anticrm/platform'
import { Applicant } from '@anticrm/recruit'
import recruit from './plugin'
async function createApplication (object: Doc): Promise<void> {
showPopup(CreateApplication, { candidate: object._id, preserveCandidate: true })
}
export async function applicantValidator (applicant: Applicant, client: Client): Promise<Status> {
if (applicant.attachedTo === undefined) {
return new Status(Severity.INFO, recruit.status.CandidateRequired, {})
}
if (applicant.space === undefined) {
return new Status(Severity.INFO, recruit.status.VacancyRequired, {})
}
const applicants = await client.findAll(recruit.class.Applicant, {
space: applicant.space,
attachedTo: applicant.attachedTo
})
if (applicants.filter((p) => p._id !== applicant._id).length > 0) {
return new Status(Severity.ERROR, recruit.status.ApplicationExists, {})
}
return OK
}
export default async (): Promise<Resources> => ({
actionImpl: {
CreateApplication: createApplication
},
validator: {
ApplicantValidator: applicantValidator
},
component: {
CreateVacancy,
CreateCandidates,

View File

@ -35,6 +35,7 @@
"@anticrm/core": "~0.6.11",
"@anticrm/view": "~0.6.0",
"@anticrm/ui": "~0.6.0",
"@anticrm/task": "~0.6.0",
"@anticrm/presentation": "~0.6.2"
}
}

View File

@ -14,19 +14,20 @@
// limitations under the License.
-->
<script lang="ts">
import { Label, Button } from '@anticrm/ui'
import { Label, Button, Status as StatusControl } from '@anticrm/ui'
import { getClient } from '@anticrm/presentation'
import core, { AttachedDoc, Collection, Doc, Ref, Space } from '@anticrm/core'
import core, { AttachedDoc, Collection, Doc, Ref, Space, SortingOrder, calcRank, Client, Class } from '@anticrm/core'
import { SpaceSelect } from '@anticrm/presentation'
import { createEventDispatcher } from 'svelte'
import ui from '@anticrm/ui'
import view from '../plugin'
import { translate } from '@anticrm/platform'
import task, { Task } from '@anticrm/task'
import { getResource, OK, Resource, Status, translate } from '@anticrm/platform'
export let object: Doc
let status: Status = OK
let currentSpace: Space | undefined
let space: Ref<Space> = object.space
const client = getClient()
const dispatch = createEventDispatcher()
const hierarchy = client.getHierarchy()
@ -37,52 +38,83 @@
$: _class && translate(_class, {}).then((res) => (classLabel = res.toLocaleLowerCase()))
async function move (doc: Doc): Promise<void> {
console.log('start move')
console.log(doc)
const attributes = hierarchy.getAllAttributes(doc._class)
for (const [name, attribute] of attributes) {
if (hierarchy.isDerived(attribute.type._class, core.class.Collection)) {
const collection = attribute.type as Collection<AttachedDoc>
console.log('find collection')
console.log(collection)
const allAttached = await client.findAll(collection.of, { attachedTo: doc._id })
console.log(allAttached)
for (const attached of allAttached) {
move(attached).catch((err) => console.log('failed to move', name, err))
}
}
}
if (doc.space === object.space) {
console.log('move doc')
console.log(doc)
client.updateDoc(doc._class, doc.space, doc._id, {
space: space
})
const update: any = {
space: doc.space
}
console.log('close')
const needStates = currentSpace ? hierarchy.isDerived(currentSpace._class, task.class.SpaceWithStates) : false
if (needStates) {
const state = await client.findOne(task.class.State, { space: doc.space })
if (state === undefined) {
throw new Error('Move: state not found')
}
const lastOne = await client.findOne(
(doc as Task)._class,
{ state: state._id },
{ sort: { rank: SortingOrder.Descending } }
)
update.state = state._id
update.rank = calcRank(lastOne, undefined)
}
client.updateDoc(doc._class, doc.space, doc._id, update)
dispatch('close')
}
$: client.findOne(core.class.Space, { _id: object.space }).then((res) => (currentSpace = res))
async function getSpace (): Promise<void> {
client.findOne(core.class.Space, { _id: object.space }).then((res) => (currentSpace = res))
}
async function invokeValidate (
action: Resource<<T extends Doc>(doc: T, client: Client) => Promise<Status>>
): Promise<Status> {
const impl = await getResource(action)
return await impl(object, client)
}
async function validate (doc: Doc, _class: Ref<Class<Doc>>): Promise<void> {
const clazz = hierarchy.getClass(_class)
const validatorMixin = hierarchy.as(clazz, view.mixin.ObjectValidator)
if (validatorMixin?.validator != null) {
status = await invokeValidate(validatorMixin.validator)
} else if (clazz.extends != null) {
await validate(doc, clazz.extends)
} else {
status = OK
}
}
$: validate(object, object._class)
</script>
<div class="container">
<div class="overflow-label fs-title mb-4">
<div class="overflow-label fs-title">
<Label label={view.string.MoveClass} params={{ class: label }} />
</div>
<div class="content-accent-color mb-4">
<StatusControl {status} />
<div class="content-accent-color mt-4 mb-4">
<Label label={view.string.SelectToMove} params={{ class: label, classLabel: classLabel }} />
</div>
<div class="spaceSelect">
{#await getSpace() then}
{#if currentSpace}
<SpaceSelect _class={currentSpace._class} label={_class ?? ''} bind:value={space} />
<SpaceSelect _class={currentSpace._class} label={_class ?? ''} bind:value={object.space} />
{/if}
{/await}
</div>
<div class="footer">
<Button
label={view.string.Move}
size={'small'}
disabled={space === object?.space}
disabled={object.space === currentSpace?._id || status !== OK}
transparent
primary
on:click={() => {
@ -116,7 +148,7 @@
padding: 1rem 1.25rem;
background-color: var(--theme-button-bg-enabled);
border: 1px solid var(--theme-bg-accent-color);
border-radius: .75rem;
border-radius: 0.75rem;
}
.footer {
@ -127,7 +159,7 @@
justify-content: start;
align-items: center;
margin-top: 1rem;
column-gap: .5rem;
column-gap: 0.5rem;
}
}
</style>

View File

@ -15,7 +15,7 @@
//
import type { Class, Client, Doc, FindOptions, Mixin, Obj, Ref, Space, UXObject } from '@anticrm/core'
import type { Asset, IntlString, Plugin, Resource } from '@anticrm/platform'
import type { Asset, IntlString, Plugin, Resource, Status } from '@anticrm/platform'
import { plugin } from '@anticrm/platform'
import type { AnyComponent, AnySvelteComponent } from '@anticrm/ui'
@ -40,6 +40,13 @@ export interface ObjectEditor extends Class<Doc> {
editor: AnyComponent
}
/**
* @public
*/
export interface ObjectValidator extends Class<Doc> {
validator: Resource<<T extends Doc>(doc: T, client: Client) => Promise<Status>>
}
/**
* @public
*/
@ -118,7 +125,8 @@ const view = plugin(viewId, {
mixin: {
AttributeEditor: '' as Ref<Mixin<AttributeEditor>>,
AttributePresenter: '' as Ref<Mixin<AttributePresenter>>,
ObjectEditor: '' as Ref<Mixin<ObjectEditor>>
ObjectEditor: '' as Ref<Mixin<ObjectEditor>>,
ObjectValidator: '' as Ref<Mixin<ObjectValidator>>
},
class: {
ViewletDescriptor: '' as Ref<Class<ViewletDescriptor>>,