mirror of
https://github.com/hcengineering/platform.git
synced 2025-04-22 16:27:22 +00:00
UBER-299 Array enum filter (#3311)
Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com>
This commit is contained in:
parent
79121cb8b0
commit
3fc89fbb52
@ -719,6 +719,10 @@ export function createModel (builder: Builder): void {
|
||||
component: view.component.ValueFilter
|
||||
})
|
||||
|
||||
builder.mixin(core.class.ArrOf, core.class.Class, view.mixin.AttributeFilter, {
|
||||
component: view.component.ArrayFilter
|
||||
})
|
||||
|
||||
builder.mixin(core.class.RefTo, core.class.Class, view.mixin.AttributeFilter, {
|
||||
component: view.component.ObjectFilter
|
||||
})
|
||||
@ -728,6 +732,26 @@ export function createModel (builder: Builder): void {
|
||||
group: 'bottom'
|
||||
})
|
||||
|
||||
builder.createDoc(
|
||||
view.class.FilterMode,
|
||||
core.space.Model,
|
||||
{
|
||||
label: view.string.FilterArrayAll,
|
||||
result: view.function.FilterArrayAllResult
|
||||
},
|
||||
view.filter.FilterArrayAll
|
||||
)
|
||||
|
||||
builder.createDoc(
|
||||
view.class.FilterMode,
|
||||
core.space.Model,
|
||||
{
|
||||
label: view.string.FilterArrayAny,
|
||||
result: view.function.FilterArrayAnyResult
|
||||
},
|
||||
view.filter.FilterArrayAny
|
||||
)
|
||||
|
||||
builder.createDoc(
|
||||
view.class.FilterMode,
|
||||
core.space.Model,
|
||||
|
@ -99,6 +99,8 @@ export default mergeIds(viewId, view, {
|
||||
MarkdownFormatting: '' as IntlString
|
||||
},
|
||||
function: {
|
||||
FilterArrayAllResult: '' as FilterFunction,
|
||||
FilterArrayAnyResult: '' as FilterFunction,
|
||||
FilterObjectInResult: '' as FilterFunction,
|
||||
FilterObjectNinResult: '' as FilterFunction,
|
||||
FilterValueInResult: '' as FilterFunction,
|
||||
|
@ -41,8 +41,27 @@ const predicates: Record<string, PredicateFactory> = {
|
||||
if (!Array.isArray(o)) {
|
||||
throw new Error('$in predicate requires array')
|
||||
}
|
||||
// eslint-disable-next-line eqeqeq
|
||||
return (docs) => execPredicate(docs, propertyKey, (value) => o.some((p) => p == value))
|
||||
return (docs) =>
|
||||
execPredicate(docs, propertyKey, (value) => {
|
||||
if (Array.isArray(value)) {
|
||||
return o.some((p) => value.includes(p))
|
||||
} else {
|
||||
// eslint-disable-next-line eqeqeq
|
||||
return o.some((p) => p == value)
|
||||
}
|
||||
})
|
||||
},
|
||||
$all: (o, propertyKey) => {
|
||||
if (!Array.isArray(o)) {
|
||||
throw new Error('$all predicate requires array')
|
||||
}
|
||||
return (docs) =>
|
||||
execPredicate(docs, propertyKey, (value: any[]) => {
|
||||
for (const val of o) {
|
||||
if (!value.includes(val)) return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
},
|
||||
$nin: (o, propertyKey) => {
|
||||
if (!Array.isArray(o)) {
|
||||
|
@ -23,6 +23,7 @@ import type { Tx } from './tx'
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
|
||||
export type QuerySelector<T> = {
|
||||
$in?: T[]
|
||||
$all?: T extends Array<any> ? T : never
|
||||
$nin?: T[]
|
||||
$ne?: T
|
||||
$gt?: T extends number ? number : never
|
||||
|
@ -98,6 +98,8 @@
|
||||
"And": "and",
|
||||
"Between": "is between",
|
||||
"ShowColors": "Use colors",
|
||||
"Show": "Show"
|
||||
"Show": "Show",
|
||||
"FilterArrayAll": "includes all",
|
||||
"FilterArrayAny": "includes any"
|
||||
}
|
||||
}
|
||||
|
@ -94,6 +94,8 @@
|
||||
"And": "и",
|
||||
"Between": "между",
|
||||
"ShowColors": "Использовать цвета",
|
||||
"Show": "Отображение"
|
||||
"Show": "Отображение",
|
||||
"FilterArrayAll": "содержит все",
|
||||
"FilterArrayAny": "содержит любое из"
|
||||
}
|
||||
}
|
||||
|
225
plugins/view-resources/src/components/filter/ArrayFilter.svelte
Normal file
225
plugins/view-resources/src/components/filter/ArrayFilter.svelte
Normal file
@ -0,0 +1,225 @@
|
||||
<!--
|
||||
// Copyright © 2023 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 core, {
|
||||
ArrOf,
|
||||
Class,
|
||||
Doc,
|
||||
EnumOf,
|
||||
FindResult,
|
||||
getObjectValue,
|
||||
Ref,
|
||||
SortingOrder,
|
||||
Space
|
||||
} from '@hcengineering/core'
|
||||
import presentation, { getClient } from '@hcengineering/presentation'
|
||||
import ui, {
|
||||
deviceOptionsStore,
|
||||
EditWithIcon,
|
||||
Icon,
|
||||
IconCheck,
|
||||
IconSearch,
|
||||
Label,
|
||||
Loading,
|
||||
resizeObserver
|
||||
} from '@hcengineering/ui'
|
||||
import { Filter } from '@hcengineering/view'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { FILTER_DEBOUNCE_MS } from '../../filter'
|
||||
import view from '../../plugin'
|
||||
import { getPresenter } from '../../utils'
|
||||
|
||||
export let _class: Ref<Class<Doc>>
|
||||
export let space: Ref<Space> | undefined = undefined
|
||||
export let filter: Filter
|
||||
export let onChange: (e: Filter) => void
|
||||
|
||||
filter.modes = [view.filter.FilterArrayAll, view.filter.FilterArrayAny]
|
||||
filter.mode = filter.mode === undefined ? filter.modes[0] : filter.mode
|
||||
|
||||
const client = getClient()
|
||||
const key = { key: filter.key.key }
|
||||
const promise = getPresenter(client, filter.key._class, key, key)
|
||||
|
||||
let values = new Set<any>()
|
||||
let selectedValues: Set<any> = new Set<any>(filter.value.map((p) => p[0]))
|
||||
const realValues = new Map<any, Set<any>>()
|
||||
|
||||
let objectsPromise: Promise<FindResult<Doc>> | undefined
|
||||
|
||||
let filterUpdateTimeout: number | undefined
|
||||
|
||||
async function getEnumValues (search: string): Promise<void> {
|
||||
const enumId = ((filter.key.attribute.type as ArrOf<Doc>).of as EnumOf).of
|
||||
const enumVal = await client.findOne(core.class.Enum, { _id: enumId })
|
||||
if (enumVal !== undefined) {
|
||||
for (const realValue of enumVal.enumValues) {
|
||||
const value = getValue(realValue) as string
|
||||
if (search !== '' && !value.includes(search.toUpperCase())) continue
|
||||
values.add(value)
|
||||
realValues.set(value, (realValues.get(value) ?? new Set()).add(realValue))
|
||||
}
|
||||
values = values
|
||||
}
|
||||
}
|
||||
|
||||
async function getValues (search: string): Promise<void> {
|
||||
if (objectsPromise) {
|
||||
await objectsPromise
|
||||
}
|
||||
objectsPromise = undefined
|
||||
values.clear()
|
||||
realValues.clear()
|
||||
if ((filter.key.attribute.type as ArrOf<Doc>).of._class === core.class.EnumOf) {
|
||||
return getEnumValues(search)
|
||||
}
|
||||
const resultQuery =
|
||||
search !== ''
|
||||
? {
|
||||
[filter.key.key]: { $like: '%' + search + '%' }
|
||||
}
|
||||
: {}
|
||||
let prefix = ''
|
||||
const hieararchy = client.getHierarchy()
|
||||
const attr = hieararchy.getAttribute(filter.key._class, filter.key.key)
|
||||
if (hieararchy.isMixin(attr.attributeOf)) {
|
||||
prefix = attr.attributeOf + '.'
|
||||
}
|
||||
objectsPromise = client.findAll(
|
||||
_class,
|
||||
{ ...resultQuery, ...(space ? { space } : {}) },
|
||||
{
|
||||
sort: { [filter.key.key]: SortingOrder.Ascending },
|
||||
projection: { [prefix + filter.key.key]: 1, space: 1 }
|
||||
}
|
||||
)
|
||||
const res = await objectsPromise
|
||||
|
||||
for (const object of res) {
|
||||
let asDoc = object
|
||||
if (hieararchy.isMixin(filter.key._class)) {
|
||||
asDoc = hieararchy.as(object, filter.key._class)
|
||||
}
|
||||
const arr = getObjectValue(filter.key.key, asDoc)
|
||||
if (!Array.isArray(arr)) continue
|
||||
for (const realValue of arr) {
|
||||
const value = getValue(realValue)
|
||||
values.add(value)
|
||||
realValues.set(value, (realValues.get(value) ?? new Set()).add(realValue))
|
||||
}
|
||||
}
|
||||
for (const object of filter.value.map((p) => p[0])) {
|
||||
values.add(object)
|
||||
}
|
||||
values = values
|
||||
objectsPromise = undefined
|
||||
}
|
||||
|
||||
function getValue (obj: any): any {
|
||||
if (typeof obj === 'string') {
|
||||
const trim = obj.trim()
|
||||
return trim.length > 0 ? trim.toUpperCase() : undefined
|
||||
} else {
|
||||
return obj ?? undefined
|
||||
}
|
||||
}
|
||||
|
||||
function isSelected (value: any, values: Set<any>): boolean {
|
||||
return values.has(value)
|
||||
}
|
||||
|
||||
function handleFilterToggle (value: any): void {
|
||||
if (isSelected(value, selectedValues)) {
|
||||
selectedValues.delete(value)
|
||||
} else {
|
||||
selectedValues.add(value)
|
||||
}
|
||||
selectedValues = selectedValues
|
||||
|
||||
updateFilter(selectedValues)
|
||||
}
|
||||
|
||||
function updateFilter (newValues: Set<any>) {
|
||||
clearTimeout(filterUpdateTimeout)
|
||||
|
||||
filterUpdateTimeout = setTimeout(() => {
|
||||
filter.value = [...newValues.values()].map((v) => {
|
||||
return [v, [...(realValues.get(v) ?? [])]]
|
||||
})
|
||||
|
||||
onChange(filter)
|
||||
}, FILTER_DEBOUNCE_MS)
|
||||
}
|
||||
|
||||
let search: string = ''
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
getValues(search)
|
||||
</script>
|
||||
|
||||
<div class="selectPopup" use:resizeObserver={() => dispatch('changeContent')}>
|
||||
<div class="header">
|
||||
<EditWithIcon
|
||||
icon={IconSearch}
|
||||
size={'large'}
|
||||
width={'100%'}
|
||||
autoFocus={!$deviceOptionsStore.isMobile}
|
||||
bind:value={search}
|
||||
placeholder={presentation.string.Search}
|
||||
on:change={() => {
|
||||
getValues(search)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div class="scroll">
|
||||
<div class="box">
|
||||
{#await promise then attribute}
|
||||
{#if objectsPromise}
|
||||
<Loading />
|
||||
{:else}
|
||||
{#each [...values.keys()] as value}
|
||||
{@const realValue = [...(realValues.get(value) ?? [])][0]}
|
||||
<button
|
||||
class="menu-item no-focus content-pointer-events-none"
|
||||
on:click={() => {
|
||||
handleFilterToggle(value)
|
||||
}}
|
||||
>
|
||||
{#if value !== undefined}
|
||||
<div class="clear-mins flex-grow">
|
||||
<svelte:component
|
||||
this={attribute.presenter}
|
||||
value={typeof value === 'string' ? realValue : value}
|
||||
{...attribute.props}
|
||||
oneLine
|
||||
/>
|
||||
</div>
|
||||
{:else}
|
||||
<span class="overflow-label flex-grow"><Label label={ui.string.NotSelected} /></span>
|
||||
{/if}
|
||||
<div class="check pointer-events-none">
|
||||
{#if isSelected(value, selectedValues)}
|
||||
<Icon icon={IconCheck} size={'small'} />
|
||||
{/if}
|
||||
</div>
|
||||
</button>
|
||||
{/each}
|
||||
{/if}
|
||||
{/await}
|
||||
</div>
|
||||
</div>
|
||||
<div class="menu-space" />
|
||||
</div>
|
@ -51,6 +51,14 @@ export function updateFilter (filter: Filter): void {
|
||||
filterStore.set(old)
|
||||
}
|
||||
|
||||
export async function arrayAllResult (filter: Filter): Promise<ObjQueryType<any>> {
|
||||
return { $all: filter.value.map((p) => p[1]).flat() }
|
||||
}
|
||||
|
||||
export async function arrayAnyResult (filter: Filter): Promise<ObjQueryType<any>> {
|
||||
return { $in: filter.value.map((p) => p[1]).flat() }
|
||||
}
|
||||
|
||||
export async function objectInResult (filter: Filter): Promise<ObjQueryType<any>> {
|
||||
return { $in: filter.value }
|
||||
}
|
||||
@ -213,7 +221,7 @@ export async function getRefs (filter: Filter, onUpdate: () => void): Promise<Ar
|
||||
return await promise
|
||||
}
|
||||
|
||||
export function buildFilterKey (
|
||||
function buildRefFilterKey (
|
||||
hierarchy: Hierarchy,
|
||||
_class: Ref<Class<Doc>>,
|
||||
key: string,
|
||||
@ -234,6 +242,16 @@ export function buildFilterKey (
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function buildFilterKey (
|
||||
hierarchy: Hierarchy,
|
||||
_class: Ref<Class<Doc>>,
|
||||
key: string,
|
||||
attribute: AnyAttribute
|
||||
): KeyFilter | undefined {
|
||||
const ref = buildRefFilterKey(hierarchy, _class, key, attribute)
|
||||
if (ref != null) return ref
|
||||
|
||||
const isCollection = hierarchy.isDerived(attribute.type._class, core.class.Collection)
|
||||
const targetClass = isCollection ? (attribute.type as Collection<AttachedDoc>).of : attribute.type._class
|
||||
|
@ -76,9 +76,12 @@ import UpDownNavigator from './components/UpDownNavigator.svelte'
|
||||
import ValueSelector from './components/ValueSelector.svelte'
|
||||
import ViewletSettingButton from './components/ViewletSettingButton.svelte'
|
||||
import DateFilterPresenter from './components/filter/DateFilterPresenter.svelte'
|
||||
import ArrayFilter from './components/filter/ArrayFilter.svelte'
|
||||
|
||||
import {
|
||||
afterResult,
|
||||
arrayAllResult,
|
||||
arrayAnyResult,
|
||||
beforeResult,
|
||||
containsResult,
|
||||
dateCustom,
|
||||
@ -179,6 +182,7 @@ function PositionElementAlignment (e?: Event): PopupAlignment | undefined {
|
||||
export default async (): Promise<Resources> => ({
|
||||
actionImpl,
|
||||
component: {
|
||||
ArrayFilter,
|
||||
ClassPresenter,
|
||||
ClassRefPresenter,
|
||||
ObjectFilter,
|
||||
@ -232,6 +236,8 @@ export default async (): Promise<Resources> => ({
|
||||
PositionElementAlignment
|
||||
},
|
||||
function: {
|
||||
FilterArrayAllResult: arrayAllResult,
|
||||
FilterArrayAnyResult: arrayAnyResult,
|
||||
FilterObjectInResult: objectInResult,
|
||||
FilterObjectNinResult: objectNinResult,
|
||||
FilterValueInResult: valueInResult,
|
||||
|
@ -23,6 +23,7 @@ export default mergeIds(viewId, view, {
|
||||
ObjectFilter: '' as AnyComponent,
|
||||
DateFilter: '' as AnyComponent,
|
||||
ValueFilter: '' as AnyComponent,
|
||||
ArrayFilter: '' as AnyComponent,
|
||||
StringFilter: '' as AnyComponent,
|
||||
TimestampFilter: '' as AnyComponent,
|
||||
FilterTypePopup: '' as AnyComponent,
|
||||
@ -50,6 +51,8 @@ export default mergeIds(viewId, view, {
|
||||
ClearFilters: '' as IntlString,
|
||||
FilterIsNot: '' as IntlString,
|
||||
FilterIsEither: '' as IntlString,
|
||||
FilterArrayAll: '' as IntlString,
|
||||
FilterArrayAny: '' as IntlString,
|
||||
FilterStatesCount: '' as IntlString,
|
||||
FilterRemoved: '' as IntlString,
|
||||
FilterUpdated: '' as IntlString,
|
||||
|
@ -787,6 +787,8 @@ const view = plugin(viewId, {
|
||||
MarkdownFormatting: '' as Ref<ActionCategory>
|
||||
},
|
||||
filter: {
|
||||
FilterArrayAll: '' as Ref<FilterMode>,
|
||||
FilterArrayAny: '' as Ref<FilterMode>,
|
||||
FilterObjectIn: '' as Ref<FilterMode>,
|
||||
FilterObjectNin: '' as Ref<FilterMode>,
|
||||
FilterValueIn: '' as Ref<FilterMode>,
|
||||
|
Loading…
Reference in New Issue
Block a user