From 15dbe75e034c1c105a642afce8278fd63dd15381 Mon Sep 17 00:00:00 2001 From: Tim deBoer Date: Thu, 16 Nov 2023 11:00:34 -0500 Subject: [PATCH] feat: table component This is a clean branch of the table component after previous review comments in PR #4545. Each page passes an array of objects and Columns to the Table component, which is responsible for creating the basic layout and calling back to the Column renderer to render each cell in the table. The Table uses grid layout and provides enough abstraction that we can implement additional features later. (e.g. optional columns, column reordering, or resizing. grouping. etc) The Table provides sorting & sort indicators, and tests are included. Support for 'object containers/grouping' in order to support Container list will be done separately since this is already a big PR. Fixes #4365. Signed-off-by: Tim deBoer --- packages/renderer/src/lib/table/Table.spec.ts | 87 +++++++++ packages/renderer/src/lib/table/Table.svelte | 165 ++++++++++++++++++ .../src/lib/table/TestColumnAge.svelte | 5 + .../src/lib/table/TestColumnName.svelte | 5 + .../renderer/src/lib/table/TestTable.svelte | 40 +++++ packages/renderer/src/lib/table/table.ts | 83 +++++++++ 6 files changed, 385 insertions(+) create mode 100644 packages/renderer/src/lib/table/Table.spec.ts create mode 100644 packages/renderer/src/lib/table/Table.svelte create mode 100644 packages/renderer/src/lib/table/TestColumnAge.svelte create mode 100644 packages/renderer/src/lib/table/TestColumnName.svelte create mode 100644 packages/renderer/src/lib/table/TestTable.svelte create mode 100644 packages/renderer/src/lib/table/table.ts diff --git a/packages/renderer/src/lib/table/Table.spec.ts b/packages/renderer/src/lib/table/Table.spec.ts new file mode 100644 index 000000000000..b9770728d51c --- /dev/null +++ b/packages/renderer/src/lib/table/Table.spec.ts @@ -0,0 +1,87 @@ +/********************************************************************** + * Copyright (C) 2023 Red Hat, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ***********************************************************************/ + +import '@testing-library/jest-dom/vitest'; +import { test, expect } from 'vitest'; +import { fireEvent, render, screen } from '@testing-library/svelte'; + +import TestTable from './TestTable.svelte'; + +test('Expect basic table layout', async () => { + // render the component + render(TestTable, {}); + + // 3 people = header + 3 rows + const rows = await screen.findAllByRole('row'); + expect(rows).toBeDefined(); + expect(rows.length).toBe(4); + + // first data row should contain John and his age + expect(rows[1].textContent).toContain('John'); + expect(rows[1].textContent).toContain('57'); + + // second data row should contain Henry and his age + expect(rows[2].textContent).toContain('Henry'); + expect(rows[2].textContent).toContain('27'); + + // last data row should contain Charlie and his age + expect(rows[3].textContent).toContain('Charlie'); + expect(rows[3].textContent).toContain('43'); +}); + +test('Expect sorting by name works', async () => { + render(TestTable, {}); + + const nameCol = screen.getByText('Name'); + expect(nameCol).toBeInTheDocument(); + + let rows = await screen.findAllByRole('row'); + expect(rows).toBeDefined(); + expect(rows.length).toBe(4); + expect(rows[1].textContent).toContain('John'); + expect(rows[2].textContent).toContain('Henry'); + expect(rows[3].textContent).toContain('Charlie'); + + await fireEvent.click(nameCol); + + rows = await screen.findAllByRole('row'); + expect(rows[1].textContent).toContain('Charlie'); + expect(rows[2].textContent).toContain('Henry'); + expect(rows[3].textContent).toContain('John'); +}); + +test('Expect sorting by age works', async () => { + render(TestTable, {}); + + const ageCol = screen.getByText('Age'); + expect(ageCol).toBeInTheDocument(); + + let rows = await screen.findAllByRole('row'); + expect(rows).toBeDefined(); + expect(rows.length).toBe(4); + expect(rows[1].textContent).toContain('John'); + expect(rows[2].textContent).toContain('Henry'); + expect(rows[3].textContent).toContain('Charlie'); + + await fireEvent.click(ageCol); + + rows = await screen.findAllByRole('row'); + expect(rows[1].textContent).toContain('Henry'); + expect(rows[2].textContent).toContain('Charlie'); + expect(rows[3].textContent).toContain('John'); +}); diff --git a/packages/renderer/src/lib/table/Table.svelte b/packages/renderer/src/lib/table/Table.svelte new file mode 100644 index 000000000000..a746a4426b13 --- /dev/null +++ b/packages/renderer/src/lib/table/Table.svelte @@ -0,0 +1,165 @@ + + + + +
+ +
+
+
+ {#if row.selectable} + + {/if} +
+ {#each columns as column} + + +
+ {column.title}{#if column.comparator}{/if} +
+ {/each} +
+ + {#each data as object (object)} +
+
+
+ {#if row.selectable} + + {/if} +
+ {#each columns as column} +
+ {#if column.info.renderer} + + {/if} +
+ {/each} +
+ {/each} +
diff --git a/packages/renderer/src/lib/table/TestColumnAge.svelte b/packages/renderer/src/lib/table/TestColumnAge.svelte new file mode 100644 index 000000000000..dc71cbc7635a --- /dev/null +++ b/packages/renderer/src/lib/table/TestColumnAge.svelte @@ -0,0 +1,5 @@ + + +{object.age} diff --git a/packages/renderer/src/lib/table/TestColumnName.svelte b/packages/renderer/src/lib/table/TestColumnName.svelte new file mode 100644 index 000000000000..4fd618eddc59 --- /dev/null +++ b/packages/renderer/src/lib/table/TestColumnName.svelte @@ -0,0 +1,5 @@ + + +{object.name} diff --git a/packages/renderer/src/lib/table/TestTable.svelte b/packages/renderer/src/lib/table/TestTable.svelte new file mode 100644 index 000000000000..62d5281c3eee --- /dev/null +++ b/packages/renderer/src/lib/table/TestTable.svelte @@ -0,0 +1,40 @@ + + + +
diff --git a/packages/renderer/src/lib/table/table.ts b/packages/renderer/src/lib/table/table.ts new file mode 100644 index 000000000000..78480245081a --- /dev/null +++ b/packages/renderer/src/lib/table/table.ts @@ -0,0 +1,83 @@ +/********************************************************************** + * Copyright (C) 2023 Red Hat, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + ***********************************************************************/ + +/** + * Options to be used when creating a Column. + */ +export interface ColumnInformation { + /** + * Column alignment, one of 'left', 'center', or 'right'. + * + * Defaults to 'left' alignment. + */ + readonly align?: 'left' | 'center' | 'right'; + + /** + * Column width, typically in pixels or fractional units (fr). + * + * Defaults to '1fr'. + */ + readonly width?: string; + + /** + * Svelte component, renderer for each cell in the column. + * The component must have a property 'object' that has the + * same type as the Column. + */ + readonly renderer?: any; +} + +/** + * A table Column. + */ +export class Column { + comparator: ((object1: Type, object2: Type) => number) | undefined; + + constructor( + readonly title: string, + readonly info: ColumnInformation, + ) {} + + /** + * Set a comparator used to sort the data by the values in this column. + * + * @param comparator + */ + setComparator(comparator: (object1: Type, object2: Type) => number) { + this.comparator = comparator; + } +} + +/** + * A table row. + */ +export class Row { + selectable?: (object: Type) => boolean; + disabledText?: string; + + /** + * Set a function to be used to determine which objects in the data can be selected. + * + * @param selectable a function that returns false when the object should not be selectable + * @param disabledText text to display as a tooltip when selection is disabled + */ + setSelectable(selectable: (object: Type) => boolean, disabledText: string) { + this.selectable = selectable; + this.disabledText = disabledText; + } +}