TSK-344: Draft for new Candidate/Person etc (#2432)

This commit is contained in:
Denis Maslennikov 2022-12-14 11:52:41 +07:00 committed by GitHub
parent fdeba8ea65
commit 2a4661748d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 269 additions and 39 deletions

View File

@ -224,7 +224,8 @@ export function createModel (builder: Builder): void {
icon: contact.icon.Person, icon: contact.icon.Person,
label: recruit.string.Talents, label: recruit.string.Talents,
createLabel: recruit.string.TalentCreateLabel, createLabel: recruit.string.TalentCreateLabel,
createComponent: recruit.component.CreateCandidate createComponent: recruit.component.CreateCandidate,
createComponentProps: { shouldSaveDraft: true }
}, },
position: 'vacancy' position: 'vacancy'
}, },

View File

@ -64,7 +64,7 @@
async function showSelectionPopup (e: MouseEvent) { async function showSelectionPopup (e: MouseEvent) {
if (!disabled) { if (!disabled) {
showPopup(SelectAvatarPopup, { avatar, email, id, icon, onSubmit: handlePopupSubmit }) showPopup(SelectAvatarPopup, { avatar, email, id, file: direct, icon, onSubmit: handlePopupSubmit })
} }
} }
</script> </script>

View File

@ -35,6 +35,9 @@
const [schema, uri] = avatar?.split('://') || [] const [schema, uri] = avatar?.split('://') || []
const initialSelectedType = (() => { const initialSelectedType = (() => {
if (file) {
return AvatarType.IMAGE
}
if (!avatar) { if (!avatar) {
return AvatarType.COLOR return AvatarType.COLOR
} }
@ -117,6 +120,7 @@
selectedFile = undefined selectedFile = undefined
} else { } else {
selectedFile = blob selectedFile = blob
selectedAvatarType = AvatarType.IMAGE
} }
}) })
} }

View File

