Add support for user-saved filtered Views (#2521)

Signed-off-by: Vyacheslav Tumanov <me@slavatumanov.me>
This commit is contained in:
Vyacheslav Tumanov 2023-01-19 20:38:18 +05:00 committed by GitHub
parent e9cc9e7b47
commit c261f8f761
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 146 additions and 32 deletions

View File

@ -19,7 +19,7 @@ import { Builder, Mixin, Model } from '@hcengineering/model'
import core, { TClass, TDoc } from '@hcengineering/model-core'
import preference, { TPreference } from '@hcengineering/model-preference'
import type { Asset, IntlString, Resource, Status } from '@hcengineering/platform'
import type { AnyComponent } from '@hcengineering/ui'
import type { AnyComponent, Location } from '@hcengineering/ui'
import type {
Action,
ActionCategory,
@ -57,9 +57,11 @@ import type {
Viewlet,
ViewletDescriptor,
ViewletPreference,
ViewOptionsModel
ViewOptionsModel,
FilteredView
} from '@hcengineering/view'
import view from './plugin'
import { DOMAIN_PREFERENCE } from '@hcengineering/preference'
export { viewOperation } from './migration'
export { ViewAction, Viewlet }
@ -91,6 +93,13 @@ export function classPresenter (
}
}
@Model(view.class.FilteredView, core.class.Doc, DOMAIN_PREFERENCE)
export class TFilteredView extends TPreference implements FilteredView {
name!: string
location!: Location
filters!: string
}
@Model(view.class.FilterMode, core.class.Doc, DOMAIN_MODEL)
export class TFilterMode extends TDoc implements FilterMode {
label!: IntlString
@ -318,7 +327,8 @@ export function createModel (builder: Builder): void {
TPreviewPresenter,
TLinkPresenter,
TArrayEditor,
TInlineAttributEditor
TInlineAttributEditor,
TFilteredView
)
classPresenter(

View File

@ -55,6 +55,10 @@
"NoGrouping": "No grouping",
"Grouping": "Grouping",
"Ordering": "Ordering",
"Manual": "Manual"
"Manual": "Manual",
"FilteredViews": "Filtered views",
"NewFilteredView": "New filtered view",
"FilteredViewName": "Filtered view name"
}
}

View File

@ -53,6 +53,9 @@
"NoGrouping": "Нет группировки",
"Grouping": "Группировка",
"Ordering": "Сортировка",
"Manual": "Пользовательский"
"Manual": "Пользовательский",
"FilteredViews": "Фильтрованные отображения",
"NewFilteredView": "Новое фильтрованное отображение",
"FilteredViewName": "Имя фильтрованного отображения"
}
}

View File

