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
@@ -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>