From c34ad1086e0ca5b6d7574bef3d22cf911ccfcd09 Mon Sep 17 00:00:00 2001 From: Benjamin Canac Date: Fri, 11 Oct 2024 14:13:17 +0200 Subject: [PATCH 01/26] feat(Table): implement component --- docs/content/3.components/table.md | 12 +- package.json | 1 + playground/app/app.vue | 1 + playground/app/pages/components/table.vue | 171 ++++++++++++++++++++++ pnpm-lock.yaml | 20 +++ src/runtime/components/Table.vue | 114 +++++++++++++++ src/runtime/types/index.ts | 1 + src/theme/index.ts | 1 + src/theme/table.ts | 14 ++ test/components/Table.spec.ts | 44 ++++++ 10 files changed, 377 insertions(+), 2 deletions(-) create mode 100644 playground/app/pages/components/table.vue create mode 100644 src/runtime/components/Table.vue create mode 100644 src/theme/table.ts create mode 100644 test/components/Table.spec.ts diff --git a/docs/content/3.components/table.md b/docs/content/3.components/table.md index dc0c2c5b0e..9e639d3428 100644 --- a/docs/content/3.components/table.md +++ b/docs/content/3.components/table.md @@ -15,7 +15,7 @@ navigation: ## Examples - +:component-theme diff --git a/package.json b/package.json index 65004637f7..b47dceb75a 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,7 @@ "@nuxtjs/color-mode": "^3.5.1", "@tailwindcss/postcss": "4.0.0-alpha.24", "@tailwindcss/vite": "4.0.0-alpha.24", + "@tanstack/vue-table": "^8.20.5", "@vueuse/core": "^11.1.0", "@vueuse/integrations": "^11.1.0", "defu": "^6.1.4", diff --git a/playground/app/app.vue b/playground/app/app.vue index b8f65cbb8d..f72b2f8a81 100644 --- a/playground/app/app.vue +++ b/playground/app/app.vue @@ -42,6 +42,7 @@ const components = [ 'slider', 'switch', 'tabs', + 'table', 'textarea', 'toast', 'tooltip' diff --git a/playground/app/pages/components/table.vue b/playground/app/pages/components/table.vue new file mode 100644 index 0000000000..3ccbda6b6e --- /dev/null +++ b/playground/app/pages/components/table.vue @@ -0,0 +1,171 @@ + + + diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0780a7ec9e..e7d74b1328 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -34,6 +34,9 @@ importers: '@tailwindcss/vite': specifier: 4.0.0-alpha.24 version: 4.0.0-alpha.24(vite@5.4.8(@types/node@22.7.4)(lightningcss@1.27.0)(terser@5.34.1)) + '@tanstack/vue-table': + specifier: ^8.20.5 + version: 8.20.5(vue@3.5.11(typescript@5.6.3)) '@vueuse/core': specifier: ^11.1.0 version: 11.1.0(vue@3.5.11(typescript@5.6.3)) @@ -2024,9 +2027,19 @@ packages: peerDependencies: vite: ^5.2.0 + '@tanstack/table-core@8.20.5': + resolution: {integrity: sha512-P9dF7XbibHph2PFRz8gfBKEXEY/HJPOhym8CHmjF8y3q5mWpKx9xtZapXQUWCgkqvsK0R46Azuz+VaxD4Xl+Tg==} + engines: {node: '>=12'} + '@tanstack/virtual-core@3.10.8': resolution: {integrity: sha512-PBu00mtt95jbKFi6Llk9aik8bnR3tR/oQP1o3TSi+iG//+Q2RTIzCEgKkHG8BB86kxMNW6O8wku+Lmi+QFR6jA==} + '@tanstack/vue-table@8.20.5': + resolution: {integrity: sha512-2xixT3BEgSDw+jOSqPt6ylO/eutDI107t2WdFMVYIZZ45UmTHLySqNriNs0+dMaKR56K5z3t+97P6VuVnI2L+Q==} + engines: {node: '>=12'} + peerDependencies: + vue: '>=3.2' + '@tanstack/vue-virtual@3.10.8': resolution: {integrity: sha512-DB5QA8c/LfqOqIUCpSs3RdOTVroRRdqeHMqBkYrcashSZtOzIv8xbiqHgg7RYxDfkH5F3Y+e0MkuuyGNDVB0BQ==} peerDependencies: @@ -8713,8 +8726,15 @@ snapshots: tailwindcss: 4.0.0-alpha.24 vite: 5.4.8(@types/node@22.7.4)(lightningcss@1.27.0)(terser@5.34.1) + '@tanstack/table-core@8.20.5': {} + '@tanstack/virtual-core@3.10.8': {} + '@tanstack/vue-table@8.20.5(vue@3.5.11(typescript@5.6.3))': + dependencies: + '@tanstack/table-core': 8.20.5 + vue: 3.5.11(typescript@5.6.3) + '@tanstack/vue-virtual@3.10.8(vue@3.5.11(typescript@5.6.3))': dependencies: '@tanstack/virtual-core': 3.10.8 diff --git a/src/runtime/components/Table.vue b/src/runtime/components/Table.vue new file mode 100644 index 0000000000..dd33f51c45 --- /dev/null +++ b/src/runtime/components/Table.vue @@ -0,0 +1,114 @@ + + + + + diff --git a/src/runtime/types/index.ts b/src/runtime/types/index.ts index bf0a1ee609..f020ed2efc 100644 --- a/src/runtime/types/index.ts +++ b/src/runtime/types/index.ts @@ -35,6 +35,7 @@ export * from '../components/Skeleton.vue' export * from '../components/Slideover.vue' export * from '../components/Slider.vue' export * from '../components/Switch.vue' +export * from '../components/Table.vue' export * from '../components/Tabs.vue' export * from '../components/Textarea.vue' export * from '../components/Toast.vue' diff --git a/src/theme/index.ts b/src/theme/index.ts index 8ed54a2ed1..d02a54c358 100644 --- a/src/theme/index.ts +++ b/src/theme/index.ts @@ -35,6 +35,7 @@ export { default as skeleton } from './skeleton' export { default as slideover } from './slideover' export { default as slider } from './slider' export { default as switch } from './switch' +export { default as table } from './table' export { default as tabs } from './tabs' export { default as textarea } from './textarea' export { default as toast } from './toast' diff --git a/src/theme/table.ts b/src/theme/table.ts new file mode 100644 index 0000000000..455ee6d5d5 --- /dev/null +++ b/src/theme/table.ts @@ -0,0 +1,14 @@ +export default { + slots: { + root: 'relative overflow-x-auto', + base: 'min-w-full table-fixed divide-y divide-[--ui-border-accented]', + thead: '', + tbody: 'divide-y divide-[--ui-border]', + tr: '', + th: 'px-4 py-3.5 text-sm text-[--ui-text-highlighted] text-left rtl:text-right font-semibold', + td: 'p-4 text-sm text-[--ui-text-muted] whitespace-nowrap' + }, + variants: { + + } +} diff --git a/test/components/Table.spec.ts b/test/components/Table.spec.ts new file mode 100644 index 0000000000..91988cf7ea --- /dev/null +++ b/test/components/Table.spec.ts @@ -0,0 +1,44 @@ +import { describe, it, expect } from 'vitest' +import Table, { type TableProps, type TableSlots } from '../../src/runtime/components/Table.vue' +import ComponentRender from '../component-render' + +describe('Table', () => { + const data = [{ + id: 'm5gr84i9', + amount: 316, + status: 'success', + email: 'ken99@yahoo.com' + }, { + id: '3u1reuv4', + amount: 242, + status: 'success', + email: 'Abe45@gmail.com' + }, { + id: 'derv1ws0', + amount: 837, + status: 'processing', + email: 'Monserrat44@gmail.com' + }, { + id: '5kma53ae', + amount: 874, + status: 'success', + email: 'Silas22@gmail.com' + }, { + id: 'bhqecj4p', + amount: 721, + status: 'failed', + email: 'carmella@hotmail.com' + }] + + it.each([ + // Props + ['with as', { props: { as: 'div' } }], + ['with class', { props: { class: '' } }], + ['with ui', { props: { ui: {} } }], + // Slots + ['with default slot', { slots: { default: () => 'Default slot' } }] + ])('renders %s correctly', async (nameOrHtml: string, options: { props?: TableProps, slots?: Partial }) => { + const html = await ComponentRender(nameOrHtml, options, Table) + expect(html).toMatchSnapshot() + }) +}) From 25713a3074d888f6dc587f68fd093702d7d305f2 Mon Sep 17 00:00:00 2001 From: Benjamin Canac Date: Fri, 11 Oct 2024 14:21:03 +0200 Subject: [PATCH 02/26] feat(InputMenu/RadioGroup/Select/SelectMenu): handle `labelKey` and use `get` to support dot notation --- src/runtime/components/InputMenu.vue | 24 ++-- src/runtime/components/RadioGroup.vue | 19 ++- src/runtime/components/Select.vue | 15 ++- src/runtime/components/SelectMenu.vue | 22 ++-- test/components/InputMenu.spec.ts | 7 ++ test/components/RadioGroup.spec.ts | 3 + test/components/Select.spec.ts | 2 + test/components/SelectMenu.spec.ts | 2 + .../__snapshots__/InputMenu.spec.ts.snap | 70 +++++++++++ .../__snapshots__/RadioGroup.spec.ts.snap | 105 +++++++++++++++++ .../__snapshots__/Select.spec.ts.snap | 110 ++++++++++++++++++ .../__snapshots__/SelectMenu.spec.ts.snap | 78 +++++++++++++ 12 files changed, 436 insertions(+), 21 deletions(-) diff --git a/src/runtime/components/InputMenu.vue b/src/runtime/components/InputMenu.vue index 3633bc2f46..ec36e3bde1 100644 --- a/src/runtime/components/InputMenu.vue +++ b/src/runtime/components/InputMenu.vue @@ -86,6 +86,11 @@ export interface InputMenuProps extends Pick, 'modelValu * @defaultValue undefined */ valueKey?: keyof T + /** + * When `items` is an array of objects, select the field to use as the label. + * @defaultValue 'label' + */ + labelKey?: string items?: T[] | T[][] /** Highlight the ring color like a focus state. */ highlight?: boolean @@ -124,10 +129,10 @@ import { useAppConfig } from '#imports' import { useButtonGroup } from '../composables/useButtonGroup' import { useComponentIcons } from '../composables/useComponentIcons' import { useFormField } from '../composables/useFormField' +import { get, escapeRegExp } from '../utils' import UIcon from './Icon.vue' import UAvatar from './Avatar.vue' import UChip from './Chip.vue' -import { get, escapeRegExp } from '../utils' defineOptions({ inheritAttrs: false }) @@ -135,7 +140,8 @@ const props = withDefaults(defineProps>(), { type: 'text', autofocusDelay: 0, portal: true, - filter: () => ['label'] + filter: () => ['label'], + labelKey: 'label' }) const emits = defineEmits>() const slots = defineSlots>() @@ -164,9 +170,9 @@ const ui = computed(() => inputMenu({ })) function displayValue(value: AcceptableValue): string { - const item = items.value.find(item => props.valueKey ? isEqual(item[props.valueKey], value) : isEqual(item, value)) + const item = items.value.find(item => props.valueKey ? isEqual(get(item as Record, props.valueKey as string), value) : isEqual(item, value)) - return item && (typeof item === 'object' ? item.label : item) + return item && (typeof item === 'object' ? get(item, props.labelKey as string) : item) } function filterFunction(items: ArrayOrWrapped, searchTerm: string): ArrayOrWrapped { @@ -174,7 +180,7 @@ function filterFunction(items: ArrayOrWrapped, searchTerm: stri return items } - const fields = Array.isArray(props.filter) ? props.filter : ['label'] + const fields = Array.isArray(props.filter) ? props.filter : [props.labelKey] const escapedSearchTerm = escapeRegExp(searchTerm) return items.filter((item) => { @@ -183,7 +189,7 @@ function filterFunction(items: ArrayOrWrapped, searchTerm: stri } return fields.some((field) => { - const child = get(item, field) + const child = get(item, field as string) return child !== null && child !== undefined && String(child).search(new RegExp(escapedSearchTerm, 'i')) !== -1 }) @@ -325,7 +331,7 @@ defineExpose({