mirror of
https://github.com/hcengineering/platform.git
synced 2024-12-22 19:11:33 +03:00
TSK-344: Draft for new Candidate/Person etc (#2432)
This commit is contained in:
parent
fdeba8ea65
commit
2a4661748d
@ -224,7 +224,8 @@ export function createModel (builder: Builder): void {
|
||||
icon: contact.icon.Person,
|
||||
label: recruit.string.Talents,
|
||||
createLabel: recruit.string.TalentCreateLabel,
|
||||
createComponent: recruit.component.CreateCandidate
|
||||
createComponent: recruit.component.CreateCandidate,
|
||||
createComponentProps: { shouldSaveDraft: true }
|
||||
},
|
||||
position: 'vacancy'
|
||||
},
|
||||
|
@ -64,7 +64,7 @@
|
||||
|
||||
async function showSelectionPopup (e: MouseEvent) {
|
||||
if (!disabled) {
|
||||
showPopup(SelectAvatarPopup, { avatar, email, id, icon, onSubmit: handlePopupSubmit })
|
||||
showPopup(SelectAvatarPopup, { avatar, email, id, file: direct, icon, onSubmit: handlePopupSubmit })
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
@ -35,6 +35,9 @@
|
||||
const [schema, uri] = avatar?.split('://') || []
|
||||
|
||||
const initialSelectedType = (() => {
|
||||
if (file) {
|
||||
return AvatarType.IMAGE
|
||||
}
|
||||
if (!avatar) {
|
||||
return AvatarType.COLOR
|
||||
}
|
||||
@ -117,6 +120,7 @@
|
||||
selectedFile = undefined
|
||||
} else {
|
||||
selectedFile = blob
|
||||
selectedAvatarType = AvatarType.IMAGE
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -22,7 +22,7 @@
|
||||
if (component) {
|
||||
action = async () => {
|
||||
closePopup()
|
||||
showPopup(component, {}, 'top')
|
||||
showPopup(component, { shouldSaveDraft: true }, 'top')
|
||||
}
|
||||
} else if (create) {
|
||||
action = await getResource(create)
|
||||
|
@ -20,6 +20,9 @@
|
||||
"Talent": "Talent",
|
||||
"TalentCreateLabel": "Talent",
|
||||
"CreateTalent": "New Talent",
|
||||
"CreateTalentDialogClose": "Do you want to close this dialog?",
|
||||
"CreateTalentDialogCloseNote": "All changes will be lost",
|
||||
"ResumeDraft": "Resume draft",
|
||||
"AssignRecruiter": "Assign recruiter",
|
||||
"UnAssignRecruiter": "Unassign recruiter",
|
||||
"UnAssignCompany": "Unassign Company",
|
||||
|
@ -17,8 +17,11 @@
|
||||
"CreateApplication": "Новый Кандидат",
|
||||
"SelectVacancy": "Выбрать вакансию",
|
||||
"Talent": "Талант",
|
||||
"TalentCreateLabel": "Таланта",
|
||||
"TalentCreateLabel": "Талант",
|
||||
"CreateTalent": "Новый Талант",
|
||||
"CreateTalentDialogClose": "Вы действительно хотите закрыть окно?",
|
||||
"CreateTalentDialogCloseNote": "Все внесенные изменения будут потеряны",
|
||||
"ResumeDraft": "Восстановить черновик",
|
||||
"AssignRecruiter": "Назначить рекрутера",
|
||||
"UnAssignRecruiter": "Отменить назначение рекрутера",
|
||||
"UnAssignCompany": "Отменить назначение компании",
|
||||
|
@ -14,6 +14,7 @@
|
||||
-->
|
||||
<script lang="ts">
|
||||
import attachment from '@hcengineering/attachment'
|
||||
import { deleteFile } from '@hcengineering/attachment-resources/src/utils'
|
||||
import contact, { Channel, ChannelProvider, combineName, findContacts, Person } from '@hcengineering/contact'
|
||||
import { ChannelsDropdown } from '@hcengineering/contact-resources'
|
||||
import PersonPresenter from '@hcengineering/contact-resources/src/components/PersonPresenter.svelte'
|
||||
@ -36,10 +37,13 @@
|
||||
EditableAvatar,
|
||||
getClient,
|
||||
getFileUrl,
|
||||
getUserDraft,
|
||||
KeyedAttribute,
|
||||
PDFViewer
|
||||
MessageBox,
|
||||
PDFViewer,
|
||||
updateUserDraft
|
||||
} from '@hcengineering/presentation'
|
||||
import type { Candidate } from '@hcengineering/recruit'
|
||||
import type { Candidate, CandidateDraft } from '@hcengineering/recruit'
|
||||
import { recognizeDocument } from '@hcengineering/rekoni'
|
||||
import tags, { findTagCategory, TagElement, TagReference } from '@hcengineering/tags'
|
||||
import {
|
||||
@ -56,24 +60,24 @@
|
||||
showPopup,
|
||||
Spinner
|
||||
} from '@hcengineering/ui'
|
||||
import deepEqual from 'deep-equal'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import recruit from '../plugin'
|
||||
import FileUpload from './icons/FileUpload.svelte'
|
||||
import YesNo from './YesNo.svelte'
|
||||
|
||||
let firstName = ''
|
||||
let lastName = ''
|
||||
let createMore: boolean = false
|
||||
export let shouldSaveDraft: boolean = false
|
||||
export let onDraftChanged: () => void
|
||||
|
||||
export function canClose (): boolean {
|
||||
return firstName === '' && lastName === '' && resume.uuid === undefined
|
||||
const draft: CandidateDraft | undefined = shouldSaveDraft ? getUserDraft(recruit.mixin.Candidate) : undefined
|
||||
const emptyObject = {
|
||||
title: '',
|
||||
city: '',
|
||||
avatar: undefined,
|
||||
onsite: undefined,
|
||||
remote: undefined
|
||||
}
|
||||
|
||||
let avatarEditor: EditableAvatar
|
||||
|
||||
let object: Candidate = {} as Candidate
|
||||
|
||||
const resume = {} as {
|
||||
type resumeFile = {
|
||||
name: string
|
||||
uuid: string
|
||||
size: number
|
||||
@ -81,21 +85,58 @@
|
||||
lastModified: number
|
||||
}
|
||||
|
||||
let candidateId = draft ? draft.candidateId : generateId()
|
||||
let firstName = draft?.firstName || ''
|
||||
let lastName = draft?.lastName || ''
|
||||
let createMore: boolean = false
|
||||
let saveTimer: number | undefined
|
||||
|
||||
export function canClose (): boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
let avatarEditor: EditableAvatar
|
||||
|
||||
function toCandidate (draft: CandidateDraft | undefined): Candidate {
|
||||
if (!draft) {
|
||||
return emptyObject as Candidate
|
||||
}
|
||||
return {
|
||||
title: draft?.title || '',
|
||||
city: draft?.city || '',
|
||||
onsite: draft?.onsite,
|
||||
remote: draft?.remote
|
||||
} as Candidate
|
||||
}
|
||||
|
||||
let object: Candidate = toCandidate(draft)
|
||||
|
||||
function resumeDraft () {
|
||||
return {
|
||||
uuid: draft?.resumeUuid,
|
||||
name: draft?.resumeName,
|
||||
size: draft?.resumeSize,
|
||||
type: draft?.resumeType,
|
||||
lastModified: draft?.resumeLastModified
|
||||
}
|
||||
}
|
||||
|
||||
let resume = resumeDraft() as resumeFile
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const client = getClient()
|
||||
let candidateId = generateId()
|
||||
|
||||
let inputFile: HTMLInputElement
|
||||
let loading = false
|
||||
let dragover = false
|
||||
|
||||
let avatar: File | undefined
|
||||
let channels: AttachedData<Channel>[] = []
|
||||
let avatar: File | undefined = draft?.avatar
|
||||
let channels: AttachedData<Channel>[] = draft?.channels || []
|
||||
|
||||
let matches: WithLookup<Person>[] = []
|
||||
let matchedChannels: AttachedData<Channel>[] = []
|
||||
|
||||
let skills: TagReference[] = []
|
||||
let skills: TagReference[] = draft?.skills || []
|
||||
const key: KeyedAttribute = {
|
||||
key: 'skills',
|
||||
attr: client.getHierarchy().getAttribute(recruit.mixin.Candidate, 'skills')
|
||||
@ -122,6 +163,85 @@
|
||||
})
|
||||
})
|
||||
|
||||
$: updateDraft(object, firstName, lastName, avatar, channels, skills, resume)
|
||||
|
||||
async function updateDraft (...param: any) {
|
||||
if (saveTimer) {
|
||||
clearTimeout(saveTimer)
|
||||
}
|
||||
saveTimer = setTimeout(() => {
|
||||
saveDraft()
|
||||
}, 200)
|
||||
}
|
||||
|
||||
async function saveDraft () {
|
||||
if (!shouldSaveDraft) {
|
||||
return
|
||||
}
|
||||
|
||||
let newDraft: Data<CandidateDraft> | undefined = createDraftFromObject()
|
||||
const isEmpty = await isDraftEmpty(newDraft)
|
||||
|
||||
if (isEmpty) {
|
||||
newDraft = undefined
|
||||
}
|
||||
|
||||
updateUserDraft(recruit.mixin.Candidate, newDraft)
|
||||
|
||||
if (onDraftChanged) {
|
||||
return onDraftChanged()
|
||||
}
|
||||
}
|
||||
|
||||
function createDraftFromObject () {
|
||||
const newDraft: Data<CandidateDraft> = {
|
||||
candidateId: candidateId as Ref<Candidate>,
|
||||
firstName,
|
||||
lastName,
|
||||
title: object.title,
|
||||
city: object.city,
|
||||
resumeUuid: resume?.uuid,
|
||||
resumeName: resume?.name,
|
||||
resumeType: resume?.type,
|
||||
resumeSize: resume?.size,
|
||||
resumeLastModified: resume?.lastModified,
|
||||
avatar,
|
||||
channels,
|
||||
onsite: object.onsite,
|
||||
remote: object.remote,
|
||||
skills
|
||||
}
|
||||
|
||||
return newDraft
|
||||
}
|
||||
|
||||
async function isDraftEmpty (draft: Data<CandidateDraft>): Promise<boolean> {
|
||||
const emptyDraft: Partial<CandidateDraft> = {
|
||||
firstName: '',
|
||||
lastName: '',
|
||||
title: '',
|
||||
city: '',
|
||||
resumeUuid: undefined,
|
||||
resumeName: undefined,
|
||||
resumeType: undefined,
|
||||
resumeSize: undefined,
|
||||
resumeLastModified: undefined,
|
||||
avatar: undefined,
|
||||
channels: [],
|
||||
onsite: undefined,
|
||||
remote: undefined,
|
||||
skills: []
|
||||
}
|
||||
|
||||
for (const key of Object.keys(emptyDraft)) {
|
||||
if (!deepEqual((emptyDraft as any)[key], (draft as any)[key])) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
async function createCandidate () {
|
||||
const candidate: Data<Person> = {
|
||||
name: combineName(firstName, lastName),
|
||||
@ -202,17 +322,11 @@
|
||||
})
|
||||
}
|
||||
|
||||
if (createMore) {
|
||||
// Prepare for next
|
||||
object = {} as Candidate
|
||||
candidateId = generateId()
|
||||
avatar = undefined
|
||||
firstName = ''
|
||||
lastName = ''
|
||||
channels = []
|
||||
} else {
|
||||
if (!createMore) {
|
||||
dispatch('close', id)
|
||||
}
|
||||
resetObject()
|
||||
saveDraft()
|
||||
}
|
||||
|
||||
function isUndef (value?: string): boolean {
|
||||
@ -327,6 +441,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteResume (): Promise<void> {
|
||||
if (resume.uuid) {
|
||||
await deleteFile(resume.uuid)
|
||||
}
|
||||
}
|
||||
|
||||
async function createAttachment (file: File) {
|
||||
loading = true
|
||||
try {
|
||||
@ -398,6 +518,52 @@
|
||||
})
|
||||
|
||||
const manager = createFocusManager()
|
||||
|
||||
function resetObject (): void {
|
||||
object = emptyObject as Candidate
|
||||
candidateId = generateId()
|
||||
avatar = undefined
|
||||
firstName = ''
|
||||
lastName = ''
|
||||
channels = []
|
||||
skills = []
|
||||
resume = {} as resumeFile
|
||||
}
|
||||
|
||||
export async function onOutsideClick () {
|
||||
saveDraft()
|
||||
|
||||
if (onDraftChanged) {
|
||||
return onDraftChanged()
|
||||
}
|
||||
}
|
||||
|
||||
async function showConfirmationDialog () {
|
||||
const newDraft = createDraftFromObject()
|
||||
const isFormEmpty = await isDraftEmpty(newDraft)
|
||||
|
||||
if (isFormEmpty) {
|
||||
console.log('isFormEmpty')
|
||||
dispatch('close')
|
||||
} else {
|
||||
showPopup(
|
||||
MessageBox,
|
||||
{
|
||||
label: recruit.string.CreateTalentDialogClose,
|
||||
message: recruit.string.CreateTalentDialogCloseNote
|
||||
},
|
||||
'top',
|
||||
(result?: boolean) => {
|
||||
if (result === true) {
|
||||
dispatch('close')
|
||||
deleteResume()
|
||||
resetObject()
|
||||
saveDraft()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<FocusHandler {manager} />
|
||||
@ -409,6 +575,7 @@
|
||||
on:close={() => {
|
||||
dispatch('close')
|
||||
}}
|
||||
onCancel={showConfirmationDialog}
|
||||
bind:createMore
|
||||
>
|
||||
<svelte:fragment slot="header">
|
||||
|
@ -13,12 +13,19 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { isUserDraftExists } from '@hcengineering/presentation'
|
||||
import { Button, showPopup } from '@hcengineering/ui'
|
||||
import recruit from '../plugin'
|
||||
import CreateCandidate from './CreateCandidate.svelte'
|
||||
|
||||
async function newIssue (): Promise<void> {
|
||||
showPopup(CreateCandidate, {}, 'top')
|
||||
let draftExists: boolean = isUserDraftExists(recruit.mixin.Candidate)
|
||||
|
||||
const handleDraftChanged = () => {
|
||||
draftExists = isUserDraftExists(recruit.mixin.Candidate)
|
||||
}
|
||||
|
||||
async function newCandidate (): Promise<void> {
|
||||
showPopup(CreateCandidate, { shouldSaveDraft: true, onDraftChanged: handleDraftChanged }, 'top')
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -26,10 +33,29 @@
|
||||
<div class="flex-grow text-md">
|
||||
<Button
|
||||
icon={recruit.icon.CreateCandidate}
|
||||
label={recruit.string.CreateTalent}
|
||||
label={draftExists ? recruit.string.ResumeDraft : recruit.string.CreateTalent}
|
||||
justify={'left'}
|
||||
width={'100%'}
|
||||
on:click={newIssue}
|
||||
/>
|
||||
on:click={newCandidate}
|
||||
>
|
||||
<div slot="content" class="draft-circle-container">
|
||||
{#if draftExists}
|
||||
<div class="draft-circle" />
|
||||
{/if}
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.draft-circle-container {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.draft-circle {
|
||||
height: 6px;
|
||||
width: 6px;
|
||||
background-color: var(--primary-bg-color);
|
||||
border-radius: 50%;
|
||||
}
|
||||
</style>
|
||||
|
@ -48,6 +48,9 @@ export default mergeIds(recruitId, recruit, {
|
||||
Talent: '' as IntlString,
|
||||
TalentCreateLabel: '' as IntlString,
|
||||
CreateTalent: '' as IntlString,
|
||||
CreateTalentDialogClose: '' as IntlString,
|
||||
CreateTalentDialogCloseNote: '' as IntlString,
|
||||
ResumeDraft: '' as IntlString,
|
||||
AssignRecruiter: '' as IntlString,
|
||||
Recruiters: '' as IntlString,
|
||||
UnAssignRecruiter: '' as IntlString,
|
||||
|
@ -32,6 +32,7 @@
|
||||
"@hcengineering/chunter": "~0.6.1",
|
||||
"@hcengineering/task": "~0.6.0",
|
||||
"@hcengineering/calendar": "~0.6.1",
|
||||
"@hcengineering/ui": "^0.6.2"
|
||||
"@hcengineering/ui": "^0.6.2",
|
||||
"@hcengineering/tags": "~0.6.2"
|
||||
}
|
||||
}
|
||||
|
@ -14,12 +14,13 @@
|
||||
//
|
||||
|
||||
import { Event } from '@hcengineering/calendar'
|
||||
import type { Organization, Person } from '@hcengineering/contact'
|
||||
import type { AttachedDoc, Class, Doc, Mixin, Ref, Space, Timestamp } from '@hcengineering/core'
|
||||
import type { Channel, Organization, Person } from '@hcengineering/contact'
|
||||
import type { AttachedData, AttachedDoc, Class, Doc, Mixin, Ref, Space, Timestamp } from '@hcengineering/core'
|
||||
import type { Asset, Plugin } from '@hcengineering/platform'
|
||||
import { plugin } from '@hcengineering/platform'
|
||||
import type { KanbanTemplateSpace, SpaceWithStates, Task } from '@hcengineering/task'
|
||||
import { AnyComponent } from '@hcengineering/ui'
|
||||
import { TagReference } from '@hcengineering/tags'
|
||||
|
||||
/**
|
||||
* @public
|
||||
@ -58,6 +59,27 @@ export interface Candidate extends Person {
|
||||
reviews?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface CandidateDraft extends Doc {
|
||||
candidateId: Ref<Candidate>
|
||||
firstName?: string
|
||||
lastName?: string
|
||||
title?: string
|
||||
city: string
|
||||
resumeUuid?: string
|
||||
resumeName?: string
|
||||
resumeSize?: number
|
||||
resumeType?: string
|
||||
resumeLastModified?: number
|
||||
avatar?: File | undefined
|
||||
channels: AttachedData<Channel>[]
|
||||
onsite?: boolean
|
||||
remote?: boolean
|
||||
skills: TagReference[]
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
|
@ -43,7 +43,7 @@ test.describe('recruit tests', () => {
|
||||
// Click :nth-match(:text("Cancel"), 2)
|
||||
// await page.click('button:has-text("Cancel")')
|
||||
await page.keyboard.press('Escape')
|
||||
await page.keyboard.press('Escape')
|
||||
// await page.keyboard.press('Escape')
|
||||
// Click button:has-text("Create")
|
||||
await page.click('button:has-text("Create")')
|
||||
await page.waitForSelector('form.antiCard', { state: 'detached' })
|
||||
|
Loading…
Reference in New Issue
Block a user