mirror of
https://github.com/hcengineering/platform.git
synced 2025-04-13 11:50:56 +00:00
Tags filter (#1820)
Signed-off-by: Denis Bykhov <80476319+BykhovDenis@users.noreply.github.com>
This commit is contained in:
parent
94b58b9236
commit
fcb81f2916
@ -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, {
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
})
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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,
|
||||
|
@ -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 {
|
||||
|
@ -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'
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
229
plugins/tags-resources/src/components/TagsFilter.svelte
Normal file
229
plugins/tags-resources/src/components/TagsFilter.svelte
Normal 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>
|
@ -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,
|
||||
|
@ -28,6 +28,7 @@ export interface TagElement extends Doc {
|
||||
description: string
|
||||
color: number
|
||||
category: Ref<TagCategory>
|
||||
refCount?: number
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -45,6 +45,7 @@
|
||||
"FilterIsEither": "is either of",
|
||||
"FilterStatesCount": "{value, plural, =1 {1 state} other {# states}}",
|
||||
"Before": "Before",
|
||||
"After": "After"
|
||||
"After": "After",
|
||||
"Apply": "Apply"
|
||||
}
|
||||
}
|
||||
|
@ -43,6 +43,7 @@
|
||||
"FilterIsEither": "один из",
|
||||
"FilterStatesCount": "{value, plural, =1 {1 состоянию} other {# состояний}}",
|
||||
"Before": "До",
|
||||
"After": "После"
|
||||
"After": "После",
|
||||
"Apply": "Применить"
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
},
|
||||
|
@ -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>
|
||||
|
@ -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] }
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
}
|
||||
})
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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<
|
||||
(
|
||||
|
Loading…
Reference in New Issue
Block a user