@ -22,7 +22,7 @@
if (component) { if (component) {
action = async () => { action = async () => {
closePopup() closePopup()
showPopup(component, {}, 'top') showPopup(component, { shouldSaveDraft: true }, 'top')
} }
} else if (create) { } else if (create) {
action = await getResource(create) action = await getResource(create)

View File

@ -20,6 +20,9 @@
"Talent": "Talent", "Talent": "Talent",
"TalentCreateLabel": "Talent", "TalentCreateLabel": "Talent",
"CreateTalent": "New Talent", "CreateTalent": "New Talent",
"CreateTalentDialogClose": "Do you want to close this dialog?",
"CreateTalentDialogCloseNote": "All changes will be lost",
"ResumeDraft": "Resume draft",
"AssignRecruiter": "Assign recruiter", "AssignRecruiter": "Assign recruiter",
"UnAssignRecruiter": "Unassign recruiter", "UnAssignRecruiter": "Unassign recruiter",
"UnAssignCompany": "Unassign Company", "UnAssignCompany": "Unassign Company",

View File

@ -17,8 +17,11 @@
"CreateApplication": "Новый Кандидат", "CreateApplication": "Новый Кандидат",
"SelectVacancy": "Выбрать вакансию", "SelectVacancy": "Выбрать вакансию",
"Talent": "Талант", "Talent": "Талант",
"TalentCreateLabel": "Таланта", "TalentCreateLabel": "Талант",
"CreateTalent": "Новый Талант", "CreateTalent": "Новый Талант",
"CreateTalentDialogClose": "Вы действительно хотите закрыть окно?",
"CreateTalentDialogCloseNote": "Все внесенные изменения будут потеряны",
"ResumeDraft": "Восстановить черновик",
"AssignRecruiter": "Назначить рекрутера", "AssignRecruiter": "Назначить рекрутера",
"UnAssignRecruiter": "Отменить назначение рекрутера", "UnAssignRecruiter": "Отменить назначение рекрутера",
"UnAssignCompany": "Отменить назначение компании", "UnAssignCompany": "Отменить назначение компании",

View File

@ -14,6 +14,7 @@
--> -->
<script lang="ts"> <script lang="ts">
import attachment from '@hcengineering/attachment' import attachment from '@hcengineering/attachment'
import { deleteFile } from '@hcengineering/attachment-resources/src/utils'
import contact, { Channel, ChannelProvider, combineName, findContacts, Person } from '@hcengineering/contact' import contact, { Channel, ChannelProvider, combineName, findContacts, Person } from '@hcengineering/contact'
import { ChannelsDropdown } from '@hcengineering/contact-resources' import { ChannelsDropdown } from '@hcengineering/contact-resources'
import PersonPresenter from '@hcengineering/contact-resources/src/components/PersonPresenter.svelte' import PersonPresenter from '@hcengineering/contact-resources/src/components/PersonPresenter.svelte'
@ -36,10 +37,13 @@
EditableAvatar, EditableAvatar,
getClient, getClient,
getFileUrl, getFileUrl,
getUserDraft,
KeyedAttribute, KeyedAttribute,
PDFViewer MessageBox,
PDFViewer,
updateUserDraft
} from '@hcengineering/presentation' } from '@hcengineering/presentation'
import type { Candidate } from '@hcengineering/recruit' import type { Candidate, CandidateDraft } from '@hcengineering/recruit'
import { recognizeDocument } from '@hcengineering/rekoni' import { recognizeDocument } from '@hcengineering/rekoni'
import tags, { findTagCategory, TagElement, TagReference } from '@hcengineering/tags' import tags, { findTagCategory, TagElement, TagReference } from '@hcengineering/tags'
import { import {
@ -56,24 +60,24 @@
showPopup, showPopup,
Spinner Spinner
} from '@hcengineering/ui' } from '@hcengineering/ui'
import deepEqual from 'deep-equal'
import { createEventDispatcher } from 'svelte' import { createEventDispatcher } from 'svelte'
import recruit from '../plugin' import recruit from '../plugin'
import FileUpload from './icons/FileUpload.svelte' import FileUpload from './icons/FileUpload.svelte'
import YesNo from './YesNo.svelte' import YesNo from './YesNo.svelte'
let firstName = '' export let shouldSaveDraft: boolean = false
let lastName = '' export let onDraftChanged: () => void
let createMore: boolean = false
export function canClose (): boolean { const draft: CandidateDraft | undefined = shouldSaveDraft ? getUserDraft(recruit.mixin.Candidate) : undefined
return firstName === '' && lastName === '' && resume.uuid === undefined const emptyObject = {
title: '',
city: '',
avatar: undefined,
onsite: undefined,
remote: undefined
} }
type resumeFile = {
let avatarEditor: EditableAvatar
let object: Candidate = {} as Candidate
const resume = {} as {
name: string name: string
uuid: string uuid: string
size: number size: number
@ -81,21 +85,58 @@
lastModified: number 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 dispatch = createEventDispatcher()
const client = getClient() const client = getClient()
let candidateId = generateId()
let inputFile: HTMLInputElement let inputFile: HTMLInputElement
let loading = false let loading = false
let dragover = false let dragover = false
let avatar: File | undefined let avatar: File | undefined = draft?.avatar
let channels: AttachedData<Channel>[] = [] let channels: AttachedData<Channel>[] = draft?.channels || []
let matches: WithLookup<Person>[] = [] let matches: WithLookup<Person>[] = []
let matchedChannels: AttachedData<Channel>[] = [] let matchedChannels: AttachedData<Channel>[] = []
let skills: TagReference[] = [] let skills: TagReference[] = draft?.skills || []
const key: KeyedAttribute = { const key: KeyedAttribute = {
key: 'skills', key: 'skills',
attr: client.getHierarchy().getAttribute(recruit.mixin.Candidate, '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 () { async function createCandidate () {
const candidate: Data<Person> = { const candidate: Data<Person> = {
name: combineName(firstName, lastName), name: combineName(firstName, lastName),
@ -202,17 +322,11 @@
}) })
} }
if (createMore) { if (!createMore) {
// Prepare for next
object = {} as Candidate
candidateId = generateId()
avatar = undefined
firstName = ''
lastName = ''
channels = []
} else {
dispatch('close', id) dispatch('close', id)
} }
resetObject()
saveDraft()
} }
function isUndef (value?: string): boolean { 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) { async function createAttachment (file: File) {
loading = true loading = true
try { try {
@ -398,6 +518,52 @@
}) })
const manager = createFocusManager() 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> </script>
<FocusHandler {manager} /> <FocusHandler {manager} />
@ -409,6 +575,7 @@
on:close={() => { on:close={() => {
dispatch('close') dispatch('close')
}} }}
onCancel={showConfirmationDialog}
bind:createMore bind:createMore
> >
<svelte:fragment slot="header"> <svelte:fragment slot="header">

View File

