diff --git a/public/globals.js b/public/globals.js index 3aab16aff..0bc3d1468 100644 --- a/public/globals.js +++ b/public/globals.js @@ -20,6 +20,14 @@ window.pkp = { preferredName: 'Daniel Barnes', }, + /** + * + * + */ + context: { + apiBaseUrl: 'https://mock/index.php/publicknowledge/api/v1/', + }, + /** * Dummy constants required by components */ diff --git a/src/components/TableNext/Table.mdx b/src/components/TableNext/Table.mdx index dce889e9b..dec749af7 100644 --- a/src/components/TableNext/Table.mdx +++ b/src/components/TableNext/Table.mdx @@ -1,6 +1,8 @@ import {Primary, Controls, Stories, Meta, ArgTypes} from '@storybook/blocks'; import * as TableStories from './Table.stories.js'; +import TableColumn from './TableColumn.vue'; +import TableCell from './TableCell.vue'; @@ -8,85 +10,24 @@ import * as TableStories from './Table.stories.js'; ## Usage -WIP Some informations are inaccurate. +Use the `Table` component to display tabular data when the user will sort, search, filter, or if interactive elements such as a button appear within the table. -## Usage - -Use the `Table` component to display tabular data when the user will sort, search, filter or edit the rows in the table, or if interactive elements such as a button appear within the table. - -## Datagrid - -This component implements a [Data Grid](https://www.w3.org/TR/wai-aria-practices/examples/grid/dataGrids.html), which provides accessible keyboard controls and markup for managing the data in the table. All interactions in the table rows, including buttons or fields to edit a row, must be accessible by keyboard. - -## <TableCell> - -All cells in the table must use the `` component in order to support the accessible keyboard navigation features. Use a `` inside of a `` in the same way that a `` element would be used. - -```html - - - dbarnes - Daniel Barnes - - - sminotue - Stephanie Minotue - - -``` - -When writing a `` in a Smarty template, you must use a `` with with the `is` attribute. +## Accessibility -```html - - - dbarnes - Daniel Barnes - - - sminotue - Stephanie Minotue - - -``` +Table component requires aria-label attribute to describe content of table. -This is because templates written in Smarty are parsed by the browser before they are parsed by Vue. Learn more about Vue's [DOM Template Parsing Caveats](https://v2.vuejs.org/v2/guide/components.html#DOM-Template-Parsing-Caveats). +One column should be [rowheader](https://www.w3.org/TR/wai-aria-1.1/#rowheader) to improve screen reader experience. Its prop on TableCell, not on TableColumn as one would expect as it allows for significantly easier implementation. -## Sorting +## Table Props -When a table can be sorted, you must [announce the changes](#/pages/announcer). When a sort is performed by making a request to the server, announce at both the start and end of the process. - -```js -methods: { - sort(col) { - this.$announcer.set('Loading'); - $.ajax({ - - // ... - - success(r) { - - // ... - - this.$announcer.set('Sorted by ' + col); - } - }); - } -} -``` + -## Accessible Caption +## TableColumn Props -Every table needs an accessible caption. Use the `caption` slot to provide a title with the correct `` heading according to the page hierarchy. The description is optional. + -```html - - -

Example Table

