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 0000000000000..3166530de182c --- /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 0000000000000..475e14e8df7d3 --- /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 0000000000000..3322246e37ca3 --- /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 0000000000000..3b419dbc691c8 --- /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 0000000000000..b1a1315dd0c5c --- /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 0000000000000..78a4e7dff43ad --- /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 0000000000000..907b86c98c9c8 --- /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 0000000000000..3fa4d573b7d7f --- /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 0000000000000..e5bc468151ad7 --- /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 0000000000000..0241347c6135a --- /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 0000000000000..19ab1d994e6cb --- /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 0000000000000..9c18f3e6acac9 --- /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 faaa58e830047..3038b3b248716 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 0000000000000..fc5bfea6ada41 --- /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 a601378f5dfc5..b2e1c681299cd 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 0000000000000..a596df7098c51 --- /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); +}