WIP: fixes to tags

This commit is contained in:
Arman
2025-03-13 17:05:26 +01:00
parent a53fc1d664
commit d3756863f9
11 changed files with 320 additions and 124 deletions
@@ -7,7 +7,7 @@
export let navigateSubMenu: (menu: SheetMenu) => void;
</script>
{#if menu.title}
{#if menu?.title}
<span class="menu-title">{menu.title}</span>
{/if}
<ActionMenu.Root>
@@ -33,7 +33,9 @@
navigateSubMenu(menuItem.subMenu);
} else if (menuItem.onClick !== undefined) {
menuItem.onClick();
isOpen = false;
if (menuItem.closeOnClick !== false) {
isOpen = false;
}
}
}}>
{menuItem.name}
@@ -40,11 +40,14 @@
</script>
<BottomSheet.Default bind:isOpen useSlots={true} bind:sheetContainerRef bind:showDivider>
<div slot="top"><SheetMenuBlock menu={activeMenu.top} {navigateSubMenu} bind:isOpen /></div>
<div slot="top">
<SheetMenuBlock menu={activeMenu.top} {navigateSubMenu} bind:isOpen />
</div>
<div slot="bottom">
{#if activeMenu.bottom}<SheetMenuBlock
menu={activeMenu.bottom}
{navigateSubMenu}
bind:isOpen />{/if}
bind:isOpen />
{/if}
</div>
</BottomSheet.Default>
+1
View File
@@ -19,6 +19,7 @@ type MenuItem = {
trailingIcon?: ComponentType;
onClick?: () => void;
href?: string;
closeOnClick?: boolean;
subMenu?: { top: SubMenu; bottom: SubMenu };
};
+28 -25
View File
@@ -5,12 +5,11 @@
InputSelect,
InputText,
InputTags,
FormList,
InputSelectCheckbox,
InputDateTime
} from '$lib/elements/forms';
import { createEventDispatcher, onMount } from 'svelte';
import { tags, operators, addFilter, queries } from './store';
import { operators, addFilter, queries, type TagValue } from './store';
import type { Column } from '$lib/helpers/types';
import type { Writable } from 'svelte/store';
import { TagList } from '.';
@@ -43,6 +42,8 @@
$: isDisabled = !operator;
let localTags: TagValue[] = [];
onMount(() => {
value = column?.array ? [] : null;
if (column?.type === 'datetime') {
@@ -66,7 +67,7 @@
clear: void;
apply: { applied: number };
}>();
dispatch('apply', { applied: $tags.length });
dispatch('apply', { applied: localTags.length });
</script>
<div>
@@ -91,27 +92,24 @@
</Layout.Stack>
{#if column && operator && !operator?.hideInput}
{#if column?.array}
<FormList class="u-margin-block-start-8">
{#if column.format === 'enum'}
<InputSelectCheckbox
name="value"
bind:tags={arrayValues}
placeholder="Select value"
options={column?.elements?.map((e) => ({
label: e?.label ?? e,
value: e?.value ?? e,
checked: arrayValues.includes(e?.value ?? e)
}))}>
</InputSelectCheckbox>
{:else}
<InputTags
label="values"
showLabel={false}
id="value"
bind:tags={arrayValues}
placeholder="Enter values" />
{/if}
</FormList>
{#if column.format === 'enum'}
<InputSelectCheckbox
name="value"
bind:tags={arrayValues}
placeholder="Select value"
options={column?.elements?.map((e) => ({
label: e?.label ?? e,
value: e?.value ?? e,
checked: arrayValues.includes(e?.value ?? e)
}))}>
</InputSelectCheckbox>
{:else}
<InputTags
label="values"
id="value"
bind:tags={arrayValues}
placeholder="Enter values" />
{/if}
{:else}
<ul class="u-margin-block-start-8">
{#if column.format === 'enum'}
@@ -155,7 +153,12 @@
{#if !singleCondition}
<ul class="u-flex u-flex-wrap u-cross-center u-gap-8 u-margin-block-start-16 tags">
<TagList />
<TagList
tags={localTags}
on:remove={(e) => {
queries.removeFilter(e.detail);
queries.apply();
}} />
</ul>
{/if}
</div>
@@ -9,7 +9,10 @@
</script>
<ActionMenu.Root>
<ActionMenu.Item.Button on:click={() => (show = true)}>Custom filters</ActionMenu.Item.Button>
<ActionMenu.Item.Button
on:click={() => {
show = true;
}}>Custom filters</ActionMenu.Item.Button>
</ActionMenu.Root>
{#if show}
+152 -45
View File
@@ -1,11 +1,21 @@
<script lang="ts">
import { Submit, trackEvent } from '$lib/actions/analytics';
import { Button } from '$lib/elements/forms';
import {
Button,
InputDateTime,
InputNumber,
InputSelect,
InputSelectCheckbox,
InputTags,
InputText
} from '$lib/elements/forms';
import type { Writable } from 'svelte/store';
import Modal from '../modal.svelte';
import Content from './content.svelte';
import { addFilter, queries, queriesAreDirty, tags, ValidOperators } from './store';
import { addFilter, generateTag, operators, queries, type TagValue } from './store';
import type { Column } from '$lib/helpers/types';
import { TagList } from '.';
import { Icon, Layout } from '@appwrite.io/pink-svelte';
import { IconPlus } from '@appwrite.io/pink-icons-svelte';
export let show = false;
export let columns: Writable<Column[]>;
@@ -16,58 +26,155 @@
let selectedColumn: string | null = null;
let operatorKey: string | null = null;
let arrayValues: string[] = [];
let applied = $tags.length;
function clearAll() {
selectedColumn = null;
queries.clearAll();
if (clearOnClick) {
trackEvent(Submit.FilterClear, { source: analyticsSource });
queries.apply();
}
}
let localTags: TagValue[] = [];
let localQueries: {
id: string;
operator: string;
value: any;
arrayValues: string[];
}[] = [];
function apply() {
if (
selectedColumn &&
operatorKey &&
(operatorKey === ValidOperators.IsNotNull ||
operatorKey === ValidOperators.IsNull ||
value ||
arrayValues.length)
) {
addFilter($columns, selectedColumn, operatorKey, value, arrayValues);
selectedColumn = null;
value = null;
operatorKey = null;
arrayValues = [];
}
localQueries.forEach((query) => {
addFilter($columns, query.id, query.operator, query.value, query.arrayValues);
});
queries.apply();
localTags = [];
localQueries = [];
trackEvent(Submit.FilterApply, {
source: analyticsSource,
filters: localTags
});
show = false;
}
$: isButtonDisabled = $queriesAreDirty
? false
: !selectedColumn ||
!operatorKey ||
(!value &&
!arrayValues.length &&
operatorKey !== ValidOperators.IsNotNull &&
operatorKey !== ValidOperators.IsNull);
function addCondition() {
const newTag = generateTag(selectedColumn, operatorKey, value, arrayValues);
if (localTags.some((t) => t.tag === newTag.tag && t.value === newTag.value)) {
return;
} else {
localQueries = [
...localQueries,
{
id: selectedColumn,
operator: operatorKey,
value: value,
arrayValues: arrayValues
}
];
localTags = [...localTags, newTag];
}
}
function removeCondition(tag: TagValue) {
localTags = localTags.filter((t) => t !== tag);
}
function clearAll() {
localTags = [];
localQueries = [];
}
$: column = $columns.find((c) => c.id === selectedColumn) as Column;
$: operatorsForColumn = Object.entries(operators)
.filter(([, v]) => v.types.includes(column?.type))
.map(([k]) => ({
label: k,
value: k
}));
</script>
<Modal title="Filters" bind:show>
<span slot="description"> Apply filter rules to refine the table view </span>
<Content
{columns}
bind:columnId={selectedColumn}
bind:operatorKey
bind:value
bind:arrayValues
on:clear={() => (applied = 0)} />
<Layout.Stack>
<Layout.Stack gap="s" direction="row" alignItems="flex-start">
<InputSelect
id="column"
options={$columns
.filter((c) => c.filter !== false)
.map((c) => ({
label: c.title,
value: c.id
}))}
placeholder="Select column"
bind:value={selectedColumn} />
<InputSelect
id="operator"
disabled={!column}
options={operatorsForColumn}
placeholder="Select operator"
bind:value={operatorKey} />
</Layout.Stack>
{#if column && operatorKey}
{#if column?.array}
{#if column.format === 'enum'}
<InputSelectCheckbox
name="value"
bind:tags={arrayValues}
placeholder="Select value"
options={column?.elements?.map((e) => ({
label: e?.label ?? e,
value: e?.value ?? e,
checked: arrayValues.includes(e?.value ?? e)
}))}>
</InputSelectCheckbox>
{:else}
<InputTags
label="values"
id="value"
bind:tags={arrayValues}
placeholder="Enter values" />
{/if}
{:else if column.format === 'enum'}
<InputSelect
id="value"
bind:value
placeholder="Select value"
options={column?.elements?.map((e) => ({
label: e?.label ?? e,
value: e?.value ?? e
}))} />
{:else if column.type === 'integer' || column.type === 'double'}
<InputNumber id="value" bind:value placeholder="Enter value" />
{:else if column.type === 'boolean'}
<InputSelect
id="value"
placeholder="Select a value"
required={true}
options={[
{ label: 'True', value: true },
{ label: 'False', value: false }
].filter(Boolean)}
bind:value />
{:else if column.type === 'datetime'}
{#key value}
<InputDateTime id="value" bind:value step={60} />
{/key}
{:else}
<InputText id="value" bind:value placeholder="Enter value" />
{/if}
{/if}
<div>
<Button text on:click={addCondition}>
<Icon icon={IconPlus} slot="start" size="s" />
Add condition
</Button>
</div>
</Layout.Stack>
{#if localTags?.length}
<Layout.Stack direction="row" gap="s" alignItems="center" wrap="wrap">
<TagList tags={localTags} on:remove={(e) => removeCondition(e.detail)} />
</Layout.Stack>
{/if}
<svelte:fragment slot="footer">
<Button size="s" text on:click={clearAll}>Clear all</Button>
<Button size="s" on:click={apply} disabled={isButtonDisabled}>Apply</Button>
{#if localTags?.length}
<Button size="s" text on:click={clearAll}>Clear all</Button>
{:else}
<Button size="s" text on:click={() => (show = false)}>Cancel</Button>
{/if}
<Button size="s" on:click={apply} disabled={!localTags?.length}>Apply</Button>
</svelte:fragment>
</Modal>
+76 -6
View File
@@ -5,16 +5,24 @@
import { type Writable } from 'svelte/store';
import { CustomFilters } from '$lib/components/filters';
import { addFilterAndApply, buildFilterCol } from './quickFilters';
import { parsedTags, setFiltersOnNavigate } from './setFilters';
import { parsedTags, setFilters } from './setFilters';
import Menu from '../menu/menu.svelte';
import { Button } from '$lib/elements/forms';
import { Icon } from '@appwrite.io/pink-svelte';
import { IconFilterLine } from '@appwrite.io/pink-icons-svelte';
import {
IconChevronLeft,
IconChevronRight,
IconFilterLine
} from '@appwrite.io/pink-icons-svelte';
import QuickfiltersSubMenu from './quickfiltersSubMenu.svelte';
import { isSmallViewport } from '$lib/stores/viewport';
import { BottomSheet } from '..';
import { capitalize } from '$lib/helpers/string';
export let columns: Writable<Column[]>;
export let analyticsSource: string;
let openBottomSheet = false;
let filterCols = $columns
.map((col) => (col.filter !== false ? buildFilterCol(col) : null))
.filter((f) => f?.options);
@@ -23,20 +31,78 @@
const paramQueries = p.to.url.searchParams.get('query');
const localQueries = queryParamToMap(paramQueries || '[]');
const localTags = Array.from(localQueries.keys());
setFiltersOnNavigate(localTags, filterCols, $columns);
console.log(paramQueries, localQueries, localTags);
setFilters(localTags, filterCols, $columns);
filterCols = filterCols;
});
const subSheets = filterCols.map((col) => {
return {
title: col.title,
top: {
title: col.title,
trailingIcon: IconChevronRight,
items: col.options.map((o) => {
return {
title: capitalize(o.label),
name: capitalize(o.label),
closeOnClick: false,
onClick: () => {
addFilterAndApply(
col.id,
col.title,
col.operator,
o.value,
col.array ? [o.value] : [],
$columns,
analyticsSource
);
}
};
})
},
bottom: {
name: 'Back',
items: [
{
name: 'Back',
leadingIcon: IconChevronLeft,
onClick: () => {
// navigate to the previous menu
}
}
]
}
};
});
$: organizationsBottomSheet = {
top: {
title: 'Filters',
items: filterCols.map((col) => {
return {
name: col.title,
onClick: () =>
console.log(subSheets.find((sheet) => sheet?.title === col?.title)),
subMenu: subSheets.find((sheet) => sheet?.title === col?.title),
trailingIcon: IconChevronRight
};
})
}
};
</script>
<Menu>
{#if $parsedTags?.length}
<Button secondary badge={`${$parsedTags?.length}`}>
<Button
secondary
badge={`${$parsedTags?.length}`}
on:click={() => (openBottomSheet = true)}>
<Icon icon={IconFilterLine} slot="start" size="s" />
Filters
</Button>
{:else}
<Button secondary>
<Button secondary on:click={() => (openBottomSheet = true)}>
<Icon icon={IconFilterLine} slot="start" size="s" />
Filters
</Button>
@@ -81,3 +147,7 @@
<CustomFilters {columns} />
</svelte:fragment>
</Menu>
<!-- {#if $isSmallViewport}
<BottomSheet.Menu bind:isOpen={openBottomSheet} menu={organizationsBottomSheet} />
{/if} -->
+1 -5
View File
@@ -5,11 +5,7 @@ import { tags, type TagValue } from './store';
export const parsedTags = writable<TagValue[]>([]);
export function setFiltersOnNavigate(
localTags: TagValue[],
filterCols: FilterData[],
$columns: Column[]
) {
export function setFilters(localTags: TagValue[], filterCols: FilterData[], $columns: Column[]) {
if (!localTags?.length) {
filterCols.forEach((filter) => {
resetOptions(filter);
+23 -19
View File
@@ -244,30 +244,13 @@ function formatArray(array: string[]) {
}
}
function generateDefaultOperators() {
export function generateDefaultOperators() {
const operators: Record<string, Operator> = {};
operatorsDefault.forEach((operator, operatorName) => {
operators[operatorName] = {
toQuery: operator.query,
toTag: (attribute, input = null, type = null) => {
if (input === null) {
return {
value: '',
tag: `**${attribute}** ${operatorName}`
};
} else if (Array.isArray(input) && input.length > 2) {
return {
value: input,
tag: `**${attribute}** ${operatorName} **${formatArray(input)}** `
};
} else if (type === ValidTypes.Datetime) {
return {
value: input,
tag: `**${attribute}** ${operatorName} **${toLocaleDateTime(input.toString())}**`
};
} else {
return { value: input, tag: `**${attribute}** ${operatorName} **${input}**` };
}
return generateTag(attribute, operatorName, input, type);
},
types: operator.types,
hideInput: operator.hideInput
@@ -276,6 +259,27 @@ function generateDefaultOperators() {
return operators;
}
export function generateTag(attribute: string, operatorName: string, input = null, type = null) {
if (input === null) {
return {
value: '',
tag: `**${attribute}** ${operatorName}`
};
} else if (Array.isArray(input) && input.length > 2) {
return {
value: input,
tag: `**${attribute}** ${operatorName} **${formatArray(input)}** `
};
} else if (type === ValidTypes.Datetime) {
return {
value: input,
tag: `**${attribute}** ${operatorName} **${toLocaleDateTime(input.toString())}**`
};
} else {
return { value: input, tag: `**${attribute}** ${operatorName} **${input}**` };
}
}
export const operators = generateDefaultOperators();
export function tagFormat(node: HTMLElement) {
+25 -18
View File
@@ -1,22 +1,29 @@
<script lang="ts">
import { queries, tagFormat, tags } from './store';
import { Tooltip } from '@appwrite.io/pink-svelte';
import { IconX } from '@appwrite.io/pink-icons-svelte';
import { tagFormat, type TagValue } from './store';
import { Icon, Tag, Tooltip } from '@appwrite.io/pink-svelte';
import { createEventDispatcher } from 'svelte';
export let tags: TagValue[];
const dispatch = createEventDispatcher();
</script>
{#each $tags as tag (tag)}
<Tooltip disabled={Array.isArray(tag.value) ? tag.value?.length < 3 : true}>
<button
type="button"
class="tag"
on:click|preventDefault={() => {
queries.removeFilter(tag);
queries.apply();
}}>
<span class="text" use:tagFormat>
{tag.tag}
</span>
<i class="icon-x" />
</button>
<span slot="tooltip">{tag?.value?.toString()}</span>
</Tooltip>
{#each tags as tag (tag)}
<span>
<Tooltip
disabled={Array.isArray(tag.value) ? tag.value?.length < 3 : true}
maxWidth="600px">
<Tag
size="s"
on:click={() => {
dispatch('remove', tag);
}}>
{#key tag.tag}
<span use:tagFormat>{tag.tag}</span>
{/key}
<Icon icon={IconX} size="s" slot="end" />
</Tag>
<span slot="tooltip">{tag?.value?.toString()}</span>
</Tooltip>
</span>
{/each}
+1 -1
View File
@@ -31,7 +31,7 @@
<slot name="menu" {toggle} />
{#if $$slots.end}
<div class="separator" use:melt={$separator} />
<slot name="end" />
<slot name="end" {toggle} />
{/if}
</Card.Base>
</div>