@ -23,6 +23,7 @@
import view from '../../plugin'
import FilterSection from './FilterSection.svelte'
import FilterTypePopup from './FilterTypePopup.svelte'
import FilterSave from './FilterSave.svelte'
export let _class: Ref<Class<Doc>>
export let query: DocumentQuery<Doc>
@ -32,7 +33,6 @@
const dispatch = createEventDispatcher()
let maxIndex = 1
// const allFilters: boolean = true
function onChange (e: Filter | undefined) {
if (e === undefined) return
@ -78,6 +78,10 @@
}
}
async function saveFilteredView () {
showPopup(FilterSave, {})
}
let loading = false
function load (_class: Ref<Class<Doc>>) {
@ -192,26 +196,13 @@
</div>
</div>
<!-- SAVE BUTTON -->
<!-- <div class="buttons-group small-gap ml-4">
{#if filters.length > 1}
<div class="flex-baseline">
<span class="overflow-label">
<Label label={view.string.IncludeItemsThatMatch} />
</span>
<button
class="filter-button"
on:click={() => {
allFilters = !allFilters
}}
>
<Label label={allFilters ? view.string.AllFilters : view.string.AnyFilter} />
</button>
</div>
<div class="buttons-divider" />
{/if}
<Button icon={view.icon.Views} label={view.string.Save} size={'small'} width={'fit-content'} />
</div> -->
<Button
icon={view.icon.Views}
label={view.string.Save}
size={'small'}
width={'fit-content'}
on:click={() => saveFilteredView()}
/>
</div>
{/if}

View File

@ -0,0 +1,51 @@
<script lang="ts">
import { Card, getClient } from '@hcengineering/presentation'
import view from '../../plugin'
import { EditBox, getCurrentLocation } from '@hcengineering/ui'
import preference from '@hcengineering/preference'
import { createEventDispatcher } from 'svelte'
import { filterStore } from '../../filter'
let filterName = ''
const client = getClient()
function getFilteredViewData () {
const loc = getCurrentLocation()
loc.fragment = undefined
loc.query = undefined
const filters = JSON.stringify($filterStore)
return {
name: filterName,
location: loc,
filters,
attachedTo: loc.path[2]
}
}
async function saveFilter () {
await client.createDoc(view.class.FilteredView, preference.space.Preference, getFilteredViewData())
}
const dispatch = createEventDispatcher()
</script>
<Card
label={view.string.NewFilteredView}
okAction={saveFilter}
canSave={filterName.length > 0}
on:close={() => {
dispatch('close')
}}
>
<div class="flex-row-center">
<div class="flex-grow flex-col">
<EditBox
placeholder={view.string.FilteredViewName}
bind:value={filterName}
kind={'large-style'}
focus
focusIndex={1}
/>
</div>
</div>
</Card>

View File

@ -33,7 +33,13 @@ import type {
} from '@hcengineering/core'
import { Asset, IntlString, Plugin, plugin, Resource, Status } from '@hcengineering/platform'
import type { Preference } from '@hcengineering/preference'
import type { AnyComponent, AnySvelteComponent, PopupAlignment, PopupPosAlignment } from '@hcengineering/ui'
import type {
AnyComponent,
AnySvelteComponent,
PopupAlignment,
PopupPosAlignment,
Location as PlatformLocation
} from '@hcengineering/ui'
/**
* @public
@ -69,6 +75,15 @@ export interface Filter {
onRemove?: () => void
}
/**
* @public
*/
export interface FilteredView extends Preference {
name: string
location: PlatformLocation
filters: string
}
/**
* @public
*/
@ -523,7 +538,8 @@ const view = plugin(viewId, {
Action: '' as Ref<Class<Action>>,
ActionCategory: '' as Ref<Class<ActionCategory>>,
LinkPresenter: '' as Ref<Class<LinkPresenter>>,
FilterMode: '' as Ref<Class<FilterMode>>
FilterMode: '' as Ref<Class<FilterMode>>,
FilteredView: '' as Ref<Class<FilteredView>>
},
action: {
Delete: '' as Ref<Action>,
@ -560,7 +576,10 @@ const view = plugin(viewId, {
string: {
CustomizeView: '' as IntlString,
LabelNA: '' as IntlString,
View: '' as IntlString
View: '' as IntlString,
FilteredViews: '' as IntlString,
NewFilteredView: '' as IntlString,
FilteredViewName: '' as IntlString
},
icon: {
Table: '' as Asset,

View File

@ -16,8 +16,8 @@
import core, { Doc, Ref, SortingOrder, Space, getCurrentAccount } from '@hcengineering/core'
import { getResource } from '@hcengineering/platform'
import { createQuery, getClient } from '@hcengineering/presentation'
import { Button, Scroller, showPopup } from '@hcengineering/ui'
import type { NavigatorModel, SpecialNavModel } from '@hcengineering/workbench'
import { Button, IconEdit, navigate, Scroller, showPopup } from '@hcengineering/ui'
import type { Application, NavigatorModel, SpecialNavModel } from '@hcengineering/workbench'
import setting from '@hcengineering/setting'
import { createEventDispatcher } from 'svelte'
import preferece, { SpacePreference } from '@hcengineering/preference'
@ -29,10 +29,15 @@
import TreeSeparator from './navigator/TreeSeparator.svelte'
import HelpAndSupport from './HelpAndSupport.svelte'
import workbench from '../plugin'
import TreeNode from './navigator/TreeNode.svelte'
import TreeItem from './navigator/TreeItem.svelte'
import view, { FilteredView } from '@hcengineering/view'
import { filterStore } from '@hcengineering/view-resources'
export let model: NavigatorModel | undefined
export let currentSpace: Ref<Space> | undefined
export let currentSpecial: string | undefined
export let currentApplication: Application | undefined
const client = getClient()
const hierarchy = client.getHierarchy()
@ -101,6 +106,18 @@
$: if (model) update(model, spaces, preferences)
function removeAction (filteredView: FilteredView) {
return [
{
icon: view.icon.Archive ?? IconEdit,
label: setting.string.Delete,
action: async (ctx: any, evt: Event) => {
await client.removeDoc(view.class.FilteredView, currentSpace, filteredView._id)
}
}
]
}
async function updateSpecials (
specials: SpecialNavModel[],
spaces: Space[],
@ -124,6 +141,11 @@
return [result, requestIndex]
}
const dispatch = createEventDispatcher()
const filteredViewsQuery = createQuery()
let filteredViews: FilteredView[] | undefined
$: filteredViewsQuery.query(view.class.FilteredView, { attachedTo: currentApplication?.alias }, (result) => {
filteredViews = result
})
</script>
{#if model}
@ -144,7 +166,20 @@
{/if}
{#if specials.length > 0}<TreeSeparator />{/if}
{#if filteredViews && filteredViews.length > 0}
<TreeNode label={view.string.FilteredViews}>
{#each filteredViews as fV}
<TreeItem
title={fV.name}
on:click={() => {
navigate(fV.location)
$filterStore = JSON.parse(fV.filters)
}}
actions={() => removeAction(fV)}
/>
{/each}
</TreeNode>
{/if}
{#if starred.length}
<StarredNav label={preference.string.Starred} spaces={starred} on:space {currentSpace} />
{/if}

View File

@ -526,6 +526,7 @@
{currentSpace}
{currentSpecial}
model={navigatorModel}
{currentApplication}
on:special={(evt) => selectSpecial(evt.detail)}
on:space={(evt) => selectSpace(evt.detail.space, evt.detail.spaceSpecial)}
on:open={checkOnHide}