@adyen/bento-mcp 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -0
- package/README.md +11 -66
- package/dist/assets/components/action-bar/action-bar.stories.ts +1 -1
- package/dist/assets/components/action-bar/action-bar.vue +1 -1
- package/dist/assets/components/ai-tag/ai-tag.docs.mdx +75 -0
- package/dist/assets/components/ai-tag/ai-tag.stories.ts +1 -0
- package/dist/assets/components/ai-tag/ai-tag.vue +1 -0
- package/dist/assets/components/anchor-scroller/anchor-scroller.docs.mdx +55 -0
- package/dist/assets/components/anchor-scroller/anchor-scroller.stories.ts +1 -0
- package/dist/assets/components/anchor-scroller/anchor-scroller.vue +1 -0
- package/dist/assets/components/anchor-scroller/components/anchor-scroller-list/anchor-scroller-list.vue +1 -0
- package/dist/assets/components/card/card.docs.mdx +3 -3
- package/dist/assets/components/data-grid/components/data-grid-cell-actions/data-grid-cell-actions.vue +1 -1
- package/dist/assets/components/data-grid/components/data-grid-tag-with-text-cell/data-grid-tag-with-text-cell.docs.mdx +35 -0
- package/dist/assets/components/data-grid/components/data-grid-tag-with-text-cell/data-grid-tag-with-text-cell.stories.ts +1 -0
- package/dist/assets/components/data-grid/components/data-grid-tag-with-text-cell/data-grid-tag-with-text-cell.vue +1 -0
- package/dist/assets/components/data-grid/data-grid.docs.mdx +29 -9
- package/dist/assets/components/data-grid/data-grid.stories.ts +1 -1
- package/dist/assets/components/data-grid/data-grid.vue +1 -1
- package/dist/assets/components/date-picker/date-picker.vue +1 -1
- package/dist/assets/components/date-range-picker/components/date-range-picker-calendar/date-range-picker-calendar.vue +1 -1
- package/dist/assets/components/date-range-picker/date-range-picker.vue +1 -1
- package/dist/assets/components/draggable-handle/draggable-handle.docs.mdx +61 -0
- package/dist/assets/components/draggable-handle/draggable-handle.stories.ts +1 -0
- package/dist/assets/components/draggable-handle/draggable-handle.vue +1 -0
- package/dist/assets/components/dropdown/components/dropdown-base-textbox/dropdown-base-textbox.vue +1 -1
- package/dist/assets/components/dropdown/components/dropdown-default-textbox/dropdown-default-textbox.vue +1 -1
- package/dist/assets/components/dropdown/components/dropdown-small-textbox/dropdown-small-textbox.vue +1 -1
- package/dist/assets/components/dropdown/dropdown.docs.mdx +3 -3
- package/dist/assets/components/dropdown/dropdown.vue +1 -1
- package/dist/assets/components/file-uploader/components/file-uploader-file-card/file-uploader-file-card.vue +1 -1
- package/dist/assets/components/file-uploader/file-uploader.vue +1 -1
- package/dist/assets/components/filter-bar/components/date-range-filter/date-range-filter.vue +1 -1
- package/dist/assets/components/filter-bar/filter-bar.docs.mdx +1 -1
- package/dist/assets/components/form-layout/components/form-layout-title/form-layout-title.vue +1 -1
- package/dist/assets/components/form-layout/form-layout.docs.mdx +2 -2
- package/dist/assets/components/inspector/inspector.docs.mdx +2 -2
- package/dist/assets/components/internal/copy/copy.docs.mdx +2 -2
- package/dist/assets/components/internal/dialog-page/dialog-page.vue +1 -1
- package/dist/assets/components/internal/fixed-scroller/fixed-scroller.vue +1 -1
- package/dist/assets/components/internal/listbox/components/listbox-multi-select/components/listbox-multi-select-options/listbox-multi-select-options.vue +1 -1
- package/dist/assets/components/layout/layout.docs.mdx +8 -8
- package/dist/assets/components/layout/layout.vue +1 -1
- package/dist/assets/components/modal/components/base-modal/base-modal.vue +1 -1
- package/dist/assets/components/modal/modal.docs.mdx +4 -4
- package/dist/assets/components/modal-fullscreen/modal-fullscreen.docs.mdx +5 -5
- package/dist/assets/components/modal-fullscreen/modal-fullscreen.vue +1 -1
- package/dist/assets/components/secondary-nav/components/secondary-nav-item/secondary-nav-item.vue +1 -1
- package/dist/assets/components/sidepanel/sidepanel.docs.mdx +27 -27
- package/dist/assets/components/sidepanel/sidepanel.vue +1 -1
- package/dist/assets/components/stepper/stepper.docs.mdx +8 -1
- package/dist/assets/components/stepper/stepper.stories.ts +1 -1
- package/dist/assets/components/summary-grid/summary-grid.docs.mdx +1 -1
- package/dist/assets/components/tabs/tabs.docs.mdx +1 -1
- package/dist/assets/components/toast/toast.docs.mdx +10 -10
- package/dist/assets/index.ts +1 -1
- package/dist/main.js +501 -427
- package/package.json +5 -4
|
@@ -1 +1 @@
|
|
|
1
|
-
import { type BentoColumn, BentoColumnOverflow, type BentoDatagridData, type BentoDatagridDataItem, type BentoDatagridDataItemId, BentoDatagridEvent, type BentoDatagridPaginationProps, type BentoDatagridSelection, type BentoDatagridSortByColumn, BentoDatagridSortDirection, } from './data-grid.types'; import { BentoCard } from '@/components/card'; import { action } from '@storybook/addon-actions'; import { type BentoFilterBarModel, type BentoFilterBarSearchBarProps, BentoFilterItemType, type BentoFilterValues, } from '../filter-bar'; import { BentoCurrency } from '@/components/currency'; import { BentoStatus, BentoStatusVariant } from '@/components/status'; import { BentoLink } from '@/components/link'; import { BentoTypography } from '@/components/typography'; import { computed, ref, watch } from 'vue'; import { storybookDocsParameter } from '@/utils/ts/storybook'; import { BentoDate, BentoDateFormat } from '@/components/date'; import { BentoCountry, BentoCountryCode } from '@/components/country'; import type { Meta, StoryObj } from '@storybook/vue'; import BentoDataGrid from './data-grid.vue'; import { isVue2 } from 'vue-demi'; import { BentoButton } from '../button'; import { BentoDashedUnderline } from '../dashed-underline'; import { BentoDataGridSummaryCell, BentoDataGridTrendCell } from './components'; import type { BentoColumnCellAction, BentoColumnCellActionEvent, BentoColumnCellActionsProp, BentoDataGridRowActionsProp, } from './components/data-grid-cell-actions/data-grid-cell-actions.types'; // Examples import DataGridEverythingBagel from './__tests__/data-grid-everything-bagel.vue?raw'; import DataGridFitContentInCardExample from './__tests__/data-grid-fit-content-in-card-example.vue?raw'; import DataGridWithSlotsExample from './__tests__/data-grid-with-slots-example.vue?raw'; import DataGridFilteringExample from './__tests__/data-grid-filtering-example.vue?raw'; import DataGridSelectionExample from './__tests__/data-grid-selection-example.vue?raw'; import DataGridTrendCellExample from './__tests__/data-grid-trend-cell-example.vue?raw'; const meta: Meta = { title: 'Data Grid', component: BentoDataGrid, argTypes: { // Values lazyLoadType: { control: { type: 'select', }, }, // Filter bar props filters: { table: { type: { summary: 'BentoFilterBarModel' }, }, }, filterSearchTerm: { name: 'filterSearchTerm', defaultValue: undefined, description: "Filter bar search term useful to filter all the data by text. If the value is non-existant (`null` or `undefined`) the search bar won't be enabled.", control: { type: 'text', }, }, // Slots default: { table: { disable: true }, }, header: { name: 'header', defaultValue: '', description: 'header text', table: { type: { summary: 'VNode[]' }, }, control: { type: 'text', }, }, footer: { name: 'footer', defaultValue: '', description: 'footer text', table: { type: { summary: 'VNode[]' }, }, control: { type: 'text', }, }, 'head-{columnKey}': { name: 'head-{columnKey}', defaultValue: '', description: 'the column slot where `{columnKey}` is the name of the column key. Allows the Column header to be customised.', table: { type: { summary: 'VNode[]' }, category: 'slots', }, control: { disable: true }, }, 'item-{columnKey}': { name: 'item-{columnKey}', defaultValue: '', description: 'the data item slot where `{columnKey}` is the name of the data item column key. Allow row cells to be customised.', table: { type: { summary: 'VNode[]' }, category: 'slots', }, control: { disable: true }, }, ['pagination-slot']: { control: { type: 'text' }, description: 'Passes content to the default pagination slot', table: { type: { summary: 'VNode[]' }, }, }, // Events [BentoDatagridEvent.COLUMN_RESIZE]: { name: BentoDatagridEvent.COLUMN_RESIZE, defaultValue: '', description: 'Triggered when a column is resized. Provides a field name and the new resized width in pixels.', table: { category: 'events', type: { summary: '({ field, newResizedWidth }) => void' }, }, control: { disable: true }, }, [BentoDatagridEvent.SELECT]: { name: BentoDatagridEvent.SELECT, defaultValue: '', description: 'Triggered when a row is selected. Provides an array of data item ids. `BentoDatagridDataItemId[]`', table: { category: 'events', type: { summary: '(selectedDataItemIds) => void' }, }, control: { disable: true }, }, [BentoDatagridEvent.NAVIGATE]: { name: BentoDatagridEvent.NAVIGATE, defaultValue: '', description: `Triggered when the navigation controls in the pagination are clicked (i.e. when the arrow buttons to the rightmost of the screen or the dropdown on the right hand are updated). It indicates to which page it should go next.`, table: { category: 'events', type: { summary: '(pageNumber: number) => void' }, }, control: { disable: true }, }, [BentoDatagridEvent.ITEMS_PAGE]: { name: BentoDatagridEvent.ITEMS_PAGE, defaultValue: '', description: `Triggered when the number of items per page is changed from the left hand dropdown of the pagination. It indicates the new number of items that will be displayed.`, table: { category: 'events', type: { summary: '(itemsPerPage: number) => void' }, }, control: { disable: true }, }, [BentoDatagridEvent.SORT]: { name: BentoDatagridEvent.SORT, defaultValue: '', description: `Triggered when a column is sorted. Provides an array of \`BentoDatagridSortByColumn\` which includes the \`field\` and the sort \`direction\` (\`BentoDatagridSortDirection\`) of the column:`, table: { category: 'events', type: { summary: '(sortByDirections) => void' }, }, control: { disable: true }, }, [BentoDatagridEvent.UPDATE_COLUMNS]: { name: BentoDatagridEvent.UPDATE_COLUMNS, defaultValue: '', description: `Triggered when a the columns have been updated. Provides an updated array of columns \`BentoColumn[]\` which should be passed back into the \`columns\` prop (easily achieved using the [sync modifier](https://v2.vuejs.org/v2/guide/components-custom-events.html#sync-Modifier)) e.g. \`:columns.sync="columns"\``, table: { category: 'events', type: { summary: '(columns) => void' }, }, control: { disable: true }, }, [BentoDatagridEvent.UPDATE_CONDENSED]: { name: 'update:condensed', defaultValue: '', description: `Triggered when the config panel's condensed view toggle is interacted with. Provides the updated condensed \`boolean\`. (easily achieved using the [sync modifier](https://v2.vuejs.org/v2/guide/components-custom-events.html#sync-Modifier)) e.g. \`:condensed.sync="condensed"\``, table: { category: 'events', type: { summary: '(condensed) => void' }, }, control: { disable: true }, }, [BentoDatagridEvent.UPDATE_FILTER_VALUES]: { name: BentoDatagridEvent.UPDATE_FILTER_VALUES, defaultValue: '', description: `Triggered when a the filters in the filterbar have updated. Provides an array of filters \`BentoFilterBarModel[]\` which should be passed back into the \`filter-values\` prop (easily achieved using the [sync modifier](https://v2.vuejs.org/v2/guide/components-custom-events.html#sync-Modifier)) e.g. \`:filter-values.sync="filtersValues"\``, table: { category: 'events', type: { summary: '(filterValues) => void' }, }, control: { disable: true }, }, [BentoDatagridEvent.UPDATE_PAGINATION]: { name: BentoDatagridEvent.UPDATE_PAGINATION, defaultValue: '', description: `Triggered when a the pagination state has been updated. Provides an updated \`pagination\` properties object which should be passed back into the \`pagination\` prop (easily achieved using the [sync modifier](https://v2.vuejs.org/v2/guide/components-custom-events.html#sync-Modifier)) e.g. \`:pagination.sync="pagination"\``, table: { category: 'events', type: { summary: '(paginationProps) => void' }, }, control: { disable: true }, }, [BentoDatagridEvent.UPDATE_SELECTION]: { name: BentoDatagridEvent.UPDATE_SELECTION, defaultValue: '', description: `Triggered when a the select data rows state has been updated. Provides an array of row IDs \`BentoDatagridDataItemId[]\` which should be passed back into the \`selection\` prop (easily achieved using the [sync modifier](https://v2.vuejs.org/v2/guide/components-custom-events.html#sync-Modifier)) e.g. \`:selection.sync="selection"\``, table: { category: 'events', type: { summary: '(selection) => void' }, }, control: { disable: true }, }, [BentoDatagridEvent.UPDATE_SORT_BY]: { name: 'update:sort-by', defaultValue: '', description: `Triggered when a the sorting state has been updated. Provides an array of sorting directions \`BentoDatagridSortByColumn[]\` which should be passed back into the \`sort-by\` prop (easily achieved using the [sync modifier](https://v2.vuejs.org/v2/guide/components-custom-events.html#sync-Modifier)) e.g. \`:sort-by.sync="sortBy"\``, table: { category: 'events', type: { summary: '(sortingDirections) => void' }, }, control: { disable: true }, }, // Filter bar search events [BentoDatagridEvent.FILTER_SEARCH_INPUT]: { name: BentoDatagridEvent.FILTER_SEARCH_INPUT, defaultValue: '', description: "Triggered whenever the filter's search is updated.", table: { category: 'events', type: { summary: '(newSearchTerm) => void' } }, control: { disable: true }, }, [BentoDatagridEvent.FILTER_SEARCH_CLEAR]: { name: BentoDatagridEvent.FILTER_SEARCH_CLEAR, defaultValue: '', description: "Triggered whenever the filter's search is cleared.", table: { category: 'events', type: { summary: '() => void' }, }, control: { disable: true }, }, [BentoDatagridEvent.UPDATE_FILTER_SEARCH_TERM]: { name: BentoDatagridEvent.UPDATE_FILTER_SEARCH_TERM, defaultValue: '', description: "Triggered whenever the filter's search is updated to sync the model.", table: { category: 'events', type: { summary: '(newSearchTerm) => void' }, }, control: { disable: true }, }, [BentoDatagridEvent.ROW_CLICK]: { name: BentoDatagridEvent.ROW_CLICK, defaultValue: '', description: 'Triggered whenever a row has been clicked.', table: { category: 'events', type: { summary: '(row: BentoDatagridDataItem, event: MouseEvent) => void' }, }, }, [BentoDatagridEvent.NESTED_CONTENT_TOGGLED]: { name: BentoDatagridEvent.NESTED_CONTENT_TOGGLED, defaultValue: '', description: 'Triggered whenever a data item toggled is nested content', table: { category: 'events', type: { summary: '(dataItem: BentoDatagridDataItem) => void' }, }, control: { disable: true }, }, }, }; export default meta; type Story = StoryObj<typeof BentoDataGrid>; const DEFAULT_COLUMNS: Array<BentoColumn> = [ { field: 'name', label: 'Name', minWidth: 300, mandatory: true }, { field: 'company', label: 'Company', autoWidth: true }, { field: 'payment', label: 'Payment', minWidth: 350 }, ]; const DEFAULT_DATA: BentoDatagridData = [ { id: '03c0a799-1ced-4017-aea3-0cebb0106335', name: 'Shepherd Kane', payment: 160405, company: 'Merchant Helium', registered: '2023-06-30T13:56:09.498Z', tags: ['deserunt', 'et', 'esse'], reference: 'UnbQaUncf3eR1D8XUBqn_UnbQaUncf3eR1D8XUBqn_UnbQaUncf3eR1D8XUBqn', paymentStatus: 1, countryCode: BentoCountryCode.NL, }, { id: 'b6d4f1fd-b1f6-40a6-a443-f785c89604ef', name: 'Santos Palmer', payment: 163500, company: 'Merchant B', registered: '2023-04-28T13:11:09.498Z', tags: ['et', 'Lorem', 'sint'], reference: 'IxUZ9FsEPCZ9dFFPpI9C', paymentStatus: 2, countryCode: BentoCountryCode.BE, }, { id: '8f9e7877-1ca2-4929-9217-f1ec78709291', name: 'Joyce Johns', payment: 78500, company: 'Merchant C', registered: '2023-06-30T11:01:09.498Z', tags: ['laborum', 'enim', 'ullamco'], reference: 'y0qAQ0WDszRyqLY2WO9f', paymentStatus: 1, countryCode: BentoCountryCode.UA, }, { id: '0789a012-85a6-4657-87bb-319141938a61', name: 'Lynnette Melendez', payment: -45000, company: 'STRALOY', registered: '2019-08-01T13:56:01.498Z', tags: ['sit', 'consectetur', 'amet'], reference: '2vv5pzbRSwx8YsFoGGOC', paymentStatus: 1, countryCode: BentoCountryCode.BR, }, { id: '0b3981b6-c73b-47ad-81d1-b06f9b95d467', name: 'Wagner Mcintosh', payment: 1112, company: 'Merchant A', registered: '2022-12-30T02:12:09.498Z', tags: ['ad', 'ullamco', 'et'], reference: '54KN4jkpBuaRY5xMfe84', paymentStatus: 1, countryCode: BentoCountryCode.US, }, { id: '51d2b31d-d577-4fca-8329-a583babbbdda', name: 'Evans Zamora', payment: 1153.4, company: 'GEEKWAGON', registered: '2023-06-30T13:51:09.498Z', tags: ['consequat', 'magna', 'sit'], reference: 'Sj84kad0MGxpuozTjEk', paymentStatus: 0, countryCode: BentoCountryCode.FR, }, { id: '9bd59a6e-bd03-4477-813f-b844953b7645', name: 'Anderson Middleton', payment: 181722.23, company: 'BALOOBA', registered: '2021-03-02T01:34:09.498Z', tags: ['aliqua', 'adipisicing', 'magna'], reference: 'MtJ5Bnew0MGxpuozTjEk', paymentStatus: 1, countryCode: BentoCountryCode.IT, }, ]; const ADDITIONAL_DATA: BentoDatagridData = [ { id: '03c0a799-1ced-4017-aea3-0cebb0106335', name: 'Additional data fetched here', payment: 160405, company: 'Merchant Helium', registered: '2023-06-30T13:56:09.498Z', tags: ['deserunt', 'et', 'esse'], reference: 'UnbQaUncf3eR1D8XUBqn_UnbQaUncf3eR1D8XUBqn_UnbQaUncf3eR1D8XUBqn', paymentStatus: 1, countryCode: BentoCountryCode.NL, }, { id: 'b6d4f1fd-b1f6-40a6-a443-f785c89604ef', name: 'Santos Palmer', payment: 163500, company: 'Merchant B', registered: '2023-04-28T13:11:09.498Z', tags: ['et', 'Lorem', 'sint'], reference: 'IxUZ9FsEPCZ9dFFPpI9C', paymentStatus: 2, countryCode: BentoCountryCode.BE, }, { id: '8f9e7877-1ca2-4929-9217-f1ec78709291', name: 'Joyce Johns', payment: 78500, company: 'Merchant C', registered: '2023-06-30T11:01:09.498Z', tags: ['laborum', 'enim', 'ullamco'], reference: 'y0qAQ0WDszRyqLY2WO9f', paymentStatus: 1, countryCode: BentoCountryCode.UA, }, { id: '0789a012-85a6-4657-87bb-319141938a61', name: 'Lynnette Melendez', payment: -45000, company: 'STRALOY', registered: '2019-08-01T13:56:01.498Z', tags: ['sit', 'consectetur', 'amet'], reference: '2vv5pzbRSwx8YsFoGGOC', paymentStatus: 1, countryCode: BentoCountryCode.BR, }, { id: '0b3981b6-c73b-47ad-81d1-b06f9b95d467', name: 'Wagner Mcintosh', payment: 1112, company: 'Merchant A', registered: '2022-12-30T02:12:09.498Z', tags: ['ad', 'ullamco', 'et'], reference: '54KN4jkpBuaRY5xMfe84', paymentStatus: 1, countryCode: BentoCountryCode.US, }, { id: '51d2b31d-d577-4fca-8329-a583babbbdda', name: 'Evans Zamora', payment: 1153.4, company: 'GEEKWAGON', registered: '2023-06-30T13:51:09.498Z', tags: ['consequat', 'magna', 'sit'], reference: 'Sj84kad0MGxpuozTjEk', paymentStatus: 0, countryCode: BentoCountryCode.FR, }, { id: '9bd59a6e-bd03-4477-813f-b844953b7645', name: 'Anderson Middleton', payment: 181722.23, company: 'BALOOBA', registered: '2021-03-02T01:34:09.498Z', tags: ['aliqua', 'adipisicing', 'magna'], reference: 'MtJ5Bnew0MGxpuozTjEk', paymentStatus: 1, countryCode: BentoCountryCode.IT, }, ]; const DEFAULT_ARGS = { data: DEFAULT_DATA, columns: DEFAULT_COLUMNS, allowColumnDragAndDrop: false, pagination: {}, emptyState: { title: 'No results were found', image: 'no-results-found', variant: 'embedded', action: { title: 'Clear filters', event: action('clear'), }, description: 'Try a different term or reset search filters', }, }; // Default const DefaultCode = ` <template> <bento-data-grid :allow-row-clicks="false" :allow-column-drag-and-drop="false" :bulk-actions="undefined" :condensed="false" :columns="columns" :column-panel="false" :data="data" :empty-state="undefined" :filters="undefined" :filters-search-term="undefined" :fit-content="false" :get-data-item-id="undefined" :has-resizeable-columns="false" :has-nested-content="false" :highlighted-rows="undefined" :is-data-item-disabled="undefined" lazy-load-type="none" :loading="false" :outline="false" :no-spacing-top="false" :pagination="undefinded" :selectable-accross-pages="false" :sticky-pagination="false" :sticky-filters="false" :selectable="false" :selection="undefined" :sort-by="undefined" :tab-title="undefined" :truncate-column-headers="false" /> </template> <script setup lang="ts"> import { BentoColumn, BentoDataGrid, BentoDatagridData } from '@adyen/bento-vue2'; import { ref } from 'vue'; const data = ref<BentoDatagridData>(${JSON.stringify(DEFAULT_DATA)}); const columns = ref<BentoColumn[]>([ { field: 'name', label: 'Name', minWidth: 300, mandatory: true }, { field: 'company', label: 'Reference' }, { field: 'payment', label: 'Payment', width: 200, minWidth: 150 }, ]); </script> `; export const Default: Story = { render: (_args, { argTypes }) => ({ components: { BentoDataGrid }, props: Object.keys(argTypes), template: ` <bento-data-grid v-bind="args" /> `, setup(props) { return { args: isVue2 ? props : _args }; }, }), args: DEFAULT_ARGS, parameters: storybookDocsParameter(DefaultCode), }; // Fluid Columns const FluidTemplateCode = ` <template> <bento-data-grid :data="data" :columns="columns" /> </template> <script setup lang="ts"> import { BentoColumn, BentoDataGrid, BentoDatagridData } from '@adyen/bento-vue2'; import { ref } from 'vue'; const data = ref<BentoDatagridData>(${JSON.stringify(DEFAULT_DATA)}); const columns = ref<BentoColumn[]>([ { field: 'name', label: 'Name', flex: 2, mandatory: true }, { field: 'company', label: 'Company', flex: 1 }, { field: 'payment', label: 'Payment', flex: 1, minWidth: 200 }, ]); </script> `; const rowActionsFluid: BentoDataGridRowActionsProp = (item: BentoDatagridDataItem) => { if (item.id === DEFAULT_DATA[3].id) { return [ { variant: 'edit', event: action('edit'), }, ]; } else if (item.id === DEFAULT_DATA[2].id) { return [ { variant: 'edit', event: action('edit') }, { variant: 'open', isShort: true, menuData: [ { text: 'Cancel payment', event: action('cancel payment'), critical: true, }, { text: 'Transfer payment', event: action('transfer payment'), }, { text: 'Delete payment', event: action('delete payment'), disabled: true, }, ], }, ]; } else if (item.id === DEFAULT_DATA[1].id) { return []; } return [ { variant: 'edit', event: action('edit'), }, { title: 'Delete', critical: true, event: action('Delete'), }, ]; }; export const Fluid: Story = { ...Default, args: { ...DEFAULT_ARGS, columns: [ { field: 'name', label: 'Name', flex: 2, mandatory: true }, { field: 'company', label: 'Company', flex: 1 }, { field: 'payment', label: 'Payment', flex: 1, minWidth: 200 }, ], rowActions: rowActionsFluid, }, parameters: storybookDocsParameter(FluidTemplateCode), }; // Slots export const Slots: Story = { render: (_args, { argTypes }) => ({ components: { BentoDataGrid, BentoLink, BentoTypography, }, props: Object.keys(argTypes), template: ` <bento-data-grid v-bind="args" > <template #header> <bento-typography variant="title" el="h2"> Users </bento-typography> </template> <template #head-name="{column}"> <bento-typography variant="caption"> {{ column.label }} </bento-typography> </template> <template #item-name="{item}"> <bento-typography variant="body"> {{ item.name }} </bento-typography> <template v-if="item.company === 'Merchant Helium'"> <div style="display: inline-flex; gap: 4px;"> <bento-typography v-for="itemTag in item.tags" :key="itemTag" variant="caption" :style="{ color: 'var(--b-color-label-secondary)' }" > { {{ itemTag }} } </bento-typography> </div> </template> </template> <template #item-company="{item}"> <bento-link isNotRouting to="https://www.adyen.com" :title="item.company" > {{ item.company }} </bento-link> <bento-typography v-if="item.company === 'Merchant B'" variant="caption" :style="{ color: 'var(--b-color-label-secondary)' }"> {{ item.registered }} </bento-typography> </template> <template #item-payment="{item}"> $ {{ item.payment }} </template> <template #footer> <bento-typography variant="caption"> Footer </bento-typography> </template> </bento-data-grid> `, setup(props) { return { args: isVue2 ? props : _args }; }, }), args: { ...DEFAULT_ARGS, columns: [ { field: 'name', label: 'Name', flex: 2, mandatory: true }, { field: 'company', label: 'Company', flex: 1 }, { field: 'payment', label: 'Payment', flex: 1, minWidth: 200 }, ], }, parameters: storybookDocsParameter(DataGridWithSlotsExample), }; // Sorting const useSort = () => { const sortBy = ref<Array<BentoDatagridSortByColumn>>([]); const data = ref<BentoDatagridData>(DEFAULT_DATA); const sort = (sortedColumns: Array<BentoDatagridSortByColumn>) => { data.value = sortedColumns[0].direction === BentoDatagridSortDirection.ASCENDING ? data.value.sort((a, b) => (a[sortedColumns[0].field] as string).localeCompare(b[sortedColumns[0].field] as string) ) : data.value .sort((a, b) => (a[sortedColumns[0].field] as string).localeCompare(b[sortedColumns[0].field] as string) ) .reverse(); action('sort')(sortedColumns); }; return { data, sortBy, sort }; }; const SortingTemplateCode = ` <template> <bento-data-grid :data="data" :columns="columns" @sort="sort" :sort-by.sync="sortBy" /> </template> <script setup lang="ts"> import { BentoDataGrid, type BentoDatagridSortByColumn, type BentoDatagridData, type BentoColumn, } from '@adyen/bento-vue2' // Sortable fields must have "sortable: true" const columns: BentoColumn = [ { field: 'id', label: 'ID', minWidth: 300, sortable: true, mandatory: true }, { field: 'name', label: 'Name', minWidth: 300, mandatory: true }, ] const data = ref<BentoDatagridData>([ { id: 1, name: 'First' }, { id: 2, name: 'Second' }, ]); const sortBy = ref<Array<BentoDatagridSortByColumn>>([]); const sort = (sortedColumns: Array<BentoDatagridSortByColumn>) => { data.value = sortedColumns[0].direction === BentoDatagridSortDirection.ASCENDING ? data.value.sort((a, b) => (a[sortedColumns[0].field] as number) .toString() .localeCompare(b[sortedColumns[0].field] as string) ) : data.value .sort((a, b) => (a[sortedColumns[0].field] as number) .toString() .localeCompare(b[sortedColumns[0].field] as string) ) .reverse(); } </script> `; export const Sorting: Story = { render: (_args, { argTypes }) => ({ components: { BentoDataGrid }, props: Object.keys(argTypes), template: ` <bento-data-grid :data="data" :columns="columns" :sort-by.sync="sortBy" @sort="sort" /> `, setup(props) { const { data, sortBy, sort } = useSort(); return { args: isVue2 ? props : _args, data, sortBy, sort, }; }, }), args: { ...DEFAULT_ARGS, columns: [ { field: 'name', label: 'Name', minWidth: 300, sortable: true, mandatory: true }, { field: 'company', label: 'Company', minWidth: 150, sortable: true }, { field: 'payment', label: 'Payment', width: 200, minWidth: 150 }, ], sortBy: [], }, parameters: storybookDocsParameter(SortingTemplateCode), }; export const Selection: Story = { render: (_args, { argTypes }) => ({ components: { BentoDataGrid }, props: Object.keys(argTypes), template: ` <div> <bento-data-grid v-bind="args" :data="pagedData" :pagination="pagination" :filters="filtersConfig" :filter-values="filterValues" :is-data-item-disabled="isDataItemDisabled" :selection="selection" :filter-search-term="searchTerm" @update:filter-search-term="searchTerm = $event" @update:pagination="pagination = $event" @update:filter-values="filterValues = $event" @select-all-across-pages="onSelectAllAcrossPages" @select-all-current-results="onSelectAllInCurrentResults" @deselect-all-current-results="onDeselectAllInCurrentResults" @filter-search-clear="onSearchCleared" @update:selection="onSelect" @row-click="rowClickAction" /> </div> `, setup(props) { const args = isVue2 ? props : _args; const selection = ref([]); const filtersConfig = computed<BentoFilterBarModel>(() => args?.filters ?? FILTERS); const filterValues = ref<BentoFilterValues>([]); const searchTerm = ref(''); const data = ref( Array.from({ length: 20 }, (_, i) => ({ id: `id-${i + 1}`, name: `Name ${i + 1}`, payment: (i + 1) * 100, company: `Merchant ${i + 1}`, })) ); const isDataItemDisabled = (row: BentoDatagridDataItem) => { return row.id === data.value[3].id; }; const pagination = ref<BentoDatagridPaginationProps>({ page: 1, size: 10, totalCount: 20, hasNext: true, }); const filteredData = computed<BentoDatagridData>(() => { let internalData = data.value; if (searchTerm.value) { const lowerCaseSearchTerm = searchTerm.value.toLowerCase(); const searchableFields = args.columns.map(column => column.field); internalData = internalData.filter((item: BentoDatagridDataItem) => { return searchableFields.some(field => { const value = String(item[field]); return value.toLowerCase().includes(lowerCaseSearchTerm); }); }); } return internalData.filter(item => filterValues.value.every(filter => filter.value ? String(item[filter.field]).toLowerCase().includes(String(filter.value).toLowerCase()) : true ) ); }); const pagedData = computed<BentoDatagridData>(() => { return filteredData.value.slice( (pagination.value.page - 1) * pagination.value.size, pagination.value.page * pagination.value.size ); }); const enabledIds = computed(() => data.value.filter(item => !isDataItemDisabled(item)).map(x => x.id)); const enabledFilteredIds = computed(() => filteredData.value.filter(item => !isDataItemDisabled(item)).map(x => x.id) ); const onSelect = value => { selection.value = value; action('update:selection')(value); }; const onSelectAllAcrossPages = () => { selection.value = enabledIds.value; action('select-all-across-pages')(); }; const onSelectAllInCurrentResults = () => { selection.value = [...new Set([...selection.value, ...enabledFilteredIds.value])]; action('select-all-current-results')(); }; const onDeselectAllInCurrentResults = () => { selection.value = selection.value.filter(item => !enabledFilteredIds.value.includes(item)); action('deselect-all-current-results')(); }; const onSearchCleared = () => { searchTerm.value = ''; action('search-clear')(); }; watch( filteredData, () => { pagination.value.totalCount = filteredData.value.length; }, { immediate: true } ); return { args, onSelect, onSelectAllAcrossPages, onSelectAllInCurrentResults, onDeselectAllInCurrentResults, pagination, selection, pagedData, filtersConfig, filterValues, searchTerm, rowClickAction: value => action('row-click')(value), isDataItemDisabled, onSearchCleared, }; }, }), args: { ...DEFAULT_ARGS, selectable: true, allowRowClicks: true, selectableAcrossPages: true, bulkActions: [ { title: 'Edit', event: () => true, }, { title: 'Delete', critical: true, event: () => true, }, ], }, parameters: storybookDocsParameter(DataGridSelectionExample), }; // Filtering const FILTERS: BentoFilterBarModel = [ { field: 'name', label: 'Name', type: BentoFilterItemType.INPUT, }, { field: 'company', label: 'Company', type: BentoFilterItemType.INPUT, }, ]; export const Filtering: Story = { render: (_args, { argTypes }) => ({ components: { BentoDataGrid }, props: Object.keys(argTypes), setup(props) { const args = isVue2 ? props : _args; const filtersConfig = computed<BentoFilterBarModel>(() => args?.filters ?? FILTERS); const filterValues = ref<BentoFilterValues>([]); const searchTerm = ref(''); const searchConfig = ref<BentoFilterBarSearchBarProps>({ inputFieldAriaLabel: 'Search for items', placeholder: 'Placeholder', hint: 'Hint', }); const dataSet = computed<BentoDatagridData>(() => { let filteredData = args.data; // Apply global search term filter if (searchTerm.value) { const lowerCaseSearchTerm = searchTerm.value.toLowerCase(); const searchableFields = args.columns.map(column => column.field); filteredData = filteredData.filter((item: BentoDatagridDataItem) => { return searchableFields.some(field => { const value = String(item[field]); return value.toLowerCase().includes(lowerCaseSearchTerm); }); }); } filteredData = filteredData.filter(item => filterValues.value?.every(filter => { return filter.value ? String(item[filter.field]).toLowerCase().includes(String(filter.value).toLowerCase()) : true; }) ); return filteredData; }); watch(filterValues, () => { action('filter-values:update')(filterValues.value); }); const onSearchCleared = action('search-clear'); const onSearchInput = action('search-input'); const onSearchTermUpdated = action('update:search-term'); return { // Values dataSet, filtersConfig, filterValues, searchTerm, searchConfig, // Methods onSearchCleared, onSearchInput, onSearchTermUpdated, // Vue 3 storybook fix args, }; }, template: ` <bento-data-grid v-bind="args" :data="dataSet" :filters="filtersConfig" :filter-values.sync="filterValues" :filter-search-term.sync="searchTerm" :filter-search-config="searchConfig" @filter-search-clear="onSearchCleared" @filter-search-input="onSearchInput" @update:filter-search-term="onSearchTermUpdated" /> `, }), args: DEFAULT_ARGS, parameters: storybookDocsParameter(DataGridFilteringExample), }; // No Results const NoResultsTemplateCode = ` <bento-data-grid :data="[]" :columns="[ { field: 'name', label: 'Name', minWidth: 300, mandatory: true }, { field: 'company', label: 'Reference' }, { field: 'payment', label: 'Payment', width: 200, minWidth: 150 }, ]" :empty-state="{ title: 'No results were found', image: 'no-results-found', variant: 'embedded', action: { title: 'Clear filters', event: clearFiltersFunction, }, description: 'Try a different term or reset search filters', }" /> `; export const NoResults: Story = { ...Filtering, args: { ...DEFAULT_ARGS, data: [], filters: FILTERS, filterValues: [{ field: 'company', value: 'CIA' }], }, parameters: storybookDocsParameter(NoResultsTemplateCode), }; // Pagination const PaginationTemplateCode = ` <template> <bento-data-grid :data="dataGridData" :columns="columnsConfig" :pagination.sync="pagination" @update:pagination="onUpdatePagination" /> </template> <script lang="ts" setup> const data="[{ id: '1', name: 'Jane' }, { id: '1', name: 'John' }]" const pagination = ref({ description: 'datagrid navigation', hasNext: true, size: 10 }); // With API call watch( () => pagination.value, () => { makeAPICall(pagination.value); } ); // Without API call const onUpdatePagination = (newPagination: BentoDatagridPaginationProps) => { pagination.value = { ...newPagination }; const newSetStartIndex = pagination.value.page * pagination.value.size; const amountOfItems = newSetStartIndex + pagination.value.size - 1; data.value = arrayWithFullSetOfValues.slice(newSetStartIndex, amountOfItems); }; </script> `; export const Pagination: Story = { render: (_args, { argTypes }) => ({ components: { BentoDataGrid }, props: Object.keys(argTypes), template: ` <bento-data-grid v-bind="args" :data="dataGridData" :pagination.sync="pagination" @update:pagination="updatePagination" @items-page="updateItemsPerPage" /> `, setup(props) { const args = isVue2 ? props : _args; const largeData = [ ...DEFAULT_ARGS.data, ...DEFAULT_ARGS.data.map(dataItem => ({ ...dataItem, id: `${dataItem.id}+1` })), ]; const pagination = ref(args.pagination); const dataGridData = computed(() => { const end = pagination.value.size <= largeData.length ? pagination.value.size : largeData.length; return largeData.slice(pagination.value.page - 1, end); }); const updateItemsPerPage = (itemsPerPage: number) => { action('items-page')(itemsPerPage); }; const updatePagination = (newPagination: BentoDatagridPaginationProps) => { action('navigate')(newPagination.page); action('update:pagination')(newPagination); }; return { // Values dataGridData, pagination, // Methods updateItemsPerPage, updatePagination, // Vue 3 storybook fixes args: isVue2 ? props : _args, }; }, }), args: { ...DEFAULT_ARGS, pagination: { hasNext: true, description: 'datagrid navigation', size: 10, page: 1, totalCount: DEFAULT_ARGS.data.length * 2, }, }, parameters: storybookDocsParameter(PaginationTemplateCode), }; // Payment Status (demo use case for datagrid V2) export const EverythingBagelExample: Story = { render: (_args, { argTypes }) => ({ components: { BentoDataGrid, BentoLink, BentoStatus, BentoCurrency, BentoDate, BentoCountry, BentoButton }, props: Object.keys(argTypes), template: ` <bento-data-grid v-bind="args" :data="filteredAndSortedData" :columns.sync="columns" :condensed="isCondensed" :filters="filtersConfig" :filter-values.sync="filterValues" :filter-search-term.sync="searchTerm" :sort-by="sortBy" @update:sort-by="sortBy = $event" @update:condensed="isCondensed = $event" :selection.sync="selection" :is-data-item-disabled="isDataItemDisabledForSelection" :row-actions="rowActions" selectable @row-click="onRowClick" > <template #item-paymentStatus="{item}"> <div style="display: flex"> <bento-status :variant="getPaymentStatus(item.paymentStatus)" /> <span>{{ getPaymentText(item.paymentStatus) }}</span> </div> </template> <template #item-reference="{item}"> <bento-link isNotRouting to="https://www.adyen.com" :title="item.reference" > {{ item.reference }} </bento-link> </template> <template #item-countryCode="{item}"> <bento-country :code="item.countryCode" /> </template> <template #item-payment="{item}"> <bento-currency currency="EUR" :value="item.payment" /> </template> <template #item-date="{item}"> <bento-date :date="new Date(item.registered)" :format='BentoDateFormat.FULL_DATE_TIME_WITH_PERIOD'/> </template> </bento-data-grid> `, setup(props) { const args = isVue2 ? props : _args; const columns = ref<Array<BentoColumn>>(args.columns); const filtersConfig = ref<BentoFilterBarModel>(args.filters); const filterValues = ref<BentoFilterValues>(args?.filterValues); const sortBy = ref<Array<BentoDatagridSortByColumn>>([]); const searchTerm = ref(''); const isCondensed = ref(args.condensed); const getPaymentStatus = (paymentStatus: number) => { switch (paymentStatus) { case 2: return BentoStatusVariant.GREEN; case 1: return BentoStatusVariant.YELLOW; case 0: default: return BentoStatusVariant.RED; } }; const getPaymentText = (paymentStatus: number) => { switch (paymentStatus) { case 2: return 'Paid'; case 1: return 'In progress'; case 0: default: return 'Not paid'; } }; const onRowClick = (item: BentoDatagridDataItem) => { action('row-click')(item); }; // Selection const isDataItemDisabledForSelection = (row: BentoDatagridDataItem) => row.id === DEFAULT_DATA[3].id; const selection = ref<BentoDatagridSelection>([DEFAULT_DATA[1].id]); // Filtering const filteredData = computed(() => // If any filters or search terms have been set filterValues.value?.length === 0 && searchTerm.value === '' ? args.data : args.data.filter(item => { const foundFilteredItem = filterValues.value?.length > 0 ? filterValues.value.every(filter => filter.value ? (item[filter.field] as string) .toLowerCase() .includes((filter.value as string).toLowerCase()) : true ) : true; const foundSearchFilteredItem = searchTerm.value ? (item?.company as string) .toLowerCase() .includes((searchTerm.value as string).toLowerCase()) : true; return foundFilteredItem && foundSearchFilteredItem; }) ); const filteredAndSortedData = computed(() => { if (sortBy.value?.length === 0) { return filteredData.value; } return sortBy.value[0].direction === BentoDatagridSortDirection.ASCENDING ? filteredData.value.sort((a, b) => (a[sortBy.value[0].field] as string).localeCompare(b[sortBy.value[0].field] as string) ) : filteredData.value .sort((a, b) => (a[sortBy.value[0].field] as string).localeCompare(b[sortBy.value[0].field] as string) ) .reverse(); }); const rowActions: BentoDataGridRowActionsProp = (item: BentoDatagridDataItem) => { if (item.id === DEFAULT_DATA[3].id) { return [ { variant: 'edit', event: action('edit'), }, ]; } else if (item.id === DEFAULT_DATA[2].id) { return [ { variant: 'edit', event: action('edit') }, { variant: 'open', isShort: true, menuData: [ { text: 'Cancel payment', event: action('cancel payment'), critical: true, }, { text: 'Transfer payment', event: action('transfer payment'), }, { text: 'Delete payment', event: action('delete payment'), disabled: true, }, ], }, ]; } else if (item.id === DEFAULT_DATA[1].id) { return []; } return [ { variant: 'edit', event: action('edit'), }, { title: 'Delete', critical: true, event: action('Delete'), }, ]; }; return { // Methods getPaymentStatus, getPaymentText, // Functions onRowClick, isDataItemDisabledForSelection, // Values columns, filteredAndSortedData, sortBy, filtersConfig, filterValues, searchTerm, isCondensed, selection, rowActions, // Enums BentoDateFormat, // Vue 3 storybook fix args, }; }, }), args: { ...DEFAULT_ARGS, zebraRows: true, filters: [ { field: 'company', label: 'Company', value: null, type: BentoFilterItemType.INPUT, }, { field: 'countryCode', label: 'Country/Region', value: undefined, options: { listboxItems: [ { value: BentoCountryCode.NL, }, { value: BentoCountryCode.BE, }, ], variant: 'name', }, type: BentoFilterItemType.SELECT_COUNTRY, }, ], columns: [ { field: 'company', label: 'Company', sortable: true, mandatory: true, autoWidth: true, frozen: true, minWidth: 200, }, { field: 'paymentStatus', label: 'Status', width: 180, infoIconTooltip: 'For current day' }, { field: 'reference', label: 'Reference', width: 320, overflow: BentoColumnOverflow.ELLIPSIS }, { field: 'countryCode', label: 'Country/Region', width: 320 }, { field: 'payment', label: 'Payment', width: 180, numeric: true }, { field: 'date', label: 'Execution Date', width: 200 }, ], allowRowClicks: true, allowColumnDragAndDrop: true, columnPanel: true, highlightedRows: [DEFAULT_DATA[0].id, DEFAULT_DATA[4].id], }, parameters: storybookDocsParameter(DataGridEverythingBagel), }; export const DisplayOptions = (_args, { argTypes }) => ({ components: { BentoDataGrid, BentoDate }, props: Object.keys(argTypes), template: ` <bento-data-grid v-bind="args" :columns.sync="columns" :condensed.sync="isCondensed"> <template #item-date="{item}"> <bento-date :date="new Date(item.registered)" :format='BentoDateFormat.FULL_DATE_TIME_WITH_PERIOD'/> </template> </bento-data-grid> `, setup(props) { const args = isVue2 ? props : _args; const isCondensed = ref(args.condensed); const columns = ref([ { field: 'name', label: 'Name', minWidth: 200, mandatory: true }, { field: 'reference', label: 'Reference', width: 320, overflow: BentoColumnOverflow.ELLIPSIS, frozen: true, }, { field: 'company', label: 'Company', minWidth: 150, width: 250, frozen: true }, { field: 'payment', label: 'Payment', width: 200, minWidth: 150 }, { field: 'countryCode', label: 'Country/Region', width: 280 }, { field: 'date', label: 'Execution Date', width: 300 }, ]); return { // Values columns, isCondensed, // Enums BentoDateFormat, // Vue 3 storybook fix args, }; }, }); DisplayOptions.args = { ...DEFAULT_ARGS, allowColumnDragAndDrop: true, columnPanel: true, }; DisplayOptions.parameters = storybookDocsParameter(` <bento-data-grid column-panel :data="dataArray" :columns.sync="columns" :condensed.sync="isCondensed" />`); export const GridInCard: Story = { render: (_args, { argTypes }) => ({ components: { BentoCard, BentoDataGrid, BentoDate }, props: Object.keys(argTypes), template: ` <div style="max-width: 500px;"> <bento-card> Grid in a card <template #content> <div style="height: 300px;"> <bento-data-grid v-bind="args" :columns.sync="columns" :condensed.sync="isCondensed" :pagination="pagination" fit-content outline > <template #item-date="{item}"> <bento-date :date="new Date(item.registered)" :format='BentoDateFormat.FULL_DATE_TIME_WITH_PERIOD'/> </template> </bento-data-grid> </div> </template> </bento-card> </div> `, setup(props) { const args = isVue2 ? props : _args; const isCondensed = ref(args.condensed); const pagination = ref({ hasNext: true, size: 20, page: 1, description: 'datagrid navigation', }); const columns = ref([ { field: 'name', label: 'Name', minWidth: 200, mandatory: true }, { field: 'reference', label: 'Reference', width: 320, overflow: BentoColumnOverflow.ELLIPSIS, }, { field: 'company', label: 'Company', minWidth: 150, width: 250 }, { field: 'payment', label: 'Payment', width: 200, minWidth: 150 }, { field: 'countryCode', label: 'Country/Region', width: 280 }, { field: 'date', label: 'Execution Date', width: 300 }, ]); return { // Values columns, isCondensed, pagination, // Enums BentoDateFormat, // Vue 3 storybook fix args, }; }, }), args: { ...DEFAULT_ARGS, columnPanel: false, }, parameters: storybookDocsParameter(DataGridFitContentInCardExample), }; // Default const lazyLoadingCode = ` // With infinite scroll <bento-data-grid :data="dataArray" :columns="[ { field: 'name', label: 'Name', minWidth: 300, mandatory: true }, { field: 'company', label: 'Reference' }, { field: 'payment', label: 'Payment', width: 200, minWidth: 150 }, ]" lazy-load-type="automatic" :loading="yourLoadingLogic" @update:load-more="yourInfiniteScrollHandler" /> // With 'load more' button <bento-data-grid :data="dataArray" :columns="[ { field: 'name', label: 'Name', minWidth: 300, mandatory: true }, { field: 'company', label: 'Reference' }, { field: 'payment', label: 'Payment', width: 200, minWidth: 150 }, ]" lazy-load-type="button" :loading="yourLoadingLogic" @update:load-more="yourInfiniteScrollHandler" />`; export const lazyLoading: Story = { render: (_args, { argTypes }) => ({ components: { BentoDataGrid }, props: Object.keys(argTypes), template: ` <div style="height: 300px;"> <bento-data-grid v-bind="args" :lazy-load-type="lazyLoadType" :loading="loading" @update:show-more="infiniteScrollHandler"/> </div> `, setup(props) { const args = isVue2 ? props : _args; const loading = ref(false); const data = ref(DEFAULT_DATA); const infiniteScrollHandler = () => { loading.value = true; setTimeout(() => { data.value.push(...ADDITIONAL_DATA); loading.value = false; }, 2000); }; return { args, loading, infiniteScrollHandler, }; }, }), args: { ...DEFAULT_ARGS, lazyLoadType: 'automatic', }, parameters: storybookDocsParameter(lazyLoadingCode), }; // Added just to be able to easily play around with the sticky headers/pagination/filters // Not really relevant as a Storybook example, so not adding storybook code example export const LongDataWithStickyFiltersAndPagination: Story = { render: (_args, { argTypes }) => ({ components: { BentoDataGrid }, props: Object.keys(argTypes), setup(props) { const args = isVue2 ? props : _args; const filtersConfig = ref<BentoFilterBarModel>([ { field: 'col1', label: 'Column 1', type: BentoFilterItemType.INPUT, }, { field: 'col2', label: 'Column 2', type: BentoFilterItemType.INPUT, }, ]); const filterValues = ref(); const searchTerm = ref(''); const isCondensed = ref(args.condensed); const columns = ref( Array.from({ length: 20 }, (_, index) => ({ field: `col${index + 1}`, label: `Column ${index + 1}`, width: 200, mandatory: index === 0, })) ); const data = ref( Array.from({ length: 50 }, (_, rowIndex) => columns.value.reduce((acc, col, colIndex) => { acc[col.field] = `Row ${rowIndex + 1} - Col ${colIndex + 1}`; return acc; }, {}) ) ); const dataSet = computed(() => // If any filters or search terms have been set filterValues.value?.length === 0 && searchTerm.value === '' ? data.value : data.value.filter(item => { const foundFilteredItem = filterValues.value?.length > 0 ? filterValues.value.every(filter => filter.value ? (item[filter.field] as string) .toLowerCase() .includes((filter.value as string).toLowerCase()) : true ) : true; const foundSearchFilteredItem = searchTerm.value ? Object.keys(item).some(key => key ? (item[key] as string) .toLowerCase() .includes((searchTerm.value as string).toLowerCase()) : true ) : true; return foundFilteredItem && foundSearchFilteredItem; }) ); const onSearchCleared = action('search-clear'); const onSearchInput = action('search-input'); const onSearchTermUpdated = action('update:search-term'); return { // Values dataSet, filtersConfig, filterValues, searchTerm, columns, isCondensed, // Methods onSearchCleared, onSearchInput, onSearchTermUpdated, // Vue 3 storybook fix args, }; }, template: ` <bento-data-grid v-bind="args" :data="dataSet" :columns.sync="columns" :condensed.sync="isCondensed" :filters="filtersConfig" :filter-values.sync="filterValues" :filter-search-term.sync="searchTerm" @filter-search-clear="onSearchCleared" @filter-search-input="onSearchInput" @update:filter-search-term="onSearchTermUpdated" /> `, }), args: { ...DEFAULT_ARGS, columnPanel: true, stickyPagination: true, stickyFilters: true }, }; // Cell actions const CellActionsTemplateCode = ` <template> <bento-data-grid :data="data" :columns="columns" /> </template> <script setup lang="ts"> import { ref } from 'vue'; import { BentoDataGrid, type BentoColumn, type BentoDatagridData, type BentoDatagridDataItem, type BentoColumnCellActionEvent, type BentoColumnCellAction, } from '@adyen/bento-vue2'; // --- Action Handlers --- const editItem: BentoColumnCellActionEvent = (rowDataItem, columnKey) => { // to be implemented }; const cancelAction: BentoColumnCellActionEvent = (rowDataItem, columnKey) => { // to be implemented }; const transferAction: BentoColumnCellActionEvent = (rowDataItem, columnKey) => { // to be implemented }; const deleteAction: BentoColumnCellActionEvent = (rowDataItem, columnKey) => { // to be implemented }; // --- Dynamic Cell Actions Function --- // This function generates actions based on the row's data. const getNameCellActions = (rowDataItem: BentoDatagridDataItem): Array<BentoColumnCellAction> => { const actions: Array<BentoColumnCellAction> = []; // Conditionally add the 'edit' action only for the first row. if (rowDataItem.id === 1) { actions.push({ variant: 'edit', event: editItem }); } // Add a 'more' menu with dynamic options. actions.push({ variant: 'more', isShort: true, menuData: [ { text: 'Cancel payment', event: cancelAction, critical: true, }, { text: 'Transfer payment', event: transferAction, }, { text: 'Delete payment', event: deleteAction, // Example: disable delete for the second row. disabled: rowDataItem.id === 2, }, ], }); return actions; }; // --- Column Definitions --- const columns: Array<BentoColumn> = [ { field: 'name', label: 'Name (Dynamic Actions)', minWidth: 300, mandatory: true, // Use a function for dynamic cell actions cellActions: getNameCellActions, }, { field: 'company', label: 'Reference', autoWidth: true }, { field: 'payment', label: 'Payment (Static Actions)', minWidth: 350, // Use a static array for cell actions cellActions: [ { variant: 'copy', getCopyData: (rowDataItem: BentoDatagridDataItem, columnKey: string) => \`\${rowDataItem[columnKey]}\`, }, { variant: 'open', event: action('open-payment') }, ], }, ]; // --- Data --- const data = ref<BentoDatagridData>([ { id: 1, name: 'First', payment: 20.9 }, { id: 2, name: 'Second', payment: 10.0 }, ]); </script> `; export const CellActions: Story = { render: (_args, { argTypes }) => ({ components: { BentoDataGrid }, props: Object.keys(argTypes), template: ` <bento-data-grid v-bind="args" :columns="columns"> <template #item-custom="{item}"> Value </template> </bento-data-grid> `, setup(props) { // --- Action Handlers --- const editItem: BentoColumnCellActionEvent = (rowDataItem, columnKey) => { action('edit')(rowDataItem, columnKey); }; const cancelAction: BentoColumnCellActionEvent = (rowDataItem, columnKey) => { action('cancel')(rowDataItem, columnKey); }; const transferAction: BentoColumnCellActionEvent = (rowDataItem, columnKey) => { action('transfer')(rowDataItem, columnKey); }; const deleteAction: BentoColumnCellActionEvent = (rowDataItem, columnKey) => { action('delete')(rowDataItem, columnKey); }; // --- Dynamic Cell Actions Function --- // This function generates actions based on the row's data. const getNameCellActions: BentoColumnCellActionsProp = rowDataItem => { const actions: Array<BentoColumnCellAction> = []; // Conditionally add the 'edit' action only for the first row. if (rowDataItem.id === DEFAULT_DATA[3].id) { actions.push({ variant: 'edit', event: editItem }); } // Add a 'more' menu with dynamic options. actions.push({ variant: 'more', isShort: true, menuData: [ { text: 'Cancel payment', event: cancelAction, critical: true, }, { text: 'Transfer payment', event: transferAction, }, { text: 'Delete payment', event: deleteAction, // Example: disable delete for the second row. disabled: rowDataItem.id === DEFAULT_DATA[2].id, }, ], }); return actions; }; // --- Column Definitions --- const columns: Array<BentoColumn> = [ { field: 'name', label: 'Name (Dynamic Actions)', minWidth: 300, mandatory: true, // Use a function for dynamic cell actions cellActions: getNameCellActions, }, { field: 'company', label: 'Reference', autoWidth: true }, { field: 'payment', label: 'Payment (Static Actions)', minWidth: 350, // Use a static array for cell actions cellActions: [ { variant: 'copy', getCopyData: (rowDataItem: BentoDatagridDataItem, columnKey: string) => `${rowDataItem[columnKey]}`, }, { variant: 'open', event: action('open-payment') }, ], }, ]; return { args: isVue2 ? props : _args, columns }; }, }), args: { ...DEFAULT_ARGS, highlightedRows: [DEFAULT_DATA[3].id], columns: [ { field: 'name', label: 'Name (Dynamic Actions)', minWidth: 300, mandatory: true, cellActions: [ () => (rowDataItem: BentoDatagridDataItem) => ({ variant: 'edit', disabled: rowDataItem.id === DEFAULT_DATA[3].id, event: action('edit'), }), { variant: 'open', menuData: [ { text: 'Cancel payment', event: action('cancel payment'), critical: true, }, { text: 'Transfer payment', event: action('transfer payment'), }, { text: 'Delete payment', event: action('delete payment'), disabled: true, }, ], }, ], }, { field: 'company', label: 'Reference', autoWidth: true }, { field: 'payment', label: 'Payment (Static Actions)', minWidth: 350, cellActions: [ { variant: 'copy', getCopyData: (rowDataItem: BentoDatagridDataItem, columnKey: string) => `$${rowDataItem[columnKey]}`, }, { variant: 'more', menuData: [ { text: 'Cancel payment', event: action('cancel payment'), critical: true, }, { text: 'Transfer payment', event: action('transfer payment'), }, { text: 'Delete payment', event: action('delete payment'), disabled: true, }, ], isShort: true, }, ], }, { field: 'custom', label: 'Custom template', minWidth: 200, cellActions: [{ variant: 'edit', event: action('edit') }], }, ], }, parameters: storybookDocsParameter(CellActionsTemplateCode), }; // NestContentSlot const NestedContentSlotTemplateCode = ` <template> <bento-data-grid :data="data" :columns="columns" has-nested-content :should-show-nested-content="filterRowsThatShowNestedContent" > <template #nested-slot="{item}"> <bento-typography>Nested content for {{ item.company }}</bento-typography> </template> </bento-data-grid> </template> <script setup lang="ts"> import { BentoDataGrid, type BentoColumn, type BentoDatagridData, type BentoDatagridShouldShowNestedContent } from '@adyen/bento-vue2' const columns: BentoColumn = [ { field: 'name', label: 'Name', minWidth: 300, mandatory: true }, { field: 'payment', label: 'Payment', minWidth: 300, mandatory: true }, ] const data = ref<BentoDatagridData>([ { id: 1, name: 'First', payment: 20.9 }, { id: 2, name: 'Second', payment: 10.0 }, ]); const filterRowsThatShowNestedContent: BentoDatagridShouldShowNestedContent = (item) => item.id === 2; </script> `; export const NestedContentSlot: Story = { render: (_args, { argTypes }) => ({ components: { BentoDataGrid, BentoLink, BentoTypography, }, props: Object.keys(argTypes), template: ` <bento-data-grid v-bind="args" > <template #nested-slot="{item}"> <div style="height: 80px; text-align: center; display: flex; align-items: center;"> <bento-typography>Nested content slot for {{ item.company }}</bento-typography> </div> </template> </bento-data-grid> `, setup(props) { return { args: isVue2 ? props : _args }; }, }), args: { ...DEFAULT_ARGS, hasNestedContent: true, zebraRows: true, shouldShowNestedContent: (item: BentoDatagridDataItem) => item.id !== DEFAULT_ARGS.data[1].id, }, parameters: storybookDocsParameter(NestedContentSlotTemplateCode), }; export const StickyHorizontalScrollbar: Story = { render: (_args, { argTypes }) => ({ components: { BentoDataGrid, BentoButton }, props: Object.keys(argTypes), setup(props) { const args = isVue2 ? props : _args; const filtersConfig = ref<BentoFilterBarModel>([ { field: 'col1', label: 'Column 1', type: BentoFilterItemType.INPUT, }, { field: 'col2', label: 'Column 2', type: BentoFilterItemType.INPUT, }, ]); const filterValues = ref(); const searchTerm = ref(''); const isCondensed = ref(args.condensed); const columns = ref( Array.from({ length: 20 }, (_, index) => ({ field: `col${index + 1}`, label: `Column ${index + 1}`, width: 200, mandatory: index === 0, })) ); const data = ref( Array.from({ length: 50 }, (_, rowIndex) => columns.value.reduce((acc, col, colIndex) => { acc[col.field] = `Row ${rowIndex + 1} - Col ${colIndex + 1}`; return acc; }, {}) ) ); const dataSet = computed(() => // If any filters or search terms have been set filterValues.value?.length === 0 && searchTerm.value === '' ? data.value : data.value.filter(item => { const foundFilteredItem = filterValues.value?.length > 0 ? filterValues.value.every(filter => filter.value ? (item[filter.field] as string) .toLowerCase() .includes((filter.value as string).toLowerCase()) : true ) : true; const foundSearchFilteredItem = searchTerm.value ? Object.keys(item).some(key => key ? (item[key] as string) .toLowerCase() .includes((searchTerm.value as string).toLowerCase()) : true ) : true; return foundFilteredItem && foundSearchFilteredItem; }) ); const onSearchCleared = action('search-clear'); const onSearchInput = action('search-input'); const onSearchTermUpdated = action('update:search-term'); return { // Values dataSet, filtersConfig, filterValues, searchTerm, columns, isCondensed, // Methods onSearchCleared, onSearchInput, onSearchTermUpdated, // Vue 3 storybook fix args, }; }, template: ` <bento-data-grid v-bind="args" :data="dataSet" :columns.sync="columns" :condensed.sync="isCondensed" :filters="filtersConfig" :filter-values.sync="filterValues" :filter-search-term.sync="searchTerm" @filter-search-clear="onSearchCleared" @filter-search-input="onSearchInput" @update:filter-search-term="onSearchTermUpdated" > <template #pagination-slot> <bento-button variant="tertiary"> Pagination slot button </bento-button> </template> </bento-data-grid> `, }), args: { ...DEFAULT_ARGS, pagination: { page: 1, hidePageSize: true }, columnPanel: true, stickyPagination: true, stickyFilters: true, stickyHorizontalScrollbar: true, }, }; const NestedRowsTemplateCode = ` <template> <bento-data-grid :data="data" :columns="columns" has-nested-rows :should-show-nested-row="filterRowsThatShowNestedContent" @nested-content-toggled="onParentRowToggle" /> </template> <script setup lang="ts"> import { BentoDataGrid, type BentoColumn, type BentoDatagridData, type BentoDatagridShouldShowNestedRowsFn } from '@adyen/bento-vue2' const columns: BentoColumn = [ { field: 'name', label: 'Name', minWidth: 300, mandatory: true }, { field: 'payment', label: 'Payment', minWidth: 300, mandatory: true }, ] const data = ref<BentoDatagridData>([ { id: 1, name: 'First', payment: 20.9 }, { id: 2, name: 'Second', payment: 10.0 }, ]); const filterRowsThatShowNestedRows: BentoDatagridShouldShowNestedRowsFn = (item) => item.id === 2; const onParentRowToggle = async (item: BentoDatagridDataItem) => { const childrenForToggledParentItem = await makeAPICallToGetChildren(item.id); dataState.value = dataState.value.map(itemToChange => { if (itemToChange.id === item.id) { return { ...itemToChange, children: childrenForToggledParentItem } } return itemToChange; }); }; </script> `; export const NestedRows: Story = { render: (_args, { argTypes }) => ({ components: { BentoDataGrid, BentoLink, BentoTypography, }, props: Object.keys(argTypes), template: ` <bento-data-grid v-bind="args" :data="dataState" @nested-content-toggled="onParentRowToggle" /> `, setup(props) { const dataState = ref<Array<BentoDatagridDataItem>>(DEFAULT_ARGS.data); const selection = ref<BentoDatagridDataItemId>(); const onParentRowToggle = (item: BentoDatagridDataItem) => { setTimeout(() => { const children = [ { id: `03c0a799-1ced-4017-aea3-111111${item.id}`, name: `Child of ${item.name} #1`, payment: 160405, company: 'Merchant Helium', registered: '2023-06-30T13:56:09.498Z', tags: ['deserunt', 'et', 'esse'], reference: 'UnbQaUncf3eR1D8XUBqn_UnbQaUncf3eR1D8XUBqn_UnbQaUncf3eR1D8XUBqn', paymentStatus: 1, countryCode: BentoCountryCode.NL, parentId: DEFAULT_ARGS.data[1].id, }, { id: `03c0a799-1ced-4017-aea3-1111113${item.id}`, name: `Child of ${item.name} #2`, payment: 163500, company: 'Merchant B', registered: '2023-04-28T13:11:09.498Z', tags: ['et', 'Lorem', 'sint'], reference: 'IxUZ9FsEPCZ9dFFPpI9C', paymentStatus: 2, countryCode: BentoCountryCode.BE, parentId: DEFAULT_ARGS.data[1].id, }, { id: `03c0a799-1ced-4017-aea3-1111114${item.id}`, name: `Child of ${item.name} #3`, payment: 78500, company: 'Merchant C', registered: '2023-06-30T11:01:09.498Z', tags: ['laborum', 'enim', 'ullamco'], reference: 'y0qAQ0WDszRyqLY2WO9f', paymentStatus: 1, countryCode: BentoCountryCode.UA, parentId: DEFAULT_ARGS.data[1].id, }, ]; dataState.value = dataState.value.map(itemToChange => { if (itemToChange.id === item.id) { return { ...itemToChange, children, }; } return itemToChange; }); }, 1000); }; return { args: isVue2 ? props : _args, dataState, selection, onParentRowToggle }; }, }), args: { ...DEFAULT_ARGS, hasNestedRows: true, zebraRows: true, shouldShowNestedRows: dataItem => { return dataItem.id !== DEFAULT_ARGS.data[1].id; }, }, parameters: storybookDocsParameter(NestedRowsTemplateCode), }; export const RowActions: Story = { render: (_args, { argTypes }) => ({ components: { BentoDataGrid }, props: Object.keys(argTypes), template: ` <bento-data-grid v-bind="args" :row-actions="rowActions" /> `, setup(props) { const rowActions: BentoDataGridRowActionsProp = (item: BentoDatagridDataItem) => { if (item.id === DEFAULT_DATA[0].id) { return [ { variant: 'edit', event: action('edit') }, { title: 'Delete', critical: true, event: action('Delete'), }, ]; } else if (item.id === DEFAULT_DATA[1].id) { return []; } else if (item.id === DEFAULT_DATA[2].id) { return [ { variant: 'edit', event: action('edit') }, { variant: 'open', isShort: true, menuData: [ { text: 'Cancel payment', event: action('cancel payment'), critical: true, }, { text: 'Transfer payment', event: action('transfer payment'), }, { text: 'Delete payment', event: action('delete payment'), disabled: true, }, ], }, ]; } return [ { variant: 'edit', event: action('edit'), }, ]; }; return { args: isVue2 ? props : _args, rowActions, }; }, }), args: { ...DEFAULT_ARGS, }, parameters: storybookDocsParameter(` <template> <bento-data-grid :data="data" :columns="columns" :row-actions="getRowActions" /> </template> <script setup lang="ts"> import { ref } from 'vue'; import { BentoDataGrid, type BentoColumn, type BentoDatagridData, type BentoDatagridDataItem, type BentoDataGridRowActionsProp } from '@adyen/bento-vue2'; const data = ref<BentoDatagridData>([ { id: '1', name: 'Shepherd Kane', company: 'Merchant Helium' }, { id: '2', name: 'Santos Palmer', company: 'Merchant B' }, { id: '3', name: 'Joyce Johns', company: 'Merchant C' }, ]); const columns = ref<BentoColumn[]>([ { field: 'name', label: 'Name' }, { field: 'company', label: 'Company' }, ]); const getRowActions: BentoDataGridRowActionsProp = (item: BentoDatagridDataItem) => { // Example: Show different actions based on the row data if (item.id === '1') { return [ { variant: 'edit', event: (item) => console.log('Editing', item) }, { title: 'Delete', critical: true, event: (item) => console.log('Deleting', item) } ]; } if (item.id === '3') { return [ { variant: 'edit', event: (item) => console.log('Editing', item) }, { variant: 'more', menuData: [ { text: 'Action 1', event: (item) => console.log('Action 1 on', item) }, { text: 'Action 2', event: (item) => console.log('Action 2 on', item), disabled: true }, ] } ]; } // Return an empty array for rows that should have no actions (e.g., row with id '2') return []; }; </script> `), }; const SummaryCellTemplateCode = ` <template> <bento-data-grid :columns="columns" :data="dataSet" :filters="filtersConfig" :filter-values.sync="filterValues" :filter-search-term="searchTerm" :filter-search-config="searchConfig" @update:filter-values="filterValues = $event" @update:filter-search-term="searchTerm = $event" @filter-search-clear="onSearchCleared" > <template #item-company="{item}"> <bento-data-grid-summary-cell :label="\`\${item.company.length} merchants\`" :items="item.company" :search-term="companyColumnSearchTerm" /> </template> </bento-data-grid> </template> <script setup lang="ts"> import { BentoColumn, BentoDataGrid, BentoDataGridSummaryCell, BentoDatagridData, BentoFilterBarModel, BentoFilterBarSearchBarProps, BentoFilterItemType, BentoFilterValues, BentoCountryCode, } from '@adyen/bento-vue2'; import { computed, ref } from 'vue'; const columns: Array<BentoColumn> = [ { field: 'name', label: 'Name', flex: 2, mandatory: true }, { field: 'company', label: 'Company', flex: 1 }, { field: 'payment', label: 'Payment', flex: 1, minWidth: 200 }, ]; /** * Data set with array of companies */ const multiData: BentoDatagridData = [ { id: '03c0a799-1ced-4017-aea3-0cebb0106335', name: 'Shepherd Kane', payment: 160405, company: ['Merchant Helium', 'Merchant A'], }, { id: '8f9e7877-1ca2-4929-9217-f1ec78709291', name: 'Joyce Johns', payment: 78500, company: ['Merchant C', 'Merchant Hedra'], }, { id: '51d2b31d-d577-4fca-8329-a583babbbdda', name: 'Evans Zamora', payment: 1153.4, company: [ 'Merchant A', 'Merchant B', 'Merchant C', 'Merchant D', 'Merchant E', 'Merchant F', 'Merchant G', 'Merchant H', 'Merchant I', 'Merchant J', ], }, ]; const filtersConfig = computed<BentoFilterBarModel>(() => [ { field: 'name', label: 'Name', type: BentoFilterItemType.INPUT, }, { field: 'company', label: 'Company', type: BentoFilterItemType.INPUT, }, ]); /** * The values of applied filters. */ const filterValues = ref<BentoFilterValues>(); /** * The current global search term that is being searched with */ const searchTerm = ref(''); /** * Global search configuration */ const searchConfig = ref<BentoFilterBarSearchBarProps>({ inputFieldAriaLabel: 'Descriptive details about the search bar', debounceTime: 1000, // in ms placeholder: 'Find merchants', }); /** * Concatenated search term using the global search and the company filter value */ const companyColumnSearchTerm = computed( () => searchTerm.value || filterValues.value?.find(filter => filter.field === 'company')?.value?.toString() || '' ); /** * Simplified computed data set that filters data depending on applied filters and global search values */ const dataSet = computed<BentoDatagridData>(() => { let filteredData = multiData; // Apply global search term filter if (searchTerm.value) { const lowerCaseSearchTerm = searchTerm.value.toLowerCase(); filteredData = filteredData.filter( item => (item.name as string).toLowerCase().includes(lowerCaseSearchTerm) || (item.company as string[]).some(company => company.toLowerCase().includes(lowerCaseSearchTerm) ) ); } // Apply filterValues filter if (filterValues.value?.length) { filteredData = filteredData.filter(item => filterValues.value?.every(filter => { if (filter.field === 'company') { return filter.value ? (item[filter.field] as string[]).some(company => (company as string) .toLowerCase() .includes((filter.value as string).toLowerCase()) ) : true; } return filter.value ? (item[filter.field] as string) .toLowerCase() .includes((filter.value as string).toLowerCase()) : true; }) ); } return filteredData; }); const onSearchCleared = () => { searchTerm.value = ''; }; </script> `; export const SummaryCell: Story = { render: (_args, { argTypes }) => ({ components: { BentoDataGrid, BentoLink, BentoTypography, BentoDashedUnderline, BentoDataGridSummaryCell, }, props: Object.keys(argTypes), template: ` <bento-data-grid v-bind="args" :data="dataSet" :filters="filtersConfig" :filter-values.sync="filterValues" :filter-search-term="searchTerm" :filter-search-config="searchConfig" @update:filter-values="filterValues = $event" @update:filter-search-term="searchTerm = $event" @filter-search-clear="onSearchCleared" > <template #item-company="{item}"> <bento-data-grid-summary-cell :label="\`\${item.company.length} merchants\`" :items="item.company" :search-term="companyColumnSearchTerm" /> </template> </bento-data-grid> `, setup(props) { /** * Data set with array of companies */ const multiData: BentoDatagridData = [ { id: '03c0a799-1ced-4017-aea3-0cebb0106335', name: 'Shepherd Kane', payment: 160405, company: ['Merchant Helium', 'Merchant A'], }, { id: 'b6d4f1fd-b1f6-40a6-a443-f785c89604ef', name: 'Santos Palmer', payment: 163500, company: ['Merchant B'], }, { id: '8f9e7877-1ca2-4929-9217-f1ec78709291', name: 'Joyce Johns', payment: 78500, company: ['Merchant C', 'Merchant Hedra'], }, { id: '0789a012-85a6-4657-87bb-319141938a61', name: 'Lynnette Melendez', payment: -45000, company: ['Merchant A', 'Merchant B', 'Merchant C'], }, { id: '0b3981b6-c73b-47ad-81d1-b06f9b95d467', name: 'Wagner Mcintosh', payment: 1112, company: ['Merchant A'], }, { id: '51d2b31d-d577-4fca-8329-a583babbbdda', name: 'Evans Zamora', payment: 1153.4, company: [ 'Merchant A', 'Merchant B', 'Merchant C', 'Merchant D', 'Merchant E', 'Merchant F', 'Merchant G', 'Merchant H', 'Merchant I', 'Merchant J', 'Merchant K', 'Merchant L', 'Merchant M', 'Merchant N', 'Merchant O', 'Merchant P', 'Merchant Q', 'Merchant R', 'Merchant S', 'Merchant T', ], }, { id: '9bd59a6e-bd03-4477-813f-b844953b7645', name: 'Anderson Middleton', payment: 181722.23, company: ['Merchant A'], }, ]; const filtersConfig = computed<BentoFilterBarModel>(() => [ { field: 'name', label: 'Name', type: BentoFilterItemType.INPUT, }, { field: 'company', label: 'Company', type: BentoFilterItemType.INPUT, }, ]); /** * The values of applied filters. */ const filterValues = ref<BentoFilterValues>(); /** * The current global search term that is being searched with */ const searchTerm = ref(''); /** * Global search configuration */ const searchConfig = ref<BentoFilterBarSearchBarProps>({ inputFieldAriaLabel: 'Descriptive details about the search bar', debounceTime: 1000, // in ms placeholder: 'Find merchants', }); /** * Concatenated search term using the global search and the company filter value */ const companyColumnSearchTerm = computed( () => searchTerm.value || filterValues.value?.find(filter => filter.field === 'company')?.value?.toString() || '' ); /** * Simplified computed data set that filters data depending on applied filters and global search values */ const dataSet = computed<BentoDatagridData>(() => { let filteredData = multiData; // Apply global search term filter if (searchTerm.value) { const lowerCaseSearchTerm = searchTerm.value.toLowerCase(); filteredData = filteredData.filter( item => (item.name as string).toLowerCase().includes(lowerCaseSearchTerm) || (item.company as Array<string>).some(company => company.toLowerCase().includes(lowerCaseSearchTerm) ) ); } // Apply filterValues filter if (filterValues.value?.length) { filteredData = filteredData.filter(item => filterValues.value?.every(filter => { if (filter.field === 'company') { return filter.value ? (item[filter.field] as Array<string>).some(company => (company as string) .toLowerCase() .includes((filter.value as string).toLowerCase()) ) : true; } return filter.value ? (item[filter.field] as string) .toLowerCase() .includes((filter.value as string).toLowerCase()) : true; }) ); } return filteredData; }); const onSearchCleared = () => { searchTerm.value = ''; }; return { args: isVue2 ? props : _args, filtersConfig, filterValues, searchTerm, searchConfig, onSearchCleared, dataSet, companyColumnSearchTerm, }; }, }), args: { ...DEFAULT_ARGS, columns: [ { field: 'name', label: 'Name', flex: 2, mandatory: true }, { field: 'company', label: 'Company', flex: 1 }, { field: 'payment', label: 'Payment', flex: 1, minWidth: 200 }, ], zebraRows: true, }, parameters: storybookDocsParameter(SummaryCellTemplateCode), }; export const TrendCell: Story = { render: (_args, { argTypes }) => ({ components: { BentoDataGrid, BentoDataGridTrendCell, }, props: Object.keys(argTypes), template: ` <bento-data-grid v-bind="args"> <template #item-performance="{ item }"> <bento-data-grid-trend-cell :value="item.performance.value" :direction="item.performance.direction" :variant="item.performance.variant" /> </template> </bento-data-grid> `, setup(props) { const columns: Array<BentoColumn> = [ { field: 'name', label: 'Name', flex: 2 }, { field: 'performance', label: 'Performance', flex: 1, numeric: true }, ]; const data: BentoDatagridData = [ { id: '1', name: 'Shepherd Kane', performance: { value: 25.5, variant: 'green', direction: 'up', }, }, { id: '2', name: 'Santos Palmer', performance: { value: 10.2, variant: 'red', direction: 'down', }, }, { id: '3', name: 'Joyce Johns', performance: { value: 0, variant: 'black', direction: 'none', }, }, { id: '4', name: 'Lynnette Melendez', performance: { value: '3.1428571429%', variant: 'green', direction: 'down', }, }, { id: '5', name: 'Wagner Mcintosh', performance: { value: '15.2%', variant: 'red', direction: 'up', }, }, { id: '6', name: 'Evans Zamora', performance: { value: '1.23', direction: 'up', }, }, { id: '7', name: 'Anderson Middleton', performance: { value: '90', direction: 'down', }, }, ]; return { args: { ...(isVue2 ? props : _args), columns, data }, }; }, }), args: { ...DEFAULT_ARGS, zebraRows: true, }, parameters: storybookDocsParameter(DataGridTrendCellExample), };
|
|
1
|
+
import { type BentoColumn, BentoColumnOverflow, type BentoDatagridData, type BentoDatagridDataItem, type BentoDatagridDataItemId, BentoDatagridEvent, type BentoDatagridPaginationProps, type BentoDatagridSelection, type BentoDatagridSortByColumn, BentoDatagridSortDirection, } from './data-grid.types'; import { BentoCard } from '@/components/card'; import { action } from '@storybook/addon-actions'; import { type BentoFilterBarModel, type BentoFilterBarSearchBarProps, BentoFilterItemType, type BentoFilterValues, } from '../filter-bar'; import { BentoCurrency } from '@/components/currency'; import { BentoStatus, BentoStatusVariant } from '@/components/status'; import { BentoLink } from '@/components/link'; import { BentoTypography } from '@/components/typography'; import { computed, ref, watch } from 'vue'; import { storybookDocsParameter } from '@/utils/ts/storybook'; import { BentoDate, BentoDateFormat } from '@/components/date'; import { BentoCountry, BentoCountryCode } from '@/components/country'; import type { Meta, StoryObj } from '@storybook/vue'; import BentoDataGrid from './data-grid.vue'; import { isVue2 } from 'vue-demi'; import { BentoButton } from '../button'; import { BentoDashedUnderline } from '../dashed-underline'; import { BentoDataGridSummaryCell, BentoDataGridTagWithTextCell, BentoDataGridTrendCell } from './components'; import type { BentoColumnCellAction, BentoColumnCellActionEvent, BentoColumnCellActionsProp, BentoDataGridRowActionsProp, } from './components/data-grid-cell-actions/data-grid-cell-actions.types'; // Examples import DataGridEverythingBagel from './__tests__/data-grid-everything-bagel.vue?raw'; import DataGridCellActionsExample from './__tests__/data-grid-cell-actions-example.vue?raw'; import DataGridRowActionsExample from './__tests__/data-grid-row-actions-example.vue?raw'; import DataGridFitContentInCardExample from './__tests__/data-grid-fit-content-in-card-example.vue?raw'; import DataGridWithSlotsExample from './__tests__/data-grid-with-slots-example.vue?raw'; import DataGridFilteringExample from './__tests__/data-grid-filtering-example.vue?raw'; import DataGridSelectionExample from './__tests__/data-grid-selection-example.vue?raw'; import DataGridTrendCellExample from './__tests__/data-grid-trend-cell-example.vue?raw'; import DataGridTagWithTextCellExample from './__tests__/data-grid-tag-with-text-cell-example.vue?raw'; import { DEFAULT_DATA } from './__tests__/data-grid-example-data'; const meta: Meta = { title: 'Data Grid', component: BentoDataGrid, argTypes: { // Values lazyLoadType: { control: { type: 'select', }, }, // Filter bar props filters: { table: { type: { summary: 'BentoFilterBarModel' }, }, }, filterSearchTerm: { name: 'filterSearchTerm', defaultValue: undefined, description: "Filter bar search term useful to filter all the data by text. If the value is non-existant (`null` or `undefined`) the search bar won't be enabled.", control: { type: 'text', }, }, // Slots default: { table: { disable: true }, }, header: { name: 'header', defaultValue: '', description: 'header text', table: { type: { summary: 'VNode[]' }, }, control: { type: 'text', }, }, footer: { name: 'footer', defaultValue: '', description: 'footer text', table: { type: { summary: 'VNode[]' }, }, control: { type: 'text', }, }, 'head-{columnKey}': { name: 'head-{columnKey}', defaultValue: '', description: 'the column slot where `{columnKey}` is the name of the column key. Allows the Column header to be customized.', table: { type: { summary: 'VNode[]' }, category: 'slots', }, control: { disable: true }, }, 'item-{columnKey}': { name: 'item-{columnKey}', defaultValue: '', description: 'the data item slot where `{columnKey}` is the name of the data item column key. Allow row cells to be customized.', table: { type: { summary: 'VNode[]' }, category: 'slots', }, control: { disable: true }, }, ['pagination-slot']: { control: { type: 'text' }, description: 'Passes content to the default pagination slot', table: { type: { summary: 'VNode[]' }, }, }, // Events [BentoDatagridEvent.COLUMN_RESIZE]: { name: BentoDatagridEvent.COLUMN_RESIZE, defaultValue: '', description: 'Triggered when a column is resized. Provides a field name and the new resized width in pixels.', table: { category: 'events', type: { summary: '({ field, newResizedWidth }) => void' }, }, control: { disable: true }, }, [BentoDatagridEvent.SELECT]: { name: BentoDatagridEvent.SELECT, defaultValue: '', description: 'Triggered when a row is selected. Provides an array of data item ids. `BentoDatagridDataItemId[]`', table: { category: 'events', type: { summary: '(selectedDataItemIds) => void' }, }, control: { disable: true }, }, [BentoDatagridEvent.NAVIGATE]: { name: BentoDatagridEvent.NAVIGATE, defaultValue: '', description: `Triggered when the navigation controls in the pagination are clicked (i.e. when the arrow buttons to the rightmost of the screen or the dropdown on the right hand are updated). It indicates to which page it should go next.`, table: { category: 'events', type: { summary: '(pageNumber: number) => void' }, }, control: { disable: true }, }, [BentoDatagridEvent.ITEMS_PAGE]: { name: BentoDatagridEvent.ITEMS_PAGE, defaultValue: '', description: `Triggered when the number of items per page is changed from the left hand dropdown of the pagination. It indicates the new number of items that will be displayed.`, table: { category: 'events', type: { summary: '(itemsPerPage: number) => void' }, }, control: { disable: true }, }, [BentoDatagridEvent.SORT]: { name: BentoDatagridEvent.SORT, defaultValue: '', description: `Triggered when a column is sorted. Provides an array of \`BentoDatagridSortByColumn\` which includes the \`field\` and the sort \`direction\` (\`BentoDatagridSortDirection\`) of the column:`, table: { category: 'events', type: { summary: '(sortByDirections) => void' }, }, control: { disable: true }, }, [BentoDatagridEvent.UPDATE_COLUMNS]: { name: BentoDatagridEvent.UPDATE_COLUMNS, defaultValue: '', description: `Triggered when a the columns have been updated. Provides an updated array of columns \`BentoColumn[]\` which should be passed back into the \`columns\` prop (easily achieved using the [sync modifier](https://v2.vuejs.org/v2/guide/components-custom-events.html#sync-Modifier)) e.g. \`:columns.sync="columns"\``, table: { category: 'events', type: { summary: '(columns) => void' }, }, control: { disable: true }, }, [BentoDatagridEvent.UPDATE_CONDENSED]: { name: 'update:condensed', defaultValue: '', description: `Triggered when the config panel's condensed view toggle is interacted with. Provides the updated condensed \`boolean\`. (easily achieved using the [sync modifier](https://v2.vuejs.org/v2/guide/components-custom-events.html#sync-Modifier)) e.g. \`:condensed.sync="condensed"\``, table: { category: 'events', type: { summary: '(condensed) => void' }, }, control: { disable: true }, }, [BentoDatagridEvent.UPDATE_FILTER_VALUES]: { name: BentoDatagridEvent.UPDATE_FILTER_VALUES, defaultValue: '', description: `Triggered when a the filters in the filterbar have updated. Provides an array of filters \`BentoFilterBarModel[]\` which should be passed back into the \`filter-values\` prop (easily achieved using the [sync modifier](https://v2.vuejs.org/v2/guide/components-custom-events.html#sync-Modifier)) e.g. \`:filter-values.sync="filtersValues"\``, table: { category: 'events', type: { summary: '(filterValues) => void' }, }, control: { disable: true }, }, [BentoDatagridEvent.UPDATE_PAGINATION]: { name: BentoDatagridEvent.UPDATE_PAGINATION, defaultValue: '', description: `Triggered when a the pagination state has been updated. Provides an updated \`pagination\` properties object which should be passed back into the \`pagination\` prop (easily achieved using the [sync modifier](https://v2.vuejs.org/v2/guide/components-custom-events.html#sync-Modifier)) e.g. \`:pagination.sync="pagination"\``, table: { category: 'events', type: { summary: '(paginationProps) => void' }, }, control: { disable: true }, }, [BentoDatagridEvent.UPDATE_SELECTION]: { name: BentoDatagridEvent.UPDATE_SELECTION, defaultValue: '', description: `Triggered when a the select data rows state has been updated. Provides an array of row IDs \`BentoDatagridDataItemId[]\` which should be passed back into the \`selection\` prop (easily achieved using the [sync modifier](https://v2.vuejs.org/v2/guide/components-custom-events.html#sync-Modifier)) e.g. \`:selection.sync="selection"\``, table: { category: 'events', type: { summary: '(selection) => void' }, }, control: { disable: true }, }, [BentoDatagridEvent.UPDATE_SORT_BY]: { name: 'update:sort-by', defaultValue: '', description: `Triggered when a the sorting state has been updated. Provides an array of sorting directions \`BentoDatagridSortByColumn[]\` which should be passed back into the \`sort-by\` prop (easily achieved using the [sync modifier](https://v2.vuejs.org/v2/guide/components-custom-events.html#sync-Modifier)) e.g. \`:sort-by.sync="sortBy"\``, table: { category: 'events', type: { summary: '(sortingDirections) => void' }, }, control: { disable: true }, }, // Filter bar search events [BentoDatagridEvent.FILTER_SEARCH_INPUT]: { name: BentoDatagridEvent.FILTER_SEARCH_INPUT, defaultValue: '', description: "Triggered whenever the filter's search is updated.", table: { category: 'events', type: { summary: '(newSearchTerm) => void' } }, control: { disable: true }, }, [BentoDatagridEvent.FILTER_SEARCH_CLEAR]: { name: BentoDatagridEvent.FILTER_SEARCH_CLEAR, defaultValue: '', description: "Triggered whenever the filter's search is cleared.", table: { category: 'events', type: { summary: '() => void' }, }, control: { disable: true }, }, [BentoDatagridEvent.UPDATE_FILTER_SEARCH_TERM]: { name: BentoDatagridEvent.UPDATE_FILTER_SEARCH_TERM, defaultValue: '', description: "Triggered whenever the filter's search is updated to sync the model.", table: { category: 'events', type: { summary: '(newSearchTerm) => void' }, }, control: { disable: true }, }, [BentoDatagridEvent.ROW_CLICK]: { name: BentoDatagridEvent.ROW_CLICK, defaultValue: '', description: 'Triggered whenever a row has been clicked.', table: { category: 'events', type: { summary: '(row: BentoDatagridDataItem, event: MouseEvent) => void' }, }, }, [BentoDatagridEvent.NESTED_CONTENT_TOGGLED]: { name: BentoDatagridEvent.NESTED_CONTENT_TOGGLED, defaultValue: '', description: 'Triggered whenever a data item toggled is nested content', table: { category: 'events', type: { summary: '(dataItem: BentoDatagridDataItem) => void' }, }, control: { disable: true }, }, }, }; export default meta; type Story = StoryObj<typeof BentoDataGrid>; const DEFAULT_COLUMNS: Array<BentoColumn> = [ { field: 'name', label: 'Name', minWidth: 300, mandatory: true }, { field: 'company', label: 'Company', autoWidth: true }, { field: 'payment', label: 'Payment', minWidth: 350 }, ]; const ADDITIONAL_DATA: BentoDatagridData = [ { id: '03c0a799-1ced-4017-aea3-0cebb0106335', name: 'Additional data fetched here', payment: 160405, company: 'Merchant Helium', registered: '2023-06-30T13:56:09.498Z', tags: ['deserunt', 'et', 'esse'], reference: 'UnbQaUncf3eR1D8XUBqn_UnbQaUncf3eR1D8XUBqn_UnbQaUncf3eR1D8XUBqn', paymentStatus: 1, countryCode: BentoCountryCode.NL, }, { id: 'b6d4f1fd-b1f6-40a6-a443-f785c89604ef', name: 'Santos Palmer', payment: 163500, company: 'Merchant B', registered: '2023-04-28T13:11:09.498Z', tags: ['et', 'Lorem', 'sint'], reference: 'IxUZ9FsEPCZ9dFFPpI9C', paymentStatus: 2, countryCode: BentoCountryCode.BE, }, { id: '8f9e7877-1ca2-4929-9217-f1ec78709291', name: 'Joyce Johns', payment: 78500, company: 'Merchant C', registered: '2023-06-30T11:01:09.498Z', tags: ['laborum', 'enim', 'ullamco'], reference: 'y0qAQ0WDszRyqLY2WO9f', paymentStatus: 1, countryCode: BentoCountryCode.UA, }, { id: '0789a012-85a6-4657-87bb-319141938a61', name: 'Lynnette Melendez', payment: -45000, company: 'STRALOY', registered: '2019-08-01T13:56:01.498Z', tags: ['sit', 'consectetur', 'amet'], reference: '2vv5pzbRSwx8YsFoGGOC', paymentStatus: 1, countryCode: BentoCountryCode.BR, }, { id: '0b3981b6-c73b-47ad-81d1-b06f9b95d467', name: 'Wagner Mcintosh', payment: 1112, company: 'Merchant A', registered: '2022-12-30T02:12:09.498Z', tags: ['ad', 'ullamco', 'et'], reference: '54KN4jkpBuaRY5xMfe84', paymentStatus: 1, countryCode: BentoCountryCode.US, }, { id: '51d2b31d-d577-4fca-8329-a583babbbdda', name: 'Evans Zamora', payment: 1153.4, company: 'GEEKWAGON', registered: '2023-06-30T13:51:09.498Z', tags: ['consequat', 'magna', 'sit'], reference: 'Sj84kad0MGxpuozTjEk', paymentStatus: 0, countryCode: BentoCountryCode.FR, }, { id: '9bd59a6e-bd03-4477-813f-b844953b7645', name: 'Anderson Middleton', payment: 181722.23, company: 'BALOOBA', registered: '2021-03-02T01:34:09.498Z', tags: ['aliqua', 'adipisicing', 'magna'], reference: 'MtJ5Bnew0MGxpuozTjEk', paymentStatus: 1, countryCode: BentoCountryCode.IT, }, ]; const DEFAULT_ARGS = { data: DEFAULT_DATA, columns: DEFAULT_COLUMNS, allowColumnDragAndDrop: false, pagination: {}, emptyState: { title: 'No results were found', image: 'no-results-found', variant: 'embedded', action: { title: 'Clear filters', event: action('clear'), }, description: 'Try a different term or reset search filters', }, }; // Default const DefaultCode = ` <template> <bento-data-grid :allow-row-clicks="false" :allow-column-drag-and-drop="false" :bulk-actions="undefined" :condensed="false" :columns="columns" :column-panel="false" :data="data" :empty-state="undefined" :filters="undefined" :filters-search-term="undefined" :fit-content="false" :get-data-item-id="undefined" :has-resizeable-columns="false" :has-nested-content="false" :highlighted-rows="undefined" :is-data-item-disabled="undefined" lazy-load-type="none" :loading="false" :outline="false" :no-spacing-top="false" :pagination="undefinded" :selectable-accross-pages="false" :sticky-pagination="false" :sticky-filters="false" :selectable="false" :selection="undefined" :sort-by="undefined" :tab-title="undefined" :truncate-column-headers="false" /> </template> <script setup lang="ts"> import { BentoColumn, BentoDataGrid, BentoDatagridData } from '@adyen/bento-vue2'; import { ref } from 'vue'; const data = ref<BentoDatagridData>(${JSON.stringify(DEFAULT_DATA)}); const columns = ref<BentoColumn[]>([ { field: 'name', label: 'Name', minWidth: 300, mandatory: true }, { field: 'company', label: 'Reference' }, { field: 'payment', label: 'Payment', width: 200, minWidth: 150 }, ]); </script> `; export const Default: Story = { render: (_args, { argTypes }) => ({ components: { BentoDataGrid }, props: Object.keys(argTypes), template: ` <bento-data-grid v-bind="args" /> `, setup(props) { return { args: isVue2 ? props : _args }; }, }), args: DEFAULT_ARGS, parameters: storybookDocsParameter(DefaultCode), }; // Fluid Columns const FluidTemplateCode = ` <template> <bento-data-grid :data="data" :columns="columns" /> </template> <script setup lang="ts"> import { BentoColumn, BentoDataGrid, BentoDatagridData } from '@adyen/bento-vue2'; import { ref } from 'vue'; const data = ref<BentoDatagridData>(${JSON.stringify(DEFAULT_DATA)}); const columns = ref<BentoColumn[]>([ { field: 'name', label: 'Name', flex: 2, mandatory: true }, { field: 'company', label: 'Company', flex: 1 }, { field: 'payment', label: 'Payment', flex: 1, minWidth: 200 }, ]); </script> `; const rowActionsFluid: BentoDataGridRowActionsProp = (item: BentoDatagridDataItem) => { if (item.id === DEFAULT_DATA[3].id) { return [ { variant: 'edit', event: action('edit'), }, ]; } else if (item.id === DEFAULT_DATA[2].id) { return [ { variant: 'edit', event: action('edit') }, { variant: 'open', isShort: true, menuData: [ { text: 'Cancel payment', event: action('cancel payment'), critical: true, }, { text: 'Transfer payment', event: action('transfer payment'), }, { text: 'Delete payment', event: action('delete payment'), disabled: true, }, ], }, ]; } else if (item.id === DEFAULT_DATA[1].id) { return []; } return [ { variant: 'edit', event: action('edit'), }, { title: 'Delete', critical: true, event: action('Delete'), }, ]; }; export const Fluid: Story = { ...Default, args: { ...DEFAULT_ARGS, columns: [ { field: 'name', label: 'Name', flex: 2, mandatory: true }, { field: 'company', label: 'Company', flex: 1 }, { field: 'payment', label: 'Payment', flex: 1, minWidth: 200 }, ], rowActions: rowActionsFluid, }, parameters: storybookDocsParameter(FluidTemplateCode), }; // Slots export const Slots: Story = { render: (_args, { argTypes }) => ({ components: { BentoDataGrid, BentoLink, BentoTypography, }, props: Object.keys(argTypes), template: ` <bento-data-grid v-bind="args" > <template #header> <bento-typography variant="title" el="h2"> Users </bento-typography> </template> <template #head-name="{column}"> <bento-typography variant="caption"> {{ column.label }} </bento-typography> </template> <template #item-name="{item}"> <bento-typography variant="body"> {{ item.name }} </bento-typography> <template v-if="item.company === 'Merchant Helium'"> <div style="display: inline-flex; gap: 4px;"> <bento-typography v-for="itemTag in item.tags" :key="itemTag" variant="caption" :style="{ color: 'var(--b-color-label-secondary)' }" > { {{ itemTag }} } </bento-typography> </div> </template> </template> <template #item-company="{item}"> <bento-link isNotRouting to="https://www.adyen.com" :title="item.company" > {{ item.company }} </bento-link> <bento-typography v-if="item.company === 'Merchant B'" variant="caption" :style="{ color: 'var(--b-color-label-secondary)' }"> {{ item.registered }} </bento-typography> </template> <template #item-payment="{item}"> $ {{ item.payment }} </template> <template #footer> <bento-typography variant="caption"> Footer </bento-typography> </template> </bento-data-grid> `, setup(props) { return { args: isVue2 ? props : _args }; }, }), args: { ...DEFAULT_ARGS, columns: [ { field: 'name', label: 'Name', flex: 2, mandatory: true }, { field: 'company', label: 'Company', flex: 1 }, { field: 'payment', label: 'Payment', flex: 1, minWidth: 200 }, ], }, parameters: storybookDocsParameter(DataGridWithSlotsExample), }; // Sorting const useSort = () => { const sortBy = ref<Array<BentoDatagridSortByColumn>>([]); const data = ref<BentoDatagridData>(DEFAULT_DATA); const sort = (sortedColumns: Array<BentoDatagridSortByColumn>) => { data.value = sortedColumns[0].direction === BentoDatagridSortDirection.ASCENDING ? data.value.sort((a, b) => (a[sortedColumns[0].field] as string).localeCompare(b[sortedColumns[0].field] as string) ) : data.value .sort((a, b) => (a[sortedColumns[0].field] as string).localeCompare(b[sortedColumns[0].field] as string) ) .reverse(); action('sort')(sortedColumns); }; return { data, sortBy, sort }; }; const SortingTemplateCode = ` <template> <bento-data-grid :data="data" :columns="columns" @sort="sort" :sort-by.sync="sortBy" /> </template> <script setup lang="ts"> import { BentoDataGrid, type BentoDatagridSortByColumn, type BentoDatagridData, type BentoColumn, } from '@adyen/bento-vue2' // Sortable fields must have "sortable: true" const columns: BentoColumn = [ { field: 'id', label: 'ID', minWidth: 300, sortable: true, mandatory: true }, { field: 'name', label: 'Name', minWidth: 300, mandatory: true }, ] const data = ref<BentoDatagridData>([ { id: 1, name: 'First' }, { id: 2, name: 'Second' }, ]); const sortBy = ref<Array<BentoDatagridSortByColumn>>([]); const sort = (sortedColumns: Array<BentoDatagridSortByColumn>) => { data.value = sortedColumns[0].direction === BentoDatagridSortDirection.ASCENDING ? data.value.sort((a, b) => (a[sortedColumns[0].field] as number) .toString() .localeCompare(b[sortedColumns[0].field] as string) ) : data.value .sort((a, b) => (a[sortedColumns[0].field] as number) .toString() .localeCompare(b[sortedColumns[0].field] as string) ) .reverse(); } </script> `; export const Sorting: Story = { render: (_args, { argTypes }) => ({ components: { BentoDataGrid }, props: Object.keys(argTypes), template: ` <bento-data-grid :data="data" :columns="columns" :sort-by.sync="sortBy" @sort="sort" /> `, setup(props) { const { data, sortBy, sort } = useSort(); return { args: isVue2 ? props : _args, data, sortBy, sort, }; }, }), args: { ...DEFAULT_ARGS, columns: [ { field: 'name', label: 'Name', minWidth: 300, sortable: true, mandatory: true }, { field: 'company', label: 'Company', minWidth: 150, sortable: true }, { field: 'payment', label: 'Payment', width: 200, minWidth: 150 }, ], sortBy: [], }, parameters: storybookDocsParameter(SortingTemplateCode), }; export const Selection: Story = { render: (_args, { argTypes }) => ({ components: { BentoDataGrid }, props: Object.keys(argTypes), template: ` <div> <bento-data-grid v-bind="args" :data="pagedData" :pagination="pagination" :filters="filtersConfig" :filter-values="filterValues" :is-data-item-disabled="isDataItemDisabled" :selection="selection" :filter-search-term="searchTerm" @update:filter-search-term="searchTerm = $event" @update:pagination="pagination = $event" @update:filter-values="filterValues = $event" @select-all-across-pages="onSelectAllAcrossPages" @select-all-current-results="onSelectAllInCurrentResults" @deselect-all-current-results="onDeselectAllInCurrentResults" @filter-search-clear="onSearchCleared" @update:selection="onSelect" @row-click="rowClickAction" /> </div> `, setup(props) { const args = isVue2 ? props : _args; const selection = ref([]); const filtersConfig = computed<BentoFilterBarModel>(() => args?.filters ?? FILTERS); const filterValues = ref<BentoFilterValues>([]); const searchTerm = ref(''); const data = ref( Array.from({ length: 20 }, (_, i) => ({ id: `id-${i + 1}`, name: `Name ${i + 1}`, payment: (i + 1) * 100, company: `Merchant ${i + 1}`, })) ); const isDataItemDisabled = (row: BentoDatagridDataItem) => { return row.id === data.value[3].id; }; const pagination = ref<BentoDatagridPaginationProps>({ page: 1, size: 10, totalCount: 20, hasNext: true, }); const filteredData = computed<BentoDatagridData>(() => { let internalData = data.value; if (searchTerm.value) { const lowerCaseSearchTerm = searchTerm.value.toLowerCase(); const searchableFields = args.columns.map(column => column.field); internalData = internalData.filter((item: BentoDatagridDataItem) => { return searchableFields.some(field => { const value = String(item[field]); return value.toLowerCase().includes(lowerCaseSearchTerm); }); }); } return internalData.filter(item => filterValues.value.every(filter => filter.value ? String(item[filter.field]).toLowerCase().includes(String(filter.value).toLowerCase()) : true ) ); }); const pagedData = computed<BentoDatagridData>(() => { return filteredData.value.slice( (pagination.value.page - 1) * pagination.value.size, pagination.value.page * pagination.value.size ); }); const enabledIds = computed(() => data.value.filter(item => !isDataItemDisabled(item)).map(x => x.id)); const enabledFilteredIds = computed(() => filteredData.value.filter(item => !isDataItemDisabled(item)).map(x => x.id) ); const onSelect = value => { selection.value = value; action('update:selection')(value); }; const onSelectAllAcrossPages = () => { selection.value = enabledIds.value; action('select-all-across-pages')(); }; const onSelectAllInCurrentResults = () => { selection.value = [...new Set([...selection.value, ...enabledFilteredIds.value])]; action('select-all-current-results')(); }; const onDeselectAllInCurrentResults = () => { selection.value = selection.value.filter(item => !enabledFilteredIds.value.includes(item)); action('deselect-all-current-results')(); }; const onSearchCleared = () => { searchTerm.value = ''; action('search-clear')(); }; watch( filteredData, () => { pagination.value.totalCount = filteredData.value.length; }, { immediate: true } ); return { args, onSelect, onSelectAllAcrossPages, onSelectAllInCurrentResults, onDeselectAllInCurrentResults, pagination, selection, pagedData, filtersConfig, filterValues, searchTerm, rowClickAction: value => action('row-click')(value), isDataItemDisabled, onSearchCleared, }; }, }), args: { ...DEFAULT_ARGS, selectable: true, allowRowClicks: true, selectableAcrossPages: true, bulkActions: [ { title: 'Edit', event: () => true, }, { title: 'Delete', critical: true, event: () => true, }, ], }, parameters: storybookDocsParameter(DataGridSelectionExample), }; // Filtering const FILTERS: BentoFilterBarModel = [ { field: 'name', label: 'Name', type: BentoFilterItemType.INPUT, }, { field: 'company', label: 'Company', type: BentoFilterItemType.INPUT, }, ]; export const Filtering: Story = { render: (_args, { argTypes }) => ({ components: { BentoDataGrid }, props: Object.keys(argTypes), setup(props) { const args = isVue2 ? props : _args; const filtersConfig = computed<BentoFilterBarModel>(() => args?.filters ?? FILTERS); const filterValues = ref<BentoFilterValues>([]); const searchTerm = ref(''); const searchConfig = ref<BentoFilterBarSearchBarProps>({ inputFieldAriaLabel: 'Search for items', placeholder: 'Placeholder', hint: 'Hint', }); const dataSet = computed<BentoDatagridData>(() => { let filteredData = args.data; // Apply global search term filter if (searchTerm.value) { const lowerCaseSearchTerm = searchTerm.value.toLowerCase(); const searchableFields = args.columns.map(column => column.field); filteredData = filteredData.filter((item: BentoDatagridDataItem) => { return searchableFields.some(field => { const value = String(item[field]); return value.toLowerCase().includes(lowerCaseSearchTerm); }); }); } filteredData = filteredData.filter(item => filterValues.value?.every(filter => { return filter.value ? String(item[filter.field]).toLowerCase().includes(String(filter.value).toLowerCase()) : true; }) ); return filteredData; }); watch(filterValues, () => { action('filter-values:update')(filterValues.value); }); const onSearchCleared = action('search-clear'); const onSearchInput = action('search-input'); const onSearchTermUpdated = action('update:search-term'); return { // Values dataSet, filtersConfig, filterValues, searchTerm, searchConfig, // Methods onSearchCleared, onSearchInput, onSearchTermUpdated, // Vue 3 storybook fix args, }; }, template: ` <bento-data-grid v-bind="args" :data="dataSet" :filters="filtersConfig" :filter-values.sync="filterValues" :filter-search-term.sync="searchTerm" :filter-search-config="searchConfig" @filter-search-clear="onSearchCleared" @filter-search-input="onSearchInput" @update:filter-search-term="onSearchTermUpdated" /> `, }), args: DEFAULT_ARGS, parameters: storybookDocsParameter(DataGridFilteringExample), }; // No Results const NoResultsTemplateCode = ` <bento-data-grid :data="[]" :columns="[ { field: 'name', label: 'Name', minWidth: 300, mandatory: true }, { field: 'company', label: 'Reference' }, { field: 'payment', label: 'Payment', width: 200, minWidth: 150 }, ]" :empty-state="{ title: 'No results were found', image: 'no-results-found', variant: 'embedded', action: { title: 'Clear filters', event: clearFiltersFunction, }, description: 'Try a different term or reset search filters', }" /> `; export const NoResults: Story = { ...Filtering, args: { ...DEFAULT_ARGS, data: [], filters: FILTERS, filterValues: [{ field: 'company', value: 'CIA' }], }, parameters: storybookDocsParameter(NoResultsTemplateCode), }; // Pagination const PaginationTemplateCode = ` <template> <bento-data-grid :data="dataGridData" :columns="columnsConfig" :pagination.sync="pagination" @update:pagination="onUpdatePagination" /> </template> <script lang="ts" setup> const data="[{ id: '1', name: 'Jane' }, { id: '1', name: 'John' }]" const pagination = ref({ description: 'datagrid navigation', hasNext: true, size: 10 }); // With API call watch( () => pagination.value, () => { makeAPICall(pagination.value); } ); // Without API call const onUpdatePagination = (newPagination: BentoDatagridPaginationProps) => { pagination.value = { ...newPagination }; const newSetStartIndex = pagination.value.page * pagination.value.size; const amountOfItems = newSetStartIndex + pagination.value.size - 1; data.value = arrayWithFullSetOfValues.slice(newSetStartIndex, amountOfItems); }; </script> `; export const Pagination: Story = { render: (_args, { argTypes }) => ({ components: { BentoDataGrid }, props: Object.keys(argTypes), template: ` <bento-data-grid v-bind="args" :data="dataGridData" :pagination.sync="pagination" @update:pagination="updatePagination" @items-page="updateItemsPerPage" /> `, setup(props) { const args = isVue2 ? props : _args; const largeData = [ ...DEFAULT_ARGS.data, ...DEFAULT_ARGS.data.map(dataItem => ({ ...dataItem, id: `${dataItem.id}+1` })), ]; const pagination = ref(args.pagination); const dataGridData = computed(() => { const end = pagination.value.size <= largeData.length ? pagination.value.size : largeData.length; return largeData.slice(pagination.value.page - 1, end); }); const updateItemsPerPage = (itemsPerPage: number) => { action('items-page')(itemsPerPage); }; const updatePagination = (newPagination: BentoDatagridPaginationProps) => { action('navigate')(newPagination.page); action('update:pagination')(newPagination); }; return { // Values dataGridData, pagination, // Methods updateItemsPerPage, updatePagination, // Vue 3 storybook fixes args: isVue2 ? props : _args, }; }, }), args: { ...DEFAULT_ARGS, pagination: { hasNext: true, description: 'datagrid navigation', size: 10, page: 1, totalCount: DEFAULT_ARGS.data.length * 2, }, }, parameters: storybookDocsParameter(PaginationTemplateCode), }; // Payment Status (demo use case for datagrid V2) export const EverythingBagelExample: Story = { render: (_args, { argTypes }) => ({ components: { BentoDataGrid, BentoLink, BentoStatus, BentoCurrency, BentoDate, BentoCountry, BentoButton }, props: Object.keys(argTypes), template: ` <bento-data-grid v-bind="args" :data="filteredAndSortedData" :columns.sync="columns" :condensed="isCondensed" :filters="filtersConfig" :filter-values.sync="filterValues" :filter-search-term.sync="searchTerm" :sort-by="sortBy" @update:sort-by="sortBy = $event" @update:condensed="isCondensed = $event" :selection.sync="selection" :is-data-item-disabled="isDataItemDisabledForSelection" :row-actions="rowActions" selectable @row-click="onRowClick" > <template #item-paymentStatus="{item}"> <div style="display: flex"> <bento-status :variant="getPaymentStatus(item.paymentStatus)" /> <span>{{ getPaymentText(item.paymentStatus) }}</span> </div> </template> <template #item-reference="{item}"> <bento-link isNotRouting to="https://www.adyen.com" :title="item.reference" > {{ item.reference }} </bento-link> </template> <template #item-countryCode="{item}"> <bento-country :code="item.countryCode" /> </template> <template #item-payment="{item}"> <bento-currency currency="EUR" :value="item.payment" /> </template> <template #item-date="{item}"> <bento-date :date="new Date(item.registered)" :format='BentoDateFormat.FULL_DATE_TIME_WITH_PERIOD'/> </template> </bento-data-grid> `, setup(props) { const args = isVue2 ? props : _args; const columns = ref<Array<BentoColumn>>(args.columns); const filtersConfig = ref<BentoFilterBarModel>(args.filters); const filterValues = ref<BentoFilterValues>(args?.filterValues); const sortBy = ref<Array<BentoDatagridSortByColumn>>([]); const searchTerm = ref(''); const isCondensed = ref(args.condensed); const getPaymentStatus = (paymentStatus: number) => { switch (paymentStatus) { case 2: return BentoStatusVariant.GREEN; case 1: return BentoStatusVariant.YELLOW; case 0: default: return BentoStatusVariant.RED; } }; const getPaymentText = (paymentStatus: number) => { switch (paymentStatus) { case 2: return 'Paid'; case 1: return 'In progress'; case 0: default: return 'Not paid'; } }; const onRowClick = (item: BentoDatagridDataItem) => { action('row-click')(item); }; // Selection const isDataItemDisabledForSelection = (row: BentoDatagridDataItem) => row.id === DEFAULT_DATA[3].id; const selection = ref<BentoDatagridSelection>([DEFAULT_DATA[1].id]); // Filtering const filteredData = computed(() => // If any filters or search terms have been set filterValues.value?.length === 0 && searchTerm.value === '' ? args.data : args.data.filter(item => { const foundFilteredItem = filterValues.value?.length > 0 ? filterValues.value.every(filter => filter.value ? (item[filter.field] as string) .toLowerCase() .includes((filter.value as string).toLowerCase()) : true ) : true; const foundSearchFilteredItem = searchTerm.value ? (item?.company as string) .toLowerCase() .includes((searchTerm.value as string).toLowerCase()) : true; return foundFilteredItem && foundSearchFilteredItem; }) ); const filteredAndSortedData = computed(() => { if (sortBy.value?.length === 0) { return filteredData.value; } return sortBy.value[0].direction === BentoDatagridSortDirection.ASCENDING ? filteredData.value.sort((a, b) => (a[sortBy.value[0].field] as string).localeCompare(b[sortBy.value[0].field] as string) ) : filteredData.value .sort((a, b) => (a[sortBy.value[0].field] as string).localeCompare(b[sortBy.value[0].field] as string) ) .reverse(); }); const rowActions: BentoDataGridRowActionsProp = (item: BentoDatagridDataItem) => { if (item.id === DEFAULT_DATA[3].id) { return [ { variant: 'edit', event: action('edit'), tooltipText: 'Edit payment', }, ]; } else if (item.id === DEFAULT_DATA[2].id) { return [ { variant: 'edit', event: action('edit'), tooltipText: 'Edit payment' }, { variant: 'open', isShort: true, menuData: [ { text: 'Cancel payment', event: action('cancel payment'), critical: true, }, { text: 'Transfer payment', event: action('transfer payment'), }, { text: 'Delete payment', event: action('delete payment'), disabled: true, }, ], }, ]; } else if (item.id === DEFAULT_DATA[1].id) { return []; } return [ { variant: 'edit', event: action('edit'), tooltipText: 'Edit payment', }, { title: 'Delete', critical: true, event: action('Delete'), tooltipText: 'Delete payment', }, ]; }; return { // Methods getPaymentStatus, getPaymentText, // Functions onRowClick, isDataItemDisabledForSelection, // Values columns, filteredAndSortedData, sortBy, filtersConfig, filterValues, searchTerm, isCondensed, selection, rowActions, // Enums BentoDateFormat, // Vue 3 storybook fix args, }; }, }), args: { ...DEFAULT_ARGS, zebraRows: true, filters: [ { field: 'company', label: 'Company', value: null, type: BentoFilterItemType.INPUT, }, { field: 'countryCode', label: 'Country/Region', value: undefined, options: { listboxItems: [ { value: BentoCountryCode.NL, }, { value: BentoCountryCode.BE, }, ], variant: 'name', }, type: BentoFilterItemType.SELECT_COUNTRY, }, ], columns: [ { field: 'company', label: 'Company', sortable: true, mandatory: true, autoWidth: true, frozen: true, minWidth: 200, }, { field: 'paymentStatus', label: 'Status', width: 180, infoIconTooltip: 'For current day' }, { field: 'reference', label: 'Reference', width: 320, overflow: BentoColumnOverflow.ELLIPSIS }, { field: 'countryCode', label: 'Country/Region', width: 320 }, { field: 'payment', label: 'Payment', width: 180, numeric: true }, { field: 'date', label: 'Execution Date', width: 200 }, ], allowRowClicks: true, allowColumnDragAndDrop: true, columnPanel: true, highlightedRows: [DEFAULT_DATA[0].id, DEFAULT_DATA[4].id], }, parameters: storybookDocsParameter(DataGridEverythingBagel), }; export const DisplayOptions = (_args, { argTypes }) => ({ components: { BentoDataGrid, BentoDate }, props: Object.keys(argTypes), template: ` <bento-data-grid v-bind="args" :columns.sync="columns" :condensed.sync="isCondensed"> <template #item-date="{item}"> <bento-date :date="new Date(item.registered)" :format='BentoDateFormat.FULL_DATE_TIME_WITH_PERIOD'/> </template> </bento-data-grid> `, setup(props) { const args = isVue2 ? props : _args; const isCondensed = ref(args.condensed); const columns = ref([ { field: 'name', label: 'Name', minWidth: 200, mandatory: true }, { field: 'reference', label: 'Reference', width: 320, overflow: BentoColumnOverflow.ELLIPSIS, frozen: true, }, { field: 'company', label: 'Company', minWidth: 150, width: 250, frozen: true }, { field: 'payment', label: 'Payment', width: 200, minWidth: 150 }, { field: 'countryCode', label: 'Country/Region', width: 280 }, { field: 'date', label: 'Execution Date', width: 300 }, ]); return { // Values columns, isCondensed, // Enums BentoDateFormat, // Vue 3 storybook fix args, }; }, }); DisplayOptions.args = { ...DEFAULT_ARGS, allowColumnDragAndDrop: true, columnPanel: true, }; DisplayOptions.parameters = storybookDocsParameter(` <bento-data-grid column-panel :data="dataArray" :columns.sync="columns" :condensed.sync="isCondensed" />`); export const GridInCard: Story = { render: (_args, { argTypes }) => ({ components: { BentoCard, BentoDataGrid, BentoDate }, props: Object.keys(argTypes), template: ` <div style="max-width: 500px;"> <bento-card> Grid in a card <template #content> <div style="height: 300px;"> <bento-data-grid v-bind="args" :columns.sync="columns" :condensed.sync="isCondensed" :pagination="pagination" fit-content > <template #item-date="{item}"> <bento-date :date="new Date(item.registered)" :format='BentoDateFormat.FULL_DATE_TIME_WITH_PERIOD'/> </template> </bento-data-grid> </div> </template> </bento-card> </div> `, setup(props) { const args = isVue2 ? props : _args; const isCondensed = ref(args.condensed); const pagination = ref({ hasNext: true, size: 20, page: 1, description: 'datagrid navigation', }); const columns = ref([ { field: 'name', label: 'Name', minWidth: 200, mandatory: true }, { field: 'reference', label: 'Reference', width: 320, overflow: BentoColumnOverflow.ELLIPSIS, }, { field: 'company', label: 'Company', minWidth: 150, width: 250 }, { field: 'payment', label: 'Payment', width: 200, minWidth: 150 }, { field: 'countryCode', label: 'Country/Region', width: 280 }, { field: 'date', label: 'Execution Date', width: 300 }, ]); return { // Values columns, isCondensed, pagination, // Enums BentoDateFormat, // Vue 3 storybook fix args, }; }, }), args: { ...DEFAULT_ARGS, columnPanel: false, }, parameters: storybookDocsParameter(DataGridFitContentInCardExample), }; // Default const lazyLoadingCode = ` // With infinite scroll <bento-data-grid :data="dataArray" :columns="[ { field: 'name', label: 'Name', minWidth: 300, mandatory: true }, { field: 'company', label: 'Reference' }, { field: 'payment', label: 'Payment', width: 200, minWidth: 150 }, ]" lazy-load-type="automatic" :loading="yourLoadingLogic" @update:load-more="yourInfiniteScrollHandler" /> // With 'load more' button <bento-data-grid :data="dataArray" :columns="[ { field: 'name', label: 'Name', minWidth: 300, mandatory: true }, { field: 'company', label: 'Reference' }, { field: 'payment', label: 'Payment', width: 200, minWidth: 150 }, ]" lazy-load-type="button" :loading="yourLoadingLogic" @update:load-more="yourInfiniteScrollHandler" />`; export const lazyLoading: Story = { render: (_args, { argTypes }) => ({ components: { BentoDataGrid }, props: Object.keys(argTypes), template: ` <div style="height: 300px;"> <bento-data-grid v-bind="args" :lazy-load-type="lazyLoadType" :loading="loading" @update:show-more="infiniteScrollHandler"/> </div> `, setup(props) { const args = isVue2 ? props : _args; const loading = ref(false); const data = ref(DEFAULT_DATA); const infiniteScrollHandler = () => { loading.value = true; setTimeout(() => { data.value.push(...ADDITIONAL_DATA); loading.value = false; }, 2000); }; return { args, loading, infiniteScrollHandler, }; }, }), args: { ...DEFAULT_ARGS, lazyLoadType: 'automatic', }, parameters: storybookDocsParameter(lazyLoadingCode), }; // Added just to be able to easily play around with the sticky headers/pagination/filters // Not really relevant as a Storybook example, so not adding storybook code example export const LongDataWithStickyFiltersAndPagination: Story = { render: (_args, { argTypes }) => ({ components: { BentoDataGrid }, props: Object.keys(argTypes), setup(props) { const args = isVue2 ? props : _args; const filtersConfig = ref<BentoFilterBarModel>([ { field: 'col1', label: 'Column 1', type: BentoFilterItemType.INPUT, }, { field: 'col2', label: 'Column 2', type: BentoFilterItemType.INPUT, }, ]); const filterValues = ref(); const searchTerm = ref(''); const isCondensed = ref(args.condensed); const columns = ref( Array.from({ length: 20 }, (_, index) => ({ field: `col${index + 1}`, label: `Column ${index + 1}`, width: 200, mandatory: index === 0, })) ); const data = ref( Array.from({ length: 50 }, (_, rowIndex) => columns.value.reduce((acc, col, colIndex) => { acc[col.field] = `Row ${rowIndex + 1} - Col ${colIndex + 1}`; return acc; }, {}) ) ); const dataSet = computed(() => // If any filters or search terms have been set filterValues.value?.length === 0 && searchTerm.value === '' ? data.value : data.value.filter(item => { const foundFilteredItem = filterValues.value?.length > 0 ? filterValues.value.every(filter => filter.value ? (item[filter.field] as string) .toLowerCase() .includes((filter.value as string).toLowerCase()) : true ) : true; const foundSearchFilteredItem = searchTerm.value ? Object.keys(item).some(key => key ? (item[key] as string) .toLowerCase() .includes((searchTerm.value as string).toLowerCase()) : true ) : true; return foundFilteredItem && foundSearchFilteredItem; }) ); const onSearchCleared = action('search-clear'); const onSearchInput = action('search-input'); const onSearchTermUpdated = action('update:search-term'); return { // Values dataSet, filtersConfig, filterValues, searchTerm, columns, isCondensed, // Methods onSearchCleared, onSearchInput, onSearchTermUpdated, // Vue 3 storybook fix args, }; }, template: ` <bento-data-grid v-bind="args" :data="dataSet" :columns.sync="columns" :condensed.sync="isCondensed" :filters="filtersConfig" :filter-values.sync="filterValues" :filter-search-term.sync="searchTerm" @filter-search-clear="onSearchCleared" @filter-search-input="onSearchInput" @update:filter-search-term="onSearchTermUpdated" /> `, }), args: { ...DEFAULT_ARGS, columnPanel: true, stickyPagination: true, stickyFilters: true }, }; // Cell actions export const CellActions: Story = { render: (_args, { argTypes }) => ({ components: { BentoDataGrid }, props: Object.keys(argTypes), template: ` <bento-data-grid v-bind="args" :columns="columns"> <template #item-custom="{item}"> Value </template> </bento-data-grid> `, setup(props) { // --- Action Handlers --- const editItem: BentoColumnCellActionEvent = (rowDataItem, columnKey) => { action('edit')(rowDataItem, columnKey); }; const cancelAction: BentoColumnCellActionEvent = (rowDataItem, columnKey) => { action('cancel')(rowDataItem, columnKey); }; const transferAction: BentoColumnCellActionEvent = (rowDataItem, columnKey) => { action('transfer')(rowDataItem, columnKey); }; const deleteAction: BentoColumnCellActionEvent = (rowDataItem, columnKey) => { action('delete')(rowDataItem, columnKey); }; // --- Dynamic Cell Actions Function --- // This function generates actions based on the row's data. const getNameCellActions: BentoColumnCellActionsProp = rowDataItem => { const actions: Array<BentoColumnCellAction> = []; // Conditionally add the 'edit' action only for the first row. if (rowDataItem.id === DEFAULT_DATA[3].id) { actions.push({ variant: 'edit', event: editItem, tooltipText: 'Edit this name' }); } // Add a 'more' menu with dynamic options. actions.push({ variant: 'more', isShort: true, tooltipText: 'Additional actions', menuData: [ { text: 'Cancel payment', event: cancelAction, critical: true, }, { text: 'Transfer payment', event: transferAction, }, { text: 'Delete payment', event: deleteAction, // Example: disable delete for the second row. disabled: rowDataItem.id === DEFAULT_DATA[2].id, }, ], }); return actions; }; // --- Column Definitions --- const columns: Array<BentoColumn> = [ { field: 'name', label: 'Name (Dynamic Actions)', minWidth: 300, mandatory: true, // Use a function for dynamic cell actions cellActions: getNameCellActions, }, { field: 'company', label: 'Reference', autoWidth: true }, { field: 'payment', label: 'Payment (Static Actions)', minWidth: 350, // Use a static array for cell actions cellActions: [ { variant: 'copy', getCopyData: (rowDataItem: BentoDatagridDataItem, columnKey: string) => `${rowDataItem[columnKey]}`, }, { variant: 'open', event: action('open-payment'), tooltipText: 'Open payment details' }, ], }, ]; return { args: isVue2 ? props : _args, columns }; }, }), args: { ...DEFAULT_ARGS, highlightedRows: [DEFAULT_DATA[3].id], columns: [ { field: 'name', label: 'Name (Dynamic Actions)', minWidth: 300, mandatory: true, cellActions: [ () => (rowDataItem: BentoDatagridDataItem) => ({ variant: 'edit', disabled: rowDataItem.id === DEFAULT_DATA[3].id, event: action('edit'), tooltipText: 'Edit this name', }), { variant: 'open', tooltipText: 'Additional actions', menuData: [ { text: 'Cancel payment', event: action('cancel payment'), critical: true, }, { text: 'Transfer payment', event: action('transfer payment'), }, { text: 'Delete payment', event: action('delete payment'), disabled: true, }, ], }, ], }, { field: 'company', label: 'Reference', autoWidth: true }, { field: 'payment', label: 'Payment (Static Actions)', minWidth: 350, cellActions: [ { variant: 'copy', getCopyData: (rowDataItem: BentoDatagridDataItem, columnKey: string) => `$${rowDataItem[columnKey]}`, }, { variant: 'more', tooltipText: 'Additional actions', menuData: [ { text: 'Cancel payment', event: action('cancel payment'), critical: true, }, { text: 'Transfer payment', event: action('transfer payment'), }, { text: 'Delete payment', event: action('delete payment'), disabled: true, }, ], isShort: true, }, ], }, { field: 'custom', label: 'Custom template', minWidth: 200, cellActions: [{ variant: 'edit', event: action('edit'), tooltipText: 'Edit custom' }], }, ], }, parameters: storybookDocsParameter(DataGridCellActionsExample), }; // NestContentSlot const NestedContentSlotTemplateCode = ` <template> <bento-data-grid :data="data" :columns="columns" has-nested-content :should-show-nested-content="filterRowsThatShowNestedContent" > <template #nested-slot="{item}"> <bento-typography>Nested content for {{ item.company }}</bento-typography> </template> </bento-data-grid> </template> <script setup lang="ts"> import { BentoDataGrid, type BentoColumn, type BentoDatagridData, type BentoDatagridShouldShowNestedContent } from '@adyen/bento-vue2' const columns: BentoColumn = [ { field: 'name', label: 'Name', minWidth: 300, mandatory: true }, { field: 'payment', label: 'Payment', minWidth: 300, mandatory: true }, ] const data = ref<BentoDatagridData>([ { id: 1, name: 'First', payment: 20.9 }, { id: 2, name: 'Second', payment: 10.0 }, ]); const filterRowsThatShowNestedContent: BentoDatagridShouldShowNestedContent = (item) => item.id === 2; </script> `; export const NestedContentSlot: Story = { render: (_args, { argTypes }) => ({ components: { BentoDataGrid, BentoLink, BentoTypography, }, props: Object.keys(argTypes), template: ` <bento-data-grid v-bind="args" > <template #nested-slot="{item}"> <div style="height: 80px; text-align: center; display: flex; align-items: center;"> <bento-typography>Nested content slot for {{ item.company }}</bento-typography> </div> </template> </bento-data-grid> `, setup(props) { return { args: isVue2 ? props : _args }; }, }), args: { ...DEFAULT_ARGS, hasNestedContent: true, zebraRows: true, shouldShowNestedContent: (item: BentoDatagridDataItem) => item.id !== DEFAULT_ARGS.data[1].id, }, parameters: storybookDocsParameter(NestedContentSlotTemplateCode), }; export const StickyHorizontalScrollbar: Story = { render: (_args, { argTypes }) => ({ components: { BentoDataGrid, BentoButton }, props: Object.keys(argTypes), setup(props) { const args = isVue2 ? props : _args; const filtersConfig = ref<BentoFilterBarModel>([ { field: 'col1', label: 'Column 1', type: BentoFilterItemType.INPUT, }, { field: 'col2', label: 'Column 2', type: BentoFilterItemType.INPUT, }, ]); const filterValues = ref(); const searchTerm = ref(''); const isCondensed = ref(args.condensed); const columns = ref( Array.from({ length: 20 }, (_, index) => ({ field: `col${index + 1}`, label: `Column ${index + 1}`, width: 200, mandatory: index === 0, })) ); const data = ref( Array.from({ length: 50 }, (_, rowIndex) => columns.value.reduce((acc, col, colIndex) => { acc[col.field] = `Row ${rowIndex + 1} - Col ${colIndex + 1}`; return acc; }, {}) ) ); const dataSet = computed(() => // If any filters or search terms have been set filterValues.value?.length === 0 && searchTerm.value === '' ? data.value : data.value.filter(item => { const foundFilteredItem = filterValues.value?.length > 0 ? filterValues.value.every(filter => filter.value ? (item[filter.field] as string) .toLowerCase() .includes((filter.value as string).toLowerCase()) : true ) : true; const foundSearchFilteredItem = searchTerm.value ? Object.keys(item).some(key => key ? (item[key] as string) .toLowerCase() .includes((searchTerm.value as string).toLowerCase()) : true ) : true; return foundFilteredItem && foundSearchFilteredItem; }) ); const onSearchCleared = action('search-clear'); const onSearchInput = action('search-input'); const onSearchTermUpdated = action('update:search-term'); return { // Values dataSet, filtersConfig, filterValues, searchTerm, columns, isCondensed, // Methods onSearchCleared, onSearchInput, onSearchTermUpdated, // Vue 3 storybook fix args, }; }, template: ` <bento-data-grid v-bind="args" :data="dataSet" :columns.sync="columns" :condensed.sync="isCondensed" :filters="filtersConfig" :filter-values.sync="filterValues" :filter-search-term.sync="searchTerm" @filter-search-clear="onSearchCleared" @filter-search-input="onSearchInput" @update:filter-search-term="onSearchTermUpdated" > <template #pagination-slot> <bento-button variant="tertiary"> Pagination slot button </bento-button> </template> </bento-data-grid> `, }), args: { ...DEFAULT_ARGS, pagination: { page: 1, hidePageSize: true }, columnPanel: true, stickyPagination: true, stickyFilters: true, stickyHorizontalScrollbar: true, }, }; const NestedRowsTemplateCode = ` <template> <bento-data-grid :data="data" :columns="columns" has-nested-rows :should-show-nested-row="filterRowsThatShowNestedContent" @nested-content-toggled="onParentRowToggle" /> </template> <script setup lang="ts"> import { BentoDataGrid, type BentoColumn, type BentoDatagridData, type BentoDatagridShouldShowNestedRowsFn } from '@adyen/bento-vue2' const columns: BentoColumn = [ { field: 'name', label: 'Name', minWidth: 300, mandatory: true }, { field: 'payment', label: 'Payment', minWidth: 300, mandatory: true }, ] const data = ref<BentoDatagridData>([ { id: 1, name: 'First', payment: 20.9 }, { id: 2, name: 'Second', payment: 10.0 }, ]); const filterRowsThatShowNestedRows: BentoDatagridShouldShowNestedRowsFn = (item) => item.id === 2; const onParentRowToggle = async (item: BentoDatagridDataItem) => { const childrenForToggledParentItem = await makeAPICallToGetChildren(item.id); dataState.value = dataState.value.map(itemToChange => { if (itemToChange.id === item.id) { return { ...itemToChange, children: childrenForToggledParentItem } } return itemToChange; }); }; </script> `; export const NestedRows: Story = { render: (_args, { argTypes }) => ({ components: { BentoDataGrid, BentoLink, BentoTypography, }, props: Object.keys(argTypes), template: ` <bento-data-grid v-bind="args" :data="dataState" @nested-content-toggled="onParentRowToggle" /> `, setup(props) { const dataState = ref<Array<BentoDatagridDataItem>>(DEFAULT_ARGS.data); const selection = ref<BentoDatagridDataItemId>(); const onParentRowToggle = (item: BentoDatagridDataItem) => { setTimeout(() => { const children = [ { id: `03c0a799-1ced-4017-aea3-111111${item.id}`, name: `Child of ${item.name} #1`, payment: 160405, company: 'Merchant Helium', registered: '2023-06-30T13:56:09.498Z', tags: ['deserunt', 'et', 'esse'], reference: 'UnbQaUncf3eR1D8XUBqn_UnbQaUncf3eR1D8XUBqn_UnbQaUncf3eR1D8XUBqn', paymentStatus: 1, countryCode: BentoCountryCode.NL, parentId: DEFAULT_ARGS.data[1].id, }, { id: `03c0a799-1ced-4017-aea3-1111113${item.id}`, name: `Child of ${item.name} #2`, payment: 163500, company: 'Merchant B', registered: '2023-04-28T13:11:09.498Z', tags: ['et', 'Lorem', 'sint'], reference: 'IxUZ9FsEPCZ9dFFPpI9C', paymentStatus: 2, countryCode: BentoCountryCode.BE, parentId: DEFAULT_ARGS.data[1].id, }, { id: `03c0a799-1ced-4017-aea3-1111114${item.id}`, name: `Child of ${item.name} #3`, payment: 78500, company: 'Merchant C', registered: '2023-06-30T11:01:09.498Z', tags: ['laborum', 'enim', 'ullamco'], reference: 'y0qAQ0WDszRyqLY2WO9f', paymentStatus: 1, countryCode: BentoCountryCode.UA, parentId: DEFAULT_ARGS.data[1].id, }, ]; dataState.value = dataState.value.map(itemToChange => { if (itemToChange.id === item.id) { return { ...itemToChange, children, }; } return itemToChange; }); }, 1000); }; return { args: isVue2 ? props : _args, dataState, selection, onParentRowToggle }; }, }), args: { ...DEFAULT_ARGS, hasNestedRows: true, zebraRows: true, shouldShowNestedRows: dataItem => { return dataItem.id !== DEFAULT_ARGS.data[1].id; }, }, parameters: storybookDocsParameter(NestedRowsTemplateCode), }; export const RowActions: Story = { render: (_args, { argTypes }) => ({ components: { BentoDataGrid }, props: Object.keys(argTypes), template: ` <bento-data-grid v-bind="args" :row-actions="rowActions" /> `, setup(props) { const rowActions: BentoDataGridRowActionsProp = (item: BentoDatagridDataItem) => { if (item.id === DEFAULT_DATA[0].id) { return [ { variant: 'edit', event: action('edit'), tooltipText: 'Edit row' }, { title: 'Delete', critical: true, event: action('Delete'), tooltipText: 'Delete row', }, ]; } else if (item.id === DEFAULT_DATA[1].id) { return []; } else if (item.id === DEFAULT_DATA[2].id) { return [ { variant: 'edit', event: action('edit'), tooltipText: 'Edit row' }, { variant: 'open', isShort: true, tooltipText: 'More options', menuData: [ { text: 'Cancel payment', event: action('cancel payment'), critical: true, }, { text: 'Transfer payment', event: action('transfer payment'), }, { text: 'Delete payment', event: action('delete payment'), disabled: true, }, ], }, ]; } return [ { variant: 'edit', event: action('edit'), tooltipText: 'Edit row', }, ]; }; return { args: isVue2 ? props : _args, rowActions, }; }, }), args: { ...DEFAULT_ARGS, }, parameters: storybookDocsParameter(DataGridRowActionsExample), }; const SummaryCellTemplateCode = ` <template> <bento-data-grid :columns="columns" :data="dataSet" :filters="filtersConfig" :filter-values.sync="filterValues" :filter-search-term="searchTerm" :filter-search-config="searchConfig" @update:filter-values="filterValues = $event" @update:filter-search-term="searchTerm = $event" @filter-search-clear="onSearchCleared" > <template #item-company="{item}"> <bento-data-grid-summary-cell :label="\`\${item.company.length} merchants\`" :items="item.company" :search-term="companyColumnSearchTerm" /> </template> </bento-data-grid> </template> <script setup lang="ts"> import { BentoColumn, BentoDataGrid, BentoDataGridSummaryCell, BentoDatagridData, BentoFilterBarModel, BentoFilterBarSearchBarProps, BentoFilterItemType, BentoFilterValues, BentoCountryCode, } from '@adyen/bento-vue2'; import { computed, ref } from 'vue'; const columns: Array<BentoColumn> = [ { field: 'name', label: 'Name', flex: 2, mandatory: true }, { field: 'company', label: 'Company', flex: 1 }, { field: 'payment', label: 'Payment', flex: 1, minWidth: 200 }, ]; /** * Data set with array of companies */ const multiData: BentoDatagridData = [ { id: '03c0a799-1ced-4017-aea3-0cebb0106335', name: 'Shepherd Kane', payment: 160405, company: ['Merchant Helium', 'Merchant A'], }, { id: '8f9e7877-1ca2-4929-9217-f1ec78709291', name: 'Joyce Johns', payment: 78500, company: ['Merchant C', 'Merchant Hedra'], }, { id: '51d2b31d-d577-4fca-8329-a583babbbdda', name: 'Evans Zamora', payment: 1153.4, company: [ 'Merchant A', 'Merchant B', 'Merchant C', 'Merchant D', 'Merchant E', 'Merchant F', 'Merchant G', 'Merchant H', 'Merchant I', 'Merchant J', ], }, ]; const filtersConfig = computed<BentoFilterBarModel>(() => [ { field: 'name', label: 'Name', type: BentoFilterItemType.INPUT, }, { field: 'company', label: 'Company', type: BentoFilterItemType.INPUT, }, ]); /** * The values of applied filters. */ const filterValues = ref<BentoFilterValues>(); /** * The current global search term that is being searched with */ const searchTerm = ref(''); /** * Global search configuration */ const searchConfig = ref<BentoFilterBarSearchBarProps>({ inputFieldAriaLabel: 'Descriptive details about the search bar', debounceTime: 1000, // in ms placeholder: 'Find merchants', }); /** * Concatenated search term using the global search and the company filter value */ const companyColumnSearchTerm = computed( () => searchTerm.value || filterValues.value?.find(filter => filter.field === 'company')?.value?.toString() || '' ); /** * Simplified computed data set that filters data depending on applied filters and global search values */ const dataSet = computed<BentoDatagridData>(() => { let filteredData = multiData; // Apply global search term filter if (searchTerm.value) { const lowerCaseSearchTerm = searchTerm.value.toLowerCase(); filteredData = filteredData.filter( item => (item.name as string).toLowerCase().includes(lowerCaseSearchTerm) || (item.company as string[]).some(company => company.toLowerCase().includes(lowerCaseSearchTerm) ) ); } // Apply filterValues filter if (filterValues.value?.length) { filteredData = filteredData.filter(item => filterValues.value?.every(filter => { if (filter.field === 'company') { return filter.value ? (item[filter.field] as string[]).some(company => (company as string) .toLowerCase() .includes((filter.value as string).toLowerCase()) ) : true; } return filter.value ? (item[filter.field] as string) .toLowerCase() .includes((filter.value as string).toLowerCase()) : true; }) ); } return filteredData; }); const onSearchCleared = () => { searchTerm.value = ''; }; </script> `; export const SummaryCell: Story = { render: (_args, { argTypes }) => ({ components: { BentoDataGrid, BentoLink, BentoTypography, BentoDashedUnderline, BentoDataGridSummaryCell, }, props: Object.keys(argTypes), template: ` <bento-data-grid v-bind="args" :data="dataSet" :filters="filtersConfig" :filter-values.sync="filterValues" :filter-search-term="searchTerm" :filter-search-config="searchConfig" @update:filter-values="filterValues = $event" @update:filter-search-term="searchTerm = $event" @filter-search-clear="onSearchCleared" > <template #item-company="{item}"> <bento-data-grid-summary-cell :label="\`\${item.company.length} merchants\`" :items="item.company" :search-term="companyColumnSearchTerm" /> </template> </bento-data-grid> `, setup(props) { /** * Data set with array of companies */ const multiData: BentoDatagridData = [ { id: '03c0a799-1ced-4017-aea3-0cebb0106335', name: 'Shepherd Kane', payment: 160405, company: ['Merchant Helium', 'Merchant A'], }, { id: 'b6d4f1fd-b1f6-40a6-a443-f785c89604ef', name: 'Santos Palmer', payment: 163500, company: ['Merchant B'], }, { id: '8f9e7877-1ca2-4929-9217-f1ec78709291', name: 'Joyce Johns', payment: 78500, company: ['Merchant C', 'Merchant Hedra'], }, { id: '0789a012-85a6-4657-87bb-319141938a61', name: 'Lynnette Melendez', payment: -45000, company: ['Merchant A', 'Merchant B', 'Merchant C'], }, { id: '0b3981b6-c73b-47ad-81d1-b06f9b95d467', name: 'Wagner Mcintosh', payment: 1112, company: ['Merchant A'], }, { id: '51d2b31d-d577-4fca-8329-a583babbbdda', name: 'Evans Zamora', payment: 1153.4, company: [ 'Merchant A', 'Merchant B', 'Merchant C', 'Merchant D', 'Merchant E', 'Merchant F', 'Merchant G', 'Merchant H', 'Merchant I', 'Merchant J', 'Merchant K', 'Merchant L', 'Merchant M', 'Merchant N', 'Merchant O', 'Merchant P', 'Merchant Q', 'Merchant R', 'Merchant S', 'Merchant T', ], }, { id: '9bd59a6e-bd03-4477-813f-b844953b7645', name: 'Anderson Middleton', payment: 181722.23, company: ['Merchant A'], }, ]; const filtersConfig = computed<BentoFilterBarModel>(() => [ { field: 'name', label: 'Name', type: BentoFilterItemType.INPUT, }, { field: 'company', label: 'Company', type: BentoFilterItemType.INPUT, }, ]); /** * The values of applied filters. */ const filterValues = ref<BentoFilterValues>(); /** * The current global search term that is being searched with */ const searchTerm = ref(''); /** * Global search configuration */ const searchConfig = ref<BentoFilterBarSearchBarProps>({ inputFieldAriaLabel: 'Descriptive details about the search bar', debounceTime: 1000, // in ms placeholder: 'Find merchants', }); /** * Concatenated search term using the global search and the company filter value */ const companyColumnSearchTerm = computed( () => searchTerm.value || filterValues.value?.find(filter => filter.field === 'company')?.value?.toString() || '' ); /** * Simplified computed data set that filters data depending on applied filters and global search values */ const dataSet = computed<BentoDatagridData>(() => { let filteredData = multiData; // Apply global search term filter if (searchTerm.value) { const lowerCaseSearchTerm = searchTerm.value.toLowerCase(); filteredData = filteredData.filter( item => (item.name as string).toLowerCase().includes(lowerCaseSearchTerm) || (item.company as Array<string>).some(company => company.toLowerCase().includes(lowerCaseSearchTerm) ) ); } // Apply filterValues filter if (filterValues.value?.length) { filteredData = filteredData.filter(item => filterValues.value?.every(filter => { if (filter.field === 'company') { return filter.value ? (item[filter.field] as Array<string>).some(company => (company as string) .toLowerCase() .includes((filter.value as string).toLowerCase()) ) : true; } return filter.value ? (item[filter.field] as string) .toLowerCase() .includes((filter.value as string).toLowerCase()) : true; }) ); } return filteredData; }); const onSearchCleared = () => { searchTerm.value = ''; }; return { args: isVue2 ? props : _args, filtersConfig, filterValues, searchTerm, searchConfig, onSearchCleared, dataSet, companyColumnSearchTerm, }; }, }), args: { ...DEFAULT_ARGS, columns: [ { field: 'name', label: 'Name', flex: 2, mandatory: true }, { field: 'company', label: 'Company', flex: 1 }, { field: 'payment', label: 'Payment', flex: 1, minWidth: 200 }, ], zebraRows: true, }, parameters: storybookDocsParameter(SummaryCellTemplateCode), }; export const TrendCell: Story = { render: (_args, { argTypes }) => ({ components: { BentoDataGrid, BentoDataGridTrendCell, }, props: Object.keys(argTypes), template: ` <bento-data-grid v-bind="args"> <template #item-performance="{ item }"> <bento-data-grid-trend-cell :value="item.performance.value" :direction="item.performance.direction" :variant="item.performance.variant" /> </template> </bento-data-grid> `, setup(props) { const columns: Array<BentoColumn> = [ { field: 'name', label: 'Name', flex: 2 }, { field: 'performance', label: 'Performance', flex: 1, numeric: true }, ]; const data: BentoDatagridData = [ { id: '1', name: 'Shepherd Kane', performance: { value: 25.5, variant: 'green', direction: 'up', }, }, { id: '2', name: 'Santos Palmer', performance: { value: 10.2, variant: 'red', direction: 'down', }, }, { id: '3', name: 'Joyce Johns', performance: { value: 0, variant: 'black', direction: 'none', }, }, { id: '4', name: 'Lynnette Melendez', performance: { value: '3.1428571429%', variant: 'green', direction: 'down', }, }, { id: '5', name: 'Wagner Mcintosh', performance: { value: '15.2%', variant: 'red', direction: 'up', }, }, { id: '6', name: 'Evans Zamora', performance: { value: '1.23', direction: 'up', }, }, { id: '7', name: 'Anderson Middleton', performance: { value: '90', direction: 'down', }, }, ]; return { args: { ...(isVue2 ? props : _args), columns, data }, }; }, }), args: { ...DEFAULT_ARGS, zebraRows: true, }, parameters: storybookDocsParameter(DataGridTrendCellExample), }; export const TagWithTextCell: Story = { render: (_args, { argTypes }) => ({ components: { BentoDataGrid, BentoDataGridTagWithTextCell, }, props: Object.keys(argTypes), template: ` <bento-data-grid v-bind="args"> <template #item-endpoint="{ item }"> <bento-data-grid-tag-with-text-cell :tag="item.endpoint.tag" :value="item.endpoint.value" /> </template> </bento-data-grid> `, setup(props) { const columns = [ { field: 'name', label: 'Name', flex: 2 }, { field: 'endpoint', label: 'Endpoint', flex: 2 }, ]; const data = [ { id: '1', name: 'Get users', endpoint: { tag: { label: 'GET', variant: 'blue' }, value: '/api/users', }, }, { id: '2', name: 'Create user', endpoint: { tag: { label: 'POST', variant: 'green' }, value: '/api/users', }, }, { id: '3', name: 'Update user', endpoint: { tag: { label: 'PUT', variant: 'yellow' }, value: '/api/users/:id', }, }, { id: '4', name: 'Delete user', endpoint: { tag: { label: 'DELETE', variant: 'red' }, value: '/api/users/:id', }, }, ]; return { args: { ...(isVue2 ? props : _args), columns, data }, }; }, }), args: { ...DEFAULT_ARGS, }, parameters: storybookDocsParameter(DataGridTagWithTextCellExample), };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
<template> <div ref="dataGridRef" class="b-data-grid" data-testid="data-grid" :class="conditionalGridClasses" aria-live="polite" :aria-busy="loading" @copy="handleClipboard" > <div v-if="filters" ref="filtersRef" class="b-data-grid__filters" :class="conditionalFilterClasses" :style="{ top: computedStickyFiltersTopPosition }" :aria-controls="dataGridUid" :aria-label="t('filterBar')" role="region" > <bento-filter-bar v-if="filters" ref="filterBarRef" :config="filters" :container-size-layout="filtersContainerSizeLayout" :filter-values="filterValues" :filter-values-object="filterValuesObject" :search="filterSearchConfig" :search-term="filterSearchTerm" @search-input="onSearchTermInput" @search-clear="onSearchTermCleared" @input="filters => $emit(BentoDatagridEvent.UPDATE_FILTERS, filters)" @update:filter-values="filterValues => $emit(BentoDatagridEvent.UPDATE_FILTER_VALUES, filterValues)" @update:filter-values-object=" filterValueObject => $emit(BentoDatagridEvent.UPDATE_FILTER_VALUES_OBJECT, filterValueObject) " /> </div> <div v-if="$slots.header" class="b-data-grid__header"> <slot name="header"></slot> </div> <div :id="dataGridUid" :role="ariaGridRole" :aria-rowcount="data.length" :aria-colcount="columns.length" :aria-label="emptyState ? t('noResults') : null" class="b-data-grid__wrapper" :style="{ top: computedColumnHeadersTopPosition }" > <div class="b-data-grid__column-headers" :style="{ top: computedColumnHeadersTopPosition }"> <div ref="columnsRef" role="rowgroup" class="b-data-grid__columns-row" @scroll="updateScrollPosition" @keydown="navigationInput" > <div v-if="columnPanel" role="row"> <bento-data-grid-config-settings class="b-data-grid__config-settings" role="gridcell" :columns="localColumnsState" :condensed="condensed" :zebra-rows="internalZebraRows" @update:condensed="onConfigSettingsCondensedUpdate" @update:zebra-rows="onConfigSettingsZebraRowsUpdate" @update:columns="onConfigSettingsColumnsUpdate" /> </div> <div :style="gridRowWidthStyle" class="b-data-grid__columns" role="row"> <template v-for="({ columnSet, id, isFrozenColumns }, index) of columnSets"> <data-grid-columns v-if="columnSet?.length" :key="`data-grid-columns-${id}`" :ref="el => registerColumnHeaderRefs(el)" :columns="columnSet" :has-resizable-columns="hasResizableColumns" :sort-by="sortBy" :all-items-selected-state="allItemsSelectedState" :is-frozen-columns="isFrozenColumns" :show-frozen-columns-separator="showFrozenColumnsSeparator" :dragging-columns="draggingColumns" :has-draggable-columns="allowColumnDragAndDrop" :has-column-panel="columnPanel" :has-left-overflow="hasLeftOverflow" :has-first-column-panel-right-aligned="index === computedColumnPanelAlignment" :hovered-over-column-state="hoveredOverColumnState" :is-measuring-column-widths="isMeasuringColumnWidths" :is-only-one-visible-column="isOnlyOneVisibleColumn" :selectable="data?.length > 0 && selectable" :selectable-across-pages="selectableAcrossPagesComputed" :selected-items="selectedItems" :visible-rows-state="visibleRowsState" :selection-state="selectionState" :truncate-column-headers="truncateColumnHeaders" :row-actions-maximum-width="rowActionsMaxWidth" v-on="draggingEventsForColumns" @column-hover="hoveredOverColumn = $event" @column-move="onMoveColumnByOffset" @column-update="onConfigSettingsColumnUpdate" @column-resize="onColumnResized" @column-resizing="onColumnWidthOverride" @select-all-rows="onAllRowSelect" @select-all-across-pages="emit(BentoDatagridEvent.SELECT_ALL_ACROSS_PAGES)" @select-all-current-results="emit(BentoDatagridEvent.SELECT_ALL_CURRENT_RESULTS)" @deselect-all-current-results="emit(BentoDatagridEvent.DESELECT_ALL_CURRENT_RESULTS)" @deselect-all="onDeselectAll" @sort="onSort" > <template v-for="(_, innerSlot) of slots" #[innerSlot]="scope"> <slot :name="innerSlot" v-bind="scope" /> </template> </data-grid-columns> </template> </div> </div> </div> <div ref="body" data-testid="data-grid-body" class="b-data-grid__body" :class="conditionalGridBodyClasses" @scroll="updateScrollPosition" > <div v-if="loading && !isLazyLoad" class="b-data-grid__loading"> <bento-loading-indicator class="b-data-grid__loading-indicator" large /> <bento-typography el="span" variant="caption">{{ t('gettingYourData') }}</bento-typography> </div> <div v-else role="rowgroup" @keydown="navigationInput"> <template v-if="data?.length === 0 && emptyState"> <div role="row" tabindex="-1"> <div role="gridcell" :aria-colspan="columns.length" tabindex="-1"> <bento-empty-state class="b-data-grid__empty-state" v-bind="limitedEmptyStateProps" /> </div> </div> </template> <data-grid-row v-for="(rowData, rowIndex) in data" v-else :key="rowIndex" :ref="el => registerRowCellRefs(el, rowIndex)" :style="gridRowWidthStyle" :allow-row-clicks="allowRowClicks" :column-sets="columnSets" :condensed="condensed" :dragging-columns="draggingColumns" :dragging-events-for-row-cell="draggingEventsForRowCell" :get-data-item-id="getDataItemId" :has-nested-content="hasNestedContent" :has-nested-rows="hasNestedRows" :has-left-overflow="hasLeftOverflow" :has-right-overflow="hasRightOverflow" :highlighted-rows="highlightedRows" :hovered-row-index="hoveredRowIndex" :hovered-over-column-state="hoveredOverColumnState" :is-data-item-disabled="isDataItemDisabled" :is-row-even="internalZebraRows ? rowIndex % 2 !== 0 : false" :is-row-last-child="rowIndex + 1 === data?.length" :is-measuring-column-widths="isMeasuringColumnWidths" :row-actions-maximum-width="rowActionsMaxWidth" :row-actions="rowActions" :row-data="rowData" :row-index="rowIndex" :row-set-size="data?.length" :show-frozen-columns-separator="showFrozenColumnsSeparator" :should-show-nested-content="shouldShowNestedContent" :should-show-nested-rows="shouldShowNestedRows" :selectable="selectable" :selected-items="selectedItems" @cell-hovered="onCellHover" @cell-focus="emit(BentoDatagridEvent.CELL_FOCUS, $event)" @nested-content-toggled="emit(BentoDatagridEvent.NESTED_CONTENT_TOGGLED, $event)" @row-click="onRowClick" @on-frozen-columns-key-press="updateScrollPositionOnFrozenColumnsNavigation" @select-row="onRowSelect" @select-nested-row="onNestedRowSelect" > <template v-for="(_, innerSlot) of slots" #[innerSlot]="scope"> <slot :name="innerSlot" v-bind="scope" /> </template> </data-grid-row> </div> </div> </div> <data-grid-lazy-load v-if="isLazyLoad" :loading="loading" :is-infinite-scroll="isInfiniteScroll" @update:show-more="emitLazyLoadEvent" /> <div v-if="stickyHorizontalScrollbar" ref="virtualHorizontalScrollbarRef" class="b-data-grid__horizontal-scrollbar" aria-hidden="true" data-testid="sticky-horizontal-scrollbar" :style="horizontalScrollbarConditionalStyles" @scroll="syncVirtualScrollerWithBody" > <div :style="{ width: `${totalWidthOfAllColumns}px`, height: '15px' }"></div> </div> <div v-if="pagination && !isLazyLoad" ref="paginationContainerRef" data-testid="pagination-container" class="b-data-grid__pagination" :class="conditionalPaginationClasses" > <bento-pagination v-bind="pagination" :aria-label="$t('pagination')" :aria-controls="dataGridUid" @navigate="onPaginationNavigate" @items-page="onItemsPerPageChange" > <slot v-if="hasSlot('pagination-slot')" name="pagination-slot"></slot> </bento-pagination> </div> <div v-if="$slots.footer" class="b-data-grid__footer"><slot name="footer"></slot></div> <bento-action-bar v-if="showActionBar" :actions="bulkActions" :item-counter="itemsSelectedCounter" :extra-bottom-spacing="stickyPagination" @close="onDeselectAll" /> </div> </template> <script lang="ts" setup> import { type ComponentPublicInstance, computed, inject, onMounted, type PropType, provide, ref, toRef, toRefs, useSlots, } from 'vue'; // Components import { BentoActionBar } from '@/components/action-bar'; import { BentoPagination } from '@/components/pagination'; import { BentoLoadingIndicator } from '@/components/loading-indicator'; import { BentoTypography } from '@/components/typography'; import { BentoDataGridConfigSettings, DataGridColumns, DataGridLazyLoad, DataGridRow } from './components'; // Composables import { DATAGRID_ROW_ACTIONS_COLUMN_FIELD_NAME, useCalculateColumnWidth, useColumnHovering, useColumnReorderer, useConfigSettings, useKeyboardNavigator, useLazyLoading, useScroller, useSelector, } from './composables'; import { useCalculateStickyElementHeight, useHasSlot } from '@/composables'; // Utils import { generateUid } from '@/core/utils/ts/generate-uid'; import { useI18n } from '@/utils/ts/i18n'; import { printDevelopmentWarning } from '@/utils/ts/print-development-warning'; import { handleClipboard } from '@/components/data-grid/utils/handle-clipboard/handle-clipboard'; // Types import { type BentoColumn, type BentoDatagridData, type BentoDatagridDataItem, type BentoDatagridDataItemId, type BentoDatagridEmptyStateProps, BentoDatagridEvent, type BentoDatagridGetDataItemIdFn, type BentoDatagridIsDataItemDisabledFn, type BentoDatagridPaginationProps, type BentoDatagridShouldShowNestedContentFn, type BentoDatagridShouldShowNestedRowsFn, type BentoDatagridSortByColumn, BentoDatagridStyleState, type BentoDataGridVisibleRowsState, } from './data-grid.types'; import { type BentoTabProvidedTitle } from '../tabs/components/tab.types'; import { type BentoDataGridTemplateDataGrid } from '@/templates/data-grid-template/data-grid-template.types'; import { type BentoLazyLoadType } from '@/types/lazy-load'; import type { BentoButtonActionsList } from '@/components/button'; import { BentoFilterBar, type BentoFilterBarContainerSizeLayout, type BentoFilterBarModel, type BentoFilterBarSearchBarProps, type BentoFilterValueObject, } from '@/components/filter-bar'; import { BentoEmptyState } from '@/components/empty-state'; import type { DatagridColumnsHeaderReferences } from '@/components/data-grid/components/data-grid-columns/data-grid-columns.types'; import type { BentoDataGridRowActionsProp } from '@/components/data-grid/components/data-grid-cell-actions/data-grid-cell-actions.types'; // Keys import { EXTERNAL_TITLE_REGISTER_ENTRY_INJECTION_KEY } from '@/components/tabs/tabs.keys'; import { DATA_GRID_REGISTER_TEMPLATE_INJECTION_KEY } from '@/templates/data-grid-template/data-grid-template.keys'; import { DATA_GRID_SLASHED_ZERO, DATA_GRID_SMALL_CONTAINER, DATA_GRID_TOUCH_DEVICE } from './data-grid.keys'; // Translations import messages from './messages.json'; import { useElementSize } from '@vueuse/core'; type MessageSchema = (typeof messages)['en-US']; const props = defineProps({ /** * When true, will emit a 'rowClick' event when a row is clicked. Will add cursor: pointer to rows. */ allowRowClicks: { type: Boolean, default: false }, /** * When true, column headers will become draggable, allowing users to rearrange columns. */ allowColumnDragAndDrop: { type: Boolean, default: true }, /** * List of actions that will be rendered in the action bar when user selects items. * * Each object in the list should have a `title`, an `event` and, * optionally, an `icon`. * * The component will display 4 actions and the hide the rest inside a menu. * * @see BentoButton for a list of all the other props supported by each button action. */ bulkActions: { type: Array as PropType<BentoButtonActionsList>, default: null, }, /** * Sets the row cells to the condensed padding state. */ condensed: { type: Boolean, default: false, }, /** * Column definitions using 'key' to relate to data object. */ columns: { type: Array as PropType<Array<BentoColumn>>, required: true, }, /** * Enables / hides the display options panel and the column contextual menu functionality. */ columnPanel: { type: Boolean, default: false }, /** * An array of data objects to display. */ data: { type: Array as PropType<BentoDatagridData>, required: true, }, /** * Empty state props that will be passed down to inner empty-state component */ emptyState: { type: Object as PropType<BentoDatagridEmptyStateProps>, default: undefined, }, /** * filter-bar value (i.e. filters) that will be passed down to inner filter-bar component * {@see BentoFilterBarModel} */ filters: { // TODO: Change this to BentoFilterBarProps['value'] when the interface is available type: Array as PropType<BentoFilterBarModel>, default: undefined, }, /** * Defines the layout (multi-line or one-line) to be used in the filter bar for different * container sizes: * - Large (>=700px): Always multi-line * - Medium (>=430px & <700px) * - Small (<430px) * * @default * { * medium: 'multi-line', * small: 'one-line' * } */ filtersContainerSizeLayout: { type: Object as PropType<BentoFilterBarContainerSizeLayout>, default: undefined, }, /** * An array of the values for each filter set in `value`. * {@see BentoFilterValues} */ filterValues: { type: BentoFilterBar.props.filterValues.type, default: () => [], }, /** * An object of the values for each filter in the filter bar where key is a the field name and the value is the BentoFilterBarValue. * * @example * { * 'company': '200', * 'name': 'some text' * }, */ filterValuesObject: { type: Object as PropType<BentoFilterValueObject>, default: () => undefined, }, // TODO: Move this prop as a part of "config" BentoFilterBarModel = Array<BentoFilterModel | BentoFilterSearchModel> in Bento 2.0. // It was not done now because it's a breaking change for the types. /** * Filter's search bar properties with which the placeholder, hint and other * "bento-search-bar" properties can be set. @see {@link BentoFilterBarSearchBarProps} */ filterSearchConfig: { type: Object as PropType<BentoFilterBarSearchBarProps>, default: undefined, }, /** * Filter bar search term useful to filter all the data by text */ filterSearchTerm: { type: BentoFilterBar.props.searchTerm.type, default: undefined, }, /** * Allows the grid to be rendered inside a confined space. * This makes the data-grid expand to fill the height of its parent container, with the grid body becoming scrollable. */ fitContent: { type: Boolean, default: false }, /** * A customisable method to retrieve a unique identifier for a data item in the array of data supplied. * This is used selecting and performance reasons and must exist in each data item. */ getDataItemId: { type: Function as PropType<BentoDatagridGetDataItemIdFn>, default: (item: BentoDatagridDataItem) => item.id, }, /** * Row actions propery that generates which button to show in row actions area */ rowActions: { type: [Function, Array] as PropType<BentoDataGridRowActionsProp>, default: undefined, }, /** * If resize is enabled */ hasResizableColumns: { type: Boolean, default: true, }, /** * Enables the nested content slot functionality. */ hasNestedContent: { type: Boolean, default: false, }, /** * Enables the nested content slot functionality. */ hasNestedRows: { type: Boolean, default: false, }, /** * Highlighted items */ highlightedRows: { type: Array as PropType<Array<BentoDatagridDataItemId>>, default: () => [], }, /** * A customisable method to determine if a row is not selectable. * The default is that every row is selectable. */ isDataItemDisabled: { type: Function as PropType<BentoDatagridIsDataItemDisabledFn>, default: () => false, }, /** * The type of `Lazy Load`. * Type "automatic" will enable infinite scrolling * Type "button" will enable lazy loading with "Show more" button * @values automatic, button, none */ lazyLoadType: { type: String as PropType<BentoLazyLoadType>, default: 'none', validator: (value: BentoLazyLoadType) => ['automatic', 'button', 'none'].includes(value), }, /** * If the loading indicator should be shown. */ loading: { type: Boolean, default: false, }, /** * Enables / Disables the table's surrounding border. */ outline: { type: Boolean, default: false, }, /** * Renders the data grid with no top spacing, to render inside containers that already have spacing */ noSpacingTop: { type: Boolean, default: false, }, /** * Pagination props that will be passed down to inner pagination component */ pagination: { type: Object as PropType<BentoDatagridPaginationProps>, default: undefined, }, /** * Adds an extra option when selecting all items that allows user to select items in other pages too. * Needs pagination.totalCount to be set. */ selectableAcrossPages: { type: Boolean, default: false, }, /** * Renders a font numeric style with slashed zeros instead of normal ones. */ slashedZero: { type: Boolean, default: false }, /** * Sticks the horizontal scrollbar to the bottom of the screen if the data-grid is larger than the screen height. */ stickyHorizontalScrollbar: { type: Boolean, default: false, }, /** * Make pagination stick to the bottom of the page when the data-grid is bigger than the screen. * Because styles have to be applied to pagination container element, this has to be a data-grid prop (rather than a pagination prop). */ stickyPagination: { type: Boolean, default: false, }, /** * Make data-grid filters stick to the top of the page along with the data-grid headers. * Defaults to true. */ stickyFilters: { type: Boolean, default: true, }, /** * Enables / Disables rows to be selected via a checkbox. On select the "select" event will be emitted. */ selectable: { type: Boolean, default: false, }, /** * Selected items */ selection: { type: Array as PropType<Array<BentoDatagridDataItemId>>, default: () => [], }, /** * Enables / Disables the nested-content slot */ shouldShowNestedContent: { type: Function as PropType<BentoDatagridShouldShowNestedContentFn>, default: (_item: BentoDatagridDataItem) => true, }, /** * Returns the nested rows for a given BentoDatagridDataItem. */ shouldShowNestedRows: { type: Function as PropType<BentoDatagridShouldShowNestedRowsFn>, default: (_item: BentoDatagridDataItem) => true, }, /** * Sortby handles proper rendering of sort icon in header. Sorting currently is only supported server side. * It does make sure the currently sorted column has a more visible sort icon, and the sort icon points in the right direction. */ sortBy: { type: Array as PropType<Array<BentoDatagridSortByColumn>>, // NOTE: is an array to allow multi-sorting in the future default: () => [], }, /** * Title of the data grid to be rendered as tab title when a data grid is passed in a tab layout. */ tabTitle: { type: String, default: null, }, /** * Truncates all data grid column headers */ truncateColumnHeaders: { type: Boolean, default: false, }, /** * Toggles the alternating background color of rows. */ zebraRows: { type: Boolean, default: false, }, }); const slots = useSlots(); const hasSlot = useHasSlot(slots); /* == Variables and references == */ // Create refs for elements const body = ref<HTMLDivElement>(null); const columnsRef = ref<HTMLDivElement>(); const columnHeaderRefs = ref<DatagridColumnsHeaderReferences>({}); const dataGridUid = ref<string>(generateUid('data-grid')); const gridcellRefs = ref<{ [key: string]: Array<HTMLDivElement> }>(null); const filtersRef = ref<HTMLDivElement>(null); const filterBarRef = ref<InstanceType<typeof BentoFilterBar>>(); const paginationContainerRef = ref<HTMLDivElement>(null); const verticalScrollPositionRef = ref(null); const virtualHorizontalScrollbarRef = ref<HTMLDivElement>(null); const { allowColumnDragAndDrop, columns, columnPanel, data, lazyLoadType, loading, hasNestedContent, hasNestedRows, selectable, selection, selectableAcrossPages, stickyPagination, outline, pagination, zebraRows, } = toRefs(props); const hoveredRowIndex = ref(-1); const { t } = useI18n<{ message: MessageSchema }>({ messages }); const frozenColumnsLength = computed(() => columnDefs.value.filter(column => column.frozen).length); /* == Responsive behaviour == */ // Identifies if the data-grid is in a container smaller than 550px const MOBILE_BREAKPOINT = 550; const dataGridRef = ref(); const { width } = useElementSize(dataGridRef); const isSmallContainer = computed(() => width.value <= MOBILE_BREAKPOINT); provide(DATA_GRID_SMALL_CONTAINER, isSmallContainer); /* == Touch device behaviour == */ // Identifies if the data-grid is rendered on a touch device const isTouchDevice = ref(false); onMounted(() => { if (typeof window !== 'undefined') { isTouchDevice.value = 'ontouchstart' in window || navigator?.maxTouchPoints > 0; } }); provide(DATA_GRID_TOUCH_DEVICE, isTouchDevice); /* == Data-grid template == */ const registerDatagridInDataGridTemplate: BentoDataGridTemplateDataGrid = inject( DATA_GRID_REGISTER_TEMPLATE_INJECTION_KEY, () => ({ additionalContentSlotRef: ref(null), isDataGridTemplate: false, stickyAditionalContent: ref(false), stickyTabs: ref(false), tabsRef: ref(null), }) ); const { additionalContentSlotRef, isDataGridTemplate, stickyTabs, tabsRef, stickyAdditionalContent } = registerDatagridInDataGridTemplate(); /* == Composable logic == */ // Column width calculation const { columnDefs, onColumnWidthOverride, gridRowWidthStyle, isMeasuringColumnWidths, isTotalWidthOfColumnsTakingUpAllAvailableGridWidth, columns: localColumnsState, totalWidthOfAllColumns, rowActionsMaxWidth, } = useCalculateColumnWidth(columns, data, body, columnHeaderRefs, gridcellRefs, { hasColumnPanel: columnPanel, hasSelectableColumn: selectable, hasSelectableAcrossPages: selectableAcrossPages, hasNestedContent, hasNestedRows, allowColumnDragAndDrop, isDataGridTemplate: isDataGridTemplate, }); /* == Emitting logic == */ const definedEmitsFunction = defineEmits([ BentoDatagridEvent.COLUMNS, BentoDatagridEvent.COLUMN_RESIZE, BentoDatagridEvent.ITEMS_PAGE, BentoDatagridEvent.FILTER_SEARCH_INPUT, BentoDatagridEvent.FILTER_SEARCH_CLEAR, BentoDatagridEvent.NAVIGATE, BentoDatagridEvent.NESTED_CONTENT_TOGGLED, BentoDatagridEvent.UPDATE_COLUMNS, BentoDatagridEvent.UPDATE_CONDENSED, BentoDatagridEvent.UPDATE_FILTER_VALUES, BentoDatagridEvent.UPDATE_FILTER_VALUES_OBJECT, BentoDatagridEvent.UPDATE_FILTERS, BentoDatagridEvent.UPDATE_FILTER_SEARCH_TERM, BentoDatagridEvent.UPDATE_SHOW_MORE, BentoDatagridEvent.UPDATE_PAGINATION, BentoDatagridEvent.UPDATE_SELECTION, BentoDatagridEvent.UPDATE_SORT_BY, BentoDatagridEvent.ROW_CLICK, BentoDatagridEvent.SELECT, BentoDatagridEvent.SELECT_ALL_ACROSS_PAGES, BentoDatagridEvent.SELECT_ALL_CURRENT_RESULTS, BentoDatagridEvent.DESELECT_ALL_CURRENT_RESULTS, BentoDatagridEvent.SORT, BentoDatagridEvent.CELL_FOCUS, ]); /** * Hooks into the emit function and updates the local columns state if we're updating columns. * This is so if users use a computed ref with the columns property the ordering / toggling visibility will still function correctly. */ const emit: typeof definedEmitsFunction = (event, ...args) => { // If we updating columns update the local columns state if (event === BentoDatagridEvent.COLUMNS || event === BentoDatagridEvent.UPDATE_COLUMNS) { [localColumnsState.value] = args; } definedEmitsFunction(event, ...args); }; // Calculate dynamic top position of sticky filters const stickyFiltersHeight = useCalculateStickyElementHeight( tabsRef, stickyTabs, stickyAdditionalContent, additionalContentSlotRef ); const computedStickyFiltersTopPosition = computed(() => props.stickyFilters ? `${stickyFiltersHeight.value}px` : null ); // Calculate dynamic top position of column headers const columnHeadersHeight = useCalculateStickyElementHeight(filtersRef, toRef(props, 'stickyFilters')); const computedColumnHeadersTopPosition = computed( () => `${columnHeadersHeight.value + stickyFiltersHeight.value}px` ); // Config Settings (condensed, visbility and freezing settings) const { sortedColumns, columnSets, isOnlyOneVisibleColumn, internalZebraRows, onConfigSettingsColumnsUpdate, onConfigSettingsColumnUpdate, onConfigSettingsCondensedUpdate, onConfigSettingsZebraRowsUpdate, } = useConfigSettings(columnDefs, zebraRows, emit); // Column hovering const { hoveredOverColumn, hoveredOverColumnState } = useColumnHovering( columnSets, selectable, outline, isTotalWidthOfColumnsTakingUpAllAvailableGridWidth ); // Re-ordering column (dragging) const { draggingColumns, draggingEventsForColumns, draggingEventsForRowCell, onMoveColumnByOffset } = useColumnReorderer(sortedColumns, allowColumnDragAndDrop, emit); // Selecting const selectableAcrossPagesComputed = computed( () => props.data?.length > 0 && !!props.selectableAcrossPages && !!props.pagination && props.pagination.totalCount > 0 ); const { onAllRowSelect, onRowSelect, onNestedRowSelect, onDeselectAll, allItemsSelectedState, selectedItems, selectionState, } = useSelector( selection, emit, data, props.getDataItemId, props.isDataItemDisabled, selectableAcrossPagesComputed, pagination ); const visibleRowsState = computed<BentoDataGridVisibleRowsState>(() => ({ filtered: filterBarRef.value?.isClearAllVisible || filterBarRef.value?.isResetAllVisible || !!props.filterSearchTerm, allRowsVisible: pagination.value?.size >= pagination.value?.totalCount, })); // Keyboard navigation const { navigationInput } = useKeyboardNavigator( gridcellRefs, columnHeaderRefs, sortedColumns, selectable, computed(() => props.rowActions !== undefined), columnPanel ); // Lazy loading const { isLazyLoad, isInfiniteScroll, verticalScrollPosition, emitLazyLoadEvent } = useLazyLoading( lazyLoadType, verticalScrollPositionRef, body, loading, emit ); // Scrolling const { updateScrollPosition, updateScrollPositionOnFrozenColumnsNavigation, syncVirtualScrollerWithBody, hasLeftOverflow, hasRightOverflow, horizontalScrollbarConditionalStyles, } = useScroller( body, columnsRef, verticalScrollPosition, virtualHorizontalScrollbarRef, paginationContainerRef, totalWidthOfAllColumns, { isLazyLoad, pagination, stickyPagination, isDataGridTemplate, } ); /* == Empty State == */ const onEmptyStateClearResetButtonClick = () => { if (filterBarRef.value.isClearAllVisible) { filterBarRef.value.clearAllFilters(); } else if (filterBarRef.value.isResetAllVisible) { filterBarRef.value.resetAllFilters(); } }; const limitedEmptyStateProps = computed<BentoDatagridEmptyStateProps>(() => ({ ...props.emptyState, variant: props.emptyState?.variant === 'embedded' || props.emptyState?.variant === 'basic' ? props.emptyState.variant : 'basic', })); /* == Styles == */ // Conditional grid classes const conditionalGridClasses = computed(() => ({ [`b-data-grid--${BentoDatagridStyleState.OUTLINE}`]: props.outline, [`b-data-grid--${BentoDatagridStyleState.SCROLL}`]: props.fitContent || isLazyLoad.value, [`b-data-grid--${BentoDatagridStyleState.STICKY_FILTERS}`]: props.filters && props.stickyFilters, [`b-data-grid--${BentoDatagridStyleState.OVERFLOW_SHADOW_LEFT}`]: hasLeftOverflow.value && !frozenColumnsLength.value, [`b-data-grid--${BentoDatagridStyleState.OVERFLOW_SHADOW_RIGHT}`]: hasRightOverflow.value && props.rowActions === undefined, [`b-data-grid--${BentoDatagridStyleState.NO_SPACING_TOP}`]: props.noSpacingTop, [`b-data-grid--${BentoDatagridStyleState.FIT_CONTENT}`]: props.fitContent, [`b-data-grid--${BentoDatagridStyleState.ZEBRA_ROWS}`]: internalZebraRows.value, 'b-data-grid--lazy-load': isLazyLoad.value, })); // Conditional grid body classes const conditionalGridBodyClasses = computed(() => ({ 'b-data-grid__body--sticky-horizontal-scrollbar': props.stickyHorizontalScrollbar, })); const computedColumnPanelAlignment = computed(() => columnSets.value.findIndex(el => el.columnSet.length)); const showFrozenColumnsSeparator = computed(() => { const isSelectionOnlyFrozenColumn = (props.selectable || props.hasNestedContent || props.hasNestedRows) && frozenColumnsLength.value === 1; return !isSelectionOnlyFrozenColumn || hasLeftOverflow.value; }); // Conditional filter classes const conditionalFilterClasses = computed(() => ({ 'b-data-grid__filters--sticky': !!props.stickyFilters || isDataGridTemplate, })); // Conditional pagination classes const conditionalPaginationClasses = computed(() => ({ 'b-data-grid__pagination--padding-left-unset': props.columns?.[0]?.padding?.left === false, 'b-data-grid__pagination--padding-right-unset': props.columns?.[props.columns.length - 1]?.padding?.right === false, 'b-data-grid__pagination--sticky': !!props.stickyPagination || isDataGridTemplate, })); /* == Ref registration methods == */ const registerColumnHeaderRefs = (el: Element | ComponentPublicInstance) => { // We always know this will be used on a DataGridColumns component so we can cast it const datagridColumnsComponent = el as InstanceType<typeof DataGridColumns>; if (el !== null && datagridColumnsComponent?.columnHeaderRefs) { Object.entries(datagridColumnsComponent?.columnHeaderRefs).forEach(([fieldName, colHeaderEl]) => { columnHeaderRefs.value[fieldName] = colHeaderEl as HTMLDivElement; }); } }; const registerRowCellRefs = (el: Element | ComponentPublicInstance, rowIndex: number) => { // We always know this will be used on a DataGridCell component so we can cast it const gridRowComponent = el as InstanceType<typeof DataGridRow>; if (el !== null) { if (gridRowComponent?.gridcellRefs) { const areRowActions = props.rowActions !== undefined && gridRowComponent?.rowActionsRef; Object.entries(gridRowComponent?.gridcellRefs).map(([field, cell]) => { if (gridcellRefs.value?.[field] === undefined) { gridcellRefs.value = { ...gridcellRefs.value, [field]: [] }; } gridcellRefs.value[field][rowIndex] = cell as HTMLDivElement; }); // This calculates the maxium width of the row actions column so we can add the same padding to the columns keep everything aligned if (areRowActions) { if (gridcellRefs.value?.[DATAGRID_ROW_ACTIONS_COLUMN_FIELD_NAME] === undefined) { gridcellRefs.value = { ...gridcellRefs.value, [DATAGRID_ROW_ACTIONS_COLUMN_FIELD_NAME]: [] }; } gridcellRefs.value[DATAGRID_ROW_ACTIONS_COLUMN_FIELD_NAME][rowIndex] = gridRowComponent .rowActionsRef?.[0] as HTMLDivElement; rowActionsMaxWidth.value = Math.max( rowActionsMaxWidth.value, gridRowComponent.rowActionsRef?.[0]?.offsetWidth ?? 0 ); } } } }; /* == Row methods == */ // Row selecting const onRowClick = (row: BentoDatagridDataItem, event: MouseEvent) => { if (props.allowRowClicks) { emit(BentoDatagridEvent.ROW_CLICK, row, event); } }; // Programmatically set the hover state on a row const onCellHover = (rowIndex: number) => { hoveredRowIndex.value = rowIndex; }; /* == a11y == */ const ariaGridRole = computed(() => (props.hasNestedContent || props.hasNestedRows ? 'treegrid' : 'grid')); /* == Action bar == */ const itemsSelectedCounter = computed(() => selectedItems.value?.length ?? 0); const showActionBar = computed(() => props.bulkActions && itemsSelectedCounter.value > 0); /* == Emits & Events == */ // Resizing const onColumnResized = (resizeEvent: { field: string; width: number }) => emit(BentoDatagridEvent.COLUMN_RESIZE, resizeEvent); // Sorting const onSort = (updatedSortBy: Array<BentoDatagridSortByColumn>) => { emit(BentoDatagridEvent.UPDATE_SORT_BY, updatedSortBy); emit(BentoDatagridEvent.SORT, updatedSortBy); }; // Pagination const onPaginationNavigate = (page: number, size: number) => { emit(BentoDatagridEvent.NAVIGATE, page); emit(BentoDatagridEvent.UPDATE_PAGINATION, { ...props.pagination, size, page, }); }; const onItemsPerPageChange = (itemsPerPage: number) => { emit(BentoDatagridEvent.ITEMS_PAGE, itemsPerPage); }; // Filter bar const onSearchTermCleared = () => { emit(BentoDatagridEvent.FILTER_SEARCH_CLEAR); }; const onSearchTermInput = newSearchTerm => { emit(BentoDatagridEvent.UPDATE_FILTER_SEARCH_TERM, newSearchTerm); emit(BentoDatagridEvent.FILTER_SEARCH_INPUT, newSearchTerm); }; /* == Warnings == */ const hasMandatoryColumn = computed(() => columns.value.some(column => column.mandatory)); if (!hasMandatoryColumn.value) { printDevelopmentWarning( 'WARNING: At least one BentoDataGrid column should be set as mandatory to ensure data is always displayed correctly.' ); } /* == Provide/inject == */ const setTitle: BentoTabProvidedTitle = inject(EXTERNAL_TITLE_REGISTER_ENTRY_INJECTION_KEY, () => null); setTitle(props.tabTitle); provide(DATA_GRID_SLASHED_ZERO, toRef(props, 'slashedZero')); /* == Expose methods == */ defineExpose({ onEmptyStateClearResetButtonClick }); </script> <script lang="ts"> /** * A component to display data in columns and row format with a whole array of configuration for visualization and interaction. * * @example * import { BentoDataGrid } from '@adyen/bento-vue2'; * * export default { * components: { BentoDataGrid }, * template: ` * <bento-data-grid * :data="[ * { id: '123', name: 'John', company: 'TestCompany', 'payment': 200 } * { id: '321', name: 'Jane', company: 'Adyen', 'payment': 300 } * ]" * :columns="[ * { field: 'name', label: 'Name', minWidth: 300, mandatory: true }, * { field: 'company', label: 'Reference' }, * { field: 'payment', label: 'Payment', width: 200, minWidth: 150 }, * ]" * /> * ` * } */ export default { i18n: { messages }, name: 'bento-data-grid', }; </script> <style lang="scss" scoped src="./data-grid.scss" />
|
|
1
|
+
<template> <div ref="dataGridRef" class="b-data-grid" data-testid="data-grid" :class="conditionalGridClasses" aria-live="polite" :aria-busy="loading" @copy="handleClipboard" > <div v-if="filters" ref="filtersRef" class="b-data-grid__filters" :class="conditionalFilterClasses" :style="{ top: computedStickyFiltersTopPosition }" :aria-controls="dataGridUid" :aria-label="t('filterBar')" role="region" > <bento-filter-bar v-if="filters" ref="filterBarRef" :config="filters" :container-size-layout="filtersContainerSizeLayout" :filter-values="filterValues" :filter-values-object="filterValuesObject" :search="filterSearchConfig" :search-term="filterSearchTerm" @search-input="onSearchTermInput" @search-clear="onSearchTermCleared" @input="filters => $emit(BentoDatagridEvent.UPDATE_FILTERS, filters)" @update:filter-values="filterValues => $emit(BentoDatagridEvent.UPDATE_FILTER_VALUES, filterValues)" @update:filter-values-object=" filterValueObject => $emit(BentoDatagridEvent.UPDATE_FILTER_VALUES_OBJECT, filterValueObject) " /> </div> <div v-if="$slots.header" class="b-data-grid__header"> <slot name="header"></slot> </div> <div :id="dataGridUid" :role="ariaGridRole" :aria-rowcount="data.length" :aria-colcount="columns.length" :aria-label="emptyState ? t('noResults') : null" class="b-data-grid__wrapper" :style="{ top: computedColumnHeadersTopPosition }" > <div class="b-data-grid__column-headers" :style="{ top: computedColumnHeadersTopPosition }"> <div ref="columnsRef" role="rowgroup" class="b-data-grid__columns-row" @scroll="updateScrollPosition" @keydown="navigationInput" > <div v-if="columnPanel" role="row"> <bento-data-grid-config-settings class="b-data-grid__config-settings" role="gridcell" :columns="localColumnsState" :condensed="condensed" :zebra-rows="internalZebraRows" @update:condensed="onConfigSettingsCondensedUpdate" @update:zebra-rows="onConfigSettingsZebraRowsUpdate" @update:columns="onConfigSettingsColumnsUpdate" /> </div> <div :style="gridRowWidthStyle" class="b-data-grid__columns" role="row"> <template v-for="({ columnSet, id, isFrozenColumns }, index) of columnSets"> <data-grid-columns v-if="columnSet?.length" :key="`data-grid-columns-${id}`" :ref="el => registerColumnHeaderRefs(el)" :columns="columnSet" :has-resizable-columns="hasResizableColumns" :sort-by="sortBy" :all-items-selected-state="allItemsSelectedState" :is-frozen-columns="isFrozenColumns" :show-frozen-columns-separator="showFrozenColumnsSeparator" :dragging-columns="draggingColumns" :has-draggable-columns="allowColumnDragAndDrop" :has-column-panel="columnPanel" :has-left-overflow="hasLeftOverflow" :has-first-column-panel-right-aligned="index === computedColumnPanelAlignment" :hovered-over-column-state="hoveredOverColumnState" :is-measuring-column-widths="isMeasuringColumnWidths" :is-only-one-visible-column="isOnlyOneVisibleColumn" :selectable="data?.length > 0 && selectable" :selectable-across-pages="selectableAcrossPagesComputed" :selected-items="selectedItems" :visible-rows-state="visibleRowsState" :selection-state="selectionState" :truncate-column-headers="truncateColumnHeaders" :row-actions-maximum-width="rowActionsMaxWidth" v-on="draggingEventsForColumns" @column-hover="hoveredOverColumn = $event" @column-move="onMoveColumnByOffset" @column-update="onConfigSettingsColumnUpdate" @column-resize="onColumnResized" @column-resizing="onColumnWidthOverride" @select-all-rows="onAllRowSelect" @select-all-across-pages="emit(BentoDatagridEvent.SELECT_ALL_ACROSS_PAGES)" @select-all-current-results="emit(BentoDatagridEvent.SELECT_ALL_CURRENT_RESULTS)" @deselect-all-current-results="emit(BentoDatagridEvent.DESELECT_ALL_CURRENT_RESULTS)" @deselect-all="onDeselectAll" @sort="onSort" > <template v-for="(_, innerSlot) of slots" #[innerSlot]="scope"> <slot :name="innerSlot" v-bind="scope" /> </template> </data-grid-columns> </template> </div> </div> </div> <div ref="body" data-testid="data-grid-body" class="b-data-grid__body" :class="conditionalGridBodyClasses" @scroll="updateScrollPosition" > <div v-if="loading && !isLazyLoad" class="b-data-grid__loading"> <bento-loading-indicator class="b-data-grid__loading-indicator" large /> <bento-typography el="span" variant="caption">{{ t('gettingYourData') }}</bento-typography> </div> <div v-else role="rowgroup" @keydown="navigationInput"> <template v-if="data?.length === 0 && emptyState"> <div role="row" tabindex="-1"> <div role="gridcell" :aria-colspan="columns.length" tabindex="-1"> <bento-empty-state class="b-data-grid__empty-state" v-bind="limitedEmptyStateProps" /> </div> </div> </template> <data-grid-row v-for="(rowData, rowIndex) in data" v-else :key="rowIndex" :ref="el => registerRowCellRefs(el, rowIndex)" :style="gridRowWidthStyle" :allow-row-clicks="allowRowClicks" :column-sets="columnSets" :condensed="condensed" :dragging-columns="draggingColumns" :dragging-events-for-row-cell="draggingEventsForRowCell" :get-data-item-id="getDataItemId" :has-nested-content="hasNestedContent" :has-nested-rows="hasNestedRows" :has-left-overflow="hasLeftOverflow" :has-right-overflow="hasRightOverflow" :highlighted-rows="highlightedRows" :hovered-row-index="hoveredRowIndex" :hovered-over-column-state="hoveredOverColumnState" :is-data-item-disabled="isDataItemDisabled" :is-row-even="internalZebraRows ? rowIndex % 2 !== 0 : false" :is-row-last-child="rowIndex + 1 === data?.length" :is-measuring-column-widths="isMeasuringColumnWidths" :row-actions-maximum-width="rowActionsMaxWidth" :row-actions="rowActions" :row-data="rowData" :row-index="rowIndex" :row-set-size="data?.length" :show-frozen-columns-separator="showFrozenColumnsSeparator" :should-show-nested-content="shouldShowNestedContent" :should-show-nested-rows="shouldShowNestedRows" :selectable="selectable" :selected-items="selectedItems" @cell-hovered="onCellHover" @cell-focus="emit(BentoDatagridEvent.CELL_FOCUS, $event)" @nested-content-toggled="emit(BentoDatagridEvent.NESTED_CONTENT_TOGGLED, $event)" @row-click="onRowClick" @on-frozen-columns-key-press="updateScrollPositionOnFrozenColumnsNavigation" @select-row="onRowSelect" @select-nested-row="onNestedRowSelect" > <template v-for="(_, innerSlot) of slots" #[innerSlot]="scope"> <slot :name="innerSlot" v-bind="scope" /> </template> </data-grid-row> </div> </div> </div> <data-grid-lazy-load v-if="isLazyLoad" :loading="loading" :is-infinite-scroll="isInfiniteScroll" @update:show-more="emitLazyLoadEvent" /> <div v-if="stickyHorizontalScrollbar" ref="virtualHorizontalScrollbarRef" class="b-data-grid__horizontal-scrollbar" aria-hidden="true" data-testid="sticky-horizontal-scrollbar" :style="horizontalScrollbarConditionalStyles" @scroll="syncVirtualScrollerWithBody" > <div :style="{ width: `${totalWidthOfAllColumns}px`, height: '15px' }"></div> </div> <div v-if="pagination && !isLazyLoad" ref="paginationContainerRef" data-testid="pagination-container" class="b-data-grid__pagination" :class="conditionalPaginationClasses" > <bento-pagination v-bind="pagination" :aria-label="$t('pagination')" :aria-controls="dataGridUid" @navigate="onPaginationNavigate" @items-page="onItemsPerPageChange" > <slot v-if="hasSlot('pagination-slot')" name="pagination-slot"></slot> </bento-pagination> </div> <div v-if="$slots.footer" class="b-data-grid__footer"><slot name="footer"></slot></div> <bento-action-bar v-if="showActionBar" :actions="bulkActions" :item-counter="itemsSelectedCounter" :extra-bottom-spacing="stickyPagination" @close="onDeselectAll" /> </div> </template> <script lang="ts" setup> import { type ComponentPublicInstance, computed, inject, onMounted, type PropType, provide, ref, toRef, toRefs, useSlots, } from 'vue'; // Components import { BentoActionBar } from '@/components/action-bar'; import { BentoPagination } from '@/components/pagination'; import { BentoLoadingIndicator } from '@/components/loading-indicator'; import { BentoTypography } from '@/components/typography'; import { BentoDataGridConfigSettings, DataGridColumns, DataGridLazyLoad, DataGridRow } from './components'; // Composables import { DATAGRID_ROW_ACTIONS_COLUMN_FIELD_NAME, useCalculateColumnWidth, useColumnHovering, useColumnReorderer, useConfigSettings, useKeyboardNavigator, useLazyLoading, useScroller, useSelector, } from './composables'; import { useCalculateStickyElementHeight, useHasSlot } from '@/composables'; // Utils import { generateUid } from '@/core/utils/ts/generate-uid'; import { useI18n } from '@/utils/ts/i18n'; import { printDevelopmentWarning } from '@/utils/ts/print-development-warning'; import { handleClipboard } from '@/components/data-grid/utils/handle-clipboard/handle-clipboard'; // Types import { type BentoColumn, type BentoDatagridData, type BentoDatagridDataItem, type BentoDatagridDataItemId, type BentoDatagridEmptyStateProps, BentoDatagridEvent, type BentoDatagridGetDataItemIdFn, type BentoDatagridIsDataItemDisabledFn, type BentoDatagridPaginationProps, type BentoDatagridShouldShowNestedContentFn, type BentoDatagridShouldShowNestedRowsFn, type BentoDatagridSortByColumn, BentoDatagridStyleState, type BentoDataGridVisibleRowsState, } from './data-grid.types'; import { type BentoTabProvidedTitle } from '../tabs/components/tab.types'; import { type BentoDataGridTemplateDataGrid } from '@/templates/data-grid-template/data-grid-template.types'; import { type BentoLazyLoadType } from '@/types/lazy-load'; import type { BentoButtonActionsList } from '@/components/button'; import { BentoFilterBar, type BentoFilterBarContainerSizeLayout, type BentoFilterBarModel, type BentoFilterBarSearchBarProps, type BentoFilterValueObject, } from '@/components/filter-bar'; import { BentoEmptyState } from '@/components/empty-state'; import type { DatagridColumnsHeaderReferences } from '@/components/data-grid/components/data-grid-columns/data-grid-columns.types'; import type { BentoDataGridRowActionsProp } from '@/components/data-grid/components/data-grid-cell-actions/data-grid-cell-actions.types'; // Keys import { EXTERNAL_TITLE_REGISTER_ENTRY_INJECTION_KEY } from '@/components/tabs/tabs.keys'; import { DATA_GRID_REGISTER_TEMPLATE_INJECTION_KEY } from '@/templates/data-grid-template/data-grid-template.keys'; import { DATA_GRID_SLASHED_ZERO, DATA_GRID_SMALL_CONTAINER, DATA_GRID_TOUCH_DEVICE } from './data-grid.keys'; // Translations import messages from './messages.json'; import { useElementSize } from '@vueuse/core'; type MessageSchema = (typeof messages)['en-US']; const props = defineProps({ /** * When true, will emit a 'rowClick' event when a row is clicked. Will add cursor: pointer to rows. */ allowRowClicks: { type: Boolean, default: false }, /** * When true, column headers will become draggable, allowing users to rearrange columns. */ allowColumnDragAndDrop: { type: Boolean, default: true }, /** * List of actions that will be rendered in the action bar when user selects items. * * Each object in the list should have a `title`, an `event` and, * optionally, an `icon`. * * The component will display 4 actions and the hide the rest inside a menu. * * @see BentoButton for a list of all the other props supported by each button action. */ bulkActions: { type: Array as PropType<BentoButtonActionsList>, default: null, }, /** * Sets the row cells to the condensed padding state. */ condensed: { type: Boolean, default: false, }, /** * Column definitions using 'key' to relate to data object. */ columns: { type: Array as PropType<Array<BentoColumn>>, required: true, }, /** * Enables / hides the display options panel and the column contextual menu functionality. */ columnPanel: { type: Boolean, default: false }, /** * An array of data objects to display. */ data: { type: Array as PropType<BentoDatagridData>, required: true, }, /** * Empty state props that will be passed down to inner empty-state component */ emptyState: { type: Object as PropType<BentoDatagridEmptyStateProps>, default: undefined, }, /** * filter-bar value (i.e. filters) that will be passed down to inner filter-bar component * {@see BentoFilterBarModel} */ filters: { // TODO: Change this to BentoFilterBarProps['value'] when the interface is available type: Array as PropType<BentoFilterBarModel>, default: undefined, }, /** * Defines the layout (multi-line or one-line) to be used in the filter bar for different * container sizes: * - Large (>=700px): Always multi-line * - Medium (>=430px & <700px) * - Small (<430px) * * @default * { * medium: 'multi-line', * small: 'one-line' * } */ filtersContainerSizeLayout: { type: Object as PropType<BentoFilterBarContainerSizeLayout>, default: undefined, }, /** * An array of the values for each filter set in `value`. * {@see BentoFilterValues} */ filterValues: { type: BentoFilterBar.props.filterValues.type, default: () => [], }, /** * An object of the values for each filter in the filter bar where key is a the field name and the value is the BentoFilterBarValue. * * @example * { * 'company': '200', * 'name': 'some text' * }, */ filterValuesObject: { type: Object as PropType<BentoFilterValueObject>, default: () => undefined, }, // TODO: Move this prop as a part of "config" BentoFilterBarModel = Array<BentoFilterModel | BentoFilterSearchModel> in Bento 2.0. // It was not done now because it's a breaking change for the types. /** * Filter's search bar properties with which the placeholder, hint and other * "bento-search-bar" properties can be set. @see {@link BentoFilterBarSearchBarProps} */ filterSearchConfig: { type: Object as PropType<BentoFilterBarSearchBarProps>, default: undefined, }, /** * Filter bar search term useful to filter all the data by text */ filterSearchTerm: { type: BentoFilterBar.props.searchTerm.type, default: undefined, }, /** * Allows the grid to be rendered inside a confined space. * This makes the data-grid expand to fill the height of its parent container, with the grid body becoming scrollable. */ fitContent: { type: Boolean, default: false }, /** * A customisable method to retrieve a unique identifier for a data item in the array of data supplied. * This is used selecting and performance reasons and must exist in each data item. */ getDataItemId: { type: Function as PropType<BentoDatagridGetDataItemIdFn>, default: (item: BentoDatagridDataItem) => item.id, }, /** * Row actions propery that generates which button to show in row actions area */ rowActions: { type: [Function, Array] as PropType<BentoDataGridRowActionsProp>, default: undefined, }, /** * If resize is enabled */ hasResizableColumns: { type: Boolean, default: true, }, /** * Enables the nested content slot functionality. */ hasNestedContent: { type: Boolean, default: false, }, /** * Enables the nested content slot functionality. */ hasNestedRows: { type: Boolean, default: false, }, /** * Highlighted items */ highlightedRows: { type: Array as PropType<Array<BentoDatagridDataItemId>>, default: () => [], }, /** * A customisable method to determine if a row is not selectable. * The default is that every row is selectable. */ isDataItemDisabled: { type: Function as PropType<BentoDatagridIsDataItemDisabledFn>, default: () => false, }, /** * The type of `Lazy Load`. * Type "automatic" will enable infinite scrolling * Type "button" will enable lazy loading with "Show more" button * @values automatic, button, none */ lazyLoadType: { type: String as PropType<BentoLazyLoadType>, default: 'none', validator: (value: BentoLazyLoadType) => ['automatic', 'button', 'none'].includes(value), }, /** * If the loading indicator should be shown. */ loading: { type: Boolean, default: false, }, /** * Enables / Disables the table's surrounding border. */ outline: { type: Boolean, default: false, }, /** * Renders the data grid with no top spacing, to render inside containers that already have spacing */ noSpacingTop: { type: Boolean, default: false, }, /** * Pagination props that will be passed down to inner pagination component */ pagination: { type: Object as PropType<BentoDatagridPaginationProps>, default: undefined, }, /** * Adds an extra option when selecting all items that allows user to select items in other pages too. * Needs pagination.totalCount to be set. */ selectableAcrossPages: { type: Boolean, default: false, }, /** * Renders a font numeric style with slashed zeros instead of normal ones. */ slashedZero: { type: Boolean, default: false }, /** * Sticks the horizontal scrollbar to the bottom of the screen if the data-grid is larger than the screen height. */ stickyHorizontalScrollbar: { type: Boolean, default: false, }, /** * Make pagination stick to the bottom of the page when the data-grid is bigger than the screen. * Because styles have to be applied to pagination container element, this has to be a data-grid prop (rather than a pagination prop). */ stickyPagination: { type: Boolean, default: false, }, /** * Make data-grid filters stick to the top of the page along with the data-grid headers. * Defaults to true. */ stickyFilters: { type: Boolean, default: true, }, /** * Enables / Disables rows to be selected via a checkbox. On select the "select" event will be emitted. */ selectable: { type: Boolean, default: false, }, /** * Selected items */ selection: { type: Array as PropType<Array<BentoDatagridDataItemId>>, default: () => [], }, /** * Enables / Disables the nested-content slot */ shouldShowNestedContent: { type: Function as PropType<BentoDatagridShouldShowNestedContentFn>, default: (_item: BentoDatagridDataItem) => true, }, /** * Returns the nested rows for a given BentoDatagridDataItem. */ shouldShowNestedRows: { type: Function as PropType<BentoDatagridShouldShowNestedRowsFn>, default: (_item: BentoDatagridDataItem) => true, }, /** * Sortby handles proper rendering of sort icon in header. Sorting currently is only supported server side. * It does make sure the currently sorted column has a more visible sort icon, and the sort icon points in the right direction. */ sortBy: { type: Array as PropType<Array<BentoDatagridSortByColumn>>, // NOTE: is an array to allow multi-sorting in the future default: () => [], }, /** * Title of the data grid to be rendered as tab title when a data grid is passed in a tab layout. */ tabTitle: { type: String, default: null, }, /** * Truncates all data grid column headers */ truncateColumnHeaders: { type: Boolean, default: false, }, /** * Toggles the alternating background color of rows. */ zebraRows: { type: Boolean, default: false, }, }); const slots = useSlots(); const hasSlot = useHasSlot(slots); /* == Variables and references == */ // Create refs for elements const body = ref<HTMLDivElement>(null); const columnsRef = ref<HTMLDivElement>(); const columnHeaderRefs = ref<DatagridColumnsHeaderReferences>({}); const dataGridUid = ref<string>(generateUid('data-grid')); const gridcellRefs = ref<{ [key: string]: Array<HTMLDivElement> }>(null); const filtersRef = ref<HTMLDivElement>(null); const filterBarRef = ref<InstanceType<typeof BentoFilterBar>>(); const paginationContainerRef = ref<HTMLDivElement>(null); const verticalScrollPositionRef = ref(null); const virtualHorizontalScrollbarRef = ref<HTMLDivElement>(null); const { allowColumnDragAndDrop, columns, columnPanel, data, lazyLoadType, loading, hasNestedContent, hasNestedRows, selectable, selection, selectableAcrossPages, stickyPagination, outline, pagination, zebraRows, } = toRefs(props); const hoveredRowIndex = ref(-1); const { t } = useI18n<{ message: MessageSchema }>({ messages }); const frozenColumnsLength = computed(() => columnDefs.value.filter(column => column.frozen).length); /* == Responsive behavior == */ // Identifies if the data-grid is in a container smaller than 550px const MOBILE_BREAKPOINT = 550; const dataGridRef = ref(); const { width } = useElementSize(dataGridRef); const isSmallContainer = computed(() => width.value <= MOBILE_BREAKPOINT); provide(DATA_GRID_SMALL_CONTAINER, isSmallContainer); /* == Touch device behavior == */ // Identifies if the data-grid is rendered on a touch device const isTouchDevice = ref(false); onMounted(() => { if (typeof window !== 'undefined') { isTouchDevice.value = 'ontouchstart' in window || navigator?.maxTouchPoints > 0; } }); provide(DATA_GRID_TOUCH_DEVICE, isTouchDevice); /* == Data-grid template == */ const registerDatagridInDataGridTemplate: BentoDataGridTemplateDataGrid = inject( DATA_GRID_REGISTER_TEMPLATE_INJECTION_KEY, () => ({ additionalContentSlotRef: ref(null), isDataGridTemplate: false, stickyAditionalContent: ref(false), stickyTabs: ref(false), tabsRef: ref(null), }) ); const { additionalContentSlotRef, isDataGridTemplate, stickyTabs, tabsRef, stickyAdditionalContent } = registerDatagridInDataGridTemplate(); /* == Composable logic == */ // Column width calculation const { columnDefs, onColumnWidthOverride, gridRowWidthStyle, isMeasuringColumnWidths, isTotalWidthOfColumnsTakingUpAllAvailableGridWidth, columns: localColumnsState, totalWidthOfAllColumns, rowActionsMaxWidth, } = useCalculateColumnWidth(columns, data, body, columnHeaderRefs, gridcellRefs, { hasColumnPanel: columnPanel, hasSelectableColumn: selectable, hasSelectableAcrossPages: selectableAcrossPages, hasNestedContent, hasNestedRows, allowColumnDragAndDrop, isDataGridTemplate: isDataGridTemplate, }); /* == Emitting logic == */ const definedEmitsFunction = defineEmits([ BentoDatagridEvent.COLUMNS, BentoDatagridEvent.COLUMN_RESIZE, BentoDatagridEvent.ITEMS_PAGE, BentoDatagridEvent.FILTER_SEARCH_INPUT, BentoDatagridEvent.FILTER_SEARCH_CLEAR, BentoDatagridEvent.NAVIGATE, BentoDatagridEvent.NESTED_CONTENT_TOGGLED, BentoDatagridEvent.UPDATE_COLUMNS, BentoDatagridEvent.UPDATE_CONDENSED, BentoDatagridEvent.UPDATE_FILTER_VALUES, BentoDatagridEvent.UPDATE_FILTER_VALUES_OBJECT, BentoDatagridEvent.UPDATE_FILTERS, BentoDatagridEvent.UPDATE_FILTER_SEARCH_TERM, BentoDatagridEvent.UPDATE_SHOW_MORE, BentoDatagridEvent.UPDATE_PAGINATION, BentoDatagridEvent.UPDATE_SELECTION, BentoDatagridEvent.UPDATE_SORT_BY, BentoDatagridEvent.ROW_CLICK, BentoDatagridEvent.SELECT, BentoDatagridEvent.SELECT_ALL_ACROSS_PAGES, BentoDatagridEvent.SELECT_ALL_CURRENT_RESULTS, BentoDatagridEvent.DESELECT_ALL_CURRENT_RESULTS, BentoDatagridEvent.SORT, BentoDatagridEvent.CELL_FOCUS, ]); /** * Hooks into the emit function and updates the local columns state if we're updating columns. * This is so if users use a computed ref with the columns property the ordering / toggling visibility will still function correctly. */ const emit: typeof definedEmitsFunction = (event, ...args) => { // If we updating columns update the local columns state if (event === BentoDatagridEvent.COLUMNS || event === BentoDatagridEvent.UPDATE_COLUMNS) { [localColumnsState.value] = args; } definedEmitsFunction(event, ...args); }; // Calculate dynamic top position of sticky filters const stickyFiltersHeight = useCalculateStickyElementHeight( tabsRef, stickyTabs, stickyAdditionalContent, additionalContentSlotRef ); const computedStickyFiltersTopPosition = computed(() => props.stickyFilters ? `${stickyFiltersHeight.value}px` : null ); // Calculate dynamic top position of column headers const columnHeadersHeight = useCalculateStickyElementHeight(filtersRef, toRef(props, 'stickyFilters')); const computedColumnHeadersTopPosition = computed( () => `${columnHeadersHeight.value + stickyFiltersHeight.value}px` ); // Config Settings (condensed, visbility and freezing settings) const { sortedColumns, columnSets, isOnlyOneVisibleColumn, internalZebraRows, onConfigSettingsColumnsUpdate, onConfigSettingsColumnUpdate, onConfigSettingsCondensedUpdate, onConfigSettingsZebraRowsUpdate, } = useConfigSettings(columnDefs, zebraRows, emit); // Column hovering const { hoveredOverColumn, hoveredOverColumnState } = useColumnHovering( columnSets, selectable, outline, isTotalWidthOfColumnsTakingUpAllAvailableGridWidth ); // Re-ordering column (dragging) const { draggingColumns, draggingEventsForColumns, draggingEventsForRowCell, onMoveColumnByOffset } = useColumnReorderer(sortedColumns, allowColumnDragAndDrop, emit); // Selecting const selectableAcrossPagesComputed = computed( () => props.data?.length > 0 && !!props.selectableAcrossPages && !!props.pagination && props.pagination.totalCount > 0 ); const { onAllRowSelect, onRowSelect, onNestedRowSelect, onDeselectAll, allItemsSelectedState, selectedItems, selectionState, } = useSelector( selection, emit, data, props.getDataItemId, props.isDataItemDisabled, selectableAcrossPagesComputed, pagination ); const visibleRowsState = computed<BentoDataGridVisibleRowsState>(() => ({ filtered: filterBarRef.value?.isClearAllVisible || filterBarRef.value?.isResetAllVisible || !!props.filterSearchTerm, allRowsVisible: pagination.value?.size >= pagination.value?.totalCount, })); // Keyboard navigation const { navigationInput } = useKeyboardNavigator( gridcellRefs, columnHeaderRefs, sortedColumns, selectable, computed(() => props.rowActions !== undefined), columnPanel ); // Lazy loading const { isLazyLoad, isInfiniteScroll, verticalScrollPosition, emitLazyLoadEvent } = useLazyLoading( lazyLoadType, verticalScrollPositionRef, body, loading, emit ); // Scrolling const { updateScrollPosition, updateScrollPositionOnFrozenColumnsNavigation, syncVirtualScrollerWithBody, hasLeftOverflow, hasRightOverflow, horizontalScrollbarConditionalStyles, } = useScroller( body, columnsRef, verticalScrollPosition, virtualHorizontalScrollbarRef, paginationContainerRef, totalWidthOfAllColumns, { isLazyLoad, pagination, stickyPagination, isDataGridTemplate, } ); /* == Empty State == */ const onEmptyStateClearResetButtonClick = () => { if (filterBarRef.value.isClearAllVisible) { filterBarRef.value.clearAllFilters(); } else if (filterBarRef.value.isResetAllVisible) { filterBarRef.value.resetAllFilters(); } }; const limitedEmptyStateProps = computed<BentoDatagridEmptyStateProps>(() => ({ ...props.emptyState, variant: props.emptyState?.variant === 'embedded' || props.emptyState?.variant === 'basic' ? props.emptyState.variant : 'basic', })); /* == Styles == */ // Conditional grid classes const conditionalGridClasses = computed(() => ({ [`b-data-grid--${BentoDatagridStyleState.OUTLINE}`]: props.outline, [`b-data-grid--${BentoDatagridStyleState.SCROLL}`]: props.fitContent || isLazyLoad.value, [`b-data-grid--${BentoDatagridStyleState.STICKY_FILTERS}`]: props.filters && props.stickyFilters, [`b-data-grid--${BentoDatagridStyleState.OVERFLOW_SHADOW_LEFT}`]: hasLeftOverflow.value && !frozenColumnsLength.value, [`b-data-grid--${BentoDatagridStyleState.OVERFLOW_SHADOW_RIGHT}`]: hasRightOverflow.value && props.rowActions === undefined, [`b-data-grid--${BentoDatagridStyleState.NO_SPACING_TOP}`]: props.noSpacingTop, [`b-data-grid--${BentoDatagridStyleState.FIT_CONTENT}`]: props.fitContent, [`b-data-grid--${BentoDatagridStyleState.ZEBRA_ROWS}`]: internalZebraRows.value, 'b-data-grid--lazy-load': isLazyLoad.value, })); // Conditional grid body classes const conditionalGridBodyClasses = computed(() => ({ 'b-data-grid__body--sticky-horizontal-scrollbar': props.stickyHorizontalScrollbar, })); const computedColumnPanelAlignment = computed(() => columnSets.value.findIndex(el => el.columnSet.length)); const showFrozenColumnsSeparator = computed(() => { const isSelectionOnlyFrozenColumn = (props.selectable || props.hasNestedContent || props.hasNestedRows) && frozenColumnsLength.value === 1; return !isSelectionOnlyFrozenColumn || hasLeftOverflow.value; }); // Conditional filter classes const conditionalFilterClasses = computed(() => ({ 'b-data-grid__filters--sticky': !!props.stickyFilters || isDataGridTemplate, })); // Conditional pagination classes const conditionalPaginationClasses = computed(() => ({ 'b-data-grid__pagination--padding-left-unset': props.columns?.[0]?.padding?.left === false, 'b-data-grid__pagination--padding-right-unset': props.columns?.[props.columns.length - 1]?.padding?.right === false, 'b-data-grid__pagination--sticky': !!props.stickyPagination || isDataGridTemplate, })); /* == Ref registration methods == */ const registerColumnHeaderRefs = (el: Element | ComponentPublicInstance) => { // We always know this will be used on a DataGridColumns component so we can cast it const datagridColumnsComponent = el as InstanceType<typeof DataGridColumns>; if (el !== null && datagridColumnsComponent?.columnHeaderRefs) { Object.entries(datagridColumnsComponent?.columnHeaderRefs).forEach(([fieldName, colHeaderEl]) => { columnHeaderRefs.value[fieldName] = colHeaderEl as HTMLDivElement; }); } }; const registerRowCellRefs = (el: Element | ComponentPublicInstance, rowIndex: number) => { // We always know this will be used on a DataGridCell component so we can cast it const gridRowComponent = el as InstanceType<typeof DataGridRow>; if (el !== null) { if (gridRowComponent?.gridcellRefs) { const areRowActions = props.rowActions !== undefined && gridRowComponent?.rowActionsRef; Object.entries(gridRowComponent?.gridcellRefs).map(([field, cell]) => { if (gridcellRefs.value?.[field] === undefined) { gridcellRefs.value = { ...gridcellRefs.value, [field]: [] }; } gridcellRefs.value[field][rowIndex] = cell as HTMLDivElement; }); // This calculates the maxium width of the row actions column so we can add the same padding to the columns keep everything aligned if (areRowActions) { if (gridcellRefs.value?.[DATAGRID_ROW_ACTIONS_COLUMN_FIELD_NAME] === undefined) { gridcellRefs.value = { ...gridcellRefs.value, [DATAGRID_ROW_ACTIONS_COLUMN_FIELD_NAME]: [] }; } gridcellRefs.value[DATAGRID_ROW_ACTIONS_COLUMN_FIELD_NAME][rowIndex] = gridRowComponent .rowActionsRef?.[0] as HTMLDivElement; rowActionsMaxWidth.value = Math.max( rowActionsMaxWidth.value, gridRowComponent.rowActionsRef?.[0]?.offsetWidth ?? 0 ); } } } }; /* == Row methods == */ // Row selecting const onRowClick = (row: BentoDatagridDataItem, event: MouseEvent) => { if (props.allowRowClicks) { emit(BentoDatagridEvent.ROW_CLICK, row, event); } }; // Programmatically set the hover state on a row const onCellHover = (rowIndex: number) => { hoveredRowIndex.value = rowIndex; }; /* == a11y == */ const ariaGridRole = computed(() => (props.hasNestedContent || props.hasNestedRows ? 'treegrid' : 'grid')); /* == Action bar == */ const itemsSelectedCounter = computed(() => selectedItems.value?.length ?? 0); const showActionBar = computed(() => props.bulkActions && itemsSelectedCounter.value > 0); /* == Emits & Events == */ // Resizing const onColumnResized = (resizeEvent: { field: string; width: number }) => emit(BentoDatagridEvent.COLUMN_RESIZE, resizeEvent); // Sorting const onSort = (updatedSortBy: Array<BentoDatagridSortByColumn>) => { emit(BentoDatagridEvent.UPDATE_SORT_BY, updatedSortBy); emit(BentoDatagridEvent.SORT, updatedSortBy); }; // Pagination const onPaginationNavigate = (page: number, size: number) => { emit(BentoDatagridEvent.NAVIGATE, page); emit(BentoDatagridEvent.UPDATE_PAGINATION, { ...props.pagination, size, page, }); }; const onItemsPerPageChange = (itemsPerPage: number) => { emit(BentoDatagridEvent.ITEMS_PAGE, itemsPerPage); }; // Filter bar const onSearchTermCleared = () => { emit(BentoDatagridEvent.FILTER_SEARCH_CLEAR); }; const onSearchTermInput = newSearchTerm => { emit(BentoDatagridEvent.UPDATE_FILTER_SEARCH_TERM, newSearchTerm); emit(BentoDatagridEvent.FILTER_SEARCH_INPUT, newSearchTerm); }; /* == Warnings == */ const hasMandatoryColumn = computed(() => columns.value.some(column => column.mandatory)); if (!hasMandatoryColumn.value) { printDevelopmentWarning( 'WARNING: At least one BentoDataGrid column should be set as mandatory to ensure data is always displayed correctly.' ); } /* == Provide/inject == */ const setTitle: BentoTabProvidedTitle = inject(EXTERNAL_TITLE_REGISTER_ENTRY_INJECTION_KEY, () => null); setTitle(props.tabTitle); provide(DATA_GRID_SLASHED_ZERO, toRef(props, 'slashedZero')); /* == Expose methods == */ defineExpose({ onEmptyStateClearResetButtonClick }); </script> <script lang="ts"> /** * A component to display data in columns and row format with a whole array of configuration for visualization and interaction. * * @example * import { BentoDataGrid } from '@adyen/bento-vue2'; * * export default { * components: { BentoDataGrid }, * template: ` * <bento-data-grid * :data="[ * { id: '123', name: 'John', company: 'TestCompany', 'payment': 200 } * { id: '321', name: 'Jane', company: 'Adyen', 'payment': 300 } * ]" * :columns="[ * { field: 'name', label: 'Name', minWidth: 300, mandatory: true }, * { field: 'company', label: 'Reference' }, * { field: 'payment', label: 'Payment', width: 200, minWidth: 150 }, * ]" * /> * ` * } */ export default { i18n: { messages }, name: 'bento-data-grid', }; </script> <style lang="scss" scoped src="./data-grid.scss" />
|