Skip to content

Commit f478571

Browse files
akurinnoyclaude
andauthored
Accessibility issues cherry-pick (#1560)
* fix: prevent workspace name reset when backup image URL changes (#1545) Assisted-by: Claude Sonnet 4.6 Signed-off-by: Oleksii Kurinnyi <okurinny@redhat.com> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(a11y): align Restore Workspace button accessible name with visible label (#1547) The button's aria-label was "Restore from backup" while the visible text was "Restore Workspace", violating WCAG 2.5.3 (Label in Name). Removing the aria-label lets the visible text serve as the accessible name. Fixes: https://issues.redhat.com/browse/CRW-10720 Assisted-by: Claude Opus 4.6 (1M context) Signed-off-by: Oleksii Kurinnyi <okurinny@redhat.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(a11y): align Search field accessible name and layout on Backups tab (#1556) WCAG 2.5.3 (Label in Name) — the Search input on the Backups tab had aria-label "Filter backups" while its visible placeholder was "Search". Rename to "Search backups" on both the TextInput and search Button. Align the Backups toolbar and table layout with the Active Workspaces tab by adding hidden checkbox elements that match the same DOM structure, ensuring pixel-perfect column alignment. Move the backup schedule label from the Backups toolbar to the page-level header so it is visible on both tabs. Remove force:true from BackupsView fetch calls so the 30s Redux cache prevents redundant API calls on tab switch. Add dedicated unit tests for BackupsListToolbar and remove redundant toolbar tests from BackupsTableView. Assisted-by: Claude Opus 4.6 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: Oleksii Kurinnyi <okurinny@redhat.com> # Conflicts: # packages/dashboard-frontend/src/pages/WorkspacesList/BackupsView/Toolbar/index.tsx * fix(test): align BackupsListToolbar test with Restore button label The test was looking for "Restore from Backup" (the old aria-label removed in #1547) instead of "Restore Workspace" (the visible text). Assisted-by: Claude Opus 4.6 (1M context) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: Oleksii Kurinnyi <okurinny@redhat.com> --------- Signed-off-by: Oleksii Kurinnyi <okurinny@redhat.com> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 20d7f1e commit f478571

21 files changed

Lines changed: 467 additions & 511 deletions

File tree

.vscode/settings.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,5 @@
77
"**/node_modules": true
88
},
99
"typescript.tsdk": "node_modules/typescript/lib",
10+
"js/ts.tsdk.path": "node_modules/typescript/lib",
1011
}

packages/dashboard-frontend/src/containers/WorkspacesList/index.tsx

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,11 @@ import Fallback from '@/components/Fallback';
1818
import WorkspacesList from '@/pages/WorkspacesList';
1919
import { RootState } from '@/store';
2020
import { fetchBackupConfig, fetchWorkspaceBackupStatus } from '@/store/Backups/actions';
21-
import { selectAllBackupsByWorkspace, selectBackupConfig } from '@/store/Backups/selectors';
21+
import {
22+
selectAllBackupsByWorkspace,
23+
selectBackupConfig,
24+
selectBackupSchedule,
25+
} from '@/store/Backups/selectors';
2226
import { selectBranding } from '@/store/Branding/selectors';
2327
import { selectCmEditors } from '@/store/Plugins/devWorkspacePlugins/selectors';
2428
import { workspacesActionCreators } from '@/store/Workspaces';
@@ -56,7 +60,7 @@ export class WorkspacesListContainer extends React.PureComponent<Props> {
5660
}
5761

