Skills search update (#1573)

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
Andrey Sobolev
2022-04-28 15:11:09 +07:00
committed by GitHub
parent e9a0225dd7
commit 18bd545c25
18 changed files with 348 additions and 43 deletions
@@ -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
}}
/>
+4 -1
View File
@@ -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"
}
}
+4 -1
View File
@@ -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>
&nbsp;{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'
]}
+3 -1
View File
@@ -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) => {
+6 -2
View File
@@ -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
}
})