Skip to content

Commit

Permalink
feat(ktable): toggle column visibility [khcp-11162] (#2114)
Browse files Browse the repository at this point in the history
Update `KTable` to add ability to toggle the visibility of columns.
For [KHCP-11162](https://konghq.atlassian.net/browse/KHCP-11162).
  • Loading branch information
kaiarrowood authored and adamdehaven committed Jun 3, 2024
1 parent 5003612 commit 7a9d039
Show file tree
Hide file tree
Showing 6 changed files with 319 additions and 14 deletions.
84 changes: 83 additions & 1 deletion docs/components/table.md
Original file line number Diff line number Diff line change
Expand Up @@ -438,6 +438,7 @@ Pass in an array of header objects for the table.
| `key` | string | A unique key for the column |
| `label` | string | The label displayed on the table for the column |
| `sortable` | boolean | Enables or disables server-side sorting for the column (`false` by default) |
| `hidable` | boolean | Enable show/hide for this column |
| `hideLabel` | boolean | Hides or displays the column label (useful for actions columns) |
| `useSortHandlerFn` | boolean | Uses the function passed in the [sortHandlerFn](#sorthandlerfn) prop to sort the column data instead of the default client sorter function |

Expand Down Expand Up @@ -465,7 +466,7 @@ Example headers array:
{ key: 'name', label: 'Name', sortable: true },
{ key: 'email', label: 'Email', sortable: true },
{ key: 'department', label: 'department', sortable: true },
{ key: 'start_date', label: 'Start Date', sortable: true },
{ key: 'start_date', label: 'Start Date', sortable: true, hidable: true },
{ key: 'actions', label: '', sortable: false, hideLabel: true },
]
}
Expand All @@ -474,6 +475,41 @@ Example headers array:
</script>
```

#### hidable

Using this modal will cause the column visibility menu to be displayed above the table after any toolbar content. Hovering over a column with `hidable: true` will trigger the display of a hide button in the header. An `@update:table-preferences` event is emitted whenever changes are applied.

<KTable :fetcher="tableHideColumnFetcher" :headers="hideColumnHeaders" :table-preferences="hidablePreferences" @update:table-preferences="(tablePrefs) => hidablePreferences.columnVisibility = tablePrefs.columnVisibility" />

```html
<template>
<KTable
:fetcher="fetcher"
:headers="headers"
:table-preferences="myPreferences"
@update:table-preferences="(newTablePreferences: TablePreferences) => {
myPreferences.columnVisibility = newTablePreferences.columnVisibility
}"
/>
</template>

<script lang="ts">
export default {
data() {
return {
headers: [
{ label: 'Host', key: 'hostname' },
{ label: 'Version', key: 'version' },
{ label: 'Connected', key: 'connected' },
{ label: 'Last Ping', key: 'last_ping', hidable: true },
{ label: 'Last Seen', key: 'last_seen', hidable: true },
]
}
}
}
</script>
```

### initialFetcherParams

Pass in an object of parameters for the initial fetcher function call. If not provided, will default to the following values:
Expand Down Expand Up @@ -922,6 +958,8 @@ interface TablePreferences {
sortColumnOrder?: 'asc' | 'desc'
/** The customized column widths, if resizing is allowed */
columnWidths?: Record<string, number>
/** Column visibility, if visibility is toggleable */
columnVisibility?: Record<string, boolean>
}
```

