From e1377393b24b941b97cad7ec382e3187fd8c8b13 Mon Sep 17 00:00:00 2001 From: Michael Uray Date: Sun, 18 Oct 2026 04:45:10 +0000 Subject: [PATCH] feat(recruit): add labels to vacancies Vacancies had no labels collection, unlike applicants (task labels) and talents (skills), so they could not be tagged, shown or filtered by label. Add a 'labels' TagReference collection to Vacancy (same attribute and label as the task labels used for applicants), show the label editor in the vacancy panel attribute bar and in the create vacancy dialog, and add a labels column to the vacancies table. The generic tag filter picks up the new collection automatically. The tag popup only lists elements of categories whose targetClass matches, so a migration step creates a default 'Other' tag category for vacancies. Fixes #8903 Signed-off-by: Michael Uray --- models/recruit/src/index.ts | 10 ++++ models/recruit/src/migration.ts | 19 +++++++ models/recruit/src/types.ts | 3 + plugins/recruit-assets/lang/cs.json | 3 +- plugins/recruit-assets/lang/de.json | 3 +- plugins/recruit-assets/lang/en.json | 3 +- plugins/recruit-assets/lang/es.json | 3 +- plugins/recruit-assets/lang/fr.json | 3 +- plugins/recruit-assets/lang/it.json | 3 +- plugins/recruit-assets/lang/ja.json | 3 +- plugins/recruit-assets/lang/ko.json | 3 +- plugins/recruit-assets/lang/pl.json | 3 +- plugins/recruit-assets/lang/pt-br.json | 3 +- plugins/recruit-assets/lang/pt.json | 3 +- plugins/recruit-assets/lang/ru.json | 3 +- plugins/recruit-assets/lang/tr.json | 3 +- plugins/recruit-assets/lang/zh.json | 3 +- .../src/components/CreateVacancy.svelte | 55 ++++++++++++++++++- .../src/components/EditVacancy.svelte | 1 + plugins/recruit-resources/src/plugin.ts | 6 +- plugins/recruit/src/types.ts | 1 + 21 files changed, 120 insertions(+), 17 deletions(-) diff --git a/models/recruit/src/index.ts b/models/recruit/src/index.ts index c210f996b7d..b209eb51629 100644 --- a/models/recruit/src/index.ts +++ b/models/recruit/src/index.ts @@ -387,6 +387,16 @@ export function createModel (builder: Builder): void { label: recruit.string.Applications }, 'comments', + { + key: '', + presenter: tags.component.TagsPresenter, + label: task.string.TaskLabels, + sortingKey: 'labels', + props: { + _class: recruit.class.Vacancy, + key: 'labels' + } + }, '$lookup.company', '$lookup.company.$lookup.channels', { diff --git a/models/recruit/src/migration.ts b/models/recruit/src/migration.ts index aaebfabcb3c..cde6484b44e 100644 --- a/models/recruit/src/migration.ts +++ b/models/recruit/src/migration.ts @@ -91,6 +91,25 @@ export const recruitOperation: MigrateOperation = { const tx = new TxOperations(client, core.account.System) await createDefaults(client, tx) } + }, + { + state: 'create-vacancy-tag-category', + func: async (client) => { + const tx = new TxOperations(client, core.account.System) + await createOrUpdate( + tx, + tags.class.TagCategory, + core.space.Workspace, + { + icon: tags.icon.Tags, + label: 'Other', + targetClass: recruit.class.Vacancy, + tags: [], + default: true + }, + recruit.category.VacancyOther + ) + } } ]) } diff --git a/models/recruit/src/types.ts b/models/recruit/src/types.ts index dd41c9e01a8..5aea05bb05e 100644 --- a/models/recruit/src/types.ts +++ b/models/recruit/src/types.ts @@ -95,6 +95,9 @@ export class TVacancy extends TProject implements Vacancy { @Prop(TypeCollection(survey.class.Poll), survey.string.Polls) polls?: Collection + + @Prop(TypeCollection(tags.class.TagReference, task.string.TaskLabels), task.string.TaskLabels) + labels?: number } @Mixin(recruit.mixin.Candidate, contact.class.Person) diff --git a/plugins/recruit-assets/lang/cs.json b/plugins/recruit-assets/lang/cs.json index c5282e70d27..43951756313 100644 --- a/plugins/recruit-assets/lang/cs.json +++ b/plugins/recruit-assets/lang/cs.json @@ -124,7 +124,8 @@ "CreateNewSkills": "Vytvořit nové dovednosti, pokud neexistují", "SwapFirstAndLastNames": "Vyměňte jméno a příjmení", "ForbidCreateVacancyPermission": "Zakázat vytvoření pozice", - "ForbidCreateVacancyPermissionDescription": "Zakazuje uživatelům vytvářet nové volné pozice" + "ForbidCreateVacancyPermissionDescription": "Zakazuje uživatelům vytvářet nové volné pozice", + "NumberLabels": "{count, plural, =0 {žádné štítky} =1 {1 štítek} other {# štítků}}" }, "status": { "ApplicationExists": "Přihláška již existuje", diff --git a/plugins/recruit-assets/lang/de.json b/plugins/recruit-assets/lang/de.json index 99d3f00291a..ba206b0b514 100644 --- a/plugins/recruit-assets/lang/de.json +++ b/plugins/recruit-assets/lang/de.json @@ -124,7 +124,8 @@ "CreateNewSkills": "Neue Fähigkeiten erstellen, wenn keine bestehenden gefunden werden", "SwapFirstAndLastNames": "Vor- und Nachnamen tauschen", "ForbidCreateVacancyPermission": "Stellenerstellung verbieten", - "ForbidCreateVacancyPermissionDescription": "Verbietet Benutzern das Erstellen neuer Stellenangebote" + "ForbidCreateVacancyPermissionDescription": "Verbietet Benutzern das Erstellen neuer Stellenangebote", + "NumberLabels": "{count, plural, =0 {keine Labels} =1 {1 Label} other {# Labels}}" }, "status": { "ApplicationExists": "Bewerbung existiert bereits", diff --git a/plugins/recruit-assets/lang/en.json b/plugins/recruit-assets/lang/en.json index 55787d737ad..db0840edc6c 100644 --- a/plugins/recruit-assets/lang/en.json +++ b/plugins/recruit-assets/lang/en.json @@ -124,7 +124,8 @@ "CreateNewSkills": "Create new skills if existing not found", "SwapFirstAndLastNames": "Swap first and last names", "ForbidCreateVacancyPermission": "Forbid create vacancy", - "ForbidCreateVacancyPermissionDescription": "Forbid users creating new vacancies" + "ForbidCreateVacancyPermissionDescription": "Forbid users creating new vacancies", + "NumberLabels": "{count, plural, =0 {no labels} =1 {1 label} other {# labels}}" }, "status": { "ApplicationExists": "Application already exists", diff --git a/plugins/recruit-assets/lang/es.json b/plugins/recruit-assets/lang/es.json index c3075d7f364..24a638e4bdb 100644 --- a/plugins/recruit-assets/lang/es.json +++ b/plugins/recruit-assets/lang/es.json @@ -121,7 +121,8 @@ "CreateNewSkills": "Crear nuevas habilidades si no se encuentran las existentes", "SwapFirstAndLastNames": "Intercambie nombres y apellidos", "ForbidCreateVacancyPermission": "Prohibir crear vacante", - "ForbidCreateVacancyPermissionDescription": "Prohíbe a los usuarios crear nuevas vacantes" + "ForbidCreateVacancyPermissionDescription": "Prohíbe a los usuarios crear nuevas vacantes", + "NumberLabels": "{count, plural, =0 {sin etiquetas} =1 {1 etiqueta} other {# etiquetas}}" }, "status": { "ApplicationExists": "La solicitud ya existe", diff --git a/plugins/recruit-assets/lang/fr.json b/plugins/recruit-assets/lang/fr.json index 986dd1a7bfa..3eb453408cb 100644 --- a/plugins/recruit-assets/lang/fr.json +++ b/plugins/recruit-assets/lang/fr.json @@ -121,7 +121,8 @@ "CreateNewSkills": "Créer de nouvelles compétences si les existantes ne sont pas trouvées", "SwapFirstAndLastNames": "Permuter le prénom et le nom", "ForbidCreateVacancyPermission": "Interdire la création de poste vacant", - "ForbidCreateVacancyPermissionDescription": "Interdit aux utilisateurs de créer de nouveaux postes vacants" + "ForbidCreateVacancyPermissionDescription": "Interdit aux utilisateurs de créer de nouveaux postes vacants", + "NumberLabels": "{count, plural, =0 {aucune étiquette} =1 {1 étiquette} other {# étiquettes}}" }, "status": { "ApplicationExists": "La candidature existe déjà", diff --git a/plugins/recruit-assets/lang/it.json b/plugins/recruit-assets/lang/it.json index 6f4676579b1..2fff4d68a1d 100644 --- a/plugins/recruit-assets/lang/it.json +++ b/plugins/recruit-assets/lang/it.json @@ -122,7 +122,8 @@ "CreateNewSkills": "Crea nuove competenze se quelle esistenti non vengono trovate", "SwapFirstAndLastNames": "Scambia nome e cognome", "ForbidCreateVacancyPermission": "Vieta creazione posizione aperta", - "ForbidCreateVacancyPermissionDescription": "Vieta agli utenti di creare nuove posizioni aperte" + "ForbidCreateVacancyPermissionDescription": "Vieta agli utenti di creare nuove posizioni aperte", + "NumberLabels": "{count, plural, =0 {nessuna etichetta} =1 {1 etichetta} other {# etichette}}" }, "status": { "ApplicationExists": "La candidatura esiste già", diff --git a/plugins/recruit-assets/lang/ja.json b/plugins/recruit-assets/lang/ja.json index ceaed738697..cc3bb471d3c 100644 --- a/plugins/recruit-assets/lang/ja.json +++ b/plugins/recruit-assets/lang/ja.json @@ -121,7 +121,8 @@ "CreateNewSkills": "既存のスキルが見つからない場合は新しいスキルを作成", "SwapFirstAndLastNames": "名と姓を入れ替える", "ForbidCreateVacancyPermission": "求人の作成禁止", - "ForbidCreateVacancyPermissionDescription": "ユーザーが新しい求人を作成することを禁止します" + "ForbidCreateVacancyPermissionDescription": "ユーザーが新しい求人を作成することを禁止します", + "NumberLabels": "{count, plural, =0 {ラベルなし} =1 {1 ラベル} other {# ラベル}}" }, "status": { "ApplicationExists": "応募はすでに存在します", diff --git a/plugins/recruit-assets/lang/ko.json b/plugins/recruit-assets/lang/ko.json index f0bb9825fb4..9011400401b 100644 --- a/plugins/recruit-assets/lang/ko.json +++ b/plugins/recruit-assets/lang/ko.json @@ -121,7 +121,8 @@ "CreateNewSkills": "기존 스킬이 없으면 새로 만들기", "SwapFirstAndLastNames": "이름과 성 바꾸기", "ForbidCreateVacancyPermission": "채용 공고 생성 금지", - "ForbidCreateVacancyPermissionDescription": "사용자의 새 채용 공고 생성을 금지합니다" + "ForbidCreateVacancyPermissionDescription": "사용자의 새 채용 공고 생성을 금지합니다", + "NumberLabels": "{count, plural, =0 {라벨 없음} =1 {라벨 1개} other {라벨 #개}}" }, "status": { "ApplicationExists": "이미 존재하는 지원입니다", diff --git a/plugins/recruit-assets/lang/pl.json b/plugins/recruit-assets/lang/pl.json index 630049f5d6d..39f410cff1c 100644 --- a/plugins/recruit-assets/lang/pl.json +++ b/plugins/recruit-assets/lang/pl.json @@ -124,7 +124,8 @@ "CreateNewSkills": "Utwórz nowe umiejętności, jeśli nie istnieją", "SwapFirstAndLastNames": "Zamień imię i nazwisko", "ForbidCreateVacancyPermission": "Zabroń tworzenia wolnych stanowisk", - "ForbidCreateVacancyPermissionDescription": "Zabroń użytkownikom tworzenia nowych stanowisk" + "ForbidCreateVacancyPermissionDescription": "Zabroń użytkownikom tworzenia nowych stanowisk", + "NumberLabels": "{count, plural, =0 {brak etykiet} =1 {1 etykieta} few {# etykiety} other {# etykiet}}" }, "status": { "ApplicationExists": "Aplikacja już istnieje", diff --git a/plugins/recruit-assets/lang/pt-br.json b/plugins/recruit-assets/lang/pt-br.json index 878e149e66f..f04b174d731 100644 --- a/plugins/recruit-assets/lang/pt-br.json +++ b/plugins/recruit-assets/lang/pt-br.json @@ -121,7 +121,8 @@ "CreateNewSkills": "Criar novas competências se as existentes não forem encontradas", "SwapFirstAndLastNames": "Trocar primeiro e último nome", "ForbidCreateVacancyPermission": "Proibir criação de vaga", - "ForbidCreateVacancyPermissionDescription": "Proíbe os utilizadores de criar novas vagas" + "ForbidCreateVacancyPermissionDescription": "Proíbe os utilizadores de criar novas vagas", + "NumberLabels": "{count, plural, =0 {sem etiquetas} =1 {1 etiqueta} other {# etiquetas}}" }, "status": { "ApplicationExists": "A candidatura já existe", diff --git a/plugins/recruit-assets/lang/pt.json b/plugins/recruit-assets/lang/pt.json index c87178ade67..ec49b14503e 100644 --- a/plugins/recruit-assets/lang/pt.json +++ b/plugins/recruit-assets/lang/pt.json @@ -121,7 +121,8 @@ "CreateNewSkills": "Criar novas competências se as existentes não forem encontradas", "SwapFirstAndLastNames": "Trocar primeiro e último nome", "ForbidCreateVacancyPermission": "Proibir criação de vaga", - "ForbidCreateVacancyPermissionDescription": "Proíbe os utilizadores de criar novas vagas" + "ForbidCreateVacancyPermissionDescription": "Proíbe os utilizadores de criar novas vagas", + "NumberLabels": "{count, plural, =0 {sem etiquetas} =1 {1 etiqueta} other {# etiquetas}}" }, "status": { "ApplicationExists": "A candidatura já existe", diff --git a/plugins/recruit-assets/lang/ru.json b/plugins/recruit-assets/lang/ru.json index 28ffab2e86b..3a63a8fdee8 100644 --- a/plugins/recruit-assets/lang/ru.json +++ b/plugins/recruit-assets/lang/ru.json @@ -124,7 +124,8 @@ "CreateNewSkills": "Создать навыки, если не найдены существующие", "SwapFirstAndLastNames": "Поменять местами имя и фамилию", "ForbidCreateVacancyPermission": "Запретить создание вакансии", - "ForbidCreateVacancyPermissionDescription": "Запрещает пользователям создавать новые вакансии" + "ForbidCreateVacancyPermissionDescription": "Запрещает пользователям создавать новые вакансии", + "NumberLabels": "{count, plural, =0 {нет меток} one {# метка} few {# метки} other {# меток}}" }, "status": { "ApplicationExists": "Кандидат уже существует", diff --git a/plugins/recruit-assets/lang/tr.json b/plugins/recruit-assets/lang/tr.json index 93b4878640a..93371b59290 100644 --- a/plugins/recruit-assets/lang/tr.json +++ b/plugins/recruit-assets/lang/tr.json @@ -121,7 +121,8 @@ "CreateNewSkills": "Mevcut bulunamazsa yeni yetenekler oluştur", "SwapFirstAndLastNames": "Ad ve soyadı değiştir", "ForbidCreateVacancyPermission": "İş ilanı oluşturmayı yasakla", - "ForbidCreateVacancyPermissionDescription": "Kullanıcıların yeni iş ilanı oluşturmasını yasaklar" + "ForbidCreateVacancyPermissionDescription": "Kullanıcıların yeni iş ilanı oluşturmasını yasaklar", + "NumberLabels": "{count, plural, =0 {etiket yok} =1 {1 etiket} other {# etiket}}" }, "status": { "ApplicationExists": "Başvuru zaten mevcut", diff --git a/plugins/recruit-assets/lang/zh.json b/plugins/recruit-assets/lang/zh.json index c955529c2ae..ebd780ba23d 100644 --- a/plugins/recruit-assets/lang/zh.json +++ b/plugins/recruit-assets/lang/zh.json @@ -124,7 +124,8 @@ "CreateNewSkills": "如果未找到现有技能,则创建新技能", "SwapFirstAndLastNames": "交换名字和姓氏", "ForbidCreateVacancyPermission": "禁止创建职位", - "ForbidCreateVacancyPermissionDescription": "禁止用户创建新职位" + "ForbidCreateVacancyPermissionDescription": "禁止用户创建新职位", + "NumberLabels": "{count, plural, =0 {无标签} =1 {1 个标签} other {# 个标签}}" }, "status": { "ApplicationExists": "申请已存在", diff --git a/plugins/recruit-resources/src/components/CreateVacancy.svelte b/plugins/recruit-resources/src/components/CreateVacancy.svelte index c9e3c46580f..137a95b7a2f 100644 --- a/plugins/recruit-resources/src/components/CreateVacancy.svelte +++ b/plugins/recruit-resources/src/components/CreateVacancy.svelte @@ -19,6 +19,8 @@ import core, { AttachedData, Data, + Doc, + PersonId, Ref, Role, RolesAssignment, @@ -39,7 +41,7 @@ getClient } from '@hcengineering/presentation' import { RecruitEvents, Vacancy, Vacancy as VacancyClass } from '@hcengineering/recruit' - import tags from '@hcengineering/tags' + import tags, { TagElement, TagReference } from '@hcengineering/tags' import task, { ProjectType } from '@hcengineering/task' import { selectedTypeStore, typeStore } from '@hcengineering/task-resources' import tracker, { Issue, IssueStatus, IssueTemplate, IssueTemplateData, Project } from '@hcengineering/tracker' @@ -110,6 +112,32 @@ const hierarchy = client.getHierarchy() const templateQ = createQuery() fillDefaults(hierarchy, vacancyData, recruit.class.Vacancy) + + let labels: TagReference[] = [] + const labelsKey = { + key: 'labels', + attr: hierarchy.getAttribute(recruit.class.Vacancy, 'labels') + } + + function addTagRef (tag: TagElement): void { + labels = [ + ...labels, + { + _class: tags.class.TagReference, + _id: generateId(), + attachedTo: '' as Ref, + attachedToClass: recruit.class.Vacancy, + collection: 'labels', + space: core.space.Space, + modifiedOn: 0, + modifiedBy: '' as PersonId, + title: tag.title, + tag: tag._id, + color: tag.color + } + ] + } + $: typeId && templateQ.query(task.class.ProjectType, { _id: typeId }, (result) => { const { _class, _id, description, targetClass, ...templateData } = result[0] @@ -254,6 +282,13 @@ const ops = client.apply() const id = await ops.createDoc(recruit.class.Vacancy, core.space.Space, data, objectId) + for (const label of labels) { + await ops.addCollection(tags.class.TagReference, core.space.Space, id, recruit.class.Vacancy, 'labels', { + title: label.title, + color: label.color, + tag: label.tag + }) + } await descriptionBox.createAttachments(undefined, ops) await ops.commit() Analytics.handleEvent(RecruitEvents.VacancyCreated, { @@ -415,6 +450,24 @@ size={'large'} /> + { + addTagRef(evt.detail) + }} + on:delete={(evt) => { + labels = labels.filter((it) => it.tag !== evt.detail._id) + }} + /> + {#each roles as role} {/if} diff --git a/plugins/recruit-resources/src/plugin.ts b/plugins/recruit-resources/src/plugin.ts index f5a771c2836..dc0bea492ca 100644 --- a/plugins/recruit-resources/src/plugin.ts +++ b/plugins/recruit-resources/src/plugin.ts @@ -128,11 +128,13 @@ export default mergeIds(recruitId, recruit, { Export: '' as IntlString, GetTalentIds: '' as IntlString, CreateNewSkills: '' as IntlString, - SwapFirstAndLastNames: '' as IntlString + SwapFirstAndLastNames: '' as IntlString, + NumberLabels: '' as IntlString }, category: { Other: '' as Ref, - Category: '' as Ref + Category: '' as Ref, + VacancyOther: '' as Ref }, completion: { ApplicationQuery: '' as Resource, diff --git a/plugins/recruit/src/types.ts b/plugins/recruit/src/types.ts index b7cdeb4d349..e7f1f496aa8 100644 --- a/plugins/recruit/src/types.ts +++ b/plugins/recruit/src/types.ts @@ -39,6 +39,7 @@ export interface Vacancy extends Project { comments?: number number: number polls?: Collection + labels?: number } /** @public */