mirror of
https://github.com/hcengineering/platform.git
synced 2026-09-07 10:17:42 +02:00
Tag categories initial (#1030)
Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
Generated
+15
-3
@@ -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
|
||||
|
||||
+1
-1
@@ -16,5 +16,5 @@
|
||||
#
|
||||
|
||||
rushx bundle
|
||||
rushx docker:build
|
||||
rushx docker:build $@
|
||||
rushx docker:push
|
||||
|
||||
@@ -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
|
||||
]
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<TagCategory>
|
||||
}
|
||||
|
||||
@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<TagElement>
|
||||
|
||||
@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<Class<Doc>>
|
||||
|
||||
@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,
|
||||
|
||||
@@ -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<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 })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<Doc[]>>
|
||||
},
|
||||
category: {
|
||||
Category: '' as Ref<TagCategory>
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,3 +15,4 @@
|
||||
|
||||
export * from './dsl'
|
||||
export * from './migration'
|
||||
export * from './utils'
|
||||
|
||||
@@ -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<Doc>, newDoc: Data<Doc>): DocumentUpdate<Doc> {
|
||||
const result: DocumentUpdate<any> = {}
|
||||
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<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 })
|
||||
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<T>(_class, space, data, _id)
|
||||
}
|
||||
}
|
||||
@@ -29,7 +29,7 @@
|
||||
<div class="overflow-label fs-title mb-4"><Label {label} /></div>
|
||||
<div class="message"><Label label={message} /></div>
|
||||
<div class="footer">
|
||||
<Button label={presentation.string.Ok} size={'small'} transparent primary on:click={() => dispatch('close', true)} />
|
||||
<Button focus label={presentation.string.Ok} size={'small'} transparent primary on:click={() => dispatch('close', true)} />
|
||||
<Button label={presentation.string.Cancel} size={'small'} transparent on:click={() => dispatch('close', false)} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<button class="button {size}" class:transparent class:primary disabled={disabled || loading} style={width ? 'width: ' + width : ''} on:click>
|
||||
<button bind:this={input} class="button {size}" class:transparent class:primary disabled={disabled || loading} style={width ? 'width: ' + width : ''} on:click>
|
||||
{#if icon && !loading}
|
||||
<div class="icon">
|
||||
<Icon {icon} size={'small'}/>
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
|
||||
import type { DropdownTextItem } from '../types'
|
||||
import { showPopup } from '..'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
|
||||
export let title: IntlString
|
||||
export let caption: IntlString | undefined = undefined
|
||||
@@ -42,6 +43,8 @@
|
||||
}
|
||||
|
||||
const none = 'None' as IntlString
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
</script>
|
||||
|
||||
<div class="flex-col cursor-pointer"
|
||||
@@ -50,7 +53,10 @@
|
||||
if (!opened) {
|
||||
opened = true
|
||||
showPopup(DropdownLabelsPopup, { title, caption, items, selected, header }, btn, (result) => {
|
||||
if (result) selected = result
|
||||
if (result) {
|
||||
selected = result
|
||||
dispatch('selected', result)
|
||||
}
|
||||
opened = false
|
||||
})
|
||||
}
|
||||
|
||||
@@ -74,5 +74,5 @@ export interface ListItem {
|
||||
|
||||
export interface DropdownTextItem {
|
||||
id: string
|
||||
label: IntlString | string
|
||||
label: string
|
||||
}
|
||||
|
||||
@@ -16,9 +16,10 @@
|
||||
|
||||
<script lang="ts">
|
||||
import contact from '@anticrm/contact'
|
||||
import { Doc, DocumentQuery } from '@anticrm/core'
|
||||
import { getClient } from '@anticrm/presentation'
|
||||
import { Button, Icon, Label, ScrollBox, SearchEdit, showPopup } from '@anticrm/ui'
|
||||
import { Doc, DocumentQuery, Ref } from '@anticrm/core'
|
||||
import { createQuery, getClient } from '@anticrm/presentation'
|
||||
import tags, { selectedTagElements, TagCategory, TagElement } from '@anticrm/tags'
|
||||
import { Button, Component, Icon, Label, ScrollBox, SearchEdit, showPopup } from '@anticrm/ui'
|
||||
import view, { Viewlet } from '@anticrm/view'
|
||||
import { Table } from '@anticrm/view-resources'
|
||||
import recruit from '../plugin'
|
||||
@@ -26,18 +27,38 @@
|
||||
|
||||
let search = ''
|
||||
let resultQuery: DocumentQuery<Doc> = {}
|
||||
|
||||
function updateResultQuery (search: string): void {
|
||||
resultQuery = (search === '') ? { } : { $search: search }
|
||||
}
|
||||
|
||||
|
||||
const client = getClient()
|
||||
const tableDescriptor = client.findOne<Viewlet>(view.class.Viewlet, { attachTo: recruit.mixin.Candidate, descriptor: view.viewlet.Table })
|
||||
|
||||
|
||||
function showCreateDialog (ev: Event) {
|
||||
showPopup(CreateCandidate, { space: recruit.space.CandidatesPublic }, ev.target as HTMLElement)
|
||||
}
|
||||
|
||||
let category: Ref<TagCategory> | undefined = undefined
|
||||
|
||||
let documentIds:Ref<Doc>[] = []
|
||||
async function updateResultQuery (search: string, documentIds:Ref<Doc>[]): Promise<void> {
|
||||
resultQuery = (search === '') ? { } : { $search: search }
|
||||
if (documentIds.length > 0) {
|
||||
resultQuery._id = { $in: documentIds }
|
||||
}
|
||||
}
|
||||
|
||||
// Find all tags for object classe with matched elements
|
||||
const query = createQuery()
|
||||
|
||||
$: query.query(tags.class.TagReference, { tag: { $in: $selectedTagElements } }, (result) => {
|
||||
documentIds = Array.from(new Set<Ref<Doc>>(result.map(it => it.attachedTo)).values())
|
||||
})
|
||||
|
||||
$: updateResultQuery(search, documentIds)
|
||||
|
||||
function updateCategory (detail: {category: Ref<TagCategory> | null, elements: TagElement[] }) {
|
||||
console.log(detail)
|
||||
category = detail.category ?? undefined
|
||||
selectedTagElements.set(Array.from(detail.elements ?? []).map(it => it._id))
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="ac-header full">
|
||||
@@ -47,11 +68,13 @@
|
||||
</div>
|
||||
|
||||
<SearchEdit bind:value={search} on:change={() => {
|
||||
updateResultQuery(search)
|
||||
updateResultQuery(search, documentIds)
|
||||
}} />
|
||||
<Button label={recruit.string.Create} primary={true} size={'small'} on:click={(ev) => showCreateDialog(ev)} />
|
||||
</div>
|
||||
|
||||
<Component is={tags.component.TagsCategoryBar} props={{ targetClass: recruit.mixin.Candidate, category }} on:change={(evt) => updateCategory(evt.detail) }/>
|
||||
|
||||
<ScrollBox vertical stretch noShift>
|
||||
{#await tableDescriptor then descr}
|
||||
{#if descr}
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
} from '@anticrm/presentation'
|
||||
import type { Candidate } from '@anticrm/recruit'
|
||||
import { recognizeDocument } from '@anticrm/rekoni'
|
||||
import tags, { TagElement, TagReference } from '@anticrm/tags'
|
||||
import tags, { findTagCategory, findTagCategory, TagElement, TagReference } from '@anticrm/tags'
|
||||
import {
|
||||
Component,
|
||||
EditBox,
|
||||
@@ -153,6 +153,8 @@
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const categories = await client.findAll(tags.class.TagCategory, {})
|
||||
// Tag elements
|
||||
const skillTagElements = new Map(Array.from((await client.findAll(tags.class.TagElement, { _id: { $in: skills.map(it => it.tag) } })).map(it => ([it._id, it]))))
|
||||
for (const skill of skills) {
|
||||
@@ -162,7 +164,8 @@
|
||||
title: skill.title,
|
||||
color: skill.color,
|
||||
targetClass: recruit.mixin.Candidate,
|
||||
description: ''
|
||||
description: '',
|
||||
category: findTagCategory(skill.title, categories)
|
||||
})
|
||||
}
|
||||
await client.addCollection(skill._class, skill.space, candidateId, recruit.mixin.Candidate, 'skills', {
|
||||
@@ -243,6 +246,9 @@
|
||||
// Create skills
|
||||
await elementsPromise
|
||||
|
||||
const categories = await client.findAll(tags.class.TagCategory, {})
|
||||
const categoriesMap = new Map(Array.from(categories.map(it => ([it._id, it]))))
|
||||
|
||||
const newSkills:TagReference[] = []
|
||||
// Create missing tag elemnts
|
||||
for (const s of doc.skills ?? []) {
|
||||
@@ -250,12 +256,15 @@
|
||||
let e = namedElements.get(title)
|
||||
if (e === undefined) {
|
||||
// No yet tag with title
|
||||
const category = findTagCategory(s, categories)
|
||||
const cinstance = categoriesMap.get(category)
|
||||
e = TxProcessor.createDoc2Doc(
|
||||
client.txFactory.createTxCreateDoc(tags.class.TagElement, tags.space.Tags, {
|
||||
title,
|
||||
description: '',
|
||||
description: `Imported skill ${s} of ${cinstance?.label ?? ''}`,
|
||||
color: getColorNumberByText(s),
|
||||
targetClass: recruit.mixin.Candidate
|
||||
targetClass: recruit.mixin.Candidate,
|
||||
category
|
||||
})
|
||||
)
|
||||
namedElements.set(title, e)
|
||||
|
||||
@@ -24,6 +24,13 @@
|
||||
"SaveLabel": "Save",
|
||||
"WeightLabel": "Weight",
|
||||
"WeightPlaceholder": "Please type weight here",
|
||||
"CategoryPlaceholder": "Please type category here"
|
||||
"CategoryPlaceholder": "Please type category here",
|
||||
|
||||
"AssetLabel": "Asset",
|
||||
"CategoryTargetClass": "Category",
|
||||
"CategoryTagsLabel": "Category Items",
|
||||
"DataStructuresLabel": "Data Structures",
|
||||
"OtherCategoryLabel": "Other",
|
||||
"AllCategories": "All Categories"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 23.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Слой_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 36 36" style="enable-background:new 0 0 36 36;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:url(#SVGID_1_);}
|
||||
.st1{fill-rule:evenodd;clip-rule:evenodd;fill:url(#SVGID_2_);}
|
||||
</style>
|
||||
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="14.3791" y1="-2.5354" x2="21.6209" y2="38.5354">
|
||||
<stop offset="5.076140e-03" style="stop-color:#C52229"/>
|
||||
<stop offset="1" style="stop-color:#802022"/>
|
||||
</linearGradient>
|
||||
<rect class="st0" width="36" height="36"/>
|
||||
<linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="18" y1="6.2743" x2="18" y2="29.7257">
|
||||
<stop offset="0" style="stop-color:#FFFFFF"/>
|
||||
<stop offset="0.5066" style="stop-color:#FDFDFD"/>
|
||||
<stop offset="0.7696" style="stop-color:#F5F5F5"/>
|
||||
<stop offset="0.9763" style="stop-color:#E8E8E8"/>
|
||||
<stop offset="1" style="stop-color:#E6E6E6"/>
|
||||
</linearGradient>
|
||||
<path class="st1" d="M7.3,9.8c2.8-1.1,7.4-1.3,9.4-1.2l1.3,1.6l1.3-1.6c2-0.1,6.6,0.1,9.4,1.2c-0.4,0.7-2.1,1.8-3,2
|
||||
c-0.1-1.4-2.1-1.7-4-1.7L18,29.7l-3.7-19.7c-2,0-4,0.3-4,1.7C9.4,11.6,7.7,10.5,7.3,9.8L7.3,9.8z M6,7.8l0.7,1.1
|
||||
c2.7-1,5.9-1.4,9.3-1.5c1.3,0,2.6,0,3.9,0c3.4,0.1,6.6,0.6,9.3,1.5L30,7.8c-3.2-1.1-6.6-1.4-10-1.5c-1.3,0-2.6,0-4,0
|
||||
C12.6,6.4,9.2,6.7,6,7.8L6,7.8z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -0,0 +1,164 @@
|
||||
<!--
|
||||
// Copyright © 2022 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Class, Doc, Ref, SortingOrder } from '@anticrm/core'
|
||||
import { createQuery, getClient } from '@anticrm/presentation'
|
||||
import { TagCategory, TagElement } from '@anticrm/tags'
|
||||
import tags from '../plugin'
|
||||
import { getPlatformColorForText, Label } from '@anticrm/ui'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { getTagStyle } from '../utils'
|
||||
|
||||
export let targetClass: Ref<Class<Doc>>
|
||||
export let category: Ref<TagCategory> | undefined = undefined
|
||||
export let gap: 'small' | 'big' = 'small'
|
||||
|
||||
let categories: TagCategory[] = []
|
||||
let visibleCategories: TagCategory[] = []
|
||||
|
||||
const stepStyle = gap === 'small' ? 'gap-1' : 'gap-2'
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const client = getClient()
|
||||
|
||||
let elements: TagElement[] = []
|
||||
|
||||
let categoryCounts = new Map<Ref<TagCategory>, TagElement[]>()
|
||||
let categoryKeys: Ref<TagCategory>[] = []
|
||||
|
||||
const elementsQuery = createQuery()
|
||||
$: elementsQuery.query(
|
||||
tags.class.TagElement,
|
||||
{ targetClass: { $in: [targetClass, ...client.getHierarchy().getAncestors(targetClass)] } },
|
||||
(res) => {
|
||||
elements = res
|
||||
},
|
||||
{
|
||||
sort: {
|
||||
title: SortingOrder.Ascending
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
$: {
|
||||
const counts = new Map<Ref<TagCategory>, TagElement[]>()
|
||||
for (const e of elements) {
|
||||
if (e.category !== undefined) {
|
||||
const els = counts.get(e.category) ?? []
|
||||
els.push(e)
|
||||
counts.set(e.category, els)
|
||||
}
|
||||
}
|
||||
categoryCounts = counts
|
||||
categoryKeys = Array.from(categoryCounts.keys())
|
||||
}
|
||||
|
||||
const categoriesQuery = createQuery()
|
||||
$: categoriesQuery.query(
|
||||
tags.class.TagCategory,
|
||||
{},
|
||||
(res) => {
|
||||
categories = res
|
||||
},
|
||||
{ sort: { label: SortingOrder.Ascending } }
|
||||
)
|
||||
|
||||
$: visibleCategories = categories.filter((it) => categoryKeys.includes(it._id))
|
||||
|
||||
const selectItem = (ev: Event, item: TagCategory): void => {
|
||||
if (category === item._id) {
|
||||
category = undefined
|
||||
} else {
|
||||
category = item._id
|
||||
}
|
||||
dispatch('change', { category, elements: category !== undefined ? categoryCounts.get(category) ?? [] : [] })
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if visibleCategories.length > 0}
|
||||
<div class="flex-between mb-4 header">
|
||||
<div class="flex-row-center buttons">
|
||||
<div
|
||||
class="button flex-center"
|
||||
class:active={category === undefined}
|
||||
on:click={() => {
|
||||
category = undefined
|
||||
dispatch('change', { category, elements: [] })
|
||||
}}
|
||||
>
|
||||
<Label label={tags.string.AllCategories} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-row-center caption-color states">
|
||||
<div class="antiStatesBar mask-none {stepStyle}">
|
||||
{#each visibleCategories as item, i (item._id)}
|
||||
<div
|
||||
class="categoryElement flex-center"
|
||||
label={item.label}
|
||||
style={getTagStyle(getPlatformColorForText(item.label), item._id === category)}
|
||||
on:click={(ev) => {
|
||||
if (item._id !== category) selectItem(ev, item)
|
||||
}}
|
||||
>
|
||||
{item.label} ({categoryCounts.get(item._id)?.length ?? ''})
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
.categoryElement {
|
||||
padding: 0.5rem 0.75rem;
|
||||
height: 2.5rem;
|
||||
white-space: nowrap;
|
||||
border: 1px solid var(--theme-button-border-enabled);
|
||||
border-radius: 0.5rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
.categoryElement + .categoryElement {
|
||||
margin-left: 0.75rem;
|
||||
}
|
||||
|
||||
.header {
|
||||
margin-left: 2.5rem;
|
||||
margin-right: 1.75rem;
|
||||
.buttons {
|
||||
padding: 0.125rem 0;
|
||||
}
|
||||
.states {
|
||||
max-width: 75%;
|
||||
}
|
||||
|
||||
.button {
|
||||
height: 2.5rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 0.5rem;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--theme-button-bg-enabled);
|
||||
border-color: var(--theme-button-border-enabled);
|
||||
}
|
||||
&.active {
|
||||
background-color: var(--theme-button-bg-enabled);
|
||||
color: var(--theme-caption-color);
|
||||
border-color: var(--theme-button-border-enabled);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,23 @@
|
||||
<!--
|
||||
// Copyright © 2022 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { TagCategory } from '@anticrm/tags'
|
||||
|
||||
export let value: TagCategory
|
||||
</script>
|
||||
|
||||
{#if value}
|
||||
{value.label}
|
||||
{/if}
|
||||
@@ -14,9 +14,10 @@
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Class, Data, Doc, generateId, Ref } from '@anticrm/core'
|
||||
import { Card, getClient } from '@anticrm/presentation'
|
||||
import { TagElement } from '@anticrm/tags'
|
||||
import { EditBox, getColorNumberByText, getPlatformColor, showPopup } from '@anticrm/ui'
|
||||
import { Card, createQuery, getClient } from '@anticrm/presentation'
|
||||
import { findTagCategory, TagCategory, TagElement } from '@anticrm/tags'
|
||||
import { DropdownLabels, EditBox, getColorNumberByText, getPlatformColor, showPopup } from '@anticrm/ui'
|
||||
import { DropdownTextItem } from '@anticrm/ui/src/types'
|
||||
import { ColorsPopup } from '@anticrm/view-resources'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import tags from '../plugin'
|
||||
@@ -29,12 +30,22 @@
|
||||
let description = ''
|
||||
let color: number = 0
|
||||
|
||||
let categoryWasSet = false
|
||||
let category: Ref<TagCategory> = tags.category.Other
|
||||
|
||||
let categories: TagCategory[] = []
|
||||
let categoryItems: DropdownTextItem[] = []
|
||||
|
||||
let colorSet = false
|
||||
|
||||
$: if (!colorSet) {
|
||||
color = getColorNumberByText(title)
|
||||
}
|
||||
|
||||
$: if (!categoryWasSet) {
|
||||
category = findTagCategory(title, categories)
|
||||
}
|
||||
|
||||
export function canClose (): boolean {
|
||||
return title === ''
|
||||
}
|
||||
@@ -43,12 +54,27 @@
|
||||
const client = getClient()
|
||||
const tagElementId = generateId()
|
||||
|
||||
const query = createQuery()
|
||||
|
||||
query.query(tags.class.TagCategory, {}, async (result) => {
|
||||
const newItems: DropdownTextItem[] = []
|
||||
for (const r of result) {
|
||||
newItems.push({
|
||||
id: r._id,
|
||||
label: r.label
|
||||
})
|
||||
}
|
||||
categories = result
|
||||
categoryItems = newItems
|
||||
})
|
||||
|
||||
async function createTagElenent () {
|
||||
const tagElement: Data<TagElement> = {
|
||||
title: title,
|
||||
description,
|
||||
targetClass,
|
||||
color
|
||||
color,
|
||||
category: category
|
||||
}
|
||||
|
||||
await client.createDoc(tags.class.TagElement, tags.space.Tags, tagElement, tagElementId)
|
||||
@@ -93,6 +119,17 @@
|
||||
<div class="text-sm mt-4">
|
||||
<EditBox placeholder={tags.string.TagDescriptionPlaceholder} maxWidth="15rem" bind:value={description} />
|
||||
</div>
|
||||
|
||||
<div class="text-sm mt-4">
|
||||
<DropdownLabels
|
||||
title={tags.string.CategoryLabel}
|
||||
bind:selected={category}
|
||||
items={categoryItems}
|
||||
on:selected={() => {
|
||||
categoryWasSet = true
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
@@ -14,9 +14,10 @@
|
||||
-->
|
||||
<script lang="ts">
|
||||
import core, { Data, DocumentUpdate } from '@anticrm/core'
|
||||
import { Card, getClient } from '@anticrm/presentation'
|
||||
import { Card, createQuery, getClient } from '@anticrm/presentation'
|
||||
import { TagElement, TagReference } from '@anticrm/tags'
|
||||
import { EditBox, getPlatformColor, showPopup } from '@anticrm/ui'
|
||||
import { DropdownLabels, EditBox, getPlatformColor, showPopup } from '@anticrm/ui'
|
||||
import { DropdownTextItem } from '@anticrm/ui/src/types'
|
||||
import { ColorsPopup } from '@anticrm/view-resources'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import tags from '../plugin'
|
||||
@@ -28,10 +29,13 @@
|
||||
const dispatch = createEventDispatcher()
|
||||
const client = getClient()
|
||||
|
||||
let categoryItems: DropdownTextItem[] = []
|
||||
|
||||
const data: Omit<Data<TagElement>, 'targetClass'> = {
|
||||
title: value.title,
|
||||
description: value.description,
|
||||
color: value.color
|
||||
color: value.color,
|
||||
category: value.category
|
||||
}
|
||||
|
||||
async function updateElement () {
|
||||
@@ -47,6 +51,10 @@
|
||||
documentUpdate.description = data.description
|
||||
}
|
||||
|
||||
if (data.category !== value.category) {
|
||||
documentUpdate.category = data.category
|
||||
}
|
||||
|
||||
if (data.color !== value.color) {
|
||||
documentUpdate.color = data.color
|
||||
refUpdate.color = data.color
|
||||
@@ -67,6 +75,18 @@
|
||||
|
||||
dispatch('close')
|
||||
}
|
||||
|
||||
const query = createQuery()
|
||||
query.query(tags.class.TagCategory, {}, async (result) => {
|
||||
const newItems: DropdownTextItem[] = []
|
||||
for (const r of result) {
|
||||
newItems.push({
|
||||
id: r._id,
|
||||
label: r.label
|
||||
})
|
||||
}
|
||||
categoryItems = newItems
|
||||
})
|
||||
</script>
|
||||
|
||||
<Card
|
||||
@@ -105,6 +125,10 @@
|
||||
<div class="fs-title mt-4">
|
||||
<EditBox placeholder={tags.string.TagDescriptionPlaceholder} maxWidth="15rem" bind:value={data.description} />
|
||||
</div>
|
||||
|
||||
<div class="text-sm mt-4">
|
||||
<DropdownLabels title={tags.string.CategoryLabel} bind:selected={data.category} items={categoryItems} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
export let tag: TagReference | undefined = undefined
|
||||
export let element: TagElement | undefined = undefined
|
||||
export let action: Asset | AnySvelteComponent | undefined = undefined
|
||||
export let selected: boolean = false
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
@@ -36,7 +37,7 @@
|
||||
>
|
||||
<div
|
||||
class="text-sm flex flex-between tag-item"
|
||||
style={`${getTagStyle(getPlatformColor(tag?.color ?? element?.color ?? 0))}`}
|
||||
style={`${getTagStyle(getPlatformColor(tag?.color ?? element?.color ?? 0), selected)}`}
|
||||
>
|
||||
{name}
|
||||
{#if action}
|
||||
|
||||
@@ -21,8 +21,6 @@
|
||||
$: values = Array.isArray(value) ? value : [value]
|
||||
</script>
|
||||
|
||||
{#each values as v, i}
|
||||
{#if i < 2}
|
||||
<TagItem tag={v} />
|
||||
{/if}
|
||||
{#each values as v}
|
||||
<TagItem tag={v} />
|
||||
{/each}
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
import { TagElement, TagReference } from '@anticrm/tags'
|
||||
import tags from '../plugin'
|
||||
import TagItem from './TagItem.svelte'
|
||||
import { selectedTagElements } from '@anticrm/tags'
|
||||
|
||||
export let object: Doc
|
||||
export let _class: Ref<Class<Doc>>
|
||||
@@ -43,8 +44,8 @@
|
||||
</script>
|
||||
|
||||
<div class="tags flex flex-wrap">
|
||||
{#each items as tag, i}
|
||||
<TagItem {tag} element={elements.get(tag.tag)} />
|
||||
{#each items as tag}
|
||||
<TagItem {tag} element={elements.get(tag.tag)} selected={$selectedTagElements.includes(tag.tag)} />
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -13,11 +13,13 @@
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { Class, Doc, DocumentQuery, Ref } from '@anticrm/core'
|
||||
import { Class, Doc, DocumentQuery, FindOptions, Ref } from '@anticrm/core'
|
||||
import { IntlString, translate } from '@anticrm/platform'
|
||||
import { TagCategory, TagElement } from '@anticrm/tags'
|
||||
import { Button, Icon, Label, ScrollBox, SearchEdit, showPopup } from '@anticrm/ui'
|
||||
import { Table } from '@anticrm/view-resources'
|
||||
import tags from '../plugin'
|
||||
import CategoryBar from './CategoryBar.svelte'
|
||||
import CreateTagElement from './CreateTagElement.svelte'
|
||||
|
||||
export let title: IntlString = tags.string.Tags
|
||||
@@ -30,15 +32,24 @@
|
||||
})
|
||||
|
||||
let search = ''
|
||||
let resultQuery: DocumentQuery<Doc> = { targetClass }
|
||||
let resultQuery: DocumentQuery<TagElement> = { targetClass }
|
||||
|
||||
function updateResultQuery (search: string): void {
|
||||
function updateResultQuery (search: string, category?: Ref<TagCategory>): void {
|
||||
resultQuery = search === '' ? { targetClass } : { $search: search, targetClass }
|
||||
if (category !== undefined) {
|
||||
resultQuery.category = category
|
||||
}
|
||||
}
|
||||
|
||||
function showCreateDialog (ev: Event) {
|
||||
showPopup(CreateTagElement, { targetClass, keyTitle }, ev.target as HTMLElement)
|
||||
}
|
||||
const opt: FindOptions<TagElement> = {
|
||||
lookup: {
|
||||
category: tags.class.TagCategory
|
||||
}
|
||||
}
|
||||
let category: Ref<TagCategory> | undefined = undefined
|
||||
</script>
|
||||
|
||||
<div class="ac-header full">
|
||||
@@ -50,12 +61,20 @@
|
||||
<SearchEdit
|
||||
bind:value={search}
|
||||
on:change={() => {
|
||||
updateResultQuery(search)
|
||||
updateResultQuery(search, category)
|
||||
}}
|
||||
/>
|
||||
<Button label={tags.string.CreateItemLabel} primary={true} size={'small'} on:click={(ev) => showCreateDialog(ev)} />
|
||||
</div>
|
||||
|
||||
<CategoryBar
|
||||
{targetClass}
|
||||
{category}
|
||||
on:change={(evt) => {
|
||||
category = evt.detail.category ?? undefined
|
||||
updateResultQuery(search, category)
|
||||
}}
|
||||
/>
|
||||
<ScrollBox vertical stretch noShift>
|
||||
<Table
|
||||
_class={tags.class.TagElement}
|
||||
@@ -67,10 +86,20 @@
|
||||
props: { edit: true, keyTitle },
|
||||
sortingKey: 'title'
|
||||
},
|
||||
...(category === undefined
|
||||
? [
|
||||
{
|
||||
key: '$lookup.category',
|
||||
presenter: tags.component.CategoryPresenter,
|
||||
sortingKey: '$lookup.category',
|
||||
label: tags.string.CategoryLabel
|
||||
}
|
||||
]
|
||||
: []),
|
||||
'description',
|
||||
'modifiedOn'
|
||||
]}
|
||||
options={{}}
|
||||
options={opt}
|
||||
query={resultQuery}
|
||||
enableChecking
|
||||
/>
|
||||
|
||||
@@ -21,7 +21,9 @@ import TagsPresenter from './components/TagsPresenter.svelte'
|
||||
import TagsItemPresenter from './components/TagsItemPresenter.svelte'
|
||||
import TagsView from './components/TagsView.svelte'
|
||||
import TagsEditor from './components/TagsEditor.svelte'
|
||||
import CategoryPresenter from './components/CategoryPresenter.svelte'
|
||||
import tags from './plugin'
|
||||
import TagsCategoryBar from './components/CategoryBar.svelte'
|
||||
|
||||
async function DeleteTagElement (doc: TagElement, client: TxOperations): Promise<Doc[]> {
|
||||
return await client.findAll(tags.class.TagReference, { tag: doc._id })
|
||||
@@ -35,7 +37,9 @@ export default async (): Promise<Resources> => ({
|
||||
TagsPresenter,
|
||||
TagsView,
|
||||
TagsEditor,
|
||||
TagsItemPresenter
|
||||
TagsItemPresenter,
|
||||
CategoryPresenter,
|
||||
TagsCategoryBar
|
||||
},
|
||||
dd: {
|
||||
DeleteTagElement
|
||||
|
||||
@@ -17,7 +17,8 @@ import { AnyComponent } from '@anticrm/ui'
|
||||
|
||||
export default mergeIds(tagsId, tags, {
|
||||
component: {
|
||||
TagElementPresenter: '' as AnyComponent
|
||||
TagElementPresenter: '' as AnyComponent,
|
||||
CategoryPresenter: '' as AnyComponent
|
||||
},
|
||||
string: {
|
||||
NoTags: '' as IntlString,
|
||||
@@ -38,6 +39,8 @@ export default mergeIds(tagsId, tags, {
|
||||
Tags: '' as IntlString,
|
||||
Tag: '' as IntlString,
|
||||
TagName: '' as IntlString,
|
||||
SaveLabel: '' as IntlString
|
||||
SaveLabel: '' as IntlString,
|
||||
CategoryLabel: '' as IntlString,
|
||||
AllCategories: '' as IntlString
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,19 +1,8 @@
|
||||
// Copyright © 2022 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
export function getTagStyle (color: string): string {
|
||||
export function getTagStyle (color: string, selected = false): string {
|
||||
return `
|
||||
background: ${color + '33'};
|
||||
border: 1px solid ${color + '66'};
|
||||
background: ${color + (selected ? 'ff' : '33')};
|
||||
border: 1px solid ${color + (selected ? 'ff' : '66')};
|
||||
`
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
"dependencies": {
|
||||
"@anticrm/platform": "~0.6.5",
|
||||
"@anticrm/core": "~0.6.11",
|
||||
"@anticrm/ui": "~0.6.0"
|
||||
"@anticrm/ui": "~0.6.0",
|
||||
"svelte": "^3.37.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import type { AttachedDoc, Class, Doc, Ref, Space } from '@anticrm/core'
|
||||
import type { Asset, Plugin } from '@anticrm/platform'
|
||||
import { plugin } from '@anticrm/platform'
|
||||
import { AnyComponent } from '@anticrm/ui'
|
||||
import { writable } from 'svelte/store'
|
||||
|
||||
/**
|
||||
* @public
|
||||
@@ -26,6 +27,7 @@ export interface TagElement extends Doc {
|
||||
targetClass: Ref<Class<Doc>>
|
||||
description: string
|
||||
color: number
|
||||
category: Ref<TagCategory>
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -37,15 +39,33 @@ export interface TagReference extends AttachedDoc {
|
||||
color: number // Copy of color from tagElement. Updated with trigger.
|
||||
}
|
||||
|
||||
/**
|
||||
* Defined set of skills per category.
|
||||
*
|
||||
* Will be used as skill category templates or category detection
|
||||
* @public
|
||||
*/
|
||||
export interface TagCategory extends Doc {
|
||||
icon: Asset
|
||||
label: string
|
||||
targetClass: Ref<Class<Doc>>
|
||||
// A list of possible variants.
|
||||
tags: string[]
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export const tagsId = 'tags' as Plugin
|
||||
|
||||
export default plugin(tagsId, {
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
const tagsPlugin = plugin(tagsId, {
|
||||
class: {
|
||||
TagElement: '' as Ref<Class<TagElement>>,
|
||||
TagReference: '' as Ref<Class<TagReference>>
|
||||
TagReference: '' as Ref<Class<TagReference>>,
|
||||
TagCategory: '' as Ref<Class<TagCategory>>
|
||||
},
|
||||
space: {
|
||||
Tags: '' as Ref<Space>
|
||||
@@ -55,6 +75,32 @@ export default plugin(tagsId, {
|
||||
},
|
||||
component: {
|
||||
TagsView: '' as AnyComponent,
|
||||
TagsEditor: '' as AnyComponent
|
||||
TagsEditor: '' as AnyComponent,
|
||||
TagsCategoryBar: '' as AnyComponent
|
||||
},
|
||||
category: {
|
||||
Other: '' as Ref<TagCategory>
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export default tagsPlugin
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export function findTagCategory (title: string, categories: TagCategory[]): Ref<TagCategory> {
|
||||
for (const c of categories) {
|
||||
if (c.tags.findIndex((it) => it.toLowerCase() === title.toLowerCase()) !== -1) {
|
||||
return c._id
|
||||
}
|
||||
}
|
||||
return tagsPlugin.category.Other
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export const selectedTagElements = writable<Ref<TagElement>[]>([])
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
let maskLeft: boolean = false
|
||||
let maskRight: boolean = false
|
||||
let mask: 'left' | 'right' | 'both' | 'none' = 'none'
|
||||
let stepStyle = (gap === 'small') ? 'gap-1' : 'gap-2'
|
||||
$: stepStyle = (gap === 'small') ? 'gap-1' : 'gap-2'
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user