mirror of
https://github.com/hcengineering/platform.git
synced 2025-04-24 09:16:43 +00:00
Multi group list (#2540)
Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com>
This commit is contained in:
parent
17f7563d85
commit
aebb215799
@ -60,7 +60,8 @@ import type {
|
|||||||
Viewlet,
|
Viewlet,
|
||||||
ViewletDescriptor,
|
ViewletDescriptor,
|
||||||
ViewletPreference,
|
ViewletPreference,
|
||||||
ViewOptionsModel
|
ViewOptionsModel,
|
||||||
|
ViewOptions
|
||||||
} from '@hcengineering/view'
|
} from '@hcengineering/view'
|
||||||
import view from './plugin'
|
import view from './plugin'
|
||||||
|
|
||||||
@ -99,6 +100,8 @@ export class TFilteredView extends TPreference implements FilteredView {
|
|||||||
name!: string
|
name!: string
|
||||||
location!: Location
|
location!: Location
|
||||||
filters!: string
|
filters!: string
|
||||||
|
viewOptions?: ViewOptions
|
||||||
|
viewletId?: Ref<Viewlet> | null
|
||||||
}
|
}
|
||||||
|
|
||||||
@Model(view.class.FilterMode, core.class.Doc, DOMAIN_MODEL)
|
@Model(view.class.FilterMode, core.class.Doc, DOMAIN_MODEL)
|
||||||
|
@ -15,8 +15,9 @@
|
|||||||
|
|
||||||
import { Ref } from '@hcengineering/core'
|
import { Ref } from '@hcengineering/core'
|
||||||
import { MigrateOperation, MigrationClient, MigrationUpgradeClient } from '@hcengineering/model'
|
import { MigrateOperation, MigrationClient, MigrationUpgradeClient } from '@hcengineering/model'
|
||||||
import { Viewlet, ViewletPreference } from '@hcengineering/view'
|
import { FilteredView, Viewlet, ViewletPreference } from '@hcengineering/view'
|
||||||
import { DOMAIN_PREFERENCE } from '@hcengineering/preference'
|
import { DOMAIN_PREFERENCE } from '@hcengineering/preference'
|
||||||
|
import view from './plugin'
|
||||||
|
|
||||||
async function migrateViewletPreference (client: MigrationClient): Promise<void> {
|
async function migrateViewletPreference (client: MigrationClient): Promise<void> {
|
||||||
const targets: Record<string, string[]> = {
|
const targets: Record<string, string[]> = {
|
||||||
@ -56,9 +57,31 @@ async function migrateViewletPreference (client: MigrationClient): Promise<void>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function migrateSavedFilters (client: MigrationClient): Promise<void> {
|
||||||
|
const preferences = await client.find<FilteredView>(DOMAIN_PREFERENCE, {
|
||||||
|
_class: view.class.FilteredView,
|
||||||
|
viewOptions: { $exists: true }
|
||||||
|
})
|
||||||
|
for (const pref of preferences) {
|
||||||
|
if (pref.viewOptions === undefined) continue
|
||||||
|
if (Array.isArray(pref.viewOptions.groupBy)) continue
|
||||||
|
pref.viewOptions.groupBy = [pref.viewOptions.groupBy]
|
||||||
|
await client.update<FilteredView>(
|
||||||
|
DOMAIN_PREFERENCE,
|
||||||
|
{
|
||||||
|
_id: pref._id
|
||||||
|
},
|
||||||
|
{
|
||||||
|
viewOptions: pref.viewOptions
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const viewOperation: MigrateOperation = {
|
export const viewOperation: MigrateOperation = {
|
||||||
async migrate (client: MigrationClient): Promise<void> {
|
async migrate (client: MigrationClient): Promise<void> {
|
||||||
await migrateViewletPreference(client)
|
await migrateViewletPreference(client)
|
||||||
|
await migrateSavedFilters(client)
|
||||||
},
|
},
|
||||||
async upgrade (client: MigrationUpgradeClient): Promise<void> {}
|
async upgrade (client: MigrationUpgradeClient): Promise<void> {}
|
||||||
}
|
}
|
||||||
|
@ -31,5 +31,9 @@
|
|||||||
"@hcengineering/ui": "^0.6.3",
|
"@hcengineering/ui": "^0.6.3",
|
||||||
"@hcengineering/contact": "^0.6.9",
|
"@hcengineering/contact": "^0.6.9",
|
||||||
"@hcengineering/setting": "^0.6.2"
|
"@hcengineering/setting": "^0.6.2"
|
||||||
|
},
|
||||||
|
"repository": "https://github.com/hcengineering/anticrm",
|
||||||
|
"publishConfig": {
|
||||||
|
"registry": "https://npm.pkg.github.com"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -62,7 +62,7 @@
|
|||||||
export let viewOptions: ViewOptions
|
export let viewOptions: ViewOptions
|
||||||
|
|
||||||
$: currentSpace = space || tracker.team.DefaultTeam
|
$: currentSpace = space || tracker.team.DefaultTeam
|
||||||
$: groupBy = (viewOptions.groupBy ?? noCategory) as IssuesGrouping
|
$: groupBy = (viewOptions.groupBy[0] ?? noCategory) as IssuesGrouping
|
||||||
$: orderBy = viewOptions.orderBy
|
$: orderBy = viewOptions.orderBy
|
||||||
$: sort = { [orderBy[0]]: orderBy[1] }
|
$: sort = { [orderBy[0]]: orderBy[1] }
|
||||||
$: dontUpdateRank = orderBy[0] !== IssuesOrdering.Manual
|
$: dontUpdateRank = orderBy[0] !== IssuesOrdering.Manual
|
||||||
|
@ -47,21 +47,30 @@
|
|||||||
const query = createQuery()
|
const query = createQuery()
|
||||||
const statusesQuery = createQuery()
|
const statusesQuery = createQuery()
|
||||||
|
|
||||||
let statuses: WithLookup<IssueStatus>[] = []
|
$: update(value)
|
||||||
|
|
||||||
$: if (value.$lookup?.subIssues !== undefined) {
|
function update (value: WithLookup<Issue>): void {
|
||||||
|
if (value.$lookup?.subIssues !== undefined) {
|
||||||
query.unsubscribe()
|
query.unsubscribe()
|
||||||
subIssues = value.$lookup.subIssues as Issue[]
|
subIssues = value.$lookup.subIssues as Issue[]
|
||||||
subIssues.sort((a, b) => (a.rank ?? '').localeCompare(b.rank ?? ''))
|
subIssues.sort((a, b) => (a.rank ?? '').localeCompare(b.rank ?? ''))
|
||||||
} else {
|
} else if (value.subIssues > 0) {
|
||||||
query.query(tracker.class.Issue, { attachedTo: value._id }, (res) => (subIssues = res), {
|
query.query(tracker.class.Issue, { attachedTo: value._id }, (res) => (subIssues = res), {
|
||||||
sort: { rank: SortingOrder.Ascending }
|
sort: { rank: SortingOrder.Ascending }
|
||||||
})
|
})
|
||||||
|
} else {
|
||||||
|
query.unsubscribe()
|
||||||
}
|
}
|
||||||
|
if (value.subIssues > 0 || value.$lookup?.subIssues !== undefined) {
|
||||||
statusesQuery.query(tracker.class.IssueStatus, {}, (res) => (statuses = res), {
|
statusesQuery.query(tracker.class.IssueStatus, {}, (res) => (statuses = res), {
|
||||||
lookup: { category: tracker.class.IssueStatusCategory }
|
lookup: { category: tracker.class.IssueStatusCategory }
|
||||||
})
|
})
|
||||||
|
} else {
|
||||||
|
statusesQuery.unsubscribe()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let statuses: WithLookup<IssueStatus>[] = []
|
||||||
|
|
||||||
$: if (statuses && subIssues) {
|
$: if (statuses && subIssues) {
|
||||||
const doneStatuses = statuses.filter((s) => s.category === tracker.issueStatusCategory.Completed).map((p) => p._id)
|
const doneStatuses = statuses.filter((s) => s.category === tracker.issueStatusCategory.Completed).map((p) => p._id)
|
||||||
|
@ -14,8 +14,8 @@
|
|||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Doc, DocumentQuery, Ref, SortingOrder, WithLookup } from '@hcengineering/core'
|
import { Doc, DocumentQuery, Ref, SortingOrder, WithLookup } from '@hcengineering/core'
|
||||||
import presentation, { createQuery, getClient } from '@hcengineering/presentation'
|
import presentation, { createQuery } from '@hcengineering/presentation'
|
||||||
import { calcRank, Issue, IssueStatus, Team } from '@hcengineering/tracker'
|
import { Issue, IssueStatus, Team } from '@hcengineering/tracker'
|
||||||
import { Label, Spinner } from '@hcengineering/ui'
|
import { Label, Spinner } from '@hcengineering/ui'
|
||||||
import { Viewlet, ViewOptions } from '@hcengineering/view'
|
import { Viewlet, ViewOptions } from '@hcengineering/view'
|
||||||
import tracker from '../../../plugin'
|
import tracker from '../../../plugin'
|
||||||
@ -29,23 +29,11 @@
|
|||||||
$: query = { 'relations._id': object._id, 'relations._class': object._class }
|
$: query = { 'relations._id': object._id, 'relations._class': object._class }
|
||||||
|
|
||||||
const subIssuesQuery = createQuery()
|
const subIssuesQuery = createQuery()
|
||||||
const client = getClient()
|
|
||||||
|
|
||||||
let subIssues: Issue[] = []
|
let subIssues: Issue[] = []
|
||||||
|
|
||||||
let teams: Map<Ref<Team>, Team> | undefined
|
let teams: Map<Ref<Team>, Team> | undefined
|
||||||
|
|
||||||
async function handleIssueSwap (ev: CustomEvent<{ fromIndex: number; toIndex: number }>) {
|
|
||||||
const { fromIndex, toIndex } = ev.detail
|
|
||||||
const [prev, next] = [
|
|
||||||
subIssues[fromIndex < toIndex ? toIndex : toIndex - 1],
|
|
||||||
subIssues[fromIndex < toIndex ? toIndex + 1 : toIndex]
|
|
||||||
]
|
|
||||||
const issue = subIssues[fromIndex]
|
|
||||||
|
|
||||||
await client.update(issue, { rank: calcRank(prev, next) })
|
|
||||||
}
|
|
||||||
|
|
||||||
$: subIssuesQuery.query(tracker.class.Issue, query, async (result) => (subIssues = result), {
|
$: subIssuesQuery.query(tracker.class.Issue, query, async (result) => (subIssues = result), {
|
||||||
sort: { rank: SortingOrder.Ascending }
|
sort: { rank: SortingOrder.Ascending }
|
||||||
})
|
})
|
||||||
@ -80,7 +68,7 @@
|
|||||||
<div class="mt-1">
|
<div class="mt-1">
|
||||||
{#if subIssues !== undefined && viewlet !== undefined}
|
{#if subIssues !== undefined && viewlet !== undefined}
|
||||||
{#if issueStatuses.size > 0 && teams}
|
{#if issueStatuses.size > 0 && teams}
|
||||||
<SubIssueList bind:viewOptions {viewlet} issues={subIssues} {teams} {issueStatuses} on:move={handleIssueSwap} />
|
<SubIssueList bind:viewOptions {viewlet} issues={subIssues} {teams} {issueStatuses} />
|
||||||
{:else}
|
{:else}
|
||||||
<div class="p-1">
|
<div class="p-1">
|
||||||
<Label label={presentation.string.NoMatchesFound} />
|
<Label label={presentation.string.NoMatchesFound} />
|
||||||
|
@ -35,7 +35,7 @@
|
|||||||
const statuses = createQuery()
|
const statuses = createQuery()
|
||||||
let issueStatuses: IdMap<IssueStatus> = new Map()
|
let issueStatuses: IdMap<IssueStatus> = new Map()
|
||||||
$: if (noParents !== undefined) {
|
$: if (noParents !== undefined) {
|
||||||
statuses.query(tracker.class.IssueStatus, { _id: { $in: Array.from(noParents.map((it) => it.status)) } }, (res) => {
|
statuses.query(tracker.class.IssueStatus, {}, (res) => {
|
||||||
issueStatuses = toIdMap(res)
|
issueStatuses = toIdMap(res)
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
|
@ -494,8 +494,7 @@ export function getDefaultViewOptionsTemplatesConfig (): ViewOptionModel[] {
|
|||||||
key: 'shouldShowEmptyGroups',
|
key: 'shouldShowEmptyGroups',
|
||||||
label: tracker.string.ShowEmptyGroups,
|
label: tracker.string.ShowEmptyGroups,
|
||||||
defaultValue: false,
|
defaultValue: false,
|
||||||
type: 'toggle',
|
type: 'toggle'
|
||||||
hidden: ({ groupBy }) => !['status', 'priority'].includes(groupBy)
|
|
||||||
}
|
}
|
||||||
const result: ViewOptionModel[] = [groupByCategory, orderByCategory]
|
const result: ViewOptionModel[] = [groupByCategory, orderByCategory]
|
||||||
result.push(showEmptyGroups)
|
result.push(showEmptyGroups)
|
||||||
|
@ -60,6 +60,7 @@
|
|||||||
|
|
||||||
"FilteredViews": "Filtered views",
|
"FilteredViews": "Filtered views",
|
||||||
"NewFilteredView": "New filtered view",
|
"NewFilteredView": "New filtered view",
|
||||||
"FilteredViewName": "Filtered view name"
|
"FilteredViewName": "Filtered view name",
|
||||||
|
"Than": "Than"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -57,6 +57,7 @@
|
|||||||
"Manual": "Пользовательский",
|
"Manual": "Пользовательский",
|
||||||
"FilteredViews": "Фильтрованные отображения",
|
"FilteredViews": "Фильтрованные отображения",
|
||||||
"NewFilteredView": "Новое фильтрованное отображение",
|
"NewFilteredView": "Новое фильтрованное отображение",
|
||||||
"FilteredViewName": "Имя фильтрованного отображения"
|
"FilteredViewName": "Имя фильтрованного отображения",
|
||||||
|
"Than": "Затем"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -33,26 +33,47 @@
|
|||||||
label: key === 'rank' ? view.string.Manual : getKeyLabel(client, viewlet.attachTo, key, lookup)
|
label: key === 'rank' ? view.string.Manual : getKeyLabel(client, viewlet.attachTo, key, lookup)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
$: groups =
|
||||||
|
viewOptions.groupBy[viewOptions.groupBy.length - 1] === noCategory
|
||||||
|
? viewOptions.groupBy
|
||||||
|
: [...viewOptions.groupBy, noCategory]
|
||||||
|
|
||||||
|
function selectGrouping (value: string, i: number) {
|
||||||
|
viewOptions.groupBy[i] = value
|
||||||
|
if (value !== noCategory) {
|
||||||
|
if (i + 1 === viewOptions.groupBy.length) {
|
||||||
|
viewOptions.groupBy[i + 1] = noCategory
|
||||||
|
viewOptions.groupBy = viewOptions.groupBy
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
viewOptions.groupBy.length = i + 1
|
||||||
|
viewOptions.groupBy = viewOptions.groupBy
|
||||||
|
}
|
||||||
|
dispatch('update', {
|
||||||
|
key: 'groupBy',
|
||||||
|
value: viewOptions.groupBy.length > 1 ? viewOptions.groupBy.filter((p) => p !== noCategory) : viewOptions.groupBy
|
||||||
|
})
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="antiCard">
|
<div class="antiCard">
|
||||||
<div class="antiCard-group grid">
|
<div class="antiCard-group grid">
|
||||||
<span class="label"><Label label={view.string.Grouping} /></span>
|
{#each groups as group, i}
|
||||||
<div class="value">
|
<span class="label"><Label label={i === 0 ? view.string.Grouping : view.string.Than} /></span>
|
||||||
|
<div class="value grouping">
|
||||||
<DropdownLabelsIntl
|
<DropdownLabelsIntl
|
||||||
label={view.string.Grouping}
|
label={view.string.Grouping}
|
||||||
items={groupBy}
|
items={groupBy.filter((p) => !viewOptions.groupBy.includes(p.id) || [group, noCategory].includes(p.id))}
|
||||||
selected={viewOptions.groupBy}
|
selected={group}
|
||||||
width="10rem"
|
width="10rem"
|
||||||
justify="left"
|
justify="left"
|
||||||
on:selected={(e) => {
|
on:selected={(e) => selectGrouping(e.detail, i)}
|
||||||
viewOptions.groupBy = e.detail
|
|
||||||
dispatch('update', { key: 'groupBy', value: e.detail })
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
{/each}
|
||||||
<span class="label"><Label label={view.string.Ordering} /></span>
|
<span class="label"><Label label={view.string.Ordering} /></span>
|
||||||
<div class="value">
|
<div class="value ordering">
|
||||||
<DropdownLabelsIntl
|
<DropdownLabelsIntl
|
||||||
label={view.string.Ordering}
|
label={view.string.Ordering}
|
||||||
items={orderBy}
|
items={orderBy}
|
||||||
|
@ -13,21 +13,14 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import core, { Class, Doc, DocumentQuery, FindOptions, Ref, Space } from '@hcengineering/core'
|
import { Class, Doc, DocumentQuery, FindOptions, Ref, Space } from '@hcengineering/core'
|
||||||
import { getResource, IntlString } from '@hcengineering/platform'
|
import { getResource, IntlString } from '@hcengineering/platform'
|
||||||
import { createQuery, getClient } from '@hcengineering/presentation'
|
import { createQuery, getClient } from '@hcengineering/presentation'
|
||||||
import { AnyComponent } from '@hcengineering/ui'
|
import { AnyComponent } from '@hcengineering/ui'
|
||||||
import view, {
|
import { BuildModelKey, ViewOptionModel, ViewOptions, ViewQueryOption } from '@hcengineering/view'
|
||||||
AttributeModel,
|
|
||||||
BuildModelKey,
|
|
||||||
ViewOptionModel,
|
|
||||||
ViewOptions,
|
|
||||||
ViewQueryOption
|
|
||||||
} from '@hcengineering/view'
|
|
||||||
import { createEventDispatcher } from 'svelte'
|
import { createEventDispatcher } from 'svelte'
|
||||||
import { buildConfigLookup, buildModel, getCategories, getPresenter, groupBy, LoadingProps } from '../../utils'
|
import { buildConfigLookup, LoadingProps } from '../../utils'
|
||||||
import { noCategory } from '../../viewOptions'
|
import ListCategories from './ListCategories.svelte'
|
||||||
import ListCategory from './ListCategory.svelte'
|
|
||||||
|
|
||||||
export let _class: Ref<Class<Doc>>
|
export let _class: Ref<Class<Doc>>
|
||||||
export let space: Ref<Space> | undefined = undefined
|
export let space: Ref<Space> | undefined = undefined
|
||||||
@ -36,7 +29,6 @@
|
|||||||
export let baseMenuClass: Ref<Class<Doc>> | undefined = undefined
|
export let baseMenuClass: Ref<Class<Doc>> | undefined = undefined
|
||||||
export let config: (string | BuildModelKey)[]
|
export let config: (string | BuildModelKey)[]
|
||||||
export let selectedObjectIds: Doc[] = []
|
export let selectedObjectIds: Doc[] = []
|
||||||
export let selectedRowIndex: number | undefined = undefined
|
|
||||||
export let loadingProps: LoadingProps | undefined = undefined
|
export let loadingProps: LoadingProps | undefined = undefined
|
||||||
export let createItemDialog: AnyComponent | undefined = undefined
|
export let createItemDialog: AnyComponent | undefined = undefined
|
||||||
export let createItemLabel: IntlString | undefined = undefined
|
export let createItemLabel: IntlString | undefined = undefined
|
||||||
@ -47,18 +39,16 @@
|
|||||||
|
|
||||||
export let documents: Doc[] | undefined = undefined
|
export let documents: Doc[] | undefined = undefined
|
||||||
|
|
||||||
const objectRefs: HTMLElement[] = []
|
const elementByIndex: Map<number, HTMLDivElement> = new Map()
|
||||||
|
const docByIndex: Map<number, Doc> = new Map()
|
||||||
|
const indexById: Map<Ref<Doc>, number> = new Map()
|
||||||
|
|
||||||
let docs: Doc[] = []
|
let docs: Doc[] = []
|
||||||
|
|
||||||
$: groupByKey = viewOptions.groupBy ?? noCategory
|
|
||||||
$: orderBy = viewOptions.orderBy
|
$: orderBy = viewOptions.orderBy
|
||||||
$: groupedDocs = groupBy(docs, groupByKey)
|
|
||||||
let categories: any[] = []
|
|
||||||
$: getCategories(client, _class, docs, groupByKey).then((p) => {
|
|
||||||
categories = p
|
|
||||||
})
|
|
||||||
|
|
||||||
const docsQuery = createQuery()
|
const docsQuery = createQuery()
|
||||||
|
$: lookup = options?.lookup ?? buildConfigLookup(client.getHierarchy(), _class, config)
|
||||||
$: resultOptions = { lookup, ...options, sort: { [orderBy[0]]: orderBy[1] } }
|
$: resultOptions = { lookup, ...options, sort: { [orderBy[0]]: orderBy[1] } }
|
||||||
|
|
||||||
let resultQuery: DocumentQuery<Doc> = query
|
let resultQuery: DocumentQuery<Doc> = query
|
||||||
@ -86,13 +76,6 @@
|
|||||||
const client = getClient()
|
const client = getClient()
|
||||||
const hierarchy = client.getHierarchy()
|
const hierarchy = client.getHierarchy()
|
||||||
|
|
||||||
$: lookup = options?.lookup ?? buildConfigLookup(client.getHierarchy(), _class, config)
|
|
||||||
|
|
||||||
const spaceQuery = createQuery()
|
|
||||||
|
|
||||||
let currentSpace: Space | undefined
|
|
||||||
let itemModels: AttributeModel[]
|
|
||||||
|
|
||||||
async function getResultQuery (
|
async function getResultQuery (
|
||||||
query: DocumentQuery<Doc>,
|
query: DocumentQuery<Doc>,
|
||||||
viewOptions: ViewOptionModel[] | undefined,
|
viewOptions: ViewOptionModel[] | undefined,
|
||||||
@ -109,6 +92,34 @@
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function uncheckAll () {
|
||||||
|
dispatch('check', { docs, value: false })
|
||||||
|
selectedObjectIds = []
|
||||||
|
}
|
||||||
|
|
||||||
|
export function select (offset: 1 | -1 | 0, of?: Doc): void {
|
||||||
|
let pos = (of !== undefined ? indexById.get(of._id) : -1) ?? -1
|
||||||
|
pos += offset
|
||||||
|
if (pos < 0) {
|
||||||
|
pos = 0
|
||||||
|
}
|
||||||
|
if (pos >= docs.length) {
|
||||||
|
pos = docs.length - 1
|
||||||
|
}
|
||||||
|
const target = docByIndex.get(pos)
|
||||||
|
if (target !== undefined) {
|
||||||
|
onRow(target)
|
||||||
|
}
|
||||||
|
const r = elementByIndex.get(pos)
|
||||||
|
if (r !== undefined) {
|
||||||
|
r.scrollIntoView({ behavior: 'auto', block: 'nearest' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onRow (object: Doc): void {
|
||||||
|
dispatch('row-focus', object)
|
||||||
|
}
|
||||||
|
|
||||||
const getLoadingElementsLength = (props: LoadingProps | undefined, options?: FindOptions<Doc>) => {
|
const getLoadingElementsLength = (props: LoadingProps | undefined, options?: FindOptions<Doc>) => {
|
||||||
if (!props) return undefined
|
if (!props) return undefined
|
||||||
if (options?.limit && options.limit > 0) {
|
if (options?.limit && options.limit > 0) {
|
||||||
@ -117,104 +128,34 @@
|
|||||||
|
|
||||||
return props.length
|
return props.length
|
||||||
}
|
}
|
||||||
|
|
||||||
$: spaceQuery.query(core.class.Space, { _id: space }, (res) => {
|
|
||||||
;[currentSpace] = res
|
|
||||||
})
|
|
||||||
|
|
||||||
function getHeader (_class: Ref<Class<Doc>>, groupByKey: string): void {
|
|
||||||
if (groupByKey === noCategory) {
|
|
||||||
headerComponent = undefined
|
|
||||||
} else {
|
|
||||||
getPresenter(client, _class, { key: groupByKey }, { key: groupByKey }).then((p) => (headerComponent = p))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let headerComponent: AttributeModel | undefined
|
|
||||||
$: getHeader(_class, groupByKey)
|
|
||||||
$: buildModel({ client, _class, keys: config, lookup }).then((res) => {
|
|
||||||
itemModels = res
|
|
||||||
})
|
|
||||||
|
|
||||||
function getInitIndex (categories: any, i: number): number {
|
|
||||||
let res = 0
|
|
||||||
for (let index = 0; index < i; index++) {
|
|
||||||
const cat = categories[index]
|
|
||||||
res += groupedDocs[cat].length
|
|
||||||
}
|
|
||||||
return res
|
|
||||||
}
|
|
||||||
|
|
||||||
function uncheckAll () {
|
|
||||||
dispatch('check', { docs, value: false })
|
|
||||||
selectedObjectIds = []
|
|
||||||
}
|
|
||||||
|
|
||||||
$: extraHeaders = getAdditionalHeader(_class)
|
|
||||||
|
|
||||||
function getAdditionalHeader (_class: Ref<Class<Doc>>): AnyComponent[] | undefined {
|
|
||||||
const clazz = hierarchy.getClass(_class)
|
|
||||||
let mixinClazz = hierarchy.getClass(_class)
|
|
||||||
let presenterMixin = hierarchy.as(clazz, view.mixin.ListHeaderExtra)
|
|
||||||
while (presenterMixin.presenters === undefined && mixinClazz.extends !== undefined) {
|
|
||||||
presenterMixin = hierarchy.as(mixinClazz, view.mixin.ListHeaderExtra)
|
|
||||||
mixinClazz = hierarchy.getClass(mixinClazz.extends)
|
|
||||||
}
|
|
||||||
return presenterMixin.presenters
|
|
||||||
}
|
|
||||||
|
|
||||||
$: flat = Object.values(groupedDocs).flat(1)
|
|
||||||
|
|
||||||
export function select (offset: 1 | -1 | 0, of?: Doc): void {
|
|
||||||
let pos = (of !== undefined ? flat.findIndex((it) => it._id === of._id) : selectedRowIndex) ?? -1
|
|
||||||
pos += offset
|
|
||||||
if (pos < 0) {
|
|
||||||
pos = 0
|
|
||||||
}
|
|
||||||
if (pos >= flat.length) {
|
|
||||||
pos = flat.length - 1
|
|
||||||
}
|
|
||||||
const r = objectRefs[pos]
|
|
||||||
selectedRowIndex = pos
|
|
||||||
onRow(flat[pos])
|
|
||||||
if (r !== undefined) {
|
|
||||||
r?.scrollIntoView({ behavior: 'auto', block: 'nearest' })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function onRow (object: Doc): void {
|
|
||||||
dispatch('row-focus', object)
|
|
||||||
}
|
|
||||||
|
|
||||||
$: objectRefs.length = flat.length
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="list-container">
|
<div class="list-container">
|
||||||
{#each categories as category, i}
|
<ListCategories
|
||||||
{@const items = groupedDocs[category] ?? []}
|
newObjectProps={space ? { space } : {}}
|
||||||
<ListCategory
|
{elementByIndex}
|
||||||
bind:selectedRowIndex
|
{indexById}
|
||||||
{extraHeaders}
|
{docs}
|
||||||
|
{_class}
|
||||||
{space}
|
{space}
|
||||||
{selectedObjectIds}
|
{lookup}
|
||||||
{headerComponent}
|
loadingPropsLength={getLoadingElementsLength(loadingProps, options)}
|
||||||
initIndex={getInitIndex(categories, i)}
|
|
||||||
{baseMenuClass}
|
{baseMenuClass}
|
||||||
{groupByKey}
|
{config}
|
||||||
{itemModels}
|
{viewOptions}
|
||||||
singleCat={categories.length === 1}
|
{docByIndex}
|
||||||
{category}
|
{viewOptionsConfig}
|
||||||
{items}
|
{selectedObjectIds}
|
||||||
|
level={0}
|
||||||
{createItemDialog}
|
{createItemDialog}
|
||||||
{createItemLabel}
|
{createItemLabel}
|
||||||
loadingPropsLength={getLoadingElementsLength(loadingProps, options)}
|
{loadingProps}
|
||||||
on:check
|
on:check
|
||||||
on:uncheckAll={uncheckAll}
|
on:uncheckAll={uncheckAll}
|
||||||
on:row-focus
|
on:row-focus
|
||||||
{flatHeaders}
|
{flatHeaders}
|
||||||
{props}
|
{props}
|
||||||
/>
|
/>
|
||||||
{/each}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
125
plugins/view-resources/src/components/list/ListCategories.svelte
Normal file
125
plugins/view-resources/src/components/list/ListCategories.svelte
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
<!--
|
||||||
|
// 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, Lookup, Ref, Space } from '@hcengineering/core'
|
||||||
|
import { IntlString } from '@hcengineering/platform'
|
||||||
|
import { getClient } from '@hcengineering/presentation'
|
||||||
|
import { AnyComponent } from '@hcengineering/ui'
|
||||||
|
import view, { AttributeModel, BuildModelKey, ViewOptions } from '@hcengineering/view'
|
||||||
|
import { buildModel, getCategories, getPresenter, groupBy } from '../../utils'
|
||||||
|
import { noCategory } from '../../viewOptions'
|
||||||
|
import ListCategory from './ListCategory.svelte'
|
||||||
|
|
||||||
|
export let elementByIndex: Map<number, HTMLDivElement>
|
||||||
|
export let indexById: Map<Ref<Doc>, number>
|
||||||
|
export let docs: Doc[]
|
||||||
|
export let _class: Ref<Class<Doc>>
|
||||||
|
export let space: Ref<Space> | undefined
|
||||||
|
export let lookup: Lookup<Doc>
|
||||||
|
export let loadingPropsLength: number | undefined
|
||||||
|
export let baseMenuClass: Ref<Class<Doc>> | undefined
|
||||||
|
export let config: (string | BuildModelKey)[]
|
||||||
|
export let selectedObjectIds: Doc[] = []
|
||||||
|
export let createItemDialog: AnyComponent | undefined
|
||||||
|
export let createItemLabel: IntlString | undefined
|
||||||
|
export let viewOptions: ViewOptions
|
||||||
|
export let flatHeaders = false
|
||||||
|
export let props: Record<string, any> = {}
|
||||||
|
export let level: number
|
||||||
|
export let initIndex = 0
|
||||||
|
export let newObjectProps: Record<string, any>
|
||||||
|
export let docByIndex: Map<number, Doc>
|
||||||
|
|
||||||
|
$: groupByKey = viewOptions.groupBy[level] ?? noCategory
|
||||||
|
$: groupedDocs = groupBy(docs, groupByKey)
|
||||||
|
let categories: any[] = []
|
||||||
|
$: getCategories(client, _class, docs, groupByKey).then((p) => {
|
||||||
|
categories = p
|
||||||
|
})
|
||||||
|
|
||||||
|
const client = getClient()
|
||||||
|
const hierarchy = client.getHierarchy()
|
||||||
|
|
||||||
|
let itemModels: AttributeModel[]
|
||||||
|
|
||||||
|
function getHeader (_class: Ref<Class<Doc>>, groupByKey: string): void {
|
||||||
|
if (groupByKey === noCategory) {
|
||||||
|
headerComponent = undefined
|
||||||
|
} else {
|
||||||
|
getPresenter(client, _class, { key: groupByKey }, { key: groupByKey }).then((p) => (headerComponent = p))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let headerComponent: AttributeModel | undefined
|
||||||
|
$: getHeader(_class, groupByKey)
|
||||||
|
$: buildModel({ client, _class, keys: config, lookup }).then((res) => {
|
||||||
|
itemModels = res
|
||||||
|
})
|
||||||
|
|
||||||
|
function getInitIndex (categories: any, i: number): number {
|
||||||
|
let res = initIndex
|
||||||
|
for (let index = 0; index < i; index++) {
|
||||||
|
const cat = categories[index]
|
||||||
|
res += groupedDocs[cat]?.length
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
$: extraHeaders = getAdditionalHeader(_class)
|
||||||
|
|
||||||
|
function getAdditionalHeader (_class: Ref<Class<Doc>>): AnyComponent[] | undefined {
|
||||||
|
const clazz = hierarchy.getClass(_class)
|
||||||
|
let mixinClazz = hierarchy.getClass(_class)
|
||||||
|
let presenterMixin = hierarchy.as(clazz, view.mixin.ListHeaderExtra)
|
||||||
|
while (presenterMixin.presenters === undefined && mixinClazz.extends !== undefined) {
|
||||||
|
presenterMixin = hierarchy.as(mixinClazz, view.mixin.ListHeaderExtra)
|
||||||
|
mixinClazz = hierarchy.getClass(mixinClazz.extends)
|
||||||
|
}
|
||||||
|
return presenterMixin.presenters
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#each categories as category, i}
|
||||||
|
{@const items = groupedDocs[category] ?? []}
|
||||||
|
<ListCategory
|
||||||
|
{elementByIndex}
|
||||||
|
{indexById}
|
||||||
|
{extraHeaders}
|
||||||
|
{space}
|
||||||
|
{selectedObjectIds}
|
||||||
|
{headerComponent}
|
||||||
|
initIndex={getInitIndex(categories, i)}
|
||||||
|
{baseMenuClass}
|
||||||
|
{level}
|
||||||
|
{viewOptions}
|
||||||
|
{groupByKey}
|
||||||
|
{config}
|
||||||
|
{docByIndex}
|
||||||
|
{itemModels}
|
||||||
|
{_class}
|
||||||
|
singleCat={level === 0 && categories.length === 1}
|
||||||
|
{category}
|
||||||
|
{items}
|
||||||
|
{newObjectProps}
|
||||||
|
{createItemDialog}
|
||||||
|
{createItemLabel}
|
||||||
|
{loadingPropsLength}
|
||||||
|
on:check
|
||||||
|
on:uncheckAll
|
||||||
|
on:row-focus
|
||||||
|
{flatHeaders}
|
||||||
|
{props}
|
||||||
|
/>
|
||||||
|
{/each}
|
@ -13,7 +13,7 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Class, Doc, Ref, Space } from '@hcengineering/core'
|
import { Class, Doc, Lookup, Ref, Space } from '@hcengineering/core'
|
||||||
import { IntlString } from '@hcengineering/platform'
|
import { IntlString } from '@hcengineering/platform'
|
||||||
import {
|
import {
|
||||||
AnyComponent,
|
AnyComponent,
|
||||||
@ -23,9 +23,11 @@
|
|||||||
showPopup,
|
showPopup,
|
||||||
Spinner
|
Spinner
|
||||||
} from '@hcengineering/ui'
|
} from '@hcengineering/ui'
|
||||||
import { AttributeModel } from '@hcengineering/view'
|
import { AttributeModel, BuildModelKey, ViewOptions } from '@hcengineering/view'
|
||||||
import { createEventDispatcher } from 'svelte'
|
import { createEventDispatcher } from 'svelte'
|
||||||
|
import { FocusSelection, focusStore } from '../../selection'
|
||||||
import Menu from '../Menu.svelte'
|
import Menu from '../Menu.svelte'
|
||||||
|
import ListCategories from './ListCategories.svelte'
|
||||||
import ListHeader from './ListHeader.svelte'
|
import ListHeader from './ListHeader.svelte'
|
||||||
import ListItem from './ListItem.svelte'
|
import ListItem from './ListItem.svelte'
|
||||||
|
|
||||||
@ -42,17 +44,26 @@
|
|||||||
export let loadingPropsLength: number | undefined
|
export let loadingPropsLength: number | undefined
|
||||||
export let selectedObjectIds: Doc[]
|
export let selectedObjectIds: Doc[]
|
||||||
export let itemModels: AttributeModel[]
|
export let itemModels: AttributeModel[]
|
||||||
export let selectedRowIndex: number | undefined
|
|
||||||
export let extraHeaders: AnyComponent[] | undefined
|
export let extraHeaders: AnyComponent[] | undefined
|
||||||
export let objectRefs: HTMLElement[] = []
|
|
||||||
export let flatHeaders = false
|
export let flatHeaders = false
|
||||||
export let props: Record<string, any> = {}
|
export let props: Record<string, any> = {}
|
||||||
|
export let level: number
|
||||||
|
export let elementByIndex: Map<number, HTMLDivElement>
|
||||||
|
export let indexById: Map<Ref<Doc>, number>
|
||||||
|
export let lookup: Lookup<Doc>
|
||||||
|
export let _class: Ref<Class<Doc>>
|
||||||
|
export let config: (string | BuildModelKey)[]
|
||||||
|
export let viewOptions: ViewOptions
|
||||||
|
export let newObjectProps: Record<string, any>
|
||||||
|
export let docByIndex: Map<number, Doc>
|
||||||
|
|
||||||
|
$: lastLevel = level + 1 >= viewOptions.groupBy.length
|
||||||
|
|
||||||
const autoFoldLimit = 20
|
const autoFoldLimit = 20
|
||||||
const defaultLimit = 20
|
const defaultLimit = 20
|
||||||
const singleCategoryLimit = 200
|
const singleCategoryLimit = 200
|
||||||
$: initialLimit = singleCat ? singleCategoryLimit : defaultLimit
|
$: initialLimit = singleCat ? singleCategoryLimit : defaultLimit
|
||||||
$: limit = initialLimit
|
$: limit = !lastLevel ? items.length : initialLimit
|
||||||
|
|
||||||
let collapsed = true
|
let collapsed = true
|
||||||
|
|
||||||
@ -62,11 +73,11 @@
|
|||||||
return items.slice(0, limit)
|
return items.slice(0, limit)
|
||||||
}
|
}
|
||||||
|
|
||||||
function initCollapsed (singleCat: boolean, category: any): void {
|
function initCollapsed (singleCat: boolean, lastLevel: boolean, category: any): void {
|
||||||
collapsed = !singleCat && items.length > autoFoldLimit
|
collapsed = !singleCat && items.length > (lastLevel ? autoFoldLimit : singleCategoryLimit)
|
||||||
}
|
}
|
||||||
|
|
||||||
$: initCollapsed(singleCat, category)
|
$: initCollapsed(singleCat, lastLevel, category)
|
||||||
|
|
||||||
const handleRowFocused = (object: Doc) => {
|
const handleRowFocused = (object: Doc) => {
|
||||||
dispatch('row-focus', object)
|
dispatch('row-focus', object)
|
||||||
@ -74,7 +85,7 @@
|
|||||||
|
|
||||||
const handleMenuOpened = async (event: MouseEvent, object: Doc, rowIndex: number) => {
|
const handleMenuOpened = async (event: MouseEvent, object: Doc, rowIndex: number) => {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
selectedRowIndex = rowIndex
|
handleRowFocused(object)
|
||||||
|
|
||||||
if (!selectedObjectIdsSet.has(object._id)) {
|
if (!selectedObjectIdsSet.has(object._id)) {
|
||||||
dispatch('uncheckAll')
|
dispatch('uncheckAll')
|
||||||
@ -83,24 +94,32 @@
|
|||||||
const items = selectedObjectIds.length > 0 ? selectedObjectIds : object
|
const items = selectedObjectIds.length > 0 ? selectedObjectIds : object
|
||||||
|
|
||||||
showPopup(Menu, { object: items, baseMenuClass }, getEventPositionElement(event), () => {
|
showPopup(Menu, { object: items, baseMenuClass }, getEventPositionElement(event), () => {
|
||||||
selectedRowIndex = undefined
|
dispatch('row-focus')
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
$: limited = limitGroup(items, limit)
|
$: limited = limitGroup(items, limit)
|
||||||
$: selectedObjectIdsSet = new Set<Ref<Doc>>(selectedObjectIds.map((it) => it._id))
|
$: selectedObjectIdsSet = new Set<Ref<Doc>>(selectedObjectIds.map((it) => it._id))
|
||||||
|
|
||||||
|
$: newObjectProps = { [groupByKey]: category, ...newObjectProps }
|
||||||
|
|
||||||
|
function isSelected (doc: Doc, focusStore: FocusSelection): boolean {
|
||||||
|
return focusStore.focus?._id === doc._id
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ListHeader
|
<ListHeader
|
||||||
{groupByKey}
|
{groupByKey}
|
||||||
{category}
|
{category}
|
||||||
{space}
|
{space}
|
||||||
|
{level}
|
||||||
limited={limited.length}
|
limited={limited.length}
|
||||||
{items}
|
{items}
|
||||||
{headerComponent}
|
{headerComponent}
|
||||||
{createItemDialog}
|
{createItemDialog}
|
||||||
{createItemLabel}
|
{createItemLabel}
|
||||||
{extraHeaders}
|
{extraHeaders}
|
||||||
|
{newObjectProps}
|
||||||
flat={flatHeaders}
|
flat={flatHeaders}
|
||||||
{props}
|
{props}
|
||||||
on:more={() => {
|
on:more={() => {
|
||||||
@ -111,15 +130,45 @@
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<ExpandCollapse isExpanded={!collapsed} duration={400}>
|
<ExpandCollapse isExpanded={!collapsed} duration={400}>
|
||||||
{#if itemModels}
|
{#if !lastLevel}
|
||||||
|
<div class="p-2">
|
||||||
|
<ListCategories
|
||||||
|
{elementByIndex}
|
||||||
|
{indexById}
|
||||||
|
docs={items}
|
||||||
|
{_class}
|
||||||
|
{space}
|
||||||
|
{lookup}
|
||||||
|
{loadingPropsLength}
|
||||||
|
{baseMenuClass}
|
||||||
|
{config}
|
||||||
|
{selectedObjectIds}
|
||||||
|
{createItemDialog}
|
||||||
|
{createItemLabel}
|
||||||
|
{viewOptions}
|
||||||
|
{newObjectProps}
|
||||||
|
{flatHeaders}
|
||||||
|
{props}
|
||||||
|
level={level + 1}
|
||||||
|
{initIndex}
|
||||||
|
{docByIndex}
|
||||||
|
on:check
|
||||||
|
on:uncheckAll
|
||||||
|
on:row-focus
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{:else if itemModels}
|
||||||
{#if limited}
|
{#if limited}
|
||||||
{#each limited as docObject, i (docObject._id)}
|
{#each limited as docObject, i (docObject._id)}
|
||||||
<ListItem
|
<ListItem
|
||||||
bind:use={objectRefs[initIndex + i]}
|
|
||||||
{docObject}
|
{docObject}
|
||||||
|
{elementByIndex}
|
||||||
|
{docByIndex}
|
||||||
|
{indexById}
|
||||||
model={itemModels}
|
model={itemModels}
|
||||||
|
index={initIndex + i}
|
||||||
{groupByKey}
|
{groupByKey}
|
||||||
selected={selectedRowIndex === initIndex + i}
|
selected={isSelected(docObject, $focusStore)}
|
||||||
checked={selectedObjectIdsSet.has(docObject._id)}
|
checked={selectedObjectIdsSet.has(docObject._id)}
|
||||||
on:check={(ev) => dispatch('check', { docs: ev.detail.docs, value: ev.detail.value })}
|
on:check={(ev) => dispatch('check', { docs: ev.detail.docs, value: ev.detail.value })}
|
||||||
on:contextmenu={(event) => handleMenuOpened(event, docObject, initIndex + i)}
|
on:contextmenu={(event) => handleMenuOpened(event, docObject, initIndex + i)}
|
||||||
@ -131,7 +180,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
{:else if loadingPropsLength !== undefined}
|
{:else if loadingPropsLength !== undefined}
|
||||||
{#each Array(Math.max(loadingPropsLength, limit)) as _, rowIndex}
|
{#each Array(Math.max(loadingPropsLength, limit)) as _, rowIndex}
|
||||||
<div class="listGrid row" class:fixed={rowIndex === selectedRowIndex}>
|
<div class="listGrid row">
|
||||||
<div class="flex-center clear-mins h-full">
|
<div class="flex-center clear-mins h-full">
|
||||||
<div class="gridElement">
|
<div class="gridElement">
|
||||||
<CheckBox checked={false} />
|
<CheckBox checked={false} />
|
||||||
|
@ -43,18 +43,26 @@
|
|||||||
export let extraHeaders: AnyComponent[] | undefined
|
export let extraHeaders: AnyComponent[] | undefined
|
||||||
export let flat = false
|
export let flat = false
|
||||||
export let props: Record<string, any> = {}
|
export let props: Record<string, any> = {}
|
||||||
|
export let level: number
|
||||||
|
export let newObjectProps: Record<string, any>
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
const handleCreateItem = (event: MouseEvent, category: string) => {
|
const handleCreateItem = (event: MouseEvent) => {
|
||||||
if (createItemDialog === undefined) return
|
if (createItemDialog === undefined) return
|
||||||
showPopup(createItemDialog, { space, ...(groupByKey ? { [groupByKey]: category } : {}) }, eventToHTMLElement(event))
|
showPopup(createItemDialog, newObjectProps, eventToHTMLElement(event))
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if headerComponent || groupByKey === noCategory}
|
{#if headerComponent || groupByKey === noCategory}
|
||||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
<div class="flex-between categoryHeader row" class:flat on:click={() => dispatch('collapse')}>
|
<div
|
||||||
|
class="flex-between categoryHeader row"
|
||||||
|
style:z-index={10 - level}
|
||||||
|
class:flat
|
||||||
|
class:subLevel={level !== 0}
|
||||||
|
on:click={() => dispatch('collapse')}
|
||||||
|
>
|
||||||
<div class="flex-row-center gap-2 clear-mins caption-color">
|
<div class="flex-row-center gap-2 clear-mins caption-color">
|
||||||
<FixedColumn key={`list_groupBy_${groupByKey}`} justify={'left'}>
|
<FixedColumn key={`list_groupBy_${groupByKey}`} justify={'left'}>
|
||||||
{#if groupByKey === noCategory}
|
{#if groupByKey === noCategory}
|
||||||
@ -95,7 +103,7 @@
|
|||||||
icon={IconAdd}
|
icon={IconAdd}
|
||||||
kind={'transparent'}
|
kind={'transparent'}
|
||||||
showTooltip={{ label: createItemLabel }}
|
showTooltip={{ label: createItemLabel }}
|
||||||
on:click={(event) => handleCreateItem(event, category)}
|
on:click={handleCreateItem}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
@ -110,7 +118,13 @@
|
|||||||
min-height: 3rem;
|
min-height: 3rem;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
background: var(--header-bg-color);
|
background: var(--header-bg-color);
|
||||||
z-index: 5;
|
|
||||||
|
&.subLevel {
|
||||||
|
min-height: 2.25rem;
|
||||||
|
height: 2.25rem;
|
||||||
|
padding: 0 0.75rem 0 2.25rem;
|
||||||
|
// here shoul be top 3rem for sticky, but with ExpandCollapse it gives strange behavior
|
||||||
|
}
|
||||||
|
|
||||||
&.flat {
|
&.flat {
|
||||||
background: var(--header-bg-color);
|
background: var(--header-bg-color);
|
||||||
|
@ -13,7 +13,7 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Doc, getObjectValue } from '@hcengineering/core'
|
import { Doc, getObjectValue, Ref } from '@hcengineering/core'
|
||||||
import notification from '@hcengineering/notification'
|
import notification from '@hcengineering/notification'
|
||||||
import { CheckBox, Component, deviceOptionsStore as deviceInfo, tooltip } from '@hcengineering/ui'
|
import { CheckBox, Component, deviceOptionsStore as deviceInfo, tooltip } from '@hcengineering/ui'
|
||||||
import { AttributeModel } from '@hcengineering/view'
|
import { AttributeModel } from '@hcengineering/view'
|
||||||
@ -22,21 +22,38 @@
|
|||||||
import view from '../../plugin'
|
import view from '../../plugin'
|
||||||
import Circles from '../icons/Circles.svelte'
|
import Circles from '../icons/Circles.svelte'
|
||||||
|
|
||||||
export let use: HTMLElement
|
|
||||||
export let docObject: Doc
|
export let docObject: Doc
|
||||||
|
export let index: number
|
||||||
export let model: AttributeModel[]
|
export let model: AttributeModel[]
|
||||||
export let groupByKey: string | undefined
|
export let groupByKey: string | undefined
|
||||||
export let checked: boolean
|
export let checked: boolean
|
||||||
export let selected: boolean
|
export let selected: boolean
|
||||||
export let props: Record<string, any> = {}
|
export let props: Record<string, any> = {}
|
||||||
|
export let elementByIndex: Map<number, HTMLDivElement>
|
||||||
|
export let indexById: Map<Ref<Doc>, number>
|
||||||
|
export let docByIndex: Map<number, Doc>
|
||||||
|
|
||||||
|
let elem: HTMLDivElement
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
|
export function getDoc () {
|
||||||
|
return docObject
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getElement () {
|
||||||
|
return elem
|
||||||
|
}
|
||||||
|
|
||||||
$: compactMode = $deviceInfo.twoRows
|
$: compactMode = $deviceInfo.twoRows
|
||||||
|
|
||||||
|
$: elem && elementByIndex.set(index, elem)
|
||||||
|
$: indexById.set(docObject._id, index)
|
||||||
|
$: docByIndex.set(index, docObject)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
bind:this={use}
|
bind:this={elem}
|
||||||
class="listGrid antiList__row row gap-2 flex-grow"
|
class="listGrid antiList__row row gap-2 flex-grow"
|
||||||
class:checking={checked}
|
class:checking={checked}
|
||||||
class:mListGridFixed={selected}
|
class:mListGridFixed={selected}
|
||||||
|
@ -4,14 +4,7 @@
|
|||||||
import { AnyComponent, issueSP, Scroller } from '@hcengineering/ui'
|
import { AnyComponent, issueSP, Scroller } from '@hcengineering/ui'
|
||||||
import { BuildModelKey, Viewlet, ViewOptions } from '@hcengineering/view'
|
import { BuildModelKey, Viewlet, ViewOptions } from '@hcengineering/view'
|
||||||
import { onMount } from 'svelte'
|
import { onMount } from 'svelte'
|
||||||
import {
|
import { ActionContext, ListSelectionProvider, LoadingProps, SelectDirection, selectionStore } from '../..'
|
||||||
ActionContext,
|
|
||||||
focusStore,
|
|
||||||
ListSelectionProvider,
|
|
||||||
LoadingProps,
|
|
||||||
SelectDirection,
|
|
||||||
selectionStore
|
|
||||||
} from '../..'
|
|
||||||
|
|
||||||
import List from './List.svelte'
|
import List from './List.svelte'
|
||||||
|
|
||||||
@ -63,7 +56,6 @@
|
|||||||
{props}
|
{props}
|
||||||
viewOptionsConfig={viewlet.viewOptions?.other}
|
viewOptionsConfig={viewlet.viewOptions?.other}
|
||||||
selectedObjectIds={$selectionStore ?? []}
|
selectedObjectIds={$selectionStore ?? []}
|
||||||
selectedRowIndex={listProvider.current($focusStore)}
|
|
||||||
on:row-focus={(event) => {
|
on:row-focus={(event) => {
|
||||||
listProvider.updateFocus(event.detail ?? undefined)
|
listProvider.updateFocus(event.detail ?? undefined)
|
||||||
}}
|
}}
|
||||||
|
@ -60,6 +60,7 @@ export default mergeIds(viewId, view, {
|
|||||||
NoGrouping: '' as IntlString,
|
NoGrouping: '' as IntlString,
|
||||||
Grouping: '' as IntlString,
|
Grouping: '' as IntlString,
|
||||||
Ordering: '' as IntlString,
|
Ordering: '' as IntlString,
|
||||||
Manual: '' as IntlString
|
Manual: '' as IntlString,
|
||||||
|
Than: '' as IntlString
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -1,11 +1,18 @@
|
|||||||
import { SortingOrder } from '@hcengineering/core'
|
import { SortingOrder } from '@hcengineering/core'
|
||||||
import { getCurrentLocation, locationToUrl } from '@hcengineering/ui'
|
import { getCurrentLocation, locationToUrl } from '@hcengineering/ui'
|
||||||
import { DropdownViewOption, ToggleViewOption, Viewlet, ViewOptionModel, ViewOptions } from '@hcengineering/view'
|
import {
|
||||||
|
DropdownViewOption,
|
||||||
|
ToggleViewOption,
|
||||||
|
Viewlet,
|
||||||
|
ViewOptionModel,
|
||||||
|
ViewOptions,
|
||||||
|
ViewOptionsModel
|
||||||
|
} from '@hcengineering/view'
|
||||||
|
|
||||||
export const noCategory = '#no_category'
|
export const noCategory = '#no_category'
|
||||||
|
|
||||||
export const defaulOptions: ViewOptions = {
|
export const defaulOptions: ViewOptions = {
|
||||||
groupBy: noCategory,
|
groupBy: [noCategory],
|
||||||
orderBy: ['modifiedBy', SortingOrder.Descending]
|
orderBy: ['modifiedBy', SortingOrder.Descending]
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -41,9 +48,37 @@ function _getViewOptions (viewlet: Viewlet): ViewOptions | null {
|
|||||||
return JSON.parse(options)
|
return JSON.parse(options)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getDefaults (viewOptions: ViewOptionsModel): ViewOptions {
|
||||||
|
const res: ViewOptions = {
|
||||||
|
groupBy: [viewOptions.groupBy[0]],
|
||||||
|
orderBy: viewOptions.orderBy[0]
|
||||||
|
}
|
||||||
|
for (const opt of viewOptions.other) {
|
||||||
|
res[opt.key] = opt.defaultValue
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
export function getViewOptions (viewlet: Viewlet | undefined, defaults = defaulOptions): ViewOptions {
|
export function getViewOptions (viewlet: Viewlet | undefined, defaults = defaulOptions): ViewOptions {
|
||||||
if (viewlet === undefined) {
|
if (viewlet === undefined) {
|
||||||
return { ...defaults }
|
return { ...defaults }
|
||||||
}
|
}
|
||||||
return _getViewOptions(viewlet) ?? defaults
|
const res = _getViewOptions(viewlet)
|
||||||
|
if (res !== null) return res
|
||||||
|
return viewlet.viewOptions != null ? getDefaults(viewlet.viewOptions) : defaults
|
||||||
|
}
|
||||||
|
|
||||||
|
export function migrateViewOpttions (): void {
|
||||||
|
for (let index = 0; index < localStorage.length; index++) {
|
||||||
|
const key = localStorage.key(index)
|
||||||
|
if (key === null) continue
|
||||||
|
if (!key.startsWith('viewOptions:')) continue
|
||||||
|
const options = localStorage.getItem(key)
|
||||||
|
if (options === null) continue
|
||||||
|
const res = JSON.parse(options) as ViewOptions
|
||||||
|
if (!Array.isArray(res.groupBy)) {
|
||||||
|
res.groupBy = [res.groupBy]
|
||||||
|
}
|
||||||
|
localStorage.setItem(key, JSON.stringify(res))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -445,7 +445,7 @@ export interface ViewletPreference extends Preference {
|
|||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
export type ViewOptions = {
|
export type ViewOptions = {
|
||||||
groupBy: string
|
groupBy: string[]
|
||||||
orderBy: OrderOption
|
orderBy: OrderOption
|
||||||
} & Record<string, any>
|
} & Record<string, any>
|
||||||
|
|
||||||
|
@ -42,7 +42,7 @@
|
|||||||
TooltipInstance
|
TooltipInstance
|
||||||
} from '@hcengineering/ui'
|
} from '@hcengineering/ui'
|
||||||
import view from '@hcengineering/view'
|
import view from '@hcengineering/view'
|
||||||
import { ActionContext, ActionHandler } from '@hcengineering/view-resources'
|
import { ActionContext, ActionHandler, migrateViewOpttions } from '@hcengineering/view-resources'
|
||||||
import type { Application, NavigatorModel, SpecialNavModel, ViewConfiguration } from '@hcengineering/workbench'
|
import type { Application, NavigatorModel, SpecialNavModel, ViewConfiguration } from '@hcengineering/workbench'
|
||||||
import { getContext, onDestroy, onMount, tick } from 'svelte'
|
import { getContext, onDestroy, onMount, tick } from 'svelte'
|
||||||
import { subscribeMobile } from '../mobile'
|
import { subscribeMobile } from '../mobile'
|
||||||
@ -77,6 +77,7 @@
|
|||||||
let createItemLabel: IntlString | undefined
|
let createItemLabel: IntlString | undefined
|
||||||
|
|
||||||
let apps: Application[] = []
|
let apps: Application[] = []
|
||||||
|
migrateViewOpttions()
|
||||||
|
|
||||||
const excludedApps = getMetadata(workbench.metadata.ExcludedApplications) ?? []
|
const excludedApps = getMetadata(workbench.metadata.ExcludedApplications) ?? []
|
||||||
|
|
||||||
|
@ -141,7 +141,7 @@ test('report-time-from-main-view', async ({ page }) => {
|
|||||||
|
|
||||||
await page.click('text="Issues"')
|
await page.click('text="Issues"')
|
||||||
await page.click('button:has-text("View")')
|
await page.click('button:has-text("View")')
|
||||||
await page.click('.value >> nth=1')
|
await page.click('.ordering >> nth=0')
|
||||||
await page.click('text="Modified"')
|
await page.click('text="Modified"')
|
||||||
await page.keyboard.press('Escape')
|
await page.keyboard.press('Escape')
|
||||||
|
|
||||||
|
@ -29,18 +29,18 @@ export async function navigate (page: Page): Promise<void> {
|
|||||||
|
|
||||||
export async function setViewGroup (page: Page, groupName: string): Promise<void> {
|
export async function setViewGroup (page: Page, groupName: string): Promise<void> {
|
||||||
await page.click('button:has-text("View")')
|
await page.click('button:has-text("View")')
|
||||||
await page.click('.antiCard >> button >> nth=0')
|
await page.click('.antiCard >> .grouping >> button >> nth=0')
|
||||||
await page.click(`.menu-item:has-text("${groupName}")`)
|
await page.click(`.menu-item:has-text("${groupName}")`)
|
||||||
await expect(page.locator('.antiCard >> button >> nth=0')).toContainText(groupName)
|
await expect(page.locator('.antiCard >> .grouping >> button >> nth=0')).toContainText(groupName)
|
||||||
|
|
||||||
await page.keyboard.press('Escape')
|
await page.keyboard.press('Escape')
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function setViewOrder (page: Page, orderName: string): Promise<void> {
|
export async function setViewOrder (page: Page, orderName: string): Promise<void> {
|
||||||
await page.click('button:has-text("View")')
|
await page.click('button:has-text("View")')
|
||||||
await page.click('.antiCard >> button >> nth=1')
|
await page.click('.antiCard >> .ordering >> button')
|
||||||
await page.click(`.menu-item:has-text("${orderName}")`)
|
await page.click(`.menu-item:has-text("${orderName}")`)
|
||||||
await expect(page.locator('.antiCard >> button >> nth=1')).toContainText(orderName)
|
await expect(page.locator('.antiCard >> .ordering >> button')).toContainText(orderName)
|
||||||
|
|
||||||
await page.keyboard.press('Escape')
|
await page.keyboard.press('Escape')
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user