Move skill definitions for recruit from tags to recruit (#1043)

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
Andrey Sobolev 2022-02-25 16:02:38 +07:00 committed by GitHub
parent 1b9a5e0395
commit 7ab4e59b7a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 150 additions and 87 deletions

View File

@ -12518,11 +12518,12 @@ packages:
dev: false dev: false
file:projects/model-recruit.tgz_typescript@4.5.4: file:projects/model-recruit.tgz_typescript@4.5.4:
resolution: {integrity: sha512-8jDIXUx3KFtzeCO7AChSlND5+RBTLNG83eAO8syMdJVAmOA5UpEhFQvpPZCRLhYEzj3VG2zgJg7USd+CZKEPhA==, tarball: file:projects/model-recruit.tgz} resolution: {integrity: sha512-e8rfK0JjHSG0Zy0OzesYS38vL7VPzaKOUx5FC9PjeckdklAGYFbPUc1PZnLkeisogVcd3tDR7UkjIL0GvohJ+w==, tarball: file:projects/model-recruit.tgz}
id: file:projects/model-recruit.tgz id: file:projects/model-recruit.tgz
name: '@rush-temp/model-recruit' name: '@rush-temp/model-recruit'
version: 0.0.0 version: 0.0.0
dependencies: dependencies:
'@anticrm/skillset': 0.6.0
'@rushstack/heft': 0.41.8 '@rushstack/heft': 0.41.8
'@types/heft-jest': 1.0.2 '@types/heft-jest': 1.0.2
'@typescript-eslint/eslint-plugin': 5.7.0_c25e8c1f4f4f7aaed27aa6f9ce042237 '@typescript-eslint/eslint-plugin': 5.7.0_c25e8c1f4f4f7aaed27aa6f9ce042237
@ -12768,7 +12769,7 @@ packages:
dev: false dev: false
file:projects/model-tags.tgz_typescript@4.5.4: file:projects/model-tags.tgz_typescript@4.5.4:
resolution: {integrity: sha512-M9oDcJc4rUtXqhC4FcPW+Z62RTGfstzLFi6DnorfVhtEANY/2VTUPiyjwLM+ml6sCHVdG49kaAx1gLHyEbAF7Q==, tarball: file:projects/model-tags.tgz} resolution: {integrity: sha512-M+xHdfGituHUoH7YgkHc5lw2TXsvV3jvf3g73EpjJgGl4NFclNd1VkBtyEQ9Czsjy3N/jff1B2FePFu7u2p6FA==, tarball: file:projects/model-tags.tgz}
id: file:projects/model-tags.tgz id: file:projects/model-tags.tgz
name: '@rush-temp/model-tags' name: '@rush-temp/model-tags'
version: 0.0.0 version: 0.0.0

View File

@ -44,6 +44,7 @@
"@anticrm/model-task": "~0.6.0", "@anticrm/model-task": "~0.6.0",
"@anticrm/workbench": "~0.6.1", "@anticrm/workbench": "~0.6.1",
"@anticrm/model-presentation": "~0.6.0", "@anticrm/model-presentation": "~0.6.0",
"@anticrm/model-tags": "~0.6.0" "@anticrm/model-tags": "~0.6.0",
"@anticrm/skillset": "^0.6.0"
} }
} }

View File

