mirror of
https://github.com/hcengineering/platform.git
synced 2025-06-03 14:19:56 +00:00
Saved filter improve (#3180)
Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com>
This commit is contained in:
parent
d137e5548c
commit
696541af2d
@ -13,23 +13,22 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
//
|
//
|
||||||
|
|
||||||
import type { Class, Client, Data, Doc, DocumentQuery, Ref, Space } from '@hcengineering/core'
|
import type { Account, Class, Client, Data, Doc, DocumentQuery, Domain, Ref, Space } from '@hcengineering/core'
|
||||||
import { DOMAIN_MODEL } from '@hcengineering/core'
|
import { DOMAIN_MODEL } from '@hcengineering/core'
|
||||||
import { Builder, Mixin, Model } from '@hcengineering/model'
|
import { Builder, Mixin, Model } from '@hcengineering/model'
|
||||||
import core, { TClass, TDoc } from '@hcengineering/model-core'
|
import core, { TClass, TDoc } from '@hcengineering/model-core'
|
||||||
import preference, { TPreference } from '@hcengineering/model-preference'
|
import preference, { TPreference } from '@hcengineering/model-preference'
|
||||||
import type { Asset, IntlString, Resource, Status } from '@hcengineering/platform'
|
import type { Asset, IntlString, Resource, Status } from '@hcengineering/platform'
|
||||||
import { DOMAIN_PREFERENCE } from '@hcengineering/preference'
|
|
||||||
import type { AnyComponent, Location } from '@hcengineering/ui'
|
import type { AnyComponent, Location } from '@hcengineering/ui'
|
||||||
import type {
|
import type {
|
||||||
Action,
|
Action,
|
||||||
ActionCategory,
|
ActionCategory,
|
||||||
|
ActivityAttributePresenter,
|
||||||
AllValuesFunc,
|
AllValuesFunc,
|
||||||
ArrayEditor,
|
ArrayEditor,
|
||||||
AttributeEditor,
|
AttributeEditor,
|
||||||
AttributeFilter,
|
AttributeFilter,
|
||||||
AttributePresenter,
|
AttributePresenter,
|
||||||
ActivityAttributePresenter,
|
|
||||||
BuildModelKey,
|
BuildModelKey,
|
||||||
ClassFilters,
|
ClassFilters,
|
||||||
ClassSortFuncs,
|
ClassSortFuncs,
|
||||||
@ -57,9 +56,9 @@ import type {
|
|||||||
ObjectValidator,
|
ObjectValidator,
|
||||||
PreviewPresenter,
|
PreviewPresenter,
|
||||||
SortFunc,
|
SortFunc,
|
||||||
SpacePresenter,
|
|
||||||
SpaceHeader,
|
SpaceHeader,
|
||||||
SpaceName,
|
SpaceName,
|
||||||
|
SpacePresenter,
|
||||||
ViewAction,
|
ViewAction,
|
||||||
ViewActionInput,
|
ViewActionInput,
|
||||||
ViewContext,
|
ViewContext,
|
||||||
@ -75,6 +74,8 @@ export { viewId } from '@hcengineering/view'
|
|||||||
export { viewOperation } from './migration'
|
export { viewOperation } from './migration'
|
||||||
export { ViewAction, Viewlet }
|
export { ViewAction, Viewlet }
|
||||||
|
|
||||||
|
export const DOMAIN_VIEW = 'view' as Domain
|
||||||
|
|
||||||
export function createAction<T extends Doc = Doc, P = Record<string, any>> (
|
export function createAction<T extends Doc = Doc, P = Record<string, any>> (
|
||||||
builder: Builder,
|
builder: Builder,
|
||||||
data: Data<Action<T, P>>,
|
data: Data<Action<T, P>>,
|
||||||
@ -108,14 +109,18 @@ export function classPresenter (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Model(view.class.FilteredView, core.class.Doc, DOMAIN_PREFERENCE)
|
@Model(view.class.FilteredView, core.class.Doc, DOMAIN_VIEW)
|
||||||
export class TFilteredView extends TPreference implements FilteredView {
|
export class TFilteredView extends TDoc implements FilteredView {
|
||||||
name!: string
|
name!: string
|
||||||
location!: Location
|
location!: Location
|
||||||
filters!: string
|
filters!: string
|
||||||
viewOptions?: ViewOptions
|
viewOptions?: ViewOptions
|
||||||
filterClass?: Ref<Class<Doc>>
|
filterClass?: Ref<Class<Doc>>
|
||||||
viewletId?: Ref<Viewlet> | null
|
viewletId?: Ref<Viewlet> | null
|
||||||
|
users!: Ref<Account>[]
|
||||||
|
createdBy!: Ref<Account>
|
||||||
|
attachedTo!: string
|
||||||
|
sharable?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
@Model(view.class.FilterMode, core.class.Doc, DOMAIN_MODEL)
|
@Model(view.class.FilterMode, core.class.Doc, DOMAIN_MODEL)
|
||||||
|
@ -13,20 +13,11 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
//
|
//
|
||||||
|
|
||||||
import core, {
|
import core, { AnyAttribute, DOMAIN_TX, Ref, TxCreateDoc, TxCUD, TxProcessor, TxRemoveDoc } from '@hcengineering/core'
|
||||||
AnyAttribute,
|
|
||||||
Class,
|
|
||||||
Doc,
|
|
||||||
DOMAIN_TX,
|
|
||||||
Ref,
|
|
||||||
TxCreateDoc,
|
|
||||||
TxCUD,
|
|
||||||
TxProcessor,
|
|
||||||
TxRemoveDoc
|
|
||||||
} from '@hcengineering/core'
|
|
||||||
import { MigrateOperation, MigrationClient, MigrationUpgradeClient } from '@hcengineering/model'
|
import { MigrateOperation, MigrationClient, MigrationUpgradeClient } from '@hcengineering/model'
|
||||||
import { DOMAIN_PREFERENCE } from '@hcengineering/preference'
|
import { DOMAIN_PREFERENCE } from '@hcengineering/preference'
|
||||||
import { BuildModelKey, FilteredView, Viewlet, ViewletPreference } from '@hcengineering/view'
|
import { BuildModelKey, FilteredView, Viewlet, ViewletPreference } from '@hcengineering/view'
|
||||||
|
import { DOMAIN_VIEW } from '.'
|
||||||
import view from './plugin'
|
import view from './plugin'
|
||||||
|
|
||||||
async function migrateViewletPreference (client: MigrationClient): Promise<void> {
|
async function migrateViewletPreference (client: MigrationClient): Promise<void> {
|
||||||
@ -68,41 +59,25 @@ async function migrateViewletPreference (client: MigrationClient): Promise<void>
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function migrateSavedFilters (client: MigrationClient): Promise<void> {
|
async function migrateSavedFilters (client: MigrationClient): Promise<void> {
|
||||||
const preferences = await client.find<FilteredView>(DOMAIN_PREFERENCE, {
|
await client.move(
|
||||||
|
DOMAIN_PREFERENCE,
|
||||||
|
{
|
||||||
|
_class: view.class.FilteredView
|
||||||
|
},
|
||||||
|
DOMAIN_VIEW
|
||||||
|
)
|
||||||
|
const preferences = await client.find<FilteredView>(DOMAIN_VIEW, {
|
||||||
_class: view.class.FilteredView,
|
_class: view.class.FilteredView,
|
||||||
viewOptions: { $exists: true }
|
users: { $exists: false }
|
||||||
})
|
})
|
||||||
for (const pref of preferences) {
|
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>(
|
await client.update<FilteredView>(
|
||||||
DOMAIN_PREFERENCE,
|
DOMAIN_VIEW,
|
||||||
{
|
{
|
||||||
_id: pref._id
|
_id: pref._id
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
viewOptions: pref.viewOptions
|
users: [pref.createdBy]
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function migrateSavedFiltersViewlets (client: MigrationClient): Promise<void> {
|
|
||||||
const preferences = await client.find<FilteredView>(DOMAIN_PREFERENCE, {
|
|
||||||
_class: view.class.FilteredView,
|
|
||||||
viewletId: /^\S{24}$/ as any,
|
|
||||||
attachedTo: 'tracker' as any
|
|
||||||
})
|
|
||||||
for (const pref of preferences) {
|
|
||||||
await client.update<FilteredView>(
|
|
||||||
DOMAIN_PREFERENCE,
|
|
||||||
{
|
|
||||||
_id: pref._id
|
|
||||||
},
|
|
||||||
{
|
|
||||||
viewletId: 'tracker:viewlet:IssueList' as Ref<Viewlet>,
|
|
||||||
filterClass: 'tracker:class:Issue' as Ref<Class<Doc>>
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -178,7 +153,6 @@ 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)
|
await migrateSavedFilters(client)
|
||||||
await migrateSavedFiltersViewlets(client)
|
|
||||||
await fixViewletPreferenceRemovedAttributes(client)
|
await fixViewletPreferenceRemovedAttributes(client)
|
||||||
await fixPreferenceObjectKey(client)
|
await fixPreferenceObjectKey(client)
|
||||||
},
|
},
|
||||||
|
@ -78,6 +78,10 @@
|
|||||||
"ThisMonth": "This month",
|
"ThisMonth": "This month",
|
||||||
"NextMonth": "Next month",
|
"NextMonth": "Next month",
|
||||||
"NotSpecified": "Not specified",
|
"NotSpecified": "Not specified",
|
||||||
"CustomDate": "Custom date"
|
"CustomDate": "Custom date",
|
||||||
|
"AddSavedView": "Add saved view",
|
||||||
|
"Public": "Public",
|
||||||
|
"Hide": "Hide",
|
||||||
|
"SaveAs": "Save as"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -75,6 +75,10 @@
|
|||||||
"ThisMonth": "Этот месяц",
|
"ThisMonth": "Этот месяц",
|
||||||
"NextMonth": "Следующий месяц",
|
"NextMonth": "Следующий месяц",
|
||||||
"NotSpecified": "Не указана",
|
"NotSpecified": "Не указана",
|
||||||
"CustomDate": "Выбранная дата"
|
"AddSavedView": "Добавить сохраненное отображение",
|
||||||
|
"CustomDate": "Выбранная дата",
|
||||||
|
"Public": "Публичныйы",
|
||||||
|
"Hide": "Спрятать",
|
||||||
|
"SaveAs": "Сохранить как"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,75 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { getCurrentAccount } from '@hcengineering/core'
|
||||||
|
import { translate } from '@hcengineering/platform'
|
||||||
|
import presentation, { createQuery, getClient } from '@hcengineering/presentation'
|
||||||
|
import { deviceOptionsStore, resizeObserver } from '@hcengineering/ui'
|
||||||
|
import { FilteredView } from '@hcengineering/view'
|
||||||
|
import { createEventDispatcher, onMount } from 'svelte'
|
||||||
|
import view from '../../plugin'
|
||||||
|
|
||||||
|
export let attachedTo: string | undefined
|
||||||
|
|
||||||
|
const me = getCurrentAccount()._id
|
||||||
|
const q = createQuery()
|
||||||
|
let views: FilteredView[] = []
|
||||||
|
|
||||||
|
const baseQuery = {
|
||||||
|
attachedTo,
|
||||||
|
sharable: true,
|
||||||
|
createdBy: { $ne: me }
|
||||||
|
}
|
||||||
|
|
||||||
|
$: query =
|
||||||
|
search === ''
|
||||||
|
? baseQuery
|
||||||
|
: {
|
||||||
|
...baseQuery,
|
||||||
|
name: { $like: `%${search}%` }
|
||||||
|
}
|
||||||
|
|
||||||
|
$: q.query(view.class.FilteredView, query, (res) => {
|
||||||
|
views = res.filter((p) => !p.users.includes(me))
|
||||||
|
})
|
||||||
|
|
||||||
|
const client = getClient()
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
|
async function add (sv: FilteredView): Promise<void> {
|
||||||
|
await client.update(sv, { $push: { users: me } })
|
||||||
|
dispatch('close')
|
||||||
|
}
|
||||||
|
|
||||||
|
let search: string = ''
|
||||||
|
let phTraslate: string = ''
|
||||||
|
let searchInput: HTMLInputElement
|
||||||
|
$: translate(presentation.string.Search, {}).then((res) => {
|
||||||
|
phTraslate = res
|
||||||
|
})
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
if (searchInput && !$deviceOptionsStore.isMobile) searchInput.focus()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="selectPopup" use:resizeObserver={() => dispatch('changeContent')}>
|
||||||
|
<div class="header">
|
||||||
|
<input bind:this={searchInput} type="text" bind:value={search} placeholder={phTraslate} />
|
||||||
|
</div>
|
||||||
|
<div class="scroll">
|
||||||
|
<div class="box">
|
||||||
|
{#each views as value}
|
||||||
|
<button
|
||||||
|
class="menu-item no-focus"
|
||||||
|
on:click={() => {
|
||||||
|
add(value)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div class="flex-row-center w-full">
|
||||||
|
{value.name}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -13,17 +13,19 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Class, Doc, DocumentQuery, Ref } from '@hcengineering/core'
|
import { Class, Doc, DocumentQuery, Ref, getCurrentAccount } from '@hcengineering/core'
|
||||||
import { getResource } from '@hcengineering/platform'
|
import { getResource } from '@hcengineering/platform'
|
||||||
import { getClient } from '@hcengineering/presentation'
|
import { getClient } from '@hcengineering/presentation'
|
||||||
import { Button, IconAdd, eventToHTMLElement, showPopup } from '@hcengineering/ui'
|
import { Button, IconAdd, eventToHTMLElement, getCurrentLocation, showPopup } from '@hcengineering/ui'
|
||||||
import { Filter, ViewOptions } from '@hcengineering/view'
|
import { Filter, FilteredView, ViewOptions, Viewlet } from '@hcengineering/view'
|
||||||
import { createEventDispatcher } from 'svelte'
|
import { createEventDispatcher } from 'svelte'
|
||||||
import { filterStore, removeFilter, updateFilter } from '../../filter'
|
import { filterStore, removeFilter, updateFilter, selectedFilterStore } from '../../filter'
|
||||||
import view from '../../plugin'
|
import view from '../../plugin'
|
||||||
import FilterSave from './FilterSave.svelte'
|
import FilterSave from './FilterSave.svelte'
|
||||||
import FilterSection from './FilterSection.svelte'
|
import FilterSection from './FilterSection.svelte'
|
||||||
import FilterTypePopup from './FilterTypePopup.svelte'
|
import FilterTypePopup from './FilterTypePopup.svelte'
|
||||||
|
import { activeViewlet, getActiveViewletId, makeViewletKey } from '../../utils'
|
||||||
|
import { getViewOptions, viewOptionStore } from '../../viewOptions'
|
||||||
|
|
||||||
export let _class: Ref<Class<Doc>>
|
export let _class: Ref<Class<Doc>>
|
||||||
export let query: DocumentQuery<Doc>
|
export let query: DocumentQuery<Doc>
|
||||||
@ -58,6 +60,23 @@
|
|||||||
showPopup(FilterSave, { viewOptions, _class })
|
showPopup(FilterSave, { viewOptions, _class })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function saveCurrentFilteredView (filter: FilteredView | undefined) {
|
||||||
|
if (filter !== undefined) {
|
||||||
|
const filters = JSON.stringify($filterStore)
|
||||||
|
await client.update(filter, {
|
||||||
|
filters,
|
||||||
|
viewOptions,
|
||||||
|
viewletId: getActiveViewletId()
|
||||||
|
})
|
||||||
|
selectedFilterStore.set({
|
||||||
|
...filter,
|
||||||
|
filters,
|
||||||
|
viewOptions,
|
||||||
|
viewletId: getActiveViewletId()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function makeQuery (query: DocumentQuery<Doc>, filters: Filter[]): Promise<void> {
|
async function makeQuery (query: DocumentQuery<Doc>, filters: Filter[]): Promise<void> {
|
||||||
const newQuery = hierarchy.clone(query)
|
const newQuery = hierarchy.clone(query)
|
||||||
for (let i = 0; i < filters.length; i++) {
|
for (let i = 0; i < filters.length; i++) {
|
||||||
@ -125,6 +144,27 @@
|
|||||||
|
|
||||||
$: clazz = hierarchy.getClass(_class)
|
$: clazz = hierarchy.getClass(_class)
|
||||||
$: visible = hierarchy.hasMixin(clazz, view.mixin.ClassFilters)
|
$: visible = hierarchy.hasMixin(clazz, view.mixin.ClassFilters)
|
||||||
|
|
||||||
|
const me = getCurrentAccount()._id
|
||||||
|
|
||||||
|
function selectedFilterChanged (
|
||||||
|
selectedFilter: FilteredView | undefined,
|
||||||
|
filters: Filter[],
|
||||||
|
activeViewlet: Record<string, Ref<Viewlet> | null>,
|
||||||
|
viewOptionStore: Map<string, ViewOptions>
|
||||||
|
): boolean {
|
||||||
|
if (selectedFilter === undefined) return false
|
||||||
|
if (selectedFilter.createdBy !== me) return false
|
||||||
|
const loc = getCurrentLocation()
|
||||||
|
const key = makeViewletKey(loc)
|
||||||
|
if (selectedFilter.viewletId !== activeViewlet[key]) return true
|
||||||
|
if (selectedFilter.filters !== JSON.stringify(filters)) return true
|
||||||
|
if (selectedFilter.viewletId !== null) {
|
||||||
|
const viewOptions = getViewOptions({ _id: selectedFilter.viewletId } as Viewlet, viewOptionStore)
|
||||||
|
if (JSON.stringify(selectedFilter.viewOptions) !== JSON.stringify(viewOptions)) return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if visible && $filterStore && $filterStore.length > 0}
|
{#if visible && $filterStore && $filterStore.length > 0}
|
||||||
@ -135,6 +175,7 @@
|
|||||||
{filter}
|
{filter}
|
||||||
on:change={() => {
|
on:change={() => {
|
||||||
makeQuery(query, $filterStore)
|
makeQuery(query, $filterStore)
|
||||||
|
updateFilter(filter)
|
||||||
}}
|
}}
|
||||||
on:remove={() => {
|
on:remove={() => {
|
||||||
removeFilter(i)
|
removeFilter(i)
|
||||||
@ -146,7 +187,22 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button icon={view.icon.Views} label={view.string.Save} width={'fit-content'} on:click={() => saveFilteredView()} />
|
<div class="flex gap-1-5">
|
||||||
|
<Button
|
||||||
|
icon={view.icon.Views}
|
||||||
|
label={view.string.SaveAs}
|
||||||
|
width={'fit-content'}
|
||||||
|
on:click={() => saveFilteredView()}
|
||||||
|
/>
|
||||||
|
{#if selectedFilterChanged($selectedFilterStore, $filterStore, $activeViewlet, $viewOptionStore)}
|
||||||
|
<Button
|
||||||
|
icon={view.icon.Views}
|
||||||
|
label={view.string.Save}
|
||||||
|
width={'fit-content'}
|
||||||
|
on:click={() => saveCurrentFilteredView($selectedFilterStore)}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
@ -15,9 +15,9 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Class, Doc, Ref, Space } from '@hcengineering/core'
|
import { Class, Doc, Ref, Space } from '@hcengineering/core'
|
||||||
import { getClient } from '@hcengineering/presentation'
|
import { getClient } from '@hcengineering/presentation'
|
||||||
import { Button, IconClose, eventToHTMLElement, showPopup } from '@hcengineering/ui'
|
import { Button, IconClose, eventToHTMLElement, resolvedLocationStore, showPopup } from '@hcengineering/ui'
|
||||||
import { Filter } from '@hcengineering/view'
|
import { Filter } from '@hcengineering/view'
|
||||||
import { filterStore, setFilters } from '../../filter'
|
import { filterStore, getFilterKey, setFilters } from '../../filter'
|
||||||
import view from '../../plugin'
|
import view from '../../plugin'
|
||||||
import FilterTypePopup from './FilterTypePopup.svelte'
|
import FilterTypePopup from './FilterTypePopup.svelte'
|
||||||
import IconFilter from '../icons/Filter.svelte'
|
import IconFilter from '../icons/Filter.svelte'
|
||||||
@ -29,6 +29,25 @@
|
|||||||
const client = getClient()
|
const client = getClient()
|
||||||
const hierarchy = client.getHierarchy()
|
const hierarchy = client.getHierarchy()
|
||||||
|
|
||||||
|
resolvedLocationStore.subscribe(() => {
|
||||||
|
load(_class)
|
||||||
|
})
|
||||||
|
|
||||||
|
function load (_class: Ref<Class<Doc>> | undefined) {
|
||||||
|
const key = getFilterKey(_class)
|
||||||
|
const items = localStorage.getItem(key)
|
||||||
|
if (items !== null) {
|
||||||
|
filterStore.set(JSON.parse(items))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function save (_class: Ref<Class<Doc>> | undefined, p: Filter[]) {
|
||||||
|
const key = getFilterKey(_class)
|
||||||
|
localStorage.setItem(key, JSON.stringify(p))
|
||||||
|
}
|
||||||
|
|
||||||
|
filterStore.subscribe((p) => save(_class, p))
|
||||||
|
|
||||||
function onChange (e: Filter | undefined) {
|
function onChange (e: Filter | undefined) {
|
||||||
if (e !== undefined) setFilters([e])
|
if (e !== undefined) setFilters([e])
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Class, Doc, Ref } from '@hcengineering/core'
|
import { Class, Doc, Ref, getCurrentAccount } from '@hcengineering/core'
|
||||||
import preference from '@hcengineering/preference'
|
import preference from '@hcengineering/preference'
|
||||||
import { Card, getClient } from '@hcengineering/presentation'
|
import { Card, getClient } from '@hcengineering/presentation'
|
||||||
import { Button, EditBox, getCurrentResolvedLocation } from '@hcengineering/ui'
|
import { Button, EditBox, ToggleWithLabel, getCurrentResolvedLocation } from '@hcengineering/ui'
|
||||||
import { ViewOptions } from '@hcengineering/view'
|
import { ViewOptions } from '@hcengineering/view'
|
||||||
import { createEventDispatcher } from 'svelte'
|
import { createEventDispatcher } from 'svelte'
|
||||||
import { filterStore } from '../../filter'
|
import { filterStore } from '../../filter'
|
||||||
@ -12,6 +12,9 @@
|
|||||||
export let viewOptions: ViewOptions | undefined = undefined
|
export let viewOptions: ViewOptions | undefined = undefined
|
||||||
export let _class: Ref<Class<Doc>>
|
export let _class: Ref<Class<Doc>>
|
||||||
|
|
||||||
|
const me = getCurrentAccount()._id
|
||||||
|
let sharable = true
|
||||||
|
|
||||||
let filterName = ''
|
let filterName = ''
|
||||||
const client = getClient()
|
const client = getClient()
|
||||||
|
|
||||||
@ -27,7 +30,9 @@
|
|||||||
filters,
|
filters,
|
||||||
attachedTo: loc.path[2] as Ref<Doc>,
|
attachedTo: loc.path[2] as Ref<Doc>,
|
||||||
viewOptions,
|
viewOptions,
|
||||||
viewletId: getActiveViewletId()
|
viewletId: getActiveViewletId(),
|
||||||
|
sharable,
|
||||||
|
users: [me]
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -57,4 +62,5 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<ToggleWithLabel bind:on={sharable} label={view.string.Public} />
|
||||||
</Card>
|
</Card>
|
||||||
|
@ -13,7 +13,7 @@ import core, {
|
|||||||
import { getResource } from '@hcengineering/platform'
|
import { getResource } from '@hcengineering/platform'
|
||||||
import { LiveQuery, createQuery, getClient } from '@hcengineering/presentation'
|
import { LiveQuery, createQuery, getClient } from '@hcengineering/presentation'
|
||||||
import { AnyComponent, locationToUrl, getCurrentResolvedLocation } from '@hcengineering/ui'
|
import { AnyComponent, locationToUrl, getCurrentResolvedLocation } from '@hcengineering/ui'
|
||||||
import { Filter, FilterMode, KeyFilter } from '@hcengineering/view'
|
import { Filter, FilterMode, FilteredView, KeyFilter } from '@hcengineering/view'
|
||||||
import { get, writable } from 'svelte/store'
|
import { get, writable } from 'svelte/store'
|
||||||
import view from './plugin'
|
import view from './plugin'
|
||||||
|
|
||||||
@ -22,6 +22,11 @@ import view from './plugin'
|
|||||||
*/
|
*/
|
||||||
export const filterStore = writable<Filter[]>([])
|
export const filterStore = writable<Filter[]>([])
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
export const selectedFilterStore = writable<FilteredView | undefined>()
|
||||||
|
|
||||||
export function setFilters (filters: Filter[]): void {
|
export function setFilters (filters: Filter[]): void {
|
||||||
const old = get(filterStore)
|
const old = get(filterStore)
|
||||||
old.forEach((p) => p.onRemove?.())
|
old.forEach((p) => p.onRemove?.())
|
||||||
@ -288,9 +293,14 @@ export function createFilter (_class: Ref<Class<Doc>>, key: string, value: any[]
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getFilterKey (_class: Ref<Class<Doc>>): string {
|
export function getFilterKey (_class: Ref<Class<Doc>> | undefined): string {
|
||||||
const loc = getCurrentResolvedLocation()
|
const loc = getCurrentResolvedLocation()
|
||||||
|
loc.path.length = 3
|
||||||
loc.fragment = undefined
|
loc.fragment = undefined
|
||||||
loc.query = undefined
|
loc.query = undefined
|
||||||
return 'filter' + locationToUrl(loc) + _class
|
let res = 'filter' + locationToUrl(loc)
|
||||||
|
if (_class !== undefined) {
|
||||||
|
res = res + _class
|
||||||
|
}
|
||||||
|
return res
|
||||||
}
|
}
|
||||||
|
@ -102,6 +102,7 @@ export { getActions, invokeAction } from './actions'
|
|||||||
export { default as ActionContext } from './components/ActionContext.svelte'
|
export { default as ActionContext } from './components/ActionContext.svelte'
|
||||||
export { default as ActionHandler } from './components/ActionHandler.svelte'
|
export { default as ActionHandler } from './components/ActionHandler.svelte'
|
||||||
export { default as FilterButton } from './components/filter/FilterButton.svelte'
|
export { default as FilterButton } from './components/filter/FilterButton.svelte'
|
||||||
|
export { default as AddSavedView } from './components/filter/AddSavedView.svelte'
|
||||||
export { default as FixedColumn } from './components/FixedColumn.svelte'
|
export { default as FixedColumn } from './components/FixedColumn.svelte'
|
||||||
export { default as SourcePresenter } from './components/inference/SourcePresenter.svelte'
|
export { default as SourcePresenter } from './components/inference/SourcePresenter.svelte'
|
||||||
export { default as LinkPresenter } from './components/LinkPresenter.svelte'
|
export { default as LinkPresenter } from './components/LinkPresenter.svelte'
|
||||||
|
@ -76,7 +76,8 @@ export default mergeIds(viewId, view, {
|
|||||||
ThisMonth: '' as IntlString,
|
ThisMonth: '' as IntlString,
|
||||||
NextMonth: '' as IntlString,
|
NextMonth: '' as IntlString,
|
||||||
NotSpecified: '' as IntlString,
|
NotSpecified: '' as IntlString,
|
||||||
CustomDate: '' as IntlString
|
CustomDate: '' as IntlString,
|
||||||
|
SaveAs: '' as IntlString
|
||||||
},
|
},
|
||||||
function: {
|
function: {
|
||||||
StatusSort: '' as SortFunc
|
StatusSort: '' as SortFunc
|
||||||
|
@ -15,6 +15,7 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
|
Account,
|
||||||
AnyAttribute,
|
AnyAttribute,
|
||||||
CategoryType,
|
CategoryType,
|
||||||
Class,
|
Class,
|
||||||
@ -35,7 +36,7 @@ import type {
|
|||||||
Type,
|
Type,
|
||||||
UXObject
|
UXObject
|
||||||
} from '@hcengineering/core'
|
} from '@hcengineering/core'
|
||||||
import { Asset, IntlString, Plugin, plugin, Resource, Status } from '@hcengineering/platform'
|
import { Asset, IntlString, Plugin, Resource, Status, plugin } from '@hcengineering/platform'
|
||||||
import type { Preference } from '@hcengineering/preference'
|
import type { Preference } from '@hcengineering/preference'
|
||||||
import type {
|
import type {
|
||||||
AnyComponent,
|
AnyComponent,
|
||||||
@ -99,13 +100,17 @@ export interface Filter {
|
|||||||
/**
|
/**
|
||||||
* @public
|
* @public
|
||||||
*/
|
*/
|
||||||
export interface FilteredView extends Preference {
|
export interface FilteredView extends Doc {
|
||||||
name: string
|
name: string
|
||||||
location: PlatformLocation
|
location: PlatformLocation
|
||||||
filters: string
|
filters: string
|
||||||
viewOptions?: ViewOptions
|
viewOptions?: ViewOptions
|
||||||
filterClass?: Ref<Class<Doc>>
|
filterClass?: Ref<Class<Doc>>
|
||||||
viewletId?: Ref<Viewlet> | null
|
viewletId?: Ref<Viewlet> | null
|
||||||
|
sharable?: boolean
|
||||||
|
users: Ref<Account>[]
|
||||||
|
createdBy: Ref<Account>
|
||||||
|
attachedTo: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -707,7 +712,10 @@ const view = plugin(viewId, {
|
|||||||
SelectToMove: '' as IntlString,
|
SelectToMove: '' as IntlString,
|
||||||
Cancel: '' as IntlString,
|
Cancel: '' as IntlString,
|
||||||
List: '' as IntlString,
|
List: '' as IntlString,
|
||||||
Timeline: '' as IntlString
|
AddSavedView: '' as IntlString,
|
||||||
|
Timeline: '' as IntlString,
|
||||||
|
Public: '' as IntlString,
|
||||||
|
Hide: '' as IntlString
|
||||||
},
|
},
|
||||||
icon: {
|
icon: {
|
||||||
Table: '' as Asset,
|
Table: '' as Asset,
|
||||||
|
@ -1,16 +1,18 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Doc, Ref } from '@hcengineering/core'
|
import { Ref, getCurrentAccount } from '@hcengineering/core'
|
||||||
import { createQuery, getClient } from '@hcengineering/presentation'
|
import { createQuery, getClient } from '@hcengineering/presentation'
|
||||||
import setting from '@hcengineering/setting'
|
import setting from '@hcengineering/setting'
|
||||||
import { Action, Location, location, navigate } from '@hcengineering/ui'
|
import { Action, IconAdd, Location, eventToHTMLElement, location, navigate, showPopup } from '@hcengineering/ui'
|
||||||
import view, { Filter, FilteredView, ViewOptions, Viewlet } from '@hcengineering/view'
|
import view, { Filter, FilteredView, ViewOptions, Viewlet } from '@hcengineering/view'
|
||||||
import {
|
import {
|
||||||
|
AddSavedView,
|
||||||
TreeItem,
|
TreeItem,
|
||||||
TreeNode,
|
TreeNode,
|
||||||
activeViewlet,
|
activeViewlet,
|
||||||
filterStore,
|
filterStore,
|
||||||
getViewOptions,
|
getViewOptions,
|
||||||
makeViewletKey,
|
makeViewletKey,
|
||||||
|
selectedFilterStore,
|
||||||
setActiveViewletId,
|
setActiveViewletId,
|
||||||
setFilters,
|
setFilters,
|
||||||
setViewOptions,
|
setViewOptions,
|
||||||
@ -23,16 +25,15 @@
|
|||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
const client = getClient()
|
const client = getClient()
|
||||||
|
const me = getCurrentAccount()._id
|
||||||
|
|
||||||
const filteredViewsQuery = createQuery()
|
const filteredViewsQuery = createQuery()
|
||||||
let filteredViews: FilteredView[] | undefined
|
let availableFilteredViews: FilteredView[] = []
|
||||||
$: filteredViewsQuery.query(
|
let myFilteredViews: FilteredView[] = []
|
||||||
view.class.FilteredView,
|
$: filteredViewsQuery.query(view.class.FilteredView, { attachedTo: currentApplication?.alias }, (result) => {
|
||||||
{ attachedTo: currentApplication?.alias as Ref<Doc> },
|
myFilteredViews = result.filter((p) => p.users.includes(me))
|
||||||
(result) => {
|
availableFilteredViews = result.filter((p) => p.sharable && !p.users.includes(me))
|
||||||
filteredViews = result
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
async function removeAction (filteredView: FilteredView): Promise<Action[]> {
|
async function removeAction (filteredView: FilteredView): Promise<Action[]> {
|
||||||
return [
|
return [
|
||||||
@ -46,9 +47,27 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function viewAction (filteredView: FilteredView): Promise<Action[]> {
|
||||||
|
if (filteredView.createdBy === me) return await removeAction(filteredView)
|
||||||
|
return await hideAction(filteredView)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function hideAction (object: FilteredView): Promise<Action[]> {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
icon: view.icon.Archive,
|
||||||
|
label: view.string.Hide,
|
||||||
|
action: async (ctx: any, evt: Event) => {
|
||||||
|
await client.update(object, { $pull: { users: me } })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
let selectedId: Ref<FilteredView> | undefined = undefined
|
let selectedId: Ref<FilteredView> | undefined = undefined
|
||||||
|
|
||||||
async function load (fv: FilteredView): Promise<void> {
|
async function load (fv: FilteredView): Promise<void> {
|
||||||
|
selectedFilterStore.set(fv)
|
||||||
navigate({
|
navigate({
|
||||||
path: fv.location.path,
|
path: fv.location.path,
|
||||||
query: fv.location.query ?? undefined,
|
query: fv.location.query ?? undefined,
|
||||||
@ -65,10 +84,28 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const clearSelection = () => {
|
const clearSelection = () => {
|
||||||
|
selectedFilterStore.set(undefined)
|
||||||
selectedId = undefined
|
selectedId = undefined
|
||||||
dispatch('select', false)
|
dispatch('select', false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function checkFilter (
|
||||||
|
fv: FilteredView,
|
||||||
|
loc: Location,
|
||||||
|
filters: string,
|
||||||
|
viewOptionStore: Map<string, ViewOptions>
|
||||||
|
): boolean {
|
||||||
|
if (fv.location.path.join() !== loc.path.join()) return false
|
||||||
|
if (fv.filters !== filters) return false
|
||||||
|
const key = makeViewletKey(loc)
|
||||||
|
if (fv.viewletId !== $activeViewlet[key]) return false
|
||||||
|
if (fv.viewletId !== null) {
|
||||||
|
const viewOptions = getViewOptions({ _id: fv.viewletId } as Viewlet, viewOptionStore)
|
||||||
|
if (JSON.stringify(fv.viewOptions) !== JSON.stringify(viewOptions)) return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
function checkSelected (
|
function checkSelected (
|
||||||
fs: Filter[],
|
fs: Filter[],
|
||||||
loc: Location,
|
loc: Location,
|
||||||
@ -77,18 +114,19 @@
|
|||||||
) {
|
) {
|
||||||
const filters = JSON.stringify(fs)
|
const filters = JSON.stringify(fs)
|
||||||
if (loc && Array.isArray(fs) && fs.length > 0 && Array.isArray(filteredViews)) {
|
if (loc && Array.isArray(fs) && fs.length > 0 && Array.isArray(filteredViews)) {
|
||||||
for (const fv of filteredViews) {
|
if ($selectedFilterStore !== undefined) {
|
||||||
if (fv.location.path.join() !== loc.path.join()) continue
|
if ($selectedFilterStore.location.path.join() === loc.path.join()) {
|
||||||
if (fv.filters !== filters) continue
|
selectedId = $selectedFilterStore._id
|
||||||
const key = makeViewletKey(loc)
|
dispatch('select', true)
|
||||||
if (fv.viewletId !== $activeViewlet[key]) continue
|
return
|
||||||
if (fv.viewletId !== null) {
|
}
|
||||||
const viewOptions = getViewOptions({ _id: fv.viewletId } as Viewlet, viewOptionStore)
|
}
|
||||||
if (JSON.stringify(fv.viewOptions) !== JSON.stringify(viewOptions)) continue
|
for (const fv of filteredViews) {
|
||||||
|
if (checkFilter(fv, loc, filters, viewOptionStore)) {
|
||||||
|
selectedId = fv._id
|
||||||
|
dispatch('select', true)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
selectedId = fv._id
|
|
||||||
dispatch('select', true)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
clearSelection()
|
clearSelection()
|
||||||
} else {
|
} else {
|
||||||
@ -96,19 +134,36 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$: checkSelected($filterStore, $location, filteredViews, $viewOptionStore)
|
$: checkSelected($filterStore, $location, myFilteredViews, $viewOptionStore)
|
||||||
$: dispatch('shown', filteredViews !== undefined && filteredViews.length > 0)
|
|
||||||
|
$: shown = myFilteredViews.length > 0 || availableFilteredViews.length > 0
|
||||||
|
$: dispatch('shown', shown)
|
||||||
|
|
||||||
|
async function getActions (availableFilteredViews: FilteredView[]): Promise<Action[]> {
|
||||||
|
if (availableFilteredViews.length > 0) {
|
||||||
|
const add: Action = {
|
||||||
|
label: view.string.AddSavedView,
|
||||||
|
icon: IconAdd,
|
||||||
|
action: async (_, e): Promise<void> => {
|
||||||
|
showPopup(AddSavedView, { attachedTo: currentApplication?.alias }, eventToHTMLElement(e as MouseEvent))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [add]
|
||||||
|
} else {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if filteredViews && filteredViews.length > 0}
|
{#if shown}
|
||||||
<TreeNode label={view.string.FilteredViews} parent>
|
<TreeNode label={view.string.FilteredViews} parent actions={async () => getActions(availableFilteredViews)}>
|
||||||
{#each filteredViews as fv}
|
{#each myFilteredViews as fv}
|
||||||
<TreeItem
|
<TreeItem
|
||||||
_id={fv._id}
|
_id={fv._id}
|
||||||
title={fv.name}
|
title={fv.name}
|
||||||
selected={selectedId === fv._id}
|
selected={selectedId === fv._id}
|
||||||
on:click={() => load(fv)}
|
on:click={() => load(fv)}
|
||||||
actions={() => removeAction(fv)}
|
actions={() => viewAction(fv)}
|
||||||
/>
|
/>
|
||||||
{/each}
|
{/each}
|
||||||
</TreeNode>
|
</TreeNode>
|
||||||
|
@ -170,7 +170,7 @@ export class FullTextIndex implements WithFind {
|
|||||||
let { docs, pass } = await this.indexer.search(classes, findQuery, fullTextLimit)
|
let { docs, pass } = await this.indexer.search(classes, findQuery, fullTextLimit)
|
||||||
|
|
||||||
if (docs.length === 0 && pass) {
|
if (docs.length === 0 && pass) {
|
||||||
docs = [...docs, ...(await this.adapter.search(classes, query, fullTextLimit))]
|
docs = await this.adapter.search(classes, query, fullTextLimit)
|
||||||
}
|
}
|
||||||
const indexedDocMap = new Map<Ref<Doc>, IndexedDoc>()
|
const indexedDocMap = new Map<Ref<Doc>, IndexedDoc>()
|
||||||
|
|
||||||
|
@ -115,10 +115,6 @@ export class FullTextPushStage implements FullTextPipelineStage {
|
|||||||
if (pipeline.cancelling) {
|
if (pipeline.cancelling) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (pipeline.cancelling) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const elasticDoc = createElasticDoc(doc)
|
const elasticDoc = createElasticDoc(doc)
|
||||||
try {
|
try {
|
||||||
updateDoc2Elastic(doc.attributes, elasticDoc)
|
updateDoc2Elastic(doc.attributes, elasticDoc)
|
||||||
@ -169,12 +165,12 @@ export class FullTextPushStage implements FullTextPipelineStage {
|
|||||||
// Perform bulk update to elastic
|
// Perform bulk update to elastic
|
||||||
try {
|
try {
|
||||||
await this.fulltextAdapter.updateMany(bulk)
|
await this.fulltextAdapter.updateMany(bulk)
|
||||||
|
for (const doc of toIndex) {
|
||||||
|
await pipeline.update(doc._id, true, {})
|
||||||
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error(err)
|
console.error(err)
|
||||||
}
|
}
|
||||||
for (const doc of toIndex) {
|
|
||||||
await pipeline.update(doc._id, true, {})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -418,6 +418,9 @@ export class FullTextIndexPipeline implements FullTextPipeline {
|
|||||||
{ collector: st.stageId },
|
{ collector: st.stageId },
|
||||||
async (ctx) => await st.collect(toIndex, this, ctx)
|
async (ctx) => await st.collect(toIndex, this, ctx)
|
||||||
)
|
)
|
||||||
|
if (this.cancelling) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
toIndex.forEach((it) => _classUpdate.add(it.objectClass))
|
toIndex.forEach((it) => _classUpdate.add(it.objectClass))
|
||||||
|
|
||||||
@ -432,6 +435,9 @@ export class FullTextIndexPipeline implements FullTextPipeline {
|
|||||||
async (ctx) => await nst.collect(toIndex2, this, ctx)
|
async (ctx) => await nst.collect(toIndex2, this, ctx)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
if (this.cancelling) {
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
break
|
break
|
||||||
|
Loading…
Reference in New Issue
Block a user