@ -13,12 +13,19 @@
// limitations under the License. // limitations under the License.
--> -->
<script lang="ts"> <script lang="ts">
import { isUserDraftExists } from '@hcengineering/presentation'
import { Button, showPopup } from '@hcengineering/ui' import { Button, showPopup } from '@hcengineering/ui'
import recruit from '../plugin' import recruit from '../plugin'
import CreateCandidate from './CreateCandidate.svelte' import CreateCandidate from './CreateCandidate.svelte'
async function newIssue (): Promise<void> { let draftExists: boolean = isUserDraftExists(recruit.mixin.Candidate)
showPopup(CreateCandidate, {}, 'top')
const handleDraftChanged = () => {
draftExists = isUserDraftExists(recruit.mixin.Candidate)
}
async function newCandidate (): Promise<void> {
showPopup(CreateCandidate, { shouldSaveDraft: true, onDraftChanged: handleDraftChanged }, 'top')
} }
</script> </script>
@ -26,10 +33,29 @@
<div class="flex-grow text-md"> <div class="flex-grow text-md">
<Button <Button
icon={recruit.icon.CreateCandidate} icon={recruit.icon.CreateCandidate}
label={recruit.string.CreateTalent} label={draftExists ? recruit.string.ResumeDraft : recruit.string.CreateTalent}
justify={'left'} justify={'left'}
width={'100%'} 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>
</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>

View File

@ -48,6 +48,9 @@ export default mergeIds(recruitId, recruit, {
Talent: '' as IntlString, Talent: '' as IntlString,
TalentCreateLabel: '' as IntlString, TalentCreateLabel: '' as IntlString,
CreateTalent: '' as IntlString, CreateTalent: '' as IntlString,
CreateTalentDialogClose: '' as IntlString,
CreateTalentDialogCloseNote: '' as IntlString,
ResumeDraft: '' as IntlString,
AssignRecruiter: '' as IntlString, AssignRecruiter: '' as IntlString,
Recruiters: '' as IntlString, Recruiters: '' as IntlString,
UnAssignRecruiter: '' as IntlString, UnAssignRecruiter: '' as IntlString,

View File

@ -32,6 +32,7 @@
"@hcengineering/chunter": "~0.6.1", "@hcengineering/chunter": "~0.6.1",
"@hcengineering/task": "~0.6.0", "@hcengineering/task": "~0.6.0",
"@hcengineering/calendar": "~0.6.1", "@hcengineering/calendar": "~0.6.1",
"@hcengineering/ui": "^0.6.2" "@hcengineering/ui": "^0.6.2",
"@hcengineering/tags": "~0.6.2"
} }
} }

View File

@ -14,12 +14,13 @@
// //
import { Event } from '@hcengineering/calendar' import { Event } from '@hcengineering/calendar'
import type { Organization, Person } from '@hcengineering/contact' import type { Channel, Organization, Person } from '@hcengineering/contact'
import type { AttachedDoc, Class, Doc, Mixin, Ref, Space, Timestamp } from '@hcengineering/core' import type { AttachedData, AttachedDoc, Class, Doc, Mixin, Ref, Space, Timestamp } from '@hcengineering/core'
import type { Asset, Plugin } from '@hcengineering/platform' import type { Asset, Plugin } from '@hcengineering/platform'
import { plugin } from '@hcengineering/platform' import { plugin } from '@hcengineering/platform'
import type { KanbanTemplateSpace, SpaceWithStates, Task } from '@hcengineering/task' import type { KanbanTemplateSpace, SpaceWithStates, Task } from '@hcengineering/task'
import { AnyComponent } from '@hcengineering/ui' import { AnyComponent } from '@hcengineering/ui'
import { TagReference } from '@hcengineering/tags'
/** /**
* @public * @public
@ -58,6 +59,27 @@ export interface Candidate extends Person {
reviews?: number 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 * @public
*/ */

View File

@ -43,7 +43,7 @@ test.describe('recruit tests', () => {
// Click :nth-match(:text("Cancel"), 2) // Click :nth-match(:text("Cancel"), 2)
// await page.click('button:has-text("Cancel")') // await page.click('button:has-text("Cancel")')
await page.keyboard.press('Escape') await page.keyboard.press('Escape')
await page.keyboard.press('Escape') // await page.keyboard.press('Escape')
// Click button:has-text("Create") // Click button:has-text("Create")
await page.click('button:has-text("Create")') await page.click('button:has-text("Create")')
await page.waitForSelector('form.antiCard', { state: 'detached' }) await page.waitForSelector('form.antiCard', { state: 'detached' })