Source ServiceDataTable
Source ServiceDataTable
Cette page est générée automatiquement à partir du dépôt local au moment de la génération de la documentation.
Fichiers inclus
packages/common/src/lib/widgets/service-data-table/service-data-table.declaration.tspackages/common/src/lib/widgets/service-data-table/service-data-table.config.tspackages/common/src/lib/widgets/service-data-table/advanced-filters/AdvancedFiltersDialog.sveltepackages/common/src/lib/widgets/service-data-table/advanced-filters/ConditionConfigurator.sveltepackages/common/src/lib/widgets/service-data-table/advanced-filters/ConditionGroupConfigurator.sveltepackages/common/src/lib/widgets/service-data-table/advanced-filters/query-layer.i18n.tspackages/common/src/lib/widgets/service-data-table/quick-draw-selector/index.tspackages/common/src/lib/widgets/service-data-table/quick-draw-selector/quick-draw-selector.model.tspackages/common/src/lib/widgets/service-data-table/quick-draw-selector/QuickDrawSelector.sveltepackages/common/src/lib/widgets/service-data-table/service-data-table.i18n.tspackages/common/src/lib/widgets/service-data-table/service-data-table.state.svelte.tspackages/common/src/lib/widgets/service-data-table/service-data-table/in-memory-feature-page-query.svelte.tspackages/common/src/lib/widgets/service-data-table/service-data-table/service-data-table.model.tspackages/common/src/lib/widgets/service-data-table/service-data-table/ServiceDataTable.sveltepackages/common/src/lib/widgets/service-data-table/ServiceDataTableTabContent.sveltepackages/common/src/lib/widgets/service-data-table/ServiceDataTableWidget.svelte
packages/common/src/lib/widgets/service-data-table/service-data-table.declaration.ts
import { widgetFactorySvelte, type WidgetProps } from '$lib/api/managers/widget';import { resultsTableContainerConfigSchema, type ServiceDataTableFullConfig } from './service-data-table.config';import type { WidgetDeclaration } from '$lib/api/managers/widget/widget-declaration';import { ServiceDataTableState } from '$lib/widgets/service-data-table/service-data-table.state.svelte';
export const declaration = { factory: () => import('./ServiceDataTableWidget.svelte').then((ServiceDataTableWidget) => widgetFactorySvelte(ServiceDataTableWidget), ), schema: () => resultsTableContainerConfigSchema, state: (props) => new ServiceDataTableState(props),} satisfies WidgetDeclaration;
export type ServiceDataTableProps = WidgetProps<ServiceDataTableFullConfig, ServiceDataTableState>;packages/common/src/lib/widgets/service-data-table/service-data-table.config.ts
import { inToolbarSchemaFrom } from '$lib/api/managers/configuration/models/widget/widget-in-toolbar.schema';import { i18nSchemaFrom } from '$lib/api/managers/i18n';import { z } from 'zod';import { defineWidgetConfig } from '$lib/api/managers/configuration';import { serviceDataTableI18n } from '$lib/widgets/service-data-table/service-data-table.i18n';
const resultsTableConfigSchema = z.object({ highlightOnOver: z.boolean().default(true), zoomOnHover: z.boolean().default(false), zoomOnSelect: z.boolean().default(true), allowExport: z.boolean().default(true), allowAdvancedFilters: z.boolean().default(true), allowAdvancedFiltersReset: z.boolean().default(true), allowSpatialFilter: z.boolean().default(true), allowSpatialFilterReset: z.boolean().default(true),});
export const resultsTableContainerConfigSchema = defineWidgetConfig({ title: serviceDataTableI18n.title, inToolbar: inToolbarSchemaFrom(false), i18n: i18nSchemaFrom(serviceDataTableI18n), config: resultsTableConfigSchema.prefault({}),});
export type ServiceDataTableFullConfig = z.infer<typeof resultsTableContainerConfigSchema>;export type ServiceDataTableI18n = ServiceDataTableFullConfig['i18n'];packages/common/src/lib/widgets/service-data-table/advanced-filters/AdvancedFiltersDialog.svelte
<script lang="ts"> import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, } from '$lib/components/shadcn/ui/dialog'; import { Button } from '$lib/components/shadcn/ui/button'; import { deepClone, type PropsWithChildren } from '$lib/api/utils/index.js'; import { getLayoutManager } from '$lib/api/managers/layout'; import { getI18n } from '$lib/api/managers/i18n'; import { ConditionOperation, type QueryConditionGroup } from '$lib/api/tools'; import ConditionGroupConfigurator from './ConditionGroupConfigurator.svelte'; import type { ApiFieldDescription } from '$lib/api/domain'; import { queryLayerI18n } from './query-layer.i18n';
interface Props { open?: boolean; title: string; condition: QueryConditionGroup; fields: ApiFieldDescription[]; allowGroups?: boolean; allowReset?: boolean; addDefaultConditionIfEmpty?: boolean; }
const layoutManager = getLayoutManager(); const i18n = getI18n();
let { open = $bindable(false), condition = $bindable(), title, fields, allowGroups, children, allowReset = true, addDefaultConditionIfEmpty = false, }: PropsWithChildren<Props> = $props();
if (addDefaultConditionIfEmpty && condition.conditions.length === 0) { condition.conditions.push({ field: fields[0], operation: ConditionOperation.EQUALS, value: null, }); }
$effect(() => { if (condition) { innerCondition = deepClone($state.snapshot(condition)); } });
// Use a deep copy of the original object to only apply the condition change on validation let innerCondition = $state(deepClone($state.snapshot(condition)));
function applyFilters() { condition = deepClone($state.snapshot(innerCondition)); open = false; }
function reset() { condition = { operator: condition.operator, conditions: [], }; }</script>
<Dialog bind:open portal={layoutManager.layout.root}> <DialogTrigger> {@render children?.()} </DialogTrigger> <DialogContent class="gv-max-w-[45vw]"> <DialogHeader> <DialogTitle>{title}</DialogTitle> </DialogHeader> <ConditionGroupConfigurator bind:condition={innerCondition} {fields} i18n={queryLayerI18n} {allowGroups} /> <DialogFooter> {#if allowReset} <Button variant="outline" onclick={reset} size="lg" data-test-id="Advanced-Filters-Reset"> {i18n('common.reset')} </Button> {/if} <Button onclick={applyFilters} size="lg" data-test-id="Advanced-Filters-Search"> {i18n('common.research')} </Button> </DialogFooter> </DialogContent></Dialog>packages/common/src/lib/widgets/service-data-table/advanced-filters/ConditionConfigurator.svelte
<script lang="ts"> import { type ApiFieldDescription, ApiFieldType, ApiFieldTypeToOperations } from '$lib/api/domain'; import { getI18n } from '$lib/api/managers/i18n'; import type { ConditionOperation, QueryCondition } from '$lib/api/tools/query'; import { ApiSelect, type ApiSelectItem } from '$lib/components/api-select'; import DynamicInput from '$lib/components/dynamic-input/DynamicInput.svelte'; import Trash2 from 'lucide-svelte/icons/trash-2'; import type { QueryLayerI18n } from './query-layer.i18n'; import { Button } from '$lib/components/shadcn/ui/button'; import { cn } from '$lib/components/shadcn/utils';
interface Props { fields: ApiFieldDescription[]; condition: QueryCondition; i18n: QueryLayerI18n; onRemove: () => void; class?: string; }
let { fields, condition = $bindable(), i18n: i18nConfig, onRemove, class: className }: Props = $props();
const i18n = getI18n(i18nConfig);
const selectedField = $derived<ApiFieldDescription>(condition.field); const operatorsList = $derived.by<ApiSelectItem<ConditionOperation>[]>(() => { if (selectedField) { return ApiFieldTypeToOperations[selectedField.type].map((operator) => ({ label: getOperatorLabel(operator), value: operator, 'data-test-id': `Advanced-Filters-Operation-${operator}`, })); } return []; }); const fieldsList = $derived<ApiSelectItem<ApiFieldDescription>[]>( fields .filter((f) => f.type !== ApiFieldType.GEOMETRY) .map((field) => ({ label: field.label, value: field, })), );
$effect(() => { const fieldInList = fields.find((field) => field.key === condition.field?.key); if (!condition.field || !fieldInList) { condition.field = fields[0]; } });
$effect(() => { const operatorInList = operatorsList.find((operator) => operator.value === condition.operation); if (!condition.operation || !operatorInList) { condition.operation = operatorsList[0]?.value; } });
function getOperatorLabel(operator: ConditionOperation) { return i18n(`condition-operation-${operator}`); }</script>
<div class={cn('gv-flex gv-gap-1.5', className)}> <ApiSelect options={fieldsList} bind:value={condition.field} getKey={(v) => v.key} class="gv-w-50" dataTestId="Advanced-Filters-Field" /> <ApiSelect options={operatorsList} bind:value={condition.operation} class="gv-w-28" dataTestId="Advanced-Filters-Operation" /> <DynamicInput field={selectedField} bind:value={condition.value} class="gv-flex-1" data-test-id="Advanced-Filters-Input" /> <Button onclick={onRemove} title={i18n('delete-condition')} variant="link" class="gv-px-0" data-test-id="Advanced-Filters-Remove" > <Trash2 class="gv-size-4" /> </Button></div>packages/common/src/lib/widgets/service-data-table/advanced-filters/ConditionGroupConfigurator.svelte
<script lang="ts"> import { type ApiFieldDescription } from '$lib/api/domain'; import { getI18n } from '$lib/api/managers/i18n'; import { ConditionOperation, isQueryConditionGroup, LogicalOperator, type QueryCondition, type QueryConditionGroup, } from '$lib/api/tools/query'; import { Button } from '$lib/components/shadcn/ui/button'; import ConditionConfigurator from './ConditionConfigurator.svelte'; import ConditionGroupConfigurator from './ConditionGroupConfigurator.svelte'; import type { QueryLayerI18n } from './query-layer.i18n'; import Plus from 'lucide-svelte/icons/plus'; import ListFilterPlus from 'lucide-svelte/icons/list-filter-plus'; import Trash2 from 'lucide-svelte/icons/trash-2'; import { Switch } from '$lib/components/shadcn/ui/switch'; import { cn } from '$lib/components/shadcn/utils';
interface Props { i18n: QueryLayerI18n; fields: ApiFieldDescription[]; condition: QueryConditionGroup; allowGroups?: boolean; class?: string; }
const { condition = $bindable(), allowGroups = false, fields, i18n: i18nConfig, class: className, }: Props = $props();
const i18n = getI18n(i18nConfig);
function getDefaultCondition(): QueryCondition { return { field: fields[0], operation: ConditionOperation.EQUALS, value: null }; }
function addCondition() { condition.conditions.push(getDefaultCondition()); }
function removeCondition(index: number) { condition.conditions.splice(index, 1); }
function handleAddConditionGroup() { condition.conditions.push({ operator: LogicalOperator.AND, conditions: [getDefaultCondition()], }); }</script>
<div class={cn('gv-space-y-2 gv-p-2', className)}> <!--Operator switch--> <div class="gv-flex gv-align-middle gv-mb-2"> <button onclick={() => (condition.operator = LogicalOperator.AND)} class={cn( 'gv-flex gv-align-middle gv-text-body', condition.operator === LogicalOperator.AND ? 'gv-font-bold' : 'gv-opacity-60', )} data-test-id="Advanced-Filters-Condition-Operator-And" > {i18n('condition-operator-and')} </button> <Switch bind:checked={ () => condition.operator === LogicalOperator.OR, (c) => (condition.operator = c ? LogicalOperator.OR : LogicalOperator.AND) } class="gv-mr-1 gv-ml-1" data-test-id="Advanced-Filters-Condition-Operator" /> <button onclick={() => (condition.operator = LogicalOperator.OR)} class={cn( 'gv-flex gv-align-middle gv-text-body', condition.operator === LogicalOperator.OR ? 'gv-font-bold' : 'gv-opacity-60', )} data-test-id="Advanced-Filters-Condition-Operator-Or" > {i18n('condition-operator-or')} </button> </div>
{#each condition.conditions as subCondition, i (subCondition)} {#if isQueryConditionGroup(condition.conditions[i])} <div class="gv-flex gv-gap-1.5"> <ConditionGroupConfigurator i18n={i18nConfig} bind:condition={condition.conditions[i]} {fields} allowGroups class="gv-border gv-border-dashed gv-flex-1 has-[+button:hover]:gv-border-primary-600" /> <Button title={i18n('delete-group')} onclick={() => removeCondition(i)} variant="link" class="gv-px-0"> <Trash2 class="gv-size-4" /> </Button> </div> {:else} <ConditionConfigurator i18n={i18nConfig} onRemove={() => removeCondition(i)} bind:condition={condition.conditions[i]} {fields} class="gv-w-full" /> {/if} {/each} <Button onclick={addCondition} variant="outline" data-test-id="Advanced-Filters-Add-Condition"> <Plus class="gv-size-4" /> {i18n('add-condition')} </Button> {#if allowGroups} <Button onclick={handleAddConditionGroup} variant="outline" data-test-id="Advanced-Filters-Add-Group"> <ListFilterPlus class="gv-size-4" /> {i18n('add-group')} </Button> {/if}</div>packages/common/src/lib/widgets/service-data-table/advanced-filters/query-layer.i18n.ts
import type { I18nRegistry } from '$lib/api/managers/i18n';
export const queryLayerI18n = { 'condition-operator-and': { fr: 'ET', nl: 'NL - ET', }, 'condition-operator-or': { fr: 'OU', nl: 'NL - OU', }, title: { fr: 'Query layer', nl: 'NL - Query layer', }, condition: { fr: 'Condition N° {{conditionNumber}}', nl: 'Voorwaarde N° {{conditionNumber}}', }, search: { fr: 'Recherche dans "{{name}}"', nl: 'NL - Recherche dans "{{name}}"', }, 'attribute-filter': { fr: 'Filtre attributaire', nl: 'NL - Filtre attributaire', }, 'spatial-filter': { fr: 'Filtre spatial', nl: 'NL - Filtre spatial', }, 'missing-filter': { fr: 'Veuillez remplir un filtre attributaire ou un filtre spatial.', nl: 'NL - Veuillez remplir un filtre attributaire ou un filtre spatial.', }, 'delete-condition': { fr: 'Surppimer la condition', nl: 'Negeer de voorwaarde', }, 'add-condition': { fr: 'Ajouter une condition', nl: 'Voeg een voorwaarde toe', }, group: { fr: 'Groupe N° {{groupNumber}}', nl: 'Groep N° {{groupNumber}}', }, 'add-group': { fr: 'Ajouter un groupe', nl: 'Voeg een groep', }, 'delete-group': { fr: 'Supprimer le groupe', nl: 'Groep verwijderen', }, 'apply-filter': { fr: 'Appliquer le filtre', nl: 'Filter toepassen', }, results: { fr: 'Résultats', nl: 'Resultaten', }, 'condition-operation-EQUALS': { fr: 'Egal', nl: 'Gelijkwaardig', }, 'condition-operation-NOT_EQUALS': { fr: 'Pas égal', nl: 'Niet gelijk', }, 'condition-operation-CONTAINS': { fr: 'Contient', nl: 'Bevat', }, 'condition-operation-GREATER_THAN': { fr: 'Plus grand', nl: 'Groter', }, 'condition-operation-GREATER_THAN_OR_EQUAL': { fr: 'Plus grand ou egal', nl: 'Groter of gelijk', }, 'condition-operation-LESS_THAN': { fr: 'Plus petit', nl: 'Kleiner', }, 'condition-operation-LESS_THAN_OR_EQUAL': { fr: 'Plus petit ou egal', nl: 'Kleiner of gelijk', },} satisfies I18nRegistry;
export type QueryLayerI18n = typeof queryLayerI18n;packages/common/src/lib/widgets/service-data-table/quick-draw-selector/index.ts
export * from './QuickDrawSelector.svelte';packages/common/src/lib/widgets/service-data-table/quick-draw-selector/quick-draw-selector.model.ts
import { type DrawCreateType } from '$lib/api/tools';import type { Icon } from '$lib/api/icons';
export type QuickDrawSelectorItem = { type: DrawCreateType; icon: Icon;};
export const quickDrawSelectorItems: QuickDrawSelectorItem[] = [ { type: 'point', icon: { lucide: 'Dot', }, }, { type: 'polyline', icon: { lucide: 'Waypoints', }, }, { type: 'polygon', icon: { geoviewer: 'geoviewer-polygon', }, },];packages/common/src/lib/widgets/service-data-table/quick-draw-selector/QuickDrawSelector.svelte
<script lang="ts"> import { ToggleGroup, ToggleGroupItem } from '$lib/components/shadcn/ui/toggle-group'; import { quickDrawSelectorItems } from './quick-draw-selector.model'; import type { DrawCreateType } from '$lib/api/tools'; import { getMapManager } from '$lib/api/map'; import { initGraphicMapServiceConfiguration } from '$lib/api/managers/configuration'; import { onDestroy } from 'svelte'; import StringUtils from '$lib/api/utils/string.utils'; import type { ToggleSize, ToggleVariant } from '$lib/components/shadcn/ui/toggle'; import { Icon } from '$lib/components/icon'; import type { ApiFeature } from '$lib/api/feature'; import type { ServiceDataTableI18n } from '$lib/widgets/service-data-table/service-data-table.config'; import { getI18n } from '$lib/api/managers/i18n';
interface Props { activeDraw?: DrawCreateType | null; feature?: ApiFeature; variant?: ToggleVariant; size?: ToggleSize; i18nConfig: ServiceDataTableI18n; }
let { activeDraw = $bindable(), feature = $bindable(), variant, size, i18nConfig }: Props = $props();
const i18n = getI18n(i18nConfig); const mapManager = getMapManager(); const drawFactory = mapManager.tools.draw;
const serviceId = StringUtils.uuid(); const graphicMapService = mapManager.addGraphicMapService( initGraphicMapServiceConfiguration({ id: serviceId, label: '', toc: { visible: false, }, }), ); const drawTool = drawFactory.create({ layer: graphicMapService });
$effect(() => { if (!feature) { graphicMapService.removeAll(); } });
$effect(() => { if (activeDraw) { drawTool.create({ type: activeDraw, onDrawComplete: (f: ApiFeature) => { if (feature) { drawTool.delete(feature); } feature = f; activeDraw = null; }, }); } else { drawTool.stop(); } });
onDestroy(() => { feature = undefined; drawTool.destroy(); mapManager.removeMapService(serviceId); });</script>
<ToggleGroup type="single" bind:value={activeDraw} {variant} {size}> {#each quickDrawSelectorItems as item} <ToggleGroupItem class="gv-border-solid gv-border-[0.1rem] gv-border-primary/70 gv-px-1" value={item.type} title={i18n(`filter-on-${item.type}`)} data-test-id={`Draw-Selector-${item.type}`} > <Icon icon={item.icon} class="gv-size-4" /> </ToggleGroupItem> {/each}</ToggleGroup>packages/common/src/lib/widgets/service-data-table/service-data-table.i18n.ts
import type { I18nRegistry } from '$lib/api/managers/i18n';
export const serviceDataTableI18n = { title: { fr: 'Tableau de résultats', nl: 'NL - Tableau de résultats', }, 'advanced-filters': { fr: 'Filtre avancé', nl: 'NL - Filtre avancé', }, 'nothing-to-export': { fr: "Il n'y a rien à exporter. Changer de filtre pour afficher des résultats dans le tableau", nl: "NL - Il n'y a rien à exporter. Changer de filtre pour afficher des résultats dans le tableau", }, 'filter-on-point': { fr: "Filtrer sur la localisation d'un point", nl: "NL - Filtrer sur la localisation d'un point", }, 'filter-on-polyline': { fr: "Filtrer sur le tracé d'une ligne", nl: "NL - Filtrer sur le tracé d'une ligne", }, 'filter-on-polygon': { fr: "Filtrer sur l'emprise d'un polygone", nl: "NL - Filtrer sur l'emprise d'un polygone", }, 'table-selection-state': { fr: '{{rows}} ligne(s) sur {{totalRows}} sélectionnée(s)', nl: 'NL - {{rows}} ligne(s) sur {{totalRows}} sélectionnée(s)', }, 'table-page-state': { fr: '{{first}} - {{last}} sur {{total}} lignes', nl: 'NL - {{first}} - {{last}} sur {{total}} lignes', }, 'table-page-size': { fr: 'Lignes par page', nl: 'NL - Lignes par page', }, 'filter-on-map-extent': { fr: 'Filtrer sur le cadrage de la carte', nl: 'NL - Filtrer sur le cadrage de la carte', }, 'reset-spatial-filter': { fr: 'Réinitialiser le filtre spatial', nl: 'NL - Réinitialiser le filtre spatial', }, 'no-results': { fr: 'Pas de résultats', nl: 'NL - Pas de résultats', }, 'reset-all': { fr: 'Réinitialiser tout', nl: 'NL - Réinitialiser tout', }, 'reset-selection': { fr: 'Vider la sélection', nl: 'NL - Vider la sélection', }, 'select-all': { fr: 'Sélectionner tous les éléments', nl: 'NL - Sélectionner tous les éléments', },} satisfies I18nRegistry;packages/common/src/lib/widgets/service-data-table/service-data-table.state.svelte.ts
import type { WidgetStateProps } from '$lib/api/managers/widget/widget-declaration';import type { Queryable } from '$lib/api/utils';import { InMemoryFeaturePageQuery } from '$lib/widgets/service-data-table/service-data-table/in-memory-feature-page-query.svelte';import type { ServiceDataTableTab } from '$lib/widgets/service-data-table/service-data-table/service-data-table.model';
export class ServiceDataTableState { readonly itemStates = new Map<Queryable, ServiceDataTableItemState>();
constructor(private readonly props: WidgetStateProps) {}
public add(tab: ServiceDataTableTab) { const service = tab.service; if (this.itemStates.has(service)) { return this.itemStates.get(service)!; } const state = new ServiceDataTableItemState(tab); this.itemStates.set(service, state); return state; }
public get(service: Queryable) { return this.itemStates.get(service); }
public remove(item: ServiceDataTableItemState) { this.itemStates.delete(item.tab.service); }}
export class ServiceDataTableItemState { readonly pageQuery = $state<InMemoryFeaturePageQuery>()!;
constructor(public readonly tab: ServiceDataTableTab) { this.pageQuery = new InMemoryFeaturePageQuery(this.tab.service, this.tab.allowSelection || false); }}packages/common/src/lib/widgets/service-data-table/service-data-table/in-memory-feature-page-query.svelte.ts
import type { ColumnFiltersState, PaginationState, RowSelectionState, SortingState } from '@tanstack/table-core';import { LogicalOperator, type QueryCondition, type QueryConditionGroup } from '$lib/api/tools';import type { ApiFeature } from '$lib/api/feature';import { defaultOperation } from '$lib/api/domain';import { type Queryable, queryState } from '$lib/api/utils';import { showToast } from '$lib/components/toast/toast.utils';import { getI18n } from '$lib/api/managers/i18n';
export class InMemoryFeaturePageQuery { pagination = $state<PaginationState>({ pageIndex: 0, pageSize: 10 }); sorting = $state<SortingState>([]); filters = $state<ColumnFiltersState>([]); selection = $state<RowSelectionState>({});
defaultFilter = { operator: LogicalOperator.AND, conditions: [], };
additionalFilter = $state<QueryConditionGroup>(this.defaultFilter);
spatialFilter = $state<ApiFeature>();
allResults = $derived.by(() => { return this.#query?.data ?? []; });
i18n = getI18n();
constructor( private readonly service: Queryable, private readonly allowSelection: boolean, ) {}
readonly #filtersAsCondition = $derived.by<QueryConditionGroup>(() => { const columnFilters = this.filters; const conditions = columnFilters.map((filter) => { const field = this.getField(filter.id); const operation = field.defaultOperation ?? defaultOperation(field.type); return { value: filter.value as string | number, field: field, operation, } satisfies QueryCondition; });
return { operator: LogicalOperator.AND, conditions, }; });
readonly #mergedCondition = $derived.by<QueryConditionGroup>(() => { const filters = this.#filtersAsCondition; const additional = this.additionalFilter; if (additional) { return { operator: LogicalOperator.AND, conditions: [filters, additional], }; } else { return filters; } });
readonly #query = queryState({ inputFn: () => ({ condition: this.#mergedCondition, sorting: this.sorting, spatialFilter: this.spatialFilter }), queryFn: async ({ condition, sorting, spatialFilter, signal }) => { const sort = sorting.map((s) => ({ field: s.id, order: s.desc ? 'desc' : 'asc' }) as const); const queryResponse = await this.service.queryWithMetadata({ conditionGroup: condition, feature: spatialFilter, sort, signal, }); const features = queryResponse.features; if (queryResponse.maxResultCountReached) { showToast({ level: 'warning', message: this.i18n('common.max-results-number-reached', { maxElements: features.length }), }); } return features; }, });
readonly loading = $derived(this.#query.loading); readonly rowCount = $derived.by(() => this.#query.data?.length ?? 0);
readonly content = $derived.by(() => { const features = this.#query.data ?? []; if (this.manualPagination) { const index = this.pagination.pageIndex; const size = this.pagination.pageSize;
if (features.length < size) { return features; }
return features.slice(index * size, index * size + size); } else { return features; } });
private getField(key: string) { return this.service.fields.find((f) => f.key === key)!; }
public onPaginationChange(state: PaginationState) { this.pagination = state; }
public onSortingChange(state: SortingState) { this.sorting = state; }
public onFiltersChange(state: ColumnFiltersState) { this.filters = state; this.pagination.pageIndex = 0; }
public onSelectionChange(selection: RowSelectionState) { this.selection = selection; }
public resetSelection() { this.selection = {}; }
public selectAll() { this.selection = this.content.reduce((result, element) => Object.assign(result, { [element.id]: true }), {}); }
public get featuresSelection() { return this.content.filter((f) => this.selection[f.id]); }
public get manualPagination() { return !this.allowSelection; }
public reset() { this.additionalFilter = this.defaultFilter; this.filters = []; this.spatialFilter = undefined; }
public get selectionSize() { return this.selection ? Object.entries(this.selection).filter(([_, selected]) => selected).length : 0; }}packages/common/src/lib/widgets/service-data-table/service-data-table/service-data-table.model.ts
import { type ApiFieldDescription, ApiFieldType } from '$lib/api/domain';import type { ApiFeature } from '$lib/api/feature';import type { ColumnDef } from '@tanstack/table-core';import type { I18nManager } from '$lib/api/managers/i18n';import { renderComponent } from '$lib/components/shadcn/ui/data-table';import { Checkbox } from '$lib/components/shadcn/ui/checkbox';import type { ComponentProps } from 'svelte';import type ServiceDataTableTabContent from '$lib/widgets/service-data-table/ServiceDataTableTabContent.svelte';import { isMapService, type Queryable } from '$lib/api/utils';import { RenderComponentConfig } from '$lib/components/shadcn/ui/data-table/render-helpers';import DisplayAttribute from '$lib/components/display-attribute/DisplayAttribute.svelte';import type { InMemoryFeaturePageQuery } from './in-memory-feature-page-query.svelte';import type { ApiMapService } from '$lib/api/mapservices';import { type ApiSublayer, getTopParent } from '$lib/api/layers';
export type ServiceDataTableTab = Partial<ComponentProps<typeof ServiceDataTableTabContent>> & { tabName: string; service: Queryable; tabId: string;};
export function getTableId(queryable: ApiMapService | ApiSublayer): string { if (isMapService(queryable)) { return queryable.id; } else { const parent = getTopParent(queryable); return `${parent.id}-${queryable.id}`; }}
export function mapToColumns( fields: ApiFieldDescription[], i18nManager: I18nManager, allowSelection: boolean, pageQuery: InMemoryFeaturePageQuery,): ColumnDef<ApiFeature>[] { const result: ColumnDef<ApiFeature>[] = []; if (allowSelection) { result.push({ id: 'select', header: ({ table }) => renderComponent(Checkbox, { checked: table.getIsAllPageRowsSelected(), indeterminate: table.getIsSomePageRowsSelected() && !table.getIsAllPageRowsSelected(), onCheckedChange: (value) => { if (value) { const selection = pageQuery.allResults.reduce( (acc, current) => { acc[current.id] = true; return acc; }, {} as Record<string, boolean>, ); table.setRowSelection(() => selection); } else { table.setRowSelection(() => ({})); } }, 'aria-label': 'Select all', }), cell: ({ row, table }) => renderComponent(Checkbox, { checked: row.getIsSelected(), onCheckedChange: (value) => { table.setRowSelection((old) => { if (!value) { // eslint-disable-next-line @typescript-eslint/no-dynamic-delete delete old[row.id]; } else { old[row.id] = true; } return old; }); }, 'aria-label': 'Select row', }), enableSorting: false, enableColumnFilter: false, enableHiding: false, }); } for (const field of fields) { // Skip geometry columns if (field.type === ApiFieldType.GEOMETRY) { continue; } result.push({ accessorKey: field.key, accessorFn: (feature) => feature.attributes?.[field.key], header: field.label, cell: ({ row }) => { const value = row.getValue(field.key); if (value && field.type === ApiFieldType.TIMESTAMP) { const date = new Date(Number(value)); return i18nManager.datetimeFormatter.format(date); } if (value && field.type === ApiFieldType.DATE) { const date = new Date(Number(value)); return i18nManager.dateFormatter.format(date); } if (value && typeof value === 'string') { return new RenderComponentConfig(DisplayAttribute, { field, value }); } return value; }, }); } return result;}packages/common/src/lib/widgets/service-data-table/service-data-table/ServiceDataTable.svelte
<script lang="ts"> import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '$lib/components/shadcn/ui/table'; import { mapToColumns } from './service-data-table.model'; import { getMapManager } from '$lib/api/map'; import type { ApiFeature } from '$lib/api/feature'; import { createSvelteTable, FlexRender } from '$lib/components/shadcn/ui/data-table'; import { getCoreRowModel, getFilteredRowModel, getPaginationRowModel } from '@tanstack/table-core'; import Pagination from '$lib/components/pagination/Pagination.svelte'; import TableSortHeader from '$lib/components/api-data-table/TableSortHeader.svelte'; import { getI18n, getI18nManager } from '$lib/api/managers/i18n'; import { ApiSelect } from '$lib/components/api-select'; import DynamicInput from '$lib/components/dynamic-input/DynamicInput.svelte'; import type { InMemoryFeaturePageQuery } from './in-memory-feature-page-query.svelte'; import Loader from '$lib/components/common/Loader.svelte'; import type { ApiFieldDescription } from '$lib/api/domain'; import { cn } from '$lib/components/shadcn/utils'; import type { ServiceDataTableI18n } from '$lib/widgets/service-data-table/service-data-table.config';
interface Props { fields: ApiFieldDescription[]; pageIndex: number; pageSize: number; pageQuery: InMemoryFeaturePageQuery; i18n: ServiceDataTableI18n; allowSelection?: boolean; highlightOnOver?: boolean; zoomOnHover?: boolean; zoomOnSelect?: boolean; onSelected?: (feature: ApiFeature) => void; class?: string; }
let { fields, pageIndex = $bindable(0), pageSize = $bindable(10), pageQuery, i18n: i18nConfig, allowSelection = false, highlightOnOver = true, zoomOnHover = false, zoomOnSelect = true, onSelected, class: className, }: Props = $props();
const mapManager = getMapManager(); const highlightManager = mapManager.tools.highlight; const i18nManager = getI18nManager(); const i18n = getI18n(i18nConfig);
let currentFeature: ApiFeature | undefined;
function highlightFeature(feature: ApiFeature): void { if (highlightOnOver) { if (currentFeature) { unhighlightFeature(currentFeature); } currentFeature = feature; highlightManager.highlightFeature(feature); if (zoomOnHover) { mapManager.tools.zoom.zoomToFeature(feature); } } }
function unhighlightFeature(feature: ApiFeature | undefined): void { if (highlightOnOver) { const featureToRemove = feature ?? currentFeature; if (featureToRemove) { highlightManager.unhighlightFeature(featureToRemove); currentFeature = undefined; } } }
function selectFeature(feature: ApiFeature): void { if (zoomOnSelect) { mapManager.tools.zoom.zoomToFeature(feature); } onSelected?.(feature); }
const columns = $derived(mapToColumns(fields, i18nManager, allowSelection, pageQuery));
const table = createSvelteTable({ get data() { return pageQuery.content; }, get columns() { return columns; }, state: { get pagination() { return pageQuery.pagination; }, get sorting() { return pageQuery.sorting; }, get columnFilters() { return pageQuery.filters; }, get rowSelection() { return pageQuery.selection; }, }, onSortingChange: (updater) => { if (typeof updater === 'function') { pageQuery.onSortingChange(updater(pageQuery.sorting)); } else { pageQuery.onSortingChange(updater); } }, onPaginationChange: (updater) => { if (typeof updater === 'function') { pageQuery.onPaginationChange(updater(pageQuery.pagination)); } else { pageQuery.onPaginationChange(updater); } }, manualFiltering: true, onColumnFiltersChange: (updater) => { if (typeof updater === 'function') { pageQuery.onFiltersChange(updater(pageQuery.filters)); } else { pageQuery.onFiltersChange(updater); } }, onRowSelectionChange: (updater) => { if (typeof updater === 'function') { pageQuery.onSelectionChange(updater(pageQuery.selection)); } else { pageQuery.onSelectionChange(updater); } }, getRowId: (row) => row.id, getCoreRowModel: getCoreRowModel(), getFilteredRowModel: getFilteredRowModel(), getPaginationRowModel: getPaginationRowModel(), manualPagination: pageQuery.manualPagination, get rowCount() { return pageQuery.rowCount; }, }); const pageState = $derived({ pageIndex: pageIndex, pageSize: pageSize, first: pageIndex * pageSize + 1, last: pageIndex * pageSize + pageSize, total: pageQuery.rowCount, }); const selectionState = $derived({ rows: pageQuery.selectionSize, totalRows: pageQuery.rowCount, });
const pageSizeOptions = [10, 20, 50].map((v) => ({ label: v.toString(), value: v }));
function getField(key: string) { return fields.find((f) => f.key === key)!; }</script>
<div class={cn('gv-flex gv-flex-col gv-relative', className)}> {#if pageQuery.loading} <div class="gv-absolute gv-w-full gv-h-[calc(100%-1px)] gv-flex gv-justify-center gv-items-center gv-bg-muted/40 gv-z-10" > <Loader /> </div> {/if} <Table class="gv-flex-1 gv-overflow-auto gv-border-collapse gv-w-full" data-loading={pageQuery.loading}> <TableHeader> {#each table.getHeaderGroups() as headerGroup (headerGroup.id)} <TableRow class="gv-sticky gv-top-0 gv-z-[1] gv-bg-muted-600 hover:gv-bg-muted-600 gv-shadow-3lg"> {#each headerGroup.headers as header (header.id)} <TableHead class="gv-px-0.5 gv-py-0.5 first:gv-pl-3 last:gv-pr-3"> {#if !header.isPlaceholder} {#if header.column.getCanSort()} <TableSortHeader sorted={header.column.getIsSorted()} onclick={header.column.getToggleSortingHandler()} class="gv-text-sm gv-justify-start gv-font-bold gv-text-black gv-w-full gv-px-sm gv-py-xs gv-h-fit" data-test-id={`Service-Datatable-${header.id}-Sort`} > <FlexRender content={header.column.columnDef.header} context={header.getContext()} /> </TableSortHeader> {:else} <FlexRender content={header.column.columnDef.header} context={header.getContext()} /> {/if} {#if header.column.getCanFilter()} {@const field = getField(header.column.id)} {@const filterValue = header.column.getFilterValue() ?? ''} <DynamicInput {field} size="sm" bind:value={ () => filterValue, (v) => header.column.setFilterValue(v?.toString()) } debounce={500} class="gv-border-muted-300 focus-visible:gv-ring-0 gv-px-2.5 gv-py-1.5" data-test-id={`Service-Datatable-${header.id}-Filter`} /> {/if} {/if} </TableHead> {/each} </TableRow> {/each} </TableHeader> <TableBody> {#each table.getRowModel().rows as row (row.id)} <TableRow data-state={row.getIsSelected() && 'selected'} class="gv-cursor-pointer" onmouseover={() => highlightFeature(row.original)} onmouseleave={() => unhighlightFeature(row.original)} onclick={() => selectFeature(row.original)} > {#each row.getVisibleCells() as cell (cell.id)} <TableCell class="first:gv-pl-3"> <FlexRender content={cell.column.columnDef.cell} context={cell.getContext()} /> </TableCell> {/each} </TableRow> {:else} <TableRow> <TableCell colspan={columns.length} class="gv-h-24 gv-text-center">{i18n('no-results')}</TableCell> </TableRow> {/each} </TableBody> </Table> <footer class="gv-w-full gv-bg-muted-400 gv-p-2.5 gv-flex gv-relative gv-items-center gv-justify-between gv-text-sm gv-font-bold" > <span> {i18n('table-page-state', pageState)} {#if allowSelection} | {i18n('table-selection-state', selectionState)} {/if} </span> <div class="gv-absolute -gv-translate-x-1/2 gv-start-1/2"> <Pagination limitButtons count={pageState.total === 0 ? 1 : pageState.total} perPage={pageState.pageSize} bind:page={pageIndex} onPageChange={() => table.setPageIndex(pageIndex)} size="sm" /> </div> <div class="gv-flex gv-items-center gv-gap-2"> <span>{i18n('table-page-size')} :</span> <ApiSelect bind:value={pageSize} options={pageSizeOptions} variant="ghost" size="sm" class="gv-w-12 gv-px-1" itemClass="gv-justify-center gv-px-0" indicator={false} /> </div> </footer></div>packages/common/src/lib/widgets/service-data-table/ServiceDataTableTabContent.svelte
<script lang="ts"> import { cn } from '$lib/components/shadcn/utils'; import { getI18n } from '$lib/api/managers/i18n'; import ExportDialog from '$lib/components/common/ExportDialog.svelte'; import { Button } from '$lib/components/shadcn/ui/button'; import SlidersHorizontal from 'lucide-svelte/icons/sliders-horizontal'; import ServiceDataTable from './service-data-table/ServiceDataTable.svelte'; import { debounceState, type Queryable } from '$lib/api/utils'; import AdvancedFiltersDialog from './advanced-filters/AdvancedFiltersDialog.svelte'; import QuickDrawSelector from '$lib/widgets/service-data-table/quick-draw-selector/QuickDrawSelector.svelte'; import { ExternalLink } from 'lucide-svelte'; import type { ApiFeature } from '$lib/api/feature'; import { Checkbox } from '$lib/components/shadcn/ui/checkbox'; import { Label } from '$lib/components/shadcn/ui/label'; import { getMapManager } from '$lib/api/map'; import { onDestroy } from 'svelte'; import type { ApiExtent } from '$lib/api/domain/api-extent.schema'; import MagicalSvelteRenderer from '$lib/components/magical-svelte-renderer/MagicalSvelteRenderer.svelte'; import type { AnyRenderConfig } from '$lib/components/magical-svelte-renderer/magical-svelte-renderer-helpers'; import type { ServiceDataTableI18n } from '$lib/widgets/service-data-table/service-data-table.config'; import type { ServiceDataTableItemState, ServiceDataTableState, } from '$lib/widgets/service-data-table/service-data-table.state.svelte'; import { showToast } from '$lib/components/toast/toast.utils';
interface Props { name: string; service: Queryable; i18n: ServiceDataTableI18n; itemState: ServiceDataTableItemState; widgetState: ServiceDataTableState; allowExport: boolean; allowAdvancedFilters: boolean; allowSpatialFilter: boolean; defaultSpatialFilter?: ApiFeature; zoomOnSelect: boolean; highlightOnOver: boolean; zoomOnHover: boolean; class?: string | undefined | boolean; additionalHeaderContent?: AnyRenderConfig[]; allowAdvancedFiltersReset?: boolean; allowSpatialFilterReset?: boolean; allowExtentFilter?: boolean; allowResetAll?: boolean; disableAllFilters?: boolean; allowSelection?: boolean; addDefaultConditionIfEmpty?: boolean; }
let { name, service, i18n: i18nConfig, itemState, widgetState, allowExport, allowAdvancedFilters, allowSpatialFilter, defaultSpatialFilter, zoomOnSelect, highlightOnOver, zoomOnHover, class: className, additionalHeaderContent, allowAdvancedFiltersReset = true, allowSpatialFilterReset = true, allowExtentFilter = true, allowResetAll = true, disableAllFilters = false, allowSelection = false, addDefaultConditionIfEmpty = true, }: Props = $props();
const i18n = getI18n(i18nConfig); const mapManager = getMapManager(); const featureFactory = mapManager.tools.featureFactory;
const pageQuery = itemState.pageQuery;
let quickDrawSelectorFeature = $state<ApiFeature | undefined>();
let exportDialogOpen = $state<boolean>(false); let filterOnMapExtent = $state<boolean>(false); let currentMapExtent = $state<ApiExtent>(mapManager.getMapExtent()); const unsub = mapManager.tools.events.watch('EXTENT', (extent) => { currentMapExtent = extent; }); const debouncedCurrentMapExtent = debounceState(() => currentMapExtent, 500); const mapExtentFeature = $derived.by(() => { const extent = debouncedCurrentMapExtent.debounced; if (!filterOnMapExtent || !extent) { return; } const coords = [ [extent.xmin, extent.ymin], [extent.xmax, extent.ymin], [extent.xmax, extent.ymax], [extent.xmin, extent.ymax], [extent.xmin, extent.ymin], ]; return featureFactory.createPolygon({ wkid: extent.wkid, coords: [coords], }); });
$effect(() => { if (defaultSpatialFilter) { pageQuery.spatialFilter = defaultSpatialFilter; } else if (quickDrawSelectorFeature) { pageQuery.spatialFilter = quickDrawSelectorFeature; } else if (mapExtentFeature) { pageQuery.spatialFilter = mapExtentFeature; } else { pageQuery.spatialFilter = undefined; } });
function resetAllFilters() { pageQuery.reset(); resetSpatialFilter(); filterOnMapExtent = false; }
function resetSpatialFilter() { quickDrawSelectorFeature = undefined; }
function openExportDialog() { if (pageQuery.allResults.length === 0) { showToast({ level: 'warning', message: i18n('nothing-to-export') }); return; } exportDialogOpen = true; }
onDestroy(() => { unsub(); widgetState.remove(itemState); });</script>
<div class={cn( 'gv-p-5 gv-flex-1 gv-overflow-auto gv-flex gv-flex-col gv-gap-5 gv-bg-background gv-pointer-events-auto', className, )}> <div class="gv-flex gv-justify-between"> <div class="gv-flex gv-items-center gv-gap-2.5"> {#if additionalHeaderContent} {#each additionalHeaderContent as content} <MagicalSvelteRenderer {content} /> {/each} {/if} {#if allowSelection} <Button variant="outline" class="gv-border-primary/70" data-test-id="Service-Datatable-Select-All" onclick={() => pageQuery.selectAll()} size="sm" > {i18n('select-all')} </Button> <Button variant="outline" class="gv-border-primary/70" data-test-id="Service-Datatable-Empty-Selection" onclick={() => pageQuery.resetSelection()} size="sm" > {i18n('reset-selection')} </Button> {/if} </div> <div class="gv-flex gv-items-center gv-gap-2.5"> {#if !disableAllFilters && allowExtentFilter} <Checkbox data-test-id="Service-Datatable-filter-on-map-extent-checkbox" bind:checked={filterOnMapExtent} id="filterOnMapExtent" /> <Label for="filterOnMapExtent">{i18n('filter-on-map-extent')}</Label> {/if} {#if !disableAllFilters && allowSpatialFilter} <QuickDrawSelector {i18nConfig} bind:feature={quickDrawSelectorFeature} /> {#if allowSpatialFilterReset} <Button data-test-id="Service-Datatable-reset-spatial-filter-button" onclick={() => resetSpatialFilter()} class="gv-text-foreground" variant="link" size="sm" > {i18n('reset-spatial-filter')} </Button> {/if} {/if} {#if !disableAllFilters && allowAdvancedFilters} <AdvancedFiltersDialog title={i18n('advanced-filters')} bind:condition={pageQuery.additionalFilter} fields={service.fields} allowGroups allowReset={allowAdvancedFiltersReset} {addDefaultConditionIfEmpty} > <Button variant="outline" class="gv-border-primary/70" data-test-id="Service-Datatable-open-advanced-filters" size="sm" > {i18n('advanced-filters')} <SlidersHorizontal class="gv-size-4" /> </Button> </AdvancedFiltersDialog> {/if}
{#if !disableAllFilters && allowResetAll} <Button data-test-id="Service-Datatable-reset-button" onclick={() => resetAllFilters()} class="gv-text-foreground" variant="link" size="sm" > {i18n('reset-all')} </Button> {/if}
{#if allowExport} <Button onclick={openExportDialog} variant="outline" class="gv-border-primary/70" data-test-id="Service-Datatable-open-export" size="sm" > {i18n('common.export')} <ExternalLink class="gv-size-4" /> </Button> <ExportDialog bind:open={exportDialogOpen} exportFileName={name} features={pageQuery.allResults} /> {/if} </div> </div>
<ServiceDataTable {zoomOnSelect} {highlightOnOver} {zoomOnHover} {pageQuery} {allowSelection} bind:pageIndex={pageQuery.pagination.pageIndex} bind:pageSize={pageQuery.pagination.pageSize} fields={service.fields} i18n={i18nConfig} class="gv-size-full gv-overflow-auto" /></div>packages/common/src/lib/widgets/service-data-table/ServiceDataTableWidget.svelte
<script lang="ts"> import { getI18n } from '$lib/api/managers/i18n'; import { KeyboardEventKey } from '$lib/api/utils'; import { cn } from '$lib/components/shadcn/utils'; import X from 'lucide-svelte/icons/x'; import { SvelteMap } from 'svelte/reactivity'; import ServiceDataTableTabContent from './ServiceDataTableTabContent.svelte'; import type { ServiceDataTableTab } from './service-data-table/service-data-table.model'; import type { ServiceDataTableProps } from './service-data-table.declaration';
const { fullConfig, reference, state: widgetState }: ServiceDataTableProps = $props();
const { config } = fullConfig; const { zoomOnSelect, highlightOnOver, zoomOnHover, allowExport, allowAdvancedFilters, allowSpatialFilter, allowAdvancedFiltersReset, allowSpatialFilterReset, } = config; const i18n = getI18n(fullConfig.i18n);
let activePanel = $state<string>(); const resultsTabs = $state(new SvelteMap<string, ServiceDataTableTab>());
function focusSearch(searchId: string) { activePanel = searchId; }
export function closePanel(tabId: string, event?: Event) { if (event) { event.stopPropagation(); } resultsTabs.delete(tabId); if (resultsTabs.size > 0 && tabId == activePanel) { const tab = resultsTabs.keys().next().value; if (tab) { focusSearch(tab); } }
// TODO This is a workaround to close the widget when there are no more results if (resultsTabs.size == 0) { reference.deactivate(); } }
export function closeAllPanelsForServiceId(serviceId: string): void { Array.from(resultsTabs.keys()) .filter((panelId) => panelId === serviceId || panelId.startsWith(`${serviceId}-`)) .forEach((panelIdToClose) => closePanel(panelIdToClose)); }
export function closeAllPanels() { for (const key of resultsTabs.keys()) { resultsTabs.delete(key); } }
export function openDataTable(tab: ServiceDataTableTab) { resultsTabs.set(tab.tabId, tab); activePanel = tab.tabId; }
export function openDataTables(tabs: ServiceDataTableTab[]) { tabs.forEach((tab) => resultsTabs.set(tab.tabId, tab)); activePanel = tabs[0].tabId; }
function onTabKeydown(event: KeyboardEvent, tabName: string) { if (event.key === KeyboardEventKey.Space || event.key === KeyboardEventKey.Enter) { focusSearch(tabName); } }</script>
<div class="gv-size-full gv-overflow-auto gv-flex gv-flex-col"> <ul class="gv-m-0 gv-flex gv-flex-wrap gv-pl-0 gv-mb-0 gv-list-none gv-border-b gv-border-b-muted gv-absolute gv-top-[-1.60rem]" > {#each Array.from(resultsTabs.keys()) as tabId (tabId)} {@const tabResult = resultsTabs.get(tabId)} <li class={cn('gv-flex gv-mb-[-1px]')}> <div title={tabResult?.tabName} class={cn( 'gv-border gv-rounded-t-md gv-block gv-p-2 gv-pl-4 gv-cursor-pointer', 'gv-max-w-[200px] gv-whitespace-nowrap gv-overflow-hidden gv-flex gv-items-center gv-justify-between', tabId === activePanel ? 'gv-text-grey-800 gv-bg-background gv-border-muted gv-border-b-transparent' : 'gv-border-transparent gv-bg-grey-100 gv-text-grey-500/60 hover:gv-border-grey-200', )} role="button" tabindex="0" onclick={() => focusSearch(tabId)} onkeydown={(e) => onTabKeydown(e, tabId)} data-test-id={`Service-Datatable-tab-${tabId}`} > <span class="gv-mr-3 gv-truncate">{tabResult?.tabName}</span> <button onclick={(evt) => closePanel(tabId, evt)} title={i18n('common.close')}> <X class="gv-size-4" /> </button> </div> </li> {/each} </ul> {#each Array.from(resultsTabs.values()) as tab (tab)} {@const itemState = widgetState.add(tab)} <ServiceDataTableTabContent name={tab.tabName} i18n={fullConfig.i18n} {widgetState} {itemState} {allowExport} {allowAdvancedFilters} {allowSpatialFilter} {zoomOnSelect} {highlightOnOver} {zoomOnHover} {allowAdvancedFiltersReset} {allowSpatialFilterReset} class={activePanel !== tab.tabId && 'gv-hidden'} {...tab} /> {/each}</div>