From 29f2629716e3693372ec9a4572113a3f3721ff5e Mon Sep 17 00:00:00 2001 From: Alex Grozav Date: Wed, 15 Mar 2023 18:52:02 +0200 Subject: [PATCH] feat: Add basic Datatable and Pagination components (#5652) * feat: add Datatable component * feat: migrate to n8n-pagination and add datatable tests * chore: fix linting issue --- .../N8nDatatable/Datatable.stories.ts | 21 ++ .../src/components/N8nDatatable/Datatable.vue | 200 ++++++++++++++++++ .../N8nDatatable/__tests__/Datatable.spec.ts | 23 ++ .../__snapshots__/Datatable.spec.ts.snap | 100 +++++++++ .../components/N8nDatatable/__tests__/data.ts | 45 ++++ .../src/components/N8nDatatable/index.ts | 3 + .../src/components/N8nDatatable/mixins.ts | 16 ++ .../N8nPagination/Pagination.stories.ts | 23 ++ .../components/N8nPagination/Pagination.vue | 22 ++ .../src/components/N8nPagination/index.ts | 3 + .../design-system/src/composables/index.ts | 1 + .../design-system/src/composables/useI18n.ts | 7 + packages/design-system/src/locale/lang/en.js | 1 + .../src/utils/__tests__/valueByPath.spec.ts | 43 ++++ packages/design-system/src/utils/index.ts | 1 + .../design-system/src/utils/valueByPath.ts | 14 ++ 16 files changed, 523 insertions(+) create mode 100644 packages/design-system/src/components/N8nDatatable/Datatable.stories.ts create mode 100644 packages/design-system/src/components/N8nDatatable/Datatable.vue create mode 100644 packages/design-system/src/components/N8nDatatable/__tests__/Datatable.spec.ts create mode 100644 packages/design-system/src/components/N8nDatatable/__tests__/__snapshots__/Datatable.spec.ts.snap create mode 100644 packages/design-system/src/components/N8nDatatable/__tests__/data.ts create mode 100644 packages/design-system/src/components/N8nDatatable/index.ts create mode 100644 packages/design-system/src/components/N8nDatatable/mixins.ts create mode 100644 packages/design-system/src/components/N8nPagination/Pagination.stories.ts create mode 100644 packages/design-system/src/components/N8nPagination/Pagination.vue create mode 100644 packages/design-system/src/components/N8nPagination/index.ts create mode 100644 packages/design-system/src/composables/index.ts create mode 100644 packages/design-system/src/composables/useI18n.ts create mode 100644 packages/design-system/src/utils/__tests__/valueByPath.spec.ts create mode 100644 packages/design-system/src/utils/valueByPath.ts diff --git a/packages/design-system/src/components/N8nDatatable/Datatable.stories.ts b/packages/design-system/src/components/N8nDatatable/Datatable.stories.ts new file mode 100644 index 0000000000..3166530de1 --- /dev/null +++ b/packages/design-system/src/components/N8nDatatable/Datatable.stories.ts @@ -0,0 +1,21 @@ +import N8nDatatable from './Datatable.vue'; +import type { StoryFn } from '@storybook/vue'; +import { rows, columns } from './__tests__/data'; + +export default { + title: 'Atoms/Datatable', + component: N8nDatatable, +}; + +export const Default: StoryFn = (args, { argTypes }) => ({ + props: Object.keys(argTypes), + components: { + N8nDatatable, + }, + template: '', +}); + +Default.args = { + columns, + rows, +}; diff --git a/packages/design-system/src/components/N8nDatatable/Datatable.vue b/packages/design-system/src/components/N8nDatatable/Datatable.vue new file mode 100644 index 0000000000..475e14e8df --- /dev/null +++ b/packages/design-system/src/components/N8nDatatable/Datatable.vue @@ -0,0 +1,200 @@ + + + + + diff --git a/packages/design-system/src/components/N8nDatatable/__tests__/Datatable.spec.ts b/packages/design-system/src/components/N8nDatatable/__tests__/Datatable.spec.ts new file mode 100644 index 0000000000..3322246e37 --- /dev/null +++ b/packages/design-system/src/components/N8nDatatable/__tests__/Datatable.spec.ts @@ -0,0 +1,23 @@ +import { render } from '@testing-library/vue'; +import N8nDatatable from '../Datatable.vue'; +import { rows, columns } from './data'; + +const stubs = ['n8n-select', 'n8n-option', 'n8n-button', 'n8n-pagination']; + +describe('components', () => { + describe('N8nDatatable', () => { + it('should render correctly', () => { + const wrapper = render(N8nDatatable, { + propsData: { + columns, + rows, + }, + stubs, + }); + + expect(wrapper.container.querySelectorAll('tbody tr').length).toEqual(10); + expect(wrapper.container.querySelectorAll('thead tr').length).toEqual(1); + expect(wrapper.html()).toMatchSnapshot(); + }); + }); +}); diff --git a/packages/design-system/src/components/N8nDatatable/__tests__/__snapshots__/Datatable.spec.ts.snap b/packages/design-system/src/components/N8nDatatable/__tests__/__snapshots__/Datatable.spec.ts.snap new file mode 100644 index 0000000000..3b419dbc69 --- /dev/null +++ b/packages/design-system/src/components/N8nDatatable/__tests__/__snapshots__/Datatable.spec.ts.snap @@ -0,0 +1,100 @@ +// Vitest Snapshot v1 + +exports[`components > N8nDatatable > should render correctly 1`] = ` +"
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ID Name Age Action
1Richard Hendricks29
2Bertram Gilfoyle44
3Dinesh Chugtai31
4Jared Dunn 38
5Richard Hendricks29
6Bertram Gilfoyle44
7Dinesh Chugtai31
8Jared Dunn 38
9Richard Hendricks29
10Bertram Gilfoyle44
+
+ +
+ + + + + + + +
+
+
" +`; diff --git a/packages/design-system/src/components/N8nDatatable/__tests__/data.ts b/packages/design-system/src/components/N8nDatatable/__tests__/data.ts new file mode 100644 index 0000000000..b1a1315dd0 --- /dev/null +++ b/packages/design-system/src/components/N8nDatatable/__tests__/data.ts @@ -0,0 +1,45 @@ +import { defineComponent, h, PropType } from 'vue'; +import { DatatableRow } from '../mixins'; +import N8nButton from '../../N8nButton'; + +// eslint-disable-next-line @typescript-eslint/naming-convention +export const ActionComponent = defineComponent({ + props: { + row: { + type: Object as PropType, + default: () => ({}), + }, + }, + setup(props) { + return () => h(N8nButton, {}, [`Button ${props.row.id}`]); + }, +}); + +export const columns = [ + { id: 'id', path: 'id', label: 'ID' }, + { id: 'name', path: 'name', label: 'Name' }, + { id: 'age', path: 'meta.age', label: 'Age' }, + { + id: 'action', + label: 'Action', + render: ActionComponent, + }, +]; + +export const rows = [ + { id: 1, name: 'Richard Hendricks', meta: { age: 29 } }, + { id: 2, name: 'Bertram Gilfoyle', meta: { age: 44 } }, + { id: 3, name: 'Dinesh Chugtai', meta: { age: 31 } }, + { id: 4, name: 'Jared Dunn ', meta: { age: 38 } }, + { id: 5, name: 'Richard Hendricks', meta: { age: 29 } }, + { id: 6, name: 'Bertram Gilfoyle', meta: { age: 44 } }, + { id: 7, name: 'Dinesh Chugtai', meta: { age: 31 } }, + { id: 8, name: 'Jared Dunn ', meta: { age: 38 } }, + { id: 9, name: 'Richard Hendricks', meta: { age: 29 } }, + { id: 10, name: 'Bertram Gilfoyle', meta: { age: 44 } }, + { id: 11, name: 'Dinesh Chugtai', meta: { age: 31 } }, + { id: 12, name: 'Jared Dunn ', meta: { age: 38 } }, + { id: 13, name: 'Richard Hendricks', meta: { age: 29 } }, + { id: 14, name: 'Bertram Gilfoyle', meta: { age: 44 } }, + { id: 15, name: 'Dinesh Chugtai', meta: { age: 31 } }, +]; diff --git a/packages/design-system/src/components/N8nDatatable/index.ts b/packages/design-system/src/components/N8nDatatable/index.ts new file mode 100644 index 0000000000..78a4e7dff4 --- /dev/null +++ b/packages/design-system/src/components/N8nDatatable/index.ts @@ -0,0 +1,3 @@ +import N8nDatatable from './Datatable.vue'; + +export default N8nDatatable; diff --git a/packages/design-system/src/components/N8nDatatable/mixins.ts b/packages/design-system/src/components/N8nDatatable/mixins.ts new file mode 100644 index 0000000000..907b86c98c --- /dev/null +++ b/packages/design-system/src/components/N8nDatatable/mixins.ts @@ -0,0 +1,16 @@ +import { VNode } from 'vue'; + +export type DatatableRowDataType = string | number | boolean | null | undefined; + +export interface DatatableRow { + id: string | number; + + [key: string]: DatatableRowDataType; +} + +export interface DatatableColumn { + id: string | number; + path: string; + label: string; + render: (row: DatatableRow) => (() => VNode | VNode[]) | DatatableRowDataType; +} diff --git a/packages/design-system/src/components/N8nPagination/Pagination.stories.ts b/packages/design-system/src/components/N8nPagination/Pagination.stories.ts new file mode 100644 index 0000000000..3fa4d573b7 --- /dev/null +++ b/packages/design-system/src/components/N8nPagination/Pagination.stories.ts @@ -0,0 +1,23 @@ +import type { StoryFn } from '@storybook/vue'; +import N8nPagination from './Pagination.vue'; + +export default { + title: 'Atoms/Pagination', + component: N8nPagination, +}; + +const Template: StoryFn = (args, { argTypes }) => ({ + props: Object.keys(argTypes), + components: { + N8nPagination, + }, + template: '', +}); + +export const Pagination: StoryFn = Template.bind({}); +Pagination.args = { + currentPage: 1, + pagerCount: 5, + pageSize: 10, + total: 100, +}; diff --git a/packages/design-system/src/components/N8nPagination/Pagination.vue b/packages/design-system/src/components/N8nPagination/Pagination.vue new file mode 100644 index 0000000000..e5bc468151 --- /dev/null +++ b/packages/design-system/src/components/N8nPagination/Pagination.vue @@ -0,0 +1,22 @@ + + + diff --git a/packages/design-system/src/components/N8nPagination/index.ts b/packages/design-system/src/components/N8nPagination/index.ts new file mode 100644 index 0000000000..0241347c61 --- /dev/null +++ b/packages/design-system/src/components/N8nPagination/index.ts @@ -0,0 +1,3 @@ +import N8nPagination from './Pagination.vue'; + +export default N8nPagination; diff --git a/packages/design-system/src/composables/index.ts b/packages/design-system/src/composables/index.ts new file mode 100644 index 0000000000..19ab1d994e --- /dev/null +++ b/packages/design-system/src/composables/index.ts @@ -0,0 +1 @@ +export * from './useI18n'; diff --git a/packages/design-system/src/composables/useI18n.ts b/packages/design-system/src/composables/useI18n.ts new file mode 100644 index 0000000000..9c18f3e6ac --- /dev/null +++ b/packages/design-system/src/composables/useI18n.ts @@ -0,0 +1,7 @@ +import { t } from '../locale'; + +export function useI18n() { + return { + t: (path: string, options: string[] = []) => t(path, options), + }; +} diff --git a/packages/design-system/src/locale/lang/en.js b/packages/design-system/src/locale/lang/en.js index faaa58e830..3038b3b248 100644 --- a/packages/design-system/src/locale/lang/en.js +++ b/packages/design-system/src/locale/lang/en.js @@ -18,4 +18,5 @@ export default { '8+ characters, at least 1 number and 1 capital letter', 'sticky.markdownHint': `You can style with Markdown`, 'tags.showMore': (count) => `+${count} more`, + 'datatable.pageSize': 'Page size', }; diff --git a/packages/design-system/src/utils/__tests__/valueByPath.spec.ts b/packages/design-system/src/utils/__tests__/valueByPath.spec.ts new file mode 100644 index 0000000000..fc5bfea6ad --- /dev/null +++ b/packages/design-system/src/utils/__tests__/valueByPath.spec.ts @@ -0,0 +1,43 @@ +import { getValueByPath } from '@/utils'; + +describe('getValueByPath()', () => { + const object = { + id: '1', + name: 'Richard Hendricks', + address: { + city: 'Palo Alto', + state: 'California', + country: 'United States', + }, + }; + + it('should return direct field from object', () => { + const path = 'name'; + + expect(getValueByPath(object, path)).toEqual(object.name); + }); + + it('should return nested field from object', () => { + const path = 'address.country'; + + expect(getValueByPath(object, path)).toEqual(object.address.country); + }); + + it('should return undefined if direct field does not exist', () => { + const path = 'other'; + + expect(getValueByPath(object, path)).toEqual(undefined); + }); + + it('should return undefined if nested field does not exist', () => { + const path = 'address.other'; + + expect(getValueByPath(object, path)).toEqual(undefined); + }); + + it('should return undefined if path does not exist', () => { + const path = 'other.other'; + + expect(getValueByPath(object, path)).toEqual(undefined); + }); +}); diff --git a/packages/design-system/src/utils/index.ts b/packages/design-system/src/utils/index.ts index a601378f5d..b2e1c68129 100644 --- a/packages/design-system/src/utils/index.ts +++ b/packages/design-system/src/utils/index.ts @@ -1,2 +1,3 @@ export * from './markdown'; export * from './uid'; +export * from './valueByPath'; diff --git a/packages/design-system/src/utils/valueByPath.ts b/packages/design-system/src/utils/valueByPath.ts new file mode 100644 index 0000000000..a596df7098 --- /dev/null +++ b/packages/design-system/src/utils/valueByPath.ts @@ -0,0 +1,14 @@ +/* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access */ + +/** + * Get a deeply nested value based on a given path string + * + * @param object + * @param path + * @returns {T} + */ +export function getValueByPath(object: any, path: string): T { + return path.split('.').reduce((acc, part) => { + return acc && acc[part]; + }, object); +}