TSK-608: Move Vacancy support. (#2597)

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
Andrey Sobolev 2023-02-07 18:15:59 +07:00 committed by GitHub
parent 472cb40dda
commit 243bc1dede
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 422 additions and 75 deletions

View File

@ -1014,6 +1014,24 @@ export function createModel (builder: Builder): void {
label: recruit.string.RelatedIssues label: recruit.string.RelatedIssues
} }
}) })
createAction(
builder,
{
label: view.string.Move,
action: recruit.actionImpl.MoveApplicant,
icon: view.icon.Move,
input: 'any',
category: view.category.General,
target: recruit.class.Applicant,
context: {
mode: ['context', 'browser'],
group: 'tools'
},
override: [task.action.Move]
},
recruit.action.MoveApplicant
)
} }
export { recruitOperation } from './migration' export { recruitOperation } from './migration'

View File

@ -29,10 +29,12 @@ export default mergeIds(recruitId, recruit, {
CreateGlobalApplication: '' as Ref<Action>, CreateGlobalApplication: '' as Ref<Action>,
CopyApplicationId: '' as Ref<Action>, CopyApplicationId: '' as Ref<Action>,
CopyApplicationLink: '' as Ref<Action>, CopyApplicationLink: '' as Ref<Action>,
CopyCandidateLink: '' as Ref<Action> CopyCandidateLink: '' as Ref<Action>,
MoveApplicant: '' as Ref<Action>
}, },
actionImpl: { actionImpl: {
CreateOpinion: '' as ViewAction CreateOpinion: '' as ViewAction,
MoveApplicant: '' as ViewAction
}, },
category: { category: {
Recruit: '' as Ref<ActionCategory> Recruit: '' as Ref<ActionCategory>

View File

@ -230,30 +230,34 @@ export class LiveQuery extends TxProcessor implements Client {
} }
} }
private async checkSearch (q: Query, pos: number, _id: Ref<Doc>): Promise<boolean> { private async checkSearch (q: Query, _id: Ref<Doc>): Promise<boolean> {
const match = await this.findOne(q._class, { $search: q.query.$search, _id }, q.options)
if (q.result instanceof Promise) { if (q.result instanceof Promise) {
q.result = await q.result q.result = await q.result
} }
const match = await this.findOne(q._class, { $search: q.query.$search, _id }, q.options)
if (match === undefined) { if (match === undefined) {
if (q.options?.limit === q.result.length) { if (q.options?.limit === q.result.length) {
await this.refresh(q) await this.refresh(q)
return true return true
} else { } else {
const pos = q.result.findIndex((p) => p._id === _id)
q.result.splice(pos, 1) q.result.splice(pos, 1)
q.total-- q.total--
} }
} else { } else {
const pos = q.result.findIndex((p) => p._id === _id)
q.result[pos] = match q.result[pos] = match
} }
return false return false
} }
private async getCurrentDoc (q: Query, pos: number, _id: Ref<Doc>): Promise<boolean> { private async getCurrentDoc (q: Query, _id: Ref<Doc>): Promise<boolean> {
const current = await this.findOne(q._class, { _id }, q.options)
if (q.result instanceof Promise) { if (q.result instanceof Promise) {
q.result = await q.result q.result = await q.result
} }
const current = await this.findOne(q._class, { _id }, q.options)
const pos = q.result.findIndex((p) => p._id === _id)
if (current !== undefined && this.match(q, current)) { if (current !== undefined && this.match(q, current)) {
q.result[pos] = current q.result[pos] = current
} else { } else {
@ -279,10 +283,11 @@ export class LiveQuery extends TxProcessor implements Client {
await this.__updateLookup(q, updatedDoc, ops) await this.__updateLookup(q, updatedDoc, ops)
} }
private async checkUpdatedDocMatch (q: Query, pos: number, updatedDoc: WithLookup<Doc>): Promise<boolean> { private async checkUpdatedDocMatch (q: Query, updatedDoc: WithLookup<Doc>): Promise<boolean> {
if (q.result instanceof Promise) { if (q.result instanceof Promise) {
q.result = await q.result q.result = await q.result
} }
const pos = q.result.findIndex((p) => p._id === updatedDoc._id)
if (!this.match(q, updatedDoc)) { if (!this.match(q, updatedDoc)) {
if (q.options?.limit === q.result.length) { if (q.options?.limit === q.result.length) {
await this.refresh(q) await this.refresh(q)
@ -315,21 +320,22 @@ export class LiveQuery extends TxProcessor implements Client {
if (pos !== -1) { if (pos !== -1) {
// If query contains search we must check use fulltext // If query contains search we must check use fulltext
if (q.query.$search != null && q.query.$search.length > 0) { if (q.query.$search != null && q.query.$search.length > 0) {
const searchRefresh = await this.checkSearch(q, pos, tx.objectId) const searchRefresh = await this.checkSearch(q, tx.objectId)
if (searchRefresh) return {} if (searchRefresh) return {}
} else { } else {
const updatedDoc = q.result[pos] const updatedDoc = q.result[pos]
if (updatedDoc.modifiedOn < tx.modifiedOn) { if (updatedDoc.modifiedOn < tx.modifiedOn) {
await this.__updateMixinDoc(q, updatedDoc, tx) await this.__updateMixinDoc(q, updatedDoc, tx)
const updateRefresh = await this.checkUpdatedDocMatch(q, pos, updatedDoc) const updateRefresh = await this.checkUpdatedDocMatch(q, updatedDoc)
if (updateRefresh) return {} if (updateRefresh) return {}
} else { } else {
const currentRefresh = await this.getCurrentDoc(q, pos, updatedDoc._id) const currentRefresh = await this.getCurrentDoc(q, updatedDoc._id)
if (currentRefresh) return {} if (currentRefresh) return {}
} }
} }
this.sort(q, tx) this.sort(q, tx)
await this.updatedDocCallback(q.result[pos], q) const udoc = q.result.find((p) => p._id === tx.objectId)
await this.updatedDocCallback(udoc, q)
} else if (isMixin) { } else if (isMixin) {
// Mixin potentially added to object we doesn't have in out results // Mixin potentially added to object we doesn't have in out results
const doc = await this.findOne(q._class, { _id: tx.objectId }, q.options) const doc = await this.findOne(q._class, { _id: tx.objectId }, q.options)
@ -398,24 +404,26 @@ export class LiveQuery extends TxProcessor implements Client {
if (pos !== -1) { if (pos !== -1) {
// If query contains search we must check use fulltext // If query contains search we must check use fulltext
if (q.query.$search != null && q.query.$search.length > 0) { if (q.query.$search != null && q.query.$search.length > 0) {
const searchRefresh = await this.checkSearch(q, pos, tx.objectId) const searchRefresh = await this.checkSearch(q, tx.objectId)
if (searchRefresh) return if (searchRefresh) return
} else { } else {
const updatedDoc = q.result[pos] const updatedDoc = q.result[pos]
if (updatedDoc.modifiedOn < tx.modifiedOn) { if (updatedDoc.modifiedOn < tx.modifiedOn) {
await this.__updateDoc(q, updatedDoc, tx) await this.__updateDoc(q, updatedDoc, tx)
const updateRefresh = await this.checkUpdatedDocMatch(q, pos, updatedDoc) const updateRefresh = await this.checkUpdatedDocMatch(q, updatedDoc)
if (updateRefresh) return if (updateRefresh) return
} else { } else {
const currentRefresh = await this.getCurrentDoc(q, pos, updatedDoc._id) const currentRefresh = await this.getCurrentDoc(q, updatedDoc._id)
if (currentRefresh) return if (currentRefresh) return
} }
} }
this.sort(q, tx) this.sort(q, tx)
await this.updatedDocCallback(q.result[pos], q) const udoc = q.result.find((p) => p._id === tx.objectId)
await this.updatedDocCallback(udoc, q)
} else if (await this.matchQuery(q, tx)) { } else if (await this.matchQuery(q, tx)) {
this.sort(q, tx) this.sort(q, tx)
await this.updatedDocCallback(q.result[pos], q) const udoc = q.result.find((p) => p._id === tx.objectId)
await this.updatedDocCallback(udoc, q)
} }
await this.handleDocUpdateLookup(q, tx) await this.handleDocUpdateLookup(q, tx)
} }
@ -953,10 +961,13 @@ export class LiveQuery extends TxProcessor implements Client {
return false return false
} }
private async updatedDocCallback (updatedDoc: Doc, q: Query): Promise<void> { private async updatedDocCallback (updatedDoc: Doc | undefined, q: Query): Promise<void> {
q.result = q.result as Doc[] q.result = q.result as Doc[]
if (q.options?.limit !== undefined && q.result.length > q.options.limit) { if (q.options?.limit !== undefined && q.result.length > q.options.limit) {
if (updatedDoc === undefined) {
return await this.refresh(q)
}
if (q.result[q.options?.limit]._id === updatedDoc._id) { if (q.result[q.options?.limit]._id === updatedDoc._id) {
return await this.refresh(q) return await this.refresh(q)
} }

View File

@ -14,12 +14,11 @@
// limitations under the License. // limitations under the License.
--> -->
<script lang="ts"> <script lang="ts">
import { onMount } from 'svelte'
import { fitPopupElement } from '../popups'
import type { AnyComponent, AnySvelteComponent, PopupAlignment, PopupOptions, PopupPositionElement } from '../types'
import { deviceOptionsStore as deviceInfo } from '..' import { deviceOptionsStore as deviceInfo } from '..'
import { fitPopupElement } from '../popups'
import type { AnySvelteComponent, PopupAlignment, PopupOptions, PopupPositionElement } from '../types'
export let is: AnyComponent | AnySvelteComponent export let is: AnySvelteComponent
export let props: object export let props: object
export let element: PopupAlignment | undefined export let element: PopupAlignment | undefined
export let onClose: ((result: any) => void) | undefined export let onClose: ((result: any) => void) | undefined
@ -68,8 +67,7 @@
_close(undefined) _close(undefined)
} }
const fitPopup = (): void => { const fitPopup = (modalHTML: HTMLElement, element: PopupAlignment | undefined): void => {
if (modalHTML) {
if ((fullSize || docSize) && element === 'float') { if ((fullSize || docSize) && element === 'float') {
options = fitPopupElement(modalHTML, 'full') options = fitPopupElement(modalHTML, 'full')
options.props.maxHeight = '100vh' options.props.maxHeight = '100vh'
@ -80,7 +78,6 @@
} }
options.fullSize = fullSize options.fullSize = fullSize
} }
}
function handleKeydown (ev: KeyboardEvent) { function handleKeydown (ev: KeyboardEvent) {
if (ev.key === 'Escape' && is && top) { if (ev.key === 'Escape' && is && top) {
@ -102,19 +99,35 @@
const alignment: PopupPositionElement = element as PopupPositionElement const alignment: PopupPositionElement = element as PopupPositionElement
let showing: boolean | undefined = alignment?.kind === 'submenu' ? undefined : false let showing: boolean | undefined = alignment?.kind === 'submenu' ? undefined : false
onMount(() => { let oldModalHTML: HTMLElement | undefined = undefined
fitPopup()
setTimeout(() => { $: if (modalHTML !== undefined && oldModalHTML !== modalHTML) {
modalHTML.addEventListener('transitionend', () => (showing = undefined), { once: true }) oldModalHTML = modalHTML
fitPopup(modalHTML, element)
showing = true showing = true
}, 0) modalHTML.addEventListener(
}) 'transitionend',
() => {
showing = undefined
},
{ once: true }
)
}
$: if ($deviceInfo.docWidth <= 900 && !docSize) docSize = true $: if ($deviceInfo.docWidth <= 900 && !docSize) docSize = true
$: if ($deviceInfo.docWidth > 900 && docSize) docSize = false $: if ($deviceInfo.docWidth > 900 && docSize) docSize = false
</script> </script>
<svelte:window on:resize={fitPopup} on:keydown={handleKeydown} /> <svelte:window
on:resize={() => {
if (modalHTML) {
fitPopup(modalHTML, element)
}
}}
on:keydown={handleKeydown}
/>
{JSON.stringify(options)}
<div <div
class="popup {showing === undefined ? 'endShow' : showing === false ? 'preShow' : 'startShow'}" class="popup {showing === undefined ? 'endShow' : showing === false ? 'preShow' : 'startShow'}"
class:anim={element === 'float'} class:anim={element === 'float'}
@ -143,10 +156,10 @@
on:close={(ev) => _close(ev?.detail)} on:close={(ev) => _close(ev?.detail)}
on:fullsize={() => { on:fullsize={() => {
fullSize = !fullSize fullSize = !fullSize
fitPopup() fitPopup(modalHTML, element)
}} }}
on:changeContent={() => { on:changeContent={() => {
fitPopup() fitPopup(modalHTML, element)
}} }}
/> />
</div> </div>

View File

@ -55,14 +55,15 @@ export function showPopup (
return popups return popups
}) })
} }
const _element = element instanceof HTMLElement ? getPopupPositionElement(element) : element
if (typeof component === 'string') { if (typeof component === 'string') {
getResource(component) getResource(component)
.then((resolved) => .then((resolved) =>
addPopup({ id, is: resolved, props, element, onClose, onUpdate, close: closePopupOp, options }) addPopup({ id, is: resolved, props, element: _element, onClose, onUpdate, close: closePopupOp, options })
) )
.catch((err) => console.log(err)) .catch((err) => console.log(err))
} else { } else {
addPopup({ id, is: component, props, element, onClose, onUpdate, close: closePopupOp, options }) addPopup({ id, is: component, props, element: _element, onClose, onUpdate, close: closePopupOp, options })
} }
return closePopupOp return closePopupOp
} }
@ -352,7 +353,8 @@ export function getPopupPositionElement (
return undefined return undefined
} }
export function getEventPositionElement (evt: MouseEvent): PopupAlignment | undefined { export function getEventPositionElement (evt: MouseEvent): PopupAlignment | undefined {
const rect = DOMRect.fromRect({ width: 1, height: 1, x: evt.clientX, y: evt.clientY })
return { return {
getBoundingClientRect: () => DOMRect.fromRect({ width: 1, height: 1, x: evt.clientX, y: evt.clientY }) getBoundingClientRect: () => rect
} }
} }

View File

@ -104,7 +104,8 @@
"VacancyMatching": "Match Talents to vacancy", "VacancyMatching": "Match Talents to vacancy",
"Score": "Score", "Score": "Score",
"Match": "Match", "Match": "Match",
"PerformMatch": "Match" "PerformMatch": "Match",
"MoveApplication": "Move to another vacancy"
}, },
"status": { "status": {
"TalentRequired": "Please select talent", "TalentRequired": "Please select talent",

View File

@ -106,7 +106,8 @@
"VacancyMatching": "Подбор кандидатов на вакансию", "VacancyMatching": "Подбор кандидатов на вакансию",
"Score": "Оценка", "Score": "Оценка",
"Match": "Совпадение", "Match": "Совпадение",
"PerformMatch": "Сопоставить" "PerformMatch": "Сопоставить",
"MoveApplication": "Поменять Вакансию"
}, },
"status": { "status": {
"TalentRequired": "Пожалуйста выберите таланта", "TalentRequired": "Пожалуйста выберите таланта",

View File

@ -0,0 +1,7 @@
import { Doc } from '@hcengineering/core'
import { showPopup } from '@hcengineering/ui'
import MoveApplication from './components/MoveApplication.svelte'
export async function MoveApplicant (docs: Doc | Doc[]): Promise<void> {
showPopup(MoveApplication, { selected: Array.isArray(docs) ? docs : [docs] })
}

View File

@ -21,10 +21,11 @@
import { createQuery, getClient } from '@hcengineering/presentation' import { createQuery, getClient } from '@hcengineering/presentation'
import { Vacancy } from '@hcengineering/recruit' import { Vacancy } from '@hcengineering/recruit'
import { FullDescriptionBox } from '@hcengineering/text-editor' import { FullDescriptionBox } from '@hcengineering/text-editor'
import { Button, EditBox, Grid, IconMoreH, showPopup } from '@hcengineering/ui' import { Button, Component, EditBox, Grid, IconMoreH, showPopup } from '@hcengineering/ui'
import { ClassAttributeBar, ContextMenu } from '@hcengineering/view-resources' import { ClassAttributeBar, ContextMenu } from '@hcengineering/view-resources'
import { createEventDispatcher } from 'svelte' import { createEventDispatcher } from 'svelte'
import recruit from '../plugin' import recruit from '../plugin'
import tracker from '@hcengineering/tracker'
export let _id: Ref<Vacancy> export let _id: Ref<Vacancy>
@ -132,6 +133,7 @@
space={object.space} space={object.space}
attachments={object.attachments ?? 0} attachments={object.attachments ?? 0}
/> />
<Component is={tracker.component.RelatedIssuesSection} props={{ object, label: recruit.string.RelatedIssues }} />
</Grid> </Grid>
</Panel> </Panel>
{/if} {/if}

View File

@ -0,0 +1,265 @@
<!--
// Copyright © 2023 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 contact from '@hcengineering/contact'
import ExpandRightDouble from '@hcengineering/contact-resources/src/components/icons/ExpandRightDouble.svelte'
import { FindOptions, SortingOrder } from '@hcengineering/core'
import { OK, Severity, Status } from '@hcengineering/platform'
import presentation, { Card, createQuery, getClient, SpaceSelect } from '@hcengineering/presentation'
import type { Applicant, Vacancy } from '@hcengineering/recruit'
import task, { State } from '@hcengineering/task'
import ui, {
Button,
ColorPopup,
createFocusManager,
deviceOptionsStore as deviceInfo,
FocusHandler,
getPlatformColor,
Label,
ListView,
showPopup,
Status as StatusControl
} from '@hcengineering/ui'
import { moveToSpace } from '@hcengineering/view-resources/src/utils'
import { createEventDispatcher } from 'svelte'
import recruit from '../plugin'
import ApplicationPresenter from './ApplicationPresenter.svelte'
import VacancyCard from './VacancyCard.svelte'
import VacancyOrgPresenter from './VacancyOrgPresenter.svelte'
export let selected: Applicant[]
const status: Status = OK
let _space = selected[0]?.space
const dispatch = createEventDispatcher()
const client = getClient()
export function canClose (): boolean {
return true
}
let loading = false
async function updateApplication () {
loading = true
if (selectedState === undefined) {
throw new Error(`Please select initial state:${_space}`)
}
const state = await client.findOne(task.class.State, { space: _space, _id: selectedState?._id })
if (state === undefined) {
throw new Error(`create application: state not found space:${_space}`)
}
const op = client.apply('application.states')
for (const a of selected) {
await moveToSpace(op, a, _space, { state: state._id, doneState: null })
}
await op.commit()
loading = false
dispatch('close')
}
let states: Array<{ id: number | string; color: number; label: string }> = []
let selectedState: State | undefined
let rawStates: State[] = []
const statesQuery = createQuery()
const spaceQuery = createQuery()
let vacancy: Vacancy | undefined
$: if (_space) {
statesQuery.query(
task.class.State,
{ space: _space },
(res) => {
rawStates = res
},
{ sort: { rank: SortingOrder.Ascending } }
)
spaceQuery.query(recruit.class.Vacancy, { _id: _space }, (res) => {
vacancy = res.shift()
})
}
$: if (rawStates.findIndex((it) => it._id === selectedState?._id) === -1) {
selectedState = rawStates[0]
}
$: states = rawStates.map((s) => {
return { id: s._id, label: s.title, color: s.color }
})
const manager = createFocusManager()
const orgOptions: FindOptions<Vacancy> = {
lookup: {
company: contact.class.Organization
}
}
let verticalContent: boolean = false
$: verticalContent = $deviceInfo.isMobile && $deviceInfo.isPortrait
let btn: HTMLButtonElement
</script>
<FocusHandler {manager} />
<Card
label={recruit.string.MoveApplication}
okAction={updateApplication}
okLabel={presentation.string.Save}
canSave={status.severity === Severity.OK}
on:close={() => {
dispatch('close')
}}
>
<svelte:fragment slot="title">
<div class="flex-row-center gap-2">
<Label label={recruit.string.MoveApplication} />
</div>
</svelte:fragment>
<StatusControl slot="error" {status} />
<div class:candidate-vacancy={!verticalContent} class:flex-col={verticalContent}>
<div class="flex flex-stretch vacancyList">
<ListView count={selected.length}>
<svelte:fragment slot="item" let:item>
<ApplicationPresenter value={selected[item]} />
</svelte:fragment>
</ListView>
</div>
<div class="flex-center" class:rotate={verticalContent}>
<ExpandRightDouble />
</div>
<div class="flex-grow">
<SpaceSelect
_class={recruit.class.Vacancy}
spaceQuery={{ archived: false }}
spaceOptions={orgOptions}
label={recruit.string.Vacancy}
create={{
component: recruit.component.CreateVacancy,
label: recruit.string.CreateVacancy
}}
bind:value={_space}
on:change={(evt) => {
_space = evt.detail
}}
component={VacancyOrgPresenter}
componentProps={{ inline: true }}
>
<svelte:fragment slot="content">
<VacancyCard {vacancy} disabled={true} />
</svelte:fragment>
</SpaceSelect>
</div>
</div>
<svelte:fragment slot="pool">
{#if states.length > 0}
<Button
focusIndex={3}
width="min-content"
size="small"
kind="no-border"
bind:input={btn}
on:click={() => {
showPopup(
ColorPopup,
{ value: states, searchable: true, placeholder: ui.string.SearchDots },
btn,
(result) => {
if (result && result.id) {
selectedState = { ...result, _id: result.id, title: result.label }
}
manager.setFocusPos(3)
}
)
}}
>
<div slot="content" class="flex-row-center" class:empty={!selectedState}>
{#if selectedState}
<div class="color" style="background-color: {getPlatformColor(selectedState.color)}" />
<span class="label overflow-label">{selectedState.title}</span>
{:else}
<div class="color" />
<span class="label overflow-label"><Label label={presentation.string.NotSelected} /></span>
{/if}
</div>
</Button>
{/if}
</svelte:fragment>
</Card>
<style lang="scss">
.candidate-vacancy {
display: grid;
grid-template-columns: 3fr 1fr 3fr;
grid-template-rows: 1fr;
}
.rotate {
transform: rotate(90deg);
}
.color {
margin-right: 0.375rem;
width: 0.875rem;
height: 0.875rem;
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 0.25rem;
}
.label {
flex-grow: 1;
min-width: 0;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
.empty {
.color {
border-color: var(--content-color);
}
.label {
color: var(--content-color);
}
&:hover .color {
border-color: var(--accent-color);
}
&:hover .label {
color: var(--accent-color);
}
}
.vacancyList {
padding: 1rem 1.5rem 1.25rem;
background-color: var(--board-card-bg-color);
border: 1px solid var(--divider-color);
border-radius: 0.5rem;
transition-property: box-shadow, background-color, border-color;
transition-timing-function: var(--timing-shadow);
transition-duration: 0.15s;
user-select: text;
min-width: 15rem;
min-height: 15rem;
&:hover {
background-color: var(--board-card-bg-hover);
border-color: var(--button-border-color);
box-shadow: var(--accent-shadow);
}
}
</style>

View File

@ -64,6 +64,8 @@ import VacancyList from './components/VacancyList.svelte'
import VacancyTemplateEditor from './components/VacancyTemplateEditor.svelte' import VacancyTemplateEditor from './components/VacancyTemplateEditor.svelte'
import MatchVacancy from './components/MatchVacancy.svelte' import MatchVacancy from './components/MatchVacancy.svelte'
import { MoveApplicant } from './actionImpl'
async function createOpinion (object: Doc): Promise<void> { async function createOpinion (object: Doc): Promise<void> {
showPopup(CreateOpinion, { space: object.space, review: object._id }) showPopup(CreateOpinion, { space: object.space, review: object._id })
} }
@ -265,7 +267,8 @@ async function noneApplicant (filter: Filter, onUpdate: () => void): Promise<Obj
export default async (): Promise<Resources> => ({ export default async (): Promise<Resources> => ({
actionImpl: { actionImpl: {
CreateOpinion: createOpinion CreateOpinion: createOpinion,
MoveApplicant
}, },
validator: { validator: {
ApplicantValidator: applicantValidator ApplicantValidator: applicantValidator

View File

@ -117,7 +117,8 @@ export default mergeIds(recruitId, recruit, {
VacancyMatching: '' as IntlString, VacancyMatching: '' as IntlString,
Score: '' as IntlString, Score: '' as IntlString,
Match: '' as IntlString, Match: '' as IntlString,
PerformMatch: '' as IntlString PerformMatch: '' as IntlString,
MoveApplication: '' as IntlString
}, },
space: { space: {
CandidatesPublic: '' as Ref<Space> CandidatesPublic: '' as Ref<Space>

View File

@ -64,7 +64,7 @@
<Icon icon={tracker.icon.Issues} size={'small'} /> <Icon icon={tracker.icon.Issues} size={'small'} />
</div> </div>
{/if} {/if}
<span title={value?.title}> <span class="select-text" title={value?.title}>
{title} {title}
</span> </span>
</span> </span>

View File

@ -36,7 +36,7 @@
<span class="titlePresenter-container" class:with-margin={shouldUseMargin} title={value.title}> <span class="titlePresenter-container" class:with-margin={shouldUseMargin} title={value.title}>
<!-- svelte-ignore a11y-click-events-have-key-events --> <!-- svelte-ignore a11y-click-events-have-key-events -->
<span <span
class="name overflow-label cursor-pointer" class="name overflow-label cursor-pointer select-text"
style:max-width={showParent ? `${value.parents.length !== 0 ? 95 : 100}%` : '100%'} style:max-width={showParent ? `${value.parents.length !== 0 ? 95 : 100}%` : '100%'}
on:click={handleIssueEditorOpened}>{value.title}</span on:click={handleIssueEditorOpened}>{value.title}</span
> >

View File

@ -14,15 +14,16 @@
// limitations under the License. // limitations under the License.
--> -->
<script lang="ts"> <script lang="ts">
import { Label, Button, Status as StatusControl } from '@hcengineering/ui'
import { getClient } from '@hcengineering/presentation' import { getClient } from '@hcengineering/presentation'
import { Button, Label, Status as StatusControl } from '@hcengineering/ui'
import core, { AttachedDoc, Collection, Doc, Ref, Space, SortingOrder, Client, Class } from '@hcengineering/core' import core, { Class, Client, Doc, Ref, SortingOrder, Space } from '@hcengineering/core'
import { getResource, OK, Resource, Status, translate } from '@hcengineering/platform'
import { SpaceSelect } from '@hcengineering/presentation' import { SpaceSelect } from '@hcengineering/presentation'
import task, { calcRank, Task } from '@hcengineering/task'
import { createEventDispatcher } from 'svelte' import { createEventDispatcher } from 'svelte'
import view from '../plugin' import view from '../plugin'
import task, { Task, calcRank } from '@hcengineering/task' import { moveToSpace } from '../utils'
import { getResource, OK, Resource, Status, translate } from '@hcengineering/platform'
export let selected: Doc | Doc[] export let selected: Doc | Doc[]
$: docs = Array.isArray(selected) ? selected : [selected] $: docs = Array.isArray(selected) ? selected : [selected]
@ -45,19 +46,6 @@
$: _class && translate(_class, {}).then((res) => (classLabel = res.toLocaleLowerCase())) $: _class && translate(_class, {}).then((res) => (classLabel = res.toLocaleLowerCase()))
async function move (doc: Doc): Promise<void> { async function move (doc: Doc): Promise<void> {
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>
const allAttached = await client.findAll(collection.of, { attachedTo: doc._id })
for (const attached of allAttached) {
move(attached).catch((err) => console.log('failed to move', name, err))
}
}
}
const update: any = {
space: doc.space
}
const needStates = currentSpace ? hierarchy.isDerived(currentSpace._class, task.class.SpaceWithStates) : false const needStates = currentSpace ? hierarchy.isDerived(currentSpace._class, task.class.SpaceWithStates) : false
if (needStates) { if (needStates) {
const state = await client.findOne(task.class.State, { space: doc.space }) const state = await client.findOne(task.class.State, { space: doc.space })
@ -69,10 +57,14 @@
{ state: state._id }, { state: state._id },
{ sort: { rank: SortingOrder.Descending } } { sort: { rank: SortingOrder.Descending } }
) )
update.state = state._id await moveToSpace(client, doc, space, {
update.rank = calcRank(lastOne, undefined) state: state._id,
rank: calcRank(lastOne, undefined)
})
} else {
await moveToSpace(client, doc, space)
} }
client.updateDoc(doc._class, doc.space, doc._id, update)
dispatch('close') dispatch('close')
} }

View File

@ -20,14 +20,16 @@ import core, {
Client, Client,
Collection, Collection,
Doc, Doc,
DocumentUpdate,
Hierarchy, Hierarchy,
Lookup, Lookup,
Obj, Obj,
Ref, Ref,
RefTo, RefTo,
TxOperations,
ReverseLookup, ReverseLookup,
ReverseLookups ReverseLookups,
Space,
TxOperations
} from '@hcengineering/core' } from '@hcengineering/core'
import type { IntlString } from '@hcengineering/platform' import type { IntlString } from '@hcengineering/platform'
import { getResource } from '@hcengineering/platform' import { getResource } from '@hcengineering/platform'
@ -36,8 +38,8 @@ import {
AnyComponent, AnyComponent,
ErrorPresenter, ErrorPresenter,
getCurrentLocation, getCurrentLocation,
Location,
getPlatformColorForText, getPlatformColorForText,
Location,
locationToUrl locationToUrl
} from '@hcengineering/ui' } from '@hcengineering/ui'
import type { BuildModelOptions, Viewlet } from '@hcengineering/view' import type { BuildModelOptions, Viewlet } from '@hcengineering/view'
@ -598,3 +600,30 @@ export function cosinesim (A: number[], B: number[]): number {
const similarity = dotproduct / (mA * mB) // here you needed extra brackets const similarity = dotproduct / (mA * mB) // here you needed extra brackets
return similarity return similarity
} }
/**
* @public
*/
export async function moveToSpace (
client: TxOperations,
doc: Doc,
space: Ref<Space>,
extra?: DocumentUpdate<any>
): Promise<void> {
const hierarchy = client.getHierarchy()
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>
const allAttached = await client.findAll(collection.of, { attachedTo: doc._id })
for (const attached of allAttached) {
// Do not use extra for childs.
await moveToSpace(client, attached, space).catch((err) => console.log('failed to move', name, err))
}
}
}
await client.update(doc, {
space,
...extra
})
}