@ -14,13 +14,16 @@
// //
import { Person } from '@anticrm/contact' import { Person } from '@anticrm/contact'
import core, { AttachedDoc, Class, Doc, DOMAIN_TX, MixinData, Ref, TxCollectionCUD, TxCreateDoc, TxMixin, TxUpdateDoc } from '@anticrm/core' import core, { AttachedDoc, Class, Doc, DocumentQuery, DOMAIN_TX, MixinData, Ref, TxCollectionCUD, TxCreateDoc, TxMixin, TxOperations, TxUpdateDoc } from '@anticrm/core'
import { MigrateOperation, MigrationClient, MigrationResult, MigrationUpgradeClient } from '@anticrm/model' import { createOrUpdate, MigrateOperation, MigrationClient, MigrationUpgradeClient } from '@anticrm/model'
import { DOMAIN_ATTACHMENT } from '@anticrm/model-attachment' import { DOMAIN_ATTACHMENT } from '@anticrm/model-attachment'
import { DOMAIN_COMMENT } from '@anticrm/model-chunter' import { DOMAIN_COMMENT } from '@anticrm/model-chunter'
import contact, { DOMAIN_CONTACT } from '@anticrm/model-contact' import contact, { DOMAIN_CONTACT } from '@anticrm/model-contact'
import tags, { DOMAIN_TAGS, TagCategory, TagElement } from '@anticrm/model-tags'
import { DOMAIN_TASK } from '@anticrm/model-task' import { DOMAIN_TASK } from '@anticrm/model-task'
import recruit, { Candidate } from '@anticrm/recruit' import { Candidate } from '@anticrm/recruit'
import recruit from './plugin'
import { getCategories } from '@anticrm/skillset'
function toCandidateData (c: Pick<Candidate, 'onsite'|'title'|'remote'|'source'> | undefined): MixinData<Person, Candidate> { function toCandidateData (c: Pick<Candidate, 'onsite'|'title'|'remote'|'source'> | undefined): MixinData<Person, Candidate> {
if (c === undefined) { if (c === undefined) {
@ -37,12 +40,15 @@ function toCandidateData (c: Pick<Candidate, 'onsite'|'title'|'remote'|'source'>
return result return result
} }
// eslint-disable-next-line @typescript-eslint/no-unused-vars function findTagCategory (title: string, categories: TagCategory[]): Ref<TagCategory> {
function logInfo (msg: string, result: MigrationResult): void { for (const c of categories) {
if (result.updated > 0) { if (c.tags.findIndex((it) => it.toLowerCase() === title.toLowerCase()) !== -1) {
console.log(`Recruit: Migrate ${msg} ${result.updated}`) return c._id
}
} }
return recruit.category.Other
} }
export const recruitOperation: MigrateOperation = { export const recruitOperation: MigrateOperation = {
async migrate (client: MigrationClient): Promise<void> { async migrate (client: MigrationClient): Promise<void> {
// Move all candidates to mixins. // Move all candidates to mixins.
@ -128,8 +134,88 @@ export const recruitOperation: MigrateOperation = {
}, { }, {
objectClass: contact.class.Person objectClass: contact.class.Person
}) })
// Rename other
const categories = await client.find(DOMAIN_TAGS, { _class: tags.class.TagCategory })
let prefix = 'tags:category:Category'
for (const c of categories) {
if (c._id.startsWith(prefix) || c._id === 'tags:category:Other') {
let newCID = c._id.replace(prefix, recruit.category.Category + '.') as Ref<TagCategory>
if (c._id === 'tags:category:Other') {
newCID = recruit.category.Other
}
await client.delete(DOMAIN_TAGS, c._id)
await client.create(DOMAIN_TAGS, { ...c, _id: newCID, targetClass: recruit.mixin.Candidate })
await client.update(DOMAIN_TAGS, { _class: tags.class.TagElement, category: c._id }, {
category: newCID,
targetClass: recruit.mixin.Candidate
})
}
}
prefix = 'recruit:category:Category'
for (const c of categories) {
if ((c._id.startsWith(prefix) && !c._id.startsWith(prefix + '.')) || c._id === 'tags:category:Other') {
let newCID = c._id.replace(prefix, recruit.category.Category + '.') as Ref<TagCategory>
if (c._id === 'tags:category:Other') {
newCID = recruit.category.Other
}
await client.delete(DOMAIN_TAGS, c._id)
try {
await client.create(DOMAIN_TAGS, { ...c, _id: newCID, targetClass: recruit.mixin.Candidate })
} catch (err: any) {
// Ignore
}
await client.update(DOMAIN_TAGS, { _class: tags.class.TagElement, category: c._id }, {
category: newCID,
targetClass: recruit.mixin.Candidate
})
}
}
}, },
async upgrade (client: MigrationUpgradeClient): Promise<void> {} async upgrade (client: MigrationUpgradeClient): Promise<void> {
const tx = new TxOperations(client, core.account.System)
await createOrUpdate(
tx,
tags.class.TagCategory,
tags.space.Tags,
{
icon: tags.icon.Tags,
label: 'Other',
targetClass: recruit.mixin.Candidate,
tags: [],
default: true
},
recruit.category.Other
)
for (const c of getCategories()) {
await createOrUpdate(
tx,
tags.class.TagCategory,
tags.space.Tags,
{
icon: tags.icon.Tags,
label: c.label,
targetClass: recruit.mixin.Candidate,
tags: c.skills,
default: false
},
(recruit.category.Category + '.' + c.id) as Ref<TagCategory>
)
}
const categories = await tx.findAll(tags.class.TagCategory, { targetClass: recruit.mixin.Candidate })
// Find all existing TagElement and update category based on skillset
const tagElements = await tx.findAll(tags.class.TagElement, { category: null } as unknown as DocumentQuery<TagElement>)
for (const t of tagElements) {
if (t.category == null) {
const category = findTagCategory(t.title, categories)
await tx.update(t, { category: category })
}
}
}
} }
async function migrateUpdateCandidateToPersonAndMixin (client: MigrationClient): Promise<void> { async function migrateUpdateCandidateToPersonAndMixin (client: MigrationClient): Promise<void> {
const updateCandidates = await client.find(DOMAIN_TX, { const updateCandidates = await client.find(DOMAIN_TX, {

View File

@ -33,7 +33,6 @@
"@anticrm/tags-resources": "~0.6.0", "@anticrm/tags-resources": "~0.6.0",
"@anticrm/ui": "~0.6.0", "@anticrm/ui": "~0.6.0",
"@anticrm/model-core": "~0.6.0", "@anticrm/model-core": "~0.6.0",
"@anticrm/model-view": "~0.6.0", "@anticrm/model-view": "~0.6.0"
"@anticrm/skillset": "^0.6.0"
} }
} }

View File

@ -22,6 +22,7 @@ import tags from './plugin'
export { tagsOperation } from './migration' export { tagsOperation } from './migration'
export { tags as default } export { tags as default }
export { TagElement, TagReference, TagCategory } from '@anticrm/tags'
export const DOMAIN_TAGS = 'tags' as Domain export const DOMAIN_TAGS = 'tags' as Domain
@ -75,6 +76,9 @@ export class TTagCategory extends TDoc implements TagCategory {
@Prop(Collection(core.class.TypeString), tags.string.CategoryTagsLabel) @Prop(Collection(core.class.TypeString), tags.string.CategoryTagsLabel)
tags!: string[] tags!: string[]
@Prop(TypeString(), tags.string.DefaultLabel)
default!: boolean
} }
export function createModel (builder: Builder): void { export function createModel (builder: Builder): void {

View File

@ -1,50 +1,7 @@
import core, { DocumentQuery, Ref, TxOperations } from '@anticrm/core' import { MigrateOperation, MigrationClient, MigrationUpgradeClient } from '@anticrm/model'
import { createOrUpdate, MigrateOperation, MigrationClient, MigrationUpgradeClient } from '@anticrm/model'
import { getCategories } from '@anticrm/skillset'
import { findTagCategory, TagCategory, TagElement } from '@anticrm/tags'
import tags from './plugin'
export const tagsOperation: MigrateOperation = { export const tagsOperation: MigrateOperation = {
async migrate (client: MigrationClient): Promise<void> {}, async migrate (client: MigrationClient): Promise<void> {},
async upgrade (client: MigrationUpgradeClient): Promise<void> { async upgrade (client: MigrationUpgradeClient): Promise<void> {
const tx = new TxOperations(client, core.account.System)
await createOrUpdate(
tx,
tags.class.TagCategory,
tags.space.Tags,
{
icon: tags.icon.Tags,
label: 'Other',
targetClass: core.class.Doc,
tags: ['']
},
tags.category.Other
)
for (const c of getCategories()) {
await createOrUpdate(
tx,
tags.class.TagCategory,
tags.space.Tags,
{
icon: tags.icon.Tags,
label: c.label,
targetClass: core.class.Doc,
tags: c.skills
},
(tags.category.Category + c.id) as Ref<TagCategory>
)
}
const categories = await tx.findAll(tags.class.TagCategory, {})
// Find all existing TagElement and update category based on skillset
const tagElements = await tx.findAll(tags.class.TagElement, { category: null } as unknown as DocumentQuery<TagElement>)
for (const t of tagElements) {
if (t.category == null) {
const category = findTagCategory(t.title, categories)
await tx.update(t, { category: category })
}
}
} }
} }

View File

@ -39,7 +39,8 @@ export default mergeIds(tagsId, tags, {
TagReference: '' as IntlString, TagReference: '' as IntlString,
AssetLabel: '' as IntlString, AssetLabel: '' as IntlString,
CategoryTargetClass: '' as IntlString, CategoryTargetClass: '' as IntlString,
CategoryTagsLabel: '' as IntlString CategoryTagsLabel: '' as IntlString,
DefaultLabel: '' as IntlString
}, },
dd: { dd: {
DeleteTagElement: '' as Resource<(doc: Doc, client: TxOperations) => Promise<Doc[]>> DeleteTagElement: '' as Resource<(doc: Doc, client: TxOperations) => Promise<Doc[]>>

View File

@ -28,15 +28,18 @@ function diffAttributes (doc: Data<Doc>, newDoc: Data<Doc>): DocumentUpdate<Doc>
} }
/** /**
* Create or update document if modified only by system account.
* @public * @public
*/ */
export async function createOrUpdate<T extends Doc> (client: TxOperations, _class: Ref<Class<T>>, space: Ref<Space>, data: Data<T>, _id: Ref<T>): Promise<void> { export async function createOrUpdate<T extends Doc> (client: TxOperations, _class: Ref<Class<T>>, space: Ref<Space>, data: Data<T>, _id: Ref<T>): Promise<void> {
const existingDoc = await client.findOne<Doc>(_class, { _id }) const existingDoc = await client.findOne<Doc>(_class, { _id })
if (existingDoc !== undefined) { if (existingDoc !== undefined) {
const { _class: _oldClass, _id, space: _oldSpace, modifiedBy, modifiedOn, ...oldData } = existingDoc const { _class: _oldClass, _id, space: _oldSpace, modifiedBy, modifiedOn, ...oldData } = existingDoc
const updateOp = diffAttributes(oldData, data) if (modifiedBy === client.txFactory.account) {
if (Object.keys(updateOp).length > 0) { const updateOp = diffAttributes(oldData, data)
await client.update(existingDoc, updateOp) if (Object.keys(updateOp).length > 0) {
await client.update(existingDoc, updateOp)
}
} }
} else { } else {
await client.createDoc<T>(_class, space, data, _id) await client.createDoc<T>(_class, space, data, _id)

View File

@ -55,7 +55,6 @@
$: updateResultQuery(search, documentIds) $: updateResultQuery(search, documentIds)
function updateCategory (detail: {category: Ref<TagCategory> | null, elements: TagElement[] }) { function updateCategory (detail: {category: Ref<TagCategory> | null, elements: TagElement[] }) {
console.log(detail)
category = detail.category ?? undefined category = detail.category ?? undefined
selectedTagElements.set(Array.from(detail.elements ?? []).map(it => it._id)) selectedTagElements.set(Array.from(detail.elements ?? []).map(it => it._id))
} }

View File

@ -154,7 +154,7 @@
) )
} }
const categories = await client.findAll(tags.class.TagCategory, {}) const categories = await client.findAll(tags.class.TagCategory, { targetClass: recruit.mixin.Candidate })
// Tag elements // Tag elements
const skillTagElements = new Map((await client.findAll(tags.class.TagElement, { _id: { $in: skills.map(it => it.tag) } })).map(it => ([it._id, it]))) const skillTagElements = new Map((await client.findAll(tags.class.TagElement, { _id: { $in: skills.map(it => it.tag) } })).map(it => ([it._id, it])))
for (const skill of skills) { for (const skill of skills) {
@ -246,7 +246,7 @@
// Create skills // Create skills
await elementsPromise await elementsPromise
const categories = await client.findAll(tags.class.TagCategory, {}) const categories = await client.findAll(tags.class.TagCategory, { targetClass: recruit.mixin.Candidate })
const categoriesMap = new Map(Array.from(categories.map(it => ([it._id, it])))) const categoriesMap = new Map(Array.from(categories.map(it => ([it._id, it]))))
const newSkills:TagReference[] = [] const newSkills:TagReference[] = []

View File

@ -14,9 +14,12 @@
// //
import type { Client, Doc } from '@anticrm/core' import type { Client, Doc } from '@anticrm/core'
import { getMetadata, IntlString, OK, Resources, Severity, Status, translate } from '@anticrm/platform' import { IntlString, OK, Resources, Severity, Status, translate } from '@anticrm/platform'
import { Applicant, Vacancy } from '@anticrm/recruit' import { ObjectSearchResult } from '@anticrm/presentation'
import { Applicant } from '@anticrm/recruit'
import task from '@anticrm/task'
import { showPopup } from '@anticrm/ui' import { showPopup } from '@anticrm/ui'
import ApplicationItem from './components/ApplicationItem.svelte'
import ApplicationPresenter from './components/ApplicationPresenter.svelte' import ApplicationPresenter from './components/ApplicationPresenter.svelte'
import Applications from './components/Applications.svelte' import Applications from './components/Applications.svelte'
import ApplicationsPresenter from './components/ApplicationsPresenter.svelte' import ApplicationsPresenter from './components/ApplicationsPresenter.svelte'
@ -27,16 +30,10 @@ import CreateVacancy from './components/CreateVacancy.svelte'
import EditApplication from './components/EditApplication.svelte' import EditApplication from './components/EditApplication.svelte'
import EditVacancy from './components/EditVacancy.svelte' import EditVacancy from './components/EditVacancy.svelte'
import KanbanCard from './components/KanbanCard.svelte' import KanbanCard from './components/KanbanCard.svelte'
import TemplatesIcon from './components/TemplatesIcon.svelte'
import recruit from './plugin'
import { ObjectSearchResult } from '@anticrm/presentation'
import task from '@anticrm/task'
import ApplicationItem from './components/ApplicationItem.svelte'
import VacancyPresenter from './components/VacancyPresenter.svelte'
import SkillsView from './components/SkillsView.svelte' import SkillsView from './components/SkillsView.svelte'
import login from '@anticrm/login' import TemplatesIcon from './components/TemplatesIcon.svelte'
import workbench from '@anticrm/workbench' import VacancyPresenter from './components/VacancyPresenter.svelte'
import view from '@anticrm/view' import recruit from './plugin'
async function createApplication (object: Doc): Promise<void> { async function createApplication (object: Doc): Promise<void> {
showPopup(CreateApplication, { candidate: object._id, preserveCandidate: true }) showPopup(CreateApplication, { candidate: object._id, preserveCandidate: true })

View File

@ -17,6 +17,7 @@ import { Ref, Space } from '@anticrm/core'
import type { IntlString, StatusCode } from '@anticrm/platform' import type { IntlString, StatusCode } from '@anticrm/platform'
import { mergeIds } from '@anticrm/platform' import { mergeIds } from '@anticrm/platform'
import recruit, { recruitId } from '@anticrm/recruit' import recruit, { recruitId } from '@anticrm/recruit'
import { TagCategory } from '@anticrm/tags'
export default mergeIds(recruitId, recruit, { export default mergeIds(recruitId, recruit, {
status: { status: {
@ -69,5 +70,9 @@ export default mergeIds(recruitId, recruit, {
}, },
space: { space: {
CandidatesPublic: '' as Ref<Space> CandidatesPublic: '' as Ref<Space>
},
category: {
Other: '' as Ref<TagCategory>,
Category: '' as Ref<TagCategory>
} }
}) })

View File

@ -29,6 +29,7 @@
"CategoryTargetClass": "Category", "CategoryTargetClass": "Category",
"CategoryTagsLabel": "Category Items", "CategoryTagsLabel": "Category Items",
"OtherCategoryLabel": "Other", "OtherCategoryLabel": "Other",
"AllCategories": "All Categories" "AllCategories": "All Categories",
"DefaultLabel": "Default category"
} }
} }

View File

@ -14,11 +14,11 @@
--> -->
<script lang="ts"> <script lang="ts">
import { Class, Doc, Ref, SortingOrder } from '@anticrm/core' import { Class, Doc, Ref, SortingOrder } from '@anticrm/core'
import { createQuery, getClient } from '@anticrm/presentation' import { createQuery } from '@anticrm/presentation'
import { TagCategory, TagElement } from '@anticrm/tags' import { TagCategory, TagElement } from '@anticrm/tags'
import tags from '../plugin'
import { getPlatformColorForText, Label } from '@anticrm/ui' import { getPlatformColorForText, Label } from '@anticrm/ui'
import { createEventDispatcher } from 'svelte' import { createEventDispatcher } from 'svelte'
import tags from '../plugin'
import { getTagStyle } from '../utils' import { getTagStyle } from '../utils'
export let targetClass: Ref<Class<Doc>> export let targetClass: Ref<Class<Doc>>
@ -31,7 +31,6 @@
const stepStyle = gap === 'small' ? 'gap-1' : 'gap-2' const stepStyle = gap === 'small' ? 'gap-1' : 'gap-2'
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
const client = getClient()
let elements: TagElement[] = [] let elements: TagElement[] = []
@ -41,7 +40,7 @@
const elementsQuery = createQuery() const elementsQuery = createQuery()
$: elementsQuery.query( $: elementsQuery.query(
tags.class.TagElement, tags.class.TagElement,
{ targetClass: { $in: [targetClass, ...client.getHierarchy().getAncestors(targetClass)] } }, { targetClass },
(res) => { (res) => {
elements = res elements = res
}, },

View File

@ -31,7 +31,7 @@
let color: number = 0 let color: number = 0
let categoryWasSet = false let categoryWasSet = false
let category: Ref<TagCategory> = tags.category.Other let category: Ref<TagCategory> | undefined
let categories: TagCategory[] = [] let categories: TagCategory[] = []
let categoryItems: DropdownTextItem[] = [] let categoryItems: DropdownTextItem[] = []
@ -42,7 +42,7 @@
color = getColorNumberByText(title) color = getColorNumberByText(title)
} }
$: if (!categoryWasSet) { $: if (!categoryWasSet && categories.length > 0) {
category = findTagCategory(title, categories) category = findTagCategory(title, categories)
} }
@ -56,7 +56,7 @@
const query = createQuery() const query = createQuery()
query.query(tags.class.TagCategory, {}, async (result) => { query.query(tags.class.TagCategory, { targetClass }, async (result) => {
const newItems: DropdownTextItem[] = [] const newItems: DropdownTextItem[] = []
for (const r of result) { for (const r of result) {
newItems.push({ newItems.push({
@ -74,7 +74,7 @@
description, description,
targetClass, targetClass,
color, color,
category: category category: category ?? tags.category.NoCategory
} }
await client.createDoc(tags.class.TagElement, tags.space.Tags, tagElement, tagElementId) await client.createDoc(tags.class.TagElement, tags.space.Tags, tagElement, tagElementId)

View File

@ -77,7 +77,7 @@
} }
const query = createQuery() const query = createQuery()
query.query(tags.class.TagCategory, {}, async (result) => { query.query(tags.class.TagCategory, { targetClass: value.targetClass }, async (result) => {
const newItems: DropdownTextItem[] = [] const newItems: DropdownTextItem[] = []
for (const r of result) { for (const r of result) {
newItems.push({ newItems.push({

View File

@ -51,6 +51,9 @@ export interface TagCategory extends Doc {
targetClass: Ref<Class<Doc>> targetClass: Ref<Class<Doc>>
// A list of possible variants. // A list of possible variants.
tags: string[] tags: string[]
// If defined, will be used for unidentified tags
default: boolean
} }
/** /**
@ -79,7 +82,7 @@ const tagsPlugin = plugin(tagsId, {
TagsCategoryBar: '' as AnyComponent TagsCategoryBar: '' as AnyComponent
}, },
category: { category: {
Other: '' as Ref<TagCategory> NoCategory: '' as Ref<TagCategory>
} }
}) })
@ -92,12 +95,19 @@ export default tagsPlugin
* @public * @public
*/ */
export function findTagCategory (title: string, categories: TagCategory[]): Ref<TagCategory> { export function findTagCategory (title: string, categories: TagCategory[]): Ref<TagCategory> {
let defaultCategory: TagCategory | undefined
for (const c of categories) { for (const c of categories) {
if (c.default) {
defaultCategory = c
}
if (c.tags.findIndex((it) => it.toLowerCase() === title.toLowerCase()) !== -1) { if (c.tags.findIndex((it) => it.toLowerCase() === title.toLowerCase()) !== -1) {
return c._id return c._id
} }
} }
return tagsPlugin.category.Other if (defaultCategory === undefined) {
throw new Error('Tag category not found')
}
return defaultCategory._id
} }
/** /**