mirror of
https://github.com/hcengineering/platform.git
synced 2024-11-23 05:53:09 +03:00
Move with state and validate (#680)
Signed-off-by: Denis Bykhov <80476319+BykhovDenis@users.noreply.github.com>
This commit is contained in:
parent
9f2670964c
commit
ec738daf75
@ -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,
|
||||
|
@ -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')
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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'}
|
||||
/>
|
||||
|
@ -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,
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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>>,
|
||||
|
Loading…
Reference in New Issue
Block a user