diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index 350730ca38..499f9cd35e 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -1,6 +1,7 @@ lockfileVersion: 5.3 specifiers: + '@anticrm/skillset': https://github.com/hcengineering/skillset.git '@elastic/elasticsearch': ^7.14.0 '@koa/cors': ^3.1.0 '@microsoft/api-extractor': ^7.18.4 @@ -210,6 +211,7 @@ specifiers: xml2js: ~0.4.23 dependencies: + '@anticrm/skillset': github.com/hcengineering/skillset/7c97534c09c7382546f10d16ae27721a5704018f '@elastic/elasticsearch': 7.16.0 '@koa/cors': 3.1.0 '@microsoft/api-extractor': 7.19.2 @@ -12597,11 +12599,12 @@ packages: dev: false file:projects/model-tags.tgz_typescript@4.5.4: - resolution: {integrity: sha512-M+xHdfGituHUoH7YgkHc5lw2TXsvV3jvf3g73EpjJgGl4NFclNd1VkBtyEQ9Czsjy3N/jff1B2FePFu7u2p6FA==, tarball: file:projects/model-tags.tgz} + resolution: {integrity: sha512-ydbpF6SS1lFreF2Dyb7mHeKUD5ajzVa/KhOSiAMiVCU9zwy7aoFfUE3Uy+98biYE+qv/6jsF9x93o3r5WfYEfg==, tarball: file:projects/model-tags.tgz} id: file:projects/model-tags.tgz name: '@rush-temp/model-tags' version: 0.0.0 dependencies: + '@anticrm/skillset': github.com/hcengineering/skillset/7c97534c09c7382546f10d16ae27721a5704018f '@rushstack/heft': 0.41.8 '@types/heft-jest': 1.0.2 '@typescript-eslint/eslint-plugin': 5.7.0_c25e8c1f4f4f7aaed27aa6f9ce042237 @@ -12744,7 +12747,7 @@ packages: dev: false file:projects/model.tgz: - resolution: {integrity: sha512-7sg1S1NADyRy2HZFRHyYjlq55h31ArIg5XVIo4Ydm9NfmOObk0gocDHP/6cP/7Y+vgvS2JlUZTTTTGQ35qG0XA==, tarball: file:projects/model.tgz} + resolution: {integrity: sha512-1k5LE7nXCQ1TvWCYoxozAUY0TJXD/l2z4QbObZl27O0O0HFPhQsu1kcjsC5zlfK1PDNIAYbG4msK5ka9E0LIYg==, tarball: file:projects/model.tgz} name: '@rush-temp/model' version: 0.0.0 dependencies: @@ -12758,6 +12761,7 @@ packages: eslint-plugin-import: 2.25.3_eslint@7.32.0 eslint-plugin-node: 11.1.0_eslint@7.32.0 eslint-plugin-promise: 5.2.0_eslint@7.32.0 + fast-equals: 2.0.4 prettier: 2.5.1 toposort: 2.0.2 typescript: 4.5.4 @@ -13581,7 +13585,7 @@ packages: dev: false file:projects/tags.tgz: - resolution: {integrity: sha512-dDZIEx6iNmqyXw6CrYnmGL1PV3li1Tjuc0OSYr00PKg6UsvhrX3ah6J6W9jHm7tjL25ccMkhrVjpWQQOJsC6LQ==, tarball: file:projects/tags.tgz} + resolution: {integrity: sha512-JUARXu0wQ++Y0afKL5DLKcEQUgTjdC4gy/HZR+KLyQ6oNBJXDW/HYGVF/X012K4V2qa3LDcNNMIBrgRggfAGHg==, tarball: file:projects/tags.tgz} name: '@rush-temp/tags' version: 0.0.0 dependencies: @@ -13595,6 +13599,7 @@ packages: eslint-plugin-node: 11.1.0_eslint@7.32.0 eslint-plugin-promise: 5.2.0_eslint@7.32.0 prettier: 2.5.1 + svelte: 3.44.3 typescript: 4.5.4 transitivePeerDependencies: - supports-color @@ -14164,3 +14169,10 @@ packages: transitivePeerDependencies: - supports-color dev: false + + github.com/hcengineering/skillset/7c97534c09c7382546f10d16ae27721a5704018f: + resolution: {tarball: https://codeload.github.com/hcengineering/skillset/tar.gz/7c97534c09c7382546f10d16ae27721a5704018f} + name: '@anticrm/skillset' + version: 0.6.0 + requiresBuild: true + dev: false diff --git a/dev/tool/build.sh b/dev/tool/build.sh index 0301e46926..b692181049 100755 --- a/dev/tool/build.sh +++ b/dev/tool/build.sh @@ -16,5 +16,5 @@ # rushx bundle -rushx docker:build +rushx docker:build $@ rushx docker:push diff --git a/models/all/src/migration.ts b/models/all/src/migration.ts index 0debdbba42..7ded8b3270 100644 --- a/models/all/src/migration.ts +++ b/models/all/src/migration.ts @@ -23,6 +23,7 @@ import { leadOperation } from '@anticrm/model-lead' import { recruitOperation } from '@anticrm/model-recruit' import { viewOperation } from '@anticrm/model-view' import { contactOperation } from '@anticrm/model-contact' +import { tagsOperation } from '@anticrm/model-tags' export const migrateOperations: MigrateOperation[] = [ coreOperation, @@ -31,5 +32,6 @@ export const migrateOperations: MigrateOperation[] = [ leadOperation, recruitOperation, viewOperation, - contactOperation + contactOperation, + tagsOperation ] diff --git a/models/recruit/src/index.ts b/models/recruit/src/index.ts index 4831838a12..d68f67d0d9 100644 --- a/models/recruit/src/index.ts +++ b/models/recruit/src/index.ts @@ -208,7 +208,7 @@ export function createModel (builder: Builder): void { // key: '$lookup.skills', // Required, since presenter require list of tag references or '' and TagsPopupPresenter presenter: tags.component.TagsPresenter, // tags.component.TagsPresenter, label: recruit.string.SkillsLabel, - // sortingKey: '$lookup.skills', + sortingKey: 'skills', props: { _class: recruit.mixin.Candidate, key: 'skills' diff --git a/models/tags/package.json b/models/tags/package.json index 3f08783088..78fad0c538 100644 --- a/models/tags/package.json +++ b/models/tags/package.json @@ -33,6 +33,7 @@ "@anticrm/tags-resources": "~0.6.0", "@anticrm/ui": "~0.6.0", "@anticrm/model-core": "~0.6.0", - "@anticrm/model-view": "~0.6.0" + "@anticrm/model-view": "~0.6.0", + "@anticrm/skillset": "https://github.com/hcengineering/skillset.git" } } diff --git a/models/tags/src/index.ts b/models/tags/src/index.ts index 5429a19424..ac130c0cf9 100644 --- a/models/tags/src/index.ts +++ b/models/tags/src/index.ts @@ -13,13 +13,14 @@ // limitations under the License. import { Class, Doc, Domain, IndexKind, Ref } from '@anticrm/core' -import { Builder, Index, Model, Prop, TypeRef, TypeString, UX } from '@anticrm/model' +import { Builder, Collection, Index, Model, Prop, TypeRef, TypeString, UX } from '@anticrm/model' import core, { TAttachedDoc, TDoc } from '@anticrm/model-core' -import view from '@anticrm/model-view' -import type { TagElement, TagReference } from '@anticrm/tags' -import { ObjectDDParticipant } from '../../view/node_modules/@anticrm/view/lib' +import view, { ObjectDDParticipant } from '@anticrm/model-view' +import { Asset, IntlString } from '@anticrm/platform' +import type { TagCategory, TagElement, TagReference } from '@anticrm/tags' import tags from './plugin' +export { tagsOperation } from './migration' export { tags as default } export const DOMAIN_TAGS = 'tags' as Domain @@ -40,6 +41,9 @@ export class TTagElement extends TDoc implements TagElement { @Prop(TypeString(), tags.string.ColorLabel) color!: number + + @Prop(TypeRef(tags.class.TagCategory), tags.string.CategoryLabel) + category!: Ref } @Model(tags.class.TagReference, core.class.AttachedDoc, DOMAIN_TAGS) @@ -50,14 +54,31 @@ export class TTagReference extends TAttachedDoc implements TagReference { title!: string @Prop(TypeRef(tags.class.TagElement), tags.string.TagLabel) + @Index(IndexKind.Indexed) tag!: Ref @Prop(TypeString(), tags.string.ColorLabel) color!: number } +@Model(tags.class.TagCategory, core.class.Doc, DOMAIN_TAGS) +@UX(tags.string.TargetCategoryLabel) +export class TTagCategory extends TDoc implements TagCategory { + @Prop(TypeString(), tags.string.AssetLabel) + icon!: Asset + + @Prop(TypeString(), tags.string.CategoryLabel) + label!: IntlString + + @Prop(TypeString(), tags.string.CategoryTargetClass) + targetClass!: Ref> + + @Prop(Collection(core.class.TypeString), tags.string.CategoryTagsLabel) + tags!: string[] +} + export function createModel (builder: Builder): void { - builder.createModel(TTagElement, TTagReference) + builder.createModel(TTagElement, TTagReference, TTagCategory) builder.createDoc( core.class.Space, diff --git a/models/tags/src/migration.ts b/models/tags/src/migration.ts new file mode 100644 index 0000000000..2e481ee57c --- /dev/null +++ b/models/tags/src/migration.ts @@ -0,0 +1,50 @@ +import core, { DocumentQuery, Ref, TxOperations } from '@anticrm/core' +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 = { + async migrate (client: MigrationClient): Promise {}, + async upgrade (client: MigrationUpgradeClient): Promise { + 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 + ) + } + + 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) + for (const t of tagElements) { + if (t.category == null) { + const category = findTagCategory(t.title, categories) + await tx.update(t, { category: category }) + } + } + } +} diff --git a/models/tags/src/plugin.ts b/models/tags/src/plugin.ts index d98eb2d419..58ed4ca8a1 100644 --- a/models/tags/src/plugin.ts +++ b/models/tags/src/plugin.ts @@ -12,9 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Doc, TxOperations } from '@anticrm/core' +import { Doc, Ref, TxOperations } from '@anticrm/core' import { IntlString, mergeIds, Resource } from '@anticrm/platform' -import { tagsId } from '@anticrm/tags' +import { TagCategory, tagsId } from '@anticrm/tags' import tags from '@anticrm/tags-resources/src/plugin' import type { AnyComponent } from '@anticrm/ui' @@ -31,14 +31,20 @@ export default mergeIds(tagsId, tags, { TitleLabel: '' as IntlString, DescriptionLabel: '' as IntlString, ColorLabel: '' as IntlString, - CategoryLabel: '' as IntlString, WeightLabel: '' as IntlString, TagReferenceLabel: '' as IntlString, TagLabel: '' as IntlString, TargetClassLabel: '' as IntlString, - TagReference: '' as IntlString + TargetCategoryLabel: '' as IntlString, + TagReference: '' as IntlString, + AssetLabel: '' as IntlString, + CategoryTargetClass: '' as IntlString, + CategoryTagsLabel: '' as IntlString }, dd: { DeleteTagElement: '' as Resource<(doc: Doc, client: TxOperations) => Promise> + }, + category: { + Category: '' as Ref } }) diff --git a/models/view/src/index.ts b/models/view/src/index.ts index afc79b9927..24efec4c2b 100644 --- a/models/view/src/index.ts +++ b/models/view/src/index.ts @@ -34,6 +34,9 @@ import type { } from '@anticrm/view' import view from './plugin' +export { ObjectDDParticipant } from '@anticrm/view' +export { viewOperation } from './migration' + @Mixin(view.mixin.AttributeEditor, core.class.Class) export class TAttributeEditor extends TClass implements AttributeEditor { editor!: AnyComponent @@ -190,5 +193,3 @@ export function createModel (builder: Builder): void { } export default view - -export { viewOperation } from './migration' diff --git a/packages/model/package.json b/packages/model/package.json index b3b6dfb353..5143ad8217 100644 --- a/packages/model/package.json +++ b/packages/model/package.json @@ -29,6 +29,7 @@ "dependencies": { "@anticrm/core": "~0.6.11", "@anticrm/platform": "~0.6.5", - "toposort": "^2.0.2" + "toposort": "^2.0.2", + "fast-equals": "^2.0.3" } } diff --git a/packages/model/src/index.ts b/packages/model/src/index.ts index 36418d439b..a6d143ca66 100644 --- a/packages/model/src/index.ts +++ b/packages/model/src/index.ts @@ -15,3 +15,4 @@ export * from './dsl' export * from './migration' +export * from './utils' diff --git a/packages/model/src/utils.ts b/packages/model/src/utils.ts new file mode 100644 index 0000000000..febc5ebf3b --- /dev/null +++ b/packages/model/src/utils.ts @@ -0,0 +1,44 @@ +import { Class, Data, Doc, DocumentUpdate, Ref, Space, TxOperations } from '@anticrm/core' +import { deepEqual } from 'fast-equals' + +function toUndef (value: any): any { + return value === null ? undefined : value +} + +function diffAttributes (doc: Data, newDoc: Data): DocumentUpdate { + const result: DocumentUpdate = {} + const allDocuments = new Map(Object.entries(doc)) + const newDocuments = new Map(Object.entries(newDoc)) + + for (const [key, value] of allDocuments) { + const newValue = toUndef(newDocuments.get(key)) + if (!deepEqual(newValue, toUndef(value))) { + // update is required, since values are different + result[key] = newValue + } + } + for (const [key, value] of newDocuments) { + const oldValue = toUndef(allDocuments.get(key)) + if (oldValue === undefined && value !== undefined) { + // Update with new value. + result[key] = value + } + } + return result +} + +/** + * @public + */ +export async function createOrUpdate (client: TxOperations, _class: Ref>, space: Ref, data: Data, _id: Ref): Promise { + const existingDoc = await client.findOne(_class, { _id }) + if (existingDoc !== undefined) { + const { _class: _oldClass, _id, space: _oldSpace, modifiedBy, modifiedOn, ...oldData } = existingDoc + const updateOp = diffAttributes(oldData, data) + if (Object.keys(updateOp).length > 0) { + await client.update(existingDoc, updateOp) + } + } else { + await client.createDoc(_class, space, data, _id) + } +} diff --git a/packages/presentation/src/components/MessageBox.svelte b/packages/presentation/src/components/MessageBox.svelte index c5f5c4bc8b..0269ac68ed 100644 --- a/packages/presentation/src/components/MessageBox.svelte +++ b/packages/presentation/src/components/MessageBox.svelte @@ -29,7 +29,7 @@
diff --git a/packages/ui/src/components/Button.svelte b/packages/ui/src/components/Button.svelte index 54955f9421..60fac111d7 100644 --- a/packages/ui/src/components/Button.svelte +++ b/packages/ui/src/components/Button.svelte @@ -18,6 +18,7 @@ import Spinner from './Spinner.svelte' import Label from './Label.svelte' import Icon from './Icon.svelte' +import { onMount } from 'svelte' export let label: IntlString export let primary: boolean = false @@ -27,9 +28,18 @@ export let loading: boolean = false export let transparent: boolean = false export let width: string | undefined = undefined + export let focus: boolean = false + + let input: HTMLButtonElement + onMount(() => { + if (focus) { + input.focus() + focus = false + } + }) -