UBER-351 Use rank for table view configuration to allow reordering of columns (#3321)

Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com>
This commit is contained in:
Denis Bykhov 2023-06-01 17:33:42 +06:00 committed by GitHub
parent f40750ec95
commit 467c7736e6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 123 additions and 59 deletions

View File

@ -286,7 +286,8 @@ export function createModel (builder: Builder): void {
} }
], ],
configOptions: { configOptions: {
hiddenKeys: ['name'] hiddenKeys: ['name'],
sortable: true
} }
}, },
contact.viewlet.TableContact contact.viewlet.TableContact

View File

@ -111,7 +111,10 @@ export function createModel (builder: Builder): void {
{ {
attachTo: inventory.class.Product, attachTo: inventory.class.Product,
descriptor: view.viewlet.Table, descriptor: view.viewlet.Table,
config: ['', 'attachedTo', 'modifiedOn'] config: ['', 'attachedTo', 'modifiedOn'],
configOptions: {
sortable: true
}
}, },
inventory.viewlet.TableProduct inventory.viewlet.TableProduct
) )

View File

@ -209,7 +209,8 @@ export function createModel (builder: Builder): void {
} }
], ],
configOptions: { configOptions: {
hiddenKeys: ['name'] hiddenKeys: ['name'],
sortable: true
}, },
options: { options: {
lookup: { lookup: {
@ -241,11 +242,6 @@ export function createModel (builder: Builder): void {
'state', 'state',
'doneState', 'doneState',
'attachments', 'attachments',
{
key: '',
presenter: tracker.component.RelatedIssueSelector,
label: tracker.string.Relations
},
'comments', 'comments',
'modifiedOn', 'modifiedOn',
{ {
@ -253,6 +249,9 @@ export function createModel (builder: Builder): void {
sortingKey: ['$lookup.attachedTo.$lookup.channels.lastMessage', '$lookup.attachedTo.channels'] sortingKey: ['$lookup.attachedTo.$lookup.channels.lastMessage', '$lookup.attachedTo.channels']
} }
], ],
configOptions: {
sortable: true
},
options: { options: {
lookup: { lookup: {
_id: { _id: {
@ -292,6 +291,7 @@ export function createModel (builder: Builder): void {
descriptor: view.viewlet.List, descriptor: view.viewlet.List,
configOptions: { configOptions: {
hiddenKeys: ['title'], hiddenKeys: ['title'],
sortable: true,
extraProps: { extraProps: {
displayProps: { displayProps: {
optional: true optional: true

View File

@ -400,7 +400,8 @@ export function createModel (builder: Builder): void {
} }
], ],
configOptions: { configOptions: {
hiddenKeys: ['name'] hiddenKeys: ['name'],
sortable: true
}, },
options: { options: {
lookup: { lookup: {
@ -420,6 +421,9 @@ export function createModel (builder: Builder): void {
attachTo: recruit.class.Applicant, attachTo: recruit.class.Applicant,
descriptor: view.viewlet.Table, descriptor: view.viewlet.Table,
config: ['', '$lookup.attachedTo', 'state', 'doneState', 'modifiedOn'], config: ['', '$lookup.attachedTo', 'state', 'doneState', 'modifiedOn'],
configOptions: {
sortable: true
},
variant: 'short' variant: 'short'
}, },
recruit.viewlet.VacancyApplicationsShort recruit.viewlet.VacancyApplicationsShort
@ -432,6 +436,9 @@ export function createModel (builder: Builder): void {
attachTo: recruit.class.Applicant, attachTo: recruit.class.Applicant,
descriptor: view.viewlet.Table, descriptor: view.viewlet.Table,
config: ['', '$lookup.space.name', '$lookup.space.$lookup.company', 'state', 'comments', 'doneState'], config: ['', '$lookup.space.name', '$lookup.space.$lookup.company', 'state', 'comments', 'doneState'],
configOptions: {
sortable: true
},
variant: 'embedded' variant: 'embedded'
}, },
recruit.viewlet.VacancyApplicationsEmbeddeed recruit.viewlet.VacancyApplicationsEmbeddeed
@ -460,7 +467,8 @@ export function createModel (builder: Builder): void {
} }
], ],
configOptions: { configOptions: {
hiddenKeys: ['name', 'space', 'modifiedOn'] hiddenKeys: ['name', 'space', 'modifiedOn'],
sortable: true
} }
}, },
recruit.viewlet.TableVacancy recruit.viewlet.TableVacancy
@ -492,7 +500,8 @@ export function createModel (builder: Builder): void {
} }
], ],
configOptions: { configOptions: {
hiddenKeys: ['name', 'space', 'modifiedOn'] hiddenKeys: ['name', 'space', 'modifiedOn'],
sortable: true
} }
}, },
recruit.viewlet.TableVacancyList recruit.viewlet.TableVacancyList
@ -533,7 +542,8 @@ export function createModel (builder: Builder): void {
} }
], ],
configOptions: { configOptions: {
hiddenKeys: ['name', 'attachedTo'] hiddenKeys: ['name', 'attachedTo'],
sortable: true
}, },
options: { options: {
lookup: { lookup: {
@ -588,7 +598,8 @@ export function createModel (builder: Builder): void {
} }
}, },
configOptions: { configOptions: {
hiddenKeys: ['name', 'attachedTo'] hiddenKeys: ['name', 'attachedTo'],
sortable: true
}, },
baseQuery: { baseQuery: {
doneState: null, doneState: null,
@ -722,6 +733,7 @@ export function createModel (builder: Builder): void {
}, },
configOptions: { configOptions: {
hiddenKeys: ['name', 'attachedTo'], hiddenKeys: ['name', 'attachedTo'],
sortable: true,
extraProps: { extraProps: {
displayProps: { displayProps: {
optional: true optional: true

View File

@ -505,6 +505,7 @@ export function createModel (builder: Builder): void {
'dueDate', 'dueDate',
'attachedTo' 'attachedTo'
], ],
sortable: true,
extraProps: { extraProps: {
displayProps: { displayProps: {
optional: true optional: true
@ -643,32 +644,51 @@ export function createModel (builder: Builder): void {
descriptor: view.viewlet.List, descriptor: view.viewlet.List,
viewOptions: subIssuesOptions, viewOptions: subIssuesOptions,
variant: 'subissue', variant: 'subissue',
configOptions: {
sortable: true,
hiddenKeys: ['priority', 'number', 'status', 'title', 'dueDate', 'milestone', 'estimation'],
extraProps: {
displayProps: {
optional: true
}
}
},
config: [ config: [
{ {
key: '', key: '',
label: tracker.string.Priority,
presenter: tracker.component.PriorityEditor, presenter: tracker.component.PriorityEditor,
props: { type: 'priority', kind: 'list', size: 'small' } props: { type: 'priority', kind: 'list', size: 'small' }
}, },
{ {
key: '', key: '',
label: tracker.string.Issue,
presenter: tracker.component.IssuePresenter, presenter: tracker.component.IssuePresenter,
props: { type: 'issue' }, props: { type: 'issue' },
displayProps: { fixed: 'left' } displayProps: { fixed: 'left' }
}, },
{ {
key: '', key: '',
label: tracker.string.Status,
presenter: tracker.component.StatusEditor, presenter: tracker.component.StatusEditor,
props: { kind: 'list', size: 'small', justify: 'center' } props: { kind: 'list', size: 'small', justify: 'center' }
}, },
{ key: '', presenter: tracker.component.TitlePresenter, props: { shouldUseMargin: true, showParent: false } },
{ key: '', presenter: tracker.component.SubIssuesSelector, props: {} },
{ {
key: '', key: '',
label: tracker.string.Title,
presenter: tracker.component.TitlePresenter,
props: { shouldUseMargin: true, showParent: false }
},
{ key: '', label: tracker.string.SubIssues, presenter: tracker.component.SubIssuesSelector, props: {} },
{
key: '',
label: tracker.string.DueDate,
presenter: tracker.component.DueDatePresenter, presenter: tracker.component.DueDatePresenter,
props: { kind: 'list' } props: { kind: 'list' }
}, },
{ {
key: '', key: '',
label: tracker.string.Milestone,
presenter: tracker.component.MilestoneEditor, presenter: tracker.component.MilestoneEditor,
props: { props: {
kind: 'list', kind: 'list',
@ -683,6 +703,7 @@ export function createModel (builder: Builder): void {
}, },
{ {
key: '', key: '',
label: tracker.string.Estimation,
presenter: tracker.component.EstimationEditor, presenter: tracker.component.EstimationEditor,
props: { kind: 'list', size: 'small' }, props: { kind: 'list', size: 'small' },
displayProps: { optional: true } displayProps: { optional: true }
@ -720,7 +741,8 @@ export function createModel (builder: Builder): void {
}, },
configOptions: { configOptions: {
hiddenKeys: ['milestone', 'estimation', 'component', 'title', 'description'], hiddenKeys: ['milestone', 'estimation', 'component', 'title', 'description'],
extraProps: { displayProps: { optional: true } } extraProps: { displayProps: { optional: true } },
sortable: true
}, },
config: [ config: [
// { key: '', presenter: tracker.component.PriorityEditor, props: { kind: 'list', size: 'small' } }, // { key: '', presenter: tracker.component.PriorityEditor, props: { kind: 'list', size: 'small' } },
@ -1861,7 +1883,8 @@ export function createModel (builder: Builder): void {
viewOptions: milestoneOptions, viewOptions: milestoneOptions,
configOptions: { configOptions: {
hiddenKeys: ['targetDate', 'label', 'description'], hiddenKeys: ['targetDate', 'label', 'description'],
extraProps: { displayProps: { optional: true } } extraProps: { displayProps: { optional: true } },
sortable: true
}, },
config: [ config: [
{ {
@ -1941,7 +1964,8 @@ export function createModel (builder: Builder): void {
viewOptions: componentListViewOptions, viewOptions: componentListViewOptions,
configOptions: { configOptions: {
hiddenKeys: ['label', 'description'], hiddenKeys: ['label', 'description'],
extraProps: { displayProps: { optional: true } } extraProps: { displayProps: { optional: true } },
sortable: true
}, },
config: [ config: [
{ {

View File

@ -67,7 +67,6 @@ import SetParentIssueActionPopup from './components/SetParentIssueActionPopup.sv
import MilestoneDatePresenter from './components/milestones/MilestoneDatePresenter.svelte' import MilestoneDatePresenter from './components/milestones/MilestoneDatePresenter.svelte'
import EditMilestone from './components/milestones/EditMilestone.svelte' import EditMilestone from './components/milestones/EditMilestone.svelte'
import CreateIssueTemplate from './components/templates/CreateIssueTemplate.svelte' import CreateIssueTemplate from './components/templates/CreateIssueTemplate.svelte'
import Views from './components/views/Views.svelte'
import Statuses from './components/workflow/Statuses.svelte' import Statuses from './components/workflow/Statuses.svelte'
import { import {
@ -384,7 +383,6 @@ export default async (): Promise<Resources> => ({
Inbox, Inbox,
MyIssues, MyIssues,
Components, Components,
Views,
IssuePresenter, IssuePresenter,
ComponentPresenter, ComponentPresenter,
ComponentTitlePresenter, ComponentTitlePresenter,

View File

@ -17,7 +17,7 @@
import { Asset, IntlString } from '@hcengineering/platform' import { Asset, IntlString } from '@hcengineering/platform'
import preferencePlugin from '@hcengineering/preference' import preferencePlugin from '@hcengineering/preference'
import { createQuery, getAttributePresenterClass, getClient, hasResource } from '@hcengineering/presentation' import { createQuery, getAttributePresenterClass, getClient, hasResource } from '@hcengineering/presentation'
import { Loading, ToggleWithLabel } from '@hcengineering/ui' import { Button, Loading, ToggleWithLabel } from '@hcengineering/ui'
import { BuildModelKey, Viewlet, ViewletPreference } from '@hcengineering/view' import { BuildModelKey, Viewlet, ViewletPreference } from '@hcengineering/view'
import { deepEqual } from 'fast-equals' import { deepEqual } from 'fast-equals'
import view from '../plugin' import view from '../plugin'
@ -36,8 +36,7 @@
}, },
(res) => { (res) => {
preference = res[0] preference = res[0]
attributes = getConfig(viewlet, preference) items = getConfig(viewlet, preference)
classes = groupByClasses(attributes)
loading = false loading = false
}, },
{ limit: 1 } { limit: 1 }
@ -48,7 +47,7 @@
const client = getClient() const client = getClient()
const hierarchy = client.getHierarchy() const hierarchy = client.getHierarchy()
let attributes: AttributeConfig[] = [] let items: AttributeConfig[] = []
let loading = true let loading = true
interface AttributeConfig { interface AttributeConfig {
@ -57,6 +56,7 @@
value: string | BuildModelKey value: string | BuildModelKey
_class: Ref<Class<Doc>> _class: Ref<Class<Doc>>
icon: Asset | undefined icon: Asset | undefined
order?: number
} }
function getObjectConfig (_class: Ref<Class<Doc>>, param: string): AttributeConfig { function getObjectConfig (_class: Ref<Class<Doc>>, param: string): AttributeConfig {
@ -201,10 +201,7 @@
} }
async function save (): Promise<void> { async function save (): Promise<void> {
const config = Array.from(classes.values()) const config = items.filter((p) => p.enabled).map((p) => p.value)
.flat()
.filter((p) => p.enabled)
.map((p) => p.value)
if (preference !== undefined) { if (preference !== undefined) {
await client.update(preference, { await client.update(preference, {
config config
@ -217,29 +214,51 @@
} }
} }
// function restoreDefault (): void { function restoreDefault (): void {
// attributes = getConfig(viewlet, undefined) items = getConfig(viewlet, undefined)
// classes = groupByClasses(attributes) save()
// } }
function setStatus (result: AttributeConfig[], preference: ViewletPreference): AttributeConfig[] { function setStatus (result: AttributeConfig[], preference: ViewletPreference): AttributeConfig[] {
for (const key of result) { for (const key of result) {
key.enabled = preference.config.findIndex((p) => deepEqual(p, key.value)) !== -1 const index = preference.config.findIndex((p) => deepEqual(p, key.value))
key.enabled = index !== -1
key.order = index !== -1 ? index : undefined
} }
result.sort((a, b) => {
if (a.order === undefined && b.order === undefined) return 0
if (a.order === undefined) return 1
if (b.order === undefined) return -1
return a.order - b.order
})
return result return result
} }
function groupByClasses (attributes: AttributeConfig[]): Map<Ref<Class<Doc>>, AttributeConfig[]> { function dragEnd () {
const res = new Map() selected = undefined
for (const attribute of attributes) { save()
const arr = res.get(attribute._class) ?? []
arr.push(attribute)
res.set(attribute._class, arr)
}
return res
} }
let classes: Map<Ref<Class<Doc>>, AttributeConfig[]> = new Map() function dragOver (e: DragEvent, i: number) {
const s = selected as number
if (dragswap(e, i, s)) {
;[items[i], items[s]] = [items[s], items[i]]
selected = i
}
}
const elements: HTMLElement[] = []
function dragswap (ev: MouseEvent, i: number, s: number): boolean {
if (i < s) {
return ev.offsetY < elements[i].offsetHeight / 2
} else if (i > s) {
return ev.offsetY > elements[i].offsetHeight / 2
}
return false
}
let selected: number | undefined
</script> </script>
<div class="selectPopup p-2"> <div class="selectPopup p-2">
@ -247,23 +266,29 @@
{#if loading} {#if loading}
<Loading /> <Loading />
{:else} {:else}
{#each Array.from(classes.keys()) as _class, i} <div class="flex-row-reverse">
{@const items = classes.get(_class) ?? []} <Button on:click={restoreDefault} label={view.string.RestoreDefaults} size={'x-small'} kind={'link'} noFocus />
{#if i !== 0} </div>
<div class="menu-separator" /> {#each items as item, i}
{/if} <div
{#each items as item} class="item"
<div class="item"> bind:this={elements[i]}
<ToggleWithLabel draggable={viewlet.configOptions?.sortable}
on={item.enabled} on:dragstart={() => {
label={item.label} selected = i
on:change={(e) => { }}
item.enabled = e.detail on:dragover|preventDefault={(e) => dragOver(e, i)}
save() on:dragend={dragEnd}
}} >
/> <ToggleWithLabel
</div> on={item.enabled}
{/each} label={item.label}
on:change={(e) => {
item.enabled = e.detail
save()
}}
/>
</div>
{/each} {/each}
{/if} {/if}
</div> </div>

View File

@ -329,6 +329,7 @@ export interface ViewletConfigOptions {
hiddenKeys?: string[] hiddenKeys?: string[]
strict?: boolean strict?: boolean
extraProps?: Omit<BuildModelKey, 'key'> extraProps?: Omit<BuildModelKey, 'key'>
sortable?: boolean
} }
/** /**