diff --git a/src/lib/components/domains/cnameTable.svelte b/src/lib/components/domains/cnameTable.svelte index 9569ce89f..eb1dcc693 100644 --- a/src/lib/components/domains/cnameTable.svelte +++ b/src/lib/components/domains/cnameTable.svelte @@ -36,6 +36,7 @@ - import { Icon, Layout, Tag, Tooltip } from '@appwrite.io/pink-svelte'; - import { queries, tagFormat, tags } from './store'; + import { + Icon, + Layout, + Tooltip, + CompoundTagRoot, + CompoundTagChild, + Typography, + ActionMenu, + Selector + } from '@appwrite.io/pink-svelte'; + import { capitalize } from '$lib/helpers/string'; + import { queries, tags } from './store'; import { IconX } from '@appwrite.io/pink-icons-svelte'; - import { parsedTags } from './setFilters'; + import { parsedTags, type ParsedTag } from './setFilters'; import { Button } from '$lib/elements/forms'; + import type { Column } from '$lib/helpers/types'; + import { writable, type Writable } from 'svelte/store'; + import Menu from '$lib/components/menu/menu.svelte'; + import { addFilterAndApply, buildFilterCol, type FilterData } from './quickFilters'; + import QuickFilters from '$lib/components/filters/quickFilters.svelte'; + import { isSmallViewport } from '$lib/stores/viewport'; + + let { + columns = writable([]), + analyticsSource = '' + }: { columns?: Writable; analyticsSource?: string } = $props(); + + function parseTagParts(tagString: string): { text: string; operator: boolean }[] { + return tagString + .split(/\*\*(.*?)\*\*/) + .map((part, index) => { + // Even indices are outside bold (operators), odd indices are inside bold (values) + if (index % 2 === 0) { + return part + .split(/\s+/) + .filter(Boolean) + .map((t) => ({ text: t, operator: true })); + } else { + return [{ text: part, operator: false }]; + } + }) + .flat() + .filter((p) => Boolean(p.text)); + } + + function getFilterFor(title: string): FilterData | null { + if (!columns) return null; + const col = ($columns as unknown as Column[]).find((c) => c.title === title); + if (!col) return null; + const filter = buildFilterCol(col); + return filter ?? null; + } + + // Build available filter definitions from provided columns + let availableFilters = $derived( + ($columns as unknown as Column[] | undefined)?.length + ? (($columns as unknown as Column[]) + .map((c) => (c.filter !== false ? buildFilterCol(c) : null)) + .filter((f) => f && f.options) as FilterData[]) + : [] + ); + + // QuickFilters uses the same filters list + let filterCols = $derived(availableFilters); + + // Always-show placeholders are derived from available filters (no hardcoding) + // Use reactive array so runes can track changes + let hiddenPlaceholders: string[] = $state([]); + + let activeTitles = $derived( + ($parsedTags || []).map((t) => (t as ParsedTag).title).filter(Boolean) as string[] + ); + + // Compute current placeholders (major filters not already active or dismissed) + let placeholders = $derived( + availableFilters + .filter((f) => !activeTitles.includes(f.title)) + .filter((f) => !hiddenPlaceholders.includes(f.title)) + ); -{#if $parsedTags?.length} - + + {#if $parsedTags?.length} {#each $parsedTags as tag (tag.tag)} - { - const t = $tags.filter((t) => t.tag.includes(tag.tag.split(' ')[0])); - t.forEach((t) => (t ? queries.removeFilter(t) : null)); - queries.apply(); - parsedTags.update((tags) => tags.filter((t) => t.tag !== tag.tag)); - }}> - {#key tag.tag} - {tag.tag} - {/key} - - + + {@const parts = parseTagParts(tag.tag)} + {@const property = (tag as ParsedTag).title} + + {#each parts as part} + + + + {#if part.operator} + {part.text} + {:else} + {part.text + .split(' or ') + .map((t) => capitalize(t)) + .join(' or ')} + {/if} + + + {#if property} + {@const filter = getFilterFor(property)} + {#if filter} + {@const isArray = filter?.array} + {@const selectedArray = Array.isArray(tag.value) + ? tag.value + : []} + {#each filter.options as option (filter.title + option.value + option.label)} + + { + if (isArray) { + const exists = + selectedArray.includes( + option.value + ); + const next = exists + ? selectedArray.filter( + (v) => + v !== option.value + ) + : [ + ...selectedArray, + option.value + ]; + addFilterAndApply( + filter.id, + filter.title, + filter.operator, + null, + next, + $columns, + analyticsSource + ); + } else { + addFilterAndApply( + filter.id, + filter.title, + filter.operator, + option.value, + [], + $columns, + analyticsSource + ); + } + }}> + + {#if isArray} + + {/if} + {capitalize(option.label)} + + + + {/each} + {/if} + {/if} + + + + {/each} + { + const t = $tags.filter((t) => + t.tag.includes((tag as ParsedTag).title) + ); + t.forEach((t) => (t ? queries.removeFilter(t) : null)); + queries.apply(); + parsedTags.update((tags) => tags.filter((t) => t.tag !== tag.tag)); + }}> + + + {tag?.value?.toString()} {/each} + {/if} + + + {#if placeholders?.length} + {#each placeholders as filter (filter.title + filter.id)} + + + + + {capitalize(filter.title)} + + { + e.stopPropagation(); + if (!hiddenPlaceholders.includes(filter.title)) { + hiddenPlaceholders = [...hiddenPlaceholders, filter.title]; + } + }}> + + + + + {#if filter.options} + {#each filter.options as option (filter.title + option.value + option.label)} + + { + addFilterAndApply( + filter.id, + filter.title, + filter.operator, + filter?.array ? null : option.value, + filter?.array ? [option.value] : [], + $columns, + analyticsSource + ); + }}> + {capitalize(option.label)} + + + {/each} + {/if} + + + + {/each} + {/if} + + {#if $parsedTags?.length} - -{/if} + {/if} + + {#if filterCols?.length && !$isSmallViewport} + + {/if} + diff --git a/src/lib/components/filters/quickFilters.svelte b/src/lib/components/filters/quickFilters.svelte index 76bbf90c5..233fa7358 100644 --- a/src/lib/components/filters/quickFilters.svelte +++ b/src/lib/components/filters/quickFilters.svelte @@ -22,9 +22,12 @@ - {#each filterCols.filter((f) => f?.options) as filter (filter.title + filter.id)} diff --git a/src/lib/components/filters/setFilters.ts b/src/lib/components/filters/setFilters.ts index e6f07083c..301a66a0a 100644 --- a/src/lib/components/filters/setFilters.ts +++ b/src/lib/components/filters/setFilters.ts @@ -3,7 +3,11 @@ import { get, writable } from 'svelte/store'; import { type FilterData } from './quickFilters'; import { tags, type TagValue } from './store'; -export const parsedTags = writable([]); +export type ParsedTag = TagValue & { + title: string; +}; + +export const parsedTags = writable([]); export function setFilters(localTags: TagValue[], filterCols: FilterData[], $columns: Column[]) { if (!localTags?.length) { @@ -47,9 +51,10 @@ export function setFilterData(filter: FilterData) { }); } cleanOldTags(filter?.title); - const newTag = { + const newTag: ParsedTag = { tag: tagData.tag.replace(',', ' or '), - value: tagData.value + value: tagData.value, + title: filter.title }; parsedTags.update((tags) => { @@ -69,9 +74,10 @@ export function setTimeFilter(filter: FilterData, columns: Column[]) { const ranges = col.elements as { value: string; label: string }[]; const timeRange = ranges.find((range) => range.value === timeTag.value); if (timeRange) { - const newTag = { + const newTag: ParsedTag = { tag: `**${filter.title}** is **${timeRange.label}**`, - value: timeRange.value + value: timeRange.value, + title: filter.title }; cleanOldTags(filter?.title); @@ -102,9 +108,10 @@ export function setSizeFilter(filter: FilterData, columns: Column[]) { if (sizeRange) { cleanOldTags(filter?.title); - const newTag = { + const newTag: ParsedTag = { tag: `**${filter.title}** is **${sizeRange.label}**`, - value: sizeTag.value + value: sizeTag.value, + title: filter.title }; parsedTags.update((tags) => { tags.push(newTag); @@ -126,9 +133,10 @@ export function setStatusCodeFilter(filter: FilterData, columns: Column[]) { const codeRange = ranges.find((c) => c?.value && c.value === statusCodeTag.value); if (codeRange) { cleanOldTags(filter?.title); - const newTag = { + const newTag: ParsedTag = { tag: `**${filter.title}** is **${codeRange.label}**`, - value: statusCodeTag.value + value: statusCodeTag.value, + title: filter.title }; parsedTags.update((tags) => { tags.push(newTag); @@ -156,9 +164,10 @@ export function setDateFilter(filter: FilterData, columns: Column[]) { }); if (dateRange) { cleanOldTags(filter?.title); - const newTag = { + const newTag: ParsedTag = { tag: `**${filter.title}** is **${dateRange.label}**`, - value: dateTag.value + value: dateTag.value, + title: filter.title }; parsedTags.update((tags) => { tags.push(newTag); diff --git a/src/lib/layout/activity.svelte b/src/lib/layout/activity.svelte index c77fa3488..04fef37d7 100644 --- a/src/lib/layout/activity.svelte +++ b/src/lib/layout/activity.svelte @@ -49,7 +49,7 @@ expanded={databasesScreen && !insideSideSheet} slotSpacing={databasesScreen && !insideSideSheet}> {#if logs.total} - + User Event diff --git a/src/lib/layout/responsiveContainerHeader.svelte b/src/lib/layout/responsiveContainerHeader.svelte index 98e10321b..7812f34f9 100644 --- a/src/lib/layout/responsiveContainerHeader.svelte +++ b/src/lib/layout/responsiveContainerHeader.svelte @@ -1,12 +1,7 @@