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 {
private unsubscribe = () => {}
unsubscribe = () => {}
constructor () {
onDestroy(() => {
@ -95,7 +95,11 @@ export class LiveQuery {
options?: FindOptions<T>
): void {
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 space: Ref<SpaceWithStates>
export let open: AnyComponent
export let search: string
export let options: FindOptions<Item> | undefined
export let config: string[]
@ -55,19 +56,51 @@
const doneStatesQ = createQuery()
$: 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)
lostState = result.find((x) => x._class === task.class.LostState)
})
}
const query = createQuery()
$: query.query(_class, { space, doneState: null }, result => { objects = result }, {
const objsQ = createQuery()
$: objsQ.query(
_class,
{
space,
doneState: null,
...search !== '' ? {$search: search} : {}
},
result => { objects = result },
{
...options,
sort: {
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) {
if (dragCard !== object) {
@ -162,7 +195,7 @@
}}>
<!-- <KanbanCardEmpty label={'Create new application'} /> -->
{#each objects as object, j (object)}
{#if object.state === state._id}
{#if object.state === state._id && (target === undefined || target.has(object._id))}
<div
class="step-tb75"
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">
import core, { Class, Doc, WithLookup } 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 { Icon, Button, EditWithIcon, IconSearch, Tooltip } from '@anticrm/ui'
import type { AnyComponent } from '@anticrm/ui'
import { showPopup } from '@anticrm/ui'
import view, { Viewlet } from '@anticrm/view'
import { classIcon } from '../utils'
import core from '@anticrm/core'
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 divider: boolean = false
export let search: string
export let viewlet: WithLookup<Viewlet> | undefined
const client = getClient()
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) {
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>
<div class="spaceheader-container" class:bottom-divider={divider}>
{#if data}
<Header icon={classIcon(client, data._class)} label={data.name} description={data.description} />
<div class="spaceheader-container">
{#if space}
<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}
<Button label={workbench.string.Create} primary={true} size={'small'} on:click={(ev) => showCreateDialog(ev)}/>
{/if}
<!-- <ActionIcon label={'Favorite'} icon={Star} size={'small'}/>
<ActionIcon label={'Create'} icon={IconAdd} size={'small'}/>
<ActionIcon label={'More...'} icon={IconMoreH} size={'small'}/> -->
{/if}
</div>
@ -65,4 +101,21 @@
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>

View File

@ -1,6 +1,5 @@
<!--
// 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
@ -15,106 +14,30 @@
-->
<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 type { Viewlet } from '@anticrm/view'
import SpaceContent from './SpaceContent.svelte'
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>>
export let space: Ref<Space>
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
}
function resetSearch (_space: Ref<Space> | undefined): void {
search = ''
}
$: resetSearch(currentSpace)
</script>
{#await getViewlets(client, _class)}
...
{:then viewlets}
{#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>
<SpaceHeader spaceId={currentSpace} _class={currentView?.class} {createItemDialog} bind:search={search} bind:viewlet={viewlet} />
{#if currentView && currentSpace}
<SpaceContent space={currentSpace} _class={currentView.class} {search} {viewlet} />
{/if}

View File

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