mirror of
https://github.com/appwrite/console.git
synced 2026-06-06 19:27:48 +00:00
WIP: fixes to tags
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -19,6 +19,7 @@ type MenuItem = {
|
||||
trailingIcon?: ComponentType;
|
||||
onClick?: () => void;
|
||||
href?: string;
|
||||
closeOnClick?: boolean;
|
||||
subMenu?: { top: SubMenu; bottom: SubMenu };
|
||||
};
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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} -->
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user