mirror of
https://github.com/hcengineering/platform.git
synced 2026-09-12 20:57:45 +02:00
Status filters in table view (#693)
Signed-off-by: Denis Bykhov <80476319+BykhovDenis@users.noreply.github.com>
This commit is contained in:
@@ -209,6 +209,32 @@ export function createModel (builder: Builder): void {
|
||||
config: ['$lookup.attachedTo', '$lookup.state', '$lookup.attachedTo.city', '$lookup.attachedTo.channels']
|
||||
})
|
||||
|
||||
builder.createDoc(view.class.Viewlet, core.space.Model, {
|
||||
attachTo: recruit.class.Applicant,
|
||||
descriptor: task.viewlet.StatusTable,
|
||||
open: contact.component.EditContact,
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
options: {
|
||||
lookup: {
|
||||
attachedTo: recruit.class.Candidate,
|
||||
state: task.class.State,
|
||||
assignee: contact.class.Employee,
|
||||
doneState: task.class.DoneState
|
||||
}
|
||||
} as FindOptions<Doc>, // TODO: fix
|
||||
config: [
|
||||
'',
|
||||
'$lookup.attachedTo',
|
||||
'$lookup.assignee',
|
||||
'$lookup.state',
|
||||
'$lookup.doneState',
|
||||
{ presenter: attachment.component.AttachmentsPresenter, label: 'Files', sortingKey: 'attachments' },
|
||||
{ presenter: chunter.component.CommentsPresenter, label: 'Comments', sortingKey: 'comments' },
|
||||
'modifiedOn',
|
||||
'$lookup.attachedTo.channels'
|
||||
]
|
||||
})
|
||||
|
||||
builder.mixin(recruit.class.Applicant, core.class.Class, task.mixin.KanbanCard, {
|
||||
card: recruit.component.KanbanCard
|
||||
})
|
||||
|
||||
@@ -55,7 +55,7 @@ export const DOMAIN_TASK = 'task' as Domain
|
||||
export const DOMAIN_STATE = 'state' as Domain
|
||||
export const DOMAIN_KANBAN = 'kanban' as Domain
|
||||
@Model(task.class.State, core.class.Doc, DOMAIN_STATE, [task.interface.DocWithRank])
|
||||
@UX('State' as IntlString, undefined, undefined, 'title')
|
||||
@UX('State' as IntlString, undefined, undefined, 'rank')
|
||||
export class TState extends TDoc implements State {
|
||||
@Prop(TypeString(), 'Title' as IntlString)
|
||||
title!: string
|
||||
@@ -246,6 +246,17 @@ export function createModel (builder: Builder): void {
|
||||
}
|
||||
})
|
||||
|
||||
builder.createDoc(
|
||||
view.class.ViewletDescriptor,
|
||||
core.space.Model,
|
||||
{
|
||||
label: 'Status' as IntlString,
|
||||
icon: task.icon.Status,
|
||||
component: task.component.StatusTableView
|
||||
},
|
||||
task.viewlet.StatusTable
|
||||
)
|
||||
|
||||
builder.createDoc(
|
||||
workbench.class.Application,
|
||||
core.space.Model,
|
||||
|
||||
@@ -56,7 +56,8 @@ export default mergeIds(taskId, task, {
|
||||
StateEditor: '' as AnyComponent,
|
||||
KanbanView: '' as AnyComponent,
|
||||
Todos: '' as AnyComponent,
|
||||
TodoItemPresenter: '' as AnyComponent
|
||||
TodoItemPresenter: '' as AnyComponent,
|
||||
StatusTableView: '' as AnyComponent
|
||||
},
|
||||
string: {
|
||||
Task: '' as IntlString,
|
||||
|
||||
@@ -45,8 +45,6 @@
|
||||
--primary-color-skyblue: #93CAF3;
|
||||
--primary-color-pink: #FA8DA1;
|
||||
|
||||
--dark-turquoise-01: #45AEA3;
|
||||
|
||||
--highlight-red: #F96E50;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,104 +0,0 @@
|
||||
<!--
|
||||
// Copyright © 2020 Anticrm Platform Contributors.
|
||||
//
|
||||
// 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 { IntlString } from '@anticrm/platform'
|
||||
import { onMount, onDestroy } from 'svelte/internal'
|
||||
import Label from './Label.svelte'
|
||||
import StatusesBarElement from './StatusesBarElement.svelte'
|
||||
|
||||
export let items: IntlString[]
|
||||
= ['New' as IntlString, 'In progress' as IntlString, 'Interview' as IntlString, 'Interview' as IntlString, 'Final' as IntlString]
|
||||
export let selected: IntlString | undefined = undefined
|
||||
|
||||
let div: HTMLElement
|
||||
let maskLeft: boolean = false
|
||||
let maskRight: boolean = false
|
||||
let mask: 'left' | 'right' | 'both' | 'none' = 'none'
|
||||
|
||||
const checkMask = (): void => {
|
||||
maskLeft = (div && div.scrollLeft > 1) ? true : false
|
||||
maskRight = (div && div.scrollWidth - div.scrollLeft - div.clientWidth > 1) ? true : false
|
||||
if (maskLeft || maskRight) {
|
||||
if (maskLeft && maskRight) mask = 'both'
|
||||
else if (maskLeft) mask = 'left'
|
||||
else if (maskRight) mask = 'right'
|
||||
} else mask = 'none'
|
||||
}
|
||||
|
||||
const selectItem = (ev: Event, item: IntlString): void => {
|
||||
const el: HTMLElement = ev.currentTarget as HTMLElement
|
||||
el.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'center' })
|
||||
selected = item
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
if (div) {
|
||||
checkMask()
|
||||
div.addEventListener('scroll', checkMask)
|
||||
}
|
||||
})
|
||||
onDestroy(() => { if (div) div.removeEventListener('scroll', checkMask) })
|
||||
</script>
|
||||
|
||||
<div bind:this={div} class="flex-row-center statusesbar-container {mask}">
|
||||
{#each items as item, i}
|
||||
<div
|
||||
class="flex-row-center cursor-pointer step-lr25"
|
||||
class:selected={item === selected}
|
||||
on:click={(ev) => { if (item !== selected) selectItem(ev, item) }}
|
||||
>
|
||||
<StatusesBarElement side={'left'} kind={i ? 'arrow' : 'round'} selected={item === selected} />
|
||||
<div class="flex-row-center overflow-label label">
|
||||
<Label label={item} />
|
||||
</div>
|
||||
<StatusesBarElement side={'right'} kind={i < items.length - 1 ? 'arrow' : 'round'} selected={item === selected} />
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.statusesbar-container {
|
||||
overflow-x: auto;
|
||||
padding: .125rem 0;
|
||||
|
||||
&::-webkit-scrollbar:horizontal { height: .125rem; }
|
||||
&::-webkit-scrollbar-track { margin: .25rem; }
|
||||
&::-webkit-scrollbar-thumb { background-color: var(--theme-bg-accent-color); }
|
||||
}
|
||||
|
||||
.label {
|
||||
padding: .5rem 2rem;
|
||||
height: 2.25rem;
|
||||
max-height: 2.25rem;
|
||||
color: var(--theme-caption-color);
|
||||
background-color: var(--theme-button-bg-enabled);
|
||||
border-top: 1px solid var(--theme-button-border-enabled);
|
||||
border-bottom: 1px solid var(--theme-button-border-enabled);
|
||||
}
|
||||
|
||||
.selected {
|
||||
cursor: auto;
|
||||
|
||||
.label {
|
||||
background-color: var(--dark-turquoise-01);
|
||||
border-color: transparent;
|
||||
}
|
||||
}
|
||||
.left { mask-image: linear-gradient(to right, rgba(0, 0, 0, 0) 0, rgba(0, 0, 0, 1) 1rem); }
|
||||
.right { mask-image: linear-gradient(to left, rgba(0, 0, 0, 0) 0, rgba(0, 0, 0, 1) 1rem); }
|
||||
.both { mask-image: linear-gradient(to right, rgba(0, 0, 0, 0) 0, rgba(0, 0, 0, 1) 1rem, rgba(0, 0, 0, 1) calc(100% - 1rem), rgba(0, 0, 0, 0) 100%); }
|
||||
.none { mask-image: linear-gradient(to right, rgba(0, 0, 0, 0) 1); }
|
||||
</style>
|
||||
@@ -1,52 +0,0 @@
|
||||
<!--
|
||||
// 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">
|
||||
export let side: 'left' | 'right'
|
||||
export let kind: 'round' | 'arrow'
|
||||
export let selected: boolean = false
|
||||
</script>
|
||||
|
||||
<svg
|
||||
width=".5rem" height="2.25rem" viewBox="0 0 8 36"
|
||||
class:selected
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
{#if side === 'left' && kind === 'arrow'}
|
||||
<path class="bg" d="M1,0C0.3,0-0.2,0.7,0.1,1.4l6,15.9c0.2,0.5,0.2,1,0,1.4l-6,15.9C-0.2,35.3,0.3,36,1,36h7V0H1z"/>
|
||||
<path class="border" d="M1,35l6-15.9c0.3-0.7,0.3-1.4,0-2.1L1,1h7V0H1C0.7,0,0.4,0.2,0.2,0.4C0,0.7-0.1,1,0.1,1.4 l6,15.9c0.2,0.5,0.2,1,0,1.4l-6,15.9C-0.1,35,0,35.3,0.2,35.6S0.7,36,1,36h7v-1H1z"/>
|
||||
{:else if side === 'left' && kind === 'round'}
|
||||
<path class="bg" d="M0,8v10v10c0,4.4,3.6,8,8,8V0C3.6,0,0,3.6,0,8z"/>
|
||||
<path class="border" d="M1,28V8c0-3.9,3.1-7,7-7V0C3.6,0,0,3.6,0,8v20c0,4.4,3.6,8,8,8v-1C4.1,35,1,31.9,1,28z"/>
|
||||
{:else if side === 'right' && kind === 'arrow'}
|
||||
<path class="bg" d="M1.8,1.3C1.5,0.5,0.8,0,0,0v36c0.8,0,1.5-0.5,1.8-1.3l6.1-16c0.2-0.5,0.2-1,0-1.4L1.8,1.3z"/>
|
||||
<path class="border" d="M1.8,1.3L1.8,1.3L1.8,1.3c0,0-0.1-0.2-0.1-0.3L1.6,0.8C1.2,0.3,0.6,0,0,0v1 c0.4,0,0.7,0.3,0.9,0.6l6.1,16c0.1,0.2,0.1,0.5,0,0.7l-6.1,16C0.7,34.7,0.4,35,0,35v1c0.8,0,1.5-0.5,1.8-1.3l6.1-16 c0.2-0.5,0.2-1,0-1.4L1.8,1.3z"/>
|
||||
{:else if side === 'right' && kind === 'round'}
|
||||
<path class="bg" d="M0,0v36c4.4,0,8-3.6,8-8V8C8,3.6,4.4,0,0,0z"/>
|
||||
<path class="border" d="M0,0v1c3.9,0,7,3.1,7,7v20c0,3.9-3.1,7-7,7v1c4.4,0,8-3.6,8-8V8C8,3.6,4.4,0,0,0z"/>
|
||||
{:else}
|
||||
<rect x={0} y={0} width={'100%'} height={'100%'} fill={'red'} opacity={.5} />
|
||||
{/if}
|
||||
</svg>
|
||||
|
||||
<style lang="scss">
|
||||
.bg { fill: var(--theme-button-bg-enabled); }
|
||||
.border { fill: var(--theme-button-border-enabled); }
|
||||
.selected {
|
||||
.bg { fill: var(--dark-turquoise-01); }
|
||||
.border { fill: transparent; }
|
||||
}
|
||||
</style>
|
||||
@@ -62,7 +62,6 @@ export { default as TimeSince } from './components/TimeSince.svelte'
|
||||
export { default as Dropdown } from './components/Dropdown.svelte'
|
||||
export { default as DumbDropdown } from './components/DumbDropdown.svelte'
|
||||
export { default as ShowMore } from './components/ShowMore.svelte'
|
||||
export { default as StatusesBar } from './components/StatusesBar.svelte'
|
||||
|
||||
export { default as IconAdd } from './components/icons/Add.svelte'
|
||||
export { default as IconClose } from './components/icons/Close.svelte'
|
||||
|
||||
@@ -0,0 +1,209 @@
|
||||
<!--
|
||||
// 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 { Class, DocumentQuery, FindOptions, Ref } from '@anticrm/core'
|
||||
import { createQuery } from '@anticrm/presentation'
|
||||
import task, { DoneState, SpaceWithStates, State, Task } from '@anticrm/task'
|
||||
import { ScrollBox } from '@anticrm/ui'
|
||||
import Label from '@anticrm/ui/src/components/Label.svelte'
|
||||
import { Table } from '@anticrm/view-resources'
|
||||
import Lost from './icons/Lost.svelte'
|
||||
import Won from './icons/Won.svelte'
|
||||
import StatesBar from './state/StatesBar.svelte'
|
||||
|
||||
export let _class: Ref<Class<Task>>
|
||||
export let space: Ref<SpaceWithStates>
|
||||
export let options: FindOptions<Task> | undefined
|
||||
export let config: string[]
|
||||
export let search: string
|
||||
|
||||
let doneStatusesView: boolean = false
|
||||
let state: Ref<State> | undefined = undefined
|
||||
let selectedDoneStates: Set<Ref<DoneState>> = new Set<Ref<DoneState>>()
|
||||
let resConfig = config
|
||||
let query = {}
|
||||
let doneStates: DoneState[] = []
|
||||
|
||||
function updateConfig (): void {
|
||||
if (state !== undefined) {
|
||||
resConfig = config.filter((p) => p !== '$lookup.state')
|
||||
return
|
||||
}
|
||||
if (selectedDoneStates.size === 1) {
|
||||
resConfig = config.filter((p) => p !== '$lookup.doneState')
|
||||
return
|
||||
}
|
||||
resConfig = config
|
||||
}
|
||||
|
||||
const doneStateQuery = createQuery()
|
||||
doneStateQuery.query(
|
||||
task.class.DoneState,
|
||||
{
|
||||
space
|
||||
},
|
||||
(res) => (doneStates = res)
|
||||
)
|
||||
|
||||
async function updateQuery (): Promise<void> {
|
||||
updateConfig()
|
||||
const result = {} as DocumentQuery<Task>
|
||||
if (search !== '') {
|
||||
result.$search = search
|
||||
}
|
||||
result.space = space
|
||||
if (state) {
|
||||
result.state = state
|
||||
}
|
||||
if (selectedDoneStates.size > 0) {
|
||||
result.doneState = {
|
||||
$in: Array.from(selectedDoneStates)
|
||||
}
|
||||
}
|
||||
query = result
|
||||
}
|
||||
|
||||
function doneStateClick (id: Ref<DoneState>): void {
|
||||
if (selectedDoneStates.has(id)) {
|
||||
selectedDoneStates.delete(id)
|
||||
} else {
|
||||
selectedDoneStates.add(id)
|
||||
}
|
||||
selectedDoneStates = selectedDoneStates
|
||||
updateQuery()
|
||||
}
|
||||
|
||||
$: updateQuery()
|
||||
</script>
|
||||
|
||||
<div class="container">
|
||||
<div class="flex-between mb-4 mt-4 header">
|
||||
<div class="flex-row-center">
|
||||
<div
|
||||
class="button flex-center"
|
||||
class:active={!doneStatusesView}
|
||||
on:click={() => {
|
||||
doneStatusesView = false
|
||||
state = undefined
|
||||
selectedDoneStates.clear()
|
||||
updateQuery()
|
||||
}}
|
||||
>
|
||||
<Label label={'All statuses'} />
|
||||
</div>
|
||||
<div
|
||||
class="button flex-center ml-4"
|
||||
class:active={doneStatusesView}
|
||||
on:click={() => {
|
||||
doneStatusesView = true
|
||||
state = undefined
|
||||
selectedDoneStates.clear()
|
||||
updateQuery()
|
||||
}}
|
||||
>
|
||||
<Label label={'Done statuses'} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-row-center caption-color states">
|
||||
{#if doneStatusesView}
|
||||
{#each doneStates as state}
|
||||
<div
|
||||
class="doneState flex-row-center"
|
||||
class:won={state._class === task.class.WonState}
|
||||
class:lost={state._class === task.class.LostState}
|
||||
class:disable={!selectedDoneStates.has(state._id)}
|
||||
on:click={() => {
|
||||
doneStateClick(state._id)
|
||||
}}
|
||||
>
|
||||
{#if state._class === task.class.WonState}
|
||||
<Won size="medium" />
|
||||
{:else}
|
||||
<Lost size="medium" />
|
||||
{/if}
|
||||
<span class="ml-2">
|
||||
{state.title}
|
||||
</span>
|
||||
</div>
|
||||
{/each}
|
||||
{:else}
|
||||
<StatesBar bind:state {space} on:change={updateQuery} />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div class="container">
|
||||
<ScrollBox vertical stretch noShift>
|
||||
<Table {_class} {query} config={resConfig} {options} enableChecking />
|
||||
</ScrollBox>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.container {
|
||||
flex-grow: 1;
|
||||
position: relative;
|
||||
padding-bottom: 2.5rem;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.header {
|
||||
margin-left: 2.5rem;
|
||||
margin-right: 1.75rem;
|
||||
|
||||
.states {
|
||||
max-width: 75%;
|
||||
}
|
||||
}
|
||||
|
||||
.button {
|
||||
cursor: pointer;
|
||||
height: 2.5rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
border-radius: 0.5rem;
|
||||
&:hover {
|
||||
background-color: var(--theme-button-bg-enabled);
|
||||
border: 1px solid var(--theme-button-border-enabled);
|
||||
}
|
||||
&.active {
|
||||
background-color: var(--theme-button-bg-enabled);
|
||||
color: var(--theme-caption-color);
|
||||
border: 1px solid var(--theme-button-border-enabled);
|
||||
}
|
||||
}
|
||||
.doneState {
|
||||
cursor: pointer;
|
||||
height: 2.5rem;
|
||||
border-radius: 0.5rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
border: 1px solid var(--theme-button-border-enabled);
|
||||
|
||||
&.won {
|
||||
background-color: #60b96e;
|
||||
}
|
||||
|
||||
&.lost {
|
||||
background-color: #f06c63;
|
||||
}
|
||||
|
||||
&.disable {
|
||||
background-color: var(--theme-button-bg-enabled);
|
||||
}
|
||||
}
|
||||
|
||||
.doneState + .doneState {
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,24 @@
|
||||
<!--
|
||||
// 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">
|
||||
export let size: 'small' | 'medium' | 'large'
|
||||
const fill: string = 'currentColor'
|
||||
</script>
|
||||
|
||||
<svg class="svg-{size}" {fill} fill-opacity="0.0" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="8" cy="8" r="6" stroke="white" />
|
||||
<path d="M12 12L4 4" stroke="white" />
|
||||
</svg>
|
||||
@@ -0,0 +1,27 @@
|
||||
<!--
|
||||
// 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">
|
||||
export let size: 'small' | 'medium' | 'large'
|
||||
const fill: string = 'currentColor'
|
||||
</script>
|
||||
|
||||
<svg class="svg-{size}" {fill} fill-opacity="0.0" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M4 10V3.83337C4 3.59767 4 3.47982 4.07322 3.4066C4.14645 3.33337 4.2643 3.33337 4.5 3.33337H10.7929C11.3066 3.33337 11.5635 3.33337 11.6274 3.4877C11.6913 3.64203 11.5097 3.82366 11.1464 4.18693L8.94951 6.38386C8.81618 6.5172 8.74951 6.58386 8.74951 6.66671C8.74951 6.74955 8.81618 6.81622 8.94951 6.94955L11.1464 9.14649C11.5097 9.50975 11.6913 9.69138 11.6274 9.84571C11.5635 10 11.3066 10 10.7929 10H4ZM4 10V12.6667"
|
||||
stroke="white"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
</svg>
|
||||
@@ -0,0 +1,155 @@
|
||||
<!--
|
||||
// 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 { Ref, SortingOrder } from '@anticrm/core'
|
||||
import { createQuery } from '@anticrm/presentation'
|
||||
import task, { SpaceWithStates, State } from '@anticrm/task'
|
||||
import { createEventDispatcher, onDestroy, onMount } from 'svelte'
|
||||
import StatesBarElement from './StatesBarElement.svelte'
|
||||
|
||||
export let space: Ref<SpaceWithStates>
|
||||
export let state: Ref<State> | undefined = undefined
|
||||
let states: State[] = []
|
||||
|
||||
let div: HTMLElement
|
||||
let maskLeft: boolean = false
|
||||
let maskRight: boolean = false
|
||||
let mask: 'left' | 'right' | 'both' | 'none' = 'none'
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
const statesQuery = createQuery()
|
||||
statesQuery.query(
|
||||
task.class.State,
|
||||
{ space },
|
||||
(res) => {
|
||||
states = res
|
||||
},
|
||||
{
|
||||
sort: {
|
||||
rank: SortingOrder.Ascending
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const checkMask = (): void => {
|
||||
maskLeft = !!(div && div.scrollLeft > 1)
|
||||
maskRight = !!(div && div.scrollWidth - div.scrollLeft - div.clientWidth > 1)
|
||||
if (maskLeft || maskRight) {
|
||||
if (maskLeft && maskRight) mask = 'both'
|
||||
else if (maskLeft) mask = 'left'
|
||||
else if (maskRight) mask = 'right'
|
||||
} else mask = 'none'
|
||||
}
|
||||
|
||||
const selectItem = (ev: Event, item: State): void => {
|
||||
const el: HTMLElement = ev.currentTarget as HTMLElement
|
||||
el.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'center' })
|
||||
if (state === item._id) {
|
||||
state = undefined
|
||||
} else {
|
||||
state = item._id
|
||||
}
|
||||
dispatch('change')
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
if (div) {
|
||||
checkMask()
|
||||
div.addEventListener('scroll', checkMask)
|
||||
}
|
||||
})
|
||||
onDestroy(() => {
|
||||
if (div) div.removeEventListener('scroll', checkMask)
|
||||
})
|
||||
</script>
|
||||
|
||||
<div bind:this={div} class="flex-row-center statusesbar-container {mask}">
|
||||
{#each states as item, i}
|
||||
<div
|
||||
class="flex-row-center cursor-pointer step-lr25"
|
||||
class:selected={item._id === state}
|
||||
on:click={(ev) => {
|
||||
selectItem(ev, item)
|
||||
}}
|
||||
>
|
||||
<StatesBarElement side={'left'} kind={i ? 'arrow' : 'round'} selected={item._id === state} color={item.color} />
|
||||
<div
|
||||
class="flex-row-center overflow-label label"
|
||||
style={item._id === state ? `background-color: ${item.color};` : ''}
|
||||
>
|
||||
{item.title}
|
||||
</div>
|
||||
<StatesBarElement
|
||||
side={'right'}
|
||||
kind={i < states.length - 1 ? 'arrow' : 'round'}
|
||||
selected={item._id === state}
|
||||
color={item.color}
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.statusesbar-container {
|
||||
overflow-x: auto;
|
||||
padding: 0.125rem 0;
|
||||
|
||||
&::-webkit-scrollbar:horizontal {
|
||||
height: 0.125rem;
|
||||
}
|
||||
&::-webkit-scrollbar-track {
|
||||
margin: 0.25rem;
|
||||
}
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: var(--theme-bg-accent-color);
|
||||
}
|
||||
}
|
||||
|
||||
.label {
|
||||
padding: 0.5rem 2rem;
|
||||
height: 2.25rem;
|
||||
max-height: 2.25rem;
|
||||
color: var(--theme-caption-color);
|
||||
background-color: var(--theme-button-bg-enabled);
|
||||
border-top: 1px solid var(--theme-button-border-enabled);
|
||||
border-bottom: 1px solid var(--theme-button-border-enabled);
|
||||
}
|
||||
|
||||
.selected {
|
||||
.label {
|
||||
border-color: transparent;
|
||||
}
|
||||
}
|
||||
.left {
|
||||
mask-image: linear-gradient(to right, rgba(0, 0, 0, 0) 0, rgba(0, 0, 0, 1) 1rem);
|
||||
}
|
||||
.right {
|
||||
mask-image: linear-gradient(to left, rgba(0, 0, 0, 0) 0, rgba(0, 0, 0, 1) 1rem);
|
||||
}
|
||||
.both {
|
||||
mask-image: linear-gradient(
|
||||
to right,
|
||||
rgba(0, 0, 0, 0) 0,
|
||||
rgba(0, 0, 0, 1) 1rem,
|
||||
rgba(0, 0, 0, 1) calc(100% - 1rem),
|
||||
rgba(0, 0, 0, 0) 100%
|
||||
);
|
||||
}
|
||||
.none {
|
||||
mask-image: linear-gradient(to right, rgba(0, 0, 0, 0) 1);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,67 @@
|
||||
<!--
|
||||
// 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">
|
||||
export let side: 'left' | 'right'
|
||||
export let kind: 'round' | 'arrow'
|
||||
export let selected: boolean = false
|
||||
export let color: string
|
||||
</script>
|
||||
|
||||
<svg width=".5rem" height="2.25rem" viewBox="0 0 8 36" class:selected xmlns="http://www.w3.org/2000/svg">
|
||||
{#if side === 'left' && kind === 'arrow'}
|
||||
<path
|
||||
class="bg"
|
||||
style={selected ? `fill: ${color}` : ''}
|
||||
d="M1,0C0.3,0-0.2,0.7,0.1,1.4l6,15.9c0.2,0.5,0.2,1,0,1.4l-6,15.9C-0.2,35.3,0.3,36,1,36h7V0H1z"
|
||||
/>
|
||||
<path
|
||||
class="border"
|
||||
d="M1,35l6-15.9c0.3-0.7,0.3-1.4,0-2.1L1,1h7V0H1C0.7,0,0.4,0.2,0.2,0.4C0,0.7-0.1,1,0.1,1.4 l6,15.9c0.2,0.5,0.2,1,0,1.4l-6,15.9C-0.1,35,0,35.3,0.2,35.6S0.7,36,1,36h7v-1H1z"
|
||||
/>
|
||||
{:else if side === 'left' && kind === 'round'}
|
||||
<path class="bg" style={selected ? `fill: ${color}` : ''} d="M0,8v10v10c0,4.4,3.6,8,8,8V0C3.6,0,0,3.6,0,8z" />
|
||||
<path class="border" d="M1,28V8c0-3.9,3.1-7,7-7V0C3.6,0,0,3.6,0,8v20c0,4.4,3.6,8,8,8v-1C4.1,35,1,31.9,1,28z" />
|
||||
{:else if side === 'right' && kind === 'arrow'}
|
||||
<path
|
||||
class="bg"
|
||||
style={selected ? `fill: ${color}` : ''}
|
||||
d="M1.8,1.3C1.5,0.5,0.8,0,0,0v36c0.8,0,1.5-0.5,1.8-1.3l6.1-16c0.2-0.5,0.2-1,0-1.4L1.8,1.3z"
|
||||
/>
|
||||
<path
|
||||
class="border"
|
||||
d="M1.8,1.3L1.8,1.3L1.8,1.3c0,0-0.1-0.2-0.1-0.3L1.6,0.8C1.2,0.3,0.6,0,0,0v1 c0.4,0,0.7,0.3,0.9,0.6l6.1,16c0.1,0.2,0.1,0.5,0,0.7l-6.1,16C0.7,34.7,0.4,35,0,35v1c0.8,0,1.5-0.5,1.8-1.3l6.1-16 c0.2-0.5,0.2-1,0-1.4L1.8,1.3z"
|
||||
/>
|
||||
{:else if side === 'right' && kind === 'round'}
|
||||
<path class="bg" style={selected ? `fill: ${color}` : ''} d="M0,0v36c4.4,0,8-3.6,8-8V8C8,3.6,4.4,0,0,0z" />
|
||||
<path class="border" d="M0,0v1c3.9,0,7,3.1,7,7v20c0,3.9-3.1,7-7,7v1c4.4,0,8-3.6,8-8V8C8,3.6,4.4,0,0,0z" />
|
||||
{:else}
|
||||
<rect x={0} y={0} width={'100%'} height={'100%'} fill={'red'} opacity={0.5} />
|
||||
{/if}
|
||||
</svg>
|
||||
|
||||
<style lang="scss">
|
||||
.bg {
|
||||
fill: var(--theme-button-bg-enabled);
|
||||
}
|
||||
.border {
|
||||
fill: var(--theme-button-border-enabled);
|
||||
}
|
||||
.selected {
|
||||
.border {
|
||||
fill: transparent;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -13,9 +13,8 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
|
||||
<script lang="ts">
|
||||
import { Ref } from '@anticrm/core'
|
||||
import { Ref, SortingOrder } from '@anticrm/core'
|
||||
import { createQuery } from '@anticrm/presentation'
|
||||
import task, { SpaceWithStates, State } from '@anticrm/task'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
@@ -24,14 +23,29 @@
|
||||
let states: State[] = []
|
||||
const dispatch = createEventDispatcher()
|
||||
const statesQuery = createQuery()
|
||||
statesQuery.query(task.class.State, { space }, (res) => { states = res })
|
||||
|
||||
statesQuery.query(
|
||||
task.class.State,
|
||||
{ space },
|
||||
(res) => {
|
||||
states = res
|
||||
},
|
||||
{
|
||||
sort: {
|
||||
rank: SortingOrder.Ascending
|
||||
}
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
<div class="flex-col statuses-container">
|
||||
{#each states as state}
|
||||
<div class="flex-row-center state" on:click={() => { dispatch('close', state) }}>
|
||||
<div class="color" style="background-color: {state.color}"></div>
|
||||
<div
|
||||
class="flex-row-center state"
|
||||
on:click={() => {
|
||||
dispatch('close', state)
|
||||
}}
|
||||
>
|
||||
<div class="color" style="background-color: {state.color}" />
|
||||
{state.title}
|
||||
</div>
|
||||
{/each}
|
||||
@@ -39,28 +53,30 @@
|
||||
|
||||
<style lang="scss">
|
||||
.statuses-container {
|
||||
padding: .5rem;
|
||||
padding: 0.5rem;
|
||||
max-height: 100%;
|
||||
min-width: 10rem;
|
||||
color: var(--theme-caption-color);
|
||||
background-color: var(--theme-button-bg-focused);
|
||||
border: 1px solid var(--theme-button-border-enabled);
|
||||
border-radius: .75rem;
|
||||
border-radius: 0.75rem;
|
||||
user-select: none;
|
||||
filter: drop-shadow(0 1.5rem 4rem rgba(0, 0, 0, .35));
|
||||
filter: drop-shadow(0 1.5rem 4rem rgba(0, 0, 0, 0.35));
|
||||
|
||||
.state {
|
||||
padding: .5rem;
|
||||
border-radius: .5rem;
|
||||
padding: 0.5rem;
|
||||
border-radius: 0.5rem;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover { background-color: var(--theme-button-bg-hovered); }
|
||||
&:hover {
|
||||
background-color: var(--theme-button-bg-hovered);
|
||||
}
|
||||
.color {
|
||||
margin-right: .75rem;
|
||||
margin-right: 0.75rem;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
border-radius: .25rem;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -35,6 +35,7 @@ import { SpaceWithStates, TodoItem } from '@anticrm/task'
|
||||
import Todos from './components/todos/Todos.svelte'
|
||||
import TodoItemPresenter from './components/todos/TodoItemPresenter.svelte'
|
||||
import TodoStatePresenter from './components/todos/TodoStatePresenter.svelte'
|
||||
import StatusTableView from './components/StatusTableView.svelte'
|
||||
|
||||
export { default as KanbanTemplateEditor } from './components/kanban/KanbanTemplateEditor.svelte'
|
||||
export { default as KanbanTemplateSelector } from './components/kanban/KanbanTemplateSelector.svelte'
|
||||
@@ -57,7 +58,8 @@ async function toggleDone (value: boolean, object: TodoItem): Promise<void> {
|
||||
object._id,
|
||||
object.attachedTo,
|
||||
object.attachedToClass,
|
||||
object.collection, {
|
||||
object.collection,
|
||||
{
|
||||
done: value
|
||||
}
|
||||
)
|
||||
@@ -119,7 +121,8 @@ export default async (): Promise<Resources> => ({
|
||||
DoneStatePresenter,
|
||||
Todos,
|
||||
TodoItemPresenter,
|
||||
TodoStatePresenter
|
||||
TodoStatePresenter,
|
||||
StatusTableView
|
||||
},
|
||||
actionImpl: {
|
||||
CreateTask: createTask,
|
||||
|
||||
+40
-23
@@ -14,7 +14,19 @@
|
||||
//
|
||||
|
||||
import type { Employee } from '@anticrm/contact'
|
||||
import { AttachedDoc, Class, Client, Data, Doc, Interface, Mixin, Ref, Space, Timestamp, TxOperations } from '@anticrm/core'
|
||||
import {
|
||||
AttachedDoc,
|
||||
Class,
|
||||
Client,
|
||||
Data,
|
||||
Doc,
|
||||
Interface,
|
||||
Mixin,
|
||||
Ref,
|
||||
Space,
|
||||
Timestamp,
|
||||
TxOperations
|
||||
} from '@anticrm/core'
|
||||
import type { Asset, Plugin } from '@anticrm/platform'
|
||||
import { plugin } from '@anticrm/platform'
|
||||
import type { AnyComponent } from '@anticrm/ui'
|
||||
@@ -80,8 +92,7 @@ export interface TodoItem extends AttachedDoc {
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface SpaceWithStates extends Space {
|
||||
}
|
||||
export interface SpaceWithStates extends Space {}
|
||||
|
||||
/**
|
||||
* @public
|
||||
@@ -130,23 +141,23 @@ export interface Sequence extends Doc {
|
||||
export interface StateTemplate extends AttachedDoc, State {}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
* @public
|
||||
*/
|
||||
export interface DoneStateTemplate extends AttachedDoc, DoneState {}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
* @public
|
||||
*/
|
||||
export interface WonStateTemplate extends DoneStateTemplate, WonState {}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
* @public
|
||||
*/
|
||||
export interface LostStateTemplate extends DoneStateTemplate, LostState {}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
* @public
|
||||
*/
|
||||
export interface KanbanTemplate extends Doc {
|
||||
title: string
|
||||
statesC: number
|
||||
@@ -154,8 +165,8 @@ export interface KanbanTemplate extends Doc {
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
* @public
|
||||
*/
|
||||
export interface KanbanTemplateSpace extends Space {
|
||||
icon: AnyComponent
|
||||
}
|
||||
@@ -195,7 +206,8 @@ const task = plugin(taskId, {
|
||||
TodoItem: '' as Ref<Class<TodoItem>>
|
||||
},
|
||||
viewlet: {
|
||||
Kanban: '' as Ref<ViewletDescriptor>
|
||||
Kanban: '' as Ref<ViewletDescriptor>,
|
||||
StatusTable: '' as Ref<ViewletDescriptor>
|
||||
},
|
||||
icon: {
|
||||
Task: '' as Asset,
|
||||
@@ -288,7 +300,11 @@ export async function createProjectKanban (
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export async function createKanban (client: Client & TxOperations, attachedTo: Ref<Space>, templateId?: Ref<KanbanTemplate>): Promise<Ref<Kanban>> {
|
||||
export async function createKanban (
|
||||
client: Client & TxOperations,
|
||||
attachedTo: Ref<Space>,
|
||||
templateId?: Ref<KanbanTemplate>
|
||||
): Promise<Ref<Kanban>> {
|
||||
if (templateId === undefined) {
|
||||
const ranks = [...genRanks(2)]
|
||||
await Promise.all([
|
||||
@@ -314,14 +330,15 @@ export async function createKanban (client: Client & TxOperations, attachedTo: R
|
||||
|
||||
const tmplStates = await client.findAll(task.class.StateTemplate, { attachedTo: template._id })
|
||||
await Promise.all(
|
||||
tmplStates.map(async (state) => await client.createDoc(
|
||||
task.class.State,
|
||||
attachedTo,
|
||||
{
|
||||
color: state.color,
|
||||
title: state.title,
|
||||
rank: state.rank
|
||||
})))
|
||||
tmplStates.map(
|
||||
async (state) =>
|
||||
await client.createDoc(task.class.State, attachedTo, {
|
||||
color: state.color,
|
||||
title: state.title,
|
||||
rank: state.rank
|
||||
})
|
||||
)
|
||||
)
|
||||
|
||||
const doneClassMap = new Map<Ref<Class<DoneStateTemplate>>, Ref<Class<DoneState>>>([
|
||||
[task.class.WonStateTemplate, task.class.WonState],
|
||||
|
||||
@@ -13,12 +13,11 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
|
||||
<script lang="ts">
|
||||
import type { Class, Doc, DocumentQuery, FindOptions, Ref } from '@anticrm/core'
|
||||
import { SortingOrder } from '@anticrm/core'
|
||||
import { createQuery, getClient } from '@anticrm/presentation'
|
||||
import { IconDown, IconUp, Label, Loading, showPopup } from '@anticrm/ui'
|
||||
import { CheckBox, IconDown, IconUp, Label, Loading, showPopup } from '@anticrm/ui'
|
||||
import { BuildModelKey } from '@anticrm/view'
|
||||
import { buildModel } from '../utils'
|
||||
import MoreV from './icons/MoreV.svelte'
|
||||
@@ -26,6 +25,7 @@
|
||||
|
||||
export let _class: Ref<Class<Doc>>
|
||||
export let query: DocumentQuery<Doc>
|
||||
export let enableChecking: boolean = false
|
||||
export let options: FindOptions<Doc> | undefined = undefined
|
||||
export let baseMenuClass: Ref<Class<Doc>> | undefined = undefined
|
||||
export let config: (BuildModelKey|string)[]
|
||||
@@ -37,7 +37,14 @@
|
||||
let objects: Doc[]
|
||||
|
||||
const q = createQuery()
|
||||
$: q.query(_class, query, result => { objects = result }, { sort: { [sortKey]: sortOrder }, ...options })
|
||||
$: q.query(
|
||||
_class,
|
||||
query,
|
||||
(result) => {
|
||||
objects = result
|
||||
},
|
||||
{ sort: { [sortKey]: sortOrder }, ...options }
|
||||
)
|
||||
|
||||
function getValue (doc: Doc, key: string): any {
|
||||
if (key.length === 0) {
|
||||
@@ -56,7 +63,9 @@
|
||||
|
||||
const showMenu = async (ev: MouseEvent, object: Doc, row: number): Promise<void> => {
|
||||
selectRow = row
|
||||
showPopup(Menu, { object, baseMenuClass }, ev.target as HTMLElement, () => { selectRow = undefined })
|
||||
showPopup(Menu, { object }, ev.target as HTMLElement, () => {
|
||||
selectRow = undefined
|
||||
})
|
||||
}
|
||||
|
||||
function changeSorting (key: string): void {
|
||||
@@ -67,20 +76,52 @@
|
||||
sortKey = key
|
||||
sortOrder = SortingOrder.Ascending
|
||||
} else {
|
||||
sortOrder = (sortOrder === SortingOrder.Ascending) ? SortingOrder.Descending : SortingOrder.Ascending
|
||||
sortOrder = sortOrder === SortingOrder.Ascending ? SortingOrder.Descending : SortingOrder.Ascending
|
||||
}
|
||||
}
|
||||
|
||||
let checked: Set<Ref<Doc>> = new Set<Ref<Doc>>()
|
||||
|
||||
function check (id: Ref<Doc>, e: Event) {
|
||||
if (!enableChecking) return
|
||||
const target = e.target as HTMLInputElement
|
||||
const value = target.checked
|
||||
if (value) {
|
||||
checked.add(id)
|
||||
} else {
|
||||
checked.delete(id)
|
||||
}
|
||||
checked = checked
|
||||
}
|
||||
</script>
|
||||
|
||||
{#await buildModel({ client, _class, keys: config, options })}
|
||||
<Loading/>
|
||||
<Loading />
|
||||
{:then model}
|
||||
<table class="table-body">
|
||||
<table class="table-body" class:enableChecking>
|
||||
<thead>
|
||||
<tr class="tr-head">
|
||||
{#each model as attribute}
|
||||
<th class:sortable={attribute.sortingKey} class:sorted={attribute.sortingKey === sortKey} on:click={() => changeSorting(attribute.sortingKey)}>
|
||||
{#each model as attribute, cellHead}
|
||||
{#if enableChecking && !cellHead}
|
||||
<th>
|
||||
<div class="checkCell" class:checkall={checked.size > 0}>
|
||||
<CheckBox
|
||||
symbol={'minus'}
|
||||
checked={objects?.length === checked.size}
|
||||
on:change={(e) => {
|
||||
objects.map((o) => check(o._id, e))
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</th>
|
||||
{/if}
|
||||
<th
|
||||
class:sortable={attribute.sortingKey}
|
||||
class:sorted={attribute.sortingKey === sortKey}
|
||||
on:click={() => changeSorting(attribute.sortingKey)}
|
||||
>
|
||||
<div class="flex-row-center whitespace-nowrap">
|
||||
<Label label = {attribute.label}/>
|
||||
<Label label={attribute.label} />
|
||||
{#if attribute.sortingKey === sortKey}
|
||||
<div class="icon">
|
||||
{#if sortOrder === SortingOrder.Ascending}
|
||||
@@ -98,15 +139,39 @@
|
||||
{#if objects}
|
||||
<tbody>
|
||||
{#each objects as object, row (object._id)}
|
||||
<tr class="tr-body" class:fixed={row === selectRow}>
|
||||
<tr class="tr-body" class:checking={checked.has(object._id)} class:fixed={row === selectRow}>
|
||||
{#each model as attribute, cell}
|
||||
{#if !cell}
|
||||
<td><div class="firstCell">
|
||||
<svelte:component this={attribute.presenter} value={getValue(object, attribute.key)} {...attribute.props}/>
|
||||
<div class="menuRow" on:click={(ev) => showMenu(ev, object, row)}><MoreV size={'small'} /></div>
|
||||
</div></td>
|
||||
{#if enableChecking}
|
||||
<td>
|
||||
<div class="checkCell">
|
||||
<CheckBox
|
||||
checked={checked.has(object._id)}
|
||||
on:change={(e) => {
|
||||
check(object._id, e)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</td>
|
||||
{/if}
|
||||
<td
|
||||
><div class="firstCell">
|
||||
<svelte:component
|
||||
this={attribute.presenter}
|
||||
value={getValue(object, attribute.key)}
|
||||
{...attribute.props}
|
||||
/>
|
||||
<div class="menuRow" on:click={(ev) => showMenu(ev, object, row)}><MoreV size={'small'} /></div>
|
||||
</div></td
|
||||
>
|
||||
{:else}
|
||||
<td><svelte:component this={attribute.presenter} value={getValue(object, attribute.key)} {...attribute.props}/></td>
|
||||
<td
|
||||
><svelte:component
|
||||
this={attribute.presenter}
|
||||
value={getValue(object, attribute.key)}
|
||||
{...attribute.props}
|
||||
/></td
|
||||
>
|
||||
{/if}
|
||||
{/each}
|
||||
</tr>
|
||||
@@ -117,7 +182,9 @@
|
||||
{/await}
|
||||
|
||||
<style lang="scss">
|
||||
.table-body { width: 100%; }
|
||||
.table-body {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.firstCell {
|
||||
display: flex;
|
||||
@@ -125,32 +192,72 @@
|
||||
align-items: center;
|
||||
.menuRow {
|
||||
visibility: hidden;
|
||||
margin-left: .5rem;
|
||||
opacity: .6;
|
||||
margin-left: 0.5rem;
|
||||
opacity: 0.6;
|
||||
cursor: pointer;
|
||||
&:hover { opacity: 1; }
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
.checkCell {
|
||||
visibility: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
padding: 0.5rem 1.5rem;
|
||||
text-align: left;
|
||||
&:first-child {
|
||||
padding-left: 0;
|
||||
}
|
||||
&:last-child {
|
||||
padding-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
th, td {
|
||||
padding: .5rem 1.5rem;
|
||||
text-align: left;
|
||||
&:first-child { padding-left: 0; }
|
||||
&:last-child { padding-right: 0; }
|
||||
.enableChecking {
|
||||
th,
|
||||
td {
|
||||
&:first-child {
|
||||
padding: 0 0.75rem;
|
||||
width: 2.5rem;
|
||||
}
|
||||
&:nth-child(2) {
|
||||
padding-left: 0;
|
||||
// padding-right: 0;
|
||||
}
|
||||
&:last-child {
|
||||
padding-right: 1.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
th {
|
||||
height: 2.5rem;
|
||||
font-weight: 500;
|
||||
font-size: .75rem;
|
||||
font-size: 0.75rem;
|
||||
color: var(--theme-content-dark-color);
|
||||
box-shadow: inset 0 -1px 0 0 var(--theme-bg-focused-color);
|
||||
user-select: none;
|
||||
z-index: 5;
|
||||
|
||||
&.sortable { cursor: pointer; }
|
||||
&.sortable {
|
||||
cursor: pointer;
|
||||
}
|
||||
&.sorted .icon {
|
||||
margin-left: .25rem;
|
||||
opacity: .6;
|
||||
margin-left: 0.25rem;
|
||||
opacity: 0.6;
|
||||
}
|
||||
&:hover {
|
||||
.checkCell {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
.checkall {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,8 +265,26 @@
|
||||
height: 3.25rem;
|
||||
color: var(--theme-caption-color);
|
||||
border-bottom: 1px solid var(--theme-button-border-hovered);
|
||||
&:hover .firstCell .menuRow { visibility: visible; }
|
||||
&:last-child { border-bottom: none; }
|
||||
&:hover .firstCell .menuRow {
|
||||
visibility: visible;
|
||||
}
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
&:hover,
|
||||
&.checking {
|
||||
background-color: var(--theme-table-bg-hover);
|
||||
.checkCell {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
}
|
||||
.fixed {
|
||||
.checkCell {
|
||||
visibility: visible;
|
||||
}
|
||||
.menuRow {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
.fixed .menuRow { visibility: visible; }
|
||||
</style>
|
||||
|
||||
@@ -13,15 +13,10 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
|
||||
<script lang="ts">
|
||||
import type { Class, Doc, DocumentQuery, FindOptions, Ref, Space } from '@anticrm/core'
|
||||
import { SortingOrder } from '@anticrm/core'
|
||||
import { createQuery, getClient } from '@anticrm/presentation'
|
||||
import { CheckBox, IconDown, IconUp, Label, Loading, ScrollBox, showPopup } from '@anticrm/ui'
|
||||
import { buildModel } from '../utils'
|
||||
import MoreV from './icons/MoreV.svelte'
|
||||
import Menu from './Menu.svelte'
|
||||
import { ScrollBox } from '@anticrm/ui'
|
||||
import Table from './Table.svelte'
|
||||
|
||||
export let _class: Ref<Class<Doc>>
|
||||
export let space: Ref<Space> | undefined = undefined
|
||||
@@ -31,160 +26,14 @@
|
||||
export let config: string[]
|
||||
export let search: string = ''
|
||||
|
||||
let sortKey = 'modifiedOn'
|
||||
let sortOrder = SortingOrder.Descending
|
||||
let selectRow: number | undefined = undefined
|
||||
|
||||
let objects: Doc[]
|
||||
|
||||
const q = createQuery()
|
||||
$: q.query(
|
||||
_class,
|
||||
{
|
||||
...query,
|
||||
...search !== '' ? { $search: search } : {},
|
||||
...search === '' && space !== undefined ? { space } : {}
|
||||
},
|
||||
(result) => {
|
||||
objects = result
|
||||
},
|
||||
{ sort: { [sortKey]: sortOrder }, ...options }
|
||||
)
|
||||
|
||||
function getValue (doc: Doc, key: string): any {
|
||||
if (key.length === 0) {
|
||||
return doc
|
||||
}
|
||||
const path = key.split('.')
|
||||
const len = path.length
|
||||
let obj = doc as any
|
||||
for (let i = 0; i < len; i++) {
|
||||
obj = obj?.[path[i]]
|
||||
}
|
||||
return obj ?? ''
|
||||
}
|
||||
|
||||
const client = getClient()
|
||||
let checked: Set<Ref<Doc>> = new Set<Ref<Doc>>()
|
||||
|
||||
const showMenu = (ev: MouseEvent, object: Doc, row: number): void => {
|
||||
selectRow = row
|
||||
showPopup(Menu, { object, baseMenuClass }, ev.target as HTMLElement, () => {
|
||||
selectRow = undefined
|
||||
})
|
||||
}
|
||||
|
||||
function changeSorting (key: string): void {
|
||||
if (key === '') {
|
||||
return
|
||||
}
|
||||
if (key !== sortKey) {
|
||||
sortKey = key
|
||||
sortOrder = SortingOrder.Ascending
|
||||
} else {
|
||||
sortOrder = sortOrder === SortingOrder.Ascending ? SortingOrder.Descending : SortingOrder.Ascending
|
||||
}
|
||||
}
|
||||
|
||||
function check (id: Ref<Doc>, e: Event) {
|
||||
const target = e.target as HTMLInputElement
|
||||
const value = target.checked
|
||||
if (value) {
|
||||
checked.add(id)
|
||||
} else {
|
||||
checked.delete(id)
|
||||
}
|
||||
checked = checked
|
||||
}
|
||||
$: query = search === '' ? { space } : { $search: search, space }
|
||||
</script>
|
||||
|
||||
{#await buildModel({ client, _class, keys: config, options })}
|
||||
<Loading />
|
||||
{:then model}
|
||||
<div class="container">
|
||||
<ScrollBox vertical stretch noShift>
|
||||
<table class="table-body">
|
||||
<thead>
|
||||
<tr class="tr-head">
|
||||
{#each model as attribute, cellHead}
|
||||
{#if !cellHead}
|
||||
<th>
|
||||
<div class="checkCell" class:checkall={checked.size > 0}>
|
||||
<CheckBox
|
||||
symbol={'minus'}
|
||||
checked={objects?.length === checked.size}
|
||||
on:change={(e) => {
|
||||
objects.map((o) => check(o._id, e))
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</th>
|
||||
{/if}
|
||||
<th
|
||||
class:sortable={attribute.sortingKey}
|
||||
class:sorted={attribute.sortingKey === sortKey}
|
||||
on:click={() => changeSorting(attribute.sortingKey)}
|
||||
>
|
||||
<div class="flex-row-center whitespace-nowrap">
|
||||
<Label label={attribute.label} />
|
||||
{#if attribute.sortingKey === sortKey}
|
||||
<div class="icon">
|
||||
{#if sortOrder === SortingOrder.Ascending}
|
||||
<IconUp size={'small'} />
|
||||
{:else}
|
||||
<IconDown size={'small'} />
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</th>
|
||||
{/each}
|
||||
</tr>
|
||||
</thead>
|
||||
{#if objects}
|
||||
<tbody>
|
||||
{#each objects as object, row (object._id)}
|
||||
<tr class="tr-body" class:checking={checked.has(object._id)} class:fixed={row === selectRow}>
|
||||
{#each model as attribute, cell}
|
||||
{#if !cell}
|
||||
<td
|
||||
><div class="checkCell">
|
||||
<CheckBox
|
||||
checked={checked.has(object._id)}
|
||||
on:change={(e) => {
|
||||
check(object._id, e)
|
||||
}}
|
||||
/>
|
||||
</div></td
|
||||
>
|
||||
<td
|
||||
><div class="firstCell">
|
||||
<svelte:component
|
||||
this={attribute.presenter}
|
||||
value={getValue(object, attribute.key)}
|
||||
{...attribute.props}
|
||||
/>
|
||||
<div class="menuRow" on:click={(ev) => showMenu(ev, object, row)}><MoreV size={'small'} /></div>
|
||||
</div></td
|
||||
>
|
||||
{:else}
|
||||
<td
|
||||
><svelte:component
|
||||
this={attribute.presenter}
|
||||
value={getValue(object, attribute.key)}
|
||||
{...attribute.props}
|
||||
/></td
|
||||
>
|
||||
{/if}
|
||||
{/each}
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
{/if}
|
||||
</table>
|
||||
</ScrollBox>
|
||||
</div>
|
||||
{/await}
|
||||
<div class="container">
|
||||
<ScrollBox vertical stretch noShift>
|
||||
<Table {_class} {config} {options} {query} {baseMenuClass} enableChecking />
|
||||
</ScrollBox>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.container {
|
||||
@@ -193,100 +42,4 @@
|
||||
padding-bottom: 2.5rem;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.table-body {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.firstCell {
|
||||
display: flex;
|
||||
// justify-content: space-between;
|
||||
align-items: center;
|
||||
.menuRow {
|
||||
visibility: hidden;
|
||||
margin-left: 0.5rem;
|
||||
opacity: 0.6;
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
.checkCell {
|
||||
visibility: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
padding: 0.5rem 1.5rem;
|
||||
text-align: left;
|
||||
&:first-child {
|
||||
padding: 0 0.75rem;
|
||||
width: 2.5rem;
|
||||
}
|
||||
&:nth-child(2) {
|
||||
padding-left: 0;
|
||||
// padding-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
th {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
height: 2.5rem;
|
||||
font-weight: 500;
|
||||
font-size: 0.75rem;
|
||||
color: var(--theme-content-dark-color);
|
||||
background-color: var(--theme-bg-color);
|
||||
box-shadow: inset 0 -1px 0 0 var(--theme-bg-focused-color);
|
||||
user-select: none;
|
||||
z-index: 5;
|
||||
|
||||
&.sortable {
|
||||
cursor: pointer;
|
||||
}
|
||||
&.sorted .icon {
|
||||
margin-left: 0.25rem;
|
||||
opacity: 0.6;
|
||||
}
|
||||
&:hover {
|
||||
.checkCell {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
.checkall {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
|
||||
.tr-body {
|
||||
height: 3.25rem;
|
||||
color: var(--theme-caption-color);
|
||||
border-bottom: 1px solid var(--theme-button-border-hovered);
|
||||
&:hover,
|
||||
&.checking {
|
||||
background-color: var(--theme-table-bg-hover);
|
||||
.checkCell {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
&:hover .firstCell .menuRow {
|
||||
visibility: visible;
|
||||
}
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
.fixed {
|
||||
background-color: var(--theme-table-bg-hover);
|
||||
.checkCell {
|
||||
visibility: visible;
|
||||
}
|
||||
.menuRow {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -68,7 +68,8 @@ export class FullTextIndex extends TxProcessor implements Storage {
|
||||
options?: FindOptions<T>
|
||||
): Promise<FindResult<T>> {
|
||||
console.log('search', query)
|
||||
const docs = await this.adapter.search(query)
|
||||
const { _id, $search, ...mainQuery } = query
|
||||
const docs = await this.adapter.search($search)
|
||||
console.log(docs)
|
||||
const ids: Ref<Doc>[] = []
|
||||
for (const doc of docs) {
|
||||
@@ -77,7 +78,7 @@ export class FullTextIndex extends TxProcessor implements Storage {
|
||||
ids.push(doc.attachedTo)
|
||||
}
|
||||
}
|
||||
return await this.dbStorage.findAll(_class, { _id: { $in: ids as any } }, options) // TODO: remove `as any`
|
||||
return await this.dbStorage.findAll(_class, { _id: { $in: ids as any }, ...mainQuery }, options) // TODO: remove `as any`
|
||||
}
|
||||
|
||||
private getFullTextAttributes (clazz: Ref<Class<Obj>>): AnyAttribute[] | undefined {
|
||||
|
||||
Reference in New Issue
Block a user