From 1297c42222ff0a7148632f6bfdf3b34b9c11152f Mon Sep 17 00:00:00 2001 From: usingtechnology <39388115+usingtechnology@users.noreply.github.com> Date: Sun, 14 Jun 2026 09:07:38 -0700 Subject: [PATCH 1/7] chore: CCP-4889 accessibility, removal of dead code, UX consistency --- .../app/[lang]/designer/[...formId]/page.tsx | 7 ++- frontend/app/[lang]/designer/page.tsx | 8 ++- frontend/app/[lang]/feedback/page.tsx | 12 ++++- frontend/app/[lang]/forms/page.tsx | 4 +- frontend/app/[lang]/help/page.tsx | 12 ++++- frontend/app/ui/Header.module.css | 7 +++ frontend/app/ui/Header.tsx | 2 +- frontend/dictionaries/en.json | 5 +- frontend/dictionaries/fr.json | 5 +- frontend/eslint.config.mts | 51 ------------------- frontend/lib/constants.ts | 8 --- frontend/lib/runtimeConfig.ts | 1 - frontend/lib/slices/keycloakSlice.ts | 2 +- frontend/lib/sobaApi.ts | 1 - frontend/src/app/providers/AppProviders.tsx | 11 +++- frontend/src/components/DataTable.tsx | 10 ++-- .../src/features/designer/ui/FormForm.tsx | 15 ++++-- .../src/features/designer/ui/FormList.tsx | 30 ++++++----- .../submit-mode/ui/SubmissionList.tsx | 25 ++++----- frontend/src/shared/config/runtimeConfig.ts | 4 -- .../tests/features/designer/FormList.test.tsx | 4 +- frontend/tests/lib/constants.test.ts | 24 --------- 22 files changed, 109 insertions(+), 139 deletions(-) delete mode 100644 frontend/eslint.config.mts delete mode 100644 frontend/lib/constants.ts delete mode 100644 frontend/lib/runtimeConfig.ts delete mode 100644 frontend/lib/sobaApi.ts delete mode 100644 frontend/tests/lib/constants.test.ts diff --git a/frontend/app/[lang]/designer/[...formId]/page.tsx b/frontend/app/[lang]/designer/[...formId]/page.tsx index 9f1398e..ccc7fac 100644 --- a/frontend/app/[lang]/designer/[...formId]/page.tsx +++ b/frontend/app/[lang]/designer/[...formId]/page.tsx @@ -1,5 +1,6 @@ import { getDictionary, hasLocale, Locale } from '../../dictionaries'; import FormDesignerLoader from '@/src/features/designer/ui/FormDesignerLoader'; +import { DsPageHeading } from '@/app/ui/DsPageHeading'; import { notFound } from 'next/navigation'; import { loadFeaturesMeta } from '@/src/shared/config/featuresMeta'; import { createIsFeatureAllowed, FEATURE_CODES } from '@/src/shared/featureFlags/flags'; @@ -28,10 +29,14 @@ export default async function Page({ params }: PageProps) { notFound(); } - const { formId } = await params; + const { lang, formId } = await params; + const dict = await getDictionary((hasLocale(lang) ? lang : 'en') as Locale); return (
+ + {dict.general.formDesigner} +
); diff --git a/frontend/app/[lang]/designer/page.tsx b/frontend/app/[lang]/designer/page.tsx index 733d464..59a0fdf 100644 --- a/frontend/app/[lang]/designer/page.tsx +++ b/frontend/app/[lang]/designer/page.tsx @@ -1,5 +1,6 @@ import { getDictionary, hasLocale, Locale } from '../dictionaries'; import FormDesignerLoader from '@/src/features/designer/ui/FormDesignerLoader'; +import { DsPageHeading } from '@/app/ui/DsPageHeading'; import { notFound } from 'next/navigation'; import { loadFeaturesMeta } from '@/src/shared/config/featuresMeta'; import { createIsFeatureAllowed, FEATURE_CODES } from '@/src/shared/featureFlags/flags'; @@ -20,7 +21,6 @@ export async function generateMetadata({ params }: PageProps) { }; } -// eslint-disable-next-line @typescript-eslint/no-unused-vars export default async function Page({ params }: PageProps) { const featuresMeta = await loadFeaturesMeta(); const isFeatureAllowed = createIsFeatureAllowed(featuresMeta); @@ -28,8 +28,14 @@ export default async function Page({ params }: PageProps) { notFound(); } + const { lang } = await params; + const dict = await getDictionary((hasLocale(lang) ? lang : 'en') as Locale); + return (
+ + {dict.general.formDesigner} +
); diff --git a/frontend/app/[lang]/feedback/page.tsx b/frontend/app/[lang]/feedback/page.tsx index 57472e4..6e8a7f5 100644 --- a/frontend/app/[lang]/feedback/page.tsx +++ b/frontend/app/[lang]/feedback/page.tsx @@ -1,4 +1,5 @@ import { getDictionary, resolveLocale } from '../dictionaries'; +import { DsPageHeading } from '@/app/ui/DsPageHeading'; type PageProps = { params: Promise<{ lang: string }>; @@ -8,7 +9,7 @@ export async function generateMetadata({ params }: PageProps) { const locale = resolveLocale(param.lang); const dict = await getDictionary(locale); return { - title: `${dict.general.title}`, + title: `${dict.general.feedback} | ${dict.general.title}`, description: dict.general.description, }; } @@ -18,5 +19,12 @@ export default async function Page({ params }: PageProps) { const locale = resolveLocale(param.lang); const dict = await getDictionary(locale); - return
{dict.general.feedback}
; + return ( +
+ {dict.general.feedback} +

+ {dict.general.comingSoon} +

+
+ ); } diff --git a/frontend/app/[lang]/forms/page.tsx b/frontend/app/[lang]/forms/page.tsx index 220898a..62236b7 100644 --- a/frontend/app/[lang]/forms/page.tsx +++ b/frontend/app/[lang]/forms/page.tsx @@ -26,12 +26,12 @@ export default async function Page({ params }: PageProps) { const locale = resolveLocale(param.lang); return ( -
+
-
+
); } diff --git a/frontend/app/[lang]/help/page.tsx b/frontend/app/[lang]/help/page.tsx index e44580f..3ba84c6 100644 --- a/frontend/app/[lang]/help/page.tsx +++ b/frontend/app/[lang]/help/page.tsx @@ -1,4 +1,5 @@ import { getDictionary, resolveLocale } from '../dictionaries'; +import { DsPageHeading } from '@/app/ui/DsPageHeading'; type PageProps = { params: Promise<{ lang: string }>; @@ -8,7 +9,7 @@ export async function generateMetadata({ params }: PageProps) { const locale = resolveLocale(param.lang); const dict = await getDictionary(locale); return { - title: `${dict.general.title}`, + title: `${dict.general.help} | ${dict.general.title}`, description: dict.general.description, }; } @@ -18,5 +19,12 @@ export default async function Page({ params }: PageProps) { const locale = resolveLocale(param.lang); const dict = await getDictionary(locale); - return
{dict.general.help}
; + return ( +
+ {dict.general.help} +

+ {dict.general.comingSoon} +

+
+ ); } diff --git a/frontend/app/ui/Header.module.css b/frontend/app/ui/Header.module.css index f2dda33..c07a240 100644 --- a/frontend/app/ui/Header.module.css +++ b/frontend/app/ui/Header.module.css @@ -29,6 +29,13 @@ border-color: var(--app-border) !important; } +.userDrop:focus-visible { + /* Keyboard focus needs a clearly visible ring; the surface-pinning rules + above otherwise leave focus indistinguishable from the resting state. */ + outline: 2px solid var(--app-focus-ring) !important; + outline-offset: 2px; +} + .limitText { max-width: 100px; /* overflow: hidden; */ diff --git a/frontend/app/ui/Header.tsx b/frontend/app/ui/Header.tsx index 0d5c62e..a012fad 100644 --- a/frontend/app/ui/Header.tsx +++ b/frontend/app/ui/Header.tsx @@ -132,7 +132,7 @@ function Header({ headerNavItems }: HeaderProps) { {displayName ? ( - + diff --git a/frontend/dictionaries/en.json b/frontend/dictionaries/en.json index ddd62a7..73a1f17 100644 --- a/frontend/dictionaries/en.json +++ b/frontend/dictionaries/en.json @@ -8,10 +8,12 @@ "description": "Next Gen Forms", "notAuthenticated": "You are not authenticated. Please login to access this section.", "forms": "Forms", + "formDesigner": "Form Designer", "feedback": "Feedback", "help": "Help", "welcomeTo": "Welcome to", - "loading": "Loading…" + "loading": "Loading…", + "comingSoon": "This feature is coming soon." }, "header": { "workspaces": "Workspaces", @@ -120,6 +122,7 @@ "visibilityAzureIDIR": "IDIR - MFA", "disclaimerLabel": "I agree to the disclaimer and statement of responsibility", "disclaimerTitle": "Disclaimer", + "viewDisclaimer": "View disclaimer", "disclaimerText1": "It is your responsibility to comply with Privacy laws governing the collection, use and disclosure of personally identifiable information.", "disclaimerText2": "Access to this form designer tool does not inherently grant permission to collect, use or disclose any personally identifiable information.", "disclaimerText3": "It is your responsibility to obtain consent to collect information as required by law.", diff --git a/frontend/dictionaries/fr.json b/frontend/dictionaries/fr.json index a08b419..98abc82 100644 --- a/frontend/dictionaries/fr.json +++ b/frontend/dictionaries/fr.json @@ -8,10 +8,12 @@ "description": "Formulaires de Nouvelle Génération", "notAuthenticated": "Vous n'êtes pas authentifié. Veuillez vous connecter pour accéder à cette section.", "forms": "Formulaires", + "formDesigner": "Concepteur de formulaires", "feedback": "Retour d'information", "help": "Aidé", "welcomeTo": "Bienvenue sur", - "loading": "Chargement…" + "loading": "Chargement…", + "comingSoon": "Cette fonctionnalité sera bientôt disponible." }, "header": { "workspaces": "Espaces de travail", @@ -120,6 +122,7 @@ "visibilityAzureIDIR": "IDIR - MFA", "disclaimerLabel": "J'accepte l'avis de non-responsabilité et la déclaration de responsabilité", "disclaimerTitle": "Avis de non-responsabilité", + "viewDisclaimer": "Voir l'avis de non-responsabilité", "disclaimerText1": "Il est de votre responsabilité de vous conformer aux lois sur la protection de la vie privée régissant la collecte, l'utilisation et la divulgation d'informations personnellement identifiables.", "disclaimerText2": "L'accès à cet outil de conception de formulaires n'accorde pas intrinsèquement l'autorisation de collecter, d'utiliser ou de divulguer des informations personnellement identifiables.", "disclaimerText3": "Il est de votre responsabilité d'obtenir le consentement pour recueillir des informations comme l'exige la loi.", diff --git a/frontend/eslint.config.mts b/frontend/eslint.config.mts deleted file mode 100644 index 6c2f3e2..0000000 --- a/frontend/eslint.config.mts +++ /dev/null @@ -1,51 +0,0 @@ -import js from '@eslint/js'; -import globals from 'globals'; -import tseslint from 'typescript-eslint'; -import react from 'eslint-plugin-react'; -import reactHooks from 'eslint-plugin-react-hooks'; -import reactRefresh from 'eslint-plugin-react-refresh'; -import prettier from 'eslint-plugin-prettier'; -import { defineConfig, globalIgnores } from 'eslint/config'; - -export default defineConfig([ - globalIgnores(['dist', 'node_modules']), - - { - files: ['**/*.{ts,tsx,js,jsx}'], - - languageOptions: { - ecmaVersion: 2020, - sourceType: 'module', - globals: { - ...globals.browser, - ...globals.node, - }, - }, - - plugins: { - react, - 'react-refresh': reactRefresh, - prettier, - }, - - extends: [ - js.configs.recommended, - tseslint.configs.recommended, - react.configs.flat.recommended, - reactHooks.configs.flat.recommended, - reactRefresh.configs.vite, - 'plugin:prettier/recommended', - ], - - rules: { - 'prettier/prettier': 'error', - 'react/react-in-jsx-scope': 'off', - }, - - settings: { - react: { - version: 'detect', - }, - }, - }, -]); diff --git a/frontend/lib/constants.ts b/frontend/lib/constants.ts deleted file mode 100644 index e0b8946..0000000 --- a/frontend/lib/constants.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type { NotificationType } from './slices/notificationSlice'; - -export const NotificationTypes: Record = { - INFO: { type: 'info' }, - SUCCESS: { type: 'success' }, - WARNING: { type: 'warning' }, - ERROR: { type: 'error' }, -}; diff --git a/frontend/lib/runtimeConfig.ts b/frontend/lib/runtimeConfig.ts deleted file mode 100644 index ab487a4..0000000 --- a/frontend/lib/runtimeConfig.ts +++ /dev/null @@ -1 +0,0 @@ -export * from '@/src/shared/config/runtimeConfig'; diff --git a/frontend/lib/slices/keycloakSlice.ts b/frontend/lib/slices/keycloakSlice.ts index 817f0b7..95020c7 100644 --- a/frontend/lib/slices/keycloakSlice.ts +++ b/frontend/lib/slices/keycloakSlice.ts @@ -2,7 +2,7 @@ import Keycloak from 'keycloak-js'; import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'; import type { PayloadAction } from '@reduxjs/toolkit'; import type { AppDispatch } from '../store'; -import { loadFrontendRuntimeConfig } from '../runtimeConfig'; +import { loadFrontendRuntimeConfig } from '@/src/shared/config/runtimeConfig'; // Keep the Keycloak instance out of Redux state (non-serializable). // Store it in a module-level variable instead. diff --git a/frontend/lib/sobaApi.ts b/frontend/lib/sobaApi.ts deleted file mode 100644 index e7036cf..0000000 --- a/frontend/lib/sobaApi.ts +++ /dev/null @@ -1 +0,0 @@ -export * from '@/src/shared/api/sobaApi'; diff --git a/frontend/src/app/providers/AppProviders.tsx b/frontend/src/app/providers/AppProviders.tsx index 052f9db..fae65b9 100644 --- a/frontend/src/app/providers/AppProviders.tsx +++ b/frontend/src/app/providers/AppProviders.tsx @@ -1,6 +1,6 @@ 'use client'; -import React, { useMemo } from 'react'; +import React, { useEffect, useMemo } from 'react'; import { Provider } from 'react-redux'; import { I18nProvider } from 'react-aria-components'; import makeStore from '@/lib/store'; @@ -22,6 +22,15 @@ export default function AppProviders({ }) { const store = useMemo(() => makeStore(), []); + // The root layout renders a static `` (it sits above the + // `[lang]` segment and can't know the locale). Keep the document language in + // sync with the active locale so assistive tech announces `/fr` pages in French. + useEffect(() => { + if (locale) { + document.documentElement.lang = locale; + } + }, [locale]); + return ( diff --git a/frontend/src/components/DataTable.tsx b/frontend/src/components/DataTable.tsx index 6bc2d72..f8213d3 100644 --- a/frontend/src/components/DataTable.tsx +++ b/frontend/src/components/DataTable.tsx @@ -21,6 +21,7 @@ export interface DataTableProps { emptyMessage?: string; loadingMessage?: string; itemName?: string; + caption?: string; pageSize?: number; currentPage?: number; totalItems?: number; @@ -38,6 +39,7 @@ export function DataTable({ emptyMessage = 'No items found.', loadingMessage = 'Loading...', itemName = 'items', + caption, pageSize = 10, currentPage = 1, totalItems, @@ -51,11 +53,13 @@ export function DataTable({ return (
- + {caption ? : null} + {columns.map((col) => ( ) : ( data.map((item) => ( - + {columns.map((col) => ( {loading ? ( - ) : data.length === 0 ? ( diff --git a/frontend/src/features/designer/ui/FormDesigner.tsx b/frontend/src/features/designer/ui/FormDesigner.tsx index be72c39..292348b 100644 --- a/frontend/src/features/designer/ui/FormDesigner.tsx +++ b/frontend/src/features/designer/ui/FormDesigner.tsx @@ -15,6 +15,7 @@ import type { } from '@formio/react'; import './FormDesigner.module.css'; import { Modal as CommonModal } from '@/src/components/Modal'; +import { CenteredProgress } from '@/app/ui/base/CenteredProgress'; import { TextArea, Button, InlineAlert } from '@bcgov/design-system-react-components'; import { useNotificationStore } from '@/lib/hooks/useNotificationStore'; @@ -232,7 +233,7 @@ const FormDesigner: React.FC = ({ onUpdateModel, initialModel = n }, [importJson, onUpdateModel, sanitizeForm, addNotification, dict.form.invalidJson]); if (initializing) { - return
Loading Designer...
; + return ; } if (!authenticated) { diff --git a/frontend/src/features/designer/ui/FormDesignerLoader.tsx b/frontend/src/features/designer/ui/FormDesignerLoader.tsx index 184e2c5..108dd9d 100644 --- a/frontend/src/features/designer/ui/FormDesignerLoader.tsx +++ b/frontend/src/features/designer/ui/FormDesignerLoader.tsx @@ -2,10 +2,11 @@ import dynamic from 'next/dynamic'; import React from 'react'; +import { CenteredProgress } from '@/app/ui/base/CenteredProgress'; const FormForm = dynamic(() => import('./FormForm'), { ssr: false, - loading: () =>
Loading Form Designer...
, + loading: () => , }); export default function FormDesignerLoader({ id }: { id?: string[] }) { diff --git a/frontend/src/features/designer/ui/FormForm.tsx b/frontend/src/features/designer/ui/FormForm.tsx index c31036e..94553a1 100644 --- a/frontend/src/features/designer/ui/FormForm.tsx +++ b/frontend/src/features/designer/ui/FormForm.tsx @@ -3,7 +3,6 @@ import { useState, useEffect, useCallback } from 'react'; import { useRouter, useParams } from 'next/navigation'; import { Tabs, Tab } from 'react-bootstrap'; import { - ProgressCircle, InlineAlert, Button, Form, @@ -16,6 +15,7 @@ import { AlertDialog, } from '@bcgov/design-system-react-components'; import { FaInfoCircle } from 'react-icons/fa'; +import { CenteredProgress } from '@/app/ui/base/CenteredProgress'; import { Modal as CommonModal } from '@/src/components/Modal'; import styles from './FormForm.module.css'; @@ -379,9 +379,7 @@ function FormForm({ id }: { id?: string[] }) {
{id && id[0] ? ( loading ? ( -
- -
+ ) : formSchema ? ( ) : ( diff --git a/frontend/src/features/designer/ui/FormList.tsx b/frontend/src/features/designer/ui/FormList.tsx index c82870f..1a2c793 100644 --- a/frontend/src/features/designer/ui/FormList.tsx +++ b/frontend/src/features/designer/ui/FormList.tsx @@ -2,7 +2,7 @@ import { useState, useEffect, useMemo, useCallback } from 'react'; import { Container } from 'react-bootstrap'; -import { Button as DSButton, ProgressCircle, TextField } from '@bcgov/design-system-react-components'; +import { Button as DSButton, TextField } from '@bcgov/design-system-react-components'; import { DataTable, type Column } from '@/src/components/DataTable'; import { DsPageHeading } from '@/app/ui/DsPageHeading'; import { useKeycloak } from '@/lib/hooks/useKeycloak'; @@ -208,13 +208,11 @@ function FormList({ ], ); - if (initializing) - return ( -
- -
- ); - if (!authenticated) return
{dict.general.notAuthenticated}
; + // Auth gate only — loading (including Keycloak init) is shown inside the table + // body so the page heading stays visible throughout. + if (!authenticated && !initializing) { + return
{dict.general.notAuthenticated}
; + } return ( @@ -263,7 +261,7 @@ function FormList({ data={paginatedForms as SobaFormSummary[]} columns={columns} - loading={loading} + loading={loading || initializing} error={error} emptyMessage="No forms found matching your criteria." loadingMessage={dict.general.loading} diff --git a/frontend/src/features/formio-v5/ui/DynamicForm.tsx b/frontend/src/features/formio-v5/ui/DynamicForm.tsx index e396bd0..761abdf 100644 --- a/frontend/src/features/formio-v5/ui/DynamicForm.tsx +++ b/frontend/src/features/formio-v5/ui/DynamicForm.tsx @@ -3,6 +3,7 @@ import dynamic from 'next/dynamic'; import type { FormProps } from '@formio/react'; import { useFormioV5FormChrome } from '@/lib/hooks/useFormioV5FormChrome'; +import { CenteredProgress } from '@/app/ui/base/CenteredProgress'; function FormioV5FormChrome({ children }: { children: React.ReactNode }) { useFormioV5FormChrome('render'); @@ -16,7 +17,7 @@ function FormioV5FormChrome({ children }: { children: React.ReactNode }) { */ const FormioForm = dynamic(() => import('@formio/react').then((m) => m.Form), { ssr: false, - loading: () =>

Loading form renderer…

, + loading: () => , }); export function DynamicForm(props: FormProps) { diff --git a/frontend/src/features/formio-v5/ui/FormioV5FormRenderLoader.tsx b/frontend/src/features/formio-v5/ui/FormioV5FormRenderLoader.tsx index 7834f87..2f17f59 100644 --- a/frontend/src/features/formio-v5/ui/FormioV5FormRenderLoader.tsx +++ b/frontend/src/features/formio-v5/ui/FormioV5FormRenderLoader.tsx @@ -1,14 +1,11 @@ 'use client'; import dynamic from 'next/dynamic'; +import { CenteredProgress } from '@/app/ui/base/CenteredProgress'; const FormioV5FormRenderClient = dynamic(() => import('./FormioV5FormRenderClient'), { ssr: false, - loading: () => ( -

- Loading… -

- ), + loading: () => , }); export default function FormioV5FormRenderLoader() { diff --git a/frontend/src/features/meta-review/ui/MetaReviewClientLoader.tsx b/frontend/src/features/meta-review/ui/MetaReviewClientLoader.tsx index 377ffe7..abe5bca 100644 --- a/frontend/src/features/meta-review/ui/MetaReviewClientLoader.tsx +++ b/frontend/src/features/meta-review/ui/MetaReviewClientLoader.tsx @@ -1,15 +1,12 @@ 'use client'; import dynamic from 'next/dynamic'; +import { CenteredProgress } from '@/app/ui/base/CenteredProgress'; /** 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: () => , }); 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 cb0fc1e..6528094 100644 --- a/frontend/src/features/submit-mode/ui/SubmissionList.tsx +++ b/frontend/src/features/submit-mode/ui/SubmissionList.tsx @@ -53,13 +53,11 @@ export function SubmissionList({ formId }: SubmissionListProps = {}) { } }, [authenticated, token, formId, activeWorkspaceId]); - const loading = !!(authenticated && token && !isLoaded); + const loading = initializing || (authenticated && (!token || !isLoaded)); - if (initializing || (authenticated && !token)) { - return
{dict.form?.loading || 'Loading submissions...'}
; - } - - if (!authenticated) { + // Auth gate only — loading (including Keycloak init) is shown inside the table + // body so the page heading stays visible throughout. + if (!authenticated && !initializing) { return null; } diff --git a/frontend/src/features/submit-mode/ui/SubmissionView.tsx b/frontend/src/features/submit-mode/ui/SubmissionView.tsx index 24d6c8b..80654c8 100644 --- a/frontend/src/features/submit-mode/ui/SubmissionView.tsx +++ b/frontend/src/features/submit-mode/ui/SubmissionView.tsx @@ -3,7 +3,8 @@ import { useParams } from 'next/navigation'; import { useEffect, useState } from 'react'; import type { FormType, Submission } from '@formio/react'; -import { ProgressCircle, InlineAlert } from '@bcgov/design-system-react-components'; +import { InlineAlert } from '@bcgov/design-system-react-components'; +import { CenteredProgress } from '@/app/ui/base/CenteredProgress'; import { useKeycloak } from '@/lib/hooks/useKeycloak'; import { useDictionary } from '@/app/[lang]/Providers'; import { useAppSelector } from '@/lib/store'; @@ -63,18 +64,14 @@ export function SubmissionView() { }, [authenticated, token, submissionId, ws, loaded]); if (initializing || (authenticated && !token)) { - return ( -
- -
- ); + return ; } if (!authenticated) return null; return (
{!loaded ? ( -

{dictSub?.loading || 'Loading…'}

+ ) : notFound || !submission ? ( {dictSub?.notFound || 'Submission not found.'} diff --git a/frontend/tests/components/DataTable.test.tsx b/frontend/tests/components/DataTable.test.tsx index 11043a7..19eea1f 100644 --- a/frontend/tests/components/DataTable.test.tsx +++ b/frontend/tests/components/DataTable.test.tsx @@ -30,7 +30,9 @@ describe('DataTable', () => { />, ); - expect(screen.getByText('Please wait...')).toBeInTheDocument(); + // No visible loading text by design — the spinner carries the message as its + // screen-reader accessible name (aria-label) inside a role="status" region. + expect(screen.getByRole('progressbar', { name: 'Please wait...' })).toBeInTheDocument(); }); it('renders rows and calls paging callbacks', async () => { diff --git a/frontend/tests/features/designer/FormDesigner.test.tsx b/frontend/tests/features/designer/FormDesigner.test.tsx index 8bb96cb..786441e 100644 --- a/frontend/tests/features/designer/FormDesigner.test.tsx +++ b/frontend/tests/features/designer/FormDesigner.test.tsx @@ -29,10 +29,10 @@ describe('FormDesigner', () => { vi.clearAllMocks(); }); - it('shows loading message when initializing', () => { + it('shows the loading indicator when initializing', () => { keycloakState = { authenticated: false, initializing: true }; render( {}} initialModel={null} />); - expect(screen.getByText('Loading Designer...')).toBeInTheDocument(); + expect(screen.getByRole('status')).toBeInTheDocument(); }); it('shows login required when not authenticated', () => { From c2e6571279405bd47e568c919a8ef5594e4fbaa1 Mon Sep 17 00:00:00 2001 From: usingtechnology <39388115+usingtechnology@users.noreply.github.com> Date: Sun, 14 Jun 2026 11:16:37 -0700 Subject: [PATCH 3/7] clean up submission flow. --- .../[lang]/submission/[submissionId]/page.tsx | 7 +- frontend/dictionaries/en.json | 1 + frontend/dictionaries/fr.json | 1 + .../formio-v5/ui/FormioV5FormRenderClient.tsx | 71 +++++++++---------- .../submit-mode/ui/SubmissionList.tsx | 11 +-- .../submit-mode/ui/SubmissionView.tsx | 10 +-- .../submit-mode/ui/WorkflowStateBadge.tsx | 20 ++++++ 7 files changed, 69 insertions(+), 52 deletions(-) create mode 100644 frontend/src/features/submit-mode/ui/WorkflowStateBadge.tsx diff --git a/frontend/app/[lang]/submission/[submissionId]/page.tsx b/frontend/app/[lang]/submission/[submissionId]/page.tsx index ae8850c..1117ad0 100644 --- a/frontend/app/[lang]/submission/[submissionId]/page.tsx +++ b/frontend/app/[lang]/submission/[submissionId]/page.tsx @@ -1,4 +1,5 @@ import { SubmissionView } from '@/src/features/submit-mode/ui/SubmissionView'; +import { DsPageHeading } from '@/app/ui/DsPageHeading'; import { getDictionary, hasLocale, Locale } from '../../dictionaries'; import { notFound } from 'next/navigation'; import { loadFeaturesMeta } from '@/src/shared/config/featuresMeta'; @@ -15,7 +16,7 @@ export async function generateMetadata({ params }: PageProps) { } const dict = await getDictionary(param.lang as Locale); return { - title: `Submission | ${dict.general.title}`, + title: `${dict.submission.pageTitle} | ${dict.general.title}`, description: dict.general.description, }; } @@ -27,9 +28,11 @@ export default async function Page({ params }: PageProps) { notFound(); } - await params; + const { lang } = await params; + const dict = await getDictionary((hasLocale(lang) ? lang : 'en') as Locale); return (
+ {dict.submission.pageTitle}
); diff --git a/frontend/dictionaries/en.json b/frontend/dictionaries/en.json index 73a1f17..93f3520 100644 --- a/frontend/dictionaries/en.json +++ b/frontend/dictionaries/en.json @@ -170,6 +170,7 @@ }, "submission": { "submissions": "Submissions", + "pageTitle": "Submission", "loading": "Loading submissions…", "empty": "No submissions in this workspace yet.", "error": "Could not load submissions.", diff --git a/frontend/dictionaries/fr.json b/frontend/dictionaries/fr.json index 98abc82..af98b1d 100644 --- a/frontend/dictionaries/fr.json +++ b/frontend/dictionaries/fr.json @@ -170,6 +170,7 @@ }, "submission": { "submissions": "Soumissions", + "pageTitle": "Soumission", "loading": "Chargement des soumissions…", "empty": "Aucune soumission dans cet espace pour l’instant.", "error": "Impossible de charger les soumissions.", diff --git a/frontend/src/features/formio-v5/ui/FormioV5FormRenderClient.tsx b/frontend/src/features/formio-v5/ui/FormioV5FormRenderClient.tsx index da970a3..8cae651 100644 --- a/frontend/src/features/formio-v5/ui/FormioV5FormRenderClient.tsx +++ b/frontend/src/features/formio-v5/ui/FormioV5FormRenderClient.tsx @@ -1,15 +1,16 @@ 'use client'; -import { useParams } from 'next/navigation'; +import { useParams, useRouter, usePathname } from 'next/navigation'; import { useEffect, useRef, useState } from 'react'; import { FormioProvider, Submission } from '@formio/react'; import type { FormType } from '@formio/react'; import { InlineAlert } from '@bcgov/design-system-react-components'; +import { CenteredProgress } from '@/app/ui/base/CenteredProgress'; import { useDictionary } from '@/app/[lang]/Providers'; +import { getLocaleFromPath } from '@/src/shared/util/locale'; import { normalizeFormioRenderError } from '@/src/features/formio-v5/normalizeFormioRenderError'; import { FormioV5FormRenderErrorBoundary } from '@/src/features/formio-v5/ui/FormioV5FormRenderErrorBoundary'; import { DynamicForm } from '@/src/features/formio-v5/ui/DynamicForm'; -import { ReadOnlyFormView } from '@/src/features/formio-v5/ui/ReadOnlyFormView'; import { getSobaFormVersions, getFormVersionSchema, @@ -18,6 +19,7 @@ import { } from '@/src/shared/api/sobaApi'; import { useKeycloak } from '@/lib/hooks/useKeycloak'; import { useAppSelector } from '@/lib/store'; +import { useNotificationStore } from '@/lib/hooks/useNotificationStore'; type FormRenderLabels = { loading: string; @@ -34,17 +36,18 @@ type FormRenderLabels = { function FormioV5FormRenderBody({ formId, labels }: { formId: string; labels: FormRenderLabels }) { const { token } = useKeycloak(); const { activeWorkspaceId } = useAppSelector((state) => state.workspace); + const { addNotification } = useNotificationStore(); + const router = useRouter(); + const locale = getLocaleFromPath(usePathname()); const ws = activeWorkspaceId || undefined; const [schema, setSchema] = useState(null); const [formVersionId, setFormVersionId] = useState(null); const [loadError, setLoadError] = useState(null); const [renderError, setRenderError] = useState(null); - const [successAlert, setSuccessAlert] = useState(false); - const [submitted, setSubmitted] = useState(null); const [loaded, setLoaded] = useState(false); - // The Form.io webform instance; in JSON mode (no `src`) we must signal it when our own - // persistence finishes, or its submit button spins forever. + // The Form.io webform instance; in JSON mode (no `src`) we must signal it on a failed + // save, or its submit button spins forever. On success we navigate away instead. const formInstanceRef = useRef<{ emit: (event: string, ...args: unknown[]) => void; } | null>(null); @@ -92,11 +95,11 @@ function FormioV5FormRenderBody({ formId, labels }: { formId: string; labels: Fo 'submit', ws, ); - setSuccessAlert(true); - // Tell the webform the submission is complete so its submit button stops spinning, - // then re-render the form read-only with the submitted answers. - formInstanceRef.current?.emit('submitDone', submission); - setSubmitted(submission); + addNotification({ text: labels.submitSuccess, type: 'success' }); + // Go straight to the saved submission's read-only view — the same page the + // submissions table links to. Navigating away unmounts the form, so there's + // no need to emit `submitDone` and no flash of Form.io's own success screen. + router.push(`/${locale}/submission/${created.id}`); } catch (err) { setRenderError(normalizeFormioRenderError(err, labels.rendererError)); formInstanceRef.current?.emit('submitError', labels.rendererError); @@ -112,7 +115,7 @@ function FormioV5FormRenderBody({ formId, labels }: { formId: string; labels: Fo } if (!schema) { - return

{labels.loading}

; + return ; } return ( @@ -122,11 +125,6 @@ function FormioV5FormRenderBody({ formId, labels }: { formId: string; labels: Fo {renderError}
) : null} - {successAlert ? ( - - {labels.submitSuccess} - - ) : null} @@ -134,26 +132,25 @@ function FormioV5FormRenderBody({ formId, labels }: { formId: string; labels: Fo } > - {submitted ? ( - - ) : ( -
- - { - formInstanceRef.current = instance; - }} - onError={(err) => { - setRenderError(normalizeFormioRenderError(err, labels.loadError)); - }} - onSubmit={submitForm} - /> - -
- )} +
+ + { + formInstanceRef.current = instance; + }} + onError={(err) => { + setRenderError(normalizeFormioRenderError(err, labels.loadError)); + }} + onSubmit={submitForm} + /> + +
); diff --git a/frontend/src/features/submit-mode/ui/SubmissionList.tsx b/frontend/src/features/submit-mode/ui/SubmissionList.tsx index 6528094..612a7eb 100644 --- a/frontend/src/features/submit-mode/ui/SubmissionList.tsx +++ b/frontend/src/features/submit-mode/ui/SubmissionList.tsx @@ -11,6 +11,7 @@ import { getSobaSubmissions } from '@/src/shared/api/sobaApiForms'; import type { SubmissionListItem } from '@/src/types/submissions'; import { DataTable, Column } from '@/src/components/DataTable'; import { DsPageHeading } from '@/app/ui/DsPageHeading'; +import { WorkflowStateBadge } from './WorkflowStateBadge'; import { useAppSelector } from '@/lib/store'; interface SubmissionListProps { @@ -97,15 +98,7 @@ export function SubmissionList({ formId }: SubmissionListProps = {}) { { key: 'workflowState', label: dict.submission?.columns?.status || 'Status', - render: (sub) => ( - - {sub.workflowState.toUpperCase()} - - ), + render: (sub) => , }, ]; diff --git a/frontend/src/features/submit-mode/ui/SubmissionView.tsx b/frontend/src/features/submit-mode/ui/SubmissionView.tsx index 80654c8..f2d86b1 100644 --- a/frontend/src/features/submit-mode/ui/SubmissionView.tsx +++ b/frontend/src/features/submit-mode/ui/SubmissionView.tsx @@ -9,6 +9,7 @@ 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 { WorkflowStateBadge } from './WorkflowStateBadge'; import { useFormatLongDate } from '@/src/shared/hooks/useFormatLongDate'; import { getSobaSubmission, @@ -79,13 +80,14 @@ export function SubmissionView() { ) : ( <>
-

{submission.formName || dict.form?.nameLabel || 'Submission'}

+

{submission.formName || dict.form?.nameLabel || 'Submission'}

v{submission.versionNo ?? 1} {' · '} - - {(submission.workflowState || '').toUpperCase()} - + {submission.submittedAt ? ( <> {' · '} diff --git a/frontend/src/features/submit-mode/ui/WorkflowStateBadge.tsx b/frontend/src/features/submit-mode/ui/WorkflowStateBadge.tsx new file mode 100644 index 0000000..9f0f055 --- /dev/null +++ b/frontend/src/features/submit-mode/ui/WorkflowStateBadge.tsx @@ -0,0 +1,20 @@ +'use client'; + +type WorkflowStateBadgeProps = { + state?: string; + 'data-testid'?: string; +}; + +/** + * The submission workflow-state pill, shared by the submissions list and the + * single-submission viewer so the status looks identical in both places. The + * state text is always rendered (not color-only) for accessibility. + */ +export function WorkflowStateBadge({ state, 'data-testid': testId }: WorkflowStateBadgeProps) { + const variant = (state || '').toLowerCase() === 'submitted' ? 'text-bg-success' : 'text-bg-secondary'; + return ( + + {(state || '').toUpperCase()} + + ); +} From 9d643a730b1d719a33b1ffebb36b9edd5739c05b Mon Sep 17 00:00:00 2001 From: usingtechnology <39388115+usingtechnology@users.noreply.github.com> Date: Sun, 14 Jun 2026 11:45:48 -0700 Subject: [PATCH 4/7] updates to designer tabs (dicts, buttons) --- frontend/dictionaries/en.json | 1 + frontend/dictionaries/fr.json | 1 + frontend/src/features/designer/ui/FormForm.tsx | 14 +++++++------- .../src/features/designer/ui/FormSettingsTab.tsx | 2 +- frontend/src/features/designer/ui/FormTeamTab.tsx | 2 +- 5 files changed, 11 insertions(+), 9 deletions(-) diff --git a/frontend/dictionaries/en.json b/frontend/dictionaries/en.json index 93f3520..d1d67a3 100644 --- a/frontend/dictionaries/en.json +++ b/frontend/dictionaries/en.json @@ -136,6 +136,7 @@ "cancel": "Cancel", "close": "Close", "designerTab": "Designer", + "designerTab": "Designer", "settingsTab": "Settings", "teamTab": "Team", "settingsPlaceholder": "Form settings will be available here.", diff --git a/frontend/dictionaries/fr.json b/frontend/dictionaries/fr.json index af98b1d..cc71b97 100644 --- a/frontend/dictionaries/fr.json +++ b/frontend/dictionaries/fr.json @@ -136,6 +136,7 @@ "cancel": "Annuler", "close": "Fermer", "designerTab": "Concepteur", + "designerTab": "Concepteur", "settingsTab": "Paramètres", "teamTab": "Équipe", "settingsPlaceholder": "Les paramètres du formulaire seront disponibles ici.", diff --git a/frontend/src/features/designer/ui/FormForm.tsx b/frontend/src/features/designer/ui/FormForm.tsx index 94553a1..6b28ef9 100644 --- a/frontend/src/features/designer/ui/FormForm.tsx +++ b/frontend/src/features/designer/ui/FormForm.tsx @@ -299,12 +299,12 @@ function FormForm({ id }: { id?: string[] }) { } }; - if (!authenticated) { - return
You must be logged in
; + if (initializing) { + return ; } - if (initializing) { - return
Forms initializing
; + if (!authenticated) { + return
{dict.general.notAuthenticated}
; } const renderDesignerContent = () => ( @@ -417,13 +417,13 @@ function FormForm({ id }: { id?: string[] }) { )} - {id && id[0] && ( @@ -504,7 +504,7 @@ function FormForm({ id }: { id?: string[] }) { onSelect={(k) => setActiveTab(k || 'designer')} className="mb-3" > - + {renderDesignerContent()} diff --git a/frontend/src/features/designer/ui/FormSettingsTab.tsx b/frontend/src/features/designer/ui/FormSettingsTab.tsx index df61b4b..8c41883 100644 --- a/frontend/src/features/designer/ui/FormSettingsTab.tsx +++ b/frontend/src/features/designer/ui/FormSettingsTab.tsx @@ -13,7 +13,7 @@ export default function FormSettingsTab({ dict }: FormSettingsTabProps) { return (
-
{dict.form.settingsTab || 'Settings'}
+

{dict.form.settingsTab || 'Settings'}

-
{dict.form.teamManagement || 'Team Management'}
+

{dict.form.teamManagement || 'Team Management'}

{dict.form.teamPlaceholder || 'Team management tools will be available here.'}

From 30cf3d50aa0310d7284d8eee819fb734ad10cb61 Mon Sep 17 00:00:00 2001 From: usingtechnology <39388115+usingtechnology@users.noreply.github.com> Date: Sun, 14 Jun 2026 12:07:34 -0700 Subject: [PATCH 5/7] standardize modal forms style --- frontend/src/components/Modal.module.css | 35 ++++++++++++++++++++++++ frontend/src/components/Modal.tsx | 15 ++++++++-- 2 files changed, 47 insertions(+), 3 deletions(-) create mode 100644 frontend/src/components/Modal.module.css diff --git a/frontend/src/components/Modal.module.css b/frontend/src/components/Modal.module.css new file mode 100644 index 0000000..ba7efe8 --- /dev/null +++ b/frontend/src/components/Modal.module.css @@ -0,0 +1,35 @@ +/* + * Baseline layout for app "form" dialogs (import/export, preview, etc.). + * + * The BC Design System plain `Dialog` only stacks its children with no internal + * structure, which is why these looked basic. We give them the same region + * model the DS `AlertDialog` uses — header / body / footer separated by thin + * borders — reusing the same design tokens so the spacing matches the rest of + * the system (e.g. the disclaimer dialog). + */ + +.header { + padding: var(--layout-padding-medium) var(--layout-padding-large); + border-bottom: var(--layout-border-width-small) solid var(--surface-color-border-default); +} + +.title { + margin: 0; + font: var(--typography-bold-h5); + color: var(--typography-color-primary); +} + +/* + * Body sits between the two thin borders. It's a flex column so passed-in + * content stretches to the full width of the padded area by default. + */ +.body { + display: flex; + flex-direction: column; + padding: var(--layout-padding-medium) var(--layout-padding-large); + border-bottom: var(--layout-border-width-small) solid var(--surface-color-border-default); +} + +.footer { + padding: var(--layout-padding-medium) var(--layout-padding-large); +} diff --git a/frontend/src/components/Modal.tsx b/frontend/src/components/Modal.tsx index 7235726..ecdf469 100644 --- a/frontend/src/components/Modal.tsx +++ b/frontend/src/components/Modal.tsx @@ -2,6 +2,7 @@ import React from 'react'; import { Modal as BCModal, Dialog, Heading, ButtonGroup } from '@bcgov/design-system-react-components'; +import styles from './Modal.module.css'; export interface ModalProps { show: boolean; @@ -40,9 +41,17 @@ export function Modal({ show, title, onClose, children, size = 'lg', footer }: M style={{ width: WIDTH_BY_SIZE[size], maxWidth: '100vw' }} > - {title} - {children} - {footer && {footer}} +
+ + {title} + +
+
{children}
+ {footer && ( +
+ {footer} +
+ )}
); From 2ccff40df9366d73f80379f7ed863c3fc0625ee0 Mon Sep 17 00:00:00 2001 From: usingtechnology <39388115+usingtechnology@users.noreply.github.com> Date: Sun, 14 Jun 2026 13:02:24 -0700 Subject: [PATCH 6/7] removal of meta UX and other "dead" code. --- frontend/app/[lang]/meta/page.tsx | 33 --- frontend/dictionaries/en.json | 34 +-- frontend/dictionaries/fr.json | 34 +-- frontend/src/app/plugins/registry.ts | 8 +- .../src/features/designer/ui/FormForm.tsx | 86 +------ .../features/designer/ui/FormSettingsTab.tsx | 14 +- .../src/features/designer/ui/FormTeamTab.tsx | 6 +- frontend/src/features/meta-review/plugin.tsx | 18 -- .../meta-review/ui/MetaReviewClient.tsx | 223 ------------------ .../meta-review/ui/MetaReviewClientLoader.tsx | 14 -- frontend/src/shared/featureFlags/flags.ts | 3 +- frontend/src/shared/util/locale.ts | 2 +- frontend/src/types/formio.ts | 7 - frontend/src/types/forms.ts | 16 -- 14 files changed, 21 insertions(+), 477 deletions(-) delete mode 100644 frontend/app/[lang]/meta/page.tsx delete mode 100644 frontend/src/features/meta-review/plugin.tsx delete mode 100644 frontend/src/features/meta-review/ui/MetaReviewClient.tsx delete mode 100644 frontend/src/features/meta-review/ui/MetaReviewClientLoader.tsx delete mode 100644 frontend/src/types/formio.ts diff --git a/frontend/app/[lang]/meta/page.tsx b/frontend/app/[lang]/meta/page.tsx deleted file mode 100644 index 6ed5193..0000000 --- a/frontend/app/[lang]/meta/page.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { DsPageHeading } from '@/app/ui/DsPageHeading'; -import MetaReviewClientLoader from '@/src/features/meta-review/ui/MetaReviewClientLoader'; -import { getDictionary, hasLocale, Locale, resolveLocale } from '../dictionaries'; - -type PageProps = { - params: Promise<{ lang: string }>; -}; - -export async function generateMetadata({ params }: PageProps) { - const param = await params; - const locale = resolveLocale(param.lang); - const dict = await getDictionary(locale); - const title = dict.metaPage?.title ?? 'Meta'; - return { - title: `${title} | ${dict.general.title}`, - description: dict.general.description, - }; -} - -export default async function Page({ params }: PageProps) { - const { lang } = await params; - const locale = hasLocale(lang) ? lang : 'en'; - const dict = await getDictionary(locale as Locale); - const heading = dict.metaPage?.title ?? 'Meta'; - return ( -
- {heading} -
- -
-
- ); -} diff --git a/frontend/dictionaries/en.json b/frontend/dictionaries/en.json index d1d67a3..0189ce0 100644 --- a/frontend/dictionaries/en.json +++ b/frontend/dictionaries/en.json @@ -19,7 +19,6 @@ "workspaces": "Workspaces", "design": "Design", "submit": "Submit", - "metaReview": "API meta", "themeToggle": "Toggle color theme", "skipToMain": "Skip to main content", "designer": "Designer", @@ -29,23 +28,6 @@ "openMenuAria": "Open navigation menu", "closeMenuAria": "Close navigation menu" }, - "metaPage": { - "title": "API meta & health", - "refresh": "Refresh", - "loadCodes": "Load reference codes", - "loadRoles": "Load roles", - "sections": { - "health": "Health", - "readiness": "Readiness", - "build": "Build", - "features": "Features", - "frontendConfig": "Frontend config", - "plugins": "Plugins", - "formEngines": "Form engines", - "codes": "Reference codes", - "roles": "Roles" - } - }, "base": { "copyToClipboard": { "copyToClipboard": "Copy to Clipboard", @@ -109,8 +91,6 @@ "namePlaceholder": "Enter form name", "slugLabel": "Form Slug", "slugPlaceholder": "Enter form slug", - "descriptionLabel": "Description", - "descriptionPlaceholder": "Enter form description", "loading": "Loading form...", "schemaNotAvailable": "Form schema not available.", "edit": "Edit", @@ -121,12 +101,6 @@ "visibilityPublic": "Public", "visibilityAzureIDIR": "IDIR - MFA", "disclaimerLabel": "I agree to the disclaimer and statement of responsibility", - "disclaimerTitle": "Disclaimer", - "viewDisclaimer": "View disclaimer", - "disclaimerText1": "It is your responsibility to comply with Privacy laws governing the collection, use and disclosure of personally identifiable information.", - "disclaimerText2": "Access to this form designer tool does not inherently grant permission to collect, use or disclose any personally identifiable information.", - "disclaimerText3": "It is your responsibility to obtain consent to collect information as required by law.", - "disclaimerText4": "If you use BCeID or BC Services Card as form access options, you MUST notify the Identity Information Management (IDIM) team by email (IDIM.Consulting@gov.bc.ca) your intent to leverage BCeID or BC Services Card.", "exportJson": "Export JSON", "importJson": "Import JSON", "importSchema": "Import Schema", @@ -136,12 +110,8 @@ "cancel": "Cancel", "close": "Close", "designerTab": "Designer", - "designerTab": "Designer", "settingsTab": "Settings", "teamTab": "Team", - "settingsPlaceholder": "Form settings will be available here.", - "teamManagement": "Team Management", - "teamPlaceholder": "Team management tools will be available here.", "readOnlyMode": "Read-Only Mode:", "viewingHistoricalVersion": "You are viewing historical version", "savePublishDisabled": "Save and Publish options are disabled.", @@ -165,9 +135,7 @@ "formPreview": "Form Preview", "untitledForm": "Untitled Form", "closePreview": "Close Preview", - "noFormLayout": "No form layout designed yet.", - "apiKey": "API Key", - "apiKeyPlaceholder": "Enter your api key" + "noFormLayout": "No form layout designed yet." }, "submission": { "submissions": "Submissions", diff --git a/frontend/dictionaries/fr.json b/frontend/dictionaries/fr.json index cc71b97..4023469 100644 --- a/frontend/dictionaries/fr.json +++ b/frontend/dictionaries/fr.json @@ -19,7 +19,6 @@ "workspaces": "Espaces de travail", "design": "Conception", "submit": "Soumettre", - "metaReview": "Méta API", "themeToggle": "Basculer le theme de couleur", "skipToMain": "Aller au contenu principal", "designer": "Générateur de Formulaire", @@ -29,23 +28,6 @@ "openMenuAria": "Ouvrir le menu de navigation", "closeMenuAria": "Fermer le menu de navigation" }, - "metaPage": { - "title": "Méta API et santé", - "refresh": "Actualiser", - "loadCodes": "Charger les codes de référence", - "loadRoles": "Charger les rôles", - "sections": { - "health": "Santé", - "readiness": "Disponibilité", - "build": "Build", - "features": "Fonctionnalités", - "frontendConfig": "Config frontale", - "plugins": "Plugins", - "formEngines": "Moteurs de formulaire", - "codes": "Codes de référence", - "roles": "Rôles" - } - }, "base": { "copyToClipboard": { "copyToClipboard": "Copier dans le presse-papiers", @@ -109,8 +91,6 @@ "namePlaceholder": "Entrez le nom du formulaire", "slugLabel": "Slug du Formulaire", "slugPlaceholder": "Entrez le slug du formulaire", - "descriptionLabel": "Description", - "descriptionPlaceholder": "Entrez la description du formulaire", "loading": "Chargement du formulaire...", "schemaNotAvailable": "Schéma du formulaire non disponible.", "edit": "Modifier", @@ -121,12 +101,6 @@ "visibilityPublic": "Publique", "visibilityAzureIDIR": "IDIR - MFA", "disclaimerLabel": "J'accepte l'avis de non-responsabilité et la déclaration de responsabilité", - "disclaimerTitle": "Avis de non-responsabilité", - "viewDisclaimer": "Voir l'avis de non-responsabilité", - "disclaimerText1": "Il est de votre responsabilité de vous conformer aux lois sur la protection de la vie privée régissant la collecte, l'utilisation et la divulgation d'informations personnellement identifiables.", - "disclaimerText2": "L'accès à cet outil de conception de formulaires n'accorde pas intrinsèquement l'autorisation de collecter, d'utiliser ou de divulguer des informations personnellement identifiables.", - "disclaimerText3": "Il est de votre responsabilité d'obtenir le consentement pour recueillir des informations comme l'exige la loi.", - "disclaimerText4": "Si vous utilisez BCeID ou BC Services Card comme options d'accès au formulaire, vous DEVEZ aviser l'équipe de gestion des informations d'identité (IDIM) par courriel (IDIM.Consulting@gov.bc.ca) de votre intention d'utiliser BCeID ou BC Services Card.", "exportJson": "Exporter JSON", "importJson": "Importer JSON", "importSchema": "Importer le schéma", @@ -136,12 +110,8 @@ "cancel": "Annuler", "close": "Fermer", "designerTab": "Concepteur", - "designerTab": "Concepteur", "settingsTab": "Paramètres", "teamTab": "Équipe", - "settingsPlaceholder": "Les paramètres du formulaire seront disponibles ici.", - "teamManagement": "Gestion d'équipe", - "teamPlaceholder": "Les outils de gestion d'équipe seront disponibles ici.", "readOnlyMode": "Mode lecture seule :", "viewingHistoricalVersion": "Vous consultez la version historique", "savePublishDisabled": "Les options Sauvegarder et Publier sont désactivées.", @@ -165,9 +135,7 @@ "formPreview": "Aperçu du formulaire", "untitledForm": "Formulaire sans titre", "closePreview": "Fermer l'aperçu", - "noFormLayout": "Aucune mise en page de formulaire n'a encore été conçue.", - "apiKey": "Clé API", - "apiKeyPlaceholder": "Entrez votre clé API" + "noFormLayout": "Aucune mise en page de formulaire n'a encore été conçue." }, "submission": { "submissions": "Soumissions", diff --git a/frontend/src/app/plugins/registry.ts b/frontend/src/app/plugins/registry.ts index edfd37f..93ba24f 100644 --- a/frontend/src/app/plugins/registry.ts +++ b/frontend/src/app/plugins/registry.ts @@ -1,16 +1,10 @@ import type { Locale } from '@/app/[lang]/dictionaries'; import { designerPlugin } from '@/src/features/designer/plugin'; -import { metaReviewPlugin } from '@/src/features/meta-review/plugin'; import { submitModePlugin } from '@/src/features/submit-mode/plugin'; import { workspacesPlugin } from '@/src/features/workspaces/plugin'; import type { AppPlugin, PluginNavItem, Dictionary } from '@/src/types/plugins'; -const allPlugins: AppPlugin[] = [ - workspacesPlugin, - designerPlugin, - submitModePlugin, - metaReviewPlugin, -]; +const allPlugins: AppPlugin[] = [workspacesPlugin, designerPlugin, submitModePlugin]; function getEnabledPlugins(isFeatureAllowed: (code: string) => boolean): AppPlugin[] { return allPlugins diff --git a/frontend/src/features/designer/ui/FormForm.tsx b/frontend/src/features/designer/ui/FormForm.tsx index 6b28ef9..7526b06 100644 --- a/frontend/src/features/designer/ui/FormForm.tsx +++ b/frontend/src/features/designer/ui/FormForm.tsx @@ -7,14 +7,10 @@ import { Button, Form, TextField, - TextArea, Select, CheckboxGroup, Checkbox, - Modal as BCModal, - AlertDialog, } from '@bcgov/design-system-react-components'; -import { FaInfoCircle } from 'react-icons/fa'; import { CenteredProgress } from '@/app/ui/base/CenteredProgress'; import { Modal as CommonModal } from '@/src/components/Modal'; import styles from './FormForm.module.css'; @@ -86,7 +82,6 @@ function FormForm({ id }: { id?: string[] }) { const [isDirty, setIsDirty] = useState(false); const [showPreview, setShowPreview] = useState(false); - const [showDisclaimerModal, setShowDisclaimerModal] = useState(false); const [agreedDisclaimer, setAgreedDisclaimer] = useState(false); useEffect(() => { @@ -151,11 +146,6 @@ function FormForm({ id }: { id?: string[] }) { [slugManuallyEdited], ); - const updateDescription = (value: string) => { - setFormDesc(value); - setIsDirty(true); - }; - const updateFormSchema = useCallback((data: FormType) => { setFormSchema(data); setIsDirty(true); @@ -353,26 +343,14 @@ function FormForm({ id }: { id?: string[] }) { ))} -
- - {dict.form.disclaimerLabel || - 'I agree to the disclaimer and statement of responsibility'} - - -
+ + {dict.form.disclaimerLabel || + 'I agree to the disclaimer and statement of responsibility'} + {/* Form Builder */} @@ -390,15 +368,8 @@ function FormForm({ id }: { id?: string[] }) { )}
- {/* Description */} -
-
{caption}
@@ -82,7 +86,7 @@ export function DataTable({
({ {!loading && data.length > 0 && totalItems !== undefined && (
Items per page: diff --git a/frontend/src/features/designer/ui/FormForm.tsx b/frontend/src/features/designer/ui/FormForm.tsx index 4d06652..c31036e 100644 --- a/frontend/src/features/designer/ui/FormForm.tsx +++ b/frontend/src/features/designer/ui/FormForm.tsx @@ -362,11 +362,16 @@ function FormForm({ id }: { id?: string[] }) { {dict.form.disclaimerLabel || 'I agree to the disclaimer and statement of responsibility'} - setShowDisclaimerModal(true)} - /> +
diff --git a/frontend/src/features/designer/ui/FormList.tsx b/frontend/src/features/designer/ui/FormList.tsx index f3b4c5f..c82870f 100644 --- a/frontend/src/features/designer/ui/FormList.tsx +++ b/frontend/src/features/designer/ui/FormList.tsx @@ -4,8 +4,10 @@ import { useState, useEffect, useMemo, useCallback } from 'react'; import { Container } from 'react-bootstrap'; import { Button as DSButton, ProgressCircle, TextField } from '@bcgov/design-system-react-components'; import { DataTable, type Column } from '@/src/components/DataTable'; +import { DsPageHeading } from '@/app/ui/DsPageHeading'; import { useKeycloak } from '@/lib/hooks/useKeycloak'; import { useDictionary } from '@/app/[lang]/Providers'; +import Link from 'next/link'; import { useRouter, usePathname } from 'next/navigation'; import { getLocaleFromPath } from '@/src/shared/util/locale'; import { FaMagnifyingGlass, FaX } from 'react-icons/fa6'; @@ -154,18 +156,13 @@ function FormList({ width: '40%', render: (form: SobaFormSummary) => { return designModeEnabled ? ( - { - e.preventDefault(); - handleAction('manage', form.id); - }} - className="text-decoration-underline" - style={{ cursor: 'pointer', color: '#00538A' }} + className="text-decoration-underline link-primary" > {form.name || dictForm?.nameLabel || 'Untitled Form'} - + ) : ( {form.name || dictForm?.nameLabel || 'Untitled Form'} ); @@ -200,7 +197,15 @@ function FormList({ ), }, ], - [handleAction, dictFormList, dictForm, designModeEnabled, submitModeEnabled, formatLongDate], + [ + handleAction, + locale, + dictFormList, + dictForm, + designModeEnabled, + submitModeEnabled, + formatLongDate, + ], ); if (initializing) @@ -213,9 +218,7 @@ function FormList({ return ( -
-

Forms

-
+ {dict.general.forms}
{designModeEnabled && ( ([]); @@ -68,19 +69,14 @@ export function SubmissionList({ formId }: SubmissionListProps = {}) { key: 'id', label: dict.submission?.columns?.id || 'Submission ID', render: (sub) => ( - { - e.preventDefault(); - router.push(`/${locale}/submission/${sub.id}`); - }} - className="text-decoration-underline font-monospace small" - style={{ cursor: 'pointer', color: '#00538A' }} + className="text-decoration-underline font-monospace small link-primary" title={dict.submission?.view || 'View'} > {sub.id} - + ), }, { @@ -117,9 +113,9 @@ export function SubmissionList({ formId }: SubmissionListProps = {}) { return ( -
-

{dict.submission?.submissions || 'Submissions'}

-
+ + {dict.submission?.submissions || 'Submissions'} + data={paginatedSubmissions} columns={columns} @@ -128,6 +124,7 @@ export function SubmissionList({ formId }: SubmissionListProps = {}) { loadingMessage={dict.submission?.loading || 'Loading submissions...'} keyExtractor={(sub) => sub.id} itemName={dict.submission?.submissions || 'submissions'} + caption={dict.submission?.submissions || 'Submissions'} totalItems={submissions.length} pageSize={pageSize} currentPage={currentPage} diff --git a/frontend/src/shared/config/runtimeConfig.ts b/frontend/src/shared/config/runtimeConfig.ts index 3fb9441..6e1b376 100644 --- a/frontend/src/shared/config/runtimeConfig.ts +++ b/frontend/src/shared/config/runtimeConfig.ts @@ -87,10 +87,6 @@ export async function loadFrontendRuntimeConfig(): Promise ({ vi.mock('@/app/[lang]/Providers', () => ({ useDictionary: () => ({ locale: 'en', - general: { notAuthenticated: 'Not authed' }, + general: { notAuthenticated: 'Not authed', forms: 'Forms' }, form: { nameLabel: 'Form Name' }, submission: { formList: { @@ -76,7 +76,7 @@ describe('FormList', () => { await act(async () => { render(); }); - expect(screen.getByText('Forms')).toBeInTheDocument(); + expect(screen.getByRole('heading', { name: 'Forms' })).toBeInTheDocument(); // DS TextField puts data-testid on its wrapper; query the input by its // accessible label instead. const input = screen.getByLabelText('Search'); diff --git a/frontend/tests/lib/constants.test.ts b/frontend/tests/lib/constants.test.ts deleted file mode 100644 index 41169d4..0000000 --- a/frontend/tests/lib/constants.test.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { NotificationTypes } from '@/lib/constants'; - -describe('NotificationTypes', () => { - it('defines INFO type', () => { - expect(NotificationTypes.INFO).toEqual({ type: 'info' }); - }); - - it('defines SUCCESS type', () => { - expect(NotificationTypes.SUCCESS).toEqual({ type: 'success' }); - }); - - it('defines WARNING type', () => { - expect(NotificationTypes.WARNING).toEqual({ type: 'warning' }); - }); - - it('defines ERROR type', () => { - expect(NotificationTypes.ERROR).toEqual({ type: 'error' }); - }); - - it('covers all four notification types', () => { - expect(Object.keys(NotificationTypes)).toHaveLength(4); - }); -}); From be3ca56f59026ae77999685b4f9ed2aeaeef976e Mon Sep 17 00:00:00 2001 From: usingtechnology <39388115+usingtechnology@users.noreply.github.com> Date: Sun, 14 Jun 2026 10:27:17 -0700 Subject: [PATCH 2/7] standardize centered spinner for data tables. --- frontend/app/ui/base/CenteredProgress.tsx | 41 +++++++++++++++++++ frontend/src/app/ui/AuthRedirect.tsx | 11 +---- frontend/src/components/DataTable.tsx | 10 ++--- .../src/features/designer/ui/FormDesigner.tsx | 3 +- .../designer/ui/FormDesignerLoader.tsx | 3 +- .../src/features/designer/ui/FormForm.tsx | 6 +-- .../src/features/designer/ui/FormList.tsx | 16 ++++---- .../src/features/formio-v5/ui/DynamicForm.tsx | 3 +- .../formio-v5/ui/FormioV5FormRenderLoader.tsx | 7 +--- .../meta-review/ui/MetaReviewClientLoader.tsx | 7 +--- .../submit-mode/ui/SubmissionList.tsx | 10 ++--- .../submit-mode/ui/SubmissionView.tsx | 11 ++--- frontend/tests/components/DataTable.test.tsx | 4 +- .../features/designer/FormDesigner.test.tsx | 4 +- 14 files changed, 79 insertions(+), 57 deletions(-) create mode 100644 frontend/app/ui/base/CenteredProgress.tsx diff --git a/frontend/app/ui/base/CenteredProgress.tsx b/frontend/app/ui/base/CenteredProgress.tsx new file mode 100644 index 0000000..afa47e1 --- /dev/null +++ b/frontend/app/ui/base/CenteredProgress.tsx @@ -0,0 +1,41 @@ +'use client'; + +import { ProgressCircle } from '@bcgov/design-system-react-components'; + +type CenteredProgressProps = { + /** + * Screen-reader-only label for the spinner. There is no visible loading text + * by design, so this is the only thing assistive tech announces — keep it + * meaningful (e.g. the localized "Loading…"). + */ + label?: string; + /** Optional minimum height so the spinner can center within a tall empty area. */ + minHeight?: string; + 'data-testid'?: string; +}; + +/** + * The single loading indicator for the app: a horizontally/vertically centered + * ProgressCircle with no visible text. Use everywhere a screen, page data area, + * or table body is pending so loading looks and behaves the same throughout. + * + * Accessibility: the wrapper is a `role="status"` live region (implicit + * `aria-live="polite"`) so screen readers announce the spinner when it appears, + * and the ProgressCircle carries an `aria-label` as its accessible name. + */ +export function CenteredProgress({ + label = 'Loading', + minHeight, + 'data-testid': testId = 'loading-indicator', +}: CenteredProgressProps) { + return ( +
+ +
+ ); +} diff --git a/frontend/src/app/ui/AuthRedirect.tsx b/frontend/src/app/ui/AuthRedirect.tsx index 2125664..fc70f1f 100644 --- a/frontend/src/app/ui/AuthRedirect.tsx +++ b/frontend/src/app/ui/AuthRedirect.tsx @@ -4,7 +4,7 @@ import { useEffect } from 'react'; import { useRouter } from 'next/navigation'; import { useKeycloak } from '@/lib/hooks/useKeycloak'; import { useDictionary } from '@/app/[lang]/Providers'; -import { ProgressCircle } from '@bcgov/design-system-react-components'; +import { CenteredProgress } from '@/app/ui/base/CenteredProgress'; export function AuthRedirect({ to, @@ -28,14 +28,7 @@ export function AuthRedirect({ }, [shouldRedirect, router, to]); if (initializing || shouldRedirect) { - return ( -
- -
- ); + return ; } return <>{children}; diff --git a/frontend/src/components/DataTable.tsx b/frontend/src/components/DataTable.tsx index f8213d3..3cf7657 100644 --- a/frontend/src/components/DataTable.tsx +++ b/frontend/src/components/DataTable.tsx @@ -2,8 +2,9 @@ import React from 'react'; import { Table } from 'react-bootstrap'; -import { ProgressCircle, Select, Button } from '@bcgov/design-system-react-components'; +import { Select, Button } from '@bcgov/design-system-react-components'; import { FaChevronLeft, FaChevronRight } from 'react-icons/fa6'; +import { CenteredProgress } from '@/app/ui/base/CenteredProgress'; export interface Column { key: string; @@ -71,11 +72,8 @@ export function DataTable({
-
- - {loadingMessage} -
+
+