Viewlet setting fixes (#1751)

Signed-off-by: Denis Bykhov <80476319+BykhovDenis@users.noreply.github.com>
This commit is contained in:
Denis Bykhov 2022-05-13 23:31:57 +06:00 committed by GitHub
parent dec594d969
commit ba17bed87e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 128 additions and 86 deletions

View File

@ -173,7 +173,8 @@ export function createModel (builder: Builder): void {
'modifiedOn', 'modifiedOn',
{ key: '', presenter: view.component.RolePresenter, label: view.string.Role }, { key: '', presenter: view.component.RolePresenter, label: view.string.Role },
'$lookup.channels' '$lookup.channels'
] ],
hiddenKeys: ['name']
}) })
builder.mixin(contact.class.Person, core.class.Class, view.mixin.ObjectEditor, { builder.mixin(contact.class.Person, core.class.Class, view.mixin.ObjectEditor, {

View File

@ -127,7 +127,8 @@ export function createModel (builder: Builder): void {
{ key: 'leads', presenter: lead.component.LeadsPresenter, label: lead.string.Leads }, { key: 'leads', presenter: lead.component.LeadsPresenter, label: lead.string.Leads },
'modifiedOn', 'modifiedOn',
'$lookup.channels' '$lookup.channels'
] ],
hiddenKeys: ['name']
}) })
builder.createDoc(view.class.Viewlet, core.space.Model, { builder.createDoc(view.class.Viewlet, core.space.Model, {

View File

@ -271,7 +271,8 @@ export function createModel (builder: Builder): void {
}, },
'modifiedOn', 'modifiedOn',
'$lookup.channels' '$lookup.channels'
] ],
hiddenKeys: ['name']
}) })
builder.createDoc(view.class.Viewlet, core.space.Model, { builder.createDoc(view.class.Viewlet, core.space.Model, {

View File

@ -139,6 +139,7 @@ export class TViewlet extends TDoc implements Viewlet {
descriptor!: Ref<ViewletDescriptor> descriptor!: Ref<ViewletDescriptor>
open!: AnyComponent open!: AnyComponent
config!: (BuildModelKey | string)[] config!: (BuildModelKey | string)[]
hiddenKeys?: string[]
} }
@Model(view.class.Action, core.class.Doc, DOMAIN_MODEL) @Model(view.class.Action, core.class.Doc, DOMAIN_MODEL)

View File

@ -38,7 +38,7 @@ export function resultSort<T extends Doc> (result: T[], sortOptions: SortingQuer
for (const key in sortOptions) { for (const key in sortOptions) {
const aValue = getValue(key, a) const aValue = getValue(key, a)
const bValue = getValue(key, b) const bValue = getValue(key, b)
const result = getSortingResult(aValue, bValue, sortOptions[key] as SortingOrder) const result = getSortingResult(aValue, bValue, sortOptions[key])
if (result !== 0) return result if (result !== 0) return result
} }
return 0 return 0

View File

@ -121,6 +121,10 @@ export type FindOptions<T extends Doc> = {
*/ */
export type SortingQuery<T extends Doc> = { export type SortingQuery<T extends Doc> = {
[P in keyof T]?: SortingOrder [P in keyof T]?: SortingOrder
} & {
// support nested queries e.g. 'user.friends.name'
// this will mark all unrecognized properties as any (including nested queries)
[key: string]: SortingOrder
} }
/** /**

View File

@ -75,17 +75,6 @@
<span class="ac-header__title"><Label label={contact.string.Contacts} /></span> <span class="ac-header__title"><Label label={contact.string.Contacts} /></span>
</div> </div>
{#if viewlet}
<ActionIcon
icon={view.icon.Setting}
direction={'top'}
size={'small'}
label={view.string.CustomizeView}
action={() => {
showPopup(ViewletSetting, { viewlet })
}}
/>
{/if}
<SearchEdit <SearchEdit
bind:value={search} bind:value={search}
on:change={() => { on:change={() => {
@ -98,6 +87,16 @@
kind={'primary'} kind={'primary'}
on:click={(ev) => showCreateDialog(ev)} on:click={(ev) => showCreateDialog(ev)}
/> />
{#if viewlet}
<ActionIcon
icon={view.icon.Setting}
size={'small'}
label={view.string.CustomizeView}
action={() => {
showPopup(ViewletSetting, { viewlet })
}}
/>
{/if}
</div> </div>
{#if viewlet} {#if viewlet}

View File

@ -22,11 +22,11 @@
} }
</script> </script>
<div bind:this={container} class="flex container"> <div bind:this={container} class="flex-center container">
<div class="pr-2 over-underline"> <div class="pr-2 over-underline">
<PersonPresenter {value} {onEdit} {shouldShowAvatar} /> <PersonPresenter {value} {onEdit} {shouldShowAvatar} />
</div> </div>
<div class="status"> <div class="status content-color">
<EmployeeStatusPresenter employeeId={value._id} /> <EmployeeStatusPresenter employeeId={value._id} />
</div> </div>
</div> </div>

View File

@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { Employee, formatName, Status } from '@anticrm/contact' import { Employee, EmployeeAccount, formatName, Status } from '@anticrm/contact'
import { Ref, Space, WithLookup } from '@anticrm/core' import { getCurrentAccount, Ref, Space, WithLookup } from '@anticrm/core'
import { Avatar, createQuery, getClient } from '@anticrm/presentation' import { Avatar, createQuery, getClient } from '@anticrm/presentation'
import { Button, Label, showPopup } from '@anticrm/ui' import { Button, Label, showPopup } from '@anticrm/ui'
import EmployeeSetStatusPopup from './EmployeeSetStatusPopup.svelte' import EmployeeSetStatusPopup from './EmployeeSetStatusPopup.svelte'
@ -13,6 +13,8 @@
export let space: Ref<Space> export let space: Ref<Space>
const client = getClient() const client = getClient()
const me = (getCurrentAccount() as EmployeeAccount).employee
$: editable = employeeId === me
const stattusQuery = createQuery() const stattusQuery = createQuery()
let status: WithLookup<Status> let status: WithLookup<Status>
@ -58,23 +60,25 @@
<Avatar size="x-large" avatar={employee?.avatar} /> <Avatar size="x-large" avatar={employee?.avatar} />
</div> </div>
<div class="pb-2">{formatName(employee?.name ?? '')}</div> <div class="pb-2">{formatName(employee?.name ?? '')}</div>
<div class="pb-2"> {#if status}
<Label label={contact.string.Status} /> <div class="pb-2">
{#if status} <Label label={contact.string.Status} />
<div class="flex-row-stretch statusContainer"> <div class="flex-row-stretch statusContainer">
<div class="pr-2"> <div class="pr-2">
<EmployeeStatusPresenter {employeeId} withTooltip={false} /> <EmployeeStatusPresenter {employeeId} withTooltip={false} />
</div> </div>
<div class="setStatusButton"> {#if editable}
<Button icon={Edit} title={contact.string.SetStatus} on:click={onEdit} /> <div class="setStatusButton">
</div> <Button icon={Edit} title={contact.string.SetStatus} on:click={onEdit} />
</div>
{/if}
</div> </div>
{:else} </div>
<div class="flex-row-stretch over-underline" on:click={onEdit}> {:else if editable}
<Label label={contact.string.SetStatus} /> <div class="flex-row-stretch over-underline pb-2" on:click={onEdit}>
</div> <Label label={contact.string.SetStatus} />
{/if} </div>
</div> {/if}
</div> </div>
<style lang="scss"> <style lang="scss">

View File

@ -75,17 +75,6 @@
<span class="ac-header__title"><Label label={inventory.string.Products} /></span> <span class="ac-header__title"><Label label={inventory.string.Products} /></span>
</div> </div>
{#if descr}
<ActionIcon
icon={view.icon.Setting}
direction={'top'}
size={'small'}
label={view.string.CustomizeView}
action={() => {
showPopup(ViewletSetting, { viewlet: descr })
}}
/>
{/if}
<EditWithIcon <EditWithIcon
icon={IconSearch} icon={IconSearch}
placeholder={ui.string.Search} placeholder={ui.string.Search}
@ -100,6 +89,16 @@
kind={'primary'} kind={'primary'}
on:click={(ev) => showCreateDialog(ev)} on:click={(ev) => showCreateDialog(ev)}
/> />
{#if descr}
<ActionIcon
icon={view.icon.Setting}
size={'small'}
label={view.string.CustomizeView}
action={() => {
showPopup(ViewletSetting, { viewlet: descr })
}}
/>
{/if}
</div> </div>
{#if descr} {#if descr}

View File

@ -64,10 +64,15 @@
<span class="ac-header__title"><Label label={lead.string.Customers} /></span> <span class="ac-header__title"><Label label={lead.string.Customers} /></span>
</div> </div>
<SearchEdit
bind:value={search}
on:change={() => {
updateResultQuery(search)
}}
/>
{#if descr} {#if descr}
<ActionIcon <ActionIcon
icon={view.icon.Setting} icon={view.icon.Setting}
direction={'top'}
size={'small'} size={'small'}
label={view.string.CustomizeView} label={view.string.CustomizeView}
action={() => { action={() => {
@ -75,12 +80,6 @@
}} }}
/> />
{/if} {/if}
<SearchEdit
bind:value={search}
on:change={() => {
updateResultQuery(search)
}}
/>
</div> </div>
{#if descr} {#if descr}

View File

@ -73,17 +73,6 @@
<span class="ac-header__title"><Label label={recruit.string.Applications} /></span> <span class="ac-header__title"><Label label={recruit.string.Applications} /></span>
</div> </div>
{#if descr}
<ActionIcon
icon={view.icon.Setting}
direction={'top'}
size={'small'}
label={view.string.CustomizeView}
action={() => {
showPopup(ViewletSetting, { viewlet: descr })
}}
/>
{/if}
<SearchEdit <SearchEdit
bind:value={search} bind:value={search}
on:change={() => { on:change={() => {
@ -91,6 +80,16 @@
}} }}
/> />
<Button icon={IconAdd} label={recruit.string.ApplicationCreateLabel} kind={'primary'} on:click={showCreateDialog} /> <Button icon={IconAdd} label={recruit.string.ApplicationCreateLabel} kind={'primary'} on:click={showCreateDialog} />
{#if descr}
<ActionIcon
icon={view.icon.Setting}
size={'small'}
label={view.string.CustomizeView}
action={() => {
showPopup(ViewletSetting, { viewlet: descr })
}}
/>
{/if}
</div> </div>
{#if descr} {#if descr}

View File

@ -87,10 +87,15 @@
<span class="ac-header__title"><Label label={recruit.string.Candidates} /></span> <span class="ac-header__title"><Label label={recruit.string.Candidates} /></span>
</div> </div>
<SearchEdit
bind:value={search}
on:change={() => {
updateResultQuery(search, documentIds)
}}
/>
{#if descr} {#if descr}
<ActionIcon <ActionIcon
icon={view.icon.Setting} icon={view.icon.Setting}
direction={'top'}
size={'small'} size={'small'}
label={view.string.CustomizeView} label={view.string.CustomizeView}
action={() => { action={() => {
@ -98,12 +103,6 @@
}} }}
/> />
{/if} {/if}
<SearchEdit
bind:value={search}
on:change={() => {
updateResultQuery(search, documentIds)
}}
/>
</div> </div>
<Component <Component

View File

@ -20,7 +20,7 @@
"MoveRight": "Move right", "MoveRight": "Move right",
"MoveUp": "Move up", "MoveUp": "Move up",
"MoveDown": "Move down", "MoveDown": "Move down",
"RestoreDefaults": "Restore defaults",
"SelectItem": "Select focused item", "SelectItem": "Select focused item",
"SelectItemAll": "Select all items", "SelectItemAll": "Select all items",
"SelectItemNone": "Deselect all items", "SelectItemNone": "Deselect all items",

View File

@ -34,6 +34,7 @@
"SelectItemNone": "Снять все выделения", "SelectItemNone": "Снять все выделения",
"ShowPreview": "Предпросмотр документа", "ShowPreview": "Предпросмотр документа",
"ShowActions": "Показать действия", "ShowActions": "Показать действия",
"RestoreDefaults": "По умолчанию",
"CustomizeView": "Настроить отображение" "CustomizeView": "Настроить отображение"
} }
} }

View File

@ -16,7 +16,7 @@
import presentation, { Card, createQuery, getAttributePresenterClass, getClient } from '@anticrm/presentation' import presentation, { Card, createQuery, getAttributePresenterClass, getClient } from '@anticrm/presentation'
import { BuildModelKey, Viewlet, ViewletPreference } from '@anticrm/view' import { BuildModelKey, Viewlet, ViewletPreference } from '@anticrm/view'
import core, { ArrOf, Class, Doc, Lookup, Ref, Type } from '@anticrm/core' import core, { ArrOf, Class, Doc, Lookup, Ref, Type } from '@anticrm/core'
import { Grid, MiniToggle } from '@anticrm/ui' import { Button, Grid, MiniToggle } from '@anticrm/ui'
import { createEventDispatcher } from 'svelte' import { createEventDispatcher } from 'svelte'
import { IntlString } from '@anticrm/platform' import { IntlString } from '@anticrm/platform'
import { buildConfigLookup, getLookupClass, getLookupLabel, getLookupProperty } from '../utils' import { buildConfigLookup, getLookupClass, getLookupLabel, getLookupProperty } from '../utils'
@ -105,7 +105,7 @@
const value = getValue(attribute.name, attribute.type) const value = getValue(attribute.name, attribute.type)
if (result.findIndex((p) => p.value === value) !== -1) continue if (result.findIndex((p) => p.value === value) !== -1) continue
if (hierarchy.isDerived(attribute.type._class, core.class.Collection)) continue if (hierarchy.isDerived(attribute.type._class, core.class.Collection)) continue
if (viewlet.hiddenKeys?.includes(value)) continue
if (attribute.hidden !== true && attribute.label !== undefined) { if (attribute.hidden !== true && attribute.label !== undefined) {
const typeClassId = getAttributePresenterClass(attribute) const typeClassId = getAttributePresenterClass(attribute)
const typeClass = hierarchy.getClass(typeClassId) const typeClass = hierarchy.getClass(typeClassId)
@ -142,6 +142,10 @@
} }
} }
async function restoreDefault (): Promise<void> {
attributes = getConfig(viewlet, undefined)
}
function getKeyLabel<T extends Doc> (_class: Ref<Class<T>>, key: string, lookup: Lookup<T> | undefined): IntlString { function getKeyLabel<T extends Doc> (_class: Ref<Class<T>>, key: string, lookup: Lookup<T> | undefined): IntlString {
if (key.startsWith('$lookup') && lookup !== undefined) { if (key.startsWith('$lookup') && lookup !== undefined) {
const lookupClass = getLookupClass(key, lookup, _class) const lookupClass = getLookupClass(key, lookup, _class)
@ -176,4 +180,7 @@
<MiniToggle label={attribute.label} bind:on={attribute.enabled} /> <MiniToggle label={attribute.label} bind:on={attribute.enabled} />
{/each} {/each}
</Grid> </Grid>
<svelte:fragment slot="footer">
<Button label={view.string.RestoreDefaults} on:click={() => restoreDefault()} />
</svelte:fragment>
</Card> </Card>

View File

@ -39,6 +39,7 @@ export default mergeIds(viewId, view, {
Type: '' as IntlString, Type: '' as IntlString,
ActionPlaceholder: '' as IntlString, ActionPlaceholder: '' as IntlString,
CreatingAttribute: '' as IntlString, CreatingAttribute: '' as IntlString,
RestoreDefaults: '' as IntlString,
CreatingAttributeConfirm: '' as IntlString CreatingAttributeConfirm: '' as IntlString
} }
}) })

View File

@ -105,6 +105,7 @@ export interface Viewlet extends Doc {
descriptor: Ref<ViewletDescriptor> descriptor: Ref<ViewletDescriptor>
options?: FindOptions<Doc> options?: FindOptions<Doc>
config: (BuildModelKey | string)[] config: (BuildModelKey | string)[]
hiddenKeys?: string[]
} }
/** /**

View File

@ -111,17 +111,6 @@
{/each} {/each}
</div> </div>
{/if} {/if}
{#if viewlet}
<ActionIcon
icon={view.icon.Setting}
direction={'top'}
size={'small'}
label={view.string.CustomizeView}
action={() => {
showPopup(ViewletSetting, { viewlet })
}}
/>
{/if}
<SearchEdit <SearchEdit
bind:value={search} bind:value={search}
on:change={() => { on:change={() => {
@ -131,5 +120,15 @@
{#if createItemDialog} {#if createItemDialog}
<Button icon={IconAdd} label={createItemLabel} kind={'primary'} on:click={(ev) => showCreateDialog(ev)} /> <Button icon={IconAdd} label={createItemLabel} kind={'primary'} on:click={(ev) => showCreateDialog(ev)} />
{/if} {/if}
{#if viewlet}
<ActionIcon
icon={view.icon.Setting}
size={'small'}
label={view.string.CustomizeView}
action={() => {
showPopup(ViewletSetting, { viewlet })
}}
/>
{/if}
{/if} {/if}
</div> </div>

View File

@ -30,6 +30,7 @@ import core, {
Ref, Ref,
ReverseLookups, ReverseLookups,
SortingOrder, SortingOrder,
SortingQuery,
toFindResult, toFindResult,
Tx, Tx,
TxCreateDoc, TxCreateDoc,
@ -56,6 +57,14 @@ function isLookupQuery<T extends Doc> (query: DocumentQuery<T>): boolean {
return false return false
} }
function isLookupSort<T extends Doc> (sort: SortingQuery<T> | undefined): boolean {
if (sort === undefined) return false
for (const key in sort) {
if (key.includes('$lookup.')) return true
}
return false
}
interface LookupStep { interface LookupStep {
from: string from: string
localField: string localField: string
@ -212,7 +221,7 @@ abstract class MongoAdapterBase extends TxProcessor {
} }
} }
} else { } else {
targetObject.$lookup[key] = this.modelDb.getObject(targetObject[key]) targetObject.$lookup[key] = (await this.modelDb.findAll(_class, { _id: targetObject[key] }))[0]
} }
} }
@ -281,7 +290,7 @@ abstract class MongoAdapterBase extends TxProcessor {
): Promise<FindResult<T>> { ): Promise<FindResult<T>> {
const pipeline = [] const pipeline = []
const match = { $match: this.translateQuery(clazz, query) } const match = { $match: this.translateQuery(clazz, query) }
const slowPipeline = isLookupQuery(query) const slowPipeline = isLookupQuery(query) || isLookupSort(options.sort)
const steps = await this.getLookups(options.lookup) const steps = await this.getLookups(options.lookup)
if (slowPipeline) { if (slowPipeline) {
for (const step of steps) { for (const step of steps) {
@ -293,11 +302,28 @@ abstract class MongoAdapterBase extends TxProcessor {
if (options.sort !== undefined) { if (options.sort !== undefined) {
const sort = {} as any const sort = {} as any
for (const _key in options.sort) { for (const _key in options.sort) {
// Check if key is belong to mixin class, we need to add prefix. let key: string = _key
const key = this.checkMixinKey<T>(_key, clazz) const arr = key.split('.').filter((p) => p)
key = ''
for (let i = 0; i < arr.length; i++) {
const element = arr[i]
if (element === '$lookup') {
key += arr[++i] + '_lookup'
} else {
if (!key.endsWith('.') && i > 0) {
key += '.'
}
key += arr[i]
if (i !== arr.length - 1) {
key += '.'
}
}
// Check if key is belong to mixin class, we need to add prefix.
key = this.checkMixinKey<T>(key, clazz)
}
sort[key] = options.sort[_key] === SortingOrder.Ascending ? 1 : -1 sort[key] = options.sort[_key] === SortingOrder.Ascending ? 1 : -1
} }
resultPipeline.push({ $sort: sort }) pipeline.push({ $sort: sort })
} }
if (options.limit !== undefined) { if (options.limit !== undefined) {
resultPipeline.push({ $limit: options.limit }) resultPipeline.push({ $limit: options.limit })