Remember viewOptions (#2120)

Signed-off-by: Dvinyanin Alexandr <dvinyanin.alexandr@gmail.com>
This commit is contained in:
Alex 2022-06-22 12:56:56 +07:00 committed by GitHub
parent de99835dc3
commit 0cadc19331
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 127 additions and 614 deletions

View File

@ -2,6 +2,10 @@
## 0.6.29 (upcoming)
Tracker:
- Remember view options
-
Chunter:
- Reactions on messages

View File

@ -1,527 +0,0 @@
<!--
// Copyright © 2022 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 contact, { Employee, formatName } from '@anticrm/contact'
import { DocumentQuery, FindOptions, Ref, SortingOrder, WithLookup } from '@anticrm/core'
import { createQuery } from '@anticrm/presentation'
import {
Issue,
Team,
IssuesGrouping,
IssuesOrdering,
IssuesDateModificationPeriod,
IssueStatus,
IssueStatusCategory
} from '@anticrm/tracker'
import { Button, Label, Scroller, showPopup, eventToHTMLElement, IconAdd, IconClose, Icon } from '@anticrm/ui'
import { IntlString } from '@anticrm/platform'
import { createEventDispatcher } from 'svelte'
import ViewOptionsPopup from './ViewOptionsPopup.svelte'
import IssuesListBrowser from './IssuesListBrowser.svelte'
import IssuesFilterMenu from './IssuesFilterMenu.svelte'
import FilterSummary from '../FilterSummary.svelte'
import tracker from '../../plugin'
import {
IssuesGroupByKeys,
issuesGroupKeyMap,
issuesOrderKeyMap,
getIssuesModificationDatePeriodTime,
issuesSortOrderMap,
getGroupedIssues,
defaultPriorities,
getArraysIntersection,
IssueFilter,
getArraysUnion
} from '../../utils'
import ViewOptionsButton from './ViewOptionsButton.svelte'
export let currentSpace: Ref<Team>
export let title: IntlString = tracker.string.AllIssues
export let query: DocumentQuery<Issue> = {}
export let search: string = ''
export let groupingKey: IssuesGrouping = IssuesGrouping.Status
export let orderingKey: IssuesOrdering = IssuesOrdering.LastUpdated
export let completedIssuesPeriod: IssuesDateModificationPeriod | null = IssuesDateModificationPeriod.All
export let shouldShowSubIssues: boolean | undefined = true
export let shouldShowEmptyGroups: boolean | undefined = false
export let includedGroups: Partial<Record<IssuesGroupByKeys, Array<any>>> = {}
export let label: string | undefined = undefined
const dispatch = createEventDispatcher()
const ENTRIES_LIMIT = 200
const spaceQuery = createQuery()
const issuesQuery = createQuery()
const resultIssuesQuery = createQuery()
const statusesQuery = createQuery()
const issuesMap: { [status: string]: number } = {}
let filterElement: HTMLElement | null = null
let filters: IssueFilter[] = []
let currentTeam: Team | undefined
let issues: Issue[] = []
let resultIssues: Issue[] = []
let statusesById: ReadonlyMap<Ref<IssueStatus>, WithLookup<IssueStatus>> = new Map()
let employees: (WithLookup<Employee> | undefined)[] = []
$: totalIssuesCount = issues.length
$: resultIssuesCount = resultIssues.length
$: isFiltersEmpty = filters.length === 0
const options: FindOptions<Issue> = {
sort: { [issuesOrderKeyMap[orderingKey]]: issuesSortOrderMap[issuesOrderKeyMap[orderingKey]] },
limit: ENTRIES_LIMIT,
lookup: {
assignee: contact.class.Employee,
status: tracker.class.IssueStatus,
space: tracker.class.Team
}
}
$: baseQuery = {
space: currentSpace,
...includedIssuesQuery,
...modifiedOnIssuesQuery,
...query
}
$: resultQuery = search === '' ? baseQuery : { $search: search, ...baseQuery }
$: spaceQuery.query(tracker.class.Team, { _id: currentSpace }, (res) => {
currentTeam = res.shift()
})
$: groupByKey = issuesGroupKeyMap[groupingKey]
$: categories = getCategories(groupByKey, resultIssues, !!shouldShowEmptyGroups)
$: groupedIssues = getGroupedIssues(groupByKey, resultIssues, categories)
$: displayedCategories = (categories as any[]).filter((x) => {
if (groupByKey === undefined || includedGroups[groupByKey] === undefined) {
return true
}
if (groupByKey === 'status') {
const category = statusesById.get(x as Ref<IssueStatus>)?.category
return !!(category && includedGroups.status?.includes(category))
}
return includedGroups[groupByKey]?.includes(x)
})
$: includedIssuesQuery = getIncludedIssuesQuery(includedGroups, statuses, shouldShowSubIssues)
$: modifiedOnIssuesQuery = getModifiedOnIssuesFilterQuery(issues, completedIssuesPeriod)
$: statuses = [...statusesById.values()]
const getIncludedIssuesQuery = (
groups: Partial<Record<IssuesGroupByKeys, Array<any>>>,
issueStatuses: IssueStatus[],
withSubIssues?: boolean
) => {
const resultMap: { [p: string]: { $in: any[] } } = {}
for (const [key, value] of Object.entries(groups)) {
const includedCategories = key === 'status' ? filterIssueStatuses(issueStatuses, value) : value
resultMap[key] = { $in: includedCategories }
}
return { ...resultMap, ...(withSubIssues ? {} : { attachedTo: tracker.ids.NoParent }) }
}
const getModifiedOnIssuesFilterQuery = (
currentIssues: WithLookup<Issue>[],
period: IssuesDateModificationPeriod | null
) => {
const filter: { _id: { $in: Array<Ref<Issue>> } } = { _id: { $in: [] } }
if (!period || period === IssuesDateModificationPeriod.All) {
return {}
}
for (const issue of currentIssues) {
if (
issue.$lookup?.status?.category === tracker.issueStatusCategory.Completed &&
issue.modifiedOn < getIssuesModificationDatePeriodTime(period)
) {
continue
}
filter._id.$in.push(issue._id)
}
return filter
}
$: issuesQuery.query<Issue>(
tracker.class.Issue,
{ ...includedIssuesQuery },
(result) => {
issues = result
employees = result.map((x) => x.$lookup?.assignee)
},
options
)
$: resultIssuesQuery.query<Issue>(
tracker.class.Issue,
{ ...resultQuery, ...getFiltersQuery(filters) },
(result) => {
resultIssues = result
employees = result.map((x) => x.$lookup?.assignee)
},
options
)
$: statusesQuery.query(
tracker.class.IssueStatus,
{ attachedTo: currentSpace },
(issueStatuses) => {
statusesById = new Map(issueStatuses.map((status) => [status._id, status]))
},
{
lookup: { category: tracker.class.IssueStatusCategory },
sort: { rank: SortingOrder.Ascending }
}
)
const getCategories = (key: IssuesGroupByKeys | undefined, elements: Issue[], shouldShowAll: boolean) => {
if (!key) {
return [undefined] // No grouping
}
const defaultStatuses = Object.values(statuses).map((x) => x._id)
const existingCategories = Array.from(
new Set(
elements.map((x) => {
return x[key]
})
)
)
if (shouldShowAll) {
if (key === 'status') {
return defaultStatuses
}
if (key === 'priority') {
return defaultPriorities
}
}
if (key === 'status') {
existingCategories.sort((s1, s2) => {
const i1 = defaultStatuses.findIndex((x) => x === s1)
const i2 = defaultStatuses.findIndex((x) => x === s2)
return i1 - i2
})
}
if (key === 'priority') {
existingCategories.sort((p1, p2) => {
const i1 = defaultPriorities.findIndex((x) => x === p1)
const i2 = defaultPriorities.findIndex((x) => x === p2)
return i1 - i2
})
}
if (key === 'assignee') {
existingCategories.sort((a1, a2) => {
const employeeId1 = a1 as Ref<Employee> | null
const employeeId2 = a2 as Ref<Employee> | null
if (employeeId1 === null && employeeId2 !== null) {
return 1
}
if (employeeId1 !== null && employeeId2 === null) {
return -1
}
if (employeeId1 !== null && employeeId2 !== null) {
const name1 = formatName(employees.find((x) => x?._id === employeeId1)?.name ?? '')
const name2 = formatName(employees.find((x) => x?._id === employeeId2)?.name ?? '')
if (name1 > name2) {
return 1
} else if (name2 > name1) {
return -1
}
return 0
}
return 0
})
}
return existingCategories
}
function filterIssueStatuses (
issueStatuses: IssueStatus[],
issueStatusCategories: Ref<IssueStatusCategory>[]
): Ref<IssueStatus>[] {
const statusCategories = new Set(issueStatusCategories)
return issueStatuses.filter((status) => statusCategories.has(status.category)).map((s) => s._id)
}
const handleOptionsUpdated = (
result:
| {
orderBy: IssuesOrdering
groupBy: IssuesGrouping
completedIssuesPeriod: IssuesDateModificationPeriod
shouldShowSubIssues: boolean
shouldShowEmptyGroups: boolean
}
| undefined
) => {
if (result === undefined) {
return
}
for (const prop of Object.getOwnPropertyNames(issuesMap)) {
delete issuesMap[prop]
}
groupingKey = result.groupBy
orderingKey = result.orderBy
completedIssuesPeriod = result.completedIssuesPeriod
shouldShowSubIssues = result.shouldShowSubIssues
shouldShowEmptyGroups = result.shouldShowEmptyGroups
if (result.groupBy === IssuesGrouping.Assignee || result.groupBy === IssuesGrouping.NoGrouping) {
shouldShowEmptyGroups = undefined
}
}
const handleOptionsEditorOpened = (event: MouseEvent) => {
if (!currentSpace) {
return
}
showPopup(
ViewOptionsPopup,
{ groupBy: groupingKey, orderBy: orderingKey, completedIssuesPeriod, shouldShowSubIssues, shouldShowEmptyGroups },
eventToHTMLElement(event),
undefined,
handleOptionsUpdated
)
}
const getFiltersQuery = (filters: IssueFilter[]) => {
const result: { [f: string]: { $in?: any[]; $nin?: any[] } } = {}
for (const filter of filters) {
for (const [key, value] of Object.entries(filter.query)) {
const { mode } = filter
if (result[key] === undefined) {
result[key] = { ...value }
continue
}
if (result[key][mode] === undefined) {
result[key][mode] = [...value[mode]]
continue
}
const resultFunction = mode === '$nin' ? getArraysUnion : getArraysIntersection
result[key][mode] = resultFunction(result[key]?.[mode] ?? [], value[mode])
}
}
return result
}
const handleFilterDeleted = (filterIndex?: number) => {
if (filterIndex !== undefined) {
filters.splice(filterIndex, 1)
} else {
filters.length = 0
}
filters = filters
}
const handleAllFiltersDeleted = () => {
handleFilterDeleted()
}
const handleFiltersModified = (result: { [p: string]: any }, index?: number) => {
const i = index === undefined ? filters.length : index
const entries = Object.entries(result)
if (entries.length !== 1) {
return
}
const [filterKey, filterValue] = entries[0]
if (filters[i]) {
const { mode, query: currentFilterQuery } = filters[i]
const currentFilterQueryConditions: any[] = currentFilterQuery[filterKey]?.[mode] ?? []
if (currentFilterQueryConditions.includes(filterValue)) {
const updatedFilterConditions = currentFilterQueryConditions.filter((x: any) => x !== filterValue)
filters[i] = { mode, query: { [filterKey]: { [mode]: updatedFilterConditions } } }
if (filters.length === 1 && updatedFilterConditions.length === 0) {
filters.length = 0
}
} else {
filters[i] = { mode, query: { [filterKey]: { $in: [...currentFilterQueryConditions, filterValue] } } }
}
} else {
filters[i] = { mode: '$in', query: { [filterKey]: { $in: [filterValue] } } }
}
filters = filters
}
const handleFilterModeChanged = (index: number) => {
if (!filters[index]) {
return
}
const { mode: currentMode, query: currentQuery } = filters[index]
const newMode = currentMode === '$in' ? '$nin' : '$in'
const [filterKey, filterValue] = Object.entries(currentQuery)[0]
filters[index] = { mode: newMode, query: { [filterKey]: { [newMode]: [...filterValue[currentMode]] } } }
}
const handleFiltersBackButtonPressed = (event: MouseEvent) => {
dispatch('close')
handleFilterMenuOpened(event, false)
}
const handleFilterMenuOpened = (event: MouseEvent, shouldUpdateFilterTargetElement: boolean = true) => {
if (!currentSpace) {
return
}
if (!filterElement || shouldUpdateFilterTargetElement) {
filterElement = eventToHTMLElement(event)
}
showPopup(
IssuesFilterMenu,
{
issues,
filters: filters,
index: filters.length,
defaultStatuses: statuses,
onBack: handleFiltersBackButtonPressed,
targetHtml: filterElement,
onUpdate: handleFiltersModified
},
filterElement
)
}
$: value = totalIssuesCount === resultIssuesCount ? totalIssuesCount : `${resultIssuesCount}/${totalIssuesCount}`
</script>
{#if currentTeam}
<div class="ac-header full divide">
<div class="ac-header__wrap-title">
<div class="ac-header__title">
{#if label}
{label}
{:else}
<Label label={title} params={{ value }} />
{/if}
</div>
<div class="ml-4">
<Button
size="small"
kind={'link-bordered'}
borderStyle={'dashed'}
on:click={isFiltersEmpty ? handleFilterMenuOpened : handleAllFiltersDeleted}
>
<svelte:fragment slot="content">
<div class="flex-row-center pointer-events-none">
{#if isFiltersEmpty}
<Icon icon={IconAdd} size={'x-small'} />
<span class="overflow-label ml-1"><Label label={tracker.string.Filter} /></span>
{:else}
<span class="overflow-label mr-1"><Label label={tracker.string.ClearFilters} /></span>
<Icon icon={IconClose} size={'x-small'} />
{/if}
</div>
</svelte:fragment>
</Button>
</div>
</div>
<ViewOptionsButton on:click={handleOptionsEditorOpened} />
</div>
{#if filters.length > 0}
<FilterSummary
{filters}
{issues}
defaultStatuses={statuses}
onAddFilter={handleFilterMenuOpened}
onUpdateFilter={handleFiltersModified}
onDeleteFilter={handleFilterDeleted}
onChangeMode={handleFilterModeChanged}
/>
{/if}
<div class="flex h-full clear-mins">
<Scroller tableFade={displayedCategories.length > 1}>
<IssuesListBrowser
_class={tracker.class.Issue}
{currentSpace}
{groupByKey}
orderBy={issuesOrderKeyMap[orderingKey]}
{statuses}
{employees}
categories={displayedCategories}
itemsConfig={[
{ key: '', presenter: tracker.component.PriorityEditor, props: { kind: 'list', size: 'small' } },
{ key: '', presenter: tracker.component.IssuePresenter, props: { currentTeam } },
{ key: '', presenter: tracker.component.StatusEditor, props: { statuses, kind: 'list', size: 'small' } },
{ key: '', presenter: tracker.component.TitlePresenter, props: { shouldUseMargin: true, fixed: 'left' } },
{ key: '', presenter: tracker.component.DueDatePresenter, props: { kind: 'list' } },
{
key: '',
presenter: tracker.component.ProjectEditor,
props: { kind: 'list', size: 'small', shape: 'round', shouldShowPlaceholder: false }
},
{ key: 'modifiedOn', presenter: tracker.component.ModificationDatePresenter, props: { fixed: 'right' } },
{
key: '$lookup.assignee',
presenter: tracker.component.AssigneePresenter,
props: { currentSpace, defaultClass: contact.class.Employee, shouldShowLabel: false }
}
]}
{groupedIssues}
/>
</Scroller>
{#if $$slots.aside !== undefined}
<div class="antiPanel-component aside border-left">
<slot name="aside" />
</div>
{/if}
</div>
{/if}

View File

@ -2,14 +2,14 @@
import { DocumentQuery, Ref, SortingOrder, WithLookup } from '@anticrm/core'
import { Component } from '@anticrm/ui'
import { BuildModelKey, Viewlet, ViewletPreference } from '@anticrm/view'
import { Issue, IssueStatus, Team, ViewOptions } from '@anticrm/tracker'
import tracker from '../../plugin'
import { Issue, IssueStatus, Team } from '@anticrm/tracker'
import { createQuery } from '@anticrm/presentation'
import { viewOptionsStore } from '../../viewOptions'
import tracker from '../../plugin'
export let currentSpace: Ref<Team>
export let viewlet: WithLookup<Viewlet>
export let query: DocumentQuery<Issue> = {}
export let viewOptions: ViewOptions
const statusesQuery = createQuery()
const spaceQuery = createQuery()
@ -62,14 +62,14 @@
{#if viewlet?.$lookup?.descriptor?.component}
<Component
is={viewlet.$lookup?.descriptor?.component}
is={viewlet.$lookup.descriptor.component}
props={{
currentSpace,
config: createConfig(viewlet, undefined),
options: viewlet.options,
viewlet,
query,
viewOptions
viewOptions: $viewOptionsStore
}}
/>
{/if}

View File

@ -1,28 +1,14 @@
<script lang="ts">
import { Ref, WithLookup } from '@anticrm/core'
import { Team, ViewOptions } from '@anticrm/tracker'
import { Icon, TabList, showPopup, eventToHTMLElement } from '@anticrm/ui'
import { Icon, TabList } from '@anticrm/ui'
import { Viewlet } from '@anticrm/view'
import { FilterButton, setActiveViewletId } from '@anticrm/view-resources'
import tracker from '../../plugin'
import ViewOptionsPopup from './ViewOptionsPopup.svelte'
import ViewOptionsButton from './ViewOptionsButton.svelte'
import { WithLookup } from '@anticrm/core'
import ViewOptions from './ViewOptions.svelte'
export let currentSpace: Ref<Team>
export let viewlet: WithLookup<Viewlet> | undefined
export let viewlets: WithLookup<Viewlet>[] = []
export let label: string
export let viewOptions: ViewOptions
const handleOptionsEditorOpened = (event: MouseEvent) => {
if (!currentSpace) {
return
}
showPopup(ViewOptionsPopup, viewOptions, eventToHTMLElement(event), undefined, (result) => {
if (result) viewOptions = { ...result }
})
}
$: viewslist = viewlets.map((views) => {
return {
@ -54,6 +40,6 @@
}}
/>
{/if}
<ViewOptionsButton on:click={handleOptionsEditorOpened} />
<ViewOptions {viewlet} />
<slot name="extra" />
</div>

View File

@ -2,14 +2,7 @@
import core, { DocumentQuery, Ref, Space, WithLookup } from '@anticrm/core'
import { IntlString, translate } from '@anticrm/platform'
import { getClient } from '@anticrm/presentation'
import {
Issue,
IssuesDateModificationPeriod,
IssuesGrouping,
IssuesOrdering,
Team,
ViewOptions
} from '@anticrm/tracker'
import { Issue, Team } from '@anticrm/tracker'
import { Button, IconDetails } from '@anticrm/ui'
import view, { Viewlet } from '@anticrm/view'
import { FilterBar } from '@anticrm/view-resources'
@ -26,13 +19,6 @@
export let panelWidth: number = 0
let viewlet: WithLookup<Viewlet> | undefined = undefined
let viewOptions: ViewOptions = {
groupBy: IssuesGrouping.Status,
orderBy: IssuesOrdering.Status,
completedIssuesPeriod: IssuesDateModificationPeriod.All,
shouldShowEmptyGroups: false,
shouldShowSubIssues: false
}
let resultQuery: DocumentQuery<Issue> = {}
const client = getClient()
@ -77,7 +63,7 @@
</script>
{#if currentSpace}
<IssuesHeader {currentSpace} {viewlets} {label} bind:viewlet bind:viewOptions>
<IssuesHeader {viewlets} {label} bind:viewlet>
<svelte:fragment slot="extra">
{#if asideFloat && $$slots.aside}
<Button
@ -95,7 +81,7 @@
<FilterBar _class={tracker.class.Issue} {query} on:change={(e) => (resultQuery = e.detail)} />
<div class="flex w-full h-full clear-mins">
{#if viewlet}
<IssuesContent {currentSpace} {viewlet} query={resultQuery} {viewOptions} />
<IssuesContent {currentSpace} {viewlet} query={resultQuery} />
{/if}
{#if $$slots.aside !== undefined && asideShown}
<div class="popupPanel-body__aside" class:float={asideFloat} class:shown={asideShown}>

View File

@ -0,0 +1,72 @@
<!--
// Copyright © 2022 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 { IssuesDateModificationPeriod, IssuesGrouping, IssuesOrdering, ViewOptions } from '@anticrm/tracker'
import { Button, eventToHTMLElement, IconDownOutline, showPopup } from '@anticrm/ui'
import { getViewOptions, setViewOptions } from '@anticrm/view-resources'
import view, { Viewlet } from '@anticrm/view'
import ViewOptionsPopup from './ViewOptionsPopup.svelte'
import { viewOptionsStore } from '../../viewOptions'
export let viewlet: Viewlet | undefined
let viewOptions: ViewOptions
$: if (viewlet) {
const savedViewOptions = getViewOptions(viewlet._id)
viewOptions = savedViewOptions
? JSON.parse(savedViewOptions)
: {
groupBy: IssuesGrouping.Status,
orderBy: IssuesOrdering.Status,
completedIssuesPeriod: IssuesDateModificationPeriod.All,
shouldShowEmptyGroups: false,
shouldShowSubIssues: false
}
}
$: $viewOptionsStore = viewOptions
const handleOptionsEditorOpened = (event: MouseEvent) => {
showPopup(ViewOptionsPopup, viewOptions, eventToHTMLElement(event), undefined, (result) => {
viewOptions = result
if (viewlet) setViewOptions(viewlet._id, JSON.stringify(viewOptions))
})
}
</script>
<Button
icon={view.icon.ViewButton}
kind={'secondary'}
size={'small'}
showTooltip={{ label: view.string.CustomizeView }}
on:click={handleOptionsEditorOpened}
>
<svelte:fragment slot="content">
<div class="flex-row-center clear-mins pointer-events-none">
<span class="text-sm font-medium">View</span>
<div class="icon"><IconDownOutline size={'full'} /></div>
</div>
</svelte:fragment>
</Button>
<style lang="scss">
.icon {
margin-left: 0.25rem;
width: 0.875rem;
height: 0.875rem;
color: var(--content-color);
}
</style>

View File

@ -1,42 +0,0 @@
<!--
// Copyright © 2022 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 { Button, IconDownOutline } from '@anticrm/ui'
import view from '@anticrm/view'
</script>
<Button
icon={view.icon.ViewButton}
kind={'secondary'}
size={'small'}
showTooltip={{ label: view.string.CustomizeView }}
on:click
>
<svelte:fragment slot="content">
<div class="flex-row-center clear-mins pointer-events-none">
<span class="text-sm font-medium">View</span>
<div class="icon"><IconDownOutline size={'full'} /></div>
</div>
</svelte:fragment>
</Button>
<style lang="scss">
.icon {
margin-left: 0.25rem;
width: 0.875rem;
height: 0.875rem;
color: var(--content-color);
}
</style>

View File

@ -26,7 +26,6 @@ import EditIssue from './components/issues/edit/EditIssue.svelte'
import IssueItem from './components/issues/IssueItem.svelte'
import IssuePresenter from './components/issues/IssuePresenter.svelte'
import IssuePreview from './components/issues/IssuePreview.svelte'
import Issues from './components/issues/Issues.svelte'
import IssuesView from './components/issues/IssuesView.svelte'
import ListView from './components/issues/ListView.svelte'
import ModificationDatePresenter from './components/issues/ModificationDatePresenter.svelte'
@ -111,7 +110,6 @@ export default async (): Promise<Resources> => ({
Active,
Backlog,
Inbox,
Issues,
MyIssues,
Projects,
Views,

View File

@ -171,7 +171,6 @@ export default mergeIds(trackerId, tracker, {
NopeComponent: '' as AnyComponent,
Inbox: '' as AnyComponent,
MyIssues: '' as AnyComponent,
Issues: '' as AnyComponent,
Views: '' as AnyComponent,
Active: '' as AnyComponent,
Backlog: '' as AnyComponent,

View File

@ -0,0 +1,7 @@
import { ViewOptions } from '@anticrm/tracker'
import { writable } from 'svelte/store'
/**
* @public
*/
export const viewOptionsStore = writable<ViewOptions>()

View File

@ -82,7 +82,9 @@ export {
getObjectPresenter,
LoadingProps,
setActiveViewletId,
getActiveViewletId
getActiveViewletId,
setViewOptions,
getViewOptions
} from './utils'
export {
HTMLPresenter,

View File

@ -422,3 +422,24 @@ export function getActiveViewletId (): Ref<Viewlet> | null {
const key = makeViewletKey()
return localStorage.getItem(key) as Ref<Viewlet> | null
}
function makeViewOptionsKey (viewletId: Ref<Viewlet>): string {
const loc = getCurrentLocation()
loc.fragment = undefined
loc.query = undefined
return `viewOptions:${viewletId}:${locationToUrl(loc)}`
}
export function setViewOptions (viewletId: Ref<Viewlet>, options: string | null): void {
const key = makeViewOptionsKey(viewletId)
if (options !== null) {
localStorage.setItem(key, options)
} else {
localStorage.removeItem(key)
}
}
export function getViewOptions (viewletId: Ref<Viewlet>): string | null {
const key = makeViewOptionsKey(viewletId)
return localStorage.getItem(key)
}

View File

@ -157,17 +157,24 @@ test('issues-status-display', async ({ page }) => {
}
})
test('save-active-viewlet', async ({ page }) => {
test('save-view-options', async ({ page }) => {
const panels = ['Issues', 'Active', 'Backlog']
await navigate(page)
for (const viewletSelector of [viewletSelectors.Board, viewletSelectors.Table]) {
for (const panel of panels) {
await page.click(`text="${panel}"`)
await page.click(viewletSelector)
await page.click('button:has-text("View")')
await page.click('.antiCard >> button >> nth=0')
await page.click('.menu-item:has-text("Assignee")')
await page.keyboard.press('Escape')
}
for (const panel of panels) {
await page.click(`text="${panel}"`)
await expect(page.locator(viewletSelector)).toHaveClass(/selected/)
await page.click('button:has-text("View")')
await expect(page.locator('.antiCard >> button >> nth=0')).toContainText('Assignee')
await page.keyboard.press('Escape')
}
}
})