-
@@ -209,8 +211,8 @@ export default function MetaReviewClient() {
type="button"
variant="secondary"
className="mb-2"
- onClick={() => void loadRoles()}
- disabled={roles.status === 'loading'}
+ onPress={() => void loadRoles()}
+ isDisabled={roles.status === 'loading'}
>
{labels.loadRoles}
diff --git a/frontend/src/features/meta-review/ui/MetaReviewClientLoader.tsx b/frontend/src/features/meta-review/ui/MetaReviewClientLoader.tsx
index 68a4ce0..377ffe7 100644
--- a/frontend/src/features/meta-review/ui/MetaReviewClientLoader.tsx
+++ b/frontend/src/features/meta-review/ui/MetaReviewClientLoader.tsx
@@ -5,7 +5,11 @@ import dynamic from 'next/dynamic';
/** Client-only: BC DS `Button` (React Aria) emits unstable `id`s under SSR → hydration mismatch. */
const MetaReviewClient = dynamic(() => import('./MetaReviewClient'), {
ssr: false,
- loading: () =>
Loading…
,
+ loading: () => (
+
+ Loading…
+
+ ),
});
export default function MetaReviewClientLoader() {
diff --git a/frontend/src/features/submit-mode/ui/SubmissionList.tsx b/frontend/src/features/submit-mode/ui/SubmissionList.tsx
index 2f88d35..84395aa 100644
--- a/frontend/src/features/submit-mode/ui/SubmissionList.tsx
+++ b/frontend/src/features/submit-mode/ui/SubmissionList.tsx
@@ -1,6 +1,7 @@
'use client';
-import { useEffect, useState } from 'react';
+import { useEffect, useMemo, useState } from 'react';
+import { Container } from 'react-bootstrap';
import { useRouter, usePathname } from 'next/navigation';
import { useKeycloak } from '@/lib/hooks/useKeycloak';
import { useDictionary } from '@/app/[lang]/Providers';
@@ -22,9 +23,16 @@ export function SubmissionList({ formId }: SubmissionListProps = {}) {
const locale = getLocaleFromPath(pathname);
const [submissions, setSubmissions] = useState
([]);
const [isLoaded, setIsLoaded] = useState(false);
+ const [pageSize, setPageSize] = useState(10);
+ const [currentPage, setCurrentPage] = useState(1);
const { activeWorkspaceId } = useAppSelector((state) => state.workspace);
+ const paginatedSubmissions = useMemo(
+ () => submissions.slice((currentPage - 1) * pageSize, currentPage * pageSize),
+ [submissions, currentPage, pageSize],
+ );
+
useEffect(() => {
if (authenticated && token) {
const fetchSubmissions = async () => {
@@ -33,8 +41,8 @@ export function SubmissionList({ formId }: SubmissionListProps = {}) {
const params = formId ? { formId } : undefined;
const data = await getSobaSubmissions(token, params, activeWorkspaceId || undefined);
setSubmissions(data.items || []);
- } catch (err) {
- console.error('Failed to fetch submissions', err);
+ } catch {
+ // Submissions failed to load; the empty state is shown to the user.
} finally {
setIsLoaded(true);
}
@@ -67,11 +75,11 @@ export function SubmissionList({ formId }: SubmissionListProps = {}) {
e.preventDefault();
router.push(`/${locale}/submission/${sub.id}`);
}}
- className="text-decoration-underline"
+ className="text-decoration-underline font-monospace small"
style={{ cursor: 'pointer', color: '#00538A' }}
title={dict.submission?.view || 'View'}
>
- {sub.id}
+ {sub.id}
),
},
@@ -79,34 +87,26 @@ export function SubmissionList({ formId }: SubmissionListProps = {}) {
key: 'formName',
label: dict.submission?.columns?.formName || dict.form?.nameLabel || 'Form Name',
render: (sub) => (
-
- {sub.formName || dict.form?.nameLabel || 'Untitled Form'}
-
+ {sub.formName || dict.form?.nameLabel || 'Untitled Form'}
),
},
{
key: 'formId',
label: dict.submission?.columns?.formId || 'Form ID',
- render: (sub) => {sub.formId},
+ render: (sub) => {sub.formId},
},
{
key: 'versionNo',
label: dict.submission?.columns?.version || 'Version',
- render: (sub) => (
-
- v{sub.versionNo || 1}
-
- ),
+ render: (sub) => v{sub.versionNo || 1},
},
{
key: 'workflowState',
label: dict.submission?.columns?.status || 'Status',
render: (sub) => (
{sub.workflowState.toUpperCase()}
@@ -116,19 +116,27 @@ export function SubmissionList({ formId }: SubmissionListProps = {}) {
];
return (
-
+
-
{dict.submission?.submissions || 'Submissions'}
+ {dict.submission?.submissions || 'Submissions'}
- data={submissions}
+ data={paginatedSubmissions}
columns={columns}
loading={loading}
emptyMessage={dict.submission?.empty || 'No submissions found yet.'}
- loadingMessage={dict.form?.loading || 'Loading submissions...'}
+ loadingMessage={dict.submission?.loading || 'Loading submissions...'}
keyExtractor={(sub) => sub.id}
itemName={dict.submission?.submissions || 'submissions'}
+ totalItems={submissions.length}
+ pageSize={pageSize}
+ currentPage={currentPage}
+ onPageChange={setCurrentPage}
+ onPageSizeChange={(size) => {
+ setPageSize(size);
+ setCurrentPage(1);
+ }}
/>
-
+
);
}
diff --git a/frontend/src/features/submit-mode/ui/SubmissionView.tsx b/frontend/src/features/submit-mode/ui/SubmissionView.tsx
index df73b69..24d6c8b 100644
--- a/frontend/src/features/submit-mode/ui/SubmissionView.tsx
+++ b/frontend/src/features/submit-mode/ui/SubmissionView.tsx
@@ -3,12 +3,12 @@
import { useParams } from 'next/navigation';
import { useEffect, useState } from 'react';
import type { FormType, Submission } from '@formio/react';
-import { Alert, Spinner } from 'react-bootstrap';
+import { ProgressCircle, InlineAlert } from '@bcgov/design-system-react-components';
import { useKeycloak } from '@/lib/hooks/useKeycloak';
import { useDictionary } from '@/app/[lang]/Providers';
import { useAppSelector } from '@/lib/store';
import { ReadOnlyFormView } from '@/src/features/formio-v5/ui/ReadOnlyFormView';
-import { formatLongDate } from '@/src/shared/util/dateFormat';
+import { useFormatLongDate } from '@/src/shared/hooks/useFormatLongDate';
import {
getSobaSubmission,
getFormVersionSchema,
@@ -23,6 +23,7 @@ export function SubmissionView() {
const { authenticated, token, initializing } = useKeycloak();
const { activeWorkspaceId } = useAppSelector((state) => state.workspace);
const ws = activeWorkspaceId || undefined;
+ const formatLongDate = useFormatLongDate();
const submissionIdRaw = params?.submissionId;
const submissionId =
@@ -64,7 +65,7 @@ export function SubmissionView() {
if (initializing || (authenticated && !token)) {
return (
);
}
@@ -75,9 +76,9 @@ export function SubmissionView() {
{!loaded ? (
{dictSub?.loading || 'Loading…'}
) : notFound || !submission ? (
-
+
{dictSub?.notFound || 'Submission not found.'}
-
+
) : (
<>
@@ -106,9 +107,9 @@ export function SubmissionView() {
testId="submission-view-form"
/>
) : (
-
+
{dictSub?.noContent || 'No submitted answers to display.'}
-
+
)}
>
)}
diff --git a/frontend/src/shared/hooks/useFormatLongDate.ts b/frontend/src/shared/hooks/useFormatLongDate.ts
new file mode 100644
index 0000000..bdbbf93
--- /dev/null
+++ b/frontend/src/shared/hooks/useFormatLongDate.ts
@@ -0,0 +1,31 @@
+'use client';
+
+import { useCallback } from 'react';
+import { useLocale } from 'react-aria-components';
+
+/**
+ * Returns a long-date formatter, e.g. "May 25, 2026" (en) / "25 mai 2026" (fr).
+ *
+ * Locale comes from the nearest React Aria `
` (wired in the
+ * `[lang]` layout), so dates follow the active app locale instead of a
+ * hardcoded one. Empty or invalid input returns ''.
+ *
+ * The returned function is stable across renders (memoized on locale), so it is
+ * safe to use as a `useMemo`/`useCallback` dependency.
+ */
+export function useFormatLongDate() {
+ const { locale } = useLocale();
+ return useCallback(
+ (dateStr?: string | null): string => {
+ if (!dateStr) return '';
+ const date = new Date(dateStr);
+ if (isNaN(date.getTime())) return '';
+ return new Intl.DateTimeFormat(locale, {
+ month: 'long',
+ day: 'numeric',
+ year: 'numeric',
+ }).format(date);
+ },
+ [locale],
+ );
+}
diff --git a/frontend/src/shared/util/dateFormat.ts b/frontend/src/shared/util/dateFormat.ts
deleted file mode 100644
index 0d64e03..0000000
--- a/frontend/src/shared/util/dateFormat.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-/** Format an ISO date string as a long date, e.g. "May 25, 2026". Returns '' for empty input. */
-export function formatLongDate(dateStr?: string | null): string {
- if (!dateStr) return '';
- return new Intl.DateTimeFormat('en-US', {
- month: 'long',
- day: 'numeric',
- year: 'numeric',
- }).format(new Date(dateStr));
-}
diff --git a/frontend/tailwind.config.cjs b/frontend/tailwind.config.cjs
deleted file mode 100644
index 54fef57..0000000
--- a/frontend/tailwind.config.cjs
+++ /dev/null
@@ -1,16 +0,0 @@
-/** @type {import('tailwindcss').Config} */
-module.exports = {
- // Use class strategy so we can toggle dark mode via the `dark` class on
- darkMode: 'class',
- content: [
- './app/**/*.{js,ts,jsx,tsx}',
- './pages/**/*.{js,ts,jsx,tsx}',
- './components/**/*.{js,ts,jsx,tsx}',
- './lib/**/*.{js,ts,jsx,tsx}',
- './src/**/*.{js,ts,jsx,tsx}',
- ],
- theme: {
- extend: {},
- },
- plugins: [],
-};
diff --git a/frontend/tests/components/DataTable.test.tsx b/frontend/tests/components/DataTable.test.tsx
index 3c3c4d2..11043a7 100644
--- a/frontend/tests/components/DataTable.test.tsx
+++ b/frontend/tests/components/DataTable.test.tsx
@@ -1,5 +1,6 @@
import React from 'react';
-import { render, screen, fireEvent } from '@testing-library/react';
+import { render, screen, within } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
import { describe, it, expect, vi } from 'vitest';
import { DataTable } from '@/src/components/DataTable';
@@ -55,12 +56,16 @@ describe('DataTable', () => {
expect(screen.getByText('Alice')).toBeInTheDocument();
- const nextBtn = screen.getByTestId('datatable-next-page-button');
- fireEvent.click(nextBtn);
+ const user = userEvent.setup();
+
+ await user.click(screen.getByTestId('datatable-next-page-button'));
expect(onPageChange).toHaveBeenCalled();
+ // DS Select is a button + popup listbox (not a native