mirror of
https://github.com/hcengineering/platform.git
synced 2024-12-22 19:11:33 +03: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
|
||||
}
|
||||
})
|
||||
|
||||
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'
|
||||
|
@ -29,10 +29,12 @@ export default mergeIds(recruitId, recruit, {
|
||||
CreateGlobalApplication: '' as Ref<Action>,
|
||||
CopyApplicationId: '' as Ref<Action>,
|
||||
CopyApplicationLink: '' as Ref<Action>,
|
||||
CopyCandidateLink: '' as Ref<Action>
|
||||
CopyCandidateLink: '' as Ref<Action>,
|
||||
MoveApplicant: '' as Ref<Action>
|
||||
},
|
||||
actionImpl: {
|
||||
CreateOpinion: '' as ViewAction
|
||||
CreateOpinion: '' as ViewAction,
|
||||
MoveApplicant: '' as ViewAction
|
||||
},
|
||||
category: {
|
||||
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) {
|
||||
q.result = await q.result
|
||||
}
|
||||
const match = await this.findOne(q._class, { $search: q.query.$search, _id }, q.options)
|
||||
if (match === undefined) {
|
||||
if (q.options?.limit === q.result.length) {
|
||||
await this.refresh(q)
|
||||
return true
|
||||
} else {
|
||||
const pos = q.result.findIndex((p) => p._id === _id)
|
||||
q.result.splice(pos, 1)
|
||||
q.total--
|
||||
}
|
||||
} else {
|
||||
const pos = q.result.findIndex((p) => p._id === _id)
|
||||
q.result[pos] = match
|
||||
}
|
||||
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) {
|
||||
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)) {
|
||||
q.result[pos] = current
|
||||
} else {
|
||||
@ -279,10 +283,11 @@ export class LiveQuery extends TxProcessor implements Client {
|
||||
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) {
|
||||
q.result = await q.result
|
||||
}
|
||||
const pos = q.result.findIndex((p) => p._id === updatedDoc._id)
|
||||
if (!this.match(q, updatedDoc)) {
|
||||
if (q.options?.limit === q.result.length) {
|
||||
await this.refresh(q)
|
||||
@ -315,21 +320,22 @@ export class LiveQuery extends TxProcessor implements Client {
|
||||
if (pos !== -1) {
|
||||
// If query contains search we must check use fulltext
|
||||
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 {}
|
||||
} else {
|
||||
const updatedDoc = q.result[pos]
|
||||
if (updatedDoc.modifiedOn < tx.modifiedOn) {
|
||||
await this.__updateMixinDoc(q, updatedDoc, tx)
|
||||
const updateRefresh = await this.checkUpdatedDocMatch(q, pos, updatedDoc)
|
||||
const updateRefresh = await this.checkUpdatedDocMatch(q, updatedDoc)
|
||||
if (updateRefresh) return {}
|
||||
} else {
|
||||
const currentRefresh = await this.getCurrentDoc(q, pos, updatedDoc._id)
|
||||
const currentRefresh = await this.getCurrentDoc(q, updatedDoc._id)
|
||||
if (currentRefresh) return {}
|
||||
}
|
||||
}
|
||||
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) {
|
||||
// Mixin potentially added to object we doesn't have in out results
|
||||
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 query contains search we must check use fulltext
|
||||
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
|
||||
} else {
|
||||
const updatedDoc = q.result[pos]
|
||||
if (updatedDoc.modifiedOn < tx.modifiedOn) {
|
||||
await this.__updateDoc(q, updatedDoc, tx)
|
||||
const updateRefresh = await this.checkUpdatedDocMatch(q, pos, updatedDoc)
|
||||
const updateRefresh = await this.checkUpdatedDocMatch(q, updatedDoc)
|
||||
if (updateRefresh) return
|
||||
} else {
|
||||
const currentRefresh = await this.getCurrentDoc(q, pos, updatedDoc._id)
|
||||
const currentRefresh = await this.getCurrentDoc(q, updatedDoc._id)
|
||||
if (currentRefresh) return
|
||||
}
|
||||
}
|
||||
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)) {
|
||||
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)
|
||||
}
|
||||
@ -953,10 +961,13 @@ export class LiveQuery extends TxProcessor implements Client {
|
||||
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[]
|
||||
|
||||
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) {
|
||||
return await this.refresh(q)
|
||||
}
|
||||
|
@ -14,12 +14,11 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<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 { 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 element: PopupAlignment | undefined
|
||||
export let onClose: ((result: any) => void) | undefined
|
||||
@ -68,18 +67,16 @@
|
||||
_close(undefined)
|
||||
}
|
||||
|
||||
const fitPopup = (): void => {
|
||||
if (modalHTML) {
|
||||
if ((fullSize || docSize) && element === 'float') {
|
||||
options = fitPopupElement(modalHTML, 'full')
|
||||
options.props.maxHeight = '100vh'
|
||||
if (!modalHTML.classList.contains('fullsize')) modalHTML.classList.add('fullsize')
|
||||
} else {
|
||||
options = fitPopupElement(modalHTML, element)
|
||||
if (modalHTML.classList.contains('fullsize')) modalHTML.classList.remove('fullsize')
|
||||
}
|
||||
options.fullSize = fullSize
|
||||
const fitPopup = (modalHTML: HTMLElement, element: PopupAlignment | undefined): void => {
|
||||
if ((fullSize || docSize) && element === 'float') {
|
||||
options = fitPopupElement(modalHTML, 'full')
|
||||
options.props.maxHeight = '100vh'
|
||||
if (!modalHTML.classList.contains('fullsize')) modalHTML.classList.add('fullsize')
|
||||
} else {
|
||||
options = fitPopupElement(modalHTML, element)
|
||||
if (modalHTML.classList.contains('fullsize')) modalHTML.classList.remove('fullsize')
|
||||
}
|
||||
options.fullSize = fullSize
|
||||
}
|
||||
|
||||
function handleKeydown (ev: KeyboardEvent) {
|
||||
@ -102,19 +99,35 @@
|
||||
const alignment: PopupPositionElement = element as PopupPositionElement
|
||||
let showing: boolean | undefined = alignment?.kind === 'submenu' ? undefined : false
|
||||
|
||||
onMount(() => {
|
||||
fitPopup()
|
||||
setTimeout(() => {
|
||||
modalHTML.addEventListener('transitionend', () => (showing = undefined), { once: true })
|
||||
showing = true
|
||||
}, 0)
|
||||
})
|
||||
let oldModalHTML: HTMLElement | undefined = undefined
|
||||
|
||||
$: if (modalHTML !== undefined && oldModalHTML !== modalHTML) {
|
||||
oldModalHTML = modalHTML
|
||||
fitPopup(modalHTML, element)
|
||||
showing = true
|
||||
modalHTML.addEventListener(
|
||||
'transitionend',
|
||||
() => {
|
||||
showing = undefined
|
||||
},
|
||||
{ once: true }
|
||||
)
|
||||
}
|
||||
|
||||
$: if ($deviceInfo.docWidth <= 900 && !docSize) docSize = true
|
||||
$: if ($deviceInfo.docWidth > 900 && docSize) docSize = false
|
||||
</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
|
||||
class="popup {showing === undefined ? 'endShow' : showing === false ? 'preShow' : 'startShow'}"
|
||||
class:anim={element === 'float'}
|
||||
@ -143,10 +156,10 @@
|
||||
on:close={(ev) => _close(ev?.detail)}
|
||||
on:fullsize={() => {
|
||||
fullSize = !fullSize
|
||||
fitPopup()
|
||||
fitPopup(modalHTML, element)
|
||||
}}
|
||||
on:changeContent={() => {
|
||||
fitPopup()
|
||||
fitPopup(modalHTML, element)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
@ -55,14 +55,15 @@ export function showPopup (
|
||||
return popups
|
||||
})
|
||||
}
|
||||
const _element = element instanceof HTMLElement ? getPopupPositionElement(element) : element
|
||||
if (typeof component === 'string') {
|
||||
getResource(component)
|
||||
.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))
|
||||
} 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
|
||||
}
|
||||
@ -352,7 +353,8 @@ export function getPopupPositionElement (
|
||||
return undefined
|
||||
}
|
||||
export function getEventPositionElement (evt: MouseEvent): PopupAlignment | undefined {
|
||||
const rect = DOMRect.fromRect({ width: 1, height: 1, x: evt.clientX, y: evt.clientY })
|
||||
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",
|
||||
"Score": "Score",
|
||||
"Match": "Match",
|
||||
"PerformMatch": "Match"
|
||||
"PerformMatch": "Match",
|
||||
"MoveApplication": "Move to another vacancy"
|
||||
},
|
||||
"status": {
|
||||
"TalentRequired": "Please select talent",
|
||||
|
@ -106,7 +106,8 @@
|
||||
"VacancyMatching": "Подбор кандидатов на вакансию",
|
||||
"Score": "Оценка",
|
||||
"Match": "Совпадение",
|
||||
"PerformMatch": "Сопоставить"
|
||||
"PerformMatch": "Сопоставить",
|
||||
"MoveApplication": "Поменять Вакансию"
|
||||
},
|
||||
"status": {
|
||||
"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 { Vacancy } from '@hcengineering/recruit'
|
||||
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 { createEventDispatcher } from 'svelte'
|
||||
import recruit from '../plugin'
|
||||
import tracker from '@hcengineering/tracker'
|
||||
|
||||
export let _id: Ref<Vacancy>
|
||||
|
||||
@ -132,6 +133,7 @@
|
||||
space={object.space}
|
||||
attachments={object.attachments ?? 0}
|
||||
/>
|
||||
<Component is={tracker.component.RelatedIssuesSection} props={{ object, label: recruit.string.RelatedIssues }} />
|
||||
</Grid>
|
||||
</Panel>
|
||||
{/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 MatchVacancy from './components/MatchVacancy.svelte'
|
||||
|
||||
import { MoveApplicant } from './actionImpl'
|
||||
|
||||
async function createOpinion (object: Doc): Promise<void> {
|
||||
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> => ({
|
||||
actionImpl: {
|
||||
CreateOpinion: createOpinion
|
||||
CreateOpinion: createOpinion,
|
||||
MoveApplicant
|
||||
},
|
||||
validator: {
|
||||
ApplicantValidator: applicantValidator
|
||||
|
@ -117,7 +117,8 @@ export default mergeIds(recruitId, recruit, {
|
||||
VacancyMatching: '' as IntlString,
|
||||
Score: '' as IntlString,
|
||||
Match: '' as IntlString,
|
||||
PerformMatch: '' as IntlString
|
||||
PerformMatch: '' as IntlString,
|
||||
MoveApplication: '' as IntlString
|
||||
},
|
||||
space: {
|
||||
CandidatesPublic: '' as Ref<Space>
|
||||
|
@ -64,7 +64,7 @@
|
||||
<Icon icon={tracker.icon.Issues} size={'small'} />
|
||||
</div>
|
||||
{/if}
|
||||
<span title={value?.title}>
|
||||
<span class="select-text" title={value?.title}>
|
||||
{title}
|
||||
</span>
|
||||
</span>
|
||||
|
@ -36,7 +36,7 @@
|
||||
<span class="titlePresenter-container" class:with-margin={shouldUseMargin} title={value.title}>
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<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%'}
|
||||
on:click={handleIssueEditorOpened}>{value.title}</span
|
||||
>
|
||||
|
@ -14,15 +14,16 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Label, Button, Status as StatusControl } from '@hcengineering/ui'
|
||||
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 task, { calcRank, Task } from '@hcengineering/task'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import view from '../plugin'
|
||||
import task, { Task, calcRank } from '@hcengineering/task'
|
||||
import { getResource, OK, Resource, Status, translate } from '@hcengineering/platform'
|
||||
import { moveToSpace } from '../utils'
|
||||
|
||||
export let selected: Doc | Doc[]
|
||||
$: docs = Array.isArray(selected) ? selected : [selected]
|
||||
@ -45,19 +46,6 @@
|
||||
$: _class && translate(_class, {}).then((res) => (classLabel = res.toLocaleLowerCase()))
|
||||
|
||||
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
|
||||
if (needStates) {
|
||||
const state = await client.findOne(task.class.State, { space: doc.space })
|
||||
@ -69,10 +57,14 @@
|
||||
{ state: state._id },
|
||||
{ sort: { rank: SortingOrder.Descending } }
|
||||
)
|
||||
update.state = state._id
|
||||
update.rank = calcRank(lastOne, undefined)
|
||||
await moveToSpace(client, doc, space, {
|
||||
state: state._id,
|
||||
rank: calcRank(lastOne, undefined)
|
||||
})
|
||||
} else {
|
||||
await moveToSpace(client, doc, space)
|
||||
}
|
||||
client.updateDoc(doc._class, doc.space, doc._id, update)
|
||||
|
||||
dispatch('close')
|
||||
}
|
||||
|
||||
|
@ -20,14 +20,16 @@ import core, {
|
||||
Client,
|
||||
Collection,
|
||||
Doc,
|
||||
DocumentUpdate,
|
||||
Hierarchy,
|
||||
Lookup,
|
||||
Obj,
|
||||
Ref,
|
||||
RefTo,
|
||||
TxOperations,
|
||||
ReverseLookup,
|
||||
ReverseLookups
|
||||
ReverseLookups,
|
||||
Space,
|
||||
TxOperations
|
||||
} from '@hcengineering/core'
|
||||
import type { IntlString } from '@hcengineering/platform'
|
||||
import { getResource } from '@hcengineering/platform'
|
||||
@ -36,8 +38,8 @@ import {
|
||||
AnyComponent,
|
||||
ErrorPresenter,
|
||||
getCurrentLocation,
|
||||
Location,
|
||||
getPlatformColorForText,
|
||||
Location,
|
||||
locationToUrl
|
||||
} from '@hcengineering/ui'
|
||||
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
|
||||
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