Kanban search (#714)

Signed-off-by: Ilya Sumbatyants <ilya.sumb@gmail.com>
This commit is contained in:
Ilya Sumbatyants 2021-12-23 16:05:50 +07:00 committed by GitHub
parent 8be72b58aa
commit 4214d62da6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 210 additions and 131 deletions

View File

@ -79,7 +79,7 @@ export function setClient (_client: Client): void {
} }
export class LiveQuery { export class LiveQuery {
private unsubscribe = () => {} unsubscribe = () => {}
constructor () { constructor () {
onDestroy(() => { onDestroy(() => {
@ -95,7 +95,11 @@ export class LiveQuery {
options?: FindOptions<T> options?: FindOptions<T>
): void { ): void {
this.unsubscribe() this.unsubscribe()
this.unsubscribe = liveQuery.query(_class, query, callback, options) const unsub = liveQuery.query(_class, query, callback, options)
this.unsubscribe = () => {
unsub()
this.unsubscribe = () => {}
}
} }
} }

View File

@ -31,6 +31,7 @@
export let _class: Ref<Class<Item>> export let _class: Ref<Class<Item>>
export let space: Ref<SpaceWithStates> export let space: Ref<SpaceWithStates>
export let open: AnyComponent export let open: AnyComponent
export let search: string
export let options: FindOptions<Item> | undefined export let options: FindOptions<Item> | undefined
export let config: string[] export let config: string[]
@ -55,19 +56,51 @@
const doneStatesQ = createQuery() const doneStatesQ = createQuery()
$: if (kanban !== undefined) { $: if (kanban !== undefined) {
doneStatesQ.query(task.class.DoneState, { space: kanban.space }, (result) => { doneStatesQ.query(
task.class.DoneState,
{ space: kanban.space, ...search !== '' ? {$search: search} : {} },
(result) => {
wonState = result.find((x) => x._class === task.class.WonState) wonState = result.find((x) => x._class === task.class.WonState)
lostState = result.find((x) => x._class === task.class.LostState) lostState = result.find((x) => x._class === task.class.LostState)
}) })
} }
const query = createQuery() const objsQ = createQuery()
$: query.query(_class, { space, doneState: null }, result => { objects = result }, { $: objsQ.query(
_class,
{
space,
doneState: null,
...search !== '' ? {$search: search} : {}
},
result => { objects = result },
{
...options, ...options,
sort: { sort: {
rank: SortingOrder.Ascending rank: SortingOrder.Ascending
}, },
}) }
)
const filteredObjsQ = createQuery()
// Undefined means no filtering
let target: Set<Ref<Doc>> | undefined
$: if (search === '') {
filteredObjsQ.unsubscribe()
target = undefined
} else {
filteredObjsQ.query(
_class,
{
space,
doneState: null,
...search !== '' ? {$search: search} : {}
},
result => { target = new Set(result.map(x => x._id)) },
options
)
}
function dragover (ev: MouseEvent, object: Item) { function dragover (ev: MouseEvent, object: Item) {
if (dragCard !== object) { if (dragCard !== object) {
@ -162,7 +195,7 @@
}}> }}>
<!-- <KanbanCardEmpty label={'Create new application'} /> --> <!-- <KanbanCardEmpty label={'Create new application'} /> -->
{#each objects as object, j (object)} {#each objects as object, j (object)}
{#if object.state === state._id} {#if object.state === state._id && (target === undefined || target.has(object._id))}
<div <div
class="step-tb75" class="step-tb75"
on:dragover|preventDefault={(ev) => { on:dragover|preventDefault={(ev) => {

View File

@ -0,0 +1,70 @@
<!--
// Copyright © 2020, 2021 Anticrm Platform Contributors.
// Copyright © 2021 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 type { Ref, Class, Doc, Space, WithLookup } from '@anticrm/core'
import type { Viewlet } from '@anticrm/view'
import { Component } from '@anticrm/ui'
export let _class: Ref<Class<Doc>>
export let space: Ref<Space>
export let search: string
export let viewlet: WithLookup<Viewlet> | undefined
</script>
{#if viewlet}
<div class="container">
<Component is={viewlet.$lookup?.descriptor?.component} props={ {
_class,
space,
open: viewlet.open,
options: viewlet.options,
config: viewlet.config,
search
} } />
</div>
{/if}
<style lang="scss">
.toolbar {
margin: 1.25rem 1.75rem 1.75rem 2.5rem;
.btn {
width: 2.5rem;
height: 2.5rem;
background-color: transparent;
border-radius: .5rem;
cursor: pointer;
color: var(--theme-content-trans-color);
&:hover { color: var(--theme-caption-color); }
&.selected {
color: var(--theme-content-accent-color);
background-color: var(--theme-button-bg-enabled);
cursor: default;
&:hover { color: var(--theme-caption-color); }
}
}
}
.container {
display: flex;
flex-direction: column;
min-height: 0;
height: 100%;
}
</style>

View File

@ -14,42 +14,78 @@
--> -->
<script lang="ts"> <script lang="ts">
import core, { Class, Doc, WithLookup } from '@anticrm/core'
import type { Ref, Space } from '@anticrm/core' import type { Ref, Space } from '@anticrm/core'
import { Icon, ActionIcon, Button, IconMoreH, IconAdd } from '@anticrm/ui'
import type { AnyComponent } from '@anticrm/ui'
import Header from './Header.svelte'
import Star from './icons/Star.svelte'
import { getClient, createQuery } from '@anticrm/presentation' import { getClient, createQuery } from '@anticrm/presentation'
import { Icon, Button, EditWithIcon, IconSearch, Tooltip } from '@anticrm/ui'
import type { AnyComponent } from '@anticrm/ui'
import { showPopup } from '@anticrm/ui' import { showPopup } from '@anticrm/ui'
import view, { Viewlet } from '@anticrm/view'
import { classIcon } from '../utils' import { classIcon } from '../utils'
import core from '@anticrm/core'
import workbench from '../plugin' import workbench from '../plugin'
export let space: Ref<Space> | undefined import Header from './Header.svelte'
export let spaceId: Ref<Space> | undefined
export let _class: Ref<Class<Doc>> | undefined
export let createItemDialog: AnyComponent | undefined export let createItemDialog: AnyComponent | undefined
export let divider: boolean = false export let search: string
export let viewlet: WithLookup<Viewlet> | undefined
const client = getClient() const client = getClient()
const query = createQuery() const query = createQuery()
let data: Space | undefined let space: Space | undefined
$: query.query(core.class.Space, { _id: space }, result => { data = result[0] }) $: query.query(core.class.Space, { _id: spaceId }, result => { space = result[0] })
function onSearch(ev: Event) {
search = (ev.target as HTMLInputElement).value
}
let viewlets: WithLookup<Viewlet>[] = []
async function getViewlets(attachTo: Ref<Class<Doc>>): Promise<void> {
viewlets = await client.findAll(view.class.Viewlet, { attachTo }, { lookup: {
descriptor: core.class.Class
}})
}
$: if (_class) {
getViewlets(_class)
}
function resetSelectedViewlet (_space: Ref<Space> | undefined) {
selectedViewlet = 0
}
$: resetSelectedViewlet(spaceId)
function showCreateDialog(ev: Event) { function showCreateDialog(ev: Event) {
showPopup(createItemDialog as AnyComponent, { space }, ev.target as HTMLElement) showPopup(createItemDialog as AnyComponent, { space: spaceId }, ev.target as HTMLElement)
} }
let selectedViewlet = 0
$: viewlet = viewlets[selectedViewlet]
</script> </script>
<div class="spaceheader-container" class:bottom-divider={divider}> <div class="spaceheader-container">
{#if data} {#if space}
<Header icon={classIcon(client, data._class)} label={data.name} description={data.description} /> <Header icon={classIcon(client, space._class)} label={space.name} description={space.description} />
{#if viewlets.length > 1}
<div class="flex">
{#each viewlets as viewlet, i}
<Tooltip label={viewlet.$lookup?.descriptor?.label} direction={'top'}>
<div class="flex-center btn" class:selected={selectedViewlet === i} on:click={()=>{ selectedViewlet = i }}>
<Icon icon={viewlet.$lookup?.descriptor?.icon} size={'small'}/>
</div>
</Tooltip>
{/each}
</div>
{/if}
<EditWithIcon icon={IconSearch} placeholder={'Search'} on:change={onSearch}/>
{#if createItemDialog} {#if createItemDialog}
<Button label={workbench.string.Create} primary={true} size={'small'} on:click={(ev) => showCreateDialog(ev)}/> <Button label={workbench.string.Create} primary={true} size={'small'} on:click={(ev) => showCreateDialog(ev)}/>
{/if} {/if}
<!-- <ActionIcon label={'Favorite'} icon={Star} size={'small'}/>
<ActionIcon label={'Create'} icon={IconAdd} size={'small'}/>
<ActionIcon label={'More...'} icon={IconMoreH} size={'small'}/> -->
{/if} {/if}
</div> </div>
@ -65,4 +101,21 @@
height: 4rem; height: 4rem;
min-height: 4rem; min-height: 4rem;
} }
.btn {
width: 2.5rem;
height: 2.5rem;
background-color: transparent;
border-radius: .5rem;
cursor: pointer;
color: var(--theme-content-trans-color);
&:hover { color: var(--theme-caption-color); }
&.selected {
color: var(--theme-content-accent-color);
background-color: var(--theme-button-bg-enabled);
cursor: default;
&:hover { color: var(--theme-caption-color); }
}
}
</style> </style>

View File

@ -1,6 +1,5 @@
<!-- <!--
// Copyright © 2020, 2021 Anticrm Platform Contributors. // Copyright © 2020, 2021 Anticrm Platform Contributors.
// Copyright © 2021 Hardcore Engineering Inc.
// //
// Licensed under the Eclipse Public License, Version 2.0 (the "License"); // 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 // you may not use this file except in compliance with the License. You may
@ -15,106 +14,30 @@
--> -->
<script lang="ts"> <script lang="ts">
import type { Ref, Space, WithLookup } from '@anticrm/core'
import type { AnyComponent } from '@anticrm/ui'
import type { Viewlet } from '@anticrm/view'
import type { ViewConfiguration } from '@anticrm/workbench'
import type { Ref, Class ,Doc, FindOptions, Space, WithLookup, Obj, Client } from '@anticrm/core' import SpaceContent from './SpaceContent.svelte'
import type { Viewlet } from '@anticrm/view' import SpaceHeader from './SpaceHeader.svelte'
import { getClient } from '@anticrm/presentation' export let currentSpace: Ref<Space> | undefined
export let currentView: ViewConfiguration | undefined
export let createItemDialog: AnyComponent | undefined
import { Icon, Component, EditWithIcon, IconSearch, Tooltip } from '@anticrm/ui' let search: string = ''
let viewlet: WithLookup<Viewlet> | undefined = undefined
import view from '@anticrm/view'
import core from '@anticrm/core'
export let _class: Ref<Class<Doc>> function resetSearch (_space: Ref<Space> | undefined): void {
export let space: Ref<Space> search = ''
}
const client = getClient()
type ViewletConfig = WithLookup<Viewlet>
async function getViewlets(client: Client, _class: Ref<Class<Obj>>): Promise<ViewletConfig[]> {
return await client.findAll(view.class.Viewlet, { attachTo: _class }, { lookup: {
descriptor: core.class.Class
}})
}
let selected = 0
function onSpace(space: Ref<Space>) {
selected = 0
}
$: onSpace(space)
let search = ''
function onSearch(ev: Event) {
search = (ev.target as HTMLInputElement).value
}
$: resetSearch(currentSpace)
</script> </script>
{#await getViewlets(client, _class)} <SpaceHeader spaceId={currentSpace} _class={currentView?.class} {createItemDialog} bind:search={search} bind:viewlet={viewlet} />
... {#if currentView && currentSpace}
{:then viewlets} <SpaceContent space={currentSpace} _class={currentView.class} {search} {viewlet} />
{/if}
{#if viewlets.length > 0}
<div class="flex-between toolbar">
<EditWithIcon icon={IconSearch} placeholder={'Search'} on:change={onSearch}/>
{#if viewlets.length > 1}
<div class="flex">
{#each viewlets as viewlet, i}
<Tooltip label={viewlet.$lookup?.descriptor?.label} direction={'top'}>
<div class="flex-center btn" class:selected={selected === i} on:click={()=>{ selected = i }}>
<Icon icon={viewlet.$lookup?.descriptor?.icon} size={'small'}/>
</div>
</Tooltip>
{/each}
</div>
{/if}
</div>
{/if}
<div class="container">
<Component is={viewlets[selected].$lookup?.descriptor?.component} props={ {
_class,
space,
open: viewlets[selected].open,
options: viewlets[selected].options,
config: viewlets[selected].config,
search
} } />
</div>
{/await}
<style lang="scss">
.toolbar {
margin: 1.25rem 1.75rem 1.75rem 2.5rem;
.btn {
width: 2.5rem;
height: 2.5rem;
background-color: transparent;
border-radius: .5rem;
cursor: pointer;
color: var(--theme-content-trans-color);
&:hover { color: var(--theme-caption-color); }
&.selected {
color: var(--theme-content-accent-color);
background-color: var(--theme-button-bg-enabled);
cursor: default;
&:hover { color: var(--theme-caption-color); }
}
}
}
.container {
display: flex;
flex-direction: column;
min-height: 0;
height: 100%;
}
</style>

View File

@ -26,7 +26,6 @@
import workbench from '../plugin' import workbench from '../plugin'
import Navigator from './Navigator.svelte' import Navigator from './Navigator.svelte'
import SpaceHeader from './SpaceHeader.svelte'
import SpaceView from './SpaceView.svelte' import SpaceView from './SpaceView.svelte'
import { AnyComponent, Component, location, Popup, showPopup, TooltipInstance, closeTooltip, ActionIcon, IconEdit, AnySvelteComponent } from '@anticrm/ui' import { AnyComponent, Component, location, Popup, showPopup, TooltipInstance, closeTooltip, ActionIcon, IconEdit, AnySvelteComponent } from '@anticrm/ui'
@ -141,10 +140,7 @@
{:else if specialComponent} {:else if specialComponent}
<Component is={specialComponent} /> <Component is={specialComponent} />
{:else} {:else}
<SpaceHeader space={currentSpace} {createItemDialog} /> <SpaceView {currentSpace} {currentView} {createItemDialog}/>
{#if currentView && currentSpace}
<SpaceView space={currentSpace} _class={currentView.class} options={currentView.options} />
{/if}
{/if} {/if}
</div> </div>
<!-- <div class="aside"><Chat thread/></div> --> <!-- <div class="aside"><Chat thread/></div> -->