mirror of
https://github.com/hcengineering/platform.git
synced 2025-04-20 07:10:02 +00:00
TSK-608: Move Vacancy support. (#2597)
Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
parent
472cb40dda
commit
243bc1dede
@ -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'
|
||||||
|
@ -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>
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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",
|
||||||
|
@ -106,7 +106,8 @@
|
|||||||
"VacancyMatching": "Подбор кандидатов на вакансию",
|
"VacancyMatching": "Подбор кандидатов на вакансию",
|
||||||
"Score": "Оценка",
|
"Score": "Оценка",
|
||||||
"Match": "Совпадение",
|
"Match": "Совпадение",
|
||||||
"PerformMatch": "Сопоставить"
|
"PerformMatch": "Сопоставить",
|
||||||
|
"MoveApplication": "Поменять Вакансию"
|
||||||
},
|
},
|
||||||
"status": {
|
"status": {
|
||||||
"TalentRequired": "Пожалуйста выберите таланта",
|
"TalentRequired": "Пожалуйста выберите таланта",
|
||||||
|
7
plugins/recruit-resources/src/actionImpl.ts
Normal file
7
plugins/recruit-resources/src/actionImpl.ts
Normal 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] })
|
||||||
|
}
|
@ -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}
|
||||||
|
265
plugins/recruit-resources/src/components/MoveApplication.svelte
Normal file
265
plugins/recruit-resources/src/components/MoveApplication.svelte
Normal 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>
|
@ -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
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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
|
||||||
>
|
>
|
||||||
|
@ -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')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user