mirror of
https://github.com/hcengineering/platform.git
synced 2026-09-28 12:35:02 +02:00
Skills search update (#1573)
Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
@@ -84,7 +84,7 @@
|
||||
|
||||
<Component
|
||||
is={tags.component.TagsCategoryBar}
|
||||
props={{ targetClass: recruit.mixin.Candidate, category }}
|
||||
props={{ targetClass: recruit.mixin.Candidate, category, selected: $selectedTagElements, mode: 'item' }}
|
||||
on:change={(evt) => updateCategory(evt.detail)}
|
||||
/>
|
||||
|
||||
|
||||
@@ -1,7 +1,25 @@
|
||||
<script lang='ts'>
|
||||
import tags from '@anticrm/tags'
|
||||
import { Component } from '@anticrm/ui'
|
||||
<script lang="ts">
|
||||
import tags, { selectedTagElements, TagElement } from '@anticrm/tags'
|
||||
import { Component, getCurrentLocation, navigate } from '@anticrm/ui'
|
||||
import recruit from '../plugin'
|
||||
|
||||
function onTag (tag: TagElement):void {
|
||||
selectedTagElements.set([tag._id])
|
||||
const loc = getCurrentLocation()
|
||||
loc.path[2] = 'candidates'
|
||||
loc.path.length = 3
|
||||
navigate(loc)
|
||||
}
|
||||
</script>
|
||||
|
||||
<Component is={tags.component.TagsView} props={{ targetClass: recruit.mixin.Candidate, title: recruit.string.SkillsLabel, item: recruit.string.SkillLabel, key: 'skills', сreateItemLabel: recruit.string.SkillCreateLabel }}/>
|
||||
<Component
|
||||
is={tags.component.TagsView}
|
||||
props={{
|
||||
targetClass: recruit.mixin.Candidate,
|
||||
title: recruit.string.SkillsLabel,
|
||||
item: recruit.string.SkillLabel,
|
||||
key: 'skills',
|
||||
сreateItemLabel: recruit.string.SkillCreateLabel,
|
||||
onTag: onTag
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -30,6 +30,9 @@
|
||||
"CategoryTagsLabel": "Category Items",
|
||||
"OtherCategoryLabel": "Other",
|
||||
"AllCategories": "All Categories",
|
||||
"DefaultLabel": "Default category"
|
||||
"DefaultLabel": "Default category",
|
||||
"SelectAll": "Select all",
|
||||
"SelectNone": "Select none",
|
||||
"ApplyTags": "Apply"
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,9 @@
|
||||
"CategoryTargetClass": "Категория",
|
||||
"CategoryTagsLabel": "Теги категории",
|
||||
"OtherCategoryLabel": "Другое",
|
||||
"AllCategories": "Все категории"
|
||||
"AllCategories": "Все категории",
|
||||
"SelectAll": "Выбрать все",
|
||||
"SelectNone": "Выбрать ничего",
|
||||
"ApplyTags": "Применить"
|
||||
}
|
||||
}
|
||||
@@ -16,27 +16,30 @@
|
||||
import { Class, Doc, Ref, SortingOrder } from '@anticrm/core'
|
||||
import { createQuery } from '@anticrm/presentation'
|
||||
import { TagCategory, TagElement } from '@anticrm/tags'
|
||||
import { getPlatformColorForText, Button } from '@anticrm/ui'
|
||||
import { Button, getPlatformColorForText, showPopup } from '@anticrm/ui'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import tags from '../plugin'
|
||||
import { getTagStyle } from '../utils'
|
||||
import TagsCategoryPopup from './TagsCategoryPopup.svelte'
|
||||
|
||||
export let targetClass: Ref<Class<Doc>>
|
||||
export let category: Ref<TagCategory> | undefined = undefined
|
||||
export let selected: Ref<TagElement>[] = []
|
||||
export let gap: 'small' | 'big' = 'small'
|
||||
|
||||
export let mode: 'item' | 'category' = 'category'
|
||||
|
||||
let categories: TagCategory[] = []
|
||||
let visibleCategories: TagCategory[] = []
|
||||
|
||||
|
||||
const stepStyle = gap === 'small' ? 'gap-1' : 'gap-2'
|
||||
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
|
||||
let elements: TagElement[] = []
|
||||
|
||||
|
||||
let categoryCounts = new Map<Ref<TagCategory>, TagElement[]>()
|
||||
let categoryKeys: Ref<TagCategory>[] = []
|
||||
|
||||
|
||||
const elementsQuery = createQuery()
|
||||
$: elementsQuery.query(
|
||||
tags.class.TagElement,
|
||||
@@ -55,6 +58,9 @@
|
||||
const counts = new Map<Ref<TagCategory>, TagElement[]>()
|
||||
for (const e of elements) {
|
||||
if (e.category !== undefined) {
|
||||
if (selected.includes(e._id) && category === undefined) {
|
||||
category = e.category
|
||||
}
|
||||
const els = counts.get(e.category) ?? []
|
||||
els.push(e)
|
||||
counts.set(e.category, els)
|
||||
@@ -82,7 +88,38 @@
|
||||
} else {
|
||||
category = item._id
|
||||
}
|
||||
dispatch('change', { category, elements: category !== undefined ? categoryCounts.get(category) ?? [] : [] })
|
||||
selected = (category !== undefined ? categoryCounts.get(category) ?? [] : []).map(it => it._id)
|
||||
dispatch('change', { category, elements: selected })
|
||||
}
|
||||
|
||||
function selectTag (evt: Event, category: TagCategory): void {
|
||||
showPopup(
|
||||
TagsCategoryPopup,
|
||||
{
|
||||
category,
|
||||
targetClass,
|
||||
selected,
|
||||
keyLabel: category.label,
|
||||
hideAdd: true
|
||||
},
|
||||
evt.target as HTMLElement,
|
||||
(result) => {
|
||||
if (result !== undefined) {
|
||||
selected = result.map((it: TagElement) => it._id)
|
||||
dispatch('change', { category: category._id, elements: result })
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
const visibleCategoriesRef: HTMLElement[] = []
|
||||
|
||||
$: visibleCategoriesRef.length = visibleCategories.length
|
||||
|
||||
$: if (category !== undefined && visibleCategories.length > 0 && visibleCategoriesRef.length > 0) {
|
||||
const idx = visibleCategories.findIndex(it => it._id === category)
|
||||
if (idx !== -1) {
|
||||
visibleCategoriesRef[idx]?.scrollIntoView({ block: 'nearest' })
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -101,16 +138,25 @@
|
||||
</div>
|
||||
<div class="flex-row-center caption-color states">
|
||||
<div class="antiStatesBar mask-none {stepStyle}">
|
||||
{#each visibleCategories as item (item._id)}
|
||||
<div
|
||||
{#each visibleCategories as item, i}
|
||||
<div bind:this={visibleCategoriesRef[i]}
|
||||
class="categoryElement flex-center"
|
||||
label={item.label}
|
||||
style={getTagStyle(getPlatformColorForText(item.label), item._id === category)}
|
||||
on:click={() => {
|
||||
if (item._id !== category) selectItem(item)
|
||||
on:click={(evt) => {
|
||||
if (mode === 'category') {
|
||||
selectItem(item)
|
||||
} else {
|
||||
selectTag(evt, item)
|
||||
}
|
||||
}}
|
||||
>
|
||||
{item.label} ({categoryCounts.get(item._id)?.length ?? ''})
|
||||
{item.label}
|
||||
{#if item._id === category && mode === 'item'}
|
||||
({selected.length}/{categoryCounts.get(item._id)?.length ?? ''})
|
||||
{:else}
|
||||
({categoryCounts.get(item._id)?.length ?? ''})
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
<!--
|
||||
// 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 { Ref } from '@anticrm/core'
|
||||
import { TagElement } from '@anticrm/tags'
|
||||
import { Icon } from '@anticrm/ui'
|
||||
import tags from '../plugin'
|
||||
|
||||
export let value: TagElement
|
||||
export let tagElements: Map<Ref<TagElement>, { count: number; modifiedOn: number }> | undefined
|
||||
export let onTag: ((tag: TagElement) => void) | undefined = undefined
|
||||
</script>
|
||||
|
||||
{#if (tagElements?.get(value._id)?.count ?? 0) > 0}
|
||||
<div class="sm-tool-icon" on:click={() => onTag?.(value)}>
|
||||
<span class="icon"><Icon icon={tags.icon.Tags} size={'small'} /> </span>
|
||||
{tagElements?.get(value._id)?.count ?? 0}
|
||||
</div>
|
||||
{/if}
|
||||
@@ -0,0 +1,162 @@
|
||||
<!--
|
||||
// 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 } from '@anticrm/core'
|
||||
import type { IntlString } from '@anticrm/platform'
|
||||
import { translate } from '@anticrm/platform'
|
||||
import presentation, { createQuery } from '@anticrm/presentation'
|
||||
import { TagCategory, TagElement } from '@anticrm/tags'
|
||||
import { Button, CheckBox, getPlatformColor, Icon, IconClose, Label } from '@anticrm/ui'
|
||||
import { createEventDispatcher, onMount } from 'svelte'
|
||||
import tags from '../plugin'
|
||||
|
||||
export let targetClass: Ref<Class<Doc>>
|
||||
export let placeholder: IntlString = presentation.string.Search
|
||||
export let selected: Ref<TagElement>[] = []
|
||||
export let keyLabel: string = ''
|
||||
export let category: TagCategory
|
||||
|
||||
let elements: TagElement[] = []
|
||||
|
||||
let search: string = ''
|
||||
let searchElement: HTMLInputElement
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const query = createQuery()
|
||||
|
||||
let phTraslate: string = ''
|
||||
$: if (placeholder) translate(placeholder, {}).then(res => { phTraslate = res })
|
||||
|
||||
$: query.query(
|
||||
tags.class.TagElement,
|
||||
{ title: { $like: '%' + search + '%' }, targetClass, category: category._id },
|
||||
(result) => {
|
||||
elements = result
|
||||
},
|
||||
{ limit: 200 }
|
||||
)
|
||||
|
||||
const isSelected = (element: TagElement, selected: Ref<TagElement>[]): boolean => {
|
||||
if (selected.filter(p => p === element._id).length > 0) return true
|
||||
return false
|
||||
}
|
||||
const checkSelected = (element: TagElement, _selected: Ref<TagElement>[]): void => {
|
||||
if (isSelected(element, _selected)) {
|
||||
selected = selected.filter(p => p !== element._id)
|
||||
} else {
|
||||
selected = [...selected, element._id]
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => { if (searchElement) searchElement.focus() })
|
||||
|
||||
type TagElementInfo = { count: number, modifiedOn: number }
|
||||
let tagElements: Map<Ref<TagElement>, TagElementInfo> | undefined
|
||||
const refQuery = createQuery()
|
||||
$: refQuery.query(
|
||||
tags.class.TagReference, { tag: { $in: elements.map(it => it._id) } },
|
||||
(res) => {
|
||||
const result = new Map<Ref<TagElement>, TagElementInfo>()
|
||||
|
||||
for (const d of res) {
|
||||
const v = result.get(d.tag) ?? { count: 0, modifiedOn: 0 }
|
||||
v.count++
|
||||
v.modifiedOn = Math.max(v.modifiedOn, d.modifiedOn)
|
||||
result.set(d.tag, v)
|
||||
}
|
||||
|
||||
tagElements = result
|
||||
}, {
|
||||
projection: {
|
||||
_id: 1, tag: 1, modifiedOn: 1
|
||||
}
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
<div class="selectPopup maxHeight">
|
||||
<div class="header no-border">
|
||||
<div class="flex-between flex-grow pr-2">
|
||||
<div class="flex-grow">
|
||||
<input bind:this={searchElement} type="text" bind:value={search} placeholder={phTraslate} style="width: 100%;" on:change/>
|
||||
</div>
|
||||
<div class="buttons-group small-gap">
|
||||
<div class="clear-btn" class:show={search !== ''} on:click={() => {
|
||||
search = ''
|
||||
searchElement.focus()
|
||||
}}>
|
||||
{#if search !== ''}<div class="icon"><Icon icon={IconClose} size={'inline'} /></div>{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="scroll">
|
||||
<div class="box">
|
||||
{#if elements.length > 0}
|
||||
<div class="sticky-wrapper">
|
||||
<div class="menu-group" style:overflow='visible'>
|
||||
{#each elements as element}
|
||||
<button class="menu-item" on:click={() => {
|
||||
checkSelected(element, selected)
|
||||
}}>
|
||||
<div class="check pointer-events-none">
|
||||
<CheckBox checked={isSelected(element, selected)} primary />
|
||||
</div>
|
||||
<div class="tag" style="background-color: {getPlatformColor(element.color)};" />
|
||||
{element.title}
|
||||
{#if (tagElements?.get(element._id)?.count ?? 0) > 0}
|
||||
({tagElements?.get(element._id)?.count})
|
||||
{/if}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{#if elements.length === 0}
|
||||
<div class="empty">
|
||||
<Label label={tags.string.NoItems} params={{ word: keyLabel }} />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div class='flex-between p-2'>
|
||||
<Button kind={'transparent'} label={tags.string.SelectAll} on:click={() => {
|
||||
selected = elements.map(it => it._id)
|
||||
}}/>
|
||||
<Button kind={'transparent'} label={tags.string.SelectNone} on:click={() => {
|
||||
selected = []
|
||||
}}/>
|
||||
</div>
|
||||
<Button shape={'round'} label={tags.string.ApplyTags} on:click={() => dispatch('close', elements.filter(it => selected.includes(it._id)))}/>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.counter {
|
||||
padding-right: .125rem;
|
||||
min-width: 1.5rem;
|
||||
text-align: right;
|
||||
font-size: .8125rem;
|
||||
color: var(--caption-color);
|
||||
}
|
||||
.empty {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
font-size: .75rem;
|
||||
color: var(--dark-color);
|
||||
border-top: 1px solid var(--popup-divider);
|
||||
}
|
||||
</style>
|
||||
@@ -14,15 +14,14 @@
|
||||
-->
|
||||
<script lang="ts">
|
||||
import type { AttachedDoc, Class, Collection, Doc, Ref } from '@anticrm/core'
|
||||
import { IntlString, translate } from '@anticrm/platform'
|
||||
import { translate } from '@anticrm/platform'
|
||||
import { KeyedAttribute } from '@anticrm/presentation'
|
||||
import { TagElement, TagReference } from '@anticrm/tags'
|
||||
import type { ButtonKind, ButtonSize, TooltipAlignment } from '@anticrm/ui'
|
||||
import { ShowMore, Label, CircleButton, Button, showPopup, Tooltip, IconAdd, IconClose } from '@anticrm/ui'
|
||||
import { createEventDispatcher, afterUpdate } from 'svelte'
|
||||
import { CircleButton, IconAdd, IconClose, Label, ShowMore, showPopup, Tooltip } from '@anticrm/ui'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import tags from '../plugin'
|
||||
import TagsPopup from './TagsPopup.svelte'
|
||||
import TagItem from './TagItem.svelte'
|
||||
import TagsPopup from './TagsPopup.svelte'
|
||||
|
||||
export let items: TagReference[] = []
|
||||
export let targetClass: Ref<Class<Doc>>
|
||||
@@ -55,7 +54,7 @@
|
||||
evt.target as HTMLElement,
|
||||
() => { },
|
||||
(result) => {
|
||||
if (result != undefined) {
|
||||
if (result !== undefined) {
|
||||
if (result.action === 'add') addRef(result.tag)
|
||||
else if (result.action === 'remove') removeTag(items.filter(it => it.tag === result.tag._id)[0]._id)
|
||||
}
|
||||
|
||||
@@ -17,8 +17,8 @@
|
||||
import type { IntlString } from '@anticrm/platform'
|
||||
import { translate } from '@anticrm/platform'
|
||||
import presentation, { createQuery, getClient } from '@anticrm/presentation'
|
||||
import { TagCategory, TagElement, TagReference } from '@anticrm/tags'
|
||||
import { CheckBox, Button, Icon, IconAdd, IconClose, Label, showPopup, getPlatformColor } from '@anticrm/ui'
|
||||
import { TagCategory, TagElement } from '@anticrm/tags'
|
||||
import { Button, CheckBox, getPlatformColor, Icon, IconAdd, IconClose, Label, showPopup } from '@anticrm/ui'
|
||||
import { createEventDispatcher, onMount } from 'svelte'
|
||||
import tags from '../plugin'
|
||||
import CreateTagElement from './CreateTagElement.svelte'
|
||||
@@ -75,7 +75,7 @@
|
||||
}
|
||||
objects = objects
|
||||
categories = categories
|
||||
dispatch('update', { action: 'selected', selected: selected})
|
||||
dispatch('update', { action: 'selected', selected: selected })
|
||||
}
|
||||
const toggleGroup = (ev: MouseEvent): void => {
|
||||
const el: HTMLElement = ev.currentTarget as HTMLElement
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
<script lang="ts">
|
||||
import { Class, Doc, DocumentQuery, FindOptions, Ref } from '@anticrm/core'
|
||||
import { IntlString, translate } from '@anticrm/platform'
|
||||
import { createQuery } from '@anticrm/presentation'
|
||||
import { TagCategory, TagElement } from '@anticrm/tags'
|
||||
import { Button, Icon, Label, Scroller, SearchEdit, showPopup, IconAdd } from '@anticrm/ui'
|
||||
import { TableBrowser } from '@anticrm/view-resources'
|
||||
@@ -26,6 +27,7 @@
|
||||
export let item: IntlString = tags.string.Tag
|
||||
export let сreateItemLabel: IntlString = tags.string.TagCreateLabel
|
||||
export let targetClass: Ref<Class<Doc>>
|
||||
export let onTag: ((tag: TagElement) => void) | undefined = undefined
|
||||
|
||||
let keyTitle: string
|
||||
$: translate(item, {}).then((t) => {
|
||||
@@ -51,6 +53,30 @@
|
||||
}
|
||||
}
|
||||
let category: Ref<TagCategory> | undefined = undefined
|
||||
|
||||
type TagElementInfo = { count: number, modifiedOn: number }
|
||||
let tagElements: Map<Ref<TagElement>, TagElementInfo> | undefined
|
||||
const refQuery = createQuery()
|
||||
$: refQuery.query(
|
||||
tags.class.TagReference, { },
|
||||
(res) => {
|
||||
const result = new Map<Ref<TagElement>, TagElementInfo>()
|
||||
|
||||
for (const d of res) {
|
||||
const v = result.get(d.tag) ?? { count: 0, modifiedOn: 0 }
|
||||
v.count++
|
||||
v.modifiedOn = Math.max(v.modifiedOn, d.modifiedOn)
|
||||
result.set(d.tag, v)
|
||||
}
|
||||
|
||||
tagElements = result
|
||||
}, {
|
||||
projection: {
|
||||
_id: 1, tag: 1, modifiedOn: 1
|
||||
}
|
||||
}
|
||||
)
|
||||
const countSorting = (a:Doc, b:Doc) => ((tagElements?.get(b._id as Ref<TagElement>)?.count ?? 0) - (tagElements?.get(a._id as Ref<TagElement>)?.count ?? 0)) ?? 0
|
||||
</script>
|
||||
|
||||
<div class="ac-header full">
|
||||
@@ -97,6 +123,14 @@
|
||||
}
|
||||
]
|
||||
: []),
|
||||
{
|
||||
key: '',
|
||||
presenter: tags.component.TagElementCountPresenter,
|
||||
label: item,
|
||||
props: { tagElements, label: item, onTag },
|
||||
sortingKey: '@tagCount',
|
||||
sortingFunction: countSorting
|
||||
},
|
||||
'description',
|
||||
'modifiedOn'
|
||||
]}
|
||||
|
||||
@@ -25,6 +25,7 @@ import TagsEditor from './components/TagsEditor.svelte'
|
||||
import TagsItemPresenter from './components/TagsItemPresenter.svelte'
|
||||
import TagsPresenter from './components/TagsPresenter.svelte'
|
||||
import TagsView from './components/TagsView.svelte'
|
||||
import TagElementCountPresenter from './components/TagElementCountPresenter.svelte'
|
||||
|
||||
export default async (): Promise<Resources> => ({
|
||||
component: {
|
||||
@@ -37,7 +38,8 @@ export default async (): Promise<Resources> => ({
|
||||
TagsDropdownEditor,
|
||||
TagsItemPresenter,
|
||||
CategoryPresenter,
|
||||
TagsCategoryBar
|
||||
TagsCategoryBar,
|
||||
TagElementCountPresenter
|
||||
},
|
||||
actionImpl: {
|
||||
Open: (value: TagElement, evt: MouseEvent) => {
|
||||
|
||||
@@ -18,7 +18,8 @@ import { AnyComponent } from '@anticrm/ui'
|
||||
export default mergeIds(tagsId, tags, {
|
||||
component: {
|
||||
TagElementPresenter: '' as AnyComponent,
|
||||
CategoryPresenter: '' as AnyComponent
|
||||
CategoryPresenter: '' as AnyComponent,
|
||||
TagElementCountPresenter: '' as AnyComponent
|
||||
},
|
||||
string: {
|
||||
NoTags: '' as IntlString,
|
||||
@@ -40,6 +41,9 @@ export default mergeIds(tagsId, tags, {
|
||||
TagName: '' as IntlString,
|
||||
SaveLabel: '' as IntlString,
|
||||
CategoryLabel: '' as IntlString,
|
||||
AllCategories: '' as IntlString
|
||||
AllCategories: '' as IntlString,
|
||||
SelectAll: '' as IntlString,
|
||||
SelectNone: '' as IntlString,
|
||||
ApplyTags: '' as IntlString
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user