platform/plugins/recruit-resources/src/components/CreateVacancy.svelte
Chunosov 417a6ac084
remove spaces from vacancy name (#7417)
Signed-off-by: Nikolay Chunosov <Chunosov.N@gmail.com>
2024-12-10 17:47:53 +07:00

446 lines
13 KiB
Svelte

<!--
// Copyright © 2020 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 { AttachmentStyledBox } from '@hcengineering/attachment-resources'
import contact, { Organization } from '@hcengineering/contact'
import { AccountArrayEditor, UserBox } from '@hcengineering/contact-resources'
import core, {
Account,
AttachedData,
Data,
Ref,
Role,
RolesAssignment,
SortingOrder,
fillDefaults,
generateId,
getCurrentAccount,
makeCollabId
} from '@hcengineering/core'
import { getEmbeddedLabel } from '@hcengineering/platform'
import {
Card,
InlineAttributeBar,
MessageBox,
createMarkup,
createQuery,
getClient
} from '@hcengineering/presentation'
import { RecruitEvents, Vacancy, Vacancy as VacancyClass } from '@hcengineering/recruit'
import tags from '@hcengineering/tags'
import task, { ProjectType, makeRank } from '@hcengineering/task'
import { selectedTypeStore, typeStore } from '@hcengineering/task-resources'
import tracker, { Issue, IssueStatus, IssueTemplate, IssueTemplateData, Project } from '@hcengineering/tracker'
import {
Button,
Component,
EditBox,
FocusHandler,
IconAttachment,
createFocusManager,
showPopup
} from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
import recruit from '../plugin'
import Company from './icons/Company.svelte'
import VacancyIcon from './icons/Vacancy.svelte'
import { Analytics } from '@hcengineering/analytics'
import { getSequenceId } from '../utils'
import { isEmptyMarkup } from '@hcengineering/text'
const dispatch = createEventDispatcher()
let name: string = ''
let template: ProjectType | undefined
let typeId: Ref<ProjectType> | undefined = $selectedTypeStore
$: typeType = typeId !== undefined ? $typeStore.get(typeId) : undefined
let appliedTemplateId: Ref<ProjectType> | undefined
let objectId: Ref<VacancyClass> = generateId()
let issueTemplates: IssueTemplate[] = []
let fullDescription: string = ''
let members = [getCurrentAccount()._id]
let membersChanged: boolean = false
$: setDefaultMembers(typeType)
function setDefaultMembers (typeType: ProjectType | undefined): void {
if (typeType === undefined) return
if (membersChanged) return
if (typeType.members === undefined || typeType.members.length === 0) return
members = typeType.members
}
export let company: Ref<Organization> | undefined
export let preserveCompany: boolean = false
let vacancyData: Data<VacancyClass> = {
archived: false,
description: '',
members,
name: '',
number: 0,
private: false,
attachments: 0,
comments: 0,
company: '' as Ref<Organization>,
fullDescription: null,
location: '',
type: typeId as Ref<ProjectType>
}
export function canClose (): boolean {
return name.trim() === '' && typeId !== undefined
}
const client = getClient()
const hierarchy = client.getHierarchy()
const templateQ = createQuery()
fillDefaults(hierarchy, vacancyData, recruit.class.Vacancy)
$: typeId &&
templateQ.query(task.class.ProjectType, { _id: typeId }, (result) => {
const { _class, _id, description, targetClass, ...templateData } = result[0]
vacancyData = { ...(templateData as unknown as Data<VacancyClass>) }
if (appliedTemplateId !== typeId) {
fullDescription = description ?? ''
appliedTemplateId = typeId
}
fillDefaults(hierarchy, vacancyData, recruit.class.Vacancy)
})
const issueTemplatesQ = createQuery()
$: issueTemplatesQ.query(tracker.class.IssueTemplate, { 'relations._id': typeId }, async (result) => {
issueTemplates = result
})
let rolesAssignment: RolesAssignment | undefined
let roles: Role[] = []
const rolesQuery = createQuery()
$: if (typeType !== undefined) {
rolesQuery.query(
core.class.Role,
{ attachedTo: typeType._id },
(res) => {
roles = res
if (rolesAssignment === undefined) {
rolesAssignment = roles.reduce<RolesAssignment>((prev, { _id }) => {
prev[_id] = []
return prev
}, {})
}
},
{
sort: {
name: SortingOrder.Ascending
}
}
)
} else {
rolesQuery.unsubscribe()
}
async function saveIssue (
id: Ref<VacancyClass>,
space: Ref<Project>,
template: IssueTemplateData,
parent: Ref<Issue> = tracker.ids.NoParent
): Promise<Ref<Issue> | undefined> {
const lastOne = await client.findOne<Issue>(
tracker.class.Issue,
{ space },
{ sort: { rank: SortingOrder.Descending } }
)
const incResult = await client.updateDoc(
tracker.class.Project,
core.space.Space,
space,
{
$inc: { sequence: 1 }
},
true
)
const project = await client.findOne(tracker.class.Project, { _id: space })
const rank = makeRank(lastOne?.rank, undefined)
const taskType = await client.findOne(task.class.TaskType, { ofClass: tracker.class.Issue })
if (taskType === undefined) {
return
}
const number = (incResult as any).object.sequence
const resId: Ref<Issue> = generateId()
const identifier = `${project?.identifier}-${number}`
const data: AttachedData<Issue> = {
title: template.title + ` (${name.trim()})`,
description: null,
assignee: template.assignee,
component: template.component,
milestone: template.milestone,
number,
status: project?.defaultIssueStatus as Ref<IssueStatus>,
priority: template.priority,
rank,
comments: 0,
subIssues: 0,
dueDate: null,
parents: [],
reportedTime: 0,
remainingTime: 0,
estimation: template.estimation,
reports: 0,
relations: [{ _id: id, _class: recruit.class.Vacancy }],
childInfo: [],
kind: taskType._id,
identifier
}
if (!isEmptyMarkup(template.description)) {
const collabId = makeCollabId(tracker.class.Issue, resId, 'description')
data.description = await createMarkup(collabId, template.description)
}
await client.addCollection(tracker.class.Issue, space, parent, tracker.class.Issue, 'subIssues', data, resId)
if ((template.labels?.length ?? 0) > 0) {
const tagElements = await client.findAll(tags.class.TagElement, { _id: { $in: template.labels } })
for (const label of tagElements) {
await client.addCollection(tags.class.TagReference, space, resId, tracker.class.Issue, 'labels', {
title: label.title,
color: label.color,
tag: label._id
})
}
}
return resId
}
async function createVacancy (): Promise<void> {
if (typeId === undefined || typeType === undefined) {
throw Error(`Failed to find target project type: ${typeId}`)
}
const sequence = await client.findOne(task.class.Sequence, { attachedTo: recruit.class.Vacancy })
if (sequence === undefined) {
throw new Error('sequence object not found')
}
const incResult = await client.update(sequence, { $inc: { sequence: 1 } }, true)
const data: Data<Vacancy> = {
...vacancyData,
name: name.trim(),
description: template?.shortDescription ?? '',
fullDescription: null,
private: false,
archived: false,
number: (incResult as any).object.sequence,
company,
members,
autoJoin: typeType.autoJoin ?? false,
owners: [getCurrentAccount()._id],
type: typeId
}
if (!isEmptyMarkup(fullDescription)) {
const collabId = makeCollabId(recruit.class.Vacancy, objectId, 'fullDescription')
data.fullDescription = await createMarkup(collabId, fullDescription)
}
const ops = client.apply()
const id = await ops.createDoc(recruit.class.Vacancy, core.space.Space, data, objectId)
await descriptionBox.createAttachments(undefined, ops)
await ops.commit()
Analytics.handleEvent(RecruitEvents.VacancyCreated, {
id: getSequenceId({
...data,
_id: id,
_class: recruit.class.Vacancy,
space: core.space.Space,
modifiedOn: 0,
modifiedBy: getCurrentAccount()._id
})
})
if (issueTemplates.length > 0) {
for (const issueTemplate of issueTemplates) {
const issue = await saveIssue(id, issueTemplate.space, issueTemplate)
if (issue !== undefined) {
for (const sub of issueTemplate.children) {
await saveIssue(id, issueTemplate.space, sub, issue)
}
}
}
}
// Add vacancy mixin with roles assignment
await client.createMixin(
objectId,
recruit.class.Vacancy,
core.space.Space,
typeType.targetClass,
rolesAssignment ?? {}
)
objectId = generateId()
dispatch('close', id)
}
const manager = createFocusManager()
let descriptionBox: AttachmentStyledBox
function handleTypeChange (evt: CustomEvent<Ref<ProjectType>>): void {
if (typeId == null) {
typeId = evt.detail
return
}
// Template is already specified, ask to replace.
showPopup(
MessageBox,
{
label: recruit.string.TemplateReplace,
message: recruit.string.TemplateReplaceConfirm,
okLabel: recruit.string.Apply
},
'top',
(result?: boolean) => {
if (result === true) {
typeId = evt.detail ?? undefined
}
}
)
}
function handleRoleAssignmentChanged (roleId: Ref<Role>, newMembers: Ref<Account>[]): void {
if (rolesAssignment === undefined) {
rolesAssignment = {}
}
rolesAssignment[roleId] = newMembers
}
</script>
<FocusHandler {manager} />
<Card
label={recruit.string.CreateVacancy}
okAction={createVacancy}
canSave={name.trim() !== ''}
gap={'gapV-4'}
on:close={() => {
dispatch('close')
}}
on:changeContent
>
<span>{typeType?.name}</span>
<div class="flex-row-center clear-mins">
<div class="mr-3">
<Button focusIndex={1} icon={VacancyIcon} size={'medium'} kind={'link-bordered'} noFocus />
</div>
<EditBox
focusIndex={2}
bind:value={name}
placeholder={recruit.string.VacancyPlaceholder}
kind={'large-style'}
autoFocus
/>
</div>
{#key appliedTemplateId}
<AttachmentStyledBox
bind:this={descriptionBox}
{objectId}
_class={recruit.class.Vacancy}
space={objectId}
alwaysEdit
showButtons={false}
maxHeight={'card'}
bind:content={fullDescription}
placeholder={recruit.string.FullDescription}
kind={'emphasized'}
/>
{/key}
<svelte:fragment slot="header">
<UserBox
focusIndex={3}
_class={contact.class.Organization}
label={recruit.string.Company}
placeholder={recruit.string.Company}
justify={'left'}
bind:value={company}
allowDeselect
titleDeselect={recruit.string.UnAssignCompany}
kind={'regular'}
size={'large'}
icon={Company}
readonly={preserveCompany}
showNavigate={false}
create={{ component: contact.component.CreateOrganization, label: contact.string.CreateOrganization }}
/>
<Component
is={task.component.ProjectTypeSelector}
props={{
descriptors: [recruit.descriptors.VacancyType],
type: typeId,
focusIndex: 4,
kind: 'regular',
size: 'large'
}}
on:change={handleTypeChange}
/>
</svelte:fragment>
<svelte:fragment slot="pool">
<InlineAttributeBar
_class={recruit.class.Vacancy}
object={vacancyData}
toClass={core.class.Space}
ignoreKeys={['fullDescription', 'company', 'type']}
extraProps={{ showNavigate: false }}
/>
<AccountArrayEditor
bind:value={members}
label={contact.string.Members}
onChange={() => {
membersChanged = true
}}
kind={'regular'}
size={'large'}
/>
{#each roles as role}
<AccountArrayEditor
value={rolesAssignment?.[role._id] ?? []}
label={getEmbeddedLabel(role.name)}
emptyLabel={getEmbeddedLabel(role.name)}
onChange={(refs) => {
handleRoleAssignmentChanged(role._id, refs)
}}
kind={'regular'}
size={'large'}
/>
{/each}
</svelte:fragment>
<svelte:fragment slot="footer">
<Button
icon={IconAttachment}
size={'large'}
on:click={() => {
descriptionBox.handleAttach()
}}
/>
</svelte:fragment>
</Card>