Tags filter (#1820)

Signed-off-by: Denis Bykhov <80476319+BykhovDenis@users.noreply.github.com>
This commit is contained in:
Denis Bykhov 2022-05-21 23:17:10 +06:00 committed by GitHub
parent 94b58b9236
commit fcb81f2916
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 458 additions and 110 deletions

View File

@ -452,7 +452,7 @@ export function createModel (builder: Builder): void {
})
builder.mixin(recruit.mixin.Candidate, core.class.Class, view.mixin.ClassFilters, {
filters: ['title', 'source', 'city', 'modifiedOn', 'onsite', 'remote']
filters: ['title', 'source', 'city', 'skills', 'modifiedOn', 'onsite', 'remote']
})
builder.mixin(recruit.class.Applicant, core.class.Class, view.mixin.ClassFilters, {

View File

@ -20,6 +20,10 @@ import serverTags from '@anticrm/server-tags'
import tags from '@anticrm/tags'
export function createModel (builder: Builder): void {
builder.createDoc(serverCore.class.Trigger, core.space.Model, {
trigger: serverTags.trigger.onTagReference
})
builder.mixin<Class<Doc>, ObjectDDParticipant>(
tags.class.TagElement,
core.class.Class,

View File

@ -13,7 +13,7 @@
// limitations under the License.
import { Class, Doc, Domain, IndexKind, Ref } from '@anticrm/core'
import { ArrOf, Builder, Index, Model, Prop, TypeRef, TypeString, UX } from '@anticrm/model'
import { ArrOf, Builder, Index, Model, Prop, TypeNumber, TypeRef, TypeString, UX } from '@anticrm/model'
import core, { TAttachedDoc, TDoc } from '@anticrm/model-core'
import view from '@anticrm/model-view'
import { Asset, IntlString } from '@anticrm/platform'
@ -45,6 +45,9 @@ export class TTagElement extends TDoc implements TagElement {
@Prop(TypeRef(tags.class.TagCategory), tags.string.CategoryLabel)
category!: Ref<TagCategory>
@Prop(TypeNumber(), tags.string.TagReference)
refCount?: number
}
@Model(tags.class.TagReference, core.class.AttachedDoc, DOMAIN_TAGS)
@ -94,4 +97,8 @@ export function createModel (builder: Builder): void {
builder.mixin(tags.class.TagElement, core.class.Class, view.mixin.AttributePresenter, {
presenter: tags.component.TagElementPresenter
})
builder.mixin(tags.class.TagReference, core.class.Class, view.mixin.AttributeFilter, {
component: tags.component.TagsFilter
})
}

View File

@ -1,8 +1,36 @@
import core, { TxOperations } from '@anticrm/core'
import { MigrateOperation, MigrationClient, MigrationUpgradeClient } from '@anticrm/model'
import core, { Ref, TxOperations } from '@anticrm/core'
import { MigrateOperation, MigrationClient, MigrationResult, MigrationUpgradeClient } from '@anticrm/model'
import { TagElement, TagReference } from '@anticrm/tags'
import { DOMAIN_TAGS } from './index'
import tags from './plugin'
async function updateTagRefCount (client: MigrationClient): Promise<void> {
const tagElements = await client.find(DOMAIN_TAGS, { _class: tags.class.TagElement, refCount: { $exists: false } })
const refs = await client.find<TagReference>(DOMAIN_TAGS, {
_class: tags.class.TagReference,
tag: { $in: tagElements.map((p) => p._id as Ref<TagElement>) }
})
const map = new Map<Ref<TagElement>, number>()
for (const ref of refs) {
map.set(ref.tag, (map.get(ref.tag) ?? 0) + 1)
}
const promises: Promise<MigrationResult>[] = []
for (const tag of map) {
promises.push(
client.update(
DOMAIN_TAGS,
{
_id: tag[0]
},
{
refCount: tag[1]
}
)
)
}
await Promise.all(promises)
}
export const tagsOperation: MigrateOperation = {
async migrate (client: MigrationClient): Promise<void> {
await client.update(
@ -15,6 +43,8 @@ export const tagsOperation: MigrateOperation = {
category: 'recruit:category:Other'
}
)
await updateTagRefCount(client)
},
async upgrade (client: MigrationUpgradeClient): Promise<void> {
const tx = new TxOperations(client, core.account.System)

View File

@ -25,7 +25,8 @@ export default mergeIds(tagsId, tags, {
Tags: '' as AnyComponent,
TagReferencePresenter: '' as AnyComponent,
TagsPresenter: '' as AnyComponent,
TagsItemPresenter: '' as AnyComponent
TagsItemPresenter: '' as AnyComponent,
TagsFilter: '' as AnyComponent
},
string: {
TagElementLabel: '' as IntlString,

View File

@ -78,11 +78,13 @@ export class LiveQuery {
private oldCallback: ((result: FindResult<any>) => void) | undefined
unsubscribe = () => {}
constructor () {
onDestroy(() => {
console.log('onDestroy query')
this.unsubscribe()
})
constructor (dontDestroy: boolean = false) {
if (!dontDestroy) {
onDestroy(() => {
console.log('onDestroy query')
this.unsubscribe()
})
}
}
query<T extends Doc>(
@ -121,8 +123,8 @@ export class LiveQuery {
}
}
export function createQuery (): LiveQuery {
return new LiveQuery()
export function createQuery (dontDestroy?: boolean): LiveQuery {
return new LiveQuery(dontDestroy)
}
export function getFileUrl (file: string): string {

View File

@ -15,10 +15,9 @@
-->
<script lang="ts">
import contact from '@anticrm/contact'
import { Doc, DocumentQuery, Ref } from '@anticrm/core'
import { Doc, DocumentQuery } from '@anticrm/core'
import { createQuery, getClient } from '@anticrm/presentation'
import tags, { selectedTagElements, TagCategory, TagElement } from '@anticrm/tags'
import { ActionIcon, showPopup, Component, Icon, Label, Loading, SearchEdit, Button, IconAdd } from '@anticrm/ui'
import { ActionIcon, showPopup, Icon, Label, Loading, SearchEdit, Button, IconAdd } from '@anticrm/ui'
import view, { Viewlet, ViewletPreference } from '@anticrm/view'
import { ActionContext, TableBrowser, ViewletSetting } from '@anticrm/view-resources'
import recruit from '../plugin'
@ -57,29 +56,11 @@
}
})
let category: Ref<TagCategory> | undefined = undefined
let documentIds: Ref<Doc>[] = []
async function updateResultQuery (search: string, documentIds: Ref<Doc>[]): Promise<void> {
async function updateResultQuery (search: string): 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[] }) {
category = detail.category ?? undefined
selectedTagElements.set(Array.from(detail.elements ?? []).map((it) => it._id))
}
$: updateResultQuery(search)
function showCreateDialog () {
showPopup(CreateCandidate, {}, 'top')
@ -95,7 +76,7 @@
<SearchEdit
bind:value={search}
on:change={() => {
updateResultQuery(search, documentIds)
updateResultQuery(search)
}}
/>
<Button icon={IconAdd} label={recruit.string.CandidateCreateLabel} kind={'primary'} on:click={showCreateDialog} />
@ -111,12 +92,6 @@
{/if}
</div>
<Component
is={tags.component.TagsCategoryBar}
props={{ targetClass: recruit.mixin.Candidate, category, selected: $selectedTagElements, mode: 'item' }}
on:change={(evt) => updateCategory(evt.detail)}
/>
<ActionContext
context={{
mode: 'browser'

View File

@ -37,6 +37,7 @@
"@anticrm/ui": "~0.6.0",
"@anticrm/presentation": "~0.6.2",
"@anticrm/core": "~0.6.16",
"@anticrm/view": "~0.6.0",
"@anticrm/view-resources": "~0.6.0"
}
}

View File

@ -0,0 +1,229 @@
<!--
// 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 context="module" lang="ts">
const liveQueries: Map<number, LiveQuery> = new Map<number, LiveQuery>()
const results: Map<number, Ref<Doc>[]> = new Map<number, Ref<Doc>[]>()
</script>
<script lang="ts">
import { Class, Doc, Ref } from '@anticrm/core'
import { translate } from '@anticrm/platform'
import presentation, { createQuery, getClient, LiveQuery } from '@anticrm/presentation'
import { Button, CheckBox, getPlatformColor } from '@anticrm/ui'
import { Filter } from '@anticrm/view'
import view from '@anticrm/view-resources/src/plugin'
import { createEventDispatcher, onMount } from 'svelte'
import tags from '../plugin'
import { TagCategory, TagElement } from '@anticrm/tags'
export let _class: Ref<Class<Doc>>
export let filter: Filter
export let onChange: (e: Filter) => void
filter.onRemove = () => {
const lq = liveQueries.get(filter.index)
lq?.unsubscribe()
liveQueries.delete(filter.index)
results.delete(filter.index)
}
const lq = getLiveQuery(filter.index)
const client = getClient()
let selected: Ref<TagElement>[] = filter.value
function getLiveQuery (index: number): LiveQuery {
let lq = liveQueries.get(index)
if (lq === undefined) {
lq = createQuery(true)
liveQueries.set(index, lq)
}
return lq
}
async function getRefs (res: Ref<TagElement>[], onUpdate: () => void): Promise<Ref<Doc>[]> {
const promise = new Promise<Ref<Doc>[]>((resolve, reject) => {
const refresh = lq.query(
tags.class.TagReference,
{
tag: { $in: res }
},
(refs) => {
const result = Array.from(new Set(refs.map((p) => p.attachedTo)))
results.set(filter.index, result)
resolve(result)
onUpdate()
}
)
if (!refresh) {
resolve(results.get(filter.index) ?? [])
}
})
return promise
}
filter.modes = [
{
label: view.string.FilterIs,
isAvailable: (res: any[]) => res.length <= 1,
result: async (res: any[], onUpdate: () => void) => {
const result = await getRefs(res, onUpdate)
return { $in: result }
}
},
{
label: view.string.FilterIsEither,
isAvailable: (res: any[]) => res.length > 1,
result: async (res: any[], onUpdate: () => void) => {
const result = await getRefs(res, onUpdate)
return { $in: result }
}
},
{
label: view.string.FilterIsNot,
isAvailable: () => true,
result: async (res: any[], onUpdate: () => void) => {
const result = await getRefs(res, onUpdate)
return { $nin: result }
}
}
]
let categories: TagCategory[] = []
let objects: TagElement[] = []
client.findAll(tags.class.TagCategory, { targetClass: _class }).then((res) => {
categories = res
})
$: getValues(search)
async function getValues (search: string): Promise<void> {
const resultQuery =
search !== ''
? {
title: { $like: '%' + search + '%' },
targetClass: _class
}
: { targetClass: _class }
objects = await client.findAll(tags.class.TagElement, resultQuery)
}
function checkMode () {
if (filter.mode?.isAvailable(filter.value)) return
const newMode = filter.modes.find((p: any) => p.isAvailable(filter.value))
filter.mode = newMode !== undefined ? newMode : filter.mode
}
let search: string = ''
let phTraslate: string = ''
let searchInput: HTMLInputElement
$: translate(presentation.string.Search, {}).then((res) => {
phTraslate = res
})
onMount(() => {
if (searchInput) searchInput.focus()
})
const toggleGroup = (ev: MouseEvent): void => {
const el: HTMLElement = ev.currentTarget as HTMLElement
el.classList.toggle('show')
}
const show: boolean = false
const getCount = (cat: TagCategory): string => {
const count = objects.filter((el) => el.category === cat._id).filter((it) => selected.includes(it._id)).length
if (count > 0) return count.toString()
return ''
}
const isSelected = (element: TagElement): boolean => {
if (selected.filter((p) => p === element._id).length > 0) return true
return false
}
const checkSelected = (element: TagElement): void => {
if (isSelected(element)) {
selected = selected.filter((p) => p !== element._id)
} else {
selected = [...selected, element._id]
}
objects = objects
categories = categories
}
const dispatch = createEventDispatcher()
</script>
<div class="selectPopup">
<div class="header">
<input bind:this={searchInput} type="text" bind:value={search} placeholder={phTraslate} />
</div>
<div class="scroll">
<div class="box">
{#each categories as cat}
{#if objects.filter((el) => el.category === cat._id).length > 0}
<div class="sticky-wrapper">
<button class="menu-group__header" class:show={search !== '' || show} on:click={toggleGroup}>
<div class="flex-row-center">
<span class="mr-1-5">{cat.label}</span>
<div class="icon">
<svg fill="var(--content-color)" viewBox="0 0 6 6" xmlns="http://www.w3.org/2000/svg">
<path d="M0,0L6,3L0,6Z" />
</svg>
</div>
</div>
<div class="flex-row-center text-xs">
<span class="content-color mr-1">({objects.filter((el) => el.category === cat._id).length})</span>
<span class="counter">{getCount(cat)}</span>
</div>
</button>
<div class="menu-group">
{#each objects.filter((el) => el.category === cat._id) as element}
<button
class="menu-item"
on:click={() => {
checkSelected(element)
}}
>
<div class="flex-between w-full">
<div class="flex">
<div class="check pointer-events-none">
<CheckBox checked={isSelected(element)} primary />
</div>
<div class="tag" style="background-color: {getPlatformColor(element.color)};" />
{element.title}
</div>
<div class="content-trans-color ml-2">
{element.refCount ?? 0}
</div>
</div>
</button>
{/each}
</div>
</div>
{/if}
{/each}
</div>
</div>
<Button
shape={'round'}
label={view.string.Apply}
on:click={async () => {
filter.value = selected
checkMode()
onChange(filter)
dispatch('close')
}}
/>
</div>

View File

@ -26,6 +26,7 @@ import TagsItemPresenter from './components/TagsItemPresenter.svelte'
import TagsPresenter from './components/TagsPresenter.svelte'
import TagsView from './components/TagsView.svelte'
import TagElementCountPresenter from './components/TagElementCountPresenter.svelte'
import TagsFilter from './components/TagsFilter.svelte'
export default async (): Promise<Resources> => ({
component: {
@ -34,6 +35,7 @@ export default async (): Promise<Resources> => ({
TagElementPresenter,
TagsPresenter,
TagsView,
TagsFilter,
TagsEditor,
TagsDropdownEditor,
TagsItemPresenter,

View File

@ -28,6 +28,7 @@ export interface TagElement extends Doc {
description: string
color: number
category: Ref<TagCategory>
refCount?: number
}
/**

View File

@ -45,6 +45,7 @@
"FilterIsEither": "is either of",
"FilterStatesCount": "{value, plural, =1 {1 state} other {# states}}",
"Before": "Before",
"After": "After"
"After": "After",
"Apply": "Apply"
}
}

View File

@ -43,6 +43,7 @@
"FilterIsEither": "один из",
"FilterStatesCount": "{value, plural, =1 {1 состоянию} other {# состояний}}",
"Before": "До",
"After": "После"
"After": "После",
"Apply": "Применить"
}
}

View File

@ -30,29 +30,29 @@
const dispatch = createEventDispatcher()
let filters: Filter[] = []
let isNew = true
let maxIndex = 0
function onChange (e: Filter | undefined) {
if (e === undefined) return
if (isNew) {
const index = filters.findIndex((p) => p.index === e.index)
if (index === -1) {
filters.push(e)
isNew = false
filters = filters
} else {
filters[filters.length - 1] = e
filters[index] = e
filters = filters
}
}
function add (e: MouseEvent) {
const target = eventToHTMLElement(e)
isNew = true
showPopup(
FilterTypePopup,
{
_class,
makeQuery: (key: string) => makeQuery(query, filters, key),
query,
target,
index: ++maxIndex,
onChange
},
target
@ -60,19 +60,21 @@
}
function remove (i: number) {
filters[i]?.onRemove?.()
filters.splice(i, 1)
filters = filters
}
function makeQuery (query: DocumentQuery<Doc>, filters: Filter[], skipKey?: string): DocumentQuery<Doc> {
async function makeQuery (query: DocumentQuery<Doc>, filters: Filter[]): Promise<void> {
const newQuery = hierarchy.clone(query)
for (let i = 0; i < filters.length; i++) {
const filter = filters[i]
if (skipKey !== undefined && filter.key.key === skipKey) continue
const newValue = await filter.mode.result(filter.value, () => {
makeQuery(query, filters)
})
if (newQuery[filter.key.key] === undefined) {
newQuery[filter.key.key] = filter.mode.result(filter.value)
newQuery[filter.key.key] = newValue
} else {
const newValue = filter.mode.result(filter.value)
let merged = false
for (const key in newValue) {
if (newQuery[filter.key.key][key] === undefined) {
@ -104,14 +106,11 @@
}
}
if (!merged) {
Object.assign(newQuery[filter.key.key], filter.mode.result(filter.value))
Object.assign(newQuery[filter.key.key], newValue)
}
}
}
if (skipKey === undefined) {
dispatch('change', newQuery)
}
return newQuery
dispatch('change', newQuery)
}
$: makeQuery(query, filters)
@ -125,7 +124,7 @@
{#each filters as filter, i}
<FilterSection
{_class}
query={makeQuery(query, filters, filter.key.key)}
{query}
{filter}
on:change={() => {
makeQuery(query, filters)

View File

@ -13,7 +13,17 @@
// limitations under the License.
-->
<script lang="ts">
import core, { AnyAttribute, ArrOf, Class, Doc, DocumentQuery, Ref, Type } from '@anticrm/core'
import core, {
AnyAttribute,
ArrOf,
AttachedDoc,
Class,
Collection,
Doc,
DocumentQuery,
Ref,
Type
} from '@anticrm/core'
import { getClient } from '@anticrm/presentation'
import { Icon, Label, showPopup } from '@anticrm/ui'
import { Filter, KeyFilter } from '@anticrm/view'
@ -21,8 +31,9 @@
import view from '../../plugin'
export let _class: Ref<Class<Doc>>
export let makeQuery: (key: string) => DocumentQuery<Doc>
export let query: DocumentQuery<Doc>
export let target: HTMLElement
export let index: number
export let onChange: (e: Filter) => void
const client = getClient()
@ -48,11 +59,13 @@
}
function buildFilter (key: string, attribute: AnyAttribute): KeyFilter | undefined {
const clazz = hierarchy.getClass(attribute.type._class)
const isCollection = hierarchy.isDerived(attribute.type._class, core.class.Collection)
const targetClass = isCollection ? (attribute.type as Collection<AttachedDoc>).of : attribute.type._class
const clazz = hierarchy.getClass(targetClass)
const filter = hierarchy.as(clazz, view.mixin.AttributeFilter)
if (filter.component === undefined) return undefined
return {
key: key,
key: isCollection ? '_id' : key,
label: attribute.label,
icon: attribute.icon,
component: filter.component
@ -108,10 +121,11 @@
type.component,
{
_class,
query: makeQuery(type.key),
query,
filter: {
key: type,
value: []
value: [],
index
},
onChange
},

View File

@ -16,11 +16,12 @@
import { Class, Doc, DocumentQuery, getObjectValue, Ref, RefTo } from '@anticrm/core'
import { translate } from '@anticrm/platform'
import presentation, { getClient } from '@anticrm/presentation'
import ui, { CheckBox, Label } from '@anticrm/ui'
import ui, { Button, CheckBox, Label } from '@anticrm/ui'
import { Filter } from '@anticrm/view'
import { onMount } from 'svelte'
import { buildConfigLookup, getPresenter } from '../../utils'
import view from '../../plugin'
import { createEventDispatcher } from 'svelte'
export let _class: Ref<Class<Doc>>
export let query: DocumentQuery<Doc>
@ -37,21 +38,21 @@
{
label: view.string.FilterIs,
isAvailable: (res: any[]) => res.length <= 1,
result: (res: any[]) => {
result: async (res: any[]) => {
return { $in: res }
}
},
{
label: view.string.FilterIsEither,
isAvailable: (res: any[]) => res.length > 1,
result: (res: any[]) => {
result: async (res: any[]) => {
return { $in: res }
}
},
{
label: view.string.FilterIsNot,
isAvailable: () => true,
result: (res: any[]) => {
result: async (res: any[]) => {
return { $nin: res }
}
}
@ -107,7 +108,6 @@
}
}
checkMode()
onChange(filter)
}
let search: string = ''
@ -120,18 +120,13 @@
onMount(() => {
if (searchInput) searchInput.focus()
})
const dispatch = createEventDispatcher()
</script>
<div class="selectPopup">
<div class="header">
<input
bind:this={searchInput}
type="text"
bind:value={search}
placeholder={phTraslate}
on:input={(ev) => {}}
on:change
/>
<input bind:this={searchInput} type="text" bind:value={search} placeholder={phTraslate} />
</div>
<div class="scroll">
<div class="box">
@ -154,7 +149,7 @@
<Label label={ui.string.NotSelected} />
{/if}
</div>
<div class="content-trans-color">
<div class="content-trans-color ml-2">
{targets.get(value?._id)}
</div>
</div>
@ -163,4 +158,12 @@
{/await}
</div>
</div>
<Button
shape={'round'}
label={view.string.Apply}
on:click={() => {
onChange(filter)
dispatch('close')
}}
/>
</div>

View File

@ -27,14 +27,14 @@
{
label: view.string.Before,
isAvailable: (res: any[]) => true,
result: (res: any[]) => {
result: async (res: any[]) => {
return { $lt: res[0] }
}
},
{
label: view.string.After,
isAvailable: () => true,
result: (res: any[]) => {
result: async (res: any[]) => {
return { $gt: res[0] }
}
}

View File

@ -16,11 +16,12 @@
import { Class, Doc, DocumentQuery, getObjectValue, Ref } from '@anticrm/core'
import { translate } from '@anticrm/platform'
import presentation, { getClient } from '@anticrm/presentation'
import ui, { CheckBox, Label } from '@anticrm/ui'
import ui, { Button, CheckBox, Label } from '@anticrm/ui'
import { Filter } from '@anticrm/view'
import { onMount } from 'svelte'
import { getPresenter } from '../../utils'
import view from '../../plugin'
import { createEventDispatcher } from 'svelte'
export let _class: Ref<Class<Doc>>
export let query: DocumentQuery<Doc>
@ -31,22 +32,22 @@
{
label: view.string.FilterIs,
isAvailable: (res: any[]) => res.length <= 1,
result: (res: any[]) => {
return { $in: res }
result: async (res: [any, any[]][]) => {
return { $in: res.map((p) => p[1]).flat() }
}
},
{
label: view.string.FilterIsEither,
isAvailable: (res: any[]) => res.length > 1,
result: (res: any[]) => {
return { $in: res }
result: async (res: [any, any[]][]) => {
return { $in: res.map((p) => p[1]).flat() }
}
},
{
label: view.string.FilterIsNot,
isAvailable: () => true,
result: (res: any[]) => {
return { $nin: res }
result: async (res: [any, any[]][]) => {
return { $nin: res.map((p) => p[1]).flat() }
}
}
]
@ -56,7 +57,7 @@
const promise = getPresenter(client, _class, key, key)
let values = new Map<any, number>()
let selectedValues: Set<any> = new Set<any>()
let selectedValues: Set<any> = new Set<any>(filter.value.map((p) => p[0]))
const realValues = new Map<any, Set<any>>()
$: getValues(search)
@ -98,11 +99,6 @@
selectedValues.add(value)
}
selectedValues = selectedValues
filter.value = Array.from(selectedValues.values())
.map((p) => Array.from(realValues.get(p) ?? []))
.flat()
checkMode()
onChange(filter)
}
let search: string = ''
@ -112,6 +108,8 @@
phTraslate = res
})
const dispatch = createEventDispatcher()
onMount(() => {
if (searchInput) searchInput.focus()
})
@ -119,14 +117,7 @@
<div class="selectPopup">
<div class="header">
<input
bind:this={searchInput}
type="text"
bind:value={search}
placeholder={phTraslate}
on:input={(ev) => {}}
on:change
/>
<input bind:this={searchInput} type="text" bind:value={search} placeholder={phTraslate} />
</div>
<div class="scroll">
<div class="box">
@ -149,7 +140,7 @@
<Label label={ui.string.NotSelected} />
{/if}
</div>
<div class="content-trans-color">
<div class="content-trans-color ml-2">
{values.get(value)}
</div>
</div>
@ -158,4 +149,16 @@
{/await}
</div>
</div>
<Button
shape={'round'}
label={view.string.Apply}
on:click={() => {
filter.value = Array.from(selectedValues.values()).map((p) => {
return [p, Array.from(realValues.get(p) ?? [])]
})
checkMode()
onChange(filter)
dispatch('close')
}}
/>
</div>

View File

@ -48,6 +48,7 @@ export default mergeIds(viewId, view, {
FilterIsEither: '' as IntlString,
FilterStatesCount: '' as IntlString,
Before: '' as IntlString,
After: '' as IntlString
After: '' as IntlString,
Apply: '' as IntlString
}
})

View File

@ -52,7 +52,7 @@ export interface KeyFilter {
export interface FilterMode {
label: IntlString
isAvailable: (values: any[]) => boolean
result: (values: any[]) => ObjQueryType<any>
result: (values: any[], onUpdate: () => void) => Promise<ObjQueryType<any>>
}
/**
@ -63,6 +63,8 @@ export interface Filter {
mode: FilterMode
modes: FilterMode[]
value: any[]
index: number
onRemove?: () => void
}
/**

View File

@ -13,27 +13,95 @@
// limitations under the License.
//
import { Class, Doc, DocumentQuery, FindOptions, FindResult, Hierarchy, Ref } from '@anticrm/core'
import tags, { TagElement } from '@anticrm/tags'
import core, {
AttachedDoc,
Class,
Doc,
DocumentQuery,
FindOptions,
FindResult,
Hierarchy,
Ref,
Tx,
TxCollectionCUD,
TxCreateDoc,
TxCUD,
TxProcessor,
TxRemoveDoc
} from '@anticrm/core'
import { TriggerControl } from '@anticrm/server-core'
import tags, { TagElement, TagReference } from '@anticrm/tags'
const extractTx = (tx: Tx): Tx => {
if (tx._class === core.class.TxCollectionCUD) {
const ctx = tx as TxCollectionCUD<Doc, AttachedDoc>
if (ctx.tx._class === core.class.TxCreateDoc) {
const create = ctx.tx as TxCreateDoc<AttachedDoc>
create.attributes.attachedTo = ctx.objectId
create.attributes.attachedToClass = ctx.objectClass
create.attributes.collection = ctx.collection
return create
}
return ctx.tx
}
return tx
}
/**
* @public
*/
export async function TagElementRemove (
doc: Doc,
hiearachy: Hierarchy,
hierarchy: Hierarchy,
findAll: <T extends Doc>(
clazz: Ref<Class<T>>,
query: DocumentQuery<T>,
options?: FindOptions<T>
) => Promise<FindResult<T>>
): Promise<Doc[]> {
if (!hiearachy.isDerived(doc._class, tags.class.TagElement)) return []
if (!hierarchy.isDerived(doc._class, tags.class.TagElement)) return []
return await findAll(tags.class.TagReference, { tag: doc._id as Ref<TagElement> })
}
/**
* @public
*/
export async function onTagReference (tx: Tx, control: TriggerControl): Promise<Tx[]> {
const actualTx = extractTx(tx)
const isCreate = control.hierarchy.isDerived(actualTx._class, core.class.TxCreateDoc)
const isRemove = control.hierarchy.isDerived(actualTx._class, core.class.TxRemoveDoc)
if (!isCreate && !isRemove) return []
if (!control.hierarchy.isDerived((actualTx as TxCUD<Doc>).objectClass, tags.class.TagReference)) return []
if (isCreate) {
const doc = TxProcessor.createDoc2Doc(actualTx as TxCreateDoc<TagReference>)
const res = control.txFactory.createTxUpdateDoc(tags.class.TagElement, tags.space.Tags, doc.tag, {
$inc: { refCount: 1 }
})
return [res]
}
if (isRemove) {
const ctx = actualTx as TxRemoveDoc<TagReference>
const createTx = (
await control.findAll(core.class.TxCollectionCUD, { 'tx.objectId': ctx.objectId }, { limit: 1 })
)[0]
if (createTx !== undefined) {
const actualCreateTx = extractTx(createTx)
const doc = TxProcessor.createDoc2Doc(actualCreateTx as TxCreateDoc<TagReference>)
const res = control.txFactory.createTxUpdateDoc(tags.class.TagElement, tags.space.Tags, doc.tag, {
$inc: { refCount: -1 }
})
return [res]
}
}
return []
}
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
export default async () => ({
trigger: {
onTagReference
},
function: {
TagElementRemove
}

View File

@ -16,6 +16,7 @@
import { Class, Doc, Hierarchy, Ref, FindResult, FindOptions, DocumentQuery } from '@anticrm/core'
import type { Plugin, Resource } from '@anticrm/platform'
import { plugin } from '@anticrm/platform'
import { TriggerFunc } from '@anticrm/server-core'
/**
* @public
@ -26,6 +27,9 @@ export const serverTagsId = 'server-tags' as Plugin
* @public
*/
export default plugin(serverTagsId, {
trigger: {
onTagReference: '' as Resource<TriggerFunc>
},
function: {
TagElementRemove: '' as Resource<
(