Expand Down Expand Up @@ -1597,6 +1635,7 @@ export default defineComponent({
enableRowClick: true,
offsetPaginationPageSize: 15,
offsetPaginationData: {},
hidablePreferences: {},
headers: [
{ label: 'Title', key: 'title', sortable: true },
{ label: 'Description', key: 'description', sortable: true },
Expand All @@ -1613,6 +1652,13 @@ export default defineComponent({
{ label: 'Enabled', key: 'enabled' },
{ key: 'actions', hideLabel: true }
],
hideColumnHeaders: [
{ label: 'Host', key: 'hostname' },
{ label: 'Version', key: 'version' },
{ label: 'Connected', key: 'connected' },
{ label: 'Last Ping', key: 'last_ping', hidable: true },
{ label: 'Last Seen', key: 'last_seen', hidable: true },
],
tableOptionsRowAttrsHeaders: [
{ label: 'Type', key: 'type' },
{ label: 'Value', key: 'value' },
Expand Down Expand Up @@ -1708,6 +1754,42 @@ export default defineComponent({
emptyFetcher () {
return { data: [] }
},
tableHideColumnFetcher () {
return {
data: [{
id: '08cc7d81-a9d8-4ae1-a42f-8d4e5a919d07',
version: '2.8.0.0-enterprise-edition',
hostname: '99e591ae3776',
last_ping: 1648855072,
connected: 'Disconnected',
last_seen: '6 days ago'
},
{
id: '08cc7d81-a9d8-4ae1-a42f-8d4e5a919d07',
version: '2.7.0.0-enterprise-edition',
hostname: '19e591ae3776',
last_ping: 1649362660,
connected: 'Connected',
last_seen: '3 hours ago',
},
{
id: '08cc7d81-a9d8-4ae1-a42f-8d4e5a919d07',
version: '2.8.1.0-enterprise-edition',
hostname: '79e591ae3776',
last_ping: 1649355460,
connected: 'Connected',
last_seen: '5 hours ago',
},
{
id: '08cc7d81-a9d8-4ae1-a42f-8d4e5a919d07',
version: '2.6.0.0-enterprise-edition',
hostname: '89e591ae3776',
last_ping: 1648155072,
connected: 'Disconnected',
last_seen: '14 days ago'
}]
}
},
tableOptionsLinkFetcher () {
return {
data: [
Expand Down
10 changes: 8 additions & 2 deletions src/components/KCheckbox/KCheckbox.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@
class="k-checkbox"
:class="[$attrs.class, kCheckboxClasses ]"
>
<div class="checkbox-input-wrapper">
<div
class="checkbox-input-wrapper"
:class="{ 'has-label': hasLabel }"
>
<input
:id="inputId"
v-bind="modifiedAttrs"
Expand Down Expand Up @@ -183,8 +186,11 @@ export default {
.checkbox-input-wrapper {
display: flex;
margin-top: 3px; // align with label
position: relative;
&.has-label {
margin-top: 3px; // align with label
}
}
/* Checkbox styles */
Expand Down
142 changes: 142 additions & 0 deletions src/components/KTable/ColumnVisibilityMenu.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
<template>
<div class="table-column-visibility-menu">
<KDropdown
data-testid="table-column-visibility-menu"
@toggle-dropdown="handleDropdownToggle"
>
<KTooltip text="Show/Hide Columns">
<KButton
appearance="secondary"
class="menu-button"
data-testid="column-visibility-menu-button"
size="large"
>
<template #icon>
<TableColumnsIcon />
</template>
</KButton>
</KTooltip>

<template #items>
<KDropdownItem
v-for="col in columns"
:key="col.key"
class="column-visibility-menu-item"
:data-testid="`column-visibility-menu-item-${col.key}`"
@click.stop="() => {
visibilityMap[col.key] = !visibilityMap[col.key]
isDirty = true
}"
>
<!-- KLabel must be separate to maintain click handling on the label within the dropdown item -->
<KCheckbox
v-model="visibilityMap[col.key]"
:aria-labelledby="`${tableId}-${col.key}-visibility-checkbox-label`"
:data-testid="`column-visibility-checkbox-${col.key}`"
/>
<KLabel
:id="`${tableId}-${col.key}-visibility-checkbox-label`"
class="visibility-checkbox-label"
>
{{ col.label }}
</KLabel>
</KDropdownItem>
<div class="apply-button-wrapper">
<KButton
appearance="tertiary"
class="apply-button"
data-testid="apply-button"
@click="handleApply"
>
Apply
</KButton>
</div>
</template>
</KDropdown>
</div>
</template>

<script setup lang="ts">
import { ref, watch, onBeforeMount, type PropType } from 'vue'
import type { TableHeader } from '@/types'
import { TableColumnsIcon } from '@kong/icons'
import KButton from '@/components/KButton/KButton.vue'
import KCheckbox from '@/components/KCheckbox/KCheckbox.vue'
import KDropdown from '@/components/KDropdown/KDropdown.vue'
import KDropdownItem from '@/components/KDropdown/KDropdownItem.vue'
const emit = defineEmits<{
(e: 'update:visibility', columnVisibility: Record<string, boolean>): void
}>()
const props = defineProps({
columns: {
type: Array as PropType<TableHeader[]>,
required: true,
},
tableId: {
type: String,
required: true,
},
visibilityPreferences: {
type: Object as PropType<Record<string, boolean>>,
default: () => ({}),
},
})
const visibilityMap = ref<Record<string, boolean>>({})
const isDirty = ref(false)
const initVisibilityMap = (): void => {
visibilityMap.value = props.columns.reduce((acc, col: TableHeader) => {
acc[col.key] = props.visibilityPreferences[col.key] === undefined ? true : props.visibilityPreferences[col.key]
return acc
}, {} as Record<string, boolean>)
isDirty.value = false
}
const handleApply = (): void => {
// pass by ref problems
emit('update:visibility', JSON.parse(JSON.stringify(visibilityMap.value)))
isDirty.value = false
}
const handleDropdownToggle = (isOpen: boolean): void => {
// reset the map if the dropdown is closed without applying changes
if (!isOpen && isDirty.value) {
initVisibilityMap()
}
}
watch(() => props.visibilityPreferences, () => {
initVisibilityMap()
})
onBeforeMount(() => {
// initialize visibility state
initVisibilityMap()
})
</script>

<style lang="scss" scoped>
.table-column-visibility-menu {
margin-left: var(--kui-space-auto, $kui-space-auto);
.apply-button-wrapper {
display: flex;
width: 100%;
.apply-button {
margin-left: var(--kui-space-auto, $kui-space-auto);
margin-right: var(--kui-space-auto, $kui-space-auto);
}
}
.visibility-checkbox-label {
cursor: pointer;
margin-bottom: var(--kui-space-0, $kui-space-0);
margin-left: calc(-1 * var(--kui-space-40, $kui-space-40)); // because dropdown item container and checkbox both have default spacing, reduce it
}
}
</style>
31 changes: 31 additions & 0 deletions src/components/KTable/KTable.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,37 @@ describe('KTable', () => {
cy.get('.k-table').find('th.resizable').should('be.visible')
cy.get('.resize-handle').should('exist')
})

it('renders column show/hide when headers.hidable is set', () => {
// make ID column hidable
options.headers[1].hidable = true
const modifiedHeaderKey = options.headers[1].key

mount(KTable, {
props: {
testMode: 'true',
headers: options.headers,
fetcher: () => { return { data: options.data } },
},
})

cy.get('.k-table').should('be.visible')
// menu button is visible
cy.getTestId('column-visibility-menu-button').should('be.visible')
cy.getTestId('column-visibility-menu-button').click()

// only columns with hidable set to true should be visible and checked by default
cy.getTestId(`column-visibility-menu-item-${modifiedHeaderKey}`).should('be.visible')
cy.getTestId(`column-visibility-menu-item-${options.headers[0].key}`).should('not.exist')
cy.getTestId(`column-visibility-checkbox-${modifiedHeaderKey}`).should('be.visible')
cy.getTestId(`column-visibility-checkbox-${modifiedHeaderKey}`).should('be.checked')

// changes are applied only when Apply button is clicked
cy.getTestId(`column-visibility-checkbox-${modifiedHeaderKey}`).click()
cy.getTestId(`k-table-header-${modifiedHeaderKey}`).should('be.visible')
cy.getTestId('apply-button').click()
cy.getTestId(`k-table-header-${modifiedHeaderKey}`).should('not.exist')
})
})

describe('data revalidates and changes as expected', () => {
Expand Down
Loading

0 comments on commit 7a9d039

Please sign in to comment.