mirror of
https://github.com/hcengineering/platform.git
synced 2024-12-22 19:11:33 +03:00
TSK-1404 Improve sprint filter (#3164)
Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com>
This commit is contained in:
parent
2e6181acf1
commit
bbf81ada7c
@ -1374,6 +1374,22 @@ export function createModel (builder: Builder): void {
|
|||||||
builder.mixin(tracker.class.Sprint, core.class.Class, view.mixin.ClassFilters, {
|
builder.mixin(tracker.class.Sprint, core.class.Class, view.mixin.ClassFilters, {
|
||||||
filters: []
|
filters: []
|
||||||
})
|
})
|
||||||
|
builder.mixin(tracker.class.Sprint, core.class.Class, view.mixin.ClassFilters, {
|
||||||
|
filters: ['status'],
|
||||||
|
strict: true
|
||||||
|
})
|
||||||
|
|
||||||
|
builder.mixin(tracker.class.Sprint, core.class.Class, view.mixin.AttributeFilter, {
|
||||||
|
component: tracker.component.SprintFilter
|
||||||
|
})
|
||||||
|
|
||||||
|
builder.mixin(tracker.class.TypeSprintStatus, core.class.Class, view.mixin.AttributePresenter, {
|
||||||
|
presenter: tracker.component.SprintStatusPresenter
|
||||||
|
})
|
||||||
|
|
||||||
|
builder.mixin(tracker.class.TypeSprintStatus, core.class.Class, view.mixin.AttributeFilter, {
|
||||||
|
component: view.component.ValueFilter
|
||||||
|
})
|
||||||
|
|
||||||
builder.mixin(tracker.class.Component, core.class.Class, view.mixin.ClassFilters, {
|
builder.mixin(tracker.class.Component, core.class.Class, view.mixin.ClassFilters, {
|
||||||
filters: []
|
filters: []
|
||||||
@ -1805,8 +1821,7 @@ export function createModel (builder: Builder): void {
|
|||||||
viewOptions: sprintOptions,
|
viewOptions: sprintOptions,
|
||||||
config: [
|
config: [
|
||||||
{
|
{
|
||||||
key: '',
|
key: 'status',
|
||||||
presenter: tracker.component.SprintStatusPresenter,
|
|
||||||
props: { width: '1rem', kind: 'list', size: 'small', justify: 'center' }
|
props: { width: '1rem', kind: 'list', size: 'small', justify: 'center' }
|
||||||
},
|
},
|
||||||
{ key: '', presenter: tracker.component.SprintPresenter, props: { shouldUseMargin: true } },
|
{ key: '', presenter: tracker.component.SprintPresenter, props: { shouldUseMargin: true } },
|
||||||
|
@ -52,7 +52,8 @@ export default mergeIds(trackerId, tracker, {
|
|||||||
SprintSelector: '' as AnyComponent,
|
SprintSelector: '' as AnyComponent,
|
||||||
IssueStatistics: '' as AnyComponent,
|
IssueStatistics: '' as AnyComponent,
|
||||||
TimeSpendReportPopup: '' as AnyComponent,
|
TimeSpendReportPopup: '' as AnyComponent,
|
||||||
NotificationIssuePresenter: '' as AnyComponent
|
NotificationIssuePresenter: '' as AnyComponent,
|
||||||
|
SprintFilter: '' as AnyComponent
|
||||||
},
|
},
|
||||||
app: {
|
app: {
|
||||||
Tracker: '' as Ref<Application>
|
Tracker: '' as Ref<Application>
|
||||||
|
@ -42,7 +42,7 @@ import type {
|
|||||||
IgnoreActions,
|
IgnoreActions,
|
||||||
InlineAttributEditor,
|
InlineAttributEditor,
|
||||||
KeyBinding,
|
KeyBinding,
|
||||||
KeyFilter,
|
KeyFilterPreset,
|
||||||
LinkPresenter,
|
LinkPresenter,
|
||||||
LinkProvider,
|
LinkProvider,
|
||||||
ListHeaderExtra,
|
ListHeaderExtra,
|
||||||
@ -126,7 +126,7 @@ export class TFilterMode extends TDoc implements FilterMode {
|
|||||||
|
|
||||||
@Mixin(view.mixin.ClassFilters, core.class.Class)
|
@Mixin(view.mixin.ClassFilters, core.class.Class)
|
||||||
export class TClassFilters extends TClass implements ClassFilters {
|
export class TClassFilters extends TClass implements ClassFilters {
|
||||||
filters!: (string | KeyFilter)[]
|
filters!: (string | KeyFilterPreset)[]
|
||||||
ignoreKeys?: string[] | undefined
|
ignoreKeys?: string[] | undefined
|
||||||
strict?: boolean | undefined
|
strict?: boolean | undefined
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,168 @@
|
|||||||
|
<!--
|
||||||
|
// 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 { Class, Doc, DocumentQuery, FindResult, Ref, SortingOrder } from '@hcengineering/core'
|
||||||
|
import { translate } from '@hcengineering/platform'
|
||||||
|
import presentation, { getClient } from '@hcengineering/presentation'
|
||||||
|
import { Project, Sprint, SprintStatus } from '@hcengineering/tracker'
|
||||||
|
import ui, { deviceOptionsStore, Icon, Label, CheckBox, Loading, resizeObserver } from '@hcengineering/ui'
|
||||||
|
import view, { Filter } from '@hcengineering/view'
|
||||||
|
import { createEventDispatcher, onMount } from 'svelte'
|
||||||
|
import tracker from '../../plugin'
|
||||||
|
import { sprintStatusAssets } from '../../types'
|
||||||
|
import SprintTitlePresenter from './SprintTitlePresenter.svelte'
|
||||||
|
|
||||||
|
export let _class: Ref<Class<Doc>>
|
||||||
|
export let space: Ref<Project> | undefined = undefined
|
||||||
|
export let filter: Filter
|
||||||
|
export let onChange: (e: Filter) => void
|
||||||
|
|
||||||
|
filter.modes = filter.modes ?? [view.filter.FilterObjectIn, view.filter.FilterObjectNin]
|
||||||
|
filter.mode = filter.mode ?? filter.modes[0]
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher()
|
||||||
|
let search: string = ''
|
||||||
|
let phTraslate: string = ''
|
||||||
|
let searchInput: HTMLInputElement
|
||||||
|
$: translate(presentation.string.Search, {}).then((res) => {
|
||||||
|
phTraslate = res
|
||||||
|
})
|
||||||
|
|
||||||
|
let values: Sprint[] = []
|
||||||
|
let objectsPromise: Promise<FindResult<Sprint>> | undefined = undefined
|
||||||
|
let selectedValues: Set<Ref<Sprint> | undefined | null> = new Set()
|
||||||
|
|
||||||
|
const client = getClient()
|
||||||
|
async function getValues (search: string): Promise<void> {
|
||||||
|
if (objectsPromise) {
|
||||||
|
await objectsPromise
|
||||||
|
}
|
||||||
|
values = []
|
||||||
|
selectedValues.clear()
|
||||||
|
const spaceQuery: DocumentQuery<Sprint> = space ? { space } : {}
|
||||||
|
const resultQuery: DocumentQuery<Sprint> =
|
||||||
|
search !== ''
|
||||||
|
? {
|
||||||
|
label: { $like: '%' + search + '%' }
|
||||||
|
}
|
||||||
|
: {}
|
||||||
|
objectsPromise = client.findAll(
|
||||||
|
tracker.class.Sprint,
|
||||||
|
{ ...resultQuery, ...spaceQuery },
|
||||||
|
{
|
||||||
|
sort: { label: SortingOrder.Ascending }
|
||||||
|
}
|
||||||
|
)
|
||||||
|
const res = await objectsPromise
|
||||||
|
|
||||||
|
values = res
|
||||||
|
selectedValues = new Set(res.map((p) => p._id).filter((p) => filter.value.includes(p)))
|
||||||
|
if (filter.value.includes(undefined) || filter.value.includes(null)) {
|
||||||
|
selectedValues.add(undefined)
|
||||||
|
}
|
||||||
|
objectsPromise = undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSelected (value: Ref<Sprint> | undefined, values: Set<Ref<Sprint> | undefined | null>): boolean {
|
||||||
|
return values.has(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggle (value: Ref<Sprint> | undefined): void {
|
||||||
|
if (isSelected(value, selectedValues)) {
|
||||||
|
selectedValues.delete(value)
|
||||||
|
} else {
|
||||||
|
selectedValues.add(value)
|
||||||
|
}
|
||||||
|
selectedValues = selectedValues
|
||||||
|
filter.value = Array.from(selectedValues)
|
||||||
|
onChange(filter)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStatusItem (status: SprintStatus, docs: Sprint[]): Sprint[] {
|
||||||
|
return docs.filter((p) => p.status === status)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStatuses (): SprintStatus[] {
|
||||||
|
const res = Array.from(Object.values(SprintStatus).filter((v) => !isNaN(Number(v)))) as SprintStatus[]
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
if (searchInput && !$deviceOptionsStore.isMobile) searchInput.focus()
|
||||||
|
})
|
||||||
|
getValues(search)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="selectPopup" use:resizeObserver={() => dispatch('changeContent')}>
|
||||||
|
<div class="header">
|
||||||
|
<input
|
||||||
|
bind:this={searchInput}
|
||||||
|
type="text"
|
||||||
|
bind:value={search}
|
||||||
|
on:change={() => {
|
||||||
|
getValues(search)
|
||||||
|
}}
|
||||||
|
placeholder={phTraslate}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="scroll">
|
||||||
|
<div class="box">
|
||||||
|
{#if objectsPromise}
|
||||||
|
<Loading />
|
||||||
|
{:else}
|
||||||
|
<button
|
||||||
|
class="menu-item"
|
||||||
|
on:click={() => {
|
||||||
|
toggle(undefined)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div class="flex clear-mins">
|
||||||
|
<div class="check pointer-events-none">
|
||||||
|
<CheckBox checked={isSelected(undefined, selectedValues)} primary />
|
||||||
|
</div>
|
||||||
|
<Label label={ui.string.NotSelected} />
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
{#each getStatuses() as group}
|
||||||
|
{@const status = sprintStatusAssets[group]}
|
||||||
|
{@const items = getStatusItem(group, values)}
|
||||||
|
{#if items.length > 0}
|
||||||
|
<div class="flex-row-center p-1">
|
||||||
|
<Icon icon={status.icon} size={'small'} />
|
||||||
|
<div class="ml-2">
|
||||||
|
<Label label={status.label} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{#each items as doc}
|
||||||
|
<button
|
||||||
|
class="menu-item"
|
||||||
|
on:click={() => {
|
||||||
|
toggle(doc._id)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div class="flex clear-mins">
|
||||||
|
<div class="check pointer-events-none">
|
||||||
|
<CheckBox checked={isSelected(doc._id, selectedValues)} primary />
|
||||||
|
</div>
|
||||||
|
<SprintTitlePresenter value={doc} />
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -20,7 +20,7 @@
|
|||||||
|
|
||||||
export let value: WithLookup<Sprint>
|
export let value: WithLookup<Sprint>
|
||||||
export let shouldShowAvatar: boolean = true
|
export let shouldShowAvatar: boolean = true
|
||||||
export let onClick: () => void | undefined
|
export let onClick: (() => void) | undefined = undefined
|
||||||
export let disabled = false
|
export let disabled = false
|
||||||
export let inline: boolean = false
|
export let inline: boolean = false
|
||||||
|
|
||||||
|
@ -13,42 +13,29 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { getClient } from '@hcengineering/presentation'
|
import { SprintStatus } from '@hcengineering/tracker'
|
||||||
import { Sprint, SprintStatus } from '@hcengineering/tracker'
|
|
||||||
import type { ButtonKind, ButtonSize } from '@hcengineering/ui'
|
import type { ButtonKind, ButtonSize } from '@hcengineering/ui'
|
||||||
import tracker from '../../plugin'
|
import tracker from '../../plugin'
|
||||||
|
|
||||||
import SprintStatusSelector from './SprintStatusSelector.svelte'
|
import SprintStatusSelector from './SprintStatusSelector.svelte'
|
||||||
|
|
||||||
export let value: Sprint
|
export let value: SprintStatus
|
||||||
export let isEditable: boolean = true
|
export let onChange: ((value: SprintStatus | undefined) => void) | undefined = undefined
|
||||||
export let shouldShowLabel: boolean = false
|
|
||||||
export let kind: ButtonKind = 'link'
|
export let kind: ButtonKind = 'link'
|
||||||
export let size: ButtonSize = 'large'
|
export let size: ButtonSize = 'large'
|
||||||
export let justify: 'left' | 'center' = 'left'
|
export let justify: 'left' | 'center' = 'left'
|
||||||
export let width: string | undefined = '100%'
|
export let width: string | undefined = '100%'
|
||||||
|
|
||||||
const client = getClient()
|
$: isEditable = onChange !== undefined
|
||||||
|
|
||||||
const handleComponentStatusChanged = async (newStatus: SprintStatus | undefined) => {
|
|
||||||
if (!isEditable || newStatus === undefined || value.status === newStatus) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
await client.update(value, { status: newStatus })
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if value}
|
<SprintStatusSelector
|
||||||
<SprintStatusSelector
|
{kind}
|
||||||
{kind}
|
{size}
|
||||||
{size}
|
{width}
|
||||||
{width}
|
{justify}
|
||||||
{justify}
|
{isEditable}
|
||||||
{isEditable}
|
showTooltip={isEditable ? { label: tracker.string.SetStatus } : undefined}
|
||||||
{shouldShowLabel}
|
selectedSprintStatus={value}
|
||||||
showTooltip={isEditable ? { label: tracker.string.SetStatus } : undefined}
|
onSprintStatusChange={onChange}
|
||||||
selectedSprintStatus={value.status}
|
/>
|
||||||
onSprintStatusChange={handleComponentStatusChanged}
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
|
@ -136,6 +136,7 @@ import ProjectPresenter from './components/projects/ProjectPresenter.svelte'
|
|||||||
import ProjectSpacePresenter from './components/projects/ProjectSpacePresenter.svelte'
|
import ProjectSpacePresenter from './components/projects/ProjectSpacePresenter.svelte'
|
||||||
import IssueStatistics from './components/sprints/IssueStatistics.svelte'
|
import IssueStatistics from './components/sprints/IssueStatistics.svelte'
|
||||||
import SprintRefPresenter from './components/sprints/SprintRefPresenter.svelte'
|
import SprintRefPresenter from './components/sprints/SprintRefPresenter.svelte'
|
||||||
|
import SprintFilter from './components/sprints/SprintFilter.svelte'
|
||||||
|
|
||||||
export { default as SubIssueList } from './components/issues/edit/SubIssueList.svelte'
|
export { default as SubIssueList } from './components/issues/edit/SubIssueList.svelte'
|
||||||
|
|
||||||
@ -445,7 +446,8 @@ export default async (): Promise<Resources> => ({
|
|||||||
TimeSpendReportPopup,
|
TimeSpendReportPopup,
|
||||||
SprintDatePresenter,
|
SprintDatePresenter,
|
||||||
SprintLeadPresenter,
|
SprintLeadPresenter,
|
||||||
NotificationIssuePresenter
|
NotificationIssuePresenter,
|
||||||
|
SprintFilter
|
||||||
},
|
},
|
||||||
completion: {
|
completion: {
|
||||||
IssueQuery: async (client: Client, query: string, filter?: { in?: RelatedDocument[], nin?: RelatedDocument[] }) =>
|
IssueQuery: async (client: Client, query: string, filter?: { in?: RelatedDocument[], nin?: RelatedDocument[] }) =>
|
||||||
|
@ -10,6 +10,7 @@
|
|||||||
"lint": "svelte-check && eslint",
|
"lint": "svelte-check && eslint",
|
||||||
"lint:fix": "eslint --fix src",
|
"lint:fix": "eslint --fix src",
|
||||||
"format": "prettier --write --plugin-search-dir=. src && eslint --fix src",
|
"format": "prettier --write --plugin-search-dir=. src && eslint --fix src",
|
||||||
|
"svelte-check": "svelte-check --output human",
|
||||||
"build:watch": "tsc --incremental --noEmit --outDir ./dist_cache"
|
"build:watch": "tsc --incremental --noEmit --outDir ./dist_cache"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
@ -25,7 +25,7 @@
|
|||||||
showPopup,
|
showPopup,
|
||||||
Submenu
|
Submenu
|
||||||
} from '@hcengineering/ui'
|
} from '@hcengineering/ui'
|
||||||
import { ClassFilters, Filter, KeyFilter } from '@hcengineering/view'
|
import { ClassFilters, Filter, KeyFilter, KeyFilterPreset } from '@hcengineering/view'
|
||||||
import { createEventDispatcher } from 'svelte'
|
import { createEventDispatcher } from 'svelte'
|
||||||
import { buildFilterKey, FilterQuery } from '../../filter'
|
import { buildFilterKey, FilterQuery } from '../../filter'
|
||||||
import view from '../../plugin'
|
import view from '../../plugin'
|
||||||
@ -36,6 +36,7 @@
|
|||||||
export let filter: Filter | undefined
|
export let filter: Filter | undefined
|
||||||
export let index: number
|
export let index: number
|
||||||
export let onChange: (e: Filter) => void
|
export let onChange: (e: Filter) => void
|
||||||
|
export let nestedFrom: KeyFilter | undefined = undefined
|
||||||
|
|
||||||
const client = getClient()
|
const client = getClient()
|
||||||
const hierarchy = client.getHierarchy()
|
const hierarchy = client.getHierarchy()
|
||||||
@ -43,7 +44,7 @@
|
|||||||
function getFilters (_class: Ref<Class<Doc>>, mixin: ClassFilters): KeyFilter[] {
|
function getFilters (_class: Ref<Class<Doc>>, mixin: ClassFilters): KeyFilter[] {
|
||||||
if (mixin.filters === undefined) return []
|
if (mixin.filters === undefined) return []
|
||||||
const filters = mixin.filters.map((p) => {
|
const filters = mixin.filters.map((p) => {
|
||||||
return typeof p === 'string' ? buildFilterFromKey(_class, p) : p
|
return typeof p === 'string' ? buildFilterFromKey(_class, p) : buildFilterFromPreset(p)
|
||||||
})
|
})
|
||||||
const result: KeyFilter[] = []
|
const result: KeyFilter[] = []
|
||||||
for (const filter of filters) {
|
for (const filter of filters) {
|
||||||
@ -52,6 +53,19 @@
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildFilterFromPreset (p: KeyFilterPreset): KeyFilter | undefined {
|
||||||
|
if (p.key !== '') {
|
||||||
|
const attribute = hierarchy.getAttribute(p._class, p.key)
|
||||||
|
const clazz = hierarchy.getClass(p._class)
|
||||||
|
return {
|
||||||
|
...p,
|
||||||
|
attribute,
|
||||||
|
label: p.label ?? attribute.label,
|
||||||
|
icon: p.icon ?? attribute.icon ?? clazz.icon ?? view.icon.Setting
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function buildFilterFromKey (_class: Ref<Class<Doc>>, key: string): KeyFilter | undefined {
|
function buildFilterFromKey (_class: Ref<Class<Doc>>, key: string): KeyFilter | undefined {
|
||||||
const attribute = hierarchy.getAttribute(_class, key)
|
const attribute = hierarchy.getAttribute(_class, key)
|
||||||
return buildFilterKey(hierarchy, _class, key, attribute)
|
return buildFilterKey(hierarchy, _class, key, attribute)
|
||||||
@ -69,7 +83,7 @@
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
const value = getValue(attribute.name, attribute.type)
|
const value = getValue(attribute.name, attribute.type)
|
||||||
if (result.findIndex((p) => p.attribute.name === value) !== -1) {
|
if (result.findIndex((p) => p.attribute?.name === value) !== -1) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const filter = buildFilterKey(hierarchy, _class, value, attribute)
|
const filter = buildFilterKey(hierarchy, _class, value, attribute)
|
||||||
@ -93,7 +107,20 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTypes (_class: Ref<Class<Doc>>): KeyFilter[] {
|
function getTypes (_class: Ref<Class<Doc>>, nestedFrom: KeyFilter | undefined): KeyFilter[] {
|
||||||
|
if (nestedFrom !== undefined) {
|
||||||
|
return getNestedTypes(nestedFrom)
|
||||||
|
} else {
|
||||||
|
return getOwnTypes(_class)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getNestedTypes (type: KeyFilter): KeyFilter[] {
|
||||||
|
const targetClass = (hierarchy.getAttribute(type._class, type.key).type as RefTo<Doc>).to
|
||||||
|
return getOwnTypes(targetClass)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getOwnTypes (_class: Ref<Class<Doc>>): KeyFilter[] {
|
||||||
const clazz = hierarchy.getClass(_class)
|
const clazz = hierarchy.getClass(_class)
|
||||||
const mixin = hierarchy.as(clazz, view.mixin.ClassFilters)
|
const mixin = hierarchy.as(clazz, view.mixin.ClassFilters)
|
||||||
const result = getFilters(_class, mixin)
|
const result = getFilters(_class, mixin)
|
||||||
@ -159,24 +186,48 @@
|
|||||||
closePopup()
|
closePopup()
|
||||||
closeTooltip()
|
closeTooltip()
|
||||||
|
|
||||||
showPopup(
|
if (nestedFrom !== undefined && type !== nestedFrom) {
|
||||||
type.component,
|
const change = (e: Filter | undefined) => {
|
||||||
{
|
if (nestedFrom) {
|
||||||
_class,
|
setNestedFilter(nestedFrom, e)
|
||||||
space,
|
}
|
||||||
filter: filter || {
|
}
|
||||||
key: type,
|
const targetClass = (hierarchy.getAttribute(nestedFrom._class, nestedFrom.key).type as RefTo<Doc>).to
|
||||||
value: [],
|
showPopup(
|
||||||
index
|
type.component,
|
||||||
|
{
|
||||||
|
_class: targetClass,
|
||||||
|
space,
|
||||||
|
filter: filter || {
|
||||||
|
key: type,
|
||||||
|
value: [],
|
||||||
|
index
|
||||||
|
},
|
||||||
|
onChange: change
|
||||||
},
|
},
|
||||||
onChange
|
target
|
||||||
},
|
)
|
||||||
target
|
} else {
|
||||||
)
|
showPopup(
|
||||||
|
type.component,
|
||||||
|
{
|
||||||
|
_class,
|
||||||
|
space,
|
||||||
|
filter: filter || {
|
||||||
|
key: type,
|
||||||
|
value: [],
|
||||||
|
index
|
||||||
|
},
|
||||||
|
onChange
|
||||||
|
},
|
||||||
|
target
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasNested (type: KeyFilter): boolean {
|
function hasNested (type: KeyFilter): boolean {
|
||||||
const targetClass = (hierarchy.getAttribute(type._class, type.key).type as RefTo<Doc>).to
|
const targetClass = (hierarchy.getAttribute(type._class, type.key).type as RefTo<Doc>).to
|
||||||
|
if (targetClass === undefined) return false
|
||||||
const clazz = hierarchy.getClass(targetClass)
|
const clazz = hierarchy.getClass(targetClass)
|
||||||
return hierarchy.hasMixin(clazz, view.mixin.ClassFilters)
|
return hierarchy.hasMixin(clazz, view.mixin.ClassFilters)
|
||||||
}
|
}
|
||||||
@ -199,38 +250,51 @@
|
|||||||
dispatch('close')
|
dispatch('close')
|
||||||
}
|
}
|
||||||
|
|
||||||
function getNestedProps (type: KeyFilter): any {
|
|
||||||
const targetClass = (hierarchy.getAttribute(type._class, type.key).type as RefTo<Doc>).to
|
|
||||||
return {
|
|
||||||
_class: targetClass,
|
|
||||||
space,
|
|
||||||
index,
|
|
||||||
target,
|
|
||||||
onChange: (e: Filter | undefined) => {
|
|
||||||
setNestedFilter(type, e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const elements: HTMLElement[] = []
|
const elements: HTMLElement[] = []
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="selectPopup" use:resizeObserver={() => dispatch('changeContent')}>
|
<div class="selectPopup" use:resizeObserver={() => dispatch('changeContent')}>
|
||||||
<Scroller>
|
<Scroller>
|
||||||
{#each getTypes(_class) as type, i}
|
{#if nestedFrom}
|
||||||
{#if filter === undefined && type.component === view.component.ObjectFilter && hasNested(type)}
|
<!-- svelte-ignore a11y-mouse-events-have-key-events -->
|
||||||
|
<button
|
||||||
|
class="menu-item withIcon"
|
||||||
|
on:keydown={(event) => keyDown(event, -1)}
|
||||||
|
on:mouseover={(event) => {
|
||||||
|
event.currentTarget.focus()
|
||||||
|
}}
|
||||||
|
on:click={() => {
|
||||||
|
if (nestedFrom) {
|
||||||
|
click(nestedFrom)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div class="icon mr-3">
|
||||||
|
{#if nestedFrom.icon}
|
||||||
|
<Icon icon={nestedFrom.icon} size={'small'} />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<div class="overflow-label pr-1"><Label label={nestedFrom.label} /></div>
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
{#each getTypes(_class, nestedFrom) as type, i}
|
||||||
|
{#if filter === undefined && hasNested(type)}
|
||||||
<Submenu
|
<Submenu
|
||||||
bind:element={elements[i]}
|
bind:element={elements[i]}
|
||||||
on:keydown={(event) => keyDown(event, i)}
|
on:keydown={(event) => keyDown(event, i)}
|
||||||
on:mouseover={() => {
|
on:mouseover={() => {
|
||||||
elements[i]?.focus()
|
elements[i]?.focus()
|
||||||
}}
|
}}
|
||||||
on:click={() => {
|
|
||||||
click(type)
|
|
||||||
}}
|
|
||||||
icon={type.icon}
|
icon={type.icon}
|
||||||
label={type.label}
|
label={type.label}
|
||||||
props={getNestedProps(type)}
|
props={{
|
||||||
|
_class,
|
||||||
|
space,
|
||||||
|
index,
|
||||||
|
target,
|
||||||
|
onChange,
|
||||||
|
nestedFrom: type
|
||||||
|
}}
|
||||||
options={{ component: view.component.FilterTypePopup }}
|
options={{ component: view.component.FilterTypePopup }}
|
||||||
withHover
|
withHover
|
||||||
/>
|
/>
|
||||||
|
@ -7,7 +7,8 @@ import core, {
|
|||||||
FindResult,
|
FindResult,
|
||||||
Hierarchy,
|
Hierarchy,
|
||||||
ObjQueryType,
|
ObjQueryType,
|
||||||
Ref
|
Ref,
|
||||||
|
RefTo
|
||||||
} from '@hcengineering/core'
|
} from '@hcengineering/core'
|
||||||
import { getResource } from '@hcengineering/platform'
|
import { getResource } from '@hcengineering/platform'
|
||||||
import { LiveQuery, createQuery, getClient } from '@hcengineering/presentation'
|
import { LiveQuery, createQuery, getClient } from '@hcengineering/presentation'
|
||||||
@ -198,12 +199,28 @@ export function buildFilterKey (
|
|||||||
key: string,
|
key: string,
|
||||||
attribute: AnyAttribute
|
attribute: AnyAttribute
|
||||||
): KeyFilter | undefined {
|
): KeyFilter | undefined {
|
||||||
|
const attrOf = hierarchy.getClass(attribute.attributeOf)
|
||||||
|
const isRef = hierarchy.isDerived(attribute.type._class, core.class.RefTo)
|
||||||
|
if (isRef) {
|
||||||
|
const targetClass = (attribute.type as RefTo<Doc>).to
|
||||||
|
const filter = hierarchy.classHierarchyMixin(targetClass, view.mixin.AttributeFilter)
|
||||||
|
if (filter?.component !== undefined) {
|
||||||
|
return {
|
||||||
|
_class,
|
||||||
|
key,
|
||||||
|
attribute,
|
||||||
|
label: attribute.label,
|
||||||
|
icon: attribute.icon ?? filter.icon ?? attrOf.icon ?? view.icon.Setting,
|
||||||
|
component: filter.component
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const isCollection = hierarchy.isDerived(attribute.type._class, core.class.Collection)
|
const isCollection = hierarchy.isDerived(attribute.type._class, core.class.Collection)
|
||||||
const targetClass = isCollection ? (attribute.type as Collection<AttachedDoc>).of : attribute.type._class
|
const targetClass = isCollection ? (attribute.type as Collection<AttachedDoc>).of : attribute.type._class
|
||||||
const clazz = hierarchy.getClass(targetClass)
|
const clazz = hierarchy.getClass(targetClass)
|
||||||
const filter = hierarchy.as(clazz, view.mixin.AttributeFilter)
|
const filter = hierarchy.as(clazz, view.mixin.AttributeFilter)
|
||||||
|
|
||||||
const attrOf = hierarchy.getClass(attribute.attributeOf)
|
|
||||||
if (filter.component === undefined) return undefined
|
if (filter.component === undefined) return undefined
|
||||||
return {
|
return {
|
||||||
_class,
|
_class,
|
||||||
|
@ -49,9 +49,19 @@ import type {
|
|||||||
/**
|
/**
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
export interface KeyFilter {
|
export interface KeyFilterPreset {
|
||||||
_class: Ref<Class<Doc>>
|
_class: Ref<Class<Doc>>
|
||||||
key: string
|
key: string
|
||||||
|
attribute?: AnyAttribute
|
||||||
|
component: AnyComponent
|
||||||
|
label?: IntlString
|
||||||
|
icon?: Asset | AnySvelteComponent | undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
export interface KeyFilter extends KeyFilterPreset {
|
||||||
attribute: AnyAttribute
|
attribute: AnyAttribute
|
||||||
component: AnyComponent
|
component: AnyComponent
|
||||||
label: IntlString
|
label: IntlString
|
||||||
@ -102,7 +112,7 @@ export interface FilteredView extends Preference {
|
|||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
export interface ClassFilters extends Class<Doc> {
|
export interface ClassFilters extends Class<Doc> {
|
||||||
filters: (KeyFilter | string)[]
|
filters: (KeyFilterPreset | string)[]
|
||||||
ignoreKeys?: string[]
|
ignoreKeys?: string[]
|
||||||
|
|
||||||
// Ignore attributes not specified in the "filters" array
|
// Ignore attributes not specified in the "filters" array
|
||||||
|
Loading…
Reference in New Issue
Block a user