Status filters in table view (#693)

Signed-off-by: Denis Bykhov <80476319+BykhovDenis@users.noreply.github.com>
This commit is contained in:
Denis Bykhov
2021-12-21 10:36:50 +01:00
committed by GitHub
parent f5aa8249dc
commit cb011b50af
18 changed files with 766 additions and 490 deletions
+26
View File
@@ -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
})
+12 -1
View File
@@ -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,
+2 -1
View File
@@ -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,
-2
View File
@@ -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>
-1
View File
@@ -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>
+5 -2
View File
@@ -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
View File
@@ -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>
+3 -2
View File
@@ -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 {