Files
2025-10-16 12:17:51 +07:00

237 lines
6.0 KiB
TypeScript

// Copyright © 2025 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.
import { type QuerySelector } from '@hcengineering/core'
import { getEmbeddedLabel, type IntlString } from '@hcengineering/platform'
import { type AnyComponent } from '@hcengineering/ui'
import view from '@hcengineering/view-resources/src/plugin'
import plugin from './plugin'
export interface Mode {
id: string
label: IntlString
query: QuerySelector<any> | string
parse?: (value: any) => any
withoutEditor?: boolean
editor?: AnyComponent
}
const Equal: Mode = {
id: 'equals',
label: getEmbeddedLabel('='),
query: '$val'
}
const NotEqual: Mode = {
id: 'notEquals',
label: getEmbeddedLabel('≠'),
query: { $ne: '$val' }
}
const GT: Mode = {
id: 'greaterThan',
label: getEmbeddedLabel('>'),
query: { $gt: '$val' as any }
}
const LT: Mode = {
id: 'lessThan',
label: getEmbeddedLabel('<'),
query: { $lt: '$val' as any }
}
const Between: Mode = {
id: 'between',
label: view.string.Between,
query: { $gte: '$val[0]' as any, $lte: '$val[1]' as any },
parse: (value: any): any => {
return [value.$gte, value.$lte]
},
editor: plugin.criteriaEditor.RangeCriteria
}
const Exists: Mode = {
id: 'exists',
label: view.string.ValueIsSet,
query: { $exists: true },
withoutEditor: true
}
const StringContains: Mode = {
id: 'contains',
label: view.string.Contains,
query: { $like: '%$val%' },
parse: (value: any): any => {
if (typeof value !== 'string') return value
return value.startsWith('%') && value.endsWith('%') ? value.slice(1, -1) : value
}
}
const ArrayAll: Mode = {
id: 'arrayAll',
label: view.string.FilterArrayAll,
query: { $all: '$val' }
}
const ArrayAny: Mode = {
id: 'arrayAny',
label: view.string.FilterArrayAny,
query: { $in: '$val' as any }
}
const ArrayNotIncludes: Mode = {
id: 'arrayNotIncludes',
label: view.string.NotContains,
query: { $nin: '$val' as any }
}
const ArraySizeEquals: Mode = {
id: 'sizeEquals',
label: getEmbeddedLabel('='),
query: { $size: '$val' as any },
editor: plugin.criteriaEditor.ArraySizeCriteria
}
const ArraySizeGt: Mode = {
id: 'sizeGt',
label: getEmbeddedLabel('>'),
query: { $size: { $gt: '$val' as any } },
editor: plugin.criteriaEditor.ArraySizeCriteria
}
const ArraySizeGte: Mode = {
id: 'sizeGte',
label: getEmbeddedLabel('≥'),
query: { $size: { $gte: '$val' as any } },
editor: plugin.criteriaEditor.ArraySizeCriteria
}
const ArraySizeLt: Mode = {
id: 'sizeLt',
label: getEmbeddedLabel('<'),
query: { $size: { $lt: '$val' as any } },
editor: plugin.criteriaEditor.ArraySizeCriteria
}
const ArraySizeLte: Mode = {
id: 'sizeLte',
label: getEmbeddedLabel('≤'),
query: { $size: { $lte: '$val' as any } },
editor: plugin.criteriaEditor.ArraySizeCriteria
}
export const Modes = {
Exists,
Equal,
NotEqual,
StringContains,
GT,
LT,
Between,
ArrayAll,
ArrayAny,
ArrayNotIncludes,
ArraySizeEquals,
ArraySizeGt,
ArraySizeGte,
ArraySizeLt,
ArraySizeLte
} as const
export type ModeId = keyof typeof Modes
function hasSameKeys (target: any, value: any): boolean {
if (typeof target === 'object' && target !== null && typeof value === 'object' && value !== null) {
const keys1 = Object.keys(target)
const keys2 = Object.keys(value)
if (keys1.length !== keys2.length) return false
for (const k of keys1) {
if (!keys2.includes(k)) return false
}
for (const k of keys1) {
if (!hasSameKeys(target[k], value[k])) return false
}
return true
} else {
return typeof target !== 'object' && isNotObject(value)
}
}
function isNotObject (value: any): boolean {
return typeof value !== 'object' || value === null || Array.isArray(value)
}
function isModeMatch (mode: Mode, value: any): boolean {
if (typeof mode.query !== 'object') return isNotObject(value)
if (typeof mode.query === 'object' && isNotObject(value)) return false
return hasSameKeys(mode.query, value)
}
function getValue (_value: any): any {
let value = _value
while (!isNotObject(value) && Object.keys(value).length === 1) {
value = value[Object.keys(value)[0]]
}
return value
}
export function parseValue (modes: Mode[], value: any): [any, Mode] {
if (value == null) {
return [value, modes[0]]
}
for (const mode of modes) {
if (isModeMatch(mode, value)) {
if (mode.parse != null) {
return [mode.parse(getValue(value)), mode]
}
return [getValue(value), mode]
}
}
return [value, modes[0]]
}
export function buildResult (mode: Mode, value: any): any {
if (typeof mode.query !== 'object') return value
const res = JSON.parse(JSON.stringify(mode.query))
fillResult(mode, res, value)
return res
}
function fillResult (mode: Mode, obj: any, value: any): void {
if (typeof obj !== 'object') return
for (const key in obj) {
const v: any = obj[key]
if (typeof v === 'object') {
fillResult(mode, v, value)
} else {
if (typeof value === 'string' && typeof v === 'string') {
obj[key] = v.replace('$val', value)
} else if (typeof v === 'string' && v === '$val') {
obj[key] = value
} else if (typeof v === 'string' && v.startsWith('$val[')) {
const index = parseInt(v.slice(5, -1))
if (Array.isArray(value) && !isNaN(index) && index >= 0 && index < value.length) {
obj[key] = value[index]
} else {
obj[key] = undefined
}
} else {
obj[key] = mode.withoutEditor === true ? v : (value ?? v)
}
}
}
}