-
-
-``` +# TableCell Props -If you do not use the `caption` slot, you must use the `labelledBy` prop to provide an accessible label for the table. See the [Labelled By](#/component/Table/with-labelledby) example. + - + diff --git a/src/components/TableNext/Table.stories.js b/src/components/TableNext/Table.stories.js index 108c85342..9ec8952bc 100644 --- a/src/components/TableNext/Table.stories.js +++ b/src/components/TableNext/Table.stories.js @@ -1,12 +1,20 @@ -import {ref, computed} from 'vue'; +import {ref, watch} from 'vue'; import PkpTable from './Table.vue'; -import TableCell from './TableCell.vue'; import TableHeader from './TableHeader.vue'; +import TableBody from './TableBody.vue'; +import TableColumn from './TableColumn.vue'; +import TableCell from './TableCell.vue'; +import TableRow from './TableRow.vue'; import ButtonRow from '@/components/ButtonRow/ButtonRow.vue'; import Pagination from '@/components/Pagination/Pagination.vue'; +import {http, HttpResponse} from 'msw'; import articleStats from '@/components/Table/mocks/articleStats.js'; import {useSorting} from '@/composables/useSorting'; + +import {useFetchPaginated} from '@/composables/useFetchPaginated'; +import {useApiUrl} from '@/composables/useApiUrl'; + export default { title: 'Components/Table', component: PkpTable, @@ -14,37 +22,41 @@ export default { export const Default = { render: (args) => ({ - components: {PkpTable, TableCell, TableHeader}, + components: { + PkpTable, + TableHeader, + TableBody, + TableRow, + TableColumn, + TableCell, + }, setup() { const rows = articleStats.slice(0, 10); return {args, rows}; }, template: ` - - - - - {{ row.object.id }} - - {{ row.object.fullTitle.en }} - - {{ row.views }} - {{ row.downloads }} - - - - + + + ID + Title + Views + Downloads + Total + + + + {{ row.object.id }} + + {{ row.object.fullTitle.en }} + + {{ row.views }} + {{ row.downloads }} + + + + + `, }), @@ -54,56 +66,58 @@ export const Default = { export const WithSorting = { render: (args) => ({ - components: {PkpTable, TableCell, TableHeader}, + components: { + PkpTable, + TableHeader, + TableBody, + TableRow, + TableColumn, + TableCell, + }, setup() { const rows = articleStats.slice(0, 10); - const {sortDirection, sortColumnId, applySort} = useSorting(); + const {sortDescriptor, applySort} = useSorting(); - return {sortDirection, sortColumnId, applySort, args, rows}; + return {sortDescriptor, applySort, args, rows}; }, template: ` - - - - - {{ row.object.id }} - - {{ row.object.fullTitle.en }} - - {{ row.views }} - {{ row.downloads }} - - - - + + + + + {{ row.object.id }} + + {{ row.object.fullTitle.en }} + + {{ row.views }} + {{ row.downloads }} + + + + + `, }), @@ -113,71 +127,94 @@ export const WithSorting = { export const WithPagination = { render: (args) => ({ - components: {PkpTable, TableCell, TableHeader, ButtonRow, Pagination}, + components: { + PkpTable, + TableHeader, + TableBody, + TableRow, + TableColumn, + TableCell, + ButtonRow, + Pagination, + }, setup() { + const {apiUrl: statsApiUrl} = useApiUrl('stats'); + + const pageSize = ref(10); const currentPage = ref(1); - const isLoading = ref(false); - const perPage = ref(10); - const rows = [...articleStats]; - const lastPage = computed(() => { - return Math.floor(rows.length / perPage.value); + const {items, pagination, fetch} = useFetchPaginated(statsApiUrl, { + currentPage, + pageSize, }); - const currentRows = computed(() => { - const start = currentPage.value * perPage.value - perPage.value; - return rows.slice(start, start + perPage.value); - }); + fetch(); + // reload after changing currentPage + watch(async (currentPage) => await fetch()); function setPage(page) { currentPage.value = page; } return { - rows, - perPage, - isLoading, + items, currentPage, - lastPage, - currentRows, + pagination, setPage, }; }, template: ` - - - - - {{ row.object.id }} - - {{ row.object.fullTitle.en }} - - {{ row.views }} - {{ row.downloads }} - - - - + + + ID + Title + Views + Downloads + Total + + + + {{ row.object.id }} + + {{ row.object.fullTitle.en }} + + {{ row.views }} + {{ row.downloads }} + + + + + `, }), + parameters: { + msw: { + handlers: [ + http.get( + 'https://mock/index.php/publicknowledge/api/v1/stats', + ({request}) => { + const url = new URL(request.url); + const offset = parseInt(url.searchParams.get('offset') || 0); + const count = parseInt(url.searchParams.get('count')); + const stats = articleStats.slice(offset, offset + count); + + return HttpResponse.json({ + itemsMax: articleStats.length, + items: stats, + }); + }, + ), + ], + }, + }, args: {}, }; diff --git a/src/components/TableNext/Table.vue b/src/components/TableNext/Table.vue index 8560b2a0f..413771c20 100644 --- a/src/components/TableNext/Table.vue +++ b/src/components/TableNext/Table.vue @@ -1,19 +1,42 @@ + + diff --git a/src/components/TableNext/TableHeader.vue b/src/components/TableNext/TableHeader.vue index 37210676c..ae384e720 100644 --- a/src/components/TableNext/TableHeader.vue +++ b/src/components/TableNext/TableHeader.vue @@ -1,108 +1,5 @@ - - - - diff --git a/src/components/TableNext/TableRow.vue b/src/components/TableNext/TableRow.vue new file mode 100644 index 000000000..1625d069a --- /dev/null +++ b/src/components/TableNext/TableRow.vue @@ -0,0 +1,9 @@ + + + diff --git a/src/composables/useFetchPaginated.js b/src/composables/useFetchPaginated.js index a88e415f9..d0bb2bff6 100644 --- a/src/composables/useFetchPaginated.js +++ b/src/composables/useFetchPaginated.js @@ -10,7 +10,7 @@ import {useFetch} from './useFetch'; * @function useFetchPaginated * @param {string} url - The URL to which the HTTP request is to be sent. This should be the endpoint for the paginated data. * @param {Object} options - Configuration options, check useFetch.js for more options. - * @param {number} options.page - The current page number. This is normalized internally to a reactive ref. + * @param {number} options.currentPage - The current page number. This is normalized internally to a reactive ref. * @param {number} options.pageSize - The number of items per page. This is also normalized to a reactive ref. * * @returns {Object} An object containing several reactive properties and a method for performing the fetch operation: @@ -22,20 +22,20 @@ import {useFetch} from './useFetch'; */ export function useFetchPaginated(url, options) { const { - page: _page, + currentPage: _currentPage, pageSize: _pageSize, query: _query, ...useFetchOpts } = options; // normalise to make these options reactive if they are not already - const page = ref(_page); + const currentPage = ref(_currentPage); const pageSize = ref(_pageSize); const query = ref(_query || {}); // add offset and count to query params const offset = computed(() => { - return (page.value - 1) * pageSize.value; + return (currentPage.value - 1) * pageSize.value; }); const useFetchQuery = computed(() => { return {...query.value, offset: offset.value, count: pageSize.value}; @@ -57,7 +57,7 @@ export function useFetchPaginated(url, options) { ); const pageCount = Math.ceil(itemCount.value / pageSize.value); return { - page: page.value, + currentPage: currentPage.value, pageSize: pageSize.value, pageCount, firstItemIndex, diff --git a/src/composables/useFetchPaginated.test.js b/src/composables/useFetchPaginated.test.js index 451cd9619..c9c0e57e5 100644 --- a/src/composables/useFetchPaginated.test.js +++ b/src/composables/useFetchPaginated.test.js @@ -64,22 +64,22 @@ beforeEach(() => { describe('typical uses', () => { test('GET 200 request', async () => { const url = ref('http://mock/get/paginated200'); - const page = ref(1); + const currentPage = ref(1); const pageSize = ref(5); const {items, pagination, isLoading, fetch} = useFetchPaginated(url, { query: {param1: 4, param2: 5}, - page, + currentPage, pageSize, }); expect(isLoading.value).toBe(false); expect(pagination.value).toMatchInlineSnapshot(` { + "currentPage": 1, "firstItemIndex": 0, "itemCount": 0, "lastItemIndex": 0, "offset": 0, - "page": 1, "pageCount": 0, "pageSize": 5, } @@ -109,17 +109,17 @@ describe('typical uses', () => { `); expect(pagination.value).toMatchInlineSnapshot(` { + "currentPage": 1, "firstItemIndex": 1, "itemCount": 11, "lastItemIndex": 5, "offset": 0, - "page": 1, "pageCount": 3, "pageSize": 5, } `); - page.value = 2; + currentPage.value = 2; pageSize.value = 3; await fetch(); @@ -139,11 +139,11 @@ describe('typical uses', () => { `); expect(pagination.value).toMatchInlineSnapshot(` { + "currentPage": 2, "firstItemIndex": 4, "itemCount": 11, "lastItemIndex": 6, "offset": 3, - "page": 2, "pageCount": 4, "pageSize": 3, } diff --git a/src/composables/useSorting.js b/src/composables/useSorting.js index e1eea6cc6..a2f319d1f 100644 --- a/src/composables/useSorting.js +++ b/src/composables/useSorting.js @@ -3,35 +3,40 @@ import {ref, computed} from 'vue'; const sortDirections = ['descending', 'ascending', 'none']; export function useSorting() { - const sortDirection = ref(''); - const sortColumnId = ref(''); + const sortDescriptor = ref({column: '', direction: 'none'}); const sortQueryParamsApi = computed(() => { - if (sortColumnId.value && sortDirection.value !== 'none') { + if ( + sortDescriptor.value?.column && + sortDescriptor.value?.direction !== 'none' + ) { return { - orderBy: sortColumnId.value, - orderDirection: sortDirection.value === 'descending' ? 'DESC' : 'ASC', + orderBy: sortDescriptor.value.column, + orderDirection: + sortDescriptor.value.direction === 'descending' ? 'DESC' : 'ASC', }; } return {}; }); function applySort(columnId) { - if (columnId === sortColumnId.value) { - const i = sortDirections.findIndex((dir) => dir === sortDirection.value); - sortDirection.value = + console.log('hi'); + if (columnId === sortDescriptor.value.column) { + const i = sortDirections.findIndex( + (dir) => dir === sortDescriptor.value.direction, + ); + sortDescriptor.value.direction = i + 1 === sortDirections.length ? sortDirections[0] : sortDirections[i + 1]; + console.log('sortDescriptor:', sortDescriptor.value); } else { - sortColumnId.value = columnId; - sortDirection.value = sortDirections[0]; + sortDescriptor.value = {column: columnId, direction: sortDirections[0]}; } } return { - sortDirection, - sortColumnId, + sortDescriptor, sortQueryParamsApi, applySort, }; diff --git a/src/pages/submissions/ColumnActions.vue b/src/pages/submissions/ColumnActions.vue index fbd98458c..584757971 100644 --- a/src/pages/submissions/ColumnActions.vue +++ b/src/pages/submissions/ColumnActions.vue @@ -1,7 +1,7 @@ - diff --git a/src/pages/submissions/ColumnId.vue b/src/pages/submissions/ColumnId.vue index 27c8b6fb3..83810fc56 100644 --- a/src/pages/submissions/ColumnId.vue +++ b/src/pages/submissions/ColumnId.vue @@ -2,15 +2,11 @@ {{ submission.id }} - diff --git a/src/pages/submissions/ColumnStage.vue b/src/pages/submissions/ColumnStage.vue index 8763b475a..7e5896eaa 100644 --- a/src/pages/submissions/ColumnStage.vue +++ b/src/pages/submissions/ColumnStage.vue @@ -1,43 +1,39 @@ - diff --git a/src/pages/submissions/ColumnTitle.vue b/src/pages/submissions/ColumnTitle.vue index e0001c28f..540c1cab8 100644 --- a/src/pages/submissions/ColumnTitle.vue +++ b/src/pages/submissions/ColumnTitle.vue @@ -1,10 +1,10 @@ - diff --git a/src/pages/submissions/SubmissionsPage.stories.js b/src/pages/submissions/SubmissionsPage.stories.js index de0fa7c02..77cfc21f5 100644 --- a/src/pages/submissions/SubmissionsPage.stories.js +++ b/src/pages/submissions/SubmissionsPage.stories.js @@ -5,7 +5,7 @@ import PageInitConfigMock from './mocks/pageInitConfig'; export default {title: 'Pages/Submissions', component: SubmissionsPage}; -export const init = { +export const Init = { render: (args) => ({ components: {SubmissionsPage}, setup() { @@ -22,6 +22,12 @@ export const init = { return HttpResponse.json(SubmissionsMock25); }, ), + http.get( + 'https://mock/index.php/publicknowledge/api/v1/_submissions/assigned', + () => { + return HttpResponse.json(SubmissionsMock25); + }, + ), ], }, }, diff --git a/src/pages/submissions/SubmissionsPage.vue b/src/pages/submissions/SubmissionsPage.vue index 9115cbe4a..414886e50 100644 --- a/src/pages/submissions/SubmissionsPage.vue +++ b/src/pages/submissions/SubmissionsPage.vue @@ -1,5 +1,5 @@