mirror of
https://github.com/hcengineering/platform.git
synced 2024-12-23 19:44:59 +03:00
Kanban search (#714)
Signed-off-by: Ilya Sumbatyants <ilya.sumb@gmail.com>
This commit is contained in:
parent
8be72b58aa
commit
4214d62da6
@ -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 = () => {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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) => {
|
||||||
|
@ -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>
|
@ -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>
|
||||||
|
@ -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>
|
|
||||||
|
@ -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> -->
|
||||||
|
Loading…
Reference in New Issue
Block a user