diff --git a/packages/documentation/data/menu.ts b/packages/documentation/data/menu.ts index ef58de8474..cd0cdc6e9d 100644 --- a/packages/documentation/data/menu.ts +++ b/packages/documentation/data/menu.ts @@ -22,6 +22,7 @@ import { KtNavbar, KtPagination, KtPopover, + KtStandardTable, KtTable, KtTableLegacy, KtTag, @@ -174,6 +175,7 @@ export const menu: Array
= [ makeComponentMenuItem(KtModal), makeComponentMenuItem(KtPagination), makeComponentMenuItem(KtPopover), + makeComponentMenuItem(KtStandardTable), makeComponentMenuItem(KtTable), makeComponentMenuItem(KtTableLegacy), makeComponentMenuItem(KtTag), diff --git a/packages/documentation/data/standard-table.ts b/packages/documentation/data/standard-table.ts new file mode 100644 index 0000000000..e8936987a9 --- /dev/null +++ b/packages/documentation/data/standard-table.ts @@ -0,0 +1,98 @@ +export const todos = [ + { + completed: true, + id: 1, + todo: 'Watch a classic movie', + userId: 68, + }, + { + completed: false, + id: 2, + todo: 'Contribute code or a monetary donation to an open-source software project', + userId: 69, + }, + { + completed: false, + id: 3, + todo: 'Invite some friends over for a game night', + userId: 104, + }, + { + completed: true, + id: 4, + todo: "Text a friend you haven't talked to in a long time", + userId: 2, + }, + { + completed: true, + id: 5, + todo: "Plan a vacation you've always wanted to take", + userId: 162, + }, + { + completed: false, + id: 6, + todo: 'Clean out car', + userId: 71, + }, + { + completed: true, + id: 7, + todo: 'Create a cookbook with favorite recipes', + userId: 53, + }, + { + completed: false, + id: 8, + todo: 'Create a compost pile', + userId: 13, + }, + { + completed: true, + id: 9, + todo: 'Take a hike at a local park', + userId: 37, + }, + { + completed: true, + id: 10, + todo: 'Take a class at local community center that interests you', + userId: 65, + }, + { + completed: true, + id: 11, + todo: 'Research a topic interested in', + userId: 130, + }, + { + completed: false, + id: 12, + todo: 'Plan a trip to another country', + userId: 140, + }, + { + completed: false, + id: 13, + todo: 'Improve touch typing', + userId: 178, + }, + { + completed: false, + id: 14, + todo: 'Learn Express.js', + userId: 194, + }, + { + completed: false, + id: 15, + todo: 'Learn calligraphy', + userId: 80, + }, + { + completed: true, + id: 16, + todo: 'Go to the gym', + userId: 142, + }, +] diff --git a/packages/documentation/pages/usage/components/form-fields.vue b/packages/documentation/pages/usage/components/form-fields.vue index 244c021c07..8e574bbc1b 100644 --- a/packages/documentation/pages/usage/components/form-fields.vue +++ b/packages/documentation/pages/usage/components/form-fields.vue @@ -1054,6 +1054,7 @@ const singleOrMultiSelectOptions: Kotti.FieldSingleSelect.Props['options'] = [ { label: 'Key 1', value: 'value1' }, { isDisabled: true, label: 'Key 3', value: 'value3' }, { label: 'Key 7', value: 'value7' }, + { label: 'Key 10', value: 'value10' }, { label: 'Key 4', value: 'value4' }, { label: 'Key 9', value: 'value9' }, { label: 'Key 6', value: 'value6' }, diff --git a/packages/documentation/pages/usage/components/standard-table.vue b/packages/documentation/pages/usage/components/standard-table.vue new file mode 100644 index 0000000000..598f2a0d81 --- /dev/null +++ b/packages/documentation/pages/usage/components/standard-table.vue @@ -0,0 +1,519 @@ + + + + + diff --git a/packages/kotti-ui/source/kotti-table/index.ts b/packages/kotti-ui/source/kotti-table/index.ts index 77c606d09b..7ce3e71242 100644 --- a/packages/kotti-ui/source/kotti-table/index.ts +++ b/packages/kotti-ui/source/kotti-table/index.ts @@ -2,16 +2,29 @@ import type { Kotti } from '../types' import { MetaDesignType } from '../types/kotti' import { attachMeta, makeInstallable } from '../utilities' +import KtStandardTableVue from './KtStandardTable.vue' import KtTableVue from './KtTable.vue' +import { KottiStandardTable } from './standard-table/types' import { KottiTable } from './table/types' +export { useKottiStandardTable } from './standard-table/hooks' +export { + type KottiStandardTableStorage, + LocalStorageAdapter, + type StorageOperationContext, +} from './standard-table/storage' export { createColumnContext, getCustomDisplay, getDisplay, } from './table/column-helper' export { useKottiTable } from './table/hooks' -export { getNumericalSorter, getTextSorter, useLocalSort } from './table/local' +export { + getDateSorter, + getNumericalSorter, + getTextSorter, + useLocalSort, +} from './table/local' const TABLE_META: Kotti.Meta = { addedVersion: '8.2.0', @@ -51,4 +64,40 @@ const TABLE_META: Kotti.Meta = { }, } +const STANDARD_META: Kotti.Meta = { + addedVersion: '7.4.0', + deprecated: null, + designs: { + type: MetaDesignType.FIGMA, + url: 'https://www.figma.com/design/0yFVivSWXgFf2ddEF92zkf/Kotti-Design-System?node-id=6305-10646&node-type=canvas&t=8lzEM5nlkrh8aUMF-0', + }, + slots: { + 'applied-filter-actions': { + description: 'slot next to the applied filters section', + scope: null, + }, + 'control-actions': { + description: 'slot next to the table controls section', + scope: null, + }, + 'header-actions': { + description: 'slot next to the table title', + scope: null, + }, + table: { + description: 'slot to show custom content instead of the KtTable', + scope: null, + }, + }, + typeScript: { + namespace: 'Kotti.StandardTable', + schema: KottiStandardTable.propsSchema, + }, +} + export const KtTable = attachMeta(makeInstallable(KtTableVue), TABLE_META) + +export const KtStandardTable = attachMeta( + makeInstallable(KtStandardTableVue), + STANDARD_META, +) diff --git a/packages/kotti-ui/source/kotti-table/standard-table/components/Columns.vue b/packages/kotti-ui/source/kotti-table/standard-table/components/Columns.vue new file mode 100644 index 0000000000..3e7185512b --- /dev/null +++ b/packages/kotti-ui/source/kotti-table/standard-table/components/Columns.vue @@ -0,0 +1,83 @@ + + + + + diff --git a/packages/kotti-ui/source/kotti-table/standard-table/components/FilterList.vue b/packages/kotti-ui/source/kotti-table/standard-table/components/FilterList.vue new file mode 100644 index 0000000000..b59f7c1eab --- /dev/null +++ b/packages/kotti-ui/source/kotti-table/standard-table/components/FilterList.vue @@ -0,0 +1,89 @@ + + + + + diff --git a/packages/kotti-ui/source/kotti-table/standard-table/components/Filters.vue b/packages/kotti-ui/source/kotti-table/standard-table/components/Filters.vue new file mode 100644 index 0000000000..b1ec075339 --- /dev/null +++ b/packages/kotti-ui/source/kotti-table/standard-table/components/Filters.vue @@ -0,0 +1,89 @@ + + + + + diff --git a/packages/kotti-ui/source/kotti-table/standard-table/components/PageSize.vue b/packages/kotti-ui/source/kotti-table/standard-table/components/PageSize.vue new file mode 100644 index 0000000000..c24292bf8d --- /dev/null +++ b/packages/kotti-ui/source/kotti-table/standard-table/components/PageSize.vue @@ -0,0 +1,55 @@ + + + + + diff --git a/packages/kotti-ui/source/kotti-table/standard-table/components/Pagination.vue b/packages/kotti-ui/source/kotti-table/standard-table/components/Pagination.vue new file mode 100644 index 0000000000..f0975a7a3b --- /dev/null +++ b/packages/kotti-ui/source/kotti-table/standard-table/components/Pagination.vue @@ -0,0 +1,73 @@ + + + + + diff --git a/packages/kotti-ui/source/kotti-table/standard-table/components/Search.vue b/packages/kotti-ui/source/kotti-table/standard-table/components/Search.vue new file mode 100644 index 0000000000..885c3b1818 --- /dev/null +++ b/packages/kotti-ui/source/kotti-table/standard-table/components/Search.vue @@ -0,0 +1,69 @@ + + + + + diff --git a/packages/kotti-ui/source/kotti-table/standard-table/components/filters/Boolean.vue b/packages/kotti-ui/source/kotti-table/standard-table/components/filters/Boolean.vue new file mode 100644 index 0000000000..8bc57e18ad --- /dev/null +++ b/packages/kotti-ui/source/kotti-table/standard-table/components/filters/Boolean.vue @@ -0,0 +1,55 @@ + + + diff --git a/packages/kotti-ui/source/kotti-table/standard-table/components/filters/DateRange.vue b/packages/kotti-ui/source/kotti-table/standard-table/components/filters/DateRange.vue new file mode 100644 index 0000000000..f2d1558b63 --- /dev/null +++ b/packages/kotti-ui/source/kotti-table/standard-table/components/filters/DateRange.vue @@ -0,0 +1,72 @@ + + + diff --git a/packages/kotti-ui/source/kotti-table/standard-table/components/filters/MultiSelect.vue b/packages/kotti-ui/source/kotti-table/standard-table/components/filters/MultiSelect.vue new file mode 100644 index 0000000000..fef84bd477 --- /dev/null +++ b/packages/kotti-ui/source/kotti-table/standard-table/components/filters/MultiSelect.vue @@ -0,0 +1,50 @@ + + + diff --git a/packages/kotti-ui/source/kotti-table/standard-table/components/filters/NumberRange.vue b/packages/kotti-ui/source/kotti-table/standard-table/components/filters/NumberRange.vue new file mode 100644 index 0000000000..d5930fea5f --- /dev/null +++ b/packages/kotti-ui/source/kotti-table/standard-table/components/filters/NumberRange.vue @@ -0,0 +1,161 @@ + + + + + diff --git a/packages/kotti-ui/source/kotti-table/standard-table/components/filters/SingleSelect.vue b/packages/kotti-ui/source/kotti-table/standard-table/components/filters/SingleSelect.vue new file mode 100644 index 0000000000..57fe6097af --- /dev/null +++ b/packages/kotti-ui/source/kotti-table/standard-table/components/filters/SingleSelect.vue @@ -0,0 +1,47 @@ + + + diff --git a/packages/kotti-ui/source/kotti-table/standard-table/context.ts b/packages/kotti-ui/source/kotti-table/standard-table/context.ts new file mode 100644 index 0000000000..936f2712f9 --- /dev/null +++ b/packages/kotti-ui/source/kotti-table/standard-table/context.ts @@ -0,0 +1,60 @@ +import { inject, provide, type Ref } from 'vue' + +import type { KottiFieldText } from '../../kotti-field-text/types' +import type { KottiTable } from '../table/types' + +import type { KottiStandardTable } from './types' + +export type StandardTableContext< + ROW extends KottiTable.AnyRow, + COLUMN_ID extends string = string, +> = Ref<{ + internal: { + appliedFilters: KottiStandardTable.AppliedFilter[] + columns: KottiTable.Column[] + filters: KottiStandardTable.FilterInternal[] + getFilter: ( + id: KottiStandardTable.FilterInternal['id'], + ) => KottiStandardTable.FilterInternal | null + isLoading: boolean + options?: KottiStandardTable.Options + pageSizeOptions: number[] + pagination: { pageIndex: number; pageSize: number } + rowCount: number + searchValue: KottiFieldText.Value + setAppliedFilters: (value: KottiStandardTable.AppliedFilter[]) => void + setPageIndex: (value: number) => void + setPageSize: (value: number) => void + setSearchValue: (value: KottiFieldText.Value) => void + } +}> + +const getStandardTableContextKey = (id: string): string => + `kt-standard-table-${id}` + +export const useProvideStandardTableContext = < + ROW extends KottiTable.AnyRow, + COLUMN_ID extends string, +>( + id: string, + standardTableContext: StandardTableContext, +): void => { + provide>( + getStandardTableContextKey(id), + standardTableContext, + ) +} + +export const useStandardTableContext = ( + id: string, +): StandardTableContext => { + const context = inject>( + getStandardTableContextKey(id), + ) + + if (!context) { + throw new Error(`KtStandardTable: could not find context for “${id}”`) + } + + return context +} diff --git a/packages/kotti-ui/source/kotti-table/standard-table/hooks.ts b/packages/kotti-ui/source/kotti-table/standard-table/hooks.ts new file mode 100644 index 0000000000..d35fddbe8d --- /dev/null +++ b/packages/kotti-ui/source/kotti-table/standard-table/hooks.ts @@ -0,0 +1,313 @@ +import isEqual from 'lodash/isEqual.js' +import isNil from 'lodash/isNil.js' +import type { Ref } from 'vue' +import { computed, onBeforeMount, ref, watch } from 'vue' +import { z } from 'zod' + +import type { KottiFieldText } from '../../kotti-field-text/types' +import type { KottiTableParameter } from '../table/hooks' +import { + paramsSchema as KottiTableHookParamsSchema, + useKottiTable, +} from '../table/hooks' +import type { KottiTable } from '../table/types' +import { useComputedRef } from '../table/use-computed-ref' + +import type { StandardTableContext } from './context' +import { useProvideStandardTableContext } from './context' +import type { StorageOperationContext } from './storage' +import { + DummyStorageAdapter, + type KottiStandardTableStorage, + serializableStateSchema, +} from './storage' +import { type FilterInfo, KottiStandardTable } from './types' + +const OPERATION_MAP = { + [KottiStandardTable.FilterType.BOOLEAN]: + KottiStandardTable.FilterOperation.Boolean.EQUAL, + [KottiStandardTable.FilterType.DATE_RANGE]: + KottiStandardTable.FilterOperation.DateRange.IN_RANGE, + [KottiStandardTable.FilterType.MULTI_SELECT]: + KottiStandardTable.FilterOperation.MultiEnum.ONE_OF, + [KottiStandardTable.FilterType.NUMBER_RANGE]: + KottiStandardTable.FilterOperation.NumberRange.IN_RANGE, + [KottiStandardTable.FilterType.SINGLE_SELECT]: + KottiStandardTable.FilterOperation.SingleEnum.EQUAL, +} + +type KottiStandardTableParameters< + ROW extends KottiTable.AnyRow, + COLUMN_ID extends string, +> = Ref<{ + filters?: KottiStandardTable.Filter[] + id: string + isLoading?: boolean + options?: KottiStandardTable.Options + paginationOptions: KottiStandardTable.Pagination + storageAdapter: KottiStandardTableStorage | null + table: Omit, 'id'> +}> + +const paramsSchema = z.object({ + filters: KottiStandardTable.filterSchema.array().default(() => []), + id: z.string(), + isLoading: z.boolean().default(false), + options: KottiStandardTable.optionsSchema.optional(), + paginationOptions: KottiStandardTable.paginationSchema, + /** + * Need to use z.any because there is currently no way in zod to ensure a class extends an interface. + */ + storageAdapter: z.any(), + table: KottiTableHookParamsSchema.omit({ + id: true, + }), +}) + +type KottiStandardTableHook< + ROW extends KottiTable.AnyRow, + COLUMN_ID extends string, +> = { + api: KottiStandardTable.Hook.Returns + context: StandardTableContext +} + +export const useKottiStandardTable = < + ROW extends KottiTable.AnyRow, + COLUMN_ID extends string, +>( + _params: KottiStandardTableParameters, +): KottiStandardTableHook => { + const params = computed(() => paramsSchema.parse(_params.value)) + + const filterInfo = computed>( + () => + new Map( + params.value.filters.map((filter) => { + const { id, operations, type } = filter + + return [id, { operations, type }] + }), + ), + ) + + const rowCount = computed(() => + params.value.paginationOptions.type === 'local' + ? params.value.table.data.length + : params.value.paginationOptions.rowCount, + ) + + // refs exposed on return/api + const searchValue = ref(null) + + // FIXME: This useComputedRef right now assumes that the filters provdided via params + // does not change. If a user would change it, it will lead to unintended behavior + const appliedFilters = useComputedRef({ + get: (value) => value, + set(value) { + return value.filter((filter) => { + const meta = filterInfo.value.get(filter.id) + if (!meta) return false + + switch (meta.type) { + case KottiStandardTable.FilterType.BOOLEAN: { + return KottiStandardTable.appliedBooleanSchema.safeParse(filter) + .success + } + case KottiStandardTable.FilterType.DATE_RANGE: { + return KottiStandardTable.appliedDateRangeSchema.safeParse(filter) + .success + } + case KottiStandardTable.FilterType.MULTI_SELECT: + return KottiStandardTable.appliedMultiEnumSchema.safeParse(filter) + .success + + case KottiStandardTable.FilterType.NUMBER_RANGE: + return KottiStandardTable.appliedNumberRangeSchema.safeParse(filter) + .success + + case KottiStandardTable.FilterType.SINGLE_SELECT: + return KottiStandardTable.appliedSingleEnumSchema.safeParse(filter) + .success + + default: + return false + } + }) + }, + value: ref( + (() => { + const filtersWithDefaultValue: KottiStandardTable.AppliedFilter[] = [] + + for (const filter of params.value.filters) { + if (isNil(filter.defaultValue)) continue + + const appliedFilter = { + id: filter.id, + operation: OPERATION_MAP[filter.type], + value: filter.defaultValue, + } as KottiStandardTable.AppliedFilter + + filtersWithDefaultValue.push(appliedFilter) + } + + return filtersWithDefaultValue + })(), + ), + }) + + // FIXME: This useComputedRef right now assumes that the pageSize provdided via params + // does not change. If a user would change it, it will lead to unintended behavior + const pagination = useComputedRef<{ pageIndex: number; pageSize: number }>({ + get: (value) => value, + set: ({ pageIndex, pageSize: _pageSize }) => { + const pageSize = (() => { + const { pageSizeOptions } = params.value.paginationOptions + if (pageSizeOptions.includes(_pageSize)) return _pageSize + + for (let i = pageSizeOptions.length - 1; i >= 0; i -= 1) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + if (pageSizeOptions[i]! < _pageSize) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + return pageSizeOptions[i]! + } + } + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + return pageSizeOptions[0]! + })() + const pageFirstRowIndex = pageIndex * pageSize + return { + pageIndex: + pageIndex < 0 || pageFirstRowIndex > rowCount.value ? 0 : pageIndex, + pageSize, + } + }, + value: ref({ + pageIndex: 0, + pageSize: params.value.paginationOptions.pageSize, + }), + }) + + const data = computed(() => { + if (params.value.paginationOptions.type === 'remote') + return params.value.table.data + + const sliceStart = pagination.value.pageIndex * pagination.value.pageSize + const sliceEnd = sliceStart + pagination.value.pageSize + return params.value.table.data.slice(sliceStart, sliceEnd) + }) + + const storageAdapter = computed( + () => params.value.storageAdapter ?? new DummyStorageAdapter(), + ) + + const tableHook = useKottiTable( + computed(() => ({ + ...(params.value.table as Omit< + KottiTableParameter, + 'id' + >), + data: data.value, + id: params.value.id, + })), + ) + + const storageOperationContext = computed( + (): StorageOperationContext => ({ + columnIds: params.value.table.columns.map((x) => x.id), + filterInfo: filterInfo.value, + }), + ) + + // eslint-disable-next-line @typescript-eslint/no-misused-promises + onBeforeMount(async () => { + const rawState = await storageAdapter.value.load( + storageOperationContext.value, + ) + + if (!rawState) { + return + } + + const state = serializableStateSchema.parse(rawState) + + tableHook.api.columnOrder.value = state.columnOrder as COLUMN_ID[] + tableHook.api.hiddenColumns.value = new Set( + state.hiddenColumns as COLUMN_ID[], + ) + tableHook.api.ordering.value = + state.ordering as KottiTable.Ordering[] + appliedFilters.value = state.appliedFilters + pagination.value = state.pagination + searchValue.value = state.searchValue + }) + + const standardTableContext: StandardTableContext = computed( + () => ({ + internal: { + appliedFilters: appliedFilters.value, + columns: _params.value.table.columns, + filters: params.value.filters, + getFilter: (id) => + params.value.filters.find((filter) => filter.id === id) ?? null, + isLoading: params.value.isLoading, + options: params.value.options, + pageSizeOptions: params.value.paginationOptions.pageSizeOptions, + pagination: pagination.value, + rowCount: rowCount.value, + searchValue: searchValue.value, + setAppliedFilters: (filters: KottiStandardTable.AppliedFilter[]) => { + appliedFilters.value = filters + }, + setPageIndex: (pageIndex: number) => { + pagination.value = { + ...pagination.value, + pageIndex, + } + }, + setPageSize: (pageSize: number) => { + pagination.value = { + pageIndex: 0, + pageSize, + } + }, + setSearchValue: (search: KottiFieldText.Value) => { + searchValue.value = search + }, + }, + }), + ) + useProvideStandardTableContext(params.value.id, standardTableContext) + + watch( + computed(() => ({ + appliedFilters: appliedFilters.value, + columnOrder: tableHook.api.columnOrder.value, + hiddenColumns: tableHook.api.hiddenColumns.value, + ordering: tableHook.api.ordering.value, + pagination: pagination.value, + searchValue: searchValue.value, + })), + async (newState, oldState) => { + if (isEqual(newState, oldState)) return + + await storageAdapter.value.save( + { + ...newState, + hiddenColumns: Array.from(newState.hiddenColumns), + }, + storageOperationContext.value, + ) + }, + ) + + return { + api: { + ...tableHook.api, + appliedFilters, + pagination, + searchValue, + }, + context: standardTableContext, + } +} diff --git a/packages/kotti-ui/source/kotti-table/standard-table/simple-hash.test.ts b/packages/kotti-ui/source/kotti-table/standard-table/simple-hash.test.ts new file mode 100644 index 0000000000..ef32483402 --- /dev/null +++ b/packages/kotti-ui/source/kotti-table/standard-table/simple-hash.test.ts @@ -0,0 +1,53 @@ +import { describe, expect, it } from 'vitest' + +import { simpleHash } from './simple-hash' + +describe('simpleHash', () => { + it('works for basic inputs', () => { + expect(simpleHash(['a', 'b', 'c'])).toMatchInlineSnapshot(`"3ZQ=="`) + expect(simpleHash(['abc', 'def', 'cia'])).toMatchInlineSnapshot(`"3ZWFr"`) + expect(simpleHash(['a', 'abc'])).toMatchInlineSnapshot(`"2BmVm"`) + }) + + it('works for lots of columns', () => { + expect( + simpleHash([ + 'columns', + 'disableRow', + 'emptyText', + 'expandMultiple', + 'filteredColumns', + 'headerClass', + 'hiddenColumns', + 'id', + 'isInteractive', + 'isScrollable', + 'isSelectable', + 'loading', + 'orderedColumns', + 'remoteSort', + 'renderActions', + 'renderEmpty', + 'renderExpand', + 'renderLoading', + 'rowKey', + 'rows', + 'selected', + 'sortMultiple', + 'sortable', + 'sortedColumns', + 'tdClasses', + 'thClasses', + 'trClasses', + 'useColumnDragToOrder', + 'useQuickSortControl', + ]), + ).toMatchInlineSnapshot(`"29i4gT87ugxwQ0NQjx0WWA5AP6BoY="`) + }) + + it('is reasonably collision resistant', () => { + expect(simpleHash(['foo', 'foobaz'])).not.toEqual( + simpleHash(['bar', 'barbaz']), + ) + }) +}) diff --git a/packages/kotti-ui/source/kotti-table/standard-table/simple-hash.ts b/packages/kotti-ui/source/kotti-table/standard-table/simple-hash.ts new file mode 100644 index 0000000000..d0db9c3625 --- /dev/null +++ b/packages/kotti-ui/source/kotti-table/standard-table/simple-hash.ts @@ -0,0 +1,22 @@ +/** + * Simple low-effort hashsum + */ +export const simpleHash = (strings: string[]): string => { + const maximumLength = strings.reduce( + (acc, next) => Math.max(acc, next.length), + 0, + ) + + const result = new window.Uint8Array(maximumLength) + + for (const string of [...strings].sort()) { + for (let charIndex = 0; charIndex < string.length; charIndex++) { + // string.length is added to make collisions less likely (e.g. foo + fooBar = baz + bazBar without this) + result[charIndex] ^= string.charCodeAt(charIndex) + string.length + } + } + + const simpleHashedValue = window.btoa(String.fromCharCode(...result)) + + return `${String(strings.length)}${simpleHashedValue}` +} diff --git a/packages/kotti-ui/source/kotti-table/standard-table/storage.ts b/packages/kotti-ui/source/kotti-table/standard-table/storage.ts new file mode 100644 index 0000000000..f9b68007bb --- /dev/null +++ b/packages/kotti-ui/source/kotti-table/standard-table/storage.ts @@ -0,0 +1,135 @@ +import { z } from 'zod' + +import { simpleHash } from './simple-hash' +import type { FilterInfo } from './types' +import { KottiStandardTable } from './types' + +export const serializableStateSchema = z + .object({ + appliedFilters: z.array(KottiStandardTable.appliedFilterSchema), + columnOrder: z.array(z.string()), + hiddenColumns: z.array(z.string()), + ordering: z.array( + z + .object({ + id: z.string(), + value: z.enum(['ascending', 'descending']), + }) + .strict(), + ), + pagination: z + .object({ + pageIndex: z.number(), + pageSize: z.number(), + }) + .strict(), + searchValue: z.string().nullable(), + }) + .strict() + +type SerializableState = z.output + +export interface KottiStandardTableStorage { + load(context: StorageOperationContext): Promise + save( + state: SerializableState, + context: StorageOperationContext, + ): Promise +} + +/** + * @knipignore + */ +export class DummyStorageAdapter implements KottiStandardTableStorage { + // eslint-disable-next-line @typescript-eslint/require-await + async load(): Promise { + return null + } + + async save(): Promise {} +} + +const localStorageSchema = z + .object({ state: z.unknown(), version: z.string() }) + .strict() + +export type StorageOperationContext = { + /** + * Used for column hashing in {@link LocalStorageAdapter} + */ + columnIds: string[] + /** + * Used to get qualifying information about filters for parsing and validating them + */ + filterInfo: Map +} + +export class LocalStorageAdapter implements KottiStandardTableStorage { + #manualVersion: string | null + #storageKey: string + + constructor(key: string, manualVersion: string | null = null) { + this.#storageKey = key + this.#manualVersion = manualVersion + } + + #getVersionHash(columnIds: string[]): string { + const version = this.#manualVersion ?? simpleHash(columnIds) + return `${this.#storageKey}@${version}` + } + + #validateVersionHash(columnIds: string[], version: string): boolean { + if (!version.startsWith(`${this.#storageKey}@`)) return false + + const correctHash = simpleHash(columnIds) + const givenHash = version.replace(`${this.#storageKey}@`, '') + + return correctHash === givenHash + } + + // eslint-disable-next-line @typescript-eslint/require-await + async load( + context: StorageOperationContext, + ): Promise { + if (typeof window === 'undefined' || !('localStorage' in window)) + return null + + const json = window.localStorage.getItem(this.#storageKey) + + if (!json) return null + + try { + const data = localStorageSchema.parse(JSON.parse(json)) + + if (!this.#validateVersionHash(context.columnIds, data.version)) + return null + + // further validation is handled by the caller + return data.state as SerializableState + } catch (error) { + // eslint-disable-next-line no-console -- this is likely something that should be visible as it is unexpected for this to fail, but not serious enough for a throw + console.warn( + 'LocalStorageAdapter: recovered after failing to load', + error, + ) + return null + } + } + + // eslint-disable-next-line @typescript-eslint/require-await + async save( + state: SerializableState, + context: StorageOperationContext, + ): Promise { + if (typeof window === 'undefined' || !('localStorage' in window)) return + + const version = this.#getVersionHash(context.columnIds) + + const json = JSON.stringify({ + state, + version, + }) + + window.localStorage.setItem(this.#storageKey, json) + } +} diff --git a/packages/kotti-ui/source/kotti-table/standard-table/types.ts b/packages/kotti-ui/source/kotti-table/standard-table/types.ts new file mode 100644 index 0000000000..57e6805902 --- /dev/null +++ b/packages/kotti-ui/source/kotti-table/standard-table/types.ts @@ -0,0 +1,321 @@ +import type { Ref } from 'vue' +import { z } from 'zod' + +import { KottiFieldDateRange } from '../../kotti-field-date/types' +import { KottiFieldNumber } from '../../kotti-field-number/types' +import { + KottiFieldMultiSelect, + KottiFieldSingleSelect, +} from '../../kotti-field-select/types' +import { KottiFieldText } from '../../kotti-field-text/types' +import { KottiFieldToggle } from '../../kotti-field-toggle/types' +import { KottiPopover } from '../../kotti-popover/types' +import type { KottiTable } from '../table/types' + +const DEFAULT_PAGE_SIZE = 10 +// eslint-disable-next-line no-magic-numbers +const DEFAULT_PAGE_SIZE_OPTIONS = [10, 25, 50, 100] +const MIN_PAGE_SIZE = 5 + +export type FilterInfo = Pick< + KottiStandardTable.FilterInternal, + 'operations' | 'type' +> + +export namespace KottiStandardTable { + export enum FilterType { + BOOLEAN = 'BOOLEAN', + DATE_RANGE = 'DATE_RANGE', + MULTI_SELECT = 'MULTI_SELECT', + NUMBER_RANGE = 'NUMBER_RANGE', + SINGLE_SELECT = 'SINGLE_SELECT', + } + export namespace FilterOperation { + export enum Boolean { + EQUAL = 'EQUAL', + } + + export enum DateRange { + IN_RANGE = 'IN_RANGE', + } + + export enum MultiEnum { + ONE_OF = 'ONE_OF', // OR + } + + export enum NumberRange { + IN_RANGE = 'IN_RANGE', + } + + export enum SingleEnum { + EQUAL = 'EQUAL', + } + + export const schema = z.union([ + z.nativeEnum(Boolean), + z.nativeEnum(DateRange), + z.nativeEnum(MultiEnum), + z.nativeEnum(NumberRange), + z.nativeEnum(SingleEnum), + ]) + } + + const sharedFilterSchema = z.object({ + dataTest: z.string().optional(), + displayInline: z.boolean().default(false), + id: z.string(), + label: z.string(), + }) + + const booleanFilterSchema = sharedFilterSchema.extend({ + defaultValue: KottiFieldToggle.valueSchema.optional(), + operations: z + .nativeEnum(FilterOperation.Boolean) + .array() + .nonempty() + .default([FilterOperation.Boolean.EQUAL]), + slotLabels: z.tuple([z.string(), z.string()]).optional(), + type: z.literal(FilterType.BOOLEAN), + }) + + const dateRangeFilterSchema = sharedFilterSchema.extend({ + defaultValue: KottiFieldDateRange.valueSchema.optional(), + operations: z + .nativeEnum(FilterOperation.DateRange) + .array() + .nonempty() + .default([FilterOperation.DateRange.IN_RANGE]), + type: z.literal(FilterType.DATE_RANGE), + }) + + const multiSelectFilterSchema = sharedFilterSchema + .merge( + KottiFieldMultiSelect.propsSchema.pick({ + isUnsorted: true, + options: true, + }), + ) + .extend({ + defaultValue: KottiFieldMultiSelect.valueSchema.optional(), + operations: z + .nativeEnum(FilterOperation.MultiEnum) + .array() + .nonempty() + .default([FilterOperation.MultiEnum.ONE_OF]), + type: z.literal(FilterType.MULTI_SELECT), + }) + + const numberRangeFilterSchema = sharedFilterSchema + .merge( + KottiFieldNumber.propsSchema.pick({ + decimalPlaces: true, + }), + ) + .extend({ + defaultValue: z + .tuple([KottiFieldNumber.valueSchema, KottiFieldNumber.valueSchema]) + .optional(), + operations: z + .nativeEnum(FilterOperation.NumberRange) + .array() + .nonempty() + .default([FilterOperation.NumberRange.IN_RANGE]), + type: z.literal(FilterType.NUMBER_RANGE), + unit: KottiFieldNumber.propsSchema.shape.prefix, + }) + + const singleSelectFilterSchema = sharedFilterSchema + .merge( + KottiFieldSingleSelect.propsSchema.pick({ + isUnsorted: true, + options: true, + }), + ) + .extend({ + defaultValue: KottiFieldSingleSelect.valueSchema.optional(), + operations: z + .nativeEnum(FilterOperation.SingleEnum) + .array() + .nonempty() + .default([FilterOperation.SingleEnum.EQUAL]), + type: z.literal(FilterType.SINGLE_SELECT), + }) + + export const filterSchema = z.discriminatedUnion('type', [ + booleanFilterSchema, + dateRangeFilterSchema, + multiSelectFilterSchema, + numberRangeFilterSchema, + singleSelectFilterSchema, + ]) + export type Filter = z.input + export type FilterInternal = z.output + + export const filterValueSchema = z.union([ + KottiFieldToggle.valueSchema, + KottiFieldDateRange.valueSchema, + KottiFieldMultiSelect.valueSchema, + z.tuple([KottiFieldNumber.valueSchema, KottiFieldNumber.valueSchema]), + KottiFieldSingleSelect.valueSchema, + ]) + export type FilterValue = z.output + + export const appliedBooleanSchema = z.object({ + id: z.string(), + operation: z.nativeEnum(FilterOperation.Boolean), + value: KottiFieldToggle.valueSchema, + }) + export const appliedDateRangeSchema = z.object({ + id: z.string(), + operation: z.nativeEnum(FilterOperation.DateRange), + value: KottiFieldDateRange.valueSchema, + }) + export const appliedMultiEnumSchema = z.object({ + id: z.string(), + operation: z.nativeEnum(FilterOperation.MultiEnum), + value: KottiFieldMultiSelect.valueSchema, + }) + export const appliedNumberRangeSchema = z.object({ + id: z.string(), + operation: z.nativeEnum(FilterOperation.NumberRange), + value: z.tuple([ + KottiFieldNumber.valueSchema, + KottiFieldNumber.valueSchema, + ]), + }) + export const appliedSingleEnumSchema = z.object({ + id: z.string(), + operation: z.nativeEnum(FilterOperation.SingleEnum), + value: KottiFieldSingleSelect.valueSchema, + }) + + export const appliedFilterSchema = z.union([ + appliedBooleanSchema, + appliedDateRangeSchema, + appliedMultiEnumSchema, + appliedNumberRangeSchema, + appliedSingleEnumSchema, + ]) + export type AppliedFilter = z.output + + export const optionsSchema = z.object({ + hideControls: z + .object({ + columns: z.boolean().default(false), + filters: z.boolean().default(false), + search: z.boolean().default(false), + }) + .optional(), + popoversSize: z + .object({ + columns: KottiPopover.propsSchema.shape.size, + filters: KottiPopover.propsSchema.shape.size, + }) + .optional(), + searchPlaceholder: z.string().optional(), + }) + export type Options = z.input + + const sharedPaginationSchema = z.object({ + pageIndex: z.number().int().finite().min(0), + pageSize: z.number().int().finite().gt(0), + pageSizeOptions: z + .array(z.number().int().finite().min(MIN_PAGE_SIZE)) + .refine( + (val) => + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + val.slice(1).every((entry, entryIndex) => entry > val[entryIndex]!), + { + message: + 'Entries in pageSizeOptions must be a strictly increasing sequence', + }, + ), + rowCount: z.number().int().finite().min(0), + }) + + export const paginationSchema = z + .discriminatedUnion('type', [ + z.object({ + pageSize: + sharedPaginationSchema.shape.pageSize.default(DEFAULT_PAGE_SIZE), + pageSizeOptions: sharedPaginationSchema.shape.pageSizeOptions.default( + () => DEFAULT_PAGE_SIZE_OPTIONS, + ), + type: z.literal('local'), + }), + z.object({ + pageSize: + sharedPaginationSchema.shape.pageSize.default(DEFAULT_PAGE_SIZE), + pageSizeOptions: sharedPaginationSchema.shape.pageSizeOptions.default( + () => DEFAULT_PAGE_SIZE_OPTIONS, + ), + rowCount: sharedPaginationSchema.shape.rowCount, + type: z.literal('remote'), + }), + ]) + .default({ + pageSize: DEFAULT_PAGE_SIZE, + pageSizeOptions: DEFAULT_PAGE_SIZE_OPTIONS, + type: 'local', + }) + export type Pagination = z.input + + export const propsSchema = z.object({ + emptyText: z.string().nullable().default(null), + tableId: z.string().min(1, { message: 'Field cannot be empty' }), + title: z.string().optional(), + }) + export type Props = z.input + + export namespace Events { + const updateFetchData = z.object({ + filters: appliedFilterSchema.array(), + ordering: z.array( + z + .object({ + id: z.string(), + value: z.enum(['ascending', 'descending']), + }) + .strict(), + ), + pagination: sharedPaginationSchema.pick({ + pageIndex: true, + pageSize: true, + }), + search: KottiFieldText.valueSchema, + }) + export type UpdateFetchData = z.output + } + + export module Hook { + export type Returns = + KottiTable.Hook.Returns & { + appliedFilters: Ref + pagination: Ref<{ + pageIndex: number + pageSize: number + }> + searchValue: Ref + } + } + + export type Translations = { + clearAll: string + editColumns: string + editFilters: string + endDate: string + lastMonth: string + lastWeek: string + lastYear: string + max: string + min: string + moreThan: string + resultsCounter: string + rowsPerPage: string + search: string + showAll: string + startDate: string + today: string + upTo: string + } +} diff --git a/packages/kotti-ui/source/kotti-table/standard-table/utilities/date.ts b/packages/kotti-ui/source/kotti-table/standard-table/utilities/date.ts new file mode 100644 index 0000000000..6272d8e89c --- /dev/null +++ b/packages/kotti-ui/source/kotti-table/standard-table/utilities/date.ts @@ -0,0 +1,23 @@ +import dayjs from 'dayjs' +import type { ManipulateType } from 'dayjs' + +import { ISO8601 } from '../../../constants' + +/** + * Returns formatted today's date. Default template is ISO8601. + * @param templateFormat dayjs compatible datetime format string + * @returns formatted date + */ +export const getToday = (templateFormat: string = ISO8601): string => + dayjs().format(templateFormat) + +/** + * Returns formatted today's date with the specified amount of time subtracted. Default template is ISO8601. + * @param unit dayjs time unit + * @param templateFormat dayjs compatible datetime format string + * @returns formatted date + */ +export const getLast = ( + unit: ManipulateType, + templateFormat: string = ISO8601, +): string => dayjs().subtract(1, unit).format(templateFormat) diff --git a/packages/kotti-ui/source/kotti-table/standard-table/utilities/filters.ts b/packages/kotti-ui/source/kotti-table/standard-table/utilities/filters.ts new file mode 100644 index 0000000000..ce96b144b3 --- /dev/null +++ b/packages/kotti-ui/source/kotti-table/standard-table/utilities/filters.ts @@ -0,0 +1,123 @@ +import { Dashes } from '@metatypes/typography' + +import type { KottiFieldDateRange } from '../../../kotti-field-date/types' +import type { KottiFieldNumber } from '../../../kotti-field-number/types' +import type { + KottiFieldMultiSelect, + KottiFieldSingleSelect, +} from '../../../kotti-field-select/types' +import type { KottiFieldToggle } from '../../../kotti-field-toggle/types' +import { useTranslationNamespace } from '../../../kotti-i18n/hooks' +import { KottiStandardTable } from '../types' + +/** + * Returns the empty nullish value + * @param filter the filter + * @returns the empty value + */ +export const getEmptyValue = ( + filter: KottiStandardTable.FilterInternal, +): KottiStandardTable.FilterValue => { + switch (filter.type) { + case KottiStandardTable.FilterType.DATE_RANGE: + case KottiStandardTable.FilterType.NUMBER_RANGE: + return [null, null] + case KottiStandardTable.FilterType.MULTI_SELECT: + return [] + default: + return null + } +} + +/** + * Returns the option label + * @param options the options array + * @param value the option value + * @returns the option label + */ +const getOptionLabel = ( + options: KottiFieldSingleSelect.Props['options'], + value: KottiFieldSingleSelect.Value, +): string => options.find((option) => option.value === value)?.label ?? '' + +/** + * Formats the filter value as a human readably string + * @param value the value + * @param filter the filter + * @returns the value as a formated string + */ +export const formatFilterValue = ( + value: KottiStandardTable.FilterValue, + filter: KottiStandardTable.FilterInternal, +): string => { + switch (filter.type) { + case KottiStandardTable.FilterType.BOOLEAN: { + // FIXME: useTranslationNamespace should not be called outside of hooks => this should be a hook or + // the translation object should be passed from the outside + const translations = useTranslationNamespace('KtTable') + const _value = value as KottiFieldToggle.Value + return _value ? translations.value.yes : '' + } + case KottiStandardTable.FilterType.DATE_RANGE: { + const _value = value as KottiFieldDateRange.Value + return _value[0] === null ? '' : _value.join(Dashes.EnDash) + } + case KottiStandardTable.FilterType.MULTI_SELECT: { + const _value = value as KottiFieldMultiSelect.Value + return _value.map((v) => getOptionLabel(filter.options, v)).join(', ') + } + case KottiStandardTable.FilterType.NUMBER_RANGE: { + const _value = value as [KottiFieldNumber.Value, KottiFieldNumber.Value] + const [min, max] = _value + + if (min === null && max === null) return '' + + const unit = filter.unit ? ` ${filter.unit}` : '' + + if (min !== null && max !== null) + return min === max + ? `${min}${unit}` + : `${min}${Dashes.EnDash}${max}${unit}` + + // FIXME: useTranslationNamespace should not be called outside of hooks => this should be a hook or + // the translation object should be passed from the outside + const translations = useTranslationNamespace('KtStandardTable') + + return max !== null + ? `${translations.value.upTo} ${max}${unit}` + : min !== null + ? `${translations.value.moreThan} ${min}${unit}` + : '' + } + case KottiStandardTable.FilterType.SINGLE_SELECT: { + const _value = value as KottiFieldSingleSelect.Value + return getOptionLabel(filter.options, _value) + } + } +} + +/** + * Checks if the value is nullish + * @param value the field value + * @returns true if the value is nullish, false otherwise + */ +export const isEmptyValue = (value: KottiStandardTable.FilterValue): boolean => + Array.isArray(value) + ? value.length === 0 || (value[0] === null && value[1] === null) + : value === null + +/** + * Re-orders the Number Range filter value to be [min, max]. + * Value is re-ordered only if both, min and max, are not null values. + * @param range the Number Range filter value + * @returns the re-ordered range value + */ +export const getReorderedRange = ( + range: [KottiFieldNumber.Value, KottiFieldNumber.Value], +): [KottiFieldNumber.Value, KottiFieldNumber.Value] => { + const [min, max] = range + + if (min !== null && max !== null && min > max) return [max, min] + + return range +} diff --git a/packages/kotti-ui/source/kotti-table/standard-table/utilities/translation.ts b/packages/kotti-ui/source/kotti-table/standard-table/utilities/translation.ts new file mode 100644 index 0000000000..bee0b4aa44 --- /dev/null +++ b/packages/kotti-ui/source/kotti-table/standard-table/utilities/translation.ts @@ -0,0 +1,34 @@ +const separatorRegex = new RegExp(/\s*\|\s*/g) + +/** + * Applies text pluralization + * @param translation the translated text with pluralization cases separated by `|` + * @param count the amount of elements + * @param values a token-value dictionary to replace tokens in the translated text + * @returns the pluralized text + */ +export const pluralize = ( + translation: string, + count: number, + values: Record, +): string => { + const cases = translation.split(separatorRegex) + + if (cases.length < 2) { + throw new Error('Invalid translation string') + } + + let result = ( + count === 0 + ? cases[0] + : count === 1 + ? cases[1] ?? cases[0] + : cases[2] ?? cases[1] ?? cases[0] + ) as string + + Object.entries(values).forEach(([key, value]) => { + result = result.replaceAll(`{${key}}`, String(value)) + }) + + return result +} diff --git a/packages/kotti-ui/source/kotti-table/table/context.ts b/packages/kotti-ui/source/kotti-table/table/context.ts index 4bbbf6924b..7ea0d1649a 100644 --- a/packages/kotti-ui/source/kotti-table/table/context.ts +++ b/packages/kotti-ui/source/kotti-table/table/context.ts @@ -1,4 +1,4 @@ -import type { Table } from '@tanstack/table-core' +import type { Table, VisibilityState } from '@tanstack/table-core' import { inject, provide, type Ref } from 'vue' import type { GetRowBehavior, KottiTable } from './types' @@ -20,6 +20,7 @@ export type TableContext< swapDraggedAndDropTarget: () => void table: Ref> triggerExpand: (rowId: string) => void + visibleColumns: VisibilityState } }> diff --git a/packages/kotti-ui/source/kotti-table/table/hooks.ts b/packages/kotti-ui/source/kotti-table/table/hooks.ts index f5f3c8b7c3..2029d7693f 100644 --- a/packages/kotti-ui/source/kotti-table/table/hooks.ts +++ b/packages/kotti-ui/source/kotti-table/table/hooks.ts @@ -3,7 +3,6 @@ import type { CellContext, ExpandedState, HeaderContext, - PaginationState, RowSelectionState, VisibilityState, } from '@tanstack/table-core' @@ -11,7 +10,6 @@ import { createColumnHelper, getCoreRowModel, getExpandedRowModel, - getPaginationRowModel, } from '@tanstack/table-core' import classNames from 'classnames' import { computed, h, ref, type Ref } from 'vue' @@ -24,7 +22,8 @@ import ToggleInner from '../../shared-components/toggle-inner/ToggleInner.vue' import { type TableContext, useProvideTableContext } from './context' import { useVueTable } from './tanstack-table' -import { type GetRowBehavior, KottiTable } from './types' +import type { KottiTable } from './types' +import { type GetRowBehavior } from './types' import { type ReactStyleUpdater, useComputedRef } from './use-computed-ref' export const EXPANSION_COLUMN_ID = 'internal-expand-column' @@ -52,10 +51,6 @@ export type KottiTableParameter< hasDragAndDrop?: boolean id: string isSelectable?: boolean // FIXME: Consider isSelectable to become `selectMode: 'single-page' | 'global' | null` when we support selection across pages. Current behavior is 'global' - // FIXME: pagination should not be part of KtTable's API (only KtStandardTable). - // Suggested fix: There should be a way for KtStandardTable to call `useVueTable` without having to use `useKottiTable` - // OR don't use tanstack to handle pagination - initialPagination?: KottiTable.Pagination } export const paramsSchema = z @@ -144,7 +139,6 @@ export const paramsSchema = z ), hasDragAndDrop: z.boolean().default(false), id: z.string(), - initialPagination: KottiTable.paginationSchema.nullable().default(null), isSelectable: z.boolean().default(false), }) .strict() @@ -160,19 +154,20 @@ type InternalKottiTableParameters< expandMode: 'multi' | 'single' | null hasDragAndDrop: boolean id: string - initialPagination: KottiTable.Pagination | null isSelectable: boolean } +type KottiTableHook = { + api: KottiTable.Hook.Returns + tableContext: TableContext +} + export const useKottiTable = < ROW extends KottiTable.AnyRow, COLUMN_ID extends string, >( _params: Ref>, -): { - api: KottiTable.Hook.Returns - tableContext: TableContext -} => { +): KottiTableHook => { const params = computed( () => paramsSchema.parse(_params.value) as InternalKottiTableParameters< @@ -232,29 +227,6 @@ export const useKottiTable = < ]), }) - const pagination = useComputedRef({ - get: (value) => value, - set: (value) => { - const currentRow = value.pageIndex * value.pageSize - if ( - currentRow > - (params.value.initialPagination?.rowCount ?? Number.MAX_SAFE_INTEGER) - ) { - return { - ...value, - pageIndex: 0, - } - } - return value - }, - value: ref( - params.value.initialPagination?.state ?? { - pageIndex: 0, - pageSize: 10, - }, - ), - }) - // FIXME: This useComputedRef will not clear out row ids that are not present // in the data provided via params. That means selection is always global. const selectedRows = useComputedRef({ @@ -573,30 +545,15 @@ export const useKottiTable = < getCoreRowModel: getCoreRowModel(), getExpandedRowModel: params.value.expandMode !== null ? getExpandedRowModel() : undefined, - getPaginationRowModel: - params.value.initialPagination?.type === 'local' - ? getPaginationRowModel() - : undefined, getRowId: (row, rowIndex) => params.value.getRowBehavior({ row, rowIndex }).id, - manualPagination: params.value.initialPagination?.type === 'remote', onColumnVisibilityChange: hiddenColumns.tanstackSetter, - onPaginationChange: params.value.initialPagination - ? pagination.tanstackSetter - : undefined, onRowSelectionChange: selectedRows.tanstackSetter, onSortingChange: ordering.tanstackSetter as ReactStyleUpdater, - rowCount: - params.value.initialPagination?.type === 'remote' - ? params.value.initialPagination.rowCount - : undefined, state: { columnOrder: columnOrder.tanstackGetter(), columnVisibility: hiddenColumns.tanstackGetter(), expanded: expandedRows.tanstackGetter(), - pagination: params.value.initialPagination - ? pagination.tanstackGetter() - : undefined, rowSelection: selectedRows.tanstackGetter(), sorting: ordering.tanstackGetter(), }, @@ -638,6 +595,7 @@ export const useKottiTable = < }, table, triggerExpand, + visibleColumns: hiddenColumns.tanstackGetter(), }, })) useProvideTableContext(params.value.id, tableContext) @@ -648,7 +606,6 @@ export const useKottiTable = < expandedRows, hiddenColumns, ordering, - pagination, selectedRows, }, tableContext, diff --git a/packages/kotti-ui/source/kotti-table/table/types.ts b/packages/kotti-ui/source/kotti-table/table/types.ts index 2a6131483d..1c644785e0 100644 --- a/packages/kotti-ui/source/kotti-table/table/types.ts +++ b/packages/kotti-ui/source/kotti-table/table/types.ts @@ -106,7 +106,6 @@ export module KottiTable { expandedRows: Ref> hiddenColumns: Ref> ordering: Ref[]> - pagination: Ref selectedRows: Ref } } @@ -116,15 +115,4 @@ export module KottiTable { noItems: string yes: string } - - // FIXME: Either use me or move me into standard table. - export const paginationSchema = z.object({ - rowCount: z.number().int().finite().min(0), - state: z.object({ - pageIndex: z.number().int().finite().min(0), - pageSize: z.number().int().finite().gt(0), - }), - type: z.enum(['local', 'remote']), - }) - export type Pagination = z.output } diff --git a/packages/kotti-ui/source/locales/input.json b/packages/kotti-ui/source/locales/input.json index 08afd11bdd..2094455990 100644 --- a/packages/kotti-ui/source/locales/input.json +++ b/packages/kotti-ui/source/locales/input.json @@ -105,6 +105,25 @@ "menuExpand": "Expand menu", "quickLinksTitle": "Quick Links" }, + "ktStandardTable": { + "clearAll": "Clear All", + "editColumns": "Edit Columns", + "editFilters": "Edit Filters", + "endDate": "End", + "lastMonth": "Last Month", + "lastWeek": "Last Week", + "lastYear": "Last Year", + "max": "Max.", + "min": "Min.", + "moreThan": "More Than", + "resultsCounter": "No items | {range} of {total} item | {range} of {total} items", + "rowsPerPage": "Rows per page", + "search": "Search", + "showAll": "Show All", + "startDate": "Start", + "today": "Today", + "upTo": "Up To" + }, "ktTable": { "no": "No", "noItems": "No Data", diff --git a/packages/kotti-ui/source/types/kotti.ts b/packages/kotti-ui/source/types/kotti.ts index 6c5b478263..783590e850 100644 --- a/packages/kotti-ui/source/types/kotti.ts +++ b/packages/kotti-ui/source/types/kotti.ts @@ -65,6 +65,7 @@ export { KottiNavbar as Navbar } from '../kotti-navbar/types' export { KottiPagination as Pagination } from '../kotti-pagination/types' export { KottiPopover as Popover } from '../kotti-popover/types' export { KottiRow as Row } from '../kotti-row/types' +export { KottiStandardTable as StandardTable } from '../kotti-table/standard-table/types' export { KottiTable as Table } from '../kotti-table/table/types' export { KottiTableLegacy as TableLegacy } from '../kotti-table-legacy/types' export { KottiTag as Tag } from '../kotti-tag/types'