5862
private fetchBackupStatuses() {
59-
const { allWorkspaces, backupConfig, backupsByWorkspace } = this.props;
63+
const { allWorkspaces, backupConfig, backupsByWorkspace: backupsByWorkspace } = this.props;
6064
if (!backupConfig?.registry) {
6165
return;
6266
}
@@ -73,10 +77,11 @@ export class WorkspacesListContainer extends React.PureComponent<Props> {
7377

7478
render() {
7579
const {
76-
backupConfig,
77-
branding,
7880
allWorkspaces,
81+
backupConfig,
7982
backupsByWorkspace,
83+
backupSchedule,
84+
branding,
8085
editors,
8186
isLoading,
8287
location,
@@ -90,12 +95,13 @@ export class WorkspacesListContainer extends React.PureComponent<Props> {
9095
return (
9196
<WorkspacesList
9297
backupConfig={backupConfig}
98+
backupsByWorkspace={backupsByWorkspace}
99+
backupSchedule={backupSchedule}
93100
branding={branding}
94101
editors={editors}
95102
location={location}
96103
navigate={navigate}
97104
workspaces={allWorkspaces}
98-
backupsByWorkspace={backupsByWorkspace}
99105
/>
100106
);
101107
}
@@ -110,10 +116,11 @@ function ContainerWrapper(props: MappedProps) {
110116

111117
const mapStateToProps = (state: RootState) => {
112118
return {
113-
backupConfig: selectBackupConfig(state),
114-
branding: selectBranding(state),
115119
allWorkspaces: selectAllWorkspaces(state),
120+
backupConfig: selectBackupConfig(state),
116121
backupsByWorkspace: selectAllBackupsByWorkspace(state),
122+
backupSchedule: selectBackupSchedule(state),
123+
branding: selectBranding(state),
117124
editors: selectCmEditors(state),
118125
isLoading: selectIsLoading(state),
119126
};

packages/dashboard-frontend/src/pages/RestoreFromBackup/DefaultRegistryForm/index.tsx

Lines changed: 27 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,14 @@
1010
* Red Hat, Inc. - initial API and implementation
1111
*/
1212

13-
// Generated by Claude Opus 4.6
13+
// Generated by Claude Sonnet 4.6
1414

1515
import { BackupItem } from '@eclipse-che/common';
16-
import { Form, ValidatedOptions } from '@patternfly/react-core';
17-
import React, { useEffect, useState } from 'react';
16+
import { Form } from '@patternfly/react-core';
17+
import React, { useCallback, useEffect, useState } from 'react';
1818

1919
import { BackupSelectorField } from '@/pages/RestoreFromBackup/DefaultRegistryForm/BackupSelectorField';
2020
import { ImagePreviewField } from '@/pages/RestoreFromBackup/DefaultRegistryForm/ImagePreviewField';
21-
import { useRestoreFormValidation } from '@/pages/RestoreFromBackup/useRestoreFormValidation';
2221
import { WorkspaceNameField } from '@/pages/RestoreFromBackup/WorkspaceNameField';
2322

2423
export type DefaultRegistryRestoreData = {
@@ -45,57 +44,41 @@ export const DefaultRegistryRestoreForm: React.FC<Props> = ({
4544
}) => {
4645
const [selectedBackup, setSelectedBackup] = useState<BackupItem | undefined>(undefined);
4746

48-
const {
49-
workspaceName,
50-
workspaceNameValidated,
51-
workspaceNameError,
52-
workspaceNameWarning,
53-
setWorkspaceName,
54-
handleWorkspaceNameChange,
55-
} = useRestoreFormValidation(existingWorkspaceNames, existingBackupNames);
47+
const [workspaceName, setWorkspaceName] = useState('');
48+
const [isWorkspaceNameValid, setIsWorkspaceNameValid] = useState(false);
5649

57-
// Determine validity and notify parent
58-
useEffect(() => {
59-
const isValid =
60-
workspaceNameValidated === ValidatedOptions.success ||
61-
workspaceNameValidated === ValidatedOptions.warning;
62-
const imageUrl = selectedBackup ? selectedBackup.imageUrl : '';
63-
64-
if (isValid) {
65-
onValidationChange(true, { workspaceName, imageUrl });
66-
} else {
67-
onValidationChange(false, null);
68-
}
69-
// eslint-disable-next-line react-hooks/exhaustive-deps
70-
}, [selectedBackup, workspaceName, workspaceNameValidated]);
71-
72-
// Pre-select backup and populate workspace name from initialImageUrl
50+
// Pre-select backup from initialImageUrl
7351
useEffect(() => {
7452
if (!initialImageUrl || selectedBackup) {
7553
return;
7654
}
7755
const match = backups.find(b => b.imageUrl === initialImageUrl);
7856
if (match) {
7957
setSelectedBackup(match);
80-
setWorkspaceName(match.workspaceName);
8158
}
82-
}, [backups, initialImageUrl, selectedBackup, setWorkspaceName]);
59+
}, [backups, initialImageUrl, selectedBackup]);
60+
61+
// Notify parent whenever form validity changes
62+
useEffect(() => {
63+
const imageUrl = selectedBackup ? selectedBackup.imageUrl : '';
64+
if (isWorkspaceNameValid) {
65+
onValidationChange(true, { workspaceName, imageUrl });
66+
} else {
67+
onValidationChange(false, null);
68+
}
69+
// eslint-disable-next-line react-hooks/exhaustive-deps
70+
}, [selectedBackup, workspaceName, isWorkspaceNameValid]);
8371

8472
const handleBackupChange = (backup: BackupItem | undefined) => {
8573
setSelectedBackup(backup);
86-
if (backup) {
87-
setWorkspaceName(backup.workspaceName);
88-
}
8974
};
9075

91-
// Re-validate when existingWorkspaceNames changes
92-
// (e.g. a workspace is created or deleted while the form is open)
93-
useEffect(() => {
94-
if (workspaceName) {
95-
setWorkspaceName(workspaceName);
96-
}
97-
}, [existingWorkspaceNames, workspaceName, setWorkspaceName]);
76+
const handleWorkspaceNameChange = useCallback((name: string, isValid: boolean) => {
77+
setWorkspaceName(name);
78+
setIsWorkspaceNameValid(isValid);
79+
}, []);
9880

81+
const suggestedWorkspaceName = selectedBackup?.workspaceName ?? '';
9982
const imageUrl = selectedBackup ? selectedBackup.imageUrl : '';
10083

10184
return (
@@ -109,11 +92,10 @@ export const DefaultRegistryRestoreForm: React.FC<Props> = ({
10992
<WorkspaceNameField
11093
fieldId="restore-workspace-name"
11194
helperText="Enter the name of the workspace to restore."
112-
value={workspaceName}
113-
validated={workspaceNameValidated}
114-
error={workspaceNameError}
115-
warning={workspaceNameWarning}
116-
onChange={value => handleWorkspaceNameChange(value)}
95+
value={suggestedWorkspaceName}
96+
existingWorkspaceNames={existingWorkspaceNames}
97+
existingBackupNames={existingBackupNames}
98+
onChange={handleWorkspaceNameChange}
11799
actionButton={actionButton}
118100
/>
119101

packages/dashboard-frontend/src/pages/RestoreFromBackup/ExternalRegistryForm/__tests__/index.spec.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,18 @@ describe('ExternalRegistryRestoreForm', () => {
109109

110110
expect(mockOnValidationChange).toHaveBeenCalledWith(false, null);
111111
});
112+
113+
test('should NOT reset workspace name when backup image URL changes after manual edit', async () => {
114+
renderComponent();
115+
116+
const nameInput = screen.getByLabelText('Workspace name');
117+
await userEvent.type(nameInput, 'my-custom-name');
118+
119+
const urlInput = screen.getByLabelText('Backup image URL');
120+
await userEvent.type(urlInput, 'registry.example.com/ns/workspace:latest');
121+
122+
expect(nameInput).toHaveValue('my-custom-name');
123+
});
112124
});
113125

114126
type ComponentOptions = {

packages/dashboard-frontend/src/pages/RestoreFromBackup/ExternalRegistryForm/index.tsx

Lines changed: 21 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,10 @@
1414

1515
import { BACKUP_IMAGE_URL_PATTERN } from '@eclipse-che/common';
1616
import { Form, ValidatedOptions } from '@patternfly/react-core';
17-
import React, { useState } from 'react';
17+
import React, { useCallback, useEffect, useState } from 'react';
1818

1919
import { ImageUrlField } from '@/pages/RestoreFromBackup/ExternalRegistryForm/ImageUrlField';
20-
import {
21-
sanitizeImageUrl,
22-
sanitizeWorkspaceName,
23-
validateWorkspaceName,
24-
ValidationState,
25-
} from '@/pages/RestoreFromBackup/helpers';
26-
import { useRestoreFormValidation } from '@/pages/RestoreFromBackup/useRestoreFormValidation';
20+
import { sanitizeImageUrl, ValidationState } from '@/pages/RestoreFromBackup/helpers';
2721
import { WorkspaceNameField } from '@/pages/RestoreFromBackup/WorkspaceNameField';
2822

2923
export type ExternalRegistryRestoreData = {
@@ -82,58 +76,29 @@ export const ExternalRegistryRestoreForm: React.FC<Props> = ({
8276
const initialUrl = initialImageUrl || '';
8377
const [imageUrl, setImageUrl] = useState(initialUrl);
8478

85-
const initialWorkspaceName = detectWorkspaceName(initialUrl);
86-
const {
87-
workspaceName,
88-
workspaceNameValidated,
89-
workspaceNameError,
90-
workspaceNameWarning,
91-
setWorkspaceName,
92-
handleWorkspaceNameChange,
93-
} = useRestoreFormValidation(existingWorkspaceNames, existingBackupNames);
94-
95-
// Initialize workspace name from initialImageUrl on first render
96-
React.useEffect(() => {
97-
if (initialWorkspaceName) {
98-
setWorkspaceName(initialWorkspaceName);
99-
}
100-
const { validationState } = computeImageUrlValidation(initialUrl);
101-
const wsIsValid = initialWorkspaceName.length > 0;
102-
const isValid = validationState === 'valid' && wsIsValid;
103-
onValidationChange(
104-
isValid,
105-
isValid ? { workspaceName: initialWorkspaceName, imageUrl: initialUrl } : null,
106-
);
107-
// eslint-disable-next-line react-hooks/exhaustive-deps
108-
}, []);
79+
const [workspaceName, setWorkspaceName] = useState('');
80+
const [isWorkspaceNameValid, setIsWorkspaceNameValid] = useState(false);
10981

110-
// Compute URL validation state during render (derived from imageUrl)
11182
const { validationState, imageUrlValidated, imageUrlError } = computeImageUrlValidation(imageUrl);
11283

84+
const detectedWorkspaceName = validationState === 'valid' ? detectWorkspaceName(imageUrl) : '';
85+
86+
// Notify parent whenever form validity changes
87+
useEffect(() => {
88+
const isValid = validationState === 'valid' && isWorkspaceNameValid;
89+
onValidationChange(isValid, isValid ? { workspaceName, imageUrl } : null);
90+
// eslint-disable-next-line react-hooks/exhaustive-deps
91+
}, [imageUrl, workspaceName, isWorkspaceNameValid, validationState]);
92+
11393
const handleImageUrlChange = (value: string) => {
11494
const sanitized = sanitizeImageUrl(value);
11595
setImageUrl(sanitized);
116-
117-
const { validationState: newValidationState } = computeImageUrlValidation(sanitized);
118-
const newWorkspaceName = newValidationState === 'valid' ? detectWorkspaceName(sanitized) : '';
119-
setWorkspaceName(newWorkspaceName);
120-
121-
const isValid = newValidationState === 'valid' && newWorkspaceName.length > 0;
122-
onValidationChange(
123-
isValid,
124-
isValid ? { workspaceName: newWorkspaceName, imageUrl: sanitized } : null,
125-
);
12696
};
12797

128-
const wrappedHandleWorkspaceNameChange = (value: string) => {
129-
handleWorkspaceNameChange(value);
130-
const sanitized = sanitizeWorkspaceName(value);
131-
const { validated } = validateWorkspaceName(sanitized);
132-
const wsIsValid =
133-
validated === ValidatedOptions.success || validated === ValidatedOptions.warning;
134-
const isValid = validationState === 'valid' && wsIsValid;
135-
onValidationChange(isValid, isValid ? { workspaceName: sanitized, imageUrl } : null);
136-
};
98+
const handleWorkspaceNameChange = useCallback((name: string, isValid: boolean) => {
99+
setWorkspaceName(name);
100+
setIsWorkspaceNameValid(isValid);
101+
}, []);
137102

138103
return (
139104
<Form isHorizontal={true} onSubmit={e => e.preventDefault()}>
@@ -147,11 +112,10 @@ export const ExternalRegistryRestoreForm: React.FC<Props> = ({
147112
<WorkspaceNameField
148113
fieldId="restore-workspace-name-cross"
149114
helperText="Name for the restored workspace. Auto-detected from the backup image."
150-
value={workspaceName}
151-
validated={workspaceNameValidated}
152-
error={workspaceNameError}
153-
warning={workspaceNameWarning}
154-
onChange={wrappedHandleWorkspaceNameChange}
115+
value={detectedWorkspaceName}
116+
existingWorkspaceNames={existingWorkspaceNames}
117+
existingBackupNames={existingBackupNames}
118+
onChange={handleWorkspaceNameChange}
155119
actionButton={actionButton}
156120
/>
157121
</Form>

packages/dashboard-frontend/src/pages/RestoreFromBackup/WorkspaceNameField/__mocks__/index.tsx

Lines changed: 0 additions & 33 deletions
This file was deleted.

0 commit comments

Comments
 (0)