Skip to content

Commit

Permalink
feat(ktableview): add row-key prop [KHCP-13502]
Browse files Browse the repository at this point in the history
  • Loading branch information
portikM committed Oct 1, 2024
1 parent a048db0 commit 0fcfb15
Show file tree
Hide file tree
Showing 3 changed files with 101 additions and 63 deletions.
1 change: 1 addition & 0 deletions sandbox/pages/SandboxTableView/SandboxTableView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,7 @@
:data="paginatedData"
:headers="headers(false, false, true)"
:pagination-attributes="{ totalCount: basicPaginatedData.length, pageSizes: [5, 10] }"
row-key="id"
@page-change="onPageChange"
>
<template #bulk-actions="{ selectedRows }">
Expand Down
8 changes: 6 additions & 2 deletions src/components/KTableView/KTableView.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,7 @@ describe('KTableView', () => {
props: {
headers: [{ label: 'Bulk actions', key: 'bulkActions' }, ...options.headers],
data: options.data,
rowKey: 'id',
},
slots: {
'bulk-action-items': () => h('span', {}, 'Bulk action'),
Expand All @@ -315,6 +316,7 @@ describe('KTableView', () => {
props: {
headers: [{ label: 'Bulk actions', key: 'bulkActions' }, ...options.headers],
data: options.data,
rowKey: 'id',
},
slots: {
'bulk-actions': () => h('span', {}, 'Bulk action'),
Expand All @@ -332,8 +334,8 @@ describe('KTableView', () => {
cy.getTestId('bulk-actions-checkbox').eq(2).should('be.checked')
cy.getTestId('indeterminate-icon').should('not.exist')

cy.wrap(Cypress.vueWrapper.emitted()).should('have.property', 'row-select').and('have.length', 3)
cy.wrap(Cypress.vueWrapper.emitted('row-select')?.[2][0]).should('have.length', options.data.length)
cy.wrap(Cypress.vueWrapper.emitted()).should('have.property', 'row-select').and('have.length', 4)
cy.wrap(Cypress.vueWrapper.emitted('row-select')?.[3][0]).should('have.length', options.data.length)
})
})

Expand All @@ -349,6 +351,7 @@ describe('KTableView', () => {

return true
},
rowKey: 'id',
},
slots: {
'bulk-actions': () => h('span', {}, 'Bulk action'),
Expand Down Expand Up @@ -550,6 +553,7 @@ describe('KTableView', () => {
data: options.data,
resizeColumns: true,
nested: true,
rowKey: 'id',
},
slots: {
'bulk-action-items': () => h('span', {}, 'Bulk action'),
Expand Down
155 changes: 94 additions & 61 deletions src/components/KTableView/KTableView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
v-if="!$slots['bulk-actions']"
:button-label="tableHeaders.find((header: TableViewHeader) => header.key === TableViewHeaderKeys.BULK_ACTIONS)!.label"
:count="bulkActionsSelectedRowsCount"
:disabled="!bulkActionsSelectedRowsCount || loading || !tableData.length"
:disabled="!bulkActionsSelectedRowsCount || loading || !data.length"
>
<template #items>
<slot
Expand Down Expand Up @@ -79,7 +79,7 @@
</div>

<div
v-else-if="!error && !loading && (tableData && !tableData.length)"
v-else-if="!error && !loading && (data && !data.length)"
class="table-empty-state"
data-testid="table-empty-state"
>
Expand Down Expand Up @@ -229,11 +229,11 @@

<tbody>
<template
v-for="(row, rowIndex) in tableData"
v-for="(row, rowIndex) in data"
:key="`table-${tableId}-row-${rowIndex}`"
>
<tr
:class="{ 'last-row': rowIndex === tableData.length - 1 && !expandedRows.includes(rowIndex) }"
:class="{ 'last-row': rowIndex === data.length - 1 && !expandedRows.includes(rowIndex) }"
:role="!!rowLink(row).to ? 'link' : undefined"
:tabindex="isClickable || !!rowLink(row).to ? 0 : undefined"
v-bind="rowAttrs(row)"
Expand Down Expand Up @@ -266,13 +266,13 @@
</slot>

<KTooltip
v-else-if="header.key === TableViewHeaderKeys.BULK_ACTIONS"
v-else-if="header.key === TableViewHeaderKeys.BULK_ACTIONS && getRowState(row)"
max-width="200"
placement="bottom-start"
:text="getRowBulkActionEnabled(row) ? undefined : getRowBulkActionTooltip(row)"
>
<KCheckbox
v-model="row.selected"
v-model="getRowState(row)!.selected"
aria-label="Toggle row selection"
class="bulk-actions-checkbox"
data-testid="bulk-actions-checkbox"
Expand Down Expand Up @@ -407,6 +407,12 @@ enum TableViewHeaderKeys {
BULK_ACTIONS = 'bulkActions',
}
interface DataSelectState {
rowKey: string
selected: boolean
disabled: boolean
}
const props = defineProps({
/**
* Allow columns to be resized
Expand Down Expand Up @@ -547,6 +553,13 @@ const props = defineProps({
type: Array as PropType<TableViewData>,
default: () => [],
},
/**
* A prop to pass row object key to be used as a unique identifier
*/
rowKey: {
type: [String, Function] as PropType<string | ((row: Record<string, any>) => string)>,
default: '',
},
maxHeight: {
type: String,
default: 'none',
Expand Down Expand Up @@ -603,6 +616,14 @@ const slots = useSlots()
const tableId = useUniqueId()
const { getSizeFromString } = useUtilities()
const getRowKey = (row: Record<string, any>): string => {
if (typeof props.rowKey === 'function') {
return props.rowKey(row)
}
return row[props.rowKey] ? String(row[props.rowKey]) : ''
}
const headerRow = ref<HTMLDivElement>()
// all headers
const tableHeaders = ref<TableViewHeader[]>([])
Expand All @@ -626,9 +647,9 @@ const hasColumnVisibilityMenu = computed((): boolean => {
}
// show when not loading and there is data
return !props.loading && !!tableData.value && !!tableData.value.length
return !props.loading && !!props.data && !!props.data.length
})
const columnVisibilityDisabled = computed((): boolean => props.loading || !(tableData.value && tableData.value.length))
const columnVisibilityDisabled = computed((): boolean => props.loading || !(props.data && props.data.length))
// columns whose visibility can be toggled
const visibilityColumns = computed((): TableViewHeader[] => tableHeaders.value.filter((header: TableViewHeader) => header.hidable && header.key !== TableViewHeaderKeys.EXPANDABLE && header.key !== TableViewHeaderKeys.BULK_ACTIONS))
// visibility preferences from the host app (initialized by app)
Expand All @@ -646,9 +667,9 @@ const tableWrapperStyles = computed((): Record<string, string> => ({
maxHeight: getSizeFromString(props.maxHeight),
}))
const tableData = ref<TableViewData>([...props.data].map((row) => ({ ...row, selected: false })))
const bulkActionsSelectedRows = ref<TableViewData>([])
const hasBulkActions = computed((): boolean => !props.nested && !props.error && tableHeaders.value.some((header: TableViewHeader) => header.key === TableViewHeaderKeys.BULK_ACTIONS) && !!(slots['bulk-action-items'] || slots['bulk-actions']))
const hasBulkActions = computed((): boolean => !!props.rowKey && !props.nested && !props.error && tableHeaders.value.some((header: TableViewHeader) => header.key === TableViewHeaderKeys.BULK_ACTIONS) && !!(slots['bulk-action-items'] || slots['bulk-actions']))
const dataSelectState = ref<DataSelectState[]>([])
const showBulkActionsToolbar = computed((): boolean => {
if (props.nested || !hasBulkActions.value || props.error) {
return false
Expand All @@ -660,7 +681,7 @@ const showBulkActionsToolbar = computed((): boolean => {
}
// show when not loading and there is data
return !props.loading && !!tableData.value && !!tableData.value.length
return !props.loading && !!props.data && !!props.data.length
})
const bulkActionsSelectedRowsCount = computed((): string => {
const selectedRowsCount = bulkActionsSelectedRows.value.length
Expand Down Expand Up @@ -952,7 +973,7 @@ const showPagination = computed((): boolean => {
return false
}
if (tableData.value && tableData.value.length && props.paginationAttributes.totalCount && props.paginationAttributes.totalCount <= tableData.value.length) {
if (props.data && props.data.length && props.paginationAttributes.totalCount && props.paginationAttributes.totalCount <= props.data.length) {
return false
}
Expand Down Expand Up @@ -1022,6 +1043,10 @@ const scrollHandler = (event: any): void => {
}
}
const getRowState = (row: Record<string, any>): DataSelectState | undefined => {
return dataSelectState.value.find((rowState) => rowState.rowKey === getRowKey(row))
}
const getRowBulkActionEnabled = (row: Record<string, any>): boolean => {
if (typeof props.rowBulkActionEnabled !== 'function') {
return false
Expand Down Expand Up @@ -1209,74 +1234,82 @@ const bulkActionsAll = ref<boolean>(false)
const isBulkActionsIndeterminate = computed((): boolean => {
// ignore thee disabled rows
const selectableRows = tableData.value.filter((row) => getRowBulkActionEnabled(row))
const selectableRowsState = dataSelectState.value.filter((rowState) => !rowState.disabled && props.data.find((row) => getRowKey(row) === rowState.rowKey))
// it is indeterminate if there are selected and unselected rows
return !!selectableRows.filter((row) => row.selected).length && !!selectableRows.filter((row) => !row.selected).length
return !!selectableRowsState.filter((rowState) => rowState.selected).length && !!selectableRowsState.filter((rowState) => !rowState.selected).length
})
const handleIndeterminateChange = (value: boolean) => {
if (value) {
// select all selectable rows
tableData.value = [...tableData.value].map((row) => ({ ...row, selected: getRowBulkActionEnabled(row) }))
} else {
// unselect and reset all
tableData.value = [...props.data].map((row) => ({ ...row, selected: false }))
}
// assign the value to all selectable rows which will result in either selecting or deselecting all selectable rows
dataSelectState.value.forEach((rowState) => {
if (props.data.find((row) => getRowKey(row) === rowState.rowKey) && !rowState.disabled) {
rowState.selected = value
}
})
}
watch(tableData, (newVal) => {
/** update the bulkActionsAll value */
/**
* Watch for changes in data and dataSelectState
*/
watch([() => props.data, dataSelectState], (newVals) => {
const [newData, newDataSelectState] = newVals
if (hasBulkActions.value) {
// add new rows to the dataSelectState
newData.forEach((row) => {
if (!getRowState(row)) {
dataSelectState.value.push({
rowKey: getRowKey(row),
selected: false,
disabled: !getRowBulkActionEnabled(row),
})
}
})
const selectableRows = newVal.filter((row) => getRowBulkActionEnabled(row))
/** update the bulkActionsAll value */
// all are selected
if (selectableRows.filter((row) => row.selected).length === selectableRows.length) {
bulkActionsAll.value = true
// all are unselected
} else if (selectableRows.filter((row) => !row.selected).length === selectableRows.length) {
bulkActionsAll.value = false
// some are selected
} else {
bulkActionsAll.value = false
}
const selectableRowsState = newDataSelectState.filter((rowState) => !rowState.disabled && newData.find((row) => getRowKey(row) === rowState.rowKey))
/** update the selected rows */
// all are selected
if (selectableRowsState.filter((rowState) => rowState.selected).length === selectableRowsState.length) {
bulkActionsAll.value = true
// all are unselected
} else if (selectableRowsState.filter((rowState) => !rowState.selected).length === selectableRowsState.length) {
bulkActionsAll.value = false
// some are selected
} else {
bulkActionsAll.value = false
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const newSelectedRows = newVal.filter((row) => row.selected).map(({ selected, ...rest }) => rest)
/** update bulkActionsSelectedRows */
const oldSelectedRows: TableViewData = []
bulkActionsSelectedRows.value.forEach((selectedRow) => {
const row = props.data.find((row) => JSON.stringify(row) === JSON.stringify(selectedRow))
// find all selected rows from the new state
const newSelectedRows: TableViewData = newData.filter((row) => {
const rowState = newDataSelectState.find((rowState) => rowState.rowKey === getRowKey(row))
if (!row) {
oldSelectedRows.push(selectedRow)
}
})
if (rowState && rowState.selected) {
return true
}
bulkActionsSelectedRows.value = [...oldSelectedRows, ...newSelectedRows]
}, { deep: true })
return false
})
/**
* Watch for changes in the data prop and update the tableData
* We want to display the selected rows from the previous data prop value
*/
watch(() => props.data, (newVal) => {
tableData.value = []
// find all selected rows from the old state
const oldSelectedRows: TableViewData = []
bulkActionsSelectedRows.value.forEach((selectedRow) => {
const row = newData.find((dataRow) => getRowKey(selectedRow) === getRowKey(dataRow))
newVal.forEach((row) => {
const selectedRow = bulkActionsSelectedRows.value.find((selectedRow) => JSON.stringify(selectedRow) === JSON.stringify(row))
if (!row) {
oldSelectedRows.push(selectedRow)
}
})
if (selectedRow) {
tableData.value.push({ ...row, selected: true })
} else {
tableData.value.push({ ...row, selected: false })
}
})
bulkActionsSelectedRows.value = [...oldSelectedRows, ...newSelectedRows]
}
expandedRows.value = []
}, { deep: true })
}, { deep: true, immediate: true })
watch(bulkActionsSelectedRows, (newVal) => {
emit('row-select', newVal)
Expand Down

0 comments on commit 0fcfb15

Please sign in to comment.