Filters improve (#2572)

Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com>
This commit is contained in:
Denis Bykhov 2023-01-31 23:18:07 +06:00 committed by GitHub
parent 67e86538bb
commit f7e220d0f2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 263 additions and 98 deletions

View File

@ -35,6 +35,7 @@ import { DOMAIN_MODEL, IndexKind } from '@hcengineering/core'
import {
Builder,
Collection,
Hidden,
Index,
Model,
Prop,
@ -76,6 +77,7 @@ export class TChannelProvider extends TDoc implements ChannelProvider {
export class TContact extends TDoc implements Contact {
@Prop(TypeString(), contact.string.Name)
@Index(IndexKind.FullText)
@Hidden()
name!: string
avatar?: string | null

View File

@ -70,17 +70,21 @@ import core from './component'
export class TObj implements Obj {
@Prop(TypeRef(core.class.Class), core.string.ClassLabel)
@Index(IndexKind.Indexed)
@Hidden()
_class!: Ref<Class<this>>
}
@Model(core.class.Doc, core.class.Obj)
@UX(core.string.Object)
export class TDoc extends TObj implements Doc {
@Prop(TypeRef(core.class.Doc), core.string.Id)
@Hidden()
// @Index(IndexKind.Indexed) // - automatically indexed by default.
_id!: Ref<this>
@Prop(TypeRef(core.class.Space), core.string.Space)
@Index(IndexKind.Indexed)
@Hidden()
space!: Ref<Space>
@Prop(TypeTimestamp(), core.string.Modified)
@ -94,10 +98,12 @@ export class TDoc extends TObj implements Doc {
export class TAttachedDoc extends TDoc implements AttachedDoc {
@Prop(TypeRef(core.class.Doc), core.string.AttachedTo)
@Index(IndexKind.Indexed)
@Hidden()
attachedTo!: Ref<Doc>
@Prop(TypeRef(core.class.Class), core.string.AttachedToClass)
@Index(IndexKind.Indexed)
@Hidden()
attachedToClass!: Ref<Class<Doc>>
@Prop(TypeString(), core.string.Collection)

View File

@ -16,7 +16,6 @@
import type { Employee, Organization } from '@hcengineering/contact'
import { Doc, FindOptions, IndexKind, Lookup, Ref, Timestamp } from '@hcengineering/core'
import {
ArrOf,
Builder,
Collection,
Index,
@ -125,7 +124,7 @@ export class TCandidate extends TPerson implements Candidate {
@Mixin(recruit.mixin.VacancyList, contact.class.Organization)
@UX(recruit.string.VacancyList, recruit.icon.RecruitApplication, undefined, 'name')
export class TVacancyList extends TOrganization implements VacancyList {
@Prop(ArrOf(TypeRef(recruit.class.Vacancy)), recruit.string.Vacancies)
@Prop(Collection(recruit.class.Vacancy), recruit.string.Vacancies)
vacancies!: number
}
@ -192,7 +191,7 @@ export function createModel (builder: Builder): void {
editor: recruit.component.Applications
})
builder.mixin(recruit.class.Vacancy, core.class.Class, view.mixin.ArrayEditor, {
builder.mixin(recruit.class.Vacancy, core.class.Class, view.mixin.CollectionEditor, {
editor: recruit.component.VacancyList
})

View File

@ -121,6 +121,7 @@ export class TTask extends TAttachedDoc implements Task {
@Prop(TypeString(), task.string.TaskNumber)
@Index(IndexKind.FullText)
@Hidden()
number!: number
// @Prop(TypeRef(contact.class.Employee), task.string.TaskAssignee)

View File

@ -512,7 +512,7 @@ export function createModel (builder: Builder): void {
)
builder.mixin(core.class.Space, core.class.Class, view.mixin.AttributePresenter, {
presenter: view.component.SpacePresenter
presenter: view.component.SpaceRefPresenter
})
// Selection stuff

View File

@ -64,7 +64,8 @@ export default mergeIds(viewId, view, {
MarkupEditor: '' as AnyComponent,
MarkupEditorPopup: '' as AnyComponent,
ListView: '' as AnyComponent,
IndexedDocumentPreview: '' as AnyComponent
IndexedDocumentPreview: '' as AnyComponent,
SpaceRefPresenter: '' as AnyComponent
},
string: {
Table: '' as IntlString,

View File

@ -150,6 +150,7 @@ export default plugin(coreId, {
Name: '' as IntlString,
Enum: '' as IntlString,
Description: '' as IntlString,
Hyperlink: '' as IntlString
Hyperlink: '' as IntlString,
Object: '' as IntlString
}
})

View File

@ -394,6 +394,34 @@ export class Hierarchy {
return result
}
getOwnAttributes (clazz: Ref<Classifier>): Map<string, AnyAttribute> {
const result = new Map<string, AnyAttribute>()
const attributes = this.attributes.get(clazz)
if (attributes !== undefined) {
for (const [name, attr] of attributes) {
result.set(name, attr)
}
}
return result
}
getParentClass (_class: Ref<Class<Obj>>): Ref<Class<Obj>> {
const baseDomain = this.getDomain(_class)
const ancestors = this.getAncestors(_class)
let result: Ref<Class<Obj>> = _class
for (const ancestor of ancestors) {
try {
const domain = this.getClass(ancestor).domain
if (domain === baseDomain) {
result = ancestor
}
} catch {}
}
return result
}
getAttribute (classifier: Ref<Classifier>, name: string): AnyAttribute {
const attr = this.findAttribute(classifier, name)
if (attr === undefined) {

View File

@ -25,6 +25,7 @@
"Array": "Array",
"Enum": "Enum",
"Members": "Members",
"Hyperlink": "URL"
"Hyperlink": "URL",
"Object": "Object"
}
}

View File

@ -25,6 +25,7 @@
"Array": "Массив",
"Enum": "Справочник",
"Members": "Участники",
"Hyperlink": "URL"
"Hyperlink": "URL",
"Object": "Объект"
}
}

View File

@ -148,18 +148,9 @@
transition-duration: 0.15s;
.btn-icon {
color: var(--content-color);
transition: color 0.15s;
pointer-events: none;
}
&:hover {
color: var(--accent-color);
transition-duration: 0;
.btn-icon {
color: var(--caption-color);
}
}
&.disabled {
color: rgb(var(--caption-color) / 40%);
@ -167,6 +158,14 @@
opacity: 0.5;
}
}
&:hover {
color: var(--caption-color);
transition-duration: 0;
.btn-icon {
color: var(--caption-color);
}
}
&.jf-left {
justify-content: flex-start;

View File

@ -20,6 +20,7 @@
import view, { Viewlet, ViewletPreference } from '@hcengineering/view'
import {
ActionContext,
FilterBar,
FilterButton,
getViewOptions,
setActiveViewletId,
@ -31,10 +32,11 @@
import { deviceOptionsStore as deviceInfo } from '@hcengineering/ui'
let search = ''
let resultQuery: DocumentQuery<Doc> = {}
let searchQuery: DocumentQuery<Doc> = {}
let resultQuery: DocumentQuery<Doc> = searchQuery
function updateResultQuery (search: string): void {
resultQuery = search === '' ? {} : { $search: search }
searchQuery = search === '' ? {} : { $search: search }
}
let viewlet: Viewlet | undefined
@ -109,6 +111,13 @@
</div>
</div>
<FilterBar
_class={contact.class.Contact}
{viewOptions}
query={searchQuery}
on:change={(e) => (resultQuery = e.detail)}
/>
{#if viewlet}
{#if loading}
<Loading />

View File

@ -19,6 +19,7 @@
import { Button, Icon, IconAdd, Label, Loading, SearchEdit, showPopup } from '@hcengineering/ui'
import view, { BuildModelKey, Viewlet, ViewletPreference } from '@hcengineering/view'
import {
FilterBar,
FilterButton,
getViewOptions,
setActiveViewletId,
@ -30,9 +31,10 @@
import { deviceOptionsStore as deviceInfo } from '@hcengineering/ui'
let search: string = ''
let searchQuery: DocumentQuery<Doc> = {}
let resultQuery: DocumentQuery<Doc> = {}
$: resultQuery = search === '' ? {} : { $search: search }
$: searchQuery = search === '' ? {} : { $search: search }
type ApplicationInfo = { count: number; modifiedOn: number }
let applications: Map<Ref<Vacancy>, ApplicationInfo> = new Map<Ref<Vacancy>, ApplicationInfo>()
@ -169,6 +171,13 @@
</div>
</div>
<FilterBar
_class={recruit.class.Vacancy}
{viewOptions}
query={searchQuery}
on:change={(e) => (resultQuery = e.detail)}
/>
{#if descr}
{#if loading}
<Loading />

View File

@ -123,8 +123,8 @@
}
}
const filtredKeys = Array.from(keysMap.values())
const { attributes, collections } = categorizeFields(hierarchy, filtredKeys, collectionArrays, allowedCollections)
keys = attributes.map((it) => it.key)
const editors: { key: KeyedAttribute; editor: AnyComponent; category: AttributeCategory }[] = []
@ -171,7 +171,7 @@
$: getEditorOrDefault(realObjectClass, showAllMixins, _id)
function getEditorOrDefault (_class: Ref<Class<Doc>>, showAllMixins: boolean, _id: Ref<Doc>): void {
parentClass = getParentClass(_class)
parentClass = hierarchy.getParentClass(_class)
mainEditor = getEditor(_class)
updateKeys(showAllMixins)
}
@ -210,21 +210,6 @@
$: icon = getIcon(realObjectClass)
function getParentClass (_class: Ref<Class<Doc>>): Ref<Class<Doc>> {
const baseDomain = hierarchy.getDomain(_class)
const ancestors = hierarchy.getAncestors(_class)
let result: Ref<Class<Doc>> = _class
for (const ancestor of ancestors) {
try {
const domain = hierarchy.getClass(ancestor).domain
if (domain === baseDomain) {
result = ancestor
}
} catch {}
}
return result
}
let title: string = ''
$: if (object !== undefined) {

View File

@ -34,7 +34,7 @@
_id: type.of
},
(res) => {
items = res[0].enumValues.map((p) => {
items = res[0]?.enumValues?.map((p) => {
return { id: p, label: p }
})
},

View File

@ -0,0 +1,22 @@
<!--
// 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, { Ref, Space } from '@hcengineering/core'
import { ObjectPresenter } from '..'
export let value: Ref<Space>
</script>
<ObjectPresenter objectId={value} _class={core.class.Space} />

View File

@ -14,10 +14,17 @@
-->
<script lang="ts">
import core, { AnyAttribute, ArrOf, Class, Doc, Ref, Type } from '@hcengineering/core'
import { IntlString } from '@hcengineering/platform'
import { Asset, IntlString } from '@hcengineering/platform'
import preferencePlugin from '@hcengineering/preference'
import presentation, { Card, createQuery, getAttributePresenterClass, getClient } from '@hcengineering/presentation'
import { Button, getPlatformColorForText, ToggleButton } from '@hcengineering/ui'
import {
Button,
getEventPositionElement,
getPlatformColorForText,
SelectPopup,
showPopup,
ToggleButton
} from '@hcengineering/ui'
import { BuildModelKey, Viewlet, ViewletPreference } from '@hcengineering/view'
import { deepEqual } from 'fast-equals'
import { createEventDispatcher } from 'svelte'
@ -55,14 +62,16 @@
label: IntlString
value: string | BuildModelKey
_class: Ref<Class<Doc>>
icon: Asset | undefined
}
function getObjectConfig (_class: Ref<Class<Doc>>, param: string): AttributeConfig {
const clazz = client.getHierarchy().getClass(_class)
const clazz = hierarchy.getClass(_class)
return {
value: param,
label: clazz.label,
enabled: true,
icon: clazz.icon,
_class
}
}
@ -70,6 +79,7 @@
function getBaseConfig (viewlet: Viewlet): AttributeConfig[] {
const lookup = buildConfigLookup(hierarchy, viewlet.attachTo, viewlet.config, viewlet.options?.lookup)
const result: AttributeConfig[] = []
const clazz = hierarchy.getClass(viewlet.attachTo)
for (const param of viewlet.config) {
if (typeof param === 'string') {
if (param.length === 0) {
@ -79,7 +89,8 @@
value: param,
enabled: true,
label: getKeyLabel(client, viewlet.attachTo, param, lookup),
_class: viewlet.attachTo
_class: viewlet.attachTo,
icon: clazz.icon
})
}
} else {
@ -87,7 +98,8 @@
value: param,
label: param.label as IntlString,
enabled: true,
_class: viewlet.attachTo
_class: viewlet.attachTo,
icon: clazz.icon
})
}
}
@ -126,13 +138,15 @@
parent = pclazz.extends
}
if (presenter === undefined) return
const clazz = hierarchy.getClass(attribute.attributeOf)
if (useMixinProxy) {
const newValue = {
value: attribute.attributeOf + '.' + attribute.name,
label: attribute.label,
enabled: false,
_class: attribute.attributeOf
_class: attribute.attributeOf,
icon: clazz.icon
}
if (!isExist(result, newValue)) {
result.push(newValue)
@ -142,7 +156,8 @@
value,
label: attribute.label,
enabled: false,
_class: attribute.attributeOf
_class: attribute.attributeOf,
icon: clazz.icon
}
if (!isExist(result, newValue)) {
result.push(newValue)
@ -169,11 +184,21 @@
}
hierarchy.getDescendants(viewlet.attachTo).forEach((it) => {
const ancestor = hierarchy.getAncestors(it)[1]
hierarchy.getAllAttributes(it, ancestor).forEach((attr) => {
if (attr.isCustom === true) {
processAttribute(attr, result, true)
}
hierarchy.getOwnAttributes(it).forEach((attr) => {
processAttribute(attr, result, true)
})
})
const ancestors = new Set(hierarchy.getAncestors(viewlet.attachTo))
const parent = hierarchy.getParentClass(viewlet.attachTo)
const parentMixins = hierarchy
.getDescendants(parent)
.map((p) => hierarchy.getClass(p))
.filter((p) => hierarchy.isMixin(p._id) && p.extends && ancestors.has(p.extends))
parentMixins.forEach((it) => {
hierarchy.getOwnAttributes(it._id).forEach((attr) => {
processAttribute(attr, result, true)
})
})
@ -252,6 +277,24 @@
const color = getPlatformColorForText(attribute._class)
return `border: 1px solid ${color + (attribute.enabled ? 'ff' : 'cc')};`
}
function groupByClasses (attributes: AttributeConfig[]): Map<Ref<Class<Doc>>, AttributeConfig[]> {
const res = new Map()
for (const attribute of attributes) {
if (attribute.enabled) continue
const arr = res.get(attribute._class) ?? []
arr.push(attribute)
res.set(attribute._class, arr)
}
return res
}
$: enabled = attributes.filter((p) => p.enabled)
$: classes = groupByClasses(attributes)
function getClassLabel (_class: Ref<Class<Doc>>): IntlString {
return hierarchy.getClass(_class).label
}
</script>
<Card
@ -264,7 +307,7 @@
}}
>
<div class="flex-row-stretch flex-wrap">
{#each attributes as attribute, i}
{#each enabled as attribute, i}
<div
class="m-0-5 border-radius-1 overflow-label"
style={getStyle(attribute)}
@ -281,7 +324,41 @@
selected = undefined
}}
>
<ToggleButton backgroundColor={getColor(attribute)} label={attribute.label} bind:value={attribute.enabled} />
<ToggleButton
backgroundColor={getColor(attribute)}
icon={attribute.icon}
label={attribute.label}
bind:value={attribute.enabled}
/>
</div>
{/each}
</div>
<div class="flex-row-stretch flex-wrap">
{#each Array.from(classes.keys()) as _class}
<div class="m-0-5">
<Button
label={getClassLabel(_class)}
on:click={(e) => {
showPopup(
SelectPopup,
{
value: classes.get(_class)?.map((it) => ({ id: it.value, label: it.label }))
},
getEventPositionElement(e),
(val) => {
console.log('val')
console.log(val)
if (val !== undefined) {
const value = classes.get(_class)?.find((it) => it.value === val)
if (value) {
value.enabled = true
attributes = attributes
}
}
}
)
}}
/>
</div>
{/each}
</div>

View File

@ -22,11 +22,20 @@
Doc,
Ref,
RefTo,
Type,
Space
Space,
Type
} from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation'
import { closePopup, closeTooltip, Icon, Label, showPopup, Submenu, resizeObserver } from '@hcengineering/ui'
import {
closePopup,
closeTooltip,
Icon,
Label,
resizeObserver,
Scroller,
showPopup,
Submenu
} from '@hcengineering/ui'
import { Filter, KeyFilter } from '@hcengineering/view'
import { createEventDispatcher } from 'svelte'
import { FilterQuery } from '../../filter'
@ -86,9 +95,6 @@
}
function buildFilterForAttr (_class: Ref<Class<Doc>>, attribute: AnyAttribute, result: KeyFilter[]): void {
if (attribute.isCustom !== true) {
return
}
if (attribute.label === undefined || attribute.hidden) {
return
}
@ -101,6 +107,7 @@
result.push(filter)
}
}
function buildFilterFor (
_class: Ref<Class<Doc>>,
allAttributes: Map<string, AnyAttribute>,
@ -118,7 +125,7 @@
const desc = hierarchy.getDescendants(_class)
for (const d of desc) {
const extra = hierarchy.getAllAttributes(d, _class)
const extra = hierarchy.getOwnAttributes(d)
for (const [k, v] of extra) {
if (!allAttributes.has(k)) {
allAttributes.set(k, v)
@ -127,6 +134,23 @@
}
}
const ancestors = new Set(hierarchy.getAncestors(_class))
const parent = hierarchy.getParentClass(_class)
const parentMixins = hierarchy
.getDescendants(parent)
.map((p) => hierarchy.getClass(p))
.filter((p) => hierarchy.isMixin(p._id) && p.extends && ancestors.has(p.extends))
for (const d of parentMixins) {
const extra = hierarchy.getOwnAttributes(d._id)
for (const [k, v] of extra) {
if (!allAttributes.has(k)) {
allAttributes.set(k, v)
buildFilterForAttr(d._id, v, result)
}
}
}
return result
}
@ -207,44 +231,42 @@
</script>
<div class="selectPopup" use:resizeObserver={() => dispatch('changeContent')}>
<div class="scroll">
<div class="box">
{#each getTypes(_class) as type, i}
{#if filter === undefined && type.component === view.component.ObjectFilter && hasNested(type)}
<Submenu
on:keydown={(event) => keyDown(event, i)}
on:click={(event) => {
click(type)
}}
icon={type.icon}
label={type.label}
props={getNestedProps(type)}
options={{ component: view.component.FilterTypePopup }}
withHover
/>
{:else}
<!-- svelte-ignore a11y-mouse-events-have-key-events -->
<button
class="menu-item withIcon"
on:keydown={(event) => keyDown(event, i)}
on:mouseover={(event) => {
event.currentTarget.focus()
}}
on:click={(event) => {
click(type)
}}
>
<div class="icon mr-3">
{#if type.icon}
<Icon icon={type.icon} size={'small'} />
{/if}
</div>
<div class="pr-1"><Label label={type.label} /></div>
</button>
{/if}
{/each}
</div>
</div>
<Scroller>
{#each getTypes(_class) as type, i}
{#if filter === undefined && type.component === view.component.ObjectFilter && hasNested(type)}
<Submenu
on:keydown={(event) => keyDown(event, i)}
on:click={(event) => {
click(type)
}}
icon={type.icon}
label={type.label}
props={getNestedProps(type)}
options={{ component: view.component.FilterTypePopup }}
withHover
/>
{:else}
<!-- svelte-ignore a11y-mouse-events-have-key-events -->
<button
class="menu-item withIcon"
on:keydown={(event) => keyDown(event, i)}
on:mouseover={(event) => {
event.currentTarget.focus()
}}
on:click={(event) => {
click(type)
}}
>
<div class="icon mr-3">
{#if type.icon}
<Icon icon={type.icon} size={'small'} />
{/if}
</div>
<div class="pr-1"><Label label={type.label} /></div>
</button>
{/if}
{/each}
</Scroller>
</div>
<style lang="scss">

View File

@ -62,6 +62,7 @@ import TimestampPresenter from './components/TimestampPresenter.svelte'
import UpDownNavigator from './components/UpDownNavigator.svelte'
import ValueSelector from './components/ValueSelector.svelte'
import ViewletSettingButton from './components/ViewletSettingButton.svelte'
import SpaceRefPresenter from './components/SpaceRefPresenter.svelte'
import {
afterResult,
@ -172,7 +173,8 @@ export default async (): Promise<Resources> => ({
HTMLEditor,
ListView,
GrowPresenter,
IndexedDocumentPreview
IndexedDocumentPreview,
SpaceRefPresenter
},
popup: {
PositionElementAlignment