mirror of
https://github.com/hcengineering/platform.git
synced 2025-04-16 05:13:06 +00:00
Table filter (#1790)
Signed-off-by: Denis Bykhov <80476319+BykhovDenis@users.noreply.github.com>
This commit is contained in:
parent
9f5b620dfd
commit
298a277729
@ -471,6 +471,15 @@ export function createModel (builder: Builder): void {
|
|||||||
builder.mixin(recruit.class.Vacancy, core.class.Class, view.mixin.IgnoreActions, {
|
builder.mixin(recruit.class.Vacancy, core.class.Class, view.mixin.IgnoreActions, {
|
||||||
actions: [view.action.Delete]
|
actions: [view.action.Delete]
|
||||||
})
|
})
|
||||||
|
|
||||||
|
builder.mixin(recruit.mixin.Candidate, core.class.Class, view.mixin.ClassFilters, {
|
||||||
|
filters: ['title', 'source', 'city', 'modifiedOn']
|
||||||
|
})
|
||||||
|
|
||||||
|
builder.mixin(recruit.class.Applicant, core.class.Class, view.mixin.ClassFilters, {
|
||||||
|
filters: ['attachedTo', 'assignee', 'modifiedOn']
|
||||||
|
})
|
||||||
|
|
||||||
createReviewModel(builder)
|
createReviewModel(builder)
|
||||||
|
|
||||||
// createAction(builder, { ...viewTemplates.open, target: recruit.class.Vacancy, context: { mode: ['browser', 'context'] } })
|
// createAction(builder, { ...viewTemplates.open, target: recruit.class.Vacancy, context: { mode: ['browser', 'context'] } })
|
||||||
|
@ -23,12 +23,15 @@ import type {
|
|||||||
Action,
|
Action,
|
||||||
ActionCategory,
|
ActionCategory,
|
||||||
AttributeEditor,
|
AttributeEditor,
|
||||||
|
AttributeFilter,
|
||||||
AttributePresenter,
|
AttributePresenter,
|
||||||
BuildModelKey,
|
BuildModelKey,
|
||||||
|
ClassFilters,
|
||||||
CollectionEditor,
|
CollectionEditor,
|
||||||
HTMLPresenter,
|
HTMLPresenter,
|
||||||
IgnoreActions,
|
IgnoreActions,
|
||||||
KeyBinding,
|
KeyBinding,
|
||||||
|
KeyFilter,
|
||||||
LinkPresenter,
|
LinkPresenter,
|
||||||
ObjectEditor,
|
ObjectEditor,
|
||||||
ObjectEditorHeader,
|
ObjectEditorHeader,
|
||||||
@ -76,6 +79,16 @@ export function classPresenter (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Mixin(view.mixin.ClassFilters, core.class.Class)
|
||||||
|
export class TClassFilters extends TClass implements ClassFilters {
|
||||||
|
filters!: (string | KeyFilter)[]
|
||||||
|
}
|
||||||
|
|
||||||
|
@Mixin(view.mixin.AttributeFilter, core.class.Class)
|
||||||
|
export class TAttributeFilter extends TClass implements AttributeFilter {
|
||||||
|
component!: AnyComponent
|
||||||
|
}
|
||||||
|
|
||||||
@Mixin(view.mixin.AttributeEditor, core.class.Class)
|
@Mixin(view.mixin.AttributeEditor, core.class.Class)
|
||||||
export class TAttributeEditor extends TClass implements AttributeEditor {
|
export class TAttributeEditor extends TClass implements AttributeEditor {
|
||||||
editor!: AnyComponent
|
editor!: AnyComponent
|
||||||
@ -224,6 +237,8 @@ export const actionTemplates = template({
|
|||||||
|
|
||||||
export function createModel (builder: Builder): void {
|
export function createModel (builder: Builder): void {
|
||||||
builder.createModel(
|
builder.createModel(
|
||||||
|
TClassFilters,
|
||||||
|
TAttributeFilter,
|
||||||
TAttributeEditor,
|
TAttributeEditor,
|
||||||
TAttributePresenter,
|
TAttributePresenter,
|
||||||
TCollectionEditor,
|
TCollectionEditor,
|
||||||
@ -474,6 +489,26 @@ export function createModel (builder: Builder): void {
|
|||||||
pattern: '(www.)?github.com/',
|
pattern: '(www.)?github.com/',
|
||||||
component: view.component.GithubPresenter
|
component: view.component.GithubPresenter
|
||||||
})
|
})
|
||||||
|
|
||||||
|
builder.mixin(core.class.TypeString, core.class.Class, view.mixin.AttributeFilter, {
|
||||||
|
component: view.component.ValueFilter
|
||||||
|
})
|
||||||
|
|
||||||
|
builder.mixin(core.class.TypeNumber, core.class.Class, view.mixin.AttributeFilter, {
|
||||||
|
component: view.component.ValueFilter
|
||||||
|
})
|
||||||
|
|
||||||
|
builder.mixin(core.class.TypeDate, core.class.Class, view.mixin.AttributeFilter, {
|
||||||
|
component: view.component.ValueFilter
|
||||||
|
})
|
||||||
|
|
||||||
|
builder.mixin(core.class.RefTo, core.class.Class, view.mixin.AttributeFilter, {
|
||||||
|
component: view.component.ObjectFilter
|
||||||
|
})
|
||||||
|
|
||||||
|
builder.mixin(core.class.TypeTimestamp, core.class.Class, view.mixin.AttributeFilter, {
|
||||||
|
component: view.component.TimestampFilter
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export default view
|
export default view
|
||||||
|
@ -60,6 +60,9 @@ export default mergeIds(viewId, view, {
|
|||||||
Open: '' as ViewAction
|
Open: '' as ViewAction
|
||||||
},
|
},
|
||||||
component: {
|
component: {
|
||||||
|
ObjectFilter: '' as AnyComponent,
|
||||||
|
ValueFilter: '' as AnyComponent,
|
||||||
|
TimestampFilter: '' as AnyComponent,
|
||||||
StringEditor: '' as AnyComponent,
|
StringEditor: '' as AnyComponent,
|
||||||
StringPresenter: '' as AnyComponent,
|
StringPresenter: '' as AnyComponent,
|
||||||
IntlStringPresenter: '' as AnyComponent,
|
IntlStringPresenter: '' as AnyComponent,
|
||||||
|
@ -151,7 +151,10 @@ export abstract class MemDb extends TxProcessor {
|
|||||||
result = result.filter((r) => (r as any)[_class] !== undefined)
|
result = result.filter((r) => (r as any)[_class] !== undefined)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options?.lookup !== undefined) result = await this.lookup(result as T[], options.lookup)
|
if (options?.lookup !== undefined) {
|
||||||
|
result = await this.lookup(result as T[], options.lookup)
|
||||||
|
result = matchQuery(result, query, _class, this.hierarchy)
|
||||||
|
}
|
||||||
|
|
||||||
if (options?.sort !== undefined) resultSort(result, options?.sort, _class, this.hierarchy)
|
if (options?.sort !== undefined) resultSort(result, options?.sort, _class, this.hierarchy)
|
||||||
const total = result.length
|
const total = result.length
|
||||||
|
@ -7,6 +7,8 @@
|
|||||||
"Minutes": "{minutes, plural, =0 {less than a minute ago} =1 {a minute ago} other {# minutes ago}}",
|
"Minutes": "{minutes, plural, =0 {less than a minute ago} =1 {a minute ago} other {# minutes ago}}",
|
||||||
"Hours": "{hours, plural, =0 {less than an hour ago} =1 {an hour ago} other {# hours ago}}",
|
"Hours": "{hours, plural, =0 {less than an hour ago} =1 {an hour ago} other {# hours ago}}",
|
||||||
"Days": "{days, plural, =0 {today} =1 {yesterday} other {# days ago}}",
|
"Days": "{days, plural, =0 {today} =1 {yesterday} other {# days ago}}",
|
||||||
|
"Months": "{months, plural, =0 {this month} =1 {a month aago} other {# months ago}}",
|
||||||
|
"Years": "{years, plural, =0 {this year} =1 {a year ago} other {# years ago}}",
|
||||||
"ShowMore": "Show more",
|
"ShowMore": "Show more",
|
||||||
"ShowLess": "Show less",
|
"ShowLess": "Show less",
|
||||||
"Search": "Search",
|
"Search": "Search",
|
||||||
|
@ -7,6 +7,8 @@
|
|||||||
"Minutes": "{minutes, plural, =0 {меньше минуты назад} =1 {минуту назад} other {# минут назад}}",
|
"Minutes": "{minutes, plural, =0 {меньше минуты назад} =1 {минуту назад} other {# минут назад}}",
|
||||||
"Hours": "{hours, plural, =0 {меньше часа назад} =1 {час назад} other {# часов назад}}",
|
"Hours": "{hours, plural, =0 {меньше часа назад} =1 {час назад} other {# часов назад}}",
|
||||||
"Days": "{days, plural, =0 {сегода} =1 {вчера} other {# дней назад}}",
|
"Days": "{days, plural, =0 {сегода} =1 {вчера} other {# дней назад}}",
|
||||||
|
"Months": "{months, plural, =0 {в этом месяце} =1 {месяц назад} =2 {2 месяца назад} =3 {3 месяца назад} =4 {4 месяца назад} other {# месяцев назад}}",
|
||||||
|
"Years": "{years, plural, =0 {в этом году} =1 {год назад} =2 {2 года назад} =3 {3 года назад} =4 {4 года назад} other {# лет назад}}",
|
||||||
"ShowMore": "Показать больше",
|
"ShowMore": "Показать больше",
|
||||||
"ShowLess": "Показать меньше",
|
"ShowLess": "Показать меньше",
|
||||||
"Search": "Поиск",
|
"Search": "Поиск",
|
||||||
|
@ -25,6 +25,8 @@
|
|||||||
const MINUTE = SECOND * 60
|
const MINUTE = SECOND * 60
|
||||||
const HOUR = MINUTE * 60
|
const HOUR = MINUTE * 60
|
||||||
const DAY = HOUR * 24
|
const DAY = HOUR * 24
|
||||||
|
const MONTH = DAY * 30
|
||||||
|
const YEAR = MONTH * 12
|
||||||
|
|
||||||
let time: string = ''
|
let time: string = ''
|
||||||
|
|
||||||
@ -35,8 +37,12 @@
|
|||||||
time = await translate(ui.string.Minutes, { minutes: Math.floor(passed / MINUTE) })
|
time = await translate(ui.string.Minutes, { minutes: Math.floor(passed / MINUTE) })
|
||||||
} else if (passed < DAY) {
|
} else if (passed < DAY) {
|
||||||
time = await translate(ui.string.Hours, { hours: Math.floor(passed / HOUR) })
|
time = await translate(ui.string.Hours, { hours: Math.floor(passed / HOUR) })
|
||||||
} else {
|
} else if (passed < MONTH) {
|
||||||
time = await translate(ui.string.Days, { days: Math.floor(passed / DAY) })
|
time = await translate(ui.string.Days, { days: Math.floor(passed / DAY) })
|
||||||
|
} else if (passed < YEAR) {
|
||||||
|
time = await translate(ui.string.Months, { months: Math.floor(passed / MONTH) })
|
||||||
|
} else {
|
||||||
|
time = await translate(ui.string.Years, { years: Math.floor(passed / YEAR) })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -32,6 +32,8 @@ export default plugin(uiId, {
|
|||||||
Minutes: '' as IntlString,
|
Minutes: '' as IntlString,
|
||||||
Hours: '' as IntlString,
|
Hours: '' as IntlString,
|
||||||
Days: '' as IntlString,
|
Days: '' as IntlString,
|
||||||
|
Months: '' as IntlString,
|
||||||
|
Years: '' as IntlString,
|
||||||
ShowMore: '' as IntlString,
|
ShowMore: '' as IntlString,
|
||||||
ShowLess: '' as IntlString,
|
ShowLess: '' as IntlString,
|
||||||
Search: '' as IntlString,
|
Search: '' as IntlString,
|
||||||
|
@ -37,6 +37,14 @@
|
|||||||
"WithTime": "WithTime",
|
"WithTime": "WithTime",
|
||||||
"CreatingAttribute": "Creating an attribute",
|
"CreatingAttribute": "Creating an attribute",
|
||||||
"CreatingAttributeConfirm": "Warning: It will not be possible for now to change or delete created attribute.",
|
"CreatingAttributeConfirm": "Warning: It will not be possible for now to change or delete created attribute.",
|
||||||
"CustomizeView": "Customize view"
|
"CustomizeView": "Customize view",
|
||||||
|
"Filter": "Filter",
|
||||||
|
"ClearFilters": "Clear filters",
|
||||||
|
"FilterIs": "is",
|
||||||
|
"FilterIsNot": "is not",
|
||||||
|
"FilterIsEither": "is either of",
|
||||||
|
"FilterStatesCount": "{value, plural, =1 {1 state} other {# states}}",
|
||||||
|
"Before": "Before",
|
||||||
|
"After": "After"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -35,6 +35,14 @@
|
|||||||
"ShowPreview": "Предпросмотр документа",
|
"ShowPreview": "Предпросмотр документа",
|
||||||
"ShowActions": "Показать действия",
|
"ShowActions": "Показать действия",
|
||||||
"RestoreDefaults": "По умолчанию",
|
"RestoreDefaults": "По умолчанию",
|
||||||
"CustomizeView": "Настроить отображение"
|
"CustomizeView": "Настроить отображение",
|
||||||
|
"Filter": "Фильтр",
|
||||||
|
"ClearFilters": "Очистить",
|
||||||
|
"FilterIs": "равен",
|
||||||
|
"FilterIsNot": "не равен",
|
||||||
|
"FilterIsEither": "один из",
|
||||||
|
"FilterStatesCount": "{value, plural, =1 {1 состоянию} other {# состояний}}",
|
||||||
|
"Before": "До",
|
||||||
|
"After": "После"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -20,6 +20,7 @@
|
|||||||
import { ActionContext } from '..'
|
import { ActionContext } from '..'
|
||||||
import { focusStore, ListSelectionProvider, SelectDirection, selectionStore } from '../selection'
|
import { focusStore, ListSelectionProvider, SelectDirection, selectionStore } from '../selection'
|
||||||
import { LoadingProps } from '../utils'
|
import { LoadingProps } from '../utils'
|
||||||
|
import FilterBar from './filter/FilterBar.svelte'
|
||||||
import Table from './Table.svelte'
|
import Table from './Table.svelte'
|
||||||
|
|
||||||
export let _class: Ref<Class<Doc>>
|
export let _class: Ref<Class<Doc>>
|
||||||
@ -32,6 +33,8 @@
|
|||||||
// If defined, will show a number of dummy items before real data will appear.
|
// If defined, will show a number of dummy items before real data will appear.
|
||||||
export let loadingProps: LoadingProps | undefined = undefined
|
export let loadingProps: LoadingProps | undefined = undefined
|
||||||
|
|
||||||
|
let resultQuery = query
|
||||||
|
|
||||||
let table: Table
|
let table: Table
|
||||||
const listProvider = new ListSelectionProvider((offset: 1 | -1 | 0, of?: Doc, dir?: SelectDirection) => {
|
const listProvider = new ListSelectionProvider((offset: 1 | -1 | 0, of?: Doc, dir?: SelectDirection) => {
|
||||||
if (dir === 'vertical') {
|
if (dir === 'vertical') {
|
||||||
@ -51,13 +54,14 @@
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<FilterBar {_class} {query} on:change={(e) => (resultQuery = e.detail)} />
|
||||||
<Scroller tableFade>
|
<Scroller tableFade>
|
||||||
<Table
|
<Table
|
||||||
bind:this={table}
|
bind:this={table}
|
||||||
{_class}
|
{_class}
|
||||||
{config}
|
{config}
|
||||||
{options}
|
{options}
|
||||||
{query}
|
query={resultQuery}
|
||||||
{showNotification}
|
{showNotification}
|
||||||
{baseMenuClass}
|
{baseMenuClass}
|
||||||
{loadingProps}
|
{loadingProps}
|
||||||
|
163
plugins/view-resources/src/components/filter/FilterBar.svelte
Normal file
163
plugins/view-resources/src/components/filter/FilterBar.svelte
Normal file
@ -0,0 +1,163 @@
|
|||||||
|
<!--
|
||||||
|
// 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, DocumentQuery, Ref } from '@anticrm/core'
|
||||||
|
import { getClient } from '@anticrm/presentation'
|
||||||
|
import { Button, eventToHTMLElement, IconAdd, IconClose, showPopup } from '@anticrm/ui'
|
||||||
|
import { Filter } from '@anticrm/view'
|
||||||
|
import { createEventDispatcher } from 'svelte'
|
||||||
|
import view from '../../plugin'
|
||||||
|
import FilterSection from './FilterSection.svelte'
|
||||||
|
import FilterTypePopup from './FilterTypePopup.svelte'
|
||||||
|
|
||||||
|
export let _class: Ref<Class<Doc>>
|
||||||
|
export let query: DocumentQuery<Doc>
|
||||||
|
|
||||||
|
const client = getClient()
|
||||||
|
const hierarchy = client.getHierarchy()
|
||||||
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
|
let filters: Filter[] = []
|
||||||
|
let isNew = true
|
||||||
|
|
||||||
|
function onChange (e: Filter | undefined) {
|
||||||
|
if (e === undefined) return
|
||||||
|
if (isNew) {
|
||||||
|
filters.push(e)
|
||||||
|
isNew = false
|
||||||
|
filters = filters
|
||||||
|
} else {
|
||||||
|
filters[filters.length - 1] = e
|
||||||
|
filters = filters
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function add (e: MouseEvent) {
|
||||||
|
const target = eventToHTMLElement(e)
|
||||||
|
isNew = true
|
||||||
|
showPopup(
|
||||||
|
FilterTypePopup,
|
||||||
|
{
|
||||||
|
_class,
|
||||||
|
makeQuery: (key: string) => makeQuery(query, filters, key),
|
||||||
|
target,
|
||||||
|
onChange
|
||||||
|
},
|
||||||
|
target
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function remove (i: number) {
|
||||||
|
filters.splice(i, 1)
|
||||||
|
filters = filters
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeQuery (query: DocumentQuery<Doc>, filters: Filter[], skipKey?: string): DocumentQuery<Doc> {
|
||||||
|
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
|
||||||
|
if (newQuery[filter.key.key] === undefined) {
|
||||||
|
newQuery[filter.key.key] = filter.mode.result(filter.value)
|
||||||
|
} else {
|
||||||
|
const newValue = filter.mode.result(filter.value)
|
||||||
|
let merged = false
|
||||||
|
for (const key in newValue) {
|
||||||
|
if (newQuery[filter.key.key][key] === undefined) {
|
||||||
|
newQuery[filter.key.key][key] = newValue[key]
|
||||||
|
merged = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (key === '$in') {
|
||||||
|
newQuery[filter.key.key][key] = newQuery[filter.key.key][key].filter((p: any) => newValue[key].includes(p))
|
||||||
|
merged = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (key === '$nin') {
|
||||||
|
newQuery[filter.key.key][key] = [...newQuery[filter.key.key][key], ...newValue[key]]
|
||||||
|
merged = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (key === '$lt') {
|
||||||
|
newQuery[filter.key.key][key] =
|
||||||
|
newQuery[filter.key.key][key] < newValue[key] ? newQuery[filter.key.key][key] : newValue[key]
|
||||||
|
merged = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (key === '$gt') {
|
||||||
|
newQuery[filter.key.key][key] =
|
||||||
|
newQuery[filter.key.key][key] > newValue[key] ? newQuery[filter.key.key][key] : newValue[key]
|
||||||
|
merged = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!merged) {
|
||||||
|
Object.assign(newQuery[filter.key.key], filter.mode.result(filter.value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (skipKey === undefined) {
|
||||||
|
dispatch('change', newQuery)
|
||||||
|
}
|
||||||
|
return newQuery
|
||||||
|
}
|
||||||
|
|
||||||
|
$: makeQuery(query, filters)
|
||||||
|
|
||||||
|
$: clazz = hierarchy.getClass(_class)
|
||||||
|
$: visible = hierarchy.hasMixin(clazz, view.mixin.ClassFilters)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if visible}
|
||||||
|
<div class="flex pl-4 pr-4">
|
||||||
|
{#each filters as filter, i}
|
||||||
|
<FilterSection
|
||||||
|
{_class}
|
||||||
|
query={makeQuery(query, filters, filter.key.key)}
|
||||||
|
{filter}
|
||||||
|
on:change={() => {
|
||||||
|
makeQuery(query, filters)
|
||||||
|
}}
|
||||||
|
on:remove={() => {
|
||||||
|
remove(i)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/each}
|
||||||
|
<div class="ml-2">
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
icon={IconAdd}
|
||||||
|
kind={'link-bordered'}
|
||||||
|
borderStyle={'dashed'}
|
||||||
|
label={view.string.Filter}
|
||||||
|
on:click={add}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{#if filters.length}
|
||||||
|
<div class="ml-2">
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
icon={IconClose}
|
||||||
|
kind={'link-bordered'}
|
||||||
|
borderStyle={'dashed'}
|
||||||
|
label={view.string.ClearFilters}
|
||||||
|
on:click={() => {
|
||||||
|
filters = []
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
@ -0,0 +1,96 @@
|
|||||||
|
<!--
|
||||||
|
// 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, DocumentQuery, Ref } from '@anticrm/core'
|
||||||
|
import { Button, eventToHTMLElement, IconClose, showPopup } from '@anticrm/ui'
|
||||||
|
import { Filter } from '@anticrm/view'
|
||||||
|
import { createEventDispatcher } from 'svelte'
|
||||||
|
import view from '../../plugin'
|
||||||
|
|
||||||
|
export let _class: Ref<Class<Doc>>
|
||||||
|
export let query: DocumentQuery<Doc>
|
||||||
|
export let filter: Filter
|
||||||
|
let current = 0
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
|
function toggle () {
|
||||||
|
const modes = filter.modes.filter((p) => p.isAvailable(filter.value))
|
||||||
|
current++
|
||||||
|
filter.mode = modes[current % modes.length]
|
||||||
|
dispatch('change')
|
||||||
|
}
|
||||||
|
|
||||||
|
function onChange (e: Filter | undefined) {
|
||||||
|
filter = filter
|
||||||
|
dispatch('change')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="root">
|
||||||
|
<div class="buttonWrapper">
|
||||||
|
<Button shape={'rectangle-right'} label={filter.key.label} icon={filter.key.icon} />
|
||||||
|
</div>
|
||||||
|
<div class="buttonWrapper">
|
||||||
|
<Button shape="rectangle" label={filter.mode.label} on:click={toggle} />
|
||||||
|
</div>
|
||||||
|
<div class="buttonWrapper">
|
||||||
|
<Button
|
||||||
|
shape={'rectangle'}
|
||||||
|
label={view.string.FilterStatesCount}
|
||||||
|
labelParams={{ value: filter.value.length }}
|
||||||
|
on:click={(e) => {
|
||||||
|
showPopup(
|
||||||
|
filter.key.component,
|
||||||
|
{
|
||||||
|
_class,
|
||||||
|
query,
|
||||||
|
filter,
|
||||||
|
onChange
|
||||||
|
},
|
||||||
|
eventToHTMLElement(e)
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="buttonWrapper">
|
||||||
|
<Button
|
||||||
|
shape={'rectangle-left'}
|
||||||
|
icon={IconClose}
|
||||||
|
on:click={() => {
|
||||||
|
dispatch('remove')
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.root {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
&:not(:first-child) {
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttonWrapper {
|
||||||
|
margin-right: 1px;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
@ -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 core, { AnyAttribute, ArrOf, Class, 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'
|
||||||
|
import { createEventDispatcher } from 'svelte'
|
||||||
|
import view from '../../plugin'
|
||||||
|
|
||||||
|
export let _class: Ref<Class<Doc>>
|
||||||
|
export let makeQuery: (key: string) => DocumentQuery<Doc>
|
||||||
|
export let target: HTMLElement
|
||||||
|
export let onChange: (e: Filter) => void
|
||||||
|
|
||||||
|
const client = getClient()
|
||||||
|
const hierarchy = client.getHierarchy()
|
||||||
|
|
||||||
|
function getFilters (_class: Ref<Class<Doc>>): KeyFilter[] {
|
||||||
|
const clazz = hierarchy.getClass(_class)
|
||||||
|
const mixin = hierarchy.as(clazz, view.mixin.ClassFilters)
|
||||||
|
if (mixin.filters === undefined) return []
|
||||||
|
const filters = mixin.filters.map((p) => {
|
||||||
|
return typeof p === 'string' ? buildFilterFromKey(p) : p
|
||||||
|
})
|
||||||
|
const result: KeyFilter[] = []
|
||||||
|
for (const filter of filters) {
|
||||||
|
if (filter !== undefined) result.push(filter)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildFilterFromKey (key: string): KeyFilter | undefined {
|
||||||
|
const attribute = hierarchy.getAttribute(_class, key)
|
||||||
|
return buildFilter(key, attribute)
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildFilter (key: string, attribute: AnyAttribute): KeyFilter | undefined {
|
||||||
|
const clazz = hierarchy.getClass(attribute.type._class)
|
||||||
|
const filter = hierarchy.as(clazz, view.mixin.AttributeFilter)
|
||||||
|
if (filter.component === undefined) return undefined
|
||||||
|
return {
|
||||||
|
key: key,
|
||||||
|
label: attribute.label,
|
||||||
|
icon: attribute.icon,
|
||||||
|
component: filter.component
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getValue (name: string, type: Type<any>): string {
|
||||||
|
if (hierarchy.isDerived(type._class, core.class.ArrOf)) {
|
||||||
|
return getValue(name, (type as ArrOf<any>).of)
|
||||||
|
}
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTypes (_class: Ref<Class<Doc>>): KeyFilter[] {
|
||||||
|
const result = getFilters(_class)
|
||||||
|
const allAttributes = hierarchy.getAllAttributes(_class)
|
||||||
|
for (const [, attribute] of allAttributes) {
|
||||||
|
if (attribute.isCustom !== true) continue
|
||||||
|
if (attribute.label === undefined || attribute.hidden) continue
|
||||||
|
const value = getValue(attribute.name, attribute.type)
|
||||||
|
if (result.findIndex((p) => p.key === value) !== -1) continue
|
||||||
|
const filter = buildFilter(value, attribute)
|
||||||
|
if (filter !== undefined) {
|
||||||
|
result.push(filter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
const actionElements: HTMLButtonElement[] = []
|
||||||
|
|
||||||
|
const keyDown = (event: KeyboardEvent, index: number) => {
|
||||||
|
if (event.key === 'ArrowDown') {
|
||||||
|
actionElements[(index + 1) % actionElements.length].focus()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key === 'ArrowUp') {
|
||||||
|
actionElements[(actionElements.length + index - 1) % actionElements.length].focus()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key === 'ArrowLeft') {
|
||||||
|
dispatch('close')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
|
function click (type: KeyFilter): void {
|
||||||
|
dispatch('close')
|
||||||
|
|
||||||
|
showPopup(
|
||||||
|
type.component,
|
||||||
|
{
|
||||||
|
_class,
|
||||||
|
query: makeQuery(type.key),
|
||||||
|
filter: {
|
||||||
|
key: type,
|
||||||
|
value: []
|
||||||
|
},
|
||||||
|
onChange
|
||||||
|
},
|
||||||
|
target
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="antiPopup">
|
||||||
|
<div class="ap-space" />
|
||||||
|
<div class="ap-scroll">
|
||||||
|
<div class="ap-box">
|
||||||
|
{#each getTypes(_class) as type, i}
|
||||||
|
<!-- svelte-ignore a11y-mouse-events-have-key-events -->
|
||||||
|
<button
|
||||||
|
class="ap-menuItem flex-row-center withIcon"
|
||||||
|
on:keydown={(event) => keyDown(event, i)}
|
||||||
|
on:mouseover={(event) => {
|
||||||
|
event.currentTarget.focus()
|
||||||
|
}}
|
||||||
|
on:click={(event) => {
|
||||||
|
click(type)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{#if type.icon}
|
||||||
|
<div class="icon"><Icon icon={type.icon} size={'small'} /></div>
|
||||||
|
{/if}
|
||||||
|
<div class="ml-3 pr-1"><Label label={type.label} /></div>
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="ap-space" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.withIcon {
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
color: var(--content-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus .icon {
|
||||||
|
color: var(--accent-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
147
plugins/view-resources/src/components/filter/ObjectFilter.svelte
Normal file
147
plugins/view-resources/src/components/filter/ObjectFilter.svelte
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
<!--
|
||||||
|
// 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, DocumentQuery, getObjectValue, Ref } from '@anticrm/core'
|
||||||
|
import { translate } from '@anticrm/platform'
|
||||||
|
import presentation, { getClient } from '@anticrm/presentation'
|
||||||
|
import { CheckBox } from '@anticrm/ui'
|
||||||
|
import { Filter } from '@anticrm/view'
|
||||||
|
import { onMount } from 'svelte'
|
||||||
|
import { buildConfigLookup, getPresenter } from '../../utils'
|
||||||
|
import view from '../../plugin'
|
||||||
|
|
||||||
|
export let _class: Ref<Class<Doc>>
|
||||||
|
export let query: DocumentQuery<Doc>
|
||||||
|
export let filter: Filter
|
||||||
|
export let onChange: (e: Filter) => void
|
||||||
|
|
||||||
|
const client = getClient()
|
||||||
|
const hierarchy = client.getHierarchy()
|
||||||
|
const tkey = '$lookup.' + filter.key.key
|
||||||
|
const key = { key: tkey }
|
||||||
|
const lookup = buildConfigLookup(hierarchy, _class, [tkey])
|
||||||
|
const promise = getPresenter(client, _class, key, key, lookup)
|
||||||
|
filter.modes = [
|
||||||
|
{
|
||||||
|
label: view.string.FilterIs,
|
||||||
|
isAvailable: (res: any[]) => res.length <= 1,
|
||||||
|
result: (res: any[]) => {
|
||||||
|
return { $in: res }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: view.string.FilterIsEither,
|
||||||
|
isAvailable: (res: any[]) => res.length > 1,
|
||||||
|
result: (res: any[]) => {
|
||||||
|
return { $in: res }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: view.string.FilterIsNot,
|
||||||
|
isAvailable: () => true,
|
||||||
|
result: (res: any[]) => {
|
||||||
|
return { $nin: res }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
let values: Set<Doc> = new Set<Doc>()
|
||||||
|
|
||||||
|
$: getValues(search)
|
||||||
|
|
||||||
|
async function getValues (search: string): Promise<void> {
|
||||||
|
const attrib = await promise
|
||||||
|
const resultQuery =
|
||||||
|
search !== ''
|
||||||
|
? {
|
||||||
|
[attrib.sortingKey]: { $like: '%' + search + '%' },
|
||||||
|
...query
|
||||||
|
}
|
||||||
|
: query
|
||||||
|
const res = await client.findAll(_class, resultQuery, { lookup })
|
||||||
|
const objects = []
|
||||||
|
const set: Set<any> = new Set<any>()
|
||||||
|
for (const obj of res) {
|
||||||
|
const value = (obj as any)[filter.key.key]
|
||||||
|
if (set.has(value)) continue
|
||||||
|
objects.push(obj)
|
||||||
|
set.add(value)
|
||||||
|
}
|
||||||
|
values = new Set(objects.map((obj) => getObjectValue(tkey, obj)))
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSelected (value: Doc, values: any[]): boolean {
|
||||||
|
return values.includes(value._id)
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkMode () {
|
||||||
|
if (filter.mode?.isAvailable(filter.value)) return
|
||||||
|
const newMode = filter.modes.find((p) => p.isAvailable(filter.value))
|
||||||
|
filter.mode = newMode !== undefined ? newMode : filter.mode
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggle (value: Doc): void {
|
||||||
|
if (isSelected(value, filter.value)) {
|
||||||
|
filter.value = filter.value.filter((p) => p !== value._id)
|
||||||
|
} else {
|
||||||
|
filter.value = [...filter.value, value._id]
|
||||||
|
}
|
||||||
|
checkMode()
|
||||||
|
onChange(filter)
|
||||||
|
}
|
||||||
|
|
||||||
|
let search: string = ''
|
||||||
|
let phTraslate: string = ''
|
||||||
|
let searchInput: HTMLInputElement
|
||||||
|
$: translate(presentation.string.Search, {}).then((res) => {
|
||||||
|
phTraslate = res
|
||||||
|
})
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
if (searchInput) searchInput.focus()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="selectPopup">
|
||||||
|
<div class="header">
|
||||||
|
<input
|
||||||
|
bind:this={searchInput}
|
||||||
|
type="text"
|
||||||
|
bind:value={search}
|
||||||
|
placeholder={phTraslate}
|
||||||
|
on:input={(ev) => {}}
|
||||||
|
on:change
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="scroll">
|
||||||
|
<div class="box">
|
||||||
|
{#await promise then attribute}
|
||||||
|
{#each Array.from(values) as value}
|
||||||
|
<button
|
||||||
|
class="menu-item"
|
||||||
|
on:click={() => {
|
||||||
|
toggle(value)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div class="check pointer-events-none">
|
||||||
|
<CheckBox checked={isSelected(value, filter.value)} primary />
|
||||||
|
</div>
|
||||||
|
<svelte:component this={attribute.presenter} {value} {...attribute.props} />
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
{/await}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -0,0 +1,84 @@
|
|||||||
|
<!--
|
||||||
|
// 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 { Filter } from '@anticrm/view'
|
||||||
|
import { createEventDispatcher } from 'svelte'
|
||||||
|
import view from '../../plugin'
|
||||||
|
import TimestampPresenter from '../TimestampPresenter.svelte'
|
||||||
|
|
||||||
|
export let filter: Filter
|
||||||
|
export let onChange: (e: Filter) => void
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
|
filter.modes = [
|
||||||
|
{
|
||||||
|
label: view.string.Before,
|
||||||
|
isAvailable: (res: any[]) => true,
|
||||||
|
result: (res: any[]) => {
|
||||||
|
return { $lt: res[0] }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: view.string.After,
|
||||||
|
isAvailable: () => true,
|
||||||
|
result: (res: any[]) => {
|
||||||
|
return { $gt: res[0] }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
filter.mode = filter.mode === undefined ? filter.modes[0] : filter.mode
|
||||||
|
|
||||||
|
function click (value: number): void {
|
||||||
|
filter.value = [value]
|
||||||
|
onChange(filter)
|
||||||
|
dispatch('close')
|
||||||
|
}
|
||||||
|
|
||||||
|
const today = new Date().setHours(0, 0, 0, 0)
|
||||||
|
function shiftDays (diff: number): number {
|
||||||
|
return new Date(today).setDate(new Date(today).getDate() - diff)
|
||||||
|
}
|
||||||
|
|
||||||
|
const values = [
|
||||||
|
shiftDays(1),
|
||||||
|
shiftDays(2),
|
||||||
|
shiftDays(3),
|
||||||
|
shiftDays(7),
|
||||||
|
shiftDays(14),
|
||||||
|
shiftDays(21),
|
||||||
|
shiftDays(30),
|
||||||
|
shiftDays(90),
|
||||||
|
shiftDays(180),
|
||||||
|
shiftDays(365)
|
||||||
|
]
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="selectPopup">
|
||||||
|
<div class="scroll">
|
||||||
|
<div class="box">
|
||||||
|
{#each values as value, i}
|
||||||
|
<div
|
||||||
|
class="menu-item"
|
||||||
|
on:click={() => {
|
||||||
|
click(value)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TimestampPresenter {value} />
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
136
plugins/view-resources/src/components/filter/ValueFilter.svelte
Normal file
136
plugins/view-resources/src/components/filter/ValueFilter.svelte
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
<!--
|
||||||
|
// 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, DocumentQuery, getObjectValue, Ref } from '@anticrm/core'
|
||||||
|
import { translate } from '@anticrm/platform'
|
||||||
|
import presentation, { getClient } from '@anticrm/presentation'
|
||||||
|
import { CheckBox } from '@anticrm/ui'
|
||||||
|
import { Filter } from '@anticrm/view'
|
||||||
|
import { onMount } from 'svelte'
|
||||||
|
import { getPresenter } from '../../utils'
|
||||||
|
import view from '../../plugin'
|
||||||
|
|
||||||
|
export let _class: Ref<Class<Doc>>
|
||||||
|
export let query: DocumentQuery<Doc>
|
||||||
|
export let filter: Filter
|
||||||
|
export let onChange: (e: Filter) => void
|
||||||
|
|
||||||
|
filter.modes = [
|
||||||
|
{
|
||||||
|
label: view.string.FilterIs,
|
||||||
|
isAvailable: (res: any[]) => res.length <= 1,
|
||||||
|
result: (res: any[]) => {
|
||||||
|
return { $in: res }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: view.string.FilterIsEither,
|
||||||
|
isAvailable: (res: any[]) => res.length > 1,
|
||||||
|
result: (res: any[]) => {
|
||||||
|
return { $in: res }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: view.string.FilterIsNot,
|
||||||
|
isAvailable: () => true,
|
||||||
|
result: (res: any[]) => {
|
||||||
|
return { $nin: res }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const client = getClient()
|
||||||
|
const key = { key: filter.key.key }
|
||||||
|
const promise = getPresenter(client, _class, key, key)
|
||||||
|
|
||||||
|
let values: Set<any> = new Set<any>()
|
||||||
|
|
||||||
|
$: getValues(search)
|
||||||
|
|
||||||
|
async function getValues (search: string): Promise<void> {
|
||||||
|
const resultQuery =
|
||||||
|
search !== ''
|
||||||
|
? {
|
||||||
|
[filter.key.key]: { $like: '%' + search + '%' },
|
||||||
|
...query
|
||||||
|
}
|
||||||
|
: query
|
||||||
|
const res = await client.findAll(_class, resultQuery)
|
||||||
|
values = new Set(res.map((obj) => getObjectValue(filter.key.key, obj)))
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSelected (value: any, values: any[]): boolean {
|
||||||
|
return values.includes(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkMode () {
|
||||||
|
if (filter.mode?.isAvailable(filter.value)) return
|
||||||
|
const newMode = filter.modes.find((p) => p.isAvailable(filter.value))
|
||||||
|
filter.mode = newMode !== undefined ? newMode : filter.mode
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggle (value: any): void {
|
||||||
|
if (isSelected(value, filter.value)) {
|
||||||
|
filter.value = filter.value.filter((p) => p !== value)
|
||||||
|
} else {
|
||||||
|
filter.value = [...filter.value, value]
|
||||||
|
}
|
||||||
|
checkMode()
|
||||||
|
onChange(filter)
|
||||||
|
}
|
||||||
|
|
||||||
|
let search: string = ''
|
||||||
|
let phTraslate: string = ''
|
||||||
|
let searchInput: HTMLInputElement
|
||||||
|
$: translate(presentation.string.Search, {}).then((res) => {
|
||||||
|
phTraslate = res
|
||||||
|
})
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
if (searchInput) searchInput.focus()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="selectPopup">
|
||||||
|
<div class="header">
|
||||||
|
<input
|
||||||
|
bind:this={searchInput}
|
||||||
|
type="text"
|
||||||
|
bind:value={search}
|
||||||
|
placeholder={phTraslate}
|
||||||
|
on:input={(ev) => {}}
|
||||||
|
on:change
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="scroll">
|
||||||
|
<div class="box">
|
||||||
|
{#await promise then attribute}
|
||||||
|
{#each Array.from(values) as value}
|
||||||
|
<button
|
||||||
|
class="menu-item"
|
||||||
|
on:click={() => {
|
||||||
|
toggle(value)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div class="check pointer-events-none">
|
||||||
|
<CheckBox checked={isSelected(value, filter.value)} primary />
|
||||||
|
</div>
|
||||||
|
<svelte:component this={attribute.presenter} {value} {...attribute.props} />
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
{/await}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -46,6 +46,9 @@ import RefEditor from './components/typeEditors/RefEditor.svelte'
|
|||||||
import DocAttributeBar from './components/DocAttributeBar.svelte'
|
import DocAttributeBar from './components/DocAttributeBar.svelte'
|
||||||
import ViewletSetting from './components/ViewletSetting.svelte'
|
import ViewletSetting from './components/ViewletSetting.svelte'
|
||||||
import TableBrowser from './components/TableBrowser.svelte'
|
import TableBrowser from './components/TableBrowser.svelte'
|
||||||
|
import ValueFilter from './components/filter/ValueFilter.svelte'
|
||||||
|
import ObjectFilter from './components/filter/ObjectFilter.svelte'
|
||||||
|
import TimestampFilter from './components/filter/TimestampFilter.svelte'
|
||||||
|
|
||||||
export { getActions, invokeAction } from './actions'
|
export { getActions, invokeAction } from './actions'
|
||||||
export { default as ActionContext } from './components/ActionContext.svelte'
|
export { default as ActionContext } from './components/ActionContext.svelte'
|
||||||
@ -72,6 +75,9 @@ export {
|
|||||||
export default async (): Promise<Resources> => ({
|
export default async (): Promise<Resources> => ({
|
||||||
actionImpl: actionImpl,
|
actionImpl: actionImpl,
|
||||||
component: {
|
component: {
|
||||||
|
ObjectFilter,
|
||||||
|
ValueFilter,
|
||||||
|
TimestampFilter,
|
||||||
TableBrowser,
|
TableBrowser,
|
||||||
ViewletSetting,
|
ViewletSetting,
|
||||||
CreateAttribute,
|
CreateAttribute,
|
||||||
|
@ -40,6 +40,14 @@ export default mergeIds(viewId, view, {
|
|||||||
ActionPlaceholder: '' as IntlString,
|
ActionPlaceholder: '' as IntlString,
|
||||||
CreatingAttribute: '' as IntlString,
|
CreatingAttribute: '' as IntlString,
|
||||||
RestoreDefaults: '' as IntlString,
|
RestoreDefaults: '' as IntlString,
|
||||||
CreatingAttributeConfirm: '' as IntlString
|
CreatingAttributeConfirm: '' as IntlString,
|
||||||
|
Filter: '' as IntlString,
|
||||||
|
ClearFilters: '' as IntlString,
|
||||||
|
FilterIs: '' as IntlString,
|
||||||
|
FilterIsNot: '' as IntlString,
|
||||||
|
FilterIsEither: '' as IntlString,
|
||||||
|
FilterStatesCount: '' as IntlString,
|
||||||
|
Before: '' as IntlString,
|
||||||
|
After: '' as IntlString
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -123,7 +123,7 @@ async function getAttributePresenter (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getPresenter<T extends Doc> (
|
export async function getPresenter<T extends Doc> (
|
||||||
client: Client,
|
client: Client,
|
||||||
_class: Ref<Class<T>>,
|
_class: Ref<Class<T>>,
|
||||||
key: BuildModelKey,
|
key: BuildModelKey,
|
||||||
|
@ -24,8 +24,10 @@ import type {
|
|||||||
Lookup,
|
Lookup,
|
||||||
Mixin,
|
Mixin,
|
||||||
Obj,
|
Obj,
|
||||||
|
ObjQueryType,
|
||||||
Ref,
|
Ref,
|
||||||
Space,
|
Space,
|
||||||
|
Type,
|
||||||
UXObject
|
UXObject
|
||||||
} from '@anticrm/core'
|
} from '@anticrm/core'
|
||||||
import type { Asset, IntlString, Plugin, Resource, Status } from '@anticrm/platform'
|
import type { Asset, IntlString, Plugin, Resource, Status } from '@anticrm/platform'
|
||||||
@ -34,6 +36,49 @@ import type { AnyComponent, AnySvelteComponent } from '@anticrm/ui'
|
|||||||
import { PopupPosAlignment } from '@anticrm/ui/src/types'
|
import { PopupPosAlignment } from '@anticrm/ui/src/types'
|
||||||
import type { Preference } from '@anticrm/preference'
|
import type { Preference } from '@anticrm/preference'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
export interface KeyFilter {
|
||||||
|
key: string
|
||||||
|
component: AnyComponent
|
||||||
|
label: IntlString
|
||||||
|
icon: Asset | undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
export interface FilterMode {
|
||||||
|
label: IntlString
|
||||||
|
isAvailable: (values: any[]) => boolean
|
||||||
|
result: (values: any[]) => ObjQueryType<any>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
export interface Filter {
|
||||||
|
key: KeyFilter
|
||||||
|
mode: FilterMode
|
||||||
|
modes: FilterMode[]
|
||||||
|
value: any[]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
export interface ClassFilters extends Class<Doc> {
|
||||||
|
filters: (KeyFilter | string)[]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
export interface AttributeFilter extends Class<Type<any>> {
|
||||||
|
component: AnyComponent
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
@ -303,6 +348,8 @@ export interface ViewletPreference extends Preference {
|
|||||||
*/
|
*/
|
||||||
const view = plugin(viewId, {
|
const view = plugin(viewId, {
|
||||||
mixin: {
|
mixin: {
|
||||||
|
ClassFilters: '' as Ref<Mixin<ClassFilters>>,
|
||||||
|
AttributeFilter: '' as Ref<Mixin<AttributeFilter>>,
|
||||||
AttributeEditor: '' as Ref<Mixin<AttributeEditor>>,
|
AttributeEditor: '' as Ref<Mixin<AttributeEditor>>,
|
||||||
CollectionEditor: '' as Ref<Mixin<CollectionEditor>>,
|
CollectionEditor: '' as Ref<Mixin<CollectionEditor>>,
|
||||||
AttributePresenter: '' as Ref<Mixin<AttributePresenter>>,
|
AttributePresenter: '' as Ref<Mixin<AttributePresenter>>,
|
||||||
|
Loading…
Reference in New Issue
Block a user