From 1c0c4dfc77ea329137a2ad2d489dace1f175433f Mon Sep 17 00:00:00 2001 From: main Date: Sat, 4 Oct 2025 11:47:58 +0200 Subject: [PATCH 01/29] refactor(ui): prepare migration --- eslint.config.mjs | 4 +++- package-lock.json | 18 +++++++++--------- package.json | 4 ++-- src/{App.jsx => App.tsx} | 0 .../{CLIEquivalent.jsx => CLIEquivalent.tsx} | 0 ...readcrumbs.jsx => DirectoryBreadcrumbs.tsx} | 0 .../{DirectoryItems.jsx => DirectoryItems.tsx} | 0 .../{GoBackButton.jsx => GoBackButton.tsx} | 0 .../{KopiaTable.jsx => KopiaTable.tsx} | 0 src/components/{Logs.jsx => Logs.tsx} | 0 ...licyEditorLink.jsx => PolicyEditorLink.tsx} | 0 ...SetupRepository.jsx => SetupRepository.tsx} | 0 ...itoryAzure.jsx => SetupRepositoryAzure.tsx} | 0 ...pRepositoryB2.jsx => SetupRepositoryB2.tsx} | 0 ...ystem.jsx => SetupRepositoryFilesystem.tsx} | 0 ...epositoryGCS.jsx => SetupRepositoryGCS.tsx} | 0 ...oryRclone.jsx => SetupRepositoryRclone.tsx} | 0 ...pRepositoryS3.jsx => SetupRepositoryS3.tsx} | 0 ...ositorySFTP.jsx => SetupRepositorySFTP.tsx} | 0 ...oryServer.jsx => SetupRepositoryServer.tsx} | 0 ...itoryToken.jsx => SetupRepositoryToken.tsx} | 0 ...oryWebDAV.jsx => SetupRepositoryWebDAV.tsx} | 0 ...otEstimation.jsx => SnapshotEstimation.tsx} | 0 ...nMethod.jsx => EmailNotificationMethod.tsx} | 0 ...cationEditor.jsx => NotificationEditor.tsx} | 0 ...ctor.jsx => NotificationFormatSelector.tsx} | 0 ...thod.jsx => PushoverNotificationMethod.tsx} | 0 ...ethod.jsx => WebHookNotificationMethod.tsx} | 0 .../{ActionRowMode.jsx => ActionRowMode.tsx} | 0 ...ActionRowScript.jsx => ActionRowScript.tsx} | 0 ...tionRowTimeout.jsx => ActionRowTimeout.tsx} | 0 ...leanValue.jsx => EffectiveBooleanValue.tsx} | 0 ...iveListValue.jsx => EffectiveListValue.tsx} | 0 ...reaValue.jsx => EffectiveTextAreaValue.tsx} | 0 ...yValue.jsx => EffectiveTimesOfDayValue.tsx} | 0 .../{EffectiveValue.jsx => EffectiveValue.tsx} | 0 ...alueColumn.jsx => EffectiveValueColumn.tsx} | 0 .../{LabelColumn.jsx => LabelColumn.tsx} | 0 .../{PolicyEditor.jsx => PolicyEditor.tsx} | 0 ...ctionHeaderRow.jsx => SectionHeaderRow.tsx} | 0 ...shotTimes.jsx => UpcomingSnapshotTimes.tsx} | 0 .../{ValueColumn.jsx => ValueColumn.tsx} | 0 ...WideValueColumn.jsx => WideValueColumn.tsx} | 0 ...etailSelector.jsx => LogDetailSelector.tsx} | 0 ...OptionalBoolean.jsx => OptionalBoolean.tsx} | 0 ...onalDirectory.jsx => OptionalDirectory.tsx} | 0 .../{OptionalField.jsx => OptionalField.tsx} | 0 ...eldNoLabel.jsx => OptionalFieldNoLabel.tsx} | 0 ...NumberField.jsx => OptionalNumberField.tsx} | 0 ...RequiredBoolean.jsx => RequiredBoolean.tsx} | 0 ...iredDirectory.jsx => RequiredDirectory.tsx} | 0 .../{RequiredField.jsx => RequiredField.tsx} | 0 ...NumberField.jsx => RequiredNumberField.tsx} | 0 src/forms/{StringList.jsx => StringList.tsx} | 0 .../{TimesOfDayList.jsx => TimesOfDayList.tsx} | 0 src/forms/{index.jsx => index.tsx} | 0 src/{index.jsx => index.tsx} | 0 src/pages/{Policies.jsx => Policies.tsx} | 0 src/pages/{Policy.jsx => Policy.tsx} | 0 src/pages/{Preferences.jsx => Preferences.tsx} | 0 src/pages/{Repository.jsx => Repository.tsx} | 0 .../{SnapshotCreate.jsx => SnapshotCreate.tsx} | 0 ...shotDirectory.jsx => SnapshotDirectory.tsx} | 0 ...SnapshotHistory.jsx => SnapshotHistory.tsx} | 0 ...SnapshotRestore.jsx => SnapshotRestore.tsx} | 0 src/pages/{Snapshots.jsx => Snapshots.tsx} | 0 src/pages/{Task.jsx => Task.tsx} | 0 src/pages/{Tasks.jsx => Tasks.tsx} | 0 src/{setupProxy.js => setupProxy.ts} | 0 src/utils/{deepstate.js => deepstate.ts} | 0 src/utils/{formatutils.js => formatutils.ts} | 0 src/utils/{policyutil.jsx => policyutil.tsx} | 0 src/utils/{taskutil.jsx => taskutil.tsx} | 0 src/utils/{uiutil.jsx => uiutil.tsx} | 0 tsconfig.json | 7 ++++--- 75 files changed, 18 insertions(+), 15 deletions(-) rename src/{App.jsx => App.tsx} (100%) rename src/components/{CLIEquivalent.jsx => CLIEquivalent.tsx} (100%) rename src/components/{DirectoryBreadcrumbs.jsx => DirectoryBreadcrumbs.tsx} (100%) rename src/components/{DirectoryItems.jsx => DirectoryItems.tsx} (100%) rename src/components/{GoBackButton.jsx => GoBackButton.tsx} (100%) rename src/components/{KopiaTable.jsx => KopiaTable.tsx} (100%) rename src/components/{Logs.jsx => Logs.tsx} (100%) rename src/components/{PolicyEditorLink.jsx => PolicyEditorLink.tsx} (100%) rename src/components/{SetupRepository.jsx => SetupRepository.tsx} (100%) rename src/components/{SetupRepositoryAzure.jsx => SetupRepositoryAzure.tsx} (100%) rename src/components/{SetupRepositoryB2.jsx => SetupRepositoryB2.tsx} (100%) rename src/components/{SetupRepositoryFilesystem.jsx => SetupRepositoryFilesystem.tsx} (100%) rename src/components/{SetupRepositoryGCS.jsx => SetupRepositoryGCS.tsx} (100%) rename src/components/{SetupRepositoryRclone.jsx => SetupRepositoryRclone.tsx} (100%) rename src/components/{SetupRepositoryS3.jsx => SetupRepositoryS3.tsx} (100%) rename src/components/{SetupRepositorySFTP.jsx => SetupRepositorySFTP.tsx} (100%) rename src/components/{SetupRepositoryServer.jsx => SetupRepositoryServer.tsx} (100%) rename src/components/{SetupRepositoryToken.jsx => SetupRepositoryToken.tsx} (100%) rename src/components/{SetupRepositoryWebDAV.jsx => SetupRepositoryWebDAV.tsx} (100%) rename src/components/{SnapshotEstimation.jsx => SnapshotEstimation.tsx} (100%) rename src/components/notifications/{EmailNotificationMethod.jsx => EmailNotificationMethod.tsx} (100%) rename src/components/notifications/{NotificationEditor.jsx => NotificationEditor.tsx} (100%) rename src/components/notifications/{NotificationFormatSelector.jsx => NotificationFormatSelector.tsx} (100%) rename src/components/notifications/{PushoverNotificationMethod.jsx => PushoverNotificationMethod.tsx} (100%) rename src/components/notifications/{WebHookNotificationMethod.jsx => WebHookNotificationMethod.tsx} (100%) rename src/components/policy-editor/{ActionRowMode.jsx => ActionRowMode.tsx} (100%) rename src/components/policy-editor/{ActionRowScript.jsx => ActionRowScript.tsx} (100%) rename src/components/policy-editor/{ActionRowTimeout.jsx => ActionRowTimeout.tsx} (100%) rename src/components/policy-editor/{EffectiveBooleanValue.jsx => EffectiveBooleanValue.tsx} (100%) rename src/components/policy-editor/{EffectiveListValue.jsx => EffectiveListValue.tsx} (100%) rename src/components/policy-editor/{EffectiveTextAreaValue.jsx => EffectiveTextAreaValue.tsx} (100%) rename src/components/policy-editor/{EffectiveTimesOfDayValue.jsx => EffectiveTimesOfDayValue.tsx} (100%) rename src/components/policy-editor/{EffectiveValue.jsx => EffectiveValue.tsx} (100%) rename src/components/policy-editor/{EffectiveValueColumn.jsx => EffectiveValueColumn.tsx} (100%) rename src/components/policy-editor/{LabelColumn.jsx => LabelColumn.tsx} (100%) rename src/components/policy-editor/{PolicyEditor.jsx => PolicyEditor.tsx} (100%) rename src/components/policy-editor/{SectionHeaderRow.jsx => SectionHeaderRow.tsx} (100%) rename src/components/policy-editor/{UpcomingSnapshotTimes.jsx => UpcomingSnapshotTimes.tsx} (100%) rename src/components/policy-editor/{ValueColumn.jsx => ValueColumn.tsx} (100%) rename src/components/policy-editor/{WideValueColumn.jsx => WideValueColumn.tsx} (100%) rename src/forms/{LogDetailSelector.jsx => LogDetailSelector.tsx} (100%) rename src/forms/{OptionalBoolean.jsx => OptionalBoolean.tsx} (100%) rename src/forms/{OptionalDirectory.jsx => OptionalDirectory.tsx} (100%) rename src/forms/{OptionalField.jsx => OptionalField.tsx} (100%) rename src/forms/{OptionalFieldNoLabel.jsx => OptionalFieldNoLabel.tsx} (100%) rename src/forms/{OptionalNumberField.jsx => OptionalNumberField.tsx} (100%) rename src/forms/{RequiredBoolean.jsx => RequiredBoolean.tsx} (100%) rename src/forms/{RequiredDirectory.jsx => RequiredDirectory.tsx} (100%) rename src/forms/{RequiredField.jsx => RequiredField.tsx} (100%) rename src/forms/{RequiredNumberField.jsx => RequiredNumberField.tsx} (100%) rename src/forms/{StringList.jsx => StringList.tsx} (100%) rename src/forms/{TimesOfDayList.jsx => TimesOfDayList.tsx} (100%) rename src/forms/{index.jsx => index.tsx} (100%) rename src/{index.jsx => index.tsx} (100%) rename src/pages/{Policies.jsx => Policies.tsx} (100%) rename src/pages/{Policy.jsx => Policy.tsx} (100%) rename src/pages/{Preferences.jsx => Preferences.tsx} (100%) rename src/pages/{Repository.jsx => Repository.tsx} (100%) rename src/pages/{SnapshotCreate.jsx => SnapshotCreate.tsx} (100%) rename src/pages/{SnapshotDirectory.jsx => SnapshotDirectory.tsx} (100%) rename src/pages/{SnapshotHistory.jsx => SnapshotHistory.tsx} (100%) rename src/pages/{SnapshotRestore.jsx => SnapshotRestore.tsx} (100%) rename src/pages/{Snapshots.jsx => Snapshots.tsx} (100%) rename src/pages/{Task.jsx => Task.tsx} (100%) rename src/pages/{Tasks.jsx => Tasks.tsx} (100%) rename src/{setupProxy.js => setupProxy.ts} (100%) rename src/utils/{deepstate.js => deepstate.ts} (100%) rename src/utils/{formatutils.js => formatutils.ts} (100%) rename src/utils/{policyutil.jsx => policyutil.tsx} (100%) rename src/utils/{taskutil.jsx => taskutil.tsx} (100%) rename src/utils/{uiutil.jsx => uiutil.tsx} (100%) diff --git a/eslint.config.mjs b/eslint.config.mjs index 069624e9..efb353f1 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -25,7 +25,9 @@ export default defineConfig([ }, { rules: { - "@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_", varsIgnorePattern: "^_" }], + "prefer-const": "warn", + "no-var": "warn", + "@typescript-eslint/no-unused-vars": ["warn", { argsIgnorePattern: "^_", varsIgnorePattern: "^_" }] }, }, pluginReact.configs.flat.recommended, diff --git a/package-lock.json b/package-lock.json index 320c326c..0810c682 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,8 +27,8 @@ "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.3.0", "@testing-library/user-event": "^14.6.1", - "@types/react": "^19.1.4", - "@types/react-dom": "^19.1.5", + "@types/react": "^19.2.0", + "@types/react-dom": "^19.2.0", "@vitejs/plugin-react": "^4.7.0", "@vitest/coverage-v8": "^3.1.4", "axios": "^1.12.1", @@ -1980,22 +1980,22 @@ "license": "MIT" }, "node_modules/@types/react": { - "version": "19.1.4", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.4.tgz", - "integrity": "sha512-EB1yiiYdvySuIITtD5lhW4yPyJ31RkJkkDw794LaQYrxCSaQV/47y5o1FMC4zF9ZyjUjzJMZwbovEnT5yHTW6g==", + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.0.tgz", + "integrity": "sha512-1LOH8xovvsKsCBq1wnT4ntDUdCJKmnEakhsuoUSy6ExlHCkGP2hqnatagYTgFk6oeL0VU31u7SNjunPN+GchtA==", "license": "MIT", "dependencies": { "csstype": "^3.0.2" } }, "node_modules/@types/react-dom": { - "version": "19.1.5", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.5.tgz", - "integrity": "sha512-CMCjrWucUBZvohgZxkjd6S9h0nZxXjzus6yDfUb+xLxYM7VvjKNH1tQrE9GWLql1XoOP4/Ds3bwFqShHUYraGg==", + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.0.tgz", + "integrity": "sha512-brtBs0MnE9SMx7px208g39lRmC5uHZs96caOJfTjFcYSLHNamvaSMfJNagChVNkup2SdtOxKX1FDBkRSJe1ZAg==", "dev": true, "license": "MIT", "peerDependencies": { - "@types/react": "^19.0.0" + "@types/react": "^19.2.0" } }, "node_modules/@types/react-transition-group": { diff --git a/package.json b/package.json index 03117169..bd209c8c 100644 --- a/package.json +++ b/package.json @@ -45,8 +45,8 @@ "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.3.0", "@testing-library/user-event": "^14.6.1", - "@types/react": "^19.1.4", - "@types/react-dom": "^19.1.5", + "@types/react": "^19.2.0", + "@types/react-dom": "^19.2.0", "@vitejs/plugin-react": "^4.7.0", "@vitest/coverage-v8": "^3.1.4", "axios": "^1.12.1", diff --git a/src/App.jsx b/src/App.tsx similarity index 100% rename from src/App.jsx rename to src/App.tsx diff --git a/src/components/CLIEquivalent.jsx b/src/components/CLIEquivalent.tsx similarity index 100% rename from src/components/CLIEquivalent.jsx rename to src/components/CLIEquivalent.tsx diff --git a/src/components/DirectoryBreadcrumbs.jsx b/src/components/DirectoryBreadcrumbs.tsx similarity index 100% rename from src/components/DirectoryBreadcrumbs.jsx rename to src/components/DirectoryBreadcrumbs.tsx diff --git a/src/components/DirectoryItems.jsx b/src/components/DirectoryItems.tsx similarity index 100% rename from src/components/DirectoryItems.jsx rename to src/components/DirectoryItems.tsx diff --git a/src/components/GoBackButton.jsx b/src/components/GoBackButton.tsx similarity index 100% rename from src/components/GoBackButton.jsx rename to src/components/GoBackButton.tsx diff --git a/src/components/KopiaTable.jsx b/src/components/KopiaTable.tsx similarity index 100% rename from src/components/KopiaTable.jsx rename to src/components/KopiaTable.tsx diff --git a/src/components/Logs.jsx b/src/components/Logs.tsx similarity index 100% rename from src/components/Logs.jsx rename to src/components/Logs.tsx diff --git a/src/components/PolicyEditorLink.jsx b/src/components/PolicyEditorLink.tsx similarity index 100% rename from src/components/PolicyEditorLink.jsx rename to src/components/PolicyEditorLink.tsx diff --git a/src/components/SetupRepository.jsx b/src/components/SetupRepository.tsx similarity index 100% rename from src/components/SetupRepository.jsx rename to src/components/SetupRepository.tsx diff --git a/src/components/SetupRepositoryAzure.jsx b/src/components/SetupRepositoryAzure.tsx similarity index 100% rename from src/components/SetupRepositoryAzure.jsx rename to src/components/SetupRepositoryAzure.tsx diff --git a/src/components/SetupRepositoryB2.jsx b/src/components/SetupRepositoryB2.tsx similarity index 100% rename from src/components/SetupRepositoryB2.jsx rename to src/components/SetupRepositoryB2.tsx diff --git a/src/components/SetupRepositoryFilesystem.jsx b/src/components/SetupRepositoryFilesystem.tsx similarity index 100% rename from src/components/SetupRepositoryFilesystem.jsx rename to src/components/SetupRepositoryFilesystem.tsx diff --git a/src/components/SetupRepositoryGCS.jsx b/src/components/SetupRepositoryGCS.tsx similarity index 100% rename from src/components/SetupRepositoryGCS.jsx rename to src/components/SetupRepositoryGCS.tsx diff --git a/src/components/SetupRepositoryRclone.jsx b/src/components/SetupRepositoryRclone.tsx similarity index 100% rename from src/components/SetupRepositoryRclone.jsx rename to src/components/SetupRepositoryRclone.tsx diff --git a/src/components/SetupRepositoryS3.jsx b/src/components/SetupRepositoryS3.tsx similarity index 100% rename from src/components/SetupRepositoryS3.jsx rename to src/components/SetupRepositoryS3.tsx diff --git a/src/components/SetupRepositorySFTP.jsx b/src/components/SetupRepositorySFTP.tsx similarity index 100% rename from src/components/SetupRepositorySFTP.jsx rename to src/components/SetupRepositorySFTP.tsx diff --git a/src/components/SetupRepositoryServer.jsx b/src/components/SetupRepositoryServer.tsx similarity index 100% rename from src/components/SetupRepositoryServer.jsx rename to src/components/SetupRepositoryServer.tsx diff --git a/src/components/SetupRepositoryToken.jsx b/src/components/SetupRepositoryToken.tsx similarity index 100% rename from src/components/SetupRepositoryToken.jsx rename to src/components/SetupRepositoryToken.tsx diff --git a/src/components/SetupRepositoryWebDAV.jsx b/src/components/SetupRepositoryWebDAV.tsx similarity index 100% rename from src/components/SetupRepositoryWebDAV.jsx rename to src/components/SetupRepositoryWebDAV.tsx diff --git a/src/components/SnapshotEstimation.jsx b/src/components/SnapshotEstimation.tsx similarity index 100% rename from src/components/SnapshotEstimation.jsx rename to src/components/SnapshotEstimation.tsx diff --git a/src/components/notifications/EmailNotificationMethod.jsx b/src/components/notifications/EmailNotificationMethod.tsx similarity index 100% rename from src/components/notifications/EmailNotificationMethod.jsx rename to src/components/notifications/EmailNotificationMethod.tsx diff --git a/src/components/notifications/NotificationEditor.jsx b/src/components/notifications/NotificationEditor.tsx similarity index 100% rename from src/components/notifications/NotificationEditor.jsx rename to src/components/notifications/NotificationEditor.tsx diff --git a/src/components/notifications/NotificationFormatSelector.jsx b/src/components/notifications/NotificationFormatSelector.tsx similarity index 100% rename from src/components/notifications/NotificationFormatSelector.jsx rename to src/components/notifications/NotificationFormatSelector.tsx diff --git a/src/components/notifications/PushoverNotificationMethod.jsx b/src/components/notifications/PushoverNotificationMethod.tsx similarity index 100% rename from src/components/notifications/PushoverNotificationMethod.jsx rename to src/components/notifications/PushoverNotificationMethod.tsx diff --git a/src/components/notifications/WebHookNotificationMethod.jsx b/src/components/notifications/WebHookNotificationMethod.tsx similarity index 100% rename from src/components/notifications/WebHookNotificationMethod.jsx rename to src/components/notifications/WebHookNotificationMethod.tsx diff --git a/src/components/policy-editor/ActionRowMode.jsx b/src/components/policy-editor/ActionRowMode.tsx similarity index 100% rename from src/components/policy-editor/ActionRowMode.jsx rename to src/components/policy-editor/ActionRowMode.tsx diff --git a/src/components/policy-editor/ActionRowScript.jsx b/src/components/policy-editor/ActionRowScript.tsx similarity index 100% rename from src/components/policy-editor/ActionRowScript.jsx rename to src/components/policy-editor/ActionRowScript.tsx diff --git a/src/components/policy-editor/ActionRowTimeout.jsx b/src/components/policy-editor/ActionRowTimeout.tsx similarity index 100% rename from src/components/policy-editor/ActionRowTimeout.jsx rename to src/components/policy-editor/ActionRowTimeout.tsx diff --git a/src/components/policy-editor/EffectiveBooleanValue.jsx b/src/components/policy-editor/EffectiveBooleanValue.tsx similarity index 100% rename from src/components/policy-editor/EffectiveBooleanValue.jsx rename to src/components/policy-editor/EffectiveBooleanValue.tsx diff --git a/src/components/policy-editor/EffectiveListValue.jsx b/src/components/policy-editor/EffectiveListValue.tsx similarity index 100% rename from src/components/policy-editor/EffectiveListValue.jsx rename to src/components/policy-editor/EffectiveListValue.tsx diff --git a/src/components/policy-editor/EffectiveTextAreaValue.jsx b/src/components/policy-editor/EffectiveTextAreaValue.tsx similarity index 100% rename from src/components/policy-editor/EffectiveTextAreaValue.jsx rename to src/components/policy-editor/EffectiveTextAreaValue.tsx diff --git a/src/components/policy-editor/EffectiveTimesOfDayValue.jsx b/src/components/policy-editor/EffectiveTimesOfDayValue.tsx similarity index 100% rename from src/components/policy-editor/EffectiveTimesOfDayValue.jsx rename to src/components/policy-editor/EffectiveTimesOfDayValue.tsx diff --git a/src/components/policy-editor/EffectiveValue.jsx b/src/components/policy-editor/EffectiveValue.tsx similarity index 100% rename from src/components/policy-editor/EffectiveValue.jsx rename to src/components/policy-editor/EffectiveValue.tsx diff --git a/src/components/policy-editor/EffectiveValueColumn.jsx b/src/components/policy-editor/EffectiveValueColumn.tsx similarity index 100% rename from src/components/policy-editor/EffectiveValueColumn.jsx rename to src/components/policy-editor/EffectiveValueColumn.tsx diff --git a/src/components/policy-editor/LabelColumn.jsx b/src/components/policy-editor/LabelColumn.tsx similarity index 100% rename from src/components/policy-editor/LabelColumn.jsx rename to src/components/policy-editor/LabelColumn.tsx diff --git a/src/components/policy-editor/PolicyEditor.jsx b/src/components/policy-editor/PolicyEditor.tsx similarity index 100% rename from src/components/policy-editor/PolicyEditor.jsx rename to src/components/policy-editor/PolicyEditor.tsx diff --git a/src/components/policy-editor/SectionHeaderRow.jsx b/src/components/policy-editor/SectionHeaderRow.tsx similarity index 100% rename from src/components/policy-editor/SectionHeaderRow.jsx rename to src/components/policy-editor/SectionHeaderRow.tsx diff --git a/src/components/policy-editor/UpcomingSnapshotTimes.jsx b/src/components/policy-editor/UpcomingSnapshotTimes.tsx similarity index 100% rename from src/components/policy-editor/UpcomingSnapshotTimes.jsx rename to src/components/policy-editor/UpcomingSnapshotTimes.tsx diff --git a/src/components/policy-editor/ValueColumn.jsx b/src/components/policy-editor/ValueColumn.tsx similarity index 100% rename from src/components/policy-editor/ValueColumn.jsx rename to src/components/policy-editor/ValueColumn.tsx diff --git a/src/components/policy-editor/WideValueColumn.jsx b/src/components/policy-editor/WideValueColumn.tsx similarity index 100% rename from src/components/policy-editor/WideValueColumn.jsx rename to src/components/policy-editor/WideValueColumn.tsx diff --git a/src/forms/LogDetailSelector.jsx b/src/forms/LogDetailSelector.tsx similarity index 100% rename from src/forms/LogDetailSelector.jsx rename to src/forms/LogDetailSelector.tsx diff --git a/src/forms/OptionalBoolean.jsx b/src/forms/OptionalBoolean.tsx similarity index 100% rename from src/forms/OptionalBoolean.jsx rename to src/forms/OptionalBoolean.tsx diff --git a/src/forms/OptionalDirectory.jsx b/src/forms/OptionalDirectory.tsx similarity index 100% rename from src/forms/OptionalDirectory.jsx rename to src/forms/OptionalDirectory.tsx diff --git a/src/forms/OptionalField.jsx b/src/forms/OptionalField.tsx similarity index 100% rename from src/forms/OptionalField.jsx rename to src/forms/OptionalField.tsx diff --git a/src/forms/OptionalFieldNoLabel.jsx b/src/forms/OptionalFieldNoLabel.tsx similarity index 100% rename from src/forms/OptionalFieldNoLabel.jsx rename to src/forms/OptionalFieldNoLabel.tsx diff --git a/src/forms/OptionalNumberField.jsx b/src/forms/OptionalNumberField.tsx similarity index 100% rename from src/forms/OptionalNumberField.jsx rename to src/forms/OptionalNumberField.tsx diff --git a/src/forms/RequiredBoolean.jsx b/src/forms/RequiredBoolean.tsx similarity index 100% rename from src/forms/RequiredBoolean.jsx rename to src/forms/RequiredBoolean.tsx diff --git a/src/forms/RequiredDirectory.jsx b/src/forms/RequiredDirectory.tsx similarity index 100% rename from src/forms/RequiredDirectory.jsx rename to src/forms/RequiredDirectory.tsx diff --git a/src/forms/RequiredField.jsx b/src/forms/RequiredField.tsx similarity index 100% rename from src/forms/RequiredField.jsx rename to src/forms/RequiredField.tsx diff --git a/src/forms/RequiredNumberField.jsx b/src/forms/RequiredNumberField.tsx similarity index 100% rename from src/forms/RequiredNumberField.jsx rename to src/forms/RequiredNumberField.tsx diff --git a/src/forms/StringList.jsx b/src/forms/StringList.tsx similarity index 100% rename from src/forms/StringList.jsx rename to src/forms/StringList.tsx diff --git a/src/forms/TimesOfDayList.jsx b/src/forms/TimesOfDayList.tsx similarity index 100% rename from src/forms/TimesOfDayList.jsx rename to src/forms/TimesOfDayList.tsx diff --git a/src/forms/index.jsx b/src/forms/index.tsx similarity index 100% rename from src/forms/index.jsx rename to src/forms/index.tsx diff --git a/src/index.jsx b/src/index.tsx similarity index 100% rename from src/index.jsx rename to src/index.tsx diff --git a/src/pages/Policies.jsx b/src/pages/Policies.tsx similarity index 100% rename from src/pages/Policies.jsx rename to src/pages/Policies.tsx diff --git a/src/pages/Policy.jsx b/src/pages/Policy.tsx similarity index 100% rename from src/pages/Policy.jsx rename to src/pages/Policy.tsx diff --git a/src/pages/Preferences.jsx b/src/pages/Preferences.tsx similarity index 100% rename from src/pages/Preferences.jsx rename to src/pages/Preferences.tsx diff --git a/src/pages/Repository.jsx b/src/pages/Repository.tsx similarity index 100% rename from src/pages/Repository.jsx rename to src/pages/Repository.tsx diff --git a/src/pages/SnapshotCreate.jsx b/src/pages/SnapshotCreate.tsx similarity index 100% rename from src/pages/SnapshotCreate.jsx rename to src/pages/SnapshotCreate.tsx diff --git a/src/pages/SnapshotDirectory.jsx b/src/pages/SnapshotDirectory.tsx similarity index 100% rename from src/pages/SnapshotDirectory.jsx rename to src/pages/SnapshotDirectory.tsx diff --git a/src/pages/SnapshotHistory.jsx b/src/pages/SnapshotHistory.tsx similarity index 100% rename from src/pages/SnapshotHistory.jsx rename to src/pages/SnapshotHistory.tsx diff --git a/src/pages/SnapshotRestore.jsx b/src/pages/SnapshotRestore.tsx similarity index 100% rename from src/pages/SnapshotRestore.jsx rename to src/pages/SnapshotRestore.tsx diff --git a/src/pages/Snapshots.jsx b/src/pages/Snapshots.tsx similarity index 100% rename from src/pages/Snapshots.jsx rename to src/pages/Snapshots.tsx diff --git a/src/pages/Task.jsx b/src/pages/Task.tsx similarity index 100% rename from src/pages/Task.jsx rename to src/pages/Task.tsx diff --git a/src/pages/Tasks.jsx b/src/pages/Tasks.tsx similarity index 100% rename from src/pages/Tasks.jsx rename to src/pages/Tasks.tsx diff --git a/src/setupProxy.js b/src/setupProxy.ts similarity index 100% rename from src/setupProxy.js rename to src/setupProxy.ts diff --git a/src/utils/deepstate.js b/src/utils/deepstate.ts similarity index 100% rename from src/utils/deepstate.js rename to src/utils/deepstate.ts diff --git a/src/utils/formatutils.js b/src/utils/formatutils.ts similarity index 100% rename from src/utils/formatutils.js rename to src/utils/formatutils.ts diff --git a/src/utils/policyutil.jsx b/src/utils/policyutil.tsx similarity index 100% rename from src/utils/policyutil.jsx rename to src/utils/policyutil.tsx diff --git a/src/utils/taskutil.jsx b/src/utils/taskutil.tsx similarity index 100% rename from src/utils/taskutil.jsx rename to src/utils/taskutil.tsx diff --git a/src/utils/uiutil.jsx b/src/utils/uiutil.tsx similarity index 100% rename from src/utils/uiutil.jsx rename to src/utils/uiutil.tsx diff --git a/tsconfig.json b/tsconfig.json index bd9c392f..14cf8438 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,7 +4,7 @@ "lib": ["dom", "dom.iterable", "esnext"], "allowJs": false, "skipLibCheck": true, - "esModuleInterop": false, + "esModuleInterop": true, "allowSyntheticDefaultImports": true, "strict": true, "forceConsistentCasingInFileNames": true, @@ -14,9 +14,10 @@ "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, - "jsx": "react-jsx", + "jsx": "react", "baseUrl": "./", "paths": {} }, - "include": ["src"] + "include": ["src/**/*"], + "exclude": ["node_modules", "build", "dist"] } From 7ad35ba0f19b74c723aaf450d4ce1744375a5b61 Mon Sep 17 00:00:00 2001 From: main Date: Sat, 4 Oct 2025 13:40:19 +0200 Subject: [PATCH 02/29] refactor(ui): Add types to various functions --- eslint.config.mjs | 3 +- src/App.tsx | 28 +++++++++++++------ .../policy-editor/EffectiveBooleanValue.tsx | 2 +- src/contexts/UIPreferencesContext.tsx | 2 +- src/forms/LogDetailSelector.tsx | 2 +- src/forms/OptionalBoolean.tsx | 2 +- src/forms/OptionalDirectory.tsx | 2 +- src/forms/OptionalField.tsx | 2 +- src/forms/OptionalFieldNoLabel.tsx | 2 +- src/forms/OptionalNumberField.tsx | 2 +- src/forms/RequiredBoolean.tsx | 2 +- src/forms/RequiredDirectory.tsx | 2 +- src/forms/RequiredField.tsx | 2 +- src/forms/RequiredNumberField.tsx | 2 +- src/forms/StringList.tsx | 2 +- src/forms/index.tsx | 4 +-- src/index.tsx | 4 +-- src/utils/deepstate.ts | 6 ++-- 18 files changed, 42 insertions(+), 29 deletions(-) diff --git a/eslint.config.mjs b/eslint.config.mjs index efb353f1..af90e0ae 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -27,7 +27,8 @@ export default defineConfig([ rules: { "prefer-const": "warn", "no-var": "warn", - "@typescript-eslint/no-unused-vars": ["warn", { argsIgnorePattern: "^_", varsIgnorePattern: "^_" }] + "@typescript-eslint/no-unused-vars": ["warn", { argsIgnorePattern: "^_", varsIgnorePattern: "^_" }], + "@typescript-eslint/no-explicit-any": "warn", }, }, pluginReact.configs.flat.recommended, diff --git a/src/App.tsx b/src/App.tsx index 0e6a6153..c0931da9 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -2,7 +2,7 @@ import "bootstrap/dist/css/bootstrap.min.css"; import "./css/Theme.css"; import "./css/App.css"; import axios from "axios"; -import { React, Component } from "react"; +import React, { Component } from "react"; import { Navbar, Nav, Container } from "react-bootstrap"; import { BrowserRouter as Router, NavLink, Navigate, Route, Routes } from "react-router-dom"; import { Policy } from "./pages/Policy"; @@ -19,7 +19,17 @@ import { SnapshotRestore } from "./pages/SnapshotRestore"; import { AppContext } from "./contexts/AppContext"; import { UIPreferenceProvider } from "./contexts/UIPreferencesContext"; -export default class App extends Component { +type AppState = { + runningTaskCount: number; + isFetching: boolean; + repoDescription: string; + isRepositoryConnected: boolean; + uiPrefs?: any; +}; + +export default class App extends Component { + taskSummaryInterval?: number; + constructor() { super(); @@ -35,7 +45,7 @@ export default class App extends Component { this.repositoryDescriptionUpdated = this.repositoryDescriptionUpdated.bind(this); this.fetchInitialRepositoryDescription = this.fetchInitialRepositoryDescription.bind(this); - const tok = document.head.querySelector('meta[name="kopia-csrf-token"]'); + const tok = document.head.querySelector('meta[name="kopia-csrf-token"]') as HTMLMetaElement | null; if (tok && tok.content) { axios.defaults.headers.common["X-Kopia-Csrf-Token"] = tok.content; } else { @@ -54,7 +64,7 @@ export default class App extends Component { this.taskSummaryInterval = window.setInterval(this.fetchTaskSummary, 5000); } - fetchInitialRepositoryDescription() { + fetchInitialRepositoryDescription(): void { axios .get("/api/v1/repo/status") .then((result) => { @@ -70,7 +80,7 @@ export default class App extends Component { }); } - fetchTaskSummary() { + fetchTaskSummary(): void { if (!this.state.isFetching) { this.setState({ isFetching: true }); axios @@ -87,12 +97,12 @@ export default class App extends Component { } } - componentWillUnmount() { + componentWillUnmount(): void { window.clearInterval(this.taskSummaryInterval); } // this is invoked via AppContext whenever repository is connected, disconnected, etc. - repositoryUpdated(isConnected) { + repositoryUpdated(isConnected: boolean): void { this.setState({ isRepositoryConnected: isConnected }); if (isConnected) { window.location.replace("/snapshots"); @@ -101,7 +111,7 @@ export default class App extends Component { } } - repositoryDescriptionUpdated(desc) { + repositoryDescriptionUpdated(desc: string): void { this.setState({ repoDescription: desc, }); @@ -112,7 +122,7 @@ export default class App extends Component { return ( - + diff --git a/src/components/policy-editor/EffectiveBooleanValue.tsx b/src/components/policy-editor/EffectiveBooleanValue.tsx index 7986867f..0894bd21 100644 --- a/src/components/policy-editor/EffectiveBooleanValue.tsx +++ b/src/components/policy-editor/EffectiveBooleanValue.tsx @@ -3,7 +3,7 @@ import Form from "react-bootstrap/Form"; import { getDeepStateProperty } from "../../utils/deepstate"; import { EffectiveValueColumn } from "./EffectiveValueColumn"; -export function EffectiveBooleanValue(component, policyField) { +export function EffectiveBooleanValue(component, policyField: string) { const dsp = getDeepStateProperty(component, "resolved.definition." + policyField, undefined); return ( diff --git a/src/contexts/UIPreferencesContext.tsx b/src/contexts/UIPreferencesContext.tsx index 985f80c9..cb08df6f 100644 --- a/src/contexts/UIPreferencesContext.tsx +++ b/src/contexts/UIPreferencesContext.tsx @@ -1,7 +1,7 @@ import React, { ReactNode, useCallback, useEffect, useState } from "react"; import axios from "axios"; -export const PAGE_SIZES = [10, 20, 30, 40, 50, 100]; +export const PAGE_SIZES: PageSize[] = [10, 20, 30, 40, 50, 100]; export const UIPreferencesContext = React.createContext({} as UIPreferences); const DEFAULT_PREFERENCES = { diff --git a/src/forms/LogDetailSelector.tsx b/src/forms/LogDetailSelector.tsx index 97ebdec2..ca22f1b6 100644 --- a/src/forms/LogDetailSelector.tsx +++ b/src/forms/LogDetailSelector.tsx @@ -2,7 +2,7 @@ import React from "react"; import Form from "react-bootstrap/Form"; import { valueToNumber, stateProperty } from "."; -export function LogDetailSelector(component, name) { +export function LogDetailSelector(component, name: string) { return ( {label && {label}} diff --git a/src/forms/OptionalDirectory.tsx b/src/forms/OptionalDirectory.tsx index d2e60c61..16a97049 100644 --- a/src/forms/OptionalDirectory.tsx +++ b/src/forms/OptionalDirectory.tsx @@ -22,7 +22,7 @@ import { setDeepStateProperty } from "../utils/deepstate"; * Additional properties of the component * @returns The form group with the components */ -export function OptionalDirectory(component, label, name, props = {}) { +export function OptionalDirectory(component, label: string, name: string, props = {}) { /** * Saves the selected path as a deepstate variable within the component * @param {The path that has been selected} path diff --git a/src/forms/OptionalField.tsx b/src/forms/OptionalField.tsx index 4b0e7e65..c084a28a 100644 --- a/src/forms/OptionalField.tsx +++ b/src/forms/OptionalField.tsx @@ -3,7 +3,7 @@ import Form from "react-bootstrap/Form"; import Col from "react-bootstrap/Col"; import { stateProperty } from "."; -export function OptionalField(component, label, name, props = {}, helpText = null) { +export function OptionalField(component, label: string, name: string, props = {}, helpText = null) { return ( {label} diff --git a/src/forms/OptionalFieldNoLabel.tsx b/src/forms/OptionalFieldNoLabel.tsx index 49d1db2f..fbe24a09 100644 --- a/src/forms/OptionalFieldNoLabel.tsx +++ b/src/forms/OptionalFieldNoLabel.tsx @@ -4,7 +4,7 @@ import Col from "react-bootstrap/Col"; import { stateProperty } from "."; -export function OptionalFieldNoLabel(component, label, name, props = {}, helpText = null, invalidFeedback = null) { +export function OptionalFieldNoLabel(component, label: string, name: string, props = {}, helpText = null, invalidFeedback = null) { return ( {label && {label}} diff --git a/src/forms/RequiredBoolean.tsx b/src/forms/RequiredBoolean.tsx index 841bba85..7dccd96a 100644 --- a/src/forms/RequiredBoolean.tsx +++ b/src/forms/RequiredBoolean.tsx @@ -11,7 +11,7 @@ function checkedToBool(t) { return false; } -export function RequiredBoolean(component, label, name, helpText) { +export function RequiredBoolean(component, label: string, name: string, helpText?: string) { return ( {label} diff --git a/src/forms/RequiredNumberField.tsx b/src/forms/RequiredNumberField.tsx index df3e3e37..e71ee566 100644 --- a/src/forms/RequiredNumberField.tsx +++ b/src/forms/RequiredNumberField.tsx @@ -3,7 +3,7 @@ import Form from "react-bootstrap/Form"; import Col from "react-bootstrap/Col"; import { stateProperty, isInvalidNumber, valueToNumber } from "."; -export function RequiredNumberField(component, label, name, props = {}) { +export function RequiredNumberField(component, label: string, name: string, props = {}) { return ( {label} diff --git a/src/forms/StringList.tsx b/src/forms/StringList.tsx index 1aaaf6ff..273696bf 100644 --- a/src/forms/StringList.tsx +++ b/src/forms/StringList.tsx @@ -20,7 +20,7 @@ export function multilineStringToList(target) { return v.split(/\n/); } -export function StringList(component, name, props = {}) { +export function StringList(component, name: string, props = {}) { return ( x.value) { setDeepStateProperty(this, event.target.name, valueGetter(event.target)); } -export function stateProperty(component, name, defaultValue = "") { +export function stateProperty(component, name: string, defaultValue = "") { const value = getDeepStateProperty(component, name); return value === undefined ? defaultValue : value; } @@ -44,7 +44,7 @@ export function valueToNumber(t) { return v; } -export function isInvalidNumber(v) { +export function isInvalidNumber(v: any) { if (v === undefined || v === "") { return false; } diff --git a/src/index.tsx b/src/index.tsx index fccc23cb..195cb560 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,7 +1,7 @@ import React from "react"; -import { createRoot } from "react-dom/client"; +import { Container, createRoot } from "react-dom/client"; import App from "./App"; import "./css/index.css"; -const root = createRoot(document.getElementById("root")); +const root = createRoot(document.getElementById("root") as Container); root.render(); diff --git a/src/utils/deepstate.ts b/src/utils/deepstate.ts index ec9a9c15..7f11b8b8 100644 --- a/src/utils/deepstate.ts +++ b/src/utils/deepstate.ts @@ -6,7 +6,7 @@ // getDeepStateProperty("a.b") returns {"c":true} // getDeepStateProperty("a.b.c") returns true -export function setDeepStateProperty(component, name, value) { +export function setDeepStateProperty(component: HasState, name: string, value: any) { let newState = { ...component.state }; let st = newState; @@ -30,12 +30,14 @@ export function setDeepStateProperty(component, name, value) { component.setState(newState); } +type HasState = { state: any; setState: (s: any) => void }; + // getDeepStateProperty returns the provided deep state property or a default value // For example: { "a": { "b": { "c": true } } } // getDeepStateProperty("a") returns {b":{"c":true}} // getDeepStateProperty("a.b") returns {"c":true} // getDeepStateProperty("a.b.c") returns true -export function getDeepStateProperty(component, name, defaultValue = "") { +export function getDeepStateProperty(component: HasState, name: string, defaultValue = "") { let st = component.state; const parts = name.split(/\./); From e5ed88953317a38933f869abe95a8ee22b525f24 Mon Sep 17 00:00:00 2001 From: main Date: Sat, 4 Oct 2025 14:25:12 +0200 Subject: [PATCH 03/29] refactor(ui): Add types 2 --- src/forms/OptionalBoolean.tsx | 2 +- src/forms/StringList.tsx | 4 ++-- src/forms/TimesOfDayList.tsx | 6 +++--- src/forms/index.tsx | 7 ++++--- src/utils/deepstate.ts | 4 ++-- src/utils/formatutils.ts | 37 +++++++++++++++++++---------------- src/utils/policyutil.tsx | 4 ++-- src/utils/uiutil.tsx | 2 +- 8 files changed, 35 insertions(+), 31 deletions(-) diff --git a/src/forms/OptionalBoolean.tsx b/src/forms/OptionalBoolean.tsx index 00ad04ec..45af3acf 100644 --- a/src/forms/OptionalBoolean.tsx +++ b/src/forms/OptionalBoolean.tsx @@ -4,7 +4,7 @@ import Col from "react-bootstrap/Col"; import { stateProperty } from "."; -function optionalBooleanValue(target) { +function optionalBooleanValue(target: HTMLSelectElement): boolean | undefined { if (target.value === "true") { return true; } diff --git a/src/forms/StringList.tsx b/src/forms/StringList.tsx index 273696bf..932785e4 100644 --- a/src/forms/StringList.tsx +++ b/src/forms/StringList.tsx @@ -3,7 +3,7 @@ import Form from "react-bootstrap/Form"; import Col from "react-bootstrap/Col"; import { stateProperty } from "."; -export function listToMultilineString(v) { +export function listToMultilineString(v): string { if (v) { return v.join("\n"); } @@ -11,7 +11,7 @@ export function listToMultilineString(v) { return ""; } -export function multilineStringToList(target) { +export function multilineStringToList(target: HTMLTextAreaElement): string[] | undefined { const v = target.value; if (v === "") { return undefined; diff --git a/src/forms/TimesOfDayList.tsx b/src/forms/TimesOfDayList.tsx index d9ed820b..f3fda9b2 100644 --- a/src/forms/TimesOfDayList.tsx +++ b/src/forms/TimesOfDayList.tsx @@ -3,9 +3,9 @@ import Form from "react-bootstrap/Form"; import FormGroup from "react-bootstrap/FormGroup"; import { stateProperty } from "."; -export function TimesOfDayList(component, name, props = {}) { - function parseTimeOfDay(v) { - var re = /(\d+):(\d+)/; +export function TimesOfDayList(component, name: string, props = {}) { + function parseTimeOfDay(v: string) { + const re = /(\d+):(\d+)/; const match = re.exec(v); if (match) { diff --git a/src/forms/index.tsx b/src/forms/index.tsx index 2aa3544f..ad93028c 100644 --- a/src/forms/index.tsx +++ b/src/forms/index.tsx @@ -1,6 +1,7 @@ +import { Component } from "react"; import { getDeepStateProperty, setDeepStateProperty } from "../utils/deepstate"; -export function validateRequiredFields(component, fields) { +export function validateRequiredFields(component, fields: any[]) { let updateState = {}; let failed = false; @@ -26,7 +27,7 @@ export function handleChange(event, valueGetter = (x) => x.value) { setDeepStateProperty(this, event.target.name, valueGetter(event.target)); } -export function stateProperty(component, name: string, defaultValue = "") { +export function stateProperty(component: Component, name: string, defaultValue = "") { const value = getDeepStateProperty(component, name); return value === undefined ? defaultValue : value; } @@ -44,7 +45,7 @@ export function valueToNumber(t) { return v; } -export function isInvalidNumber(v: any) { +export function isInvalidNumber(v: any): boolean { if (v === undefined || v === "") { return false; } diff --git a/src/utils/deepstate.ts b/src/utils/deepstate.ts index 7f11b8b8..acfc1102 100644 --- a/src/utils/deepstate.ts +++ b/src/utils/deepstate.ts @@ -6,7 +6,7 @@ // getDeepStateProperty("a.b") returns {"c":true} // getDeepStateProperty("a.b.c") returns true -export function setDeepStateProperty(component: HasState, name: string, value: any) { +export function setDeepStateProperty(component: HasState, name: string, value: any): void { let newState = { ...component.state }; let st = newState; @@ -37,7 +37,7 @@ type HasState = { state: any; setState: (s: any) => void }; // getDeepStateProperty("a") returns {b":{"c":true}} // getDeepStateProperty("a.b") returns {"c":true} // getDeepStateProperty("a.b.c") returns true -export function getDeepStateProperty(component: HasState, name: string, defaultValue = "") { +export function getDeepStateProperty(component: HasState, name: string, defaultValue = ""): any { let st = component.state; const parts = name.split(/\./); diff --git a/src/utils/formatutils.ts b/src/utils/formatutils.ts index cb65de00..8b92412b 100644 --- a/src/utils/formatutils.ts +++ b/src/utils/formatutils.ts @@ -3,12 +3,12 @@ const locale = "en-US"; const base10UnitPrefixes = ["", "K", "M", "G", "T"]; const base2UnitPrefixes = ["", "Ki", "Mi", "Gi", "Ti"]; -function formatNumber(f) { +function formatNumber(f: number): string { return Math.round(f * 10) / 10.0 + ""; } -function toDecimalUnitString(f, thousand, prefixes, suffix) { - for (var i = 0; i < prefixes.length; i++) { +function toDecimalUnitString(f: number, thousand: number, prefixes: string[], suffix: string): string { + for (let i = 0; i < prefixes.length; i++) { if (f < 0.9 * thousand) { return formatNumber(f) + " " + prefixes[i] + suffix; } @@ -18,7 +18,7 @@ function toDecimalUnitString(f, thousand, prefixes, suffix) { return formatNumber(f) + " " + prefixes[prefixes.length - 1] + suffix; } -export function sizeDisplayName(size, bytesStringBase2) { +export function sizeDisplayName(size: number, bytesStringBase2: boolean): string { if (size === undefined) { return ""; } @@ -39,17 +39,18 @@ export function timesOfDayDisplayName(v) { return v.length + " times"; } -export function parseQuery(queryString) { - var query = {}; - var pairs = (queryString[0] === "?" ? queryString.substr(1) : queryString).split("&"); - for (var i = 0; i < pairs.length; i++) { - var pair = pairs[i].split("="); +type QueryParams = { [param: string]: string }; +export function parseQuery(queryString: string): QueryParams { + const query: QueryParams = {}; + const pairs = (queryString[0] === "?" ? queryString.substr(1) : queryString).split("&"); + for (let i = 0; i < pairs.length; i++) { + const pair = pairs[i].split("="); query[decodeURIComponent(pair[0])] = decodeURIComponent(pair[1] || ""); } return query; } -export function rfc3339TimestampForDisplay(n) { +export function rfc3339TimestampForDisplay(n: number | string | Date) { if (!n) { return ""; } @@ -58,18 +59,18 @@ export function rfc3339TimestampForDisplay(n) { return t.toLocaleString(); } -export function objectLink(n) { +export function objectLink(n: string) { if (n.startsWith("k") || n.startsWith("Ik")) { return "/snapshots/dir/" + n; } return "/api/v1/objects/" + n; } -export function formatOwnerName(s) { +export function formatOwnerName(s): string { return s.userName + "@" + s.host; } -export function compare(a, b) { +export function compare(a: any, b: any): -1 | 0 | 1 { return a < b ? -1 : a > b ? 1 : 0; } @@ -81,12 +82,14 @@ export function compare(a, b) { * @param {number} ms - A duration (as a number of milliseconds). * @returns {string} A string representation of the duration. */ -export function formatMillisecondsUsingMultipleUnits(ms) { +export function formatMillisecondsUsingMultipleUnits(ms: number): string { const magnitudes = separateMillisecondsIntoMagnitudes(ms); const str = formatMagnitudesUsingMultipleUnits(magnitudes, true); return str; } +type Magnitudes = { days: number; hours: number; minutes: number; seconds: number; milliseconds: number }; + /** * Separate a duration into integer magnitudes of multiple units which, * when combined together, equal the original duration (minus any partial @@ -100,7 +103,7 @@ export function formatMillisecondsUsingMultipleUnits(ms) { * when combined together, represent the original duration * (minus any partial milliseconds). */ -export function separateMillisecondsIntoMagnitudes(ms) { +export function separateMillisecondsIntoMagnitudes(ms: number): Magnitudes { const magnitudes = { days: Math.trunc(ms / (1000 * 60 * 60 * 24)), hours: Math.trunc(ms / (1000 * 60 * 60)) % 24, @@ -133,7 +136,7 @@ export function separateMillisecondsIntoMagnitudes(ms) { * @param {boolean} abbreviateUnits - Whether you want to use short unit names. * @returns {string} Formatted string representing the specified duration. */ -export function formatMagnitudesUsingMultipleUnits(magnitudes, abbreviateUnits = false) { +export function formatMagnitudesUsingMultipleUnits(magnitudes: Magnitudes, abbreviateUnits = false) { // Define the label we will use for each unit, depending upon whether that // unit's magnitude is `1` or not (e.g. "0 minutes" vs. "1 minute"). // Note: This object is not used in the final "else" block below. @@ -196,7 +199,7 @@ export function formatMagnitudesUsingMultipleUnits(magnitudes, abbreviateUnits = * @param {boolean} useMultipleUnits - Whether you want to use multiple units. * @returns {string} The formatted string. */ -export function formatMilliseconds(ms, useMultipleUnits = false) { +export function formatMilliseconds(ms: number, useMultipleUnits = false): string { if (useMultipleUnits) { return formatMillisecondsUsingMultipleUnits(ms); } diff --git a/src/utils/policyutil.tsx b/src/utils/policyutil.tsx index 015aeecf..7934167a 100644 --- a/src/utils/policyutil.tsx +++ b/src/utils/policyutil.tsx @@ -1,4 +1,4 @@ -export function isAbsolutePath(p) { +export function isAbsolutePath(p: string) { // Unix-style path. if (p.startsWith("/")) { return true; @@ -20,7 +20,7 @@ export function isAbsolutePath(p) { } // Refer to kopia/snapshot/source.go:ParseSourceInfo -export function checkPolicyPath(path) { +export function checkPolicyPath(path: string) { if (path === "(global)") { return "Cannot create the global policy, it already exists."; } diff --git a/src/utils/uiutil.tsx b/src/utils/uiutil.tsx index 8a31bafa..d7f3d489 100644 --- a/src/utils/uiutil.tsx +++ b/src/utils/uiutil.tsx @@ -3,7 +3,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import React from "react"; import { sizeDisplayName } from "./formatutils.js"; -export function sizeWithFailures(size, summ, bytesStringBase2) { +export function sizeWithFailures(size?: number, summ, bytesStringBase2: boolean) { if (size === undefined) { return ""; } From b9ad11272ced72695b3d2b71434569f257f15937 Mon Sep 17 00:00:00 2001 From: main Date: Sat, 4 Oct 2025 16:59:24 +0200 Subject: [PATCH 04/29] refactor(ui): Add types 3 --- src/components/Logs.tsx | 15 ++++-- src/components/PolicyEditorLink.tsx | 4 +- src/components/SetupRepository.tsx | 5 +- src/components/SetupRepositoryAzure.tsx | 6 ++- src/components/SetupRepositoryB2.tsx | 5 +- src/components/SetupRepositoryFilesystem.tsx | 7 ++- src/components/SetupRepositoryGCS.tsx | 5 +- src/components/SetupRepositoryRclone.tsx | 5 +- src/components/SetupRepositoryS3.tsx | 5 +- src/components/SetupRepositorySFTP.tsx | 5 +- src/components/SetupRepositoryServer.tsx | 5 +- src/components/SetupRepositoryToken.tsx | 5 +- src/components/SetupRepositoryWebDAV.tsx | 7 ++- src/components/SnapshotEstimation.tsx | 2 + .../policy-editor/ActionRowMode.tsx | 2 +- .../policy-editor/EffectiveListValue.tsx | 2 +- .../policy-editor/EffectiveTextAreaValue.tsx | 2 +- src/components/policy-editor/PolicyEditor.tsx | 9 ++-- .../policy-editor/UpcomingSnapshotTimes.tsx | 5 +- src/components/types.ts | 7 +++ src/forms/LogDetailSelector.tsx | 3 +- src/forms/OptionalBoolean.tsx | 3 +- src/forms/OptionalDirectory.tsx | 5 +- src/forms/OptionalField.tsx | 3 +- src/forms/OptionalFieldNoLabel.tsx | 3 +- src/forms/OptionalNumberField.tsx | 3 +- src/forms/RequiredBoolean.tsx | 2 +- src/forms/RequiredDirectory.tsx | 5 +- src/forms/RequiredField.tsx | 3 +- src/forms/RequiredNumberField.tsx | 3 +- src/forms/StringList.tsx | 7 +-- src/forms/TimesOfDayList.tsx | 17 ++++--- src/forms/index.tsx | 7 +-- src/global.d.ts | 6 +++ src/pages/Policies.tsx | 48 +++++++++++-------- src/pages/Preferences.tsx | 8 ++-- src/pages/Repository.tsx | 4 ++ src/pages/SnapshotCreate.tsx | 6 ++- src/pages/SnapshotDirectory.tsx | 1 + src/pages/SnapshotHistory.tsx | 8 ++-- src/pages/SnapshotRestore.tsx | 5 +- src/pages/Snapshots.tsx | 5 +- src/pages/Task.tsx | 6 ++- src/pages/Tasks.tsx | 8 +++- src/setupProxy.ts | 2 +- src/utils/deepstate.ts | 2 +- src/utils/formatutils.ts | 12 ++--- src/utils/policyutil.tsx | 10 +++- 48 files changed, 203 insertions(+), 100 deletions(-) create mode 100644 src/components/types.ts create mode 100644 src/global.d.ts diff --git a/src/components/Logs.tsx b/src/components/Logs.tsx index bfbc37d5..391b8b5e 100644 --- a/src/components/Logs.tsx +++ b/src/components/Logs.tsx @@ -4,8 +4,13 @@ import Table from "react-bootstrap/Table"; import { handleChange } from "../forms"; import { redirect } from "../utils/uiutil"; import PropTypes from "prop-types"; +import { ComponentChangeHandling, ChangeEventHandle } from "./types"; + +export class Logs extends Component implements ComponentChangeHandling { + handleChange: ChangeEventHandle; + interval: number; + messagesEndRef: React.RefObject; -export class Logs extends Component { constructor() { super(); this.state = { @@ -46,7 +51,7 @@ export class Logs extends Component { axios .get("/api/v1/tasks/" + this.props.taskID + "/logs") .then((result) => { - let oldLogs = this.state.logs; + const oldLogs = this.state.logs; this.setState({ logs: result.data.logs, isLoading: false, @@ -65,11 +70,11 @@ export class Logs extends Component { }); } - fullLogTime(x) { + fullLogTime(x: number) { return new Date(x * 1000).toLocaleString(); } - formatLogTime(x) { + formatLogTime(x: number) { const d = new Date(x * 1000); let result = ""; @@ -88,7 +93,7 @@ export class Logs extends Component { // if there are any properties other than `msg, ts, level, mod` output them as JSON. // eslint-disable-next-line @typescript-eslint/no-unused-vars - let { msg, ts, level, mod, ...parametersOnly } = entry; + const { msg, ts, level, mod, ...parametersOnly } = entry; const p = JSON.stringify(parametersOnly); if (p !== "{}") { diff --git a/src/components/PolicyEditorLink.tsx b/src/components/PolicyEditorLink.tsx index 883c006e..08451d77 100644 --- a/src/components/PolicyEditorLink.tsx +++ b/src/components/PolicyEditorLink.tsx @@ -1,7 +1,7 @@ import React from "react"; import { Link } from "react-router-dom"; -import { PolicyTypeName, policyEditorURL } from "../utils/policyutil"; +import { PolicyQueryParams, PolicyTypeName, policyEditorURL } from "../utils/policyutil"; -export function PolicyEditorLink(s) { +export function PolicyEditorLink(s: PolicyQueryParams) { return {PolicyTypeName(s)}; } diff --git a/src/components/SetupRepository.tsx b/src/components/SetupRepository.tsx index d5438a4a..245af54d 100644 --- a/src/components/SetupRepository.tsx +++ b/src/components/SetupRepository.tsx @@ -23,6 +23,7 @@ import { SetupRepositorySFTP } from "./SetupRepositorySFTP"; import { SetupRepositoryToken } from "./SetupRepositoryToken"; import { SetupRepositoryWebDAV } from "./SetupRepositoryWebDAV"; import { toAlgorithmOption } from "../utils/uiutil"; +import { ComponentChangeHandling, ChangeEventHandle } from "./types"; const supportedProviders = [ { @@ -73,7 +74,9 @@ const supportedProviders = [ }, ]; -export class SetupRepository extends Component { +export class SetupRepository extends Component implements ComponentChangeHandling { + handleChange: ChangeEventHandle; + constructor() { super(); diff --git a/src/components/SetupRepositoryAzure.tsx b/src/components/SetupRepositoryAzure.tsx index 283c9d92..9f893b94 100644 --- a/src/components/SetupRepositoryAzure.tsx +++ b/src/components/SetupRepositoryAzure.tsx @@ -4,7 +4,11 @@ import { handleChange, validateRequiredFields } from "../forms"; import { OptionalField } from "../forms/OptionalField"; import { RequiredField } from "../forms/RequiredField"; import PropTypes from "prop-types"; -export class SetupRepositoryAzure extends Component { +import { ComponentChangeHandling, ChangeEventHandle } from "./types"; + +export class SetupRepositoryAzure extends Component implements ComponentChangeHandling { + handleChange: ChangeEventHandle; + constructor(props) { super(); diff --git a/src/components/SetupRepositoryB2.tsx b/src/components/SetupRepositoryB2.tsx index 95edf041..3696eaa3 100644 --- a/src/components/SetupRepositoryB2.tsx +++ b/src/components/SetupRepositoryB2.tsx @@ -4,8 +4,11 @@ import { handleChange, validateRequiredFields } from "../forms"; import { RequiredField } from "../forms/RequiredField"; import { OptionalField } from "../forms/OptionalField"; import PropTypes from "prop-types"; +import { ComponentChangeHandling, ChangeEventHandle } from "./types"; + +export class SetupRepositoryB2 extends Component implements ComponentChangeHandling { + handleChange: ChangeEventHandle; -export class SetupRepositoryB2 extends Component { constructor(props) { super(); diff --git a/src/components/SetupRepositoryFilesystem.tsx b/src/components/SetupRepositoryFilesystem.tsx index 9ad2500e..d3aab12b 100644 --- a/src/components/SetupRepositoryFilesystem.tsx +++ b/src/components/SetupRepositoryFilesystem.tsx @@ -2,9 +2,12 @@ import React, { Component } from "react"; import { handleChange, validateRequiredFields } from "../forms"; import { RequiredDirectory } from "../forms/RequiredDirectory"; import PropTypes from "prop-types"; +import { ComponentChangeHandling, ChangeEventHandle } from "./types"; -export class SetupRepositoryFilesystem extends Component { - constructor(props) { +export class SetupRepositoryFilesystem extends Component implements ComponentChangeHandling { + handleChange: ChangeEventHandle; + + constructor(props: any) { super(); this.state = { diff --git a/src/components/SetupRepositoryGCS.tsx b/src/components/SetupRepositoryGCS.tsx index 426bb104..e75b4a06 100644 --- a/src/components/SetupRepositoryGCS.tsx +++ b/src/components/SetupRepositoryGCS.tsx @@ -4,8 +4,11 @@ import { handleChange, validateRequiredFields } from "../forms"; import { OptionalField } from "../forms/OptionalField"; import { RequiredField } from "../forms/RequiredField"; import PropTypes from "prop-types"; +import { ComponentChangeHandling, ChangeEventHandle } from "./types"; + +export class SetupRepositoryGCS extends Component implements ComponentChangeHandling { + handleChange: ChangeEventHandle; -export class SetupRepositoryGCS extends Component { constructor(props) { super(); diff --git a/src/components/SetupRepositoryRclone.tsx b/src/components/SetupRepositoryRclone.tsx index 50adb98c..b6a6fead 100644 --- a/src/components/SetupRepositoryRclone.tsx +++ b/src/components/SetupRepositoryRclone.tsx @@ -4,8 +4,11 @@ import { handleChange, validateRequiredFields } from "../forms"; import { OptionalField } from "../forms/OptionalField"; import { RequiredField } from "../forms/RequiredField"; import PropTypes from "prop-types"; +import { ComponentChangeHandling, ChangeEventHandle } from "./types"; + +export class SetupRepositoryRclone extends Component implements ComponentChangeHandling { + handleChange: ChangeEventHandle; -export class SetupRepositoryRclone extends Component { constructor(props) { super(); diff --git a/src/components/SetupRepositoryS3.tsx b/src/components/SetupRepositoryS3.tsx index b7da3877..74f3c1be 100644 --- a/src/components/SetupRepositoryS3.tsx +++ b/src/components/SetupRepositoryS3.tsx @@ -5,8 +5,11 @@ import { OptionalField } from "../forms/OptionalField"; import { RequiredBoolean } from "../forms/RequiredBoolean"; import { RequiredField } from "../forms/RequiredField"; import PropTypes from "prop-types"; +import { ComponentChangeHandling, ChangeEventHandle } from "./types"; + +export class SetupRepositoryS3 extends Component implements ComponentChangeHandling { + handleChange: ChangeEventHandle; -export class SetupRepositoryS3 extends Component { constructor(props) { super(); diff --git a/src/components/SetupRepositorySFTP.tsx b/src/components/SetupRepositorySFTP.tsx index 8bd2bc21..6a96615c 100644 --- a/src/components/SetupRepositorySFTP.tsx +++ b/src/components/SetupRepositorySFTP.tsx @@ -6,6 +6,7 @@ import { OptionalNumberField } from "../forms/OptionalNumberField"; import { RequiredBoolean } from "../forms/RequiredBoolean"; import { RequiredField } from "../forms/RequiredField"; import PropTypes from "prop-types"; +import { ComponentChangeHandling, ChangeEventHandle } from "./types"; function hasExactlyOneOf(component, names) { let count = 0; @@ -19,7 +20,9 @@ function hasExactlyOneOf(component, names) { return count === 1; } -export class SetupRepositorySFTP extends Component { +export class SetupRepositorySFTP extends Component implements ComponentChangeHandling { + handleChange: ChangeEventHandle; + constructor(props) { super(); diff --git a/src/components/SetupRepositoryServer.tsx b/src/components/SetupRepositoryServer.tsx index 2a930e3d..9fbfd2f3 100644 --- a/src/components/SetupRepositoryServer.tsx +++ b/src/components/SetupRepositoryServer.tsx @@ -4,7 +4,10 @@ import { handleChange, validateRequiredFields } from "../forms"; import { OptionalField } from "../forms/OptionalField"; import { RequiredField } from "../forms/RequiredField"; import PropTypes from "prop-types"; -export class SetupRepositoryServer extends Component { +import { ComponentChangeHandling, ChangeEventHandle } from "./types"; +export class SetupRepositoryServer extends Component implements ComponentChangeHandling { + handleChange: ChangeEventHandle; + constructor(props) { super(); diff --git a/src/components/SetupRepositoryToken.tsx b/src/components/SetupRepositoryToken.tsx index 0da23324..df6a8e27 100644 --- a/src/components/SetupRepositoryToken.tsx +++ b/src/components/SetupRepositoryToken.tsx @@ -3,8 +3,11 @@ import Row from "react-bootstrap/Row"; import { handleChange, validateRequiredFields } from "../forms"; import { RequiredField } from "../forms/RequiredField"; import PropTypes from "prop-types"; +import { ComponentChangeHandling, ChangeEventHandle } from "./types"; + +export class SetupRepositoryToken extends Component implements ComponentChangeHandling { + handleChange: ChangeEventHandle; -export class SetupRepositoryToken extends Component { constructor(props) { super(); diff --git a/src/components/SetupRepositoryWebDAV.tsx b/src/components/SetupRepositoryWebDAV.tsx index 5847e1e3..4117bfd0 100644 --- a/src/components/SetupRepositoryWebDAV.tsx +++ b/src/components/SetupRepositoryWebDAV.tsx @@ -4,9 +4,12 @@ import { handleChange, validateRequiredFields } from "../forms"; import { OptionalField } from "../forms/OptionalField"; import { RequiredField } from "../forms/RequiredField"; import PropTypes from "prop-types"; +import { ComponentChangeHandling, ChangeEventHandle } from "./types"; -export class SetupRepositoryWebDAV extends Component { - constructor(props) { +export class SetupRepositoryWebDAV extends Component implements ComponentChangeHandling { + handleChange: ChangeEventHandle; + + constructor(props: any) { super(); this.state = { diff --git a/src/components/SnapshotEstimation.tsx b/src/components/SnapshotEstimation.tsx index d65bca00..993b47ab 100644 --- a/src/components/SnapshotEstimation.tsx +++ b/src/components/SnapshotEstimation.tsx @@ -13,6 +13,8 @@ import { UIPreferencesContext } from "../contexts/UIPreferencesContext"; import { useNavigate, useLocation, useParams } from "react-router-dom"; export class SnapshotEstimationInternal extends Component { + interval: number | null; + constructor() { super(); this.state = { diff --git a/src/components/policy-editor/ActionRowMode.tsx b/src/components/policy-editor/ActionRowMode.tsx index ded41ec4..0364ec8e 100644 --- a/src/components/policy-editor/ActionRowMode.tsx +++ b/src/components/policy-editor/ActionRowMode.tsx @@ -6,7 +6,7 @@ import { LabelColumn } from "./LabelColumn"; import { WideValueColumn } from "./WideValueColumn"; import { EffectiveValue } from "./EffectiveValue"; -export function ActionRowMode(component, action) { +export function ActionRowMode(component, action: string) { return ( diff --git a/src/components/policy-editor/EffectiveTextAreaValue.tsx b/src/components/policy-editor/EffectiveTextAreaValue.tsx index 9ce63edf..9de14d22 100644 --- a/src/components/policy-editor/EffectiveTextAreaValue.tsx +++ b/src/components/policy-editor/EffectiveTextAreaValue.tsx @@ -13,7 +13,7 @@ export function EffectiveTextAreaValue(component, policyField) { data-testid={"effective-" + policyField} size="sm" as="textarea" - rows="5" + rows={5} value={getDeepStateProperty(component, "resolved.effective." + policyField, undefined)} readOnly={true} /> diff --git a/src/components/policy-editor/PolicyEditor.tsx b/src/components/policy-editor/PolicyEditor.tsx index 7d4c5d30..5ee42ed5 100644 --- a/src/components/policy-editor/PolicyEditor.tsx +++ b/src/components/policy-editor/PolicyEditor.tsx @@ -26,7 +26,7 @@ import { OptionalNumberField } from "../../forms/OptionalNumberField"; import { RequiredBoolean } from "../../forms/RequiredBoolean"; import { TimesOfDayList } from "../../forms/TimesOfDayList"; import { errorAlert, toAlgorithmOption } from "../../utils/uiutil"; -import { sourceQueryStringParams } from "../../utils/policyutil"; +import { PolicyQueryParams, sourceQueryStringParams } from "../../utils/policyutil"; import { PolicyEditorLink } from "../PolicyEditorLink"; import { LabelColumn } from "./LabelColumn"; import { ValueColumn } from "./ValueColumn"; @@ -43,8 +43,11 @@ import { ActionRowScript } from "./ActionRowScript"; import { ActionRowTimeout } from "./ActionRowTimeout"; import { ActionRowMode } from "./ActionRowMode"; import PropTypes from "prop-types"; +import { ChangeEventHandle, ComponentChangeHandling } from "../types"; + +export class PolicyEditor extends Component implements ComponentChangeHandling { + handleChange: ChangeEventHandle; -export class PolicyEditor extends Component { constructor() { super(); this.state = { @@ -131,7 +134,7 @@ export class PolicyEditor extends Component { } } - PolicyDefinitionPoint(p) { + PolicyDefinitionPoint(p: PolicyQueryParams) { if (!p) { return ""; } diff --git a/src/components/policy-editor/UpcomingSnapshotTimes.tsx b/src/components/policy-editor/UpcomingSnapshotTimes.tsx index ae8ac902..3844c34b 100644 --- a/src/components/policy-editor/UpcomingSnapshotTimes.tsx +++ b/src/components/policy-editor/UpcomingSnapshotTimes.tsx @@ -2,16 +2,17 @@ import moment from "moment"; import React from "react"; import { LabelColumn } from "./LabelColumn"; -export function UpcomingSnapshotTimes(resolved) { +export function UpcomingSnapshotTimes(resolved: { schedulingError?: string; upcomingSnapshotTimes: string[] } | null) { if (!resolved) { return null; } + // This is only mentioned here if (resolved.schedulingError) { return

{resolved.schedulingError}

; } - const times = resolved.upcomingSnapshotTimes; + const times: string[] = resolved.upcomingSnapshotTimes; if (!times) { return ; diff --git a/src/components/types.ts b/src/components/types.ts new file mode 100644 index 00000000..7aa9cbf3 --- /dev/null +++ b/src/components/types.ts @@ -0,0 +1,7 @@ +import { Component } from "react"; + +export type ChangeEventHandle = (event: React.ChangeEvent, valueGetter?: (x: any) => any) => void; + +export type ComponentChangeHandling = Component & { + handleChange: ChangeEventHandle; +}; \ No newline at end of file diff --git a/src/forms/LogDetailSelector.tsx b/src/forms/LogDetailSelector.tsx index ca22f1b6..87fd2eff 100644 --- a/src/forms/LogDetailSelector.tsx +++ b/src/forms/LogDetailSelector.tsx @@ -1,8 +1,9 @@ import React from "react"; import Form from "react-bootstrap/Form"; import { valueToNumber, stateProperty } from "."; +import { ComponentChangeHandling } from "src/components/types"; -export function LogDetailSelector(component, name: string) { +export function LogDetailSelector(component: ComponentChangeHandling, name: string) { return ( {label && {label}} diff --git a/src/forms/OptionalDirectory.tsx b/src/forms/OptionalDirectory.tsx index 16a97049..afc6a064 100644 --- a/src/forms/OptionalDirectory.tsx +++ b/src/forms/OptionalDirectory.tsx @@ -6,6 +6,7 @@ import { faFolderOpen } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { stateProperty } from "."; import { setDeepStateProperty } from "../utils/deepstate"; +import { ComponentChangeHandling } from "src/components/types"; /** * This functions returns a directory selector that allows the user to select a directory. @@ -22,7 +23,7 @@ import { setDeepStateProperty } from "../utils/deepstate"; * Additional properties of the component * @returns The form group with the components */ -export function OptionalDirectory(component, label: string, name: string, props = {}) { +export function OptionalDirectory(component: ComponentChangeHandling, label: string | null, name: string, props = {}) { /** * Saves the selected path as a deepstate variable within the component * @param {The path that has been selected} path @@ -49,7 +50,7 @@ export function OptionalDirectory(component, label: string, name: string, props {...props} > {window.kopiaUI && ( - )} diff --git a/src/forms/OptionalField.tsx b/src/forms/OptionalField.tsx index c084a28a..73d6d1f4 100644 --- a/src/forms/OptionalField.tsx +++ b/src/forms/OptionalField.tsx @@ -2,8 +2,9 @@ import React from "react"; import Form from "react-bootstrap/Form"; import Col from "react-bootstrap/Col"; import { stateProperty } from "."; +import { ComponentChangeHandling } from "src/components/types"; -export function OptionalField(component, label: string, name: string, props = {}, helpText = null) { +export function OptionalField(component: ComponentChangeHandling, label: string, name: string, props = {}, helpText = null) { return ( {label} diff --git a/src/forms/OptionalFieldNoLabel.tsx b/src/forms/OptionalFieldNoLabel.tsx index fbe24a09..28c8941e 100644 --- a/src/forms/OptionalFieldNoLabel.tsx +++ b/src/forms/OptionalFieldNoLabel.tsx @@ -3,8 +3,9 @@ import Form from "react-bootstrap/Form"; import Col from "react-bootstrap/Col"; import { stateProperty } from "."; +import { ComponentChangeHandling } from "src/components/types"; -export function OptionalFieldNoLabel(component, label: string, name: string, props = {}, helpText = null, invalidFeedback = null) { +export function OptionalFieldNoLabel(component: ComponentChangeHandling, label: string, name: string, props = {}, helpText = null, invalidFeedback = null) { return ( {label && {label}} diff --git a/src/forms/RequiredBoolean.tsx b/src/forms/RequiredBoolean.tsx index 7dccd96a..34ffc9e5 100644 --- a/src/forms/RequiredBoolean.tsx +++ b/src/forms/RequiredBoolean.tsx @@ -3,7 +3,7 @@ import Form from "react-bootstrap/Form"; import Col from "react-bootstrap/Col"; import { stateProperty } from "."; -function checkedToBool(t) { +function checkedToBool(t: HTMLInputElement): boolean { if (t.checked) { return true; } diff --git a/src/forms/RequiredDirectory.tsx b/src/forms/RequiredDirectory.tsx index 9a39cd17..4e9e981a 100644 --- a/src/forms/RequiredDirectory.tsx +++ b/src/forms/RequiredDirectory.tsx @@ -6,6 +6,7 @@ import { faFolderOpen } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { stateProperty } from "."; import { setDeepStateProperty } from "../utils/deepstate"; +import { ComponentChangeHandling } from "src/components/types"; /** * This functions returns a directory selector that allows the user to select a directory. @@ -22,7 +23,7 @@ import { setDeepStateProperty } from "../utils/deepstate"; * Additional properties of the component * @returns The form group with the components */ -export function RequiredDirectory(component, label: string, name: string, props = {}) { +export function RequiredDirectory(component: ComponentChangeHandling, label: string | null, name: string, props = {}) { /** * Saves the selected path as a deepstate variable within the component * @param {The path that has been selected} path @@ -46,7 +47,7 @@ export function RequiredDirectory(component, label: string, name: string, props {...props} > {window.kopiaUI && ( - )} diff --git a/src/forms/RequiredField.tsx b/src/forms/RequiredField.tsx index 93f0e5c4..f68f2c41 100644 --- a/src/forms/RequiredField.tsx +++ b/src/forms/RequiredField.tsx @@ -2,8 +2,9 @@ import React from "react"; import Form from "react-bootstrap/Form"; import Col from "react-bootstrap/Col"; import { stateProperty } from "."; +import { ComponentChangeHandling } from "src/components/types"; -export function RequiredField(component, label: string, name: string, props = {}, helpText = null) { +export function RequiredField(component: ComponentChangeHandling, label: string, name: string, props = {}, helpText: string | null = null) { return ( {label} diff --git a/src/forms/RequiredNumberField.tsx b/src/forms/RequiredNumberField.tsx index e71ee566..96aa357f 100644 --- a/src/forms/RequiredNumberField.tsx +++ b/src/forms/RequiredNumberField.tsx @@ -2,8 +2,9 @@ import React from "react"; import Form from "react-bootstrap/Form"; import Col from "react-bootstrap/Col"; import { stateProperty, isInvalidNumber, valueToNumber } from "."; +import { ComponentChangeHandling } from "src/components/types"; -export function RequiredNumberField(component, label: string, name: string, props = {}) { +export function RequiredNumberField(component: ComponentChangeHandling, label: string, name: string, props = {}) { return ( {label} diff --git a/src/forms/StringList.tsx b/src/forms/StringList.tsx index 932785e4..749b39e9 100644 --- a/src/forms/StringList.tsx +++ b/src/forms/StringList.tsx @@ -2,8 +2,9 @@ import React from "react"; import Form from "react-bootstrap/Form"; import Col from "react-bootstrap/Col"; import { stateProperty } from "."; +import { ComponentChangeHandling } from "src/components/types"; -export function listToMultilineString(v): string { +export function listToMultilineString(v: any): string { if (v) { return v.join("\n"); } @@ -20,7 +21,7 @@ export function multilineStringToList(target: HTMLTextAreaElement): string[] | u return v.split(/\n/); } -export function StringList(component, name: string, props = {}) { +export function StringList(component: ComponentChangeHandling, name: string, props = {}) { return ( component.handleChange(e, multilineStringToList)} as="textarea" - rows="5" + rows={5} {...props} > diff --git a/src/forms/TimesOfDayList.tsx b/src/forms/TimesOfDayList.tsx index f3fda9b2..658fabce 100644 --- a/src/forms/TimesOfDayList.tsx +++ b/src/forms/TimesOfDayList.tsx @@ -2,9 +2,10 @@ import React from "react"; import Form from "react-bootstrap/Form"; import FormGroup from "react-bootstrap/FormGroup"; import { stateProperty } from "."; +import { ComponentChangeHandling } from "src/components/types"; -export function TimesOfDayList(component, name: string, props = {}) { - function parseTimeOfDay(v: string) { +export function TimesOfDayList(component: ComponentChangeHandling, name: string, props = {}) { + function parseTimeOfDay(v: string): { hour: number; min: number } | string { const re = /(\d+):(\d+)/; const match = re.exec(v); @@ -25,9 +26,9 @@ export function TimesOfDayList(component, name: string, props = {}) { return v; } - function toMultilineString(v) { + function toMultilineString(v): string { if (v) { - let tmp = []; + const tmp = []; for (const tod of v) { if (typeof tod === "object") { @@ -43,18 +44,16 @@ export function TimesOfDayList(component, name: string, props = {}) { return ""; } - function fromMultilineString(target) { + function fromMultilineString(target: HTMLTextAreaElement): ({ hour: number; min: number } | string)[] | undefined { const v = target.value; if (v === "") { return undefined; } - let result = []; - + const result: ({ hour: number; min: number } | string)[] = []; for (const line of v.split(/\n/)) { result.push(parseTimeOfDay(line)); } - return result; } @@ -66,7 +65,7 @@ export function TimesOfDayList(component, name: string, props = {}) { value={toMultilineString(stateProperty(component, name))} onChange={(e) => component.handleChange(e, fromMultilineString)} as="textarea" - rows="5" + rows={5} {...props} > Invalid Times of Day diff --git a/src/forms/index.tsx b/src/forms/index.tsx index ad93028c..4bd49d12 100644 --- a/src/forms/index.tsx +++ b/src/forms/index.tsx @@ -1,8 +1,9 @@ import { Component } from "react"; import { getDeepStateProperty, setDeepStateProperty } from "../utils/deepstate"; +import { ComponentChangeHandling } from "src/components/types"; -export function validateRequiredFields(component, fields: any[]) { - let updateState = {}; +export function validateRequiredFields(component: ComponentChangeHandling, fields: string[]) { + const updateState: {[field: string]: any } = {}; let failed = false; for (let i = 0; i < fields.length; i++) { @@ -27,7 +28,7 @@ export function handleChange(event, valueGetter = (x) => x.value) { setDeepStateProperty(this, event.target.name, valueGetter(event.target)); } -export function stateProperty(component: Component, name: string, defaultValue = "") { +export function stateProperty(component: Component, name: string, defaultValue: string | null | undefined = "") { const value = getDeepStateProperty(component, name); return value === undefined ? defaultValue : value; } diff --git a/src/global.d.ts b/src/global.d.ts new file mode 100644 index 00000000..78df4314 --- /dev/null +++ b/src/global.d.ts @@ -0,0 +1,6 @@ +// Extend the Window interface to include the kopiaUI property +declare interface Window { + kopiaUI?: { + selectDirectory: (callback: (path: string) => void) => void; + }; +} \ No newline at end of file diff --git a/src/pages/Policies.tsx b/src/pages/Policies.tsx index 179f1507..550fb279 100644 --- a/src/pages/Policies.tsx +++ b/src/pages/Policies.tsx @@ -17,6 +17,7 @@ import { compare, formatOwnerName } from "../utils/formatutils"; import { redirect } from "../utils/uiutil"; import { checkPolicyPath, policyEditorURL } from "../utils/policyutil"; import PropTypes from "prop-types"; +import { ComponentChangeHandling, ChangeEventHandle } from "src/components/types"; const applicablePolicies = "Applicable Policies"; const localPolicies = "Local Path Policies"; @@ -25,7 +26,23 @@ const globalPolicy = "Global Policy"; const perUserPolicies = "Per-User Policies"; const perHostPolicies = "Per-Host Policies"; -export class PoliciesInternal extends Component { +interface PoliciesState { + policies: any[]; + isLoading: boolean; + error: Error | null; + editorTarget: any; + selectedOwner: string; + policyPath: string; + sources: any[]; + localHost?: string; + localUsername?: string; + localSourceName?: string; + multiUser?: any; +} + +export class PoliciesInternal extends Component implements ComponentChangeHandling { + handleChange: ChangeEventHandle; + constructor() { super(); this.state = { @@ -116,33 +133,30 @@ export class PoliciesInternal extends Component { return; } - const error = checkPolicyPath(this.state.policyPath, this.state.localHost, this.state.localUsername); + const error = checkPolicyPath(this.state.policyPath/*, this.state.localHost, this.state.localUsername*/); if (error) { - alert( - error + - "\nMust be either an absolute path, `user@host:/absolute/path`, `user@host` or `@host`. Use backslashes on Windows.", - ); + alert(error + "\nMust be either an absolute path, `user@host:/absolute/path`, `user@host` or `@host`. Use backslashes on Windows."); return; } this.props.navigate( policyEditorURL({ - userName: this.state.localUsername, - host: this.state.localHost, + userName: this.state.localUsername!, + host: this.state.localHost!, path: this.state.policyPath, }), ); } - selectOwner(h) { + selectOwner(h: string) { this.setState({ selectedOwner: h, }); } policySummary(policies) { - let bits = []; + const bits = []; /** * Check if the object is empty * @param {*} obj @@ -161,13 +175,13 @@ export class PoliciesInternal extends Component { * @param {*} obj * @returns */ - function isEmpty(obj) { - for (var key in obj) { + function isEmpty(obj: any) { + for (const key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) return isEmptyObject(obj[key]); } return true; } - for (let pol in policies.policy) { + for (const pol in policies.policy) { if (!isEmpty(policies.policy[pol])) { bits.push( @@ -200,7 +214,7 @@ export class PoliciesInternal extends Component { return

Loading ...

; } - let uniqueOwners = sources.reduce((a, d) => { + const uniqueOwners: any[] = sources.reduce((a, d) => { const owner = formatOwnerName(d.source); if (!a.includes(owner)) { @@ -215,29 +229,23 @@ export class PoliciesInternal extends Component { case allPolicies: // do nothing; break; - case globalPolicy: policies = policies.filter((x) => this.isGlobalPolicy(x)); break; - case localPolicies: policies = policies.filter((x) => this.isLocalUserPolicy(x)); break; - case applicablePolicies: policies = policies.filter( (x) => this.isLocalUserPolicy(x) || this.isLocalHostPolicy(x) || this.isGlobalPolicy(x), ); break; - case perUserPolicies: policies = policies.filter((x) => !!x.target.userName && !!x.target.host && !x.target.path); break; - case perHostPolicies: policies = policies.filter((x) => !x.target.userName && !!x.target.host && !x.target.path); break; - default: policies = policies.filter((x) => formatOwnerName(x.target) === this.state.selectedOwner); break; diff --git a/src/pages/Preferences.tsx b/src/pages/Preferences.tsx index 2fe976ab..aa7f7a6c 100644 --- a/src/pages/Preferences.tsx +++ b/src/pages/Preferences.tsx @@ -1,4 +1,4 @@ -import { useContext, React } from "react"; +import React, { useContext } from "react"; import Col from "react-bootstrap/Col"; import Container from "react-bootstrap/Container"; import Form from "react-bootstrap/Form"; @@ -6,7 +6,7 @@ import Row from "react-bootstrap/Row"; import Tab from "react-bootstrap/Tab"; import Tabs from "react-bootstrap/Tabs"; import { NotificationEditor } from "../components/notifications/NotificationEditor"; -import { UIPreferencesContext } from "../contexts/UIPreferencesContext"; +import { Theme, UIPreferencesContext } from "../contexts/UIPreferencesContext"; /** * Class that exports preferences @@ -27,7 +27,7 @@ export function Preferences() { title="Select theme" id="themeSelector" value={theme} - onChange={(e) => setTheme(e.target.value)} + onChange={(e) => setTheme(e.target.value as Theme)} > @@ -55,7 +55,7 @@ export function Preferences() { className="form-select form-select-sm" title="Select byte representation" id="bytesBaseInput" - value={bytesStringBase2} + value={bytesStringBase2 ? "true" : "false"} onChange={(e) => setByteStringBase(e.target.value)} > diff --git a/src/pages/Repository.tsx b/src/pages/Repository.tsx index 404fd5d7..3d36cdb9 100644 --- a/src/pages/Repository.tsx +++ b/src/pages/Repository.tsx @@ -15,8 +15,12 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faCheck, faChevronCircleDown, faChevronCircleUp, faWindowClose } from "@fortawesome/free-solid-svg-icons"; import { Logs } from "../components/Logs"; import { AppContext } from "../contexts/AppContext"; +import { ChangeEventHandle } from "src/components/types"; export class Repository extends Component { + handleChange: ChangeEventHandle; + mounted: boolean; + constructor() { super(); diff --git a/src/pages/SnapshotCreate.tsx b/src/pages/SnapshotCreate.tsx index 7d735ebf..ceab9093 100644 --- a/src/pages/SnapshotCreate.tsx +++ b/src/pages/SnapshotCreate.tsx @@ -13,8 +13,12 @@ import { CLIEquivalent } from "../components/CLIEquivalent"; import { errorAlert, redirect } from "../utils/uiutil"; import { GoBackButton } from "../components/GoBackButton"; import PropTypes from "prop-types"; +import { ComponentChangeHandling, ChangeEventHandle } from "src/components/types"; + +class SnapshotCreateInternal extends Component implements ComponentChangeHandling { + handleChange: ChangeEventHandle; + policyEditorRef: React.RefObject; -class SnapshotCreateInternal extends Component { constructor() { super(); this.state = { diff --git a/src/pages/SnapshotDirectory.tsx b/src/pages/SnapshotDirectory.tsx index 65747e54..ec4b606d 100644 --- a/src/pages/SnapshotDirectory.tsx +++ b/src/pages/SnapshotDirectory.tsx @@ -11,6 +11,7 @@ import { DirectoryItems } from "../components/DirectoryItems"; import { CLIEquivalent } from "../components/CLIEquivalent"; import { DirectoryBreadcrumbs } from "../components/DirectoryBreadcrumbs"; import PropTypes from "prop-types"; +import { ComponentChangeHandling, ChangeEventHandle } from "src/components/types"; class SnapshotDirectoryInternal extends Component { constructor() { diff --git a/src/pages/SnapshotHistory.tsx b/src/pages/SnapshotHistory.tsx index 4dd8853b..8939e0d1 100644 --- a/src/pages/SnapshotHistory.tsx +++ b/src/pages/SnapshotHistory.tsx @@ -75,7 +75,7 @@ class SnapshotHistoryInternal extends Component { } selectAll() { - let snapIds = {}; + const snapIds = {}; for (const sn of this.state.snapshots) { snapIds[sn.id] = true; } @@ -127,7 +127,7 @@ class SnapshotHistoryInternal extends Component { } deleteSelectedSnapshots() { - let req = { + const req = { source: { host: this.state.host, userName: this.state.userName, @@ -161,7 +161,7 @@ class SnapshotHistoryInternal extends Component { } deleteSnapshotSource() { - let req = { + const req = { source: { host: this.state.host, userName: this.state.userName, @@ -673,7 +673,7 @@ SnapshotHistoryInternal.propTypes = { navigate: PropTypes.func, }; -export function SnapshotHistory(props) { +export function SnapshotHistory(props: any) { const navigate = useNavigate(); const location = useLocation(); useContext(UIPreferencesContext); diff --git a/src/pages/SnapshotRestore.tsx b/src/pages/SnapshotRestore.tsx index 106bf0f6..4746cc76 100644 --- a/src/pages/SnapshotRestore.tsx +++ b/src/pages/SnapshotRestore.tsx @@ -13,8 +13,11 @@ import { RequiredNumberField } from "../forms/RequiredNumberField"; import { errorAlert } from "../utils/uiutil"; import { GoBackButton } from "../components/GoBackButton"; import PropTypes from "prop-types"; +import { ChangeEventHandle, ComponentChangeHandling } from "src/components/types"; + +export class SnapshotRestoreInternal extends Component implements ComponentChangeHandling { + handleChange: ChangeEventHandle; -export class SnapshotRestoreInternal extends Component { constructor() { super(); diff --git a/src/pages/Snapshots.tsx b/src/pages/Snapshots.tsx index cc993623..091d05c8 100644 --- a/src/pages/Snapshots.tsx +++ b/src/pages/Snapshots.tsx @@ -17,11 +17,14 @@ import { errorAlert, redirect, sizeWithFailures } from "../utils/uiutil"; import { policyEditorURL, sourceQueryStringParams } from "../utils/policyutil"; import { CLIEquivalent } from "../components/CLIEquivalent"; import { UIPreferencesContext } from "../contexts/UIPreferencesContext"; +import { ChangeEventHandle, ComponentChangeHandling } from "src/components/types"; const localSnapshots = "Local Snapshots"; const allSnapshots = "All Snapshots"; -export class Snapshots extends Component { +export class Snapshots extends Component implements ComponentChangeHandling { + handleChange: ChangeEventHandle; + constructor() { super(); this.state = { diff --git a/src/pages/Task.tsx b/src/pages/Task.tsx index b2813467..25c4a3f7 100644 --- a/src/pages/Task.tsx +++ b/src/pages/Task.tsx @@ -19,6 +19,8 @@ import { UIPreferencesContext } from "../contexts/UIPreferencesContext"; import PropTypes from "prop-types"; class TaskInternal extends Component { + interval: number | null; + constructor() { super(); this.state = { @@ -63,7 +65,7 @@ class TaskInternal extends Component { }); if (result.data.endTime) { - window.clearInterval(this.interval); + window.clearInterval(this.interval!); this.interval = null; } }) @@ -127,7 +129,7 @@ class TaskInternal extends Component { return 0; } - counterBadge(label, c) { + counterBadge(label: string, c) { if (c.value <= this.valueThreshold()) { return ""; } diff --git a/src/pages/Tasks.tsx b/src/pages/Tasks.tsx index 610fdc03..dfd56406 100644 --- a/src/pages/Tasks.tsx +++ b/src/pages/Tasks.tsx @@ -13,8 +13,12 @@ import { handleChange } from "../forms"; import KopiaTable from "../components/KopiaTable"; import { redirect } from "../utils/uiutil"; import { taskStatusSymbol } from "../utils/taskutil"; +import { ChangeEventHandle, ComponentChangeHandling } from "src/components/types"; + +export class Tasks extends Component implements ComponentChangeHandling { + handleChange: ChangeEventHandle; + interval: number; -export class Tasks extends Component { constructor() { super(); this.state = { @@ -94,7 +98,7 @@ export class Tasks extends Component { return true; } - filterItems(items) { + filterItems(items: any[]) { return items.filter((c) => this.taskMatches(c)); } diff --git a/src/setupProxy.ts b/src/setupProxy.ts index 94b3efd5..7675b0a3 100644 --- a/src/setupProxy.ts +++ b/src/setupProxy.ts @@ -1,6 +1,6 @@ import { createProxyMiddleware } from "http-proxy-middleware"; -export default function (app) { +export default function (app: any) { app.use( "/api", createProxyMiddleware({ diff --git a/src/utils/deepstate.ts b/src/utils/deepstate.ts index acfc1102..e0cc9d7c 100644 --- a/src/utils/deepstate.ts +++ b/src/utils/deepstate.ts @@ -7,7 +7,7 @@ // getDeepStateProperty("a.b.c") returns true export function setDeepStateProperty(component: HasState, name: string, value: any): void { - let newState = { ...component.state }; + const newState = { ...component.state }; let st = newState; const parts = name.split(/\./); diff --git a/src/utils/formatutils.ts b/src/utils/formatutils.ts index 8b92412b..eebf09b0 100644 --- a/src/utils/formatutils.ts +++ b/src/utils/formatutils.ts @@ -55,19 +55,19 @@ export function rfc3339TimestampForDisplay(n: number | string | Date) { return ""; } - let t = new Date(n); + const t = new Date(n); return t.toLocaleString(); } export function objectLink(n: string) { if (n.startsWith("k") || n.startsWith("Ik")) { - return "/snapshots/dir/" + n; + return `/snapshots/dir/${n}`; } - return "/api/v1/objects/" + n; + return `/api/v1/objects/${n}`; } -export function formatOwnerName(s): string { - return s.userName + "@" + s.host; +export function formatOwnerName(s: { userName: string, host: string }): string { + return `${s.userName}@${s.host}`; } export function compare(a: any, b: any): -1 | 0 | 1 { @@ -215,7 +215,7 @@ export function formatMilliseconds(ms: number, useMultipleUnits = false): string ); } -export function formatDuration(from, to, useMultipleUnits = false) { +export function formatDuration(from: number | string | Date, to: number | string | Date, useMultipleUnits = false) { if (!from) { return ""; } diff --git a/src/utils/policyutil.tsx b/src/utils/policyutil.tsx index 7934167a..0947e5e4 100644 --- a/src/utils/policyutil.tsx +++ b/src/utils/policyutil.tsx @@ -68,7 +68,13 @@ export function PolicyTypeName(s) { return "Directory: " + s.userName + "@" + s.host + ":" + s.path; } -export function sourceQueryStringParams(src) { +export interface PolicyQueryParams { + userName: string; + host: string; + path: string; +}; + +export function sourceQueryStringParams(src: PolicyQueryParams) { return ( "userName=" + encodeURIComponent(src.userName) + @@ -79,6 +85,6 @@ export function sourceQueryStringParams(src) { ); } -export function policyEditorURL(s) { +export function policyEditorURL(s: PolicyQueryParams) { return "/policies/edit?" + sourceQueryStringParams(s); } From ed69f835a4ebc7755f8706970c1c1ba43e21871a Mon Sep 17 00:00:00 2001 From: main Date: Sat, 4 Oct 2025 19:14:57 +0200 Subject: [PATCH 05/29] refactor(ui): Add types 4 --- src/pages/SnapshotRestore.tsx | 52 ++++++++++++++++++++++++++++++++--- src/pages/Snapshots.tsx | 27 ++++++------------ src/utils/formatutils.ts | 1 + src/utils/uiutil.tsx | 2 +- 4 files changed, 59 insertions(+), 23 deletions(-) diff --git a/src/pages/SnapshotRestore.tsx b/src/pages/SnapshotRestore.tsx index 4746cc76..ccf8cf10 100644 --- a/src/pages/SnapshotRestore.tsx +++ b/src/pages/SnapshotRestore.tsx @@ -15,7 +15,51 @@ import { GoBackButton } from "../components/GoBackButton"; import PropTypes from "prop-types"; import { ChangeEventHandle, ComponentChangeHandling } from "src/components/types"; -export class SnapshotRestoreInternal extends Component implements ComponentChangeHandling { +interface SnapshotRestoreRequest { + root: any; + options: { + incremental: boolean; + ignoreErrors: boolean; + restoreDirEntryAtDepth: number; + minSizeForPlaceholder: number; + }; + zipFile?: string; + uncompressedZip?: boolean; + tarFile?: string; + fsOutput?: { + targetPath: string; + skipOwners: boolean; + skipPermissions: boolean; + skipTimes: boolean; + ignorePermissionErrors: boolean; + overwriteFiles: boolean; + overwriteDirectories: boolean; + overwriteSymlinks: boolean; + writeFilesAtomically: boolean; + writeSparseFiles: boolean; + }; +} + +interface SnapshotRestoreInternalState { + incremental: boolean; + continueOnErrors: boolean; + restoreOwnership: boolean; + restorePermissions: boolean; + restoreModTimes: boolean; + uncompressedZip: boolean; + overwriteFiles: boolean; + overwriteDirectories: boolean; + overwriteSymlinks: boolean; + ignorePermissionErrors: boolean; + writeFilesAtomically: boolean; + writeSparseFiles: boolean; + restoreDirEntryAtDepth: number; + minSizeForPlaceholder: number; + restoreTask: string; + destination?: string; +}; + +export class SnapshotRestoreInternal extends Component implements ComponentChangeHandling { handleChange: ChangeEventHandle; constructor() { @@ -43,7 +87,7 @@ export class SnapshotRestoreInternal extends Component implements ComponentChang this.start = this.start.bind(this); } - start(e) { + start(e: React.FormEvent) { e.preventDefault(); if (!validateRequiredFields(this, ["destination"])) { @@ -52,7 +96,7 @@ export class SnapshotRestoreInternal extends Component implements ComponentChang const dst = this.state.destination + ""; - let req = { + const req: SnapshotRestoreRequest = { root: this.props.params.oid, options: { incremental: this.state.incremental, @@ -100,7 +144,7 @@ export class SnapshotRestoreInternal extends Component implements ComponentChang return (

- + Go To Restore Task . diff --git a/src/pages/Snapshots.tsx b/src/pages/Snapshots.tsx index 091d05c8..05ee443d 100644 --- a/src/pages/Snapshots.tsx +++ b/src/pages/Snapshots.tsx @@ -24,6 +24,7 @@ const allSnapshots = "All Snapshots"; export class Snapshots extends Component implements ComponentChangeHandling { handleChange: ChangeEventHandle; + interval?: number; constructor() { super(); @@ -140,7 +141,7 @@ export class Snapshots extends Component implements ComponentChangeHandling { * @param x - the cell which content is changed * @returns - the content of the cell */ - statusCell(x, parent, bytesStringBase2) { + statusCell(x, parent, bytesStringBase2: boolean) { this.setHeader(x); switch (x.cell.getValue()) { case "IDLE": @@ -184,33 +185,23 @@ export class Snapshots extends Component implements ComponentChangeHandling { ); case "UPLOADING": { - let u = x.row.original.upload; + const u = x.row.original.upload; let title = ""; let totals = ""; if (u) { - title = - " hashed " + - u.hashedFiles + - " files (" + - sizeDisplayName(u.hashedBytes, bytesStringBase2) + - ")\n" + - " cached " + - u.cachedFiles + - " files (" + - sizeDisplayName(u.cachedBytes, bytesStringBase2) + - ")\n" + - " dir " + - u.directory; - const totalBytes = u.hashedBytes + u.cachedBytes; + const totalSize = sizeDisplayName(totalBytes, bytesStringBase2); + const hashedSize = sizeDisplayName(u.hashedBytes, bytesStringBase2); + const cachedSize = sizeDisplayName(u.cachedBytes, bytesStringBase2); - totals = sizeDisplayName(totalBytes, bytesStringBase2); + title = ` hashed ${u.hashedFiles} files (${hashedSize})\n cached ${u.cachedFiles} files (${cachedSize})\n dir ${u.directory}`; + totals = totalSize; if (u.estimatedBytes) { totals += "/" + sizeDisplayName(u.estimatedBytes, bytesStringBase2); const percent = Math.round((totalBytes * 1000.0) / u.estimatedBytes) / 10.0; if (percent <= 100) { - totals += " " + percent + "%"; + totals += ` ${percent}%`; } } } diff --git a/src/utils/formatutils.ts b/src/utils/formatutils.ts index eebf09b0..f269f83f 100644 --- a/src/utils/formatutils.ts +++ b/src/utils/formatutils.ts @@ -32,6 +32,7 @@ export function intervalDisplayName() { return "-"; } +// This is never used export function timesOfDayDisplayName(v) { if (!v) { return "(none)"; diff --git a/src/utils/uiutil.tsx b/src/utils/uiutil.tsx index d7f3d489..ba5b01ac 100644 --- a/src/utils/uiutil.tsx +++ b/src/utils/uiutil.tsx @@ -39,7 +39,7 @@ export function redirect(e) { } } -export function errorAlert(err, prefix) { +export function errorAlert(err, prefix?) { if (!prefix) { prefix = "Error"; } From eaa020570d777335dceb9ce052c4059079d38c09 Mon Sep 17 00:00:00 2001 From: QazCetelic Date: Sun, 5 Oct 2025 17:19:30 +0200 Subject: [PATCH 06/29] Change Vite config to use env variable if available --- vite.config.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vite.config.js b/vite.config.js index 110d5c82..9b4e31ef 100644 --- a/vite.config.js +++ b/vite.config.js @@ -22,10 +22,10 @@ export default defineConfig(() => { host: "localhost", https: false, strictPort: true, - open: true, + open: process.env.VITE_KOPIA_ENDPOINT ? false : true, proxy: { "/api": { - target: "http://localhost:51515", + target: process.env.VITE_KOPIA_ENDPOINT, changeOrigin: true, secure: false, }, From 92d52b84e04fc1b8d152ed14c7256a3da6ed8531 Mon Sep 17 00:00:00 2001 From: QazCetelic Date: Sun, 5 Oct 2025 19:33:09 +0200 Subject: [PATCH 07/29] refactor(ui): Add types to tests 1 --- package-lock.json | 472 ++++++++++++++++++ package.json | 1 + src/utils/deepstate.ts | 7 +- src/utils/formatutils.ts | 14 +- src/utils/policyutil.tsx | 13 +- .../{deepstate.test.js => deepstate.test.ts} | 0 ...ormatutils.test.js => formatutils.test.ts} | 0 ...{policyutil.test.js => policyutil.test.ts} | 34 +- 8 files changed, 501 insertions(+), 40 deletions(-) rename tests/utils/{deepstate.test.js => deepstate.test.ts} (100%) rename tests/utils/{formatutils.test.js => formatutils.test.ts} (100%) rename tests/utils/{policyutil.test.js => policyutil.test.ts} (86%) diff --git a/package-lock.json b/package-lock.json index 0810c682..24157527 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,6 +27,7 @@ "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.3.0", "@testing-library/user-event": "^14.6.1", + "@types/jest": "^30.0.0", "@types/react": "^19.2.0", "@types/react-dom": "^19.2.0", "@vitejs/plugin-react": "^4.7.0", @@ -1248,6 +1249,85 @@ "node": ">=8" } }, + "node_modules/@jest/diff-sequences": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.0.1.tgz", + "integrity": "sha512-n5H8QLDJ47QqbCNn5SuFjCRDrOLEZ0h8vAHCK5RL9Ls7Xa8AQLa/YxAc9UjFqoEDM48muwtBGjtMY5cr0PLDCw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.2.0.tgz", + "integrity": "sha512-1JnRfhqpD8HGpOmQp180Fo9Zt69zNtC+9lR+kT7NVL05tNXIi+QC8Csz7lfidMoVLPD3FnOtcmp0CEFnxExGEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/get-type": { + "version": "30.1.0", + "resolved": "https://registry.npmjs.org/@jest/get-type/-/get-type-30.1.0.tgz", + "integrity": "sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/pattern": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.0.1.tgz", + "integrity": "sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-regex-util": "30.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.34.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/types": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.2.0.tgz", + "integrity": "sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.12", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz", @@ -1730,6 +1810,13 @@ "win32" ] }, + "node_modules/@sinclair/typebox": { + "version": "0.34.41", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.41.tgz", + "integrity": "sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g==", + "dev": true, + "license": "MIT" + }, "node_modules/@swc/helpers": { "version": "0.5.17", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.17.tgz", @@ -1957,6 +2044,79 @@ "@types/node": "*" } }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "30.0.0", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-30.0.0.tgz", + "integrity": "sha512-XTYugzhuwqWjws0CVz8QpM36+T+Dz5mTEBKhNs/esGLnCIlGdRy+Dq78NRjd7ls7r8BC8ZRMOrKlkO1hU0JOwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^30.0.0", + "pretty-format": "^30.0.0" + } + }, + "node_modules/@types/jest/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@types/jest/node_modules/pretty-format": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz", + "integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@types/jest/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -2007,12 +2167,36 @@ "@types/react": "*" } }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/warning": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/warning/-/warning-3.0.3.tgz", "integrity": "sha512-D1XC7WK8K+zZEveUPY+cf4+kgauk8N4eHr/XIHXGlGYkHLud6hK9lYfZk1ry1TNh798cZUCgb6MqGEG8DkJt6Q==", "license": "MIT" }, + "node_modules/@types/yargs": { + "version": "17.0.33", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", + "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.33.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.33.0.tgz", @@ -2935,6 +3119,22 @@ "node": ">= 16" } }, + "node_modules/ci-info": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.1.tgz", + "integrity": "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/classnames": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", @@ -3736,6 +3936,24 @@ "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", "license": "MIT" }, + "node_modules/expect": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-30.2.0.tgz", + "integrity": "sha512-u/feCi0GPsI+988gU2FLcsHyAHTU0MX1Wg68NhAnN7z/+C5wqG+CY8J53N9ioe8RXgaoz0nBR/TYMf3AycUuPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "30.2.0", + "@jest/get-type": "30.1.0", + "jest-matcher-utils": "30.2.0", + "jest-message-util": "30.2.0", + "jest-mock": "30.2.0", + "jest-util": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, "node_modules/expect-type": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.1.tgz", @@ -4165,6 +4383,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, "node_modules/graphemer": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", @@ -4956,6 +5181,220 @@ "@pkgjs/parseargs": "^0.11.0" } }, + "node_modules/jest-diff": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.2.0.tgz", + "integrity": "sha512-dQHFo3Pt4/NLlG5z4PxZ/3yZTZ1C7s9hveiOj+GCN+uT109NC2QgsoVZsVOAvbJ3RgKkvyLGXZV9+piDpWbm6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/diff-sequences": "30.0.1", + "@jest/get-type": "30.1.0", + "chalk": "^4.1.2", + "pretty-format": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-diff/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-diff/node_modules/pretty-format": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz", + "integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-diff/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-matcher-utils": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.2.0.tgz", + "integrity": "sha512-dQ94Nq4dbzmUWkQ0ANAWS9tBRfqCrn0bV9AMYdOi/MHW726xn7eQmMeRTpX2ViC00bpNaWXq+7o4lIQ3AX13Hg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "chalk": "^4.1.2", + "jest-diff": "30.2.0", + "pretty-format": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-matcher-utils/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-matcher-utils/node_modules/pretty-format": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz", + "integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-matcher-utils/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-message-util": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.2.0.tgz", + "integrity": "sha512-y4DKFLZ2y6DxTWD4cDe07RglV88ZiNEdlRfGtqahfbIjfsw1nMCPx49Uev4IA/hWn3sDKyAnSPwoYSsAEdcimw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@jest/types": "30.2.0", + "@types/stack-utils": "^2.0.3", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "micromatch": "^4.0.8", + "pretty-format": "30.2.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.6" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-message-util/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-message-util/node_modules/pretty-format": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz", + "integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-message-util/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-mock": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.2.0.tgz", + "integrity": "sha512-JNNNl2rj4b5ICpmAcq+WbLH83XswjPbjH4T7yvGzfAGCPh1rw+xVNbtk+FnRslvt9lkCcdn9i1oAoKUuFsOxRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.2.0", + "@types/node": "*", + "jest-util": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-regex-util": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.1.tgz", + "integrity": "sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-util": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.2.0.tgz", + "integrity": "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.2.0", + "@types/node": "*", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-util/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -6401,6 +6840,16 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -6411,6 +6860,29 @@ "node": ">=0.10.0" } }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/stackback": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", diff --git a/package.json b/package.json index bd209c8c..f7b9350b 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.3.0", "@testing-library/user-event": "^14.6.1", + "@types/jest": "^30.0.0", "@types/react": "^19.2.0", "@types/react-dom": "^19.2.0", "@vitejs/plugin-react": "^4.7.0", diff --git a/src/utils/deepstate.ts b/src/utils/deepstate.ts index e0cc9d7c..1a84ac75 100644 --- a/src/utils/deepstate.ts +++ b/src/utils/deepstate.ts @@ -6,7 +6,7 @@ // getDeepStateProperty("a.b") returns {"c":true} // getDeepStateProperty("a.b.c") returns true -export function setDeepStateProperty(component: HasState, name: string, value: any): void { +export function setDeepStateProperty(component: StateReadwrite, name: string, value: any): void { const newState = { ...component.state }; let st = newState; @@ -30,14 +30,15 @@ export function setDeepStateProperty(component: HasState, name: string, value: a component.setState(newState); } -type HasState = { state: any; setState: (s: any) => void }; +type StateReadwrite = { state: any; setState: (s: any) => void }; +type StateReadonly = { state: any }; // getDeepStateProperty returns the provided deep state property or a default value // For example: { "a": { "b": { "c": true } } } // getDeepStateProperty("a") returns {b":{"c":true}} // getDeepStateProperty("a.b") returns {"c":true} // getDeepStateProperty("a.b.c") returns true -export function getDeepStateProperty(component: HasState, name: string, defaultValue = ""): any { +export function getDeepStateProperty(component: StateReadonly, name: string, defaultValue: any = ""): any { let st = component.state; const parts = name.split(/\./); diff --git a/src/utils/formatutils.ts b/src/utils/formatutils.ts index f269f83f..00ebb20b 100644 --- a/src/utils/formatutils.ts +++ b/src/utils/formatutils.ts @@ -18,7 +18,7 @@ function toDecimalUnitString(f: number, thousand: number, prefixes: string[], su return formatNumber(f) + " " + prefixes[prefixes.length - 1] + suffix; } -export function sizeDisplayName(size: number, bytesStringBase2: boolean): string { +export function sizeDisplayName(size: number | undefined, bytesStringBase2: boolean = false): string { if (size === undefined) { return ""; } @@ -32,14 +32,6 @@ export function intervalDisplayName() { return "-"; } -// This is never used -export function timesOfDayDisplayName(v) { - if (!v) { - return "(none)"; - } - return v.length + " times"; -} - type QueryParams = { [param: string]: string }; export function parseQuery(queryString: string): QueryParams { const query: QueryParams = {}; @@ -51,7 +43,7 @@ export function parseQuery(queryString: string): QueryParams { return query; } -export function rfc3339TimestampForDisplay(n: number | string | Date) { +export function rfc3339TimestampForDisplay(n: null | undefined | number | string | Date) { if (!n) { return ""; } @@ -216,7 +208,7 @@ export function formatMilliseconds(ms: number, useMultipleUnits = false): string ); } -export function formatDuration(from: number | string | Date, to: number | string | Date, useMultipleUnits = false) { +export function formatDuration(from: undefined | null | number | string | Date, to?: number | string | Date, useMultipleUnits = false) { if (!from) { return ""; } diff --git a/src/utils/policyutil.tsx b/src/utils/policyutil.tsx index 0947e5e4..1fa42adb 100644 --- a/src/utils/policyutil.tsx +++ b/src/utils/policyutil.tsx @@ -52,7 +52,7 @@ export function checkPolicyPath(path: string) { return "Policies can not be defined for relative paths."; } -export function PolicyTypeName(s) { +export function PolicyTypeName(s: Partial) { if (!s.host && !s.userName) { return "Global Policy"; } @@ -75,16 +75,9 @@ export interface PolicyQueryParams { }; export function sourceQueryStringParams(src: PolicyQueryParams) { - return ( - "userName=" + - encodeURIComponent(src.userName) + - "&host=" + - encodeURIComponent(src.host) + - "&path=" + - encodeURIComponent(src.path) - ); + return `userName=${encodeURIComponent(src.userName)}&host=${encodeURIComponent(src.host)}&path=${encodeURIComponent(src.path)}`; } export function policyEditorURL(s: PolicyQueryParams) { - return "/policies/edit?" + sourceQueryStringParams(s); + return `/policies/edit?${sourceQueryStringParams(s)}`; } diff --git a/tests/utils/deepstate.test.js b/tests/utils/deepstate.test.ts similarity index 100% rename from tests/utils/deepstate.test.js rename to tests/utils/deepstate.test.ts diff --git a/tests/utils/formatutils.test.js b/tests/utils/formatutils.test.ts similarity index 100% rename from tests/utils/formatutils.test.js rename to tests/utils/formatutils.test.ts diff --git a/tests/utils/policyutil.test.js b/tests/utils/policyutil.test.ts similarity index 86% rename from tests/utils/policyutil.test.js rename to tests/utils/policyutil.test.ts index d89b0a1e..92d3defc 100644 --- a/tests/utils/policyutil.test.js +++ b/tests/utils/policyutil.test.ts @@ -104,17 +104,18 @@ describe("sourceQueryStringParams", () => { expect(result).toContain("path=%2Fpath%20with%20spaces"); }); - it("handles undefined or null values", () => { - const source = { - userName: undefined, - host: "examplehost", - path: null, - }; - const result = sourceQueryStringParams(source); - expect(result).toContain("userName=undefined"); - expect(result).toContain("host=examplehost"); - expect(result).toContain("path=null"); - }); + // This shouldn't be possible with the current type definition and the expected fallback behavior is pointless since getSnapshotSourceFromURL in the backend doesn't support it. + // it("handles undefined or null values", () => { + // const source = { + // userName: undefined, + // host: "examplehost", + // path: null, + // }; + // const result = sourceQueryStringParams(source); + // expect(result).toContain("userName=undefined"); + // expect(result).toContain("host=examplehost"); + // expect(result).toContain("path=null"); + // }); it("handles empty values", () => { const source = { @@ -195,11 +196,12 @@ describe("policyEditorURL", () => { expect(url).toContain("path=%2Fpath%20with%20spaces"); }); - it("handles empty source", () => { - const source = {}; - const url = policyEditorURL(source); - expect(url).toBe("/policies/edit?userName=undefined&host=undefined&path=undefined"); - }); + // This shouldn't be possible with the current type definition and the expected fallback behavior is pointless since getSnapshotSourceFromURL in the backend doesn't support it. + // it("handles empty source", () => { + // const source = {}; + // const url = policyEditorURL(source); + // expect(url).toBe("/policies/edit?userName=undefined&host=undefined&path=undefined"); + // }); it("generates URL for global policy", () => { const source = { userName: "", host: "", path: "" }; From ad1391e94480982988219d13bab4e62e2702ba9c Mon Sep 17 00:00:00 2001 From: QazCetelic Date: Sun, 5 Oct 2025 19:47:42 +0200 Subject: [PATCH 08/29] refactor(ui): Add types to tests 2 --- src/utils/uiutil.tsx | 19 ++++++++++++------- .../{Policies.test.jsx => Policies.test.tsx} | 10 +++++----- .../{Policy.test.jsx => Policy.test.tsx} | 7 ++++--- .../testutils/{api-mocks.js => api-mocks.ts} | 0 .../utils/{uiutil.test.js => uiutil.test.ts} | 0 5 files changed, 21 insertions(+), 15 deletions(-) rename tests/pages/{Policies.test.jsx => Policies.test.tsx} (98%) rename tests/pages/{Policy.test.jsx => Policy.test.tsx} (97%) rename tests/testutils/{api-mocks.js => api-mocks.ts} (100%) rename tests/utils/{uiutil.test.js => uiutil.test.ts} (100%) diff --git a/src/utils/uiutil.tsx b/src/utils/uiutil.tsx index ba5b01ac..11364984 100644 --- a/src/utils/uiutil.tsx +++ b/src/utils/uiutil.tsx @@ -55,19 +55,24 @@ export function errorAlert(err, prefix?) { } } -export function toAlgorithmOption(x, defaultID) { - let text = x.id; +type Algorithm = { + id: string; + deprecated?: boolean; +}; - if (x.id === defaultID) { - text = x.id + " (RECOMMENDED)"; +export function toAlgorithmOption(algorithm: Algorithm, defaultID?) { + let text = algorithm.id; + + if (algorithm.id === defaultID) { + text = algorithm.id + " (RECOMMENDED)"; } - if (x.deprecated) { - text = x.id + " (NOT RECOMMENDED)"; + if (algorithm.deprecated) { + text = algorithm.id + " (NOT RECOMMENDED)"; } return ( - ); diff --git a/tests/pages/Policies.test.jsx b/tests/pages/Policies.test.tsx similarity index 98% rename from tests/pages/Policies.test.jsx rename to tests/pages/Policies.test.tsx index 556b9fec..b4969492 100644 --- a/tests/pages/Policies.test.jsx +++ b/tests/pages/Policies.test.tsx @@ -3,10 +3,10 @@ import { describe, test, expect, beforeEach, afterEach, vi } from "vitest"; import { render, screen, waitFor } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { BrowserRouter } from "react-router-dom"; -import { Policies, PoliciesInternal } from "../../src/pages/Policies"; -import { AppContext } from "../../src/contexts/AppContext"; -import { UIPreferencesContext } from "../../src/contexts/UIPreferencesContext"; -import { setupAPIMock } from "../testutils/api-mocks"; +import { Policies, PoliciesInternal } from "../../src/pages/Policies.js"; +import { AppContext } from "../../src/contexts/AppContext.js"; +import { PageSize, UIPreferences, UIPreferencesContext } from "../../src/contexts/UIPreferencesContext.js"; +import { setupAPIMock } from "../testutils/api-mocks.js"; import "@testing-library/jest-dom"; import { mockNavigate, resetRouterMocks } from "../testutils/react-router-mock.jsx"; @@ -25,7 +25,7 @@ const mockAppContextValue = { repoDescription: "Test Repository", }; -const mockUIPreferencesContext = { +const mockUIPreferencesContext: UIPreferences = { pageSize: 10, setPageSize: vi.fn(), theme: "light", diff --git a/tests/pages/Policy.test.jsx b/tests/pages/Policy.test.tsx similarity index 97% rename from tests/pages/Policy.test.jsx rename to tests/pages/Policy.test.tsx index 608a2494..84b898c2 100644 --- a/tests/pages/Policy.test.jsx +++ b/tests/pages/Policy.test.tsx @@ -5,9 +5,10 @@ import { render, screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { MemoryRouter } from "react-router-dom"; import "@testing-library/jest-dom"; -import { Policy } from "../../src/pages/Policy"; -import { setupAPIMock } from "../testutils/api-mocks"; +import { Policy } from "../../src/pages/Policy.js"; +import { setupAPIMock } from "../testutils/api-mocks.js"; import { mockNavigate, resetRouterMocks, updateRouterMocks } from "../testutils/react-router-mock.jsx"; +import { PolicyQueryParams } from "../../src/utils/policyutil.js"; // Mock React Router hooks using unified helper vi.mock("react-router-dom", async () => { @@ -17,7 +18,7 @@ vi.mock("react-router-dom", async () => { // Mock the PolicyEditor component to avoid complex dependencies vi.mock("../../src/components/policy-editor/PolicyEditor", () => ({ - PolicyEditor: React.forwardRef(function MockPolicyEditor(props, _ref) { + PolicyEditor: React.forwardRef(function MockPolicyEditor(props: PolicyQueryParams & { close }, _ref) { return (

PolicyEditor Mock
diff --git a/tests/testutils/api-mocks.js b/tests/testutils/api-mocks.ts similarity index 100% rename from tests/testutils/api-mocks.js rename to tests/testutils/api-mocks.ts diff --git a/tests/utils/uiutil.test.js b/tests/utils/uiutil.test.ts similarity index 100% rename from tests/utils/uiutil.test.js rename to tests/utils/uiutil.test.ts From bccdad1b62be4a2e49c31c93fa861234a5565b3b Mon Sep 17 00:00:00 2001 From: QazCetelic Date: Sun, 5 Oct 2025 20:13:29 +0200 Subject: [PATCH 09/29] refactor(ui): Add types to tests 3 --- src/global.d.ts => global.d.ts | 3 ++- .../{Preferences.test.jsx => Preferences.test.tsx} | 2 +- .../pages/{Repository.test.jsx => Repository.test.tsx} | 2 +- ...SnapshotCreate.test.jsx => SnapshotCreate.test.tsx} | 10 +++++----- ...otDirectory.test.jsx => SnapshotDirectory.test.tsx} | 9 +++++---- tsconfig.json | 2 +- 6 files changed, 15 insertions(+), 13 deletions(-) rename src/global.d.ts => global.d.ts (55%) rename tests/pages/{Preferences.test.jsx => Preferences.test.tsx} (92%) rename tests/pages/{Repository.test.jsx => Repository.test.tsx} (99%) rename tests/pages/{SnapshotCreate.test.jsx => SnapshotCreate.test.tsx} (97%) rename tests/pages/{SnapshotDirectory.test.jsx => SnapshotDirectory.test.tsx} (98%) diff --git a/src/global.d.ts b/global.d.ts similarity index 55% rename from src/global.d.ts rename to global.d.ts index 78df4314..d47c59fa 100644 --- a/src/global.d.ts +++ b/global.d.ts @@ -1,6 +1,7 @@ // Extend the Window interface to include the kopiaUI property declare interface Window { kopiaUI?: { - selectDirectory: (callback: (path: string) => void) => void; + selectDirectory?: (callback: (path: string) => void) => void; + browseDirectory?; }; } \ No newline at end of file diff --git a/tests/pages/Preferences.test.jsx b/tests/pages/Preferences.test.tsx similarity index 92% rename from tests/pages/Preferences.test.jsx rename to tests/pages/Preferences.test.tsx index e93bf9c5..034771f5 100644 --- a/tests/pages/Preferences.test.jsx +++ b/tests/pages/Preferences.test.tsx @@ -34,7 +34,7 @@ describe("Select the light theme", () => { screen.getByRole("option", { name: "light" }), ); - expect(screen.getByRole("option", { name: "light" }).selected).toBe(true); + expect((screen.getByRole("option", { name: "light" }) as HTMLOptionElement).selected).toBe(true); }); }); diff --git a/tests/pages/Repository.test.jsx b/tests/pages/Repository.test.tsx similarity index 99% rename from tests/pages/Repository.test.jsx rename to tests/pages/Repository.test.tsx index 05dbfc79..c723ee88 100644 --- a/tests/pages/Repository.test.jsx +++ b/tests/pages/Repository.test.tsx @@ -299,7 +299,7 @@ describe("Repository component - initializing state", () => { if (callCount === 1) { return [200, { connected: false, initTaskID: "task-123" }]; } else { - return [200, { connected: true, description: "Connected!", ...connectedStatus }]; + return [200, { ...connectedStatus, connected: true, description: "Connected!" }]; } }); diff --git a/tests/pages/SnapshotCreate.test.jsx b/tests/pages/SnapshotCreate.test.tsx similarity index 97% rename from tests/pages/SnapshotCreate.test.jsx rename to tests/pages/SnapshotCreate.test.tsx index c6455bf0..a7b4486b 100644 --- a/tests/pages/SnapshotCreate.test.jsx +++ b/tests/pages/SnapshotCreate.test.tsx @@ -2,8 +2,8 @@ import React from "react"; import { describe, test, expect, beforeEach, afterEach, vi } from "vitest"; import { render, screen, waitFor, act } from "@testing-library/react"; -import { SnapshotCreate } from "../../src/pages/SnapshotCreate"; -import { setupAPIMock } from "../testutils/api-mocks"; +import { SnapshotCreate } from "../../src/pages/SnapshotCreate.js"; +import { setupAPIMock } from "../testutils/api-mocks.js"; import { fireEvent } from "@testing-library/react"; import "@testing-library/jest-dom"; @@ -31,7 +31,7 @@ vi.mock("../../src/components/GoBackButton", () => ({ // Mock PolicyEditor with a simple implementation that tracks ref calls vi.mock("../../src/components/policy-editor/PolicyEditor", () => ({ - PolicyEditor: React.forwardRef((props, ref) => { + PolicyEditor: React.forwardRef((props: { embedded, path }, ref) => { React.useImperativeHandle(ref, () => ({ getAndValidatePolicy: vi.fn(() => ({ somePolicy: "data" })), })); @@ -296,7 +296,7 @@ describe("SnapshotCreate component", () => { }); test("handles API errors gracefully", async () => { - const { errorAlert } = await import("../../src/utils/uiutil"); + const { errorAlert } = await import("../../src/utils/uiutil.js"); const mockErrorAlert = vi.mocked(errorAlert); axiosMock.onGet("/api/v1/sources").reply(200, { @@ -477,7 +477,7 @@ describe("SnapshotCreate component", () => { }); test("handles sources API failure on mount", async () => { - const { redirect } = await import("../../src/utils/uiutil"); + const { redirect } = await import("../../src/utils/uiutil.js"); const mockRedirect = vi.mocked(redirect); axiosMock.onGet("/api/v1/sources").reply(500, { diff --git a/tests/pages/SnapshotDirectory.test.jsx b/tests/pages/SnapshotDirectory.test.tsx similarity index 98% rename from tests/pages/SnapshotDirectory.test.jsx rename to tests/pages/SnapshotDirectory.test.tsx index cc13427f..36d3a4e5 100644 --- a/tests/pages/SnapshotDirectory.test.jsx +++ b/tests/pages/SnapshotDirectory.test.tsx @@ -2,10 +2,11 @@ import React from "react"; import { describe, test, expect, beforeEach, afterEach, vi } from "vitest"; import { render, screen, waitFor } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; -import { SnapshotDirectory } from "../../src/pages/SnapshotDirectory"; -import { UIPreferencesContext } from "../../src/contexts/UIPreferencesContext"; -import { setupAPIMock } from "../testutils/api-mocks"; +import { SnapshotDirectory } from "../../src/pages/SnapshotDirectory.js"; +import { UIPreferences, UIPreferencesContext } from "../../src/contexts/UIPreferencesContext.js"; +import { setupAPIMock } from "../testutils/api-mocks.js"; import "@testing-library/jest-dom"; +import "../../global.d.ts"; let axiosMock; @@ -33,7 +34,7 @@ vi.mock("../../src/components/DirectoryItems", () => ({ })); // Minimal UIPreferences context value -const mockUIPreferences = { +const mockUIPreferences: UIPreferences = { pageSize: 10, theme: "light", bytesStringBase2: false, diff --git a/tsconfig.json b/tsconfig.json index 14cf8438..61bec004 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -18,6 +18,6 @@ "baseUrl": "./", "paths": {} }, - "include": ["src/**/*"], + "include": ["src/**/*", "global.d.ts", "test/global.d.ts"], "exclude": ["node_modules", "build", "dist"] } From b7e676ce2188ce4c6c409771e02eb16b40d05bc7 Mon Sep 17 00:00:00 2001 From: QazCetelic Date: Sun, 5 Oct 2025 20:26:08 +0200 Subject: [PATCH 10/29] refactor(ui): Add types to tests 4 --- ...tory.test.jsx => SnapshotHistory.test.tsx} | 50 +++++++++---------- ...tore.test.jsx => SnapshotRestore.test.tsx} | 10 ++-- ...{Snapshots.test.jsx => Snapshots.test.tsx} | 10 ++-- tests/pages/{Task.test.jsx => Task.test.tsx} | 8 +-- .../pages/{Tasks.test.jsx => Tasks.test.tsx} | 12 ++--- 5 files changed, 45 insertions(+), 45 deletions(-) rename tests/pages/{SnapshotHistory.test.jsx => SnapshotHistory.test.tsx} (88%) rename tests/pages/{SnapshotRestore.test.jsx => SnapshotRestore.test.tsx} (97%) rename tests/pages/{Snapshots.test.jsx => Snapshots.test.tsx} (97%) rename tests/pages/{Task.test.jsx => Task.test.tsx} (96%) rename tests/pages/{Tasks.test.jsx => Tasks.test.tsx} (97%) diff --git a/tests/pages/SnapshotHistory.test.jsx b/tests/pages/SnapshotHistory.test.tsx similarity index 88% rename from tests/pages/SnapshotHistory.test.jsx rename to tests/pages/SnapshotHistory.test.tsx index a61b0d2d..6cbf8210 100644 --- a/tests/pages/SnapshotHistory.test.jsx +++ b/tests/pages/SnapshotHistory.test.tsx @@ -2,10 +2,10 @@ import React from "react"; import { render, screen, fireEvent, waitFor, act } from "@testing-library/react"; import "@testing-library/jest-dom"; import { describe, it, expect, vi, beforeEach } from "vitest"; -import { SnapshotHistory } from "../../src/pages/SnapshotHistory"; +import { SnapshotHistory } from "../../src/pages/SnapshotHistory.js"; import { BrowserRouter } from "react-router-dom"; import axios from "axios"; -import { UIPreferencesContext } from "../../src/contexts/UIPreferencesContext"; +import { UIPreferences, UIPreferencesContext } from "../../src/contexts/UIPreferencesContext.js"; import { mockNavigate, resetRouterMocks } from "../testutils/react-router-mock.jsx"; // Mock axios @@ -28,7 +28,7 @@ const renderWithProviders = (component) => { showIdenticalSnapshots: false, setShowIdenticalSnapshots: vi.fn(), bytesStringBase2: vi.fn((size) => `${size} bytes`), // Mock function for size formatting - }; + } as unknown as UIPreferences; return render( @@ -41,12 +41,12 @@ describe("SnapshotHistory", () => { beforeEach(() => { resetRouterMocks(); vi.clearAllMocks(); - axios.get.mockReset(); - axios.post.mockReset(); + (axios.get as jest.Mock).mockReset(); + (axios.post as jest.Mock).mockReset(); }); it("should render loading spinner initially", () => { - axios.get.mockImplementation(() => new Promise(() => {})); // Never resolves + (axios.get as jest.Mock).mockImplementation(() => new Promise(() => {})); // Never resolves renderWithProviders(); // Look for Bootstrap spinner div @@ -80,7 +80,7 @@ describe("SnapshotHistory", () => { }, }; - axios.get.mockResolvedValue(mockResponse); + (axios.get as jest.Mock).mockResolvedValue(mockResponse); renderWithProviders(); await waitFor(() => { @@ -100,7 +100,7 @@ describe("SnapshotHistory", () => { }, }; - axios.get.mockResolvedValue(mockResponse); + (axios.get as jest.Mock).mockResolvedValue(mockResponse); renderWithProviders(); await waitFor(() => { @@ -120,7 +120,7 @@ describe("SnapshotHistory", () => { }); it("should display error when fetch fails", async () => { - axios.get.mockRejectedValue(new Error("Network error")); + (axios.get as jest.Mock).mockRejectedValue(new Error("Network error")); // Mock console.error to avoid error output in tests const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {}); @@ -141,7 +141,7 @@ describe("SnapshotHistory", () => { }, }; - axios.get.mockResolvedValue(mockResponse); + (axios.get as jest.Mock).mockResolvedValue(mockResponse); renderWithProviders(); await waitFor(() => { @@ -156,7 +156,7 @@ describe("SnapshotHistory", () => { }, }; - axios.get.mockResolvedValue(mockResponse); + (axios.get as jest.Mock).mockResolvedValue(mockResponse); renderWithProviders(); await waitFor(() => { @@ -176,7 +176,7 @@ describe("SnapshotHistory", () => { }, }; - axios.get.mockResolvedValue(mockResponse); + (axios.get as jest.Mock).mockResolvedValue(mockResponse); renderWithProviders(); await waitFor(() => { @@ -199,7 +199,7 @@ describe("SnapshotHistory", () => { }, }; - axios.get.mockResolvedValue(mockResponse); + (axios.get as jest.Mock).mockResolvedValue(mockResponse); renderWithProviders(); await waitFor(() => { @@ -214,7 +214,7 @@ describe("SnapshotHistory", () => { }, }; - axios.get.mockResolvedValue(mockResponse); + (axios.get as jest.Mock).mockResolvedValue(mockResponse); renderWithProviders(); await waitFor(() => { @@ -272,7 +272,7 @@ describe("SnapshotHistory", () => { }, }; - axios.get.mockResolvedValue(mockResponse); + (axios.get as jest.Mock).mockResolvedValue(mockResponse); renderWithProviders(); await waitFor(() => { @@ -341,7 +341,7 @@ describe("SnapshotHistory", () => { }, }; - axios.get.mockResolvedValue(emptyResponse); + (axios.get as jest.Mock).mockResolvedValue(emptyResponse); renderWithProviders(); await waitFor(() => { @@ -358,8 +358,8 @@ describe("SnapshotHistory", () => { }, }; - axios.get.mockResolvedValue(emptyResponse); - axios.post.mockResolvedValue({ data: { success: true } }); + (axios.get as jest.Mock).mockResolvedValue(emptyResponse); + (axios.post as jest.Mock).mockResolvedValue({ data: { success: true } }); renderWithProviders(); await waitFor(() => { @@ -382,7 +382,7 @@ describe("SnapshotHistory", () => { }); it("should display select all button when snapshots exist", async () => { - axios.get.mockResolvedValue(mockSnapshotsResponse); + (axios.get as jest.Mock).mockResolvedValue(mockSnapshotsResponse); renderWithProviders(); await waitFor(() => { @@ -394,7 +394,7 @@ describe("SnapshotHistory", () => { }); it("should show correct snapshot count", async () => { - axios.get.mockResolvedValue(mockSnapshotsResponse); + (axios.get as jest.Mock).mockResolvedValue(mockSnapshotsResponse); renderWithProviders(); await waitFor(() => { @@ -403,7 +403,7 @@ describe("SnapshotHistory", () => { }); it("should show table headers for snapshot data", async () => { - axios.get.mockResolvedValue(mockSnapshotsResponse); + (axios.get as jest.Mock).mockResolvedValue(mockSnapshotsResponse); renderWithProviders(); await waitFor(() => { @@ -426,8 +426,8 @@ describe("SnapshotHistory", () => { }, }; - axios.get.mockResolvedValue(emptyResponse); - axios.post.mockRejectedValue(new Error("Delete failed")); + (axios.get as jest.Mock).mockResolvedValue(emptyResponse); + (axios.post as jest.Mock).mockRejectedValue(new Error("Delete failed")); // Mock console.error to avoid test output noise const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); @@ -458,8 +458,8 @@ describe("SnapshotHistory", () => { }, }; - axios.get.mockResolvedValue(emptyResponse); - axios.post.mockResolvedValue({ data: { success: true } }); + (axios.get as jest.Mock).mockResolvedValue(emptyResponse); + (axios.post as jest.Mock).mockResolvedValue({ data: { success: true } }); renderWithProviders(); await waitFor(() => { diff --git a/tests/pages/SnapshotRestore.test.jsx b/tests/pages/SnapshotRestore.test.tsx similarity index 97% rename from tests/pages/SnapshotRestore.test.jsx rename to tests/pages/SnapshotRestore.test.tsx index c47f0713..0d174e12 100644 --- a/tests/pages/SnapshotRestore.test.jsx +++ b/tests/pages/SnapshotRestore.test.tsx @@ -1,9 +1,9 @@ import React from "react"; import { describe, test, expect, beforeEach, afterEach, vi } from "vitest"; import { render, screen, waitFor } from "@testing-library/react"; -import { SnapshotRestore } from "../../src/pages/SnapshotRestore"; -import { UIPreferencesContext } from "../../src/contexts/UIPreferencesContext"; -import { setupAPIMock } from "../testutils/api-mocks"; +import { SnapshotRestore } from "../../src/pages/SnapshotRestore.js"; +import { UIPreferences, UIPreferencesContext } from "../../src/contexts/UIPreferencesContext.js"; +import { setupAPIMock } from "../testutils/api-mocks.js"; import { fireEvent } from "@testing-library/react"; import "@testing-library/jest-dom"; @@ -30,7 +30,7 @@ vi.mock("../../src/utils/uiutil", () => ({ })); // Minimal UIPreferences context value -const mockUIPreferences = { +const mockUIPreferences: UIPreferences = { pageSize: 10, theme: "light", bytesStringBase2: false, @@ -235,7 +235,7 @@ describe("SnapshotRestore component", () => { }); test("handles API error gracefully", async () => { - const { errorAlert } = await import("../../src/utils/uiutil"); + const { errorAlert } = await import("../../src/utils/uiutil.js"); const mockErrorAlert = vi.mocked(errorAlert); axiosMock.onPost("/api/v1/restore").reply(500, { diff --git a/tests/pages/Snapshots.test.jsx b/tests/pages/Snapshots.test.tsx similarity index 97% rename from tests/pages/Snapshots.test.jsx rename to tests/pages/Snapshots.test.tsx index 3800bcf2..23e36192 100644 --- a/tests/pages/Snapshots.test.jsx +++ b/tests/pages/Snapshots.test.tsx @@ -2,9 +2,9 @@ import React from "react"; import { describe, test, expect, beforeEach, afterEach, vi } from "vitest"; import { render, screen, waitFor } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; -import { Snapshots } from "../../src/pages/Snapshots"; -import { UIPreferencesContext } from "../../src/contexts/UIPreferencesContext"; -import { setupAPIMock } from "../testutils/api-mocks"; +import { Snapshots } from "../../src/pages/Snapshots.js"; +import { UIPreferences, UIPreferencesContext } from "../../src/contexts/UIPreferencesContext.js"; +import { setupAPIMock } from "../testutils/api-mocks.js"; import "@testing-library/jest-dom"; let axiosMock; @@ -16,7 +16,7 @@ vi.mock("react-router-dom", async () => { }); // Minimal UIPreferences context value -const mockUIPreferences = { +const mockUIPreferences: UIPreferences = { pageSize: 10, theme: "light", bytesStringBase2: false, @@ -174,7 +174,7 @@ describe("Snapshots component", () => { // The sync button contains an icon that has the onClick handler const syncButton = screen.getByTitle("Synchronize"); const syncIcon = syncButton.querySelector("svg"); - await userEvent.click(syncIcon); + await userEvent.click(syncIcon!); await waitFor(() => { expect(axiosMock.history.post).toHaveLength(1); diff --git a/tests/pages/Task.test.jsx b/tests/pages/Task.test.tsx similarity index 96% rename from tests/pages/Task.test.jsx rename to tests/pages/Task.test.tsx index 5fe0ed44..26a782c0 100644 --- a/tests/pages/Task.test.jsx +++ b/tests/pages/Task.test.tsx @@ -1,9 +1,9 @@ import React from "react"; import { describe, test, expect, beforeEach, afterEach, vi } from "vitest"; import { render, screen, waitFor } from "@testing-library/react"; -import { Task } from "../../src/pages/Task"; -import { UIPreferencesContext } from "../../src/contexts/UIPreferencesContext"; -import { setupAPIMock } from "../testutils/api-mocks"; +import { Task } from "../../src/pages/Task.js"; +import { UIPreferences, UIPreferencesContext } from "../../src/contexts/UIPreferencesContext.js"; +import { setupAPIMock } from "../testutils/api-mocks.js"; import "@testing-library/jest-dom"; let axiosMock; @@ -26,7 +26,7 @@ vi.mock("../../src/components/Logs", () => ({ })); // Minimal UIPreferences context value -const mockUIPreferences = { +const mockUIPreferences: UIPreferences = { pageSize: 10, theme: "light", bytesStringBase2: false, diff --git a/tests/pages/Tasks.test.jsx b/tests/pages/Tasks.test.tsx similarity index 97% rename from tests/pages/Tasks.test.jsx rename to tests/pages/Tasks.test.tsx index c6ade955..4907c5aa 100644 --- a/tests/pages/Tasks.test.jsx +++ b/tests/pages/Tasks.test.tsx @@ -2,12 +2,12 @@ import React from "react"; import { describe, test, expect, beforeEach, afterEach, vi } from "vitest"; import { render, screen, waitFor } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; -import { Tasks } from "../../src/pages/Tasks"; -import { UIPreferencesContext } from "../../src/contexts/UIPreferencesContext"; -import { setupAPIMock } from "../testutils/api-mocks"; +import { Tasks } from "../../src/pages/Tasks.js"; +import { UIPreferences, UIPreferencesContext } from "../../src/contexts/UIPreferencesContext.js"; +import { setupAPIMock } from "../testutils/api-mocks.js"; import "@testing-library/jest-dom"; import { fireEvent } from "@testing-library/react"; -import { setupIntervalMocks, cleanupIntervalMocks, triggerIntervals } from "../testutils/interval-mocks"; +import { setupIntervalMocks, cleanupIntervalMocks, triggerIntervals } from "../testutils/interval-mocks.js"; let axiosMock; @@ -18,7 +18,7 @@ vi.mock("react-router-dom", async () => { }); // Minimal UIPreferences context value -const mockUIPreferences = { +const mockUIPreferences: UIPreferences = { pageSize: 10, theme: "light", bytesStringBase2: false, @@ -208,7 +208,7 @@ describe("Tasks component", () => { const snapshotOptions = screen.getAllByText("Snapshot"); // Click on the dropdown option (not the table content) const dropdownOption = snapshotOptions.find((el) => el.classList.contains("dropdown-item")); - await userEvent.click(dropdownOption); + await userEvent.click(dropdownOption!); // Should only show snapshot tasks expect(screen.getByText("Snapshot task")).toBeInTheDocument(); From 6c018c973e705ca336412a148d4d67ef22e7066c Mon Sep 17 00:00:00 2001 From: QazCetelic Date: Mon, 6 Oct 2025 19:10:36 +0200 Subject: [PATCH 11/29] refactor(ui): Add types to tests 5 --- tests/{index.test.jsx => index.test.tsx} | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) rename tests/{index.test.jsx => index.test.tsx} (91%) diff --git a/tests/index.test.jsx b/tests/index.test.tsx similarity index 91% rename from tests/index.test.jsx rename to tests/index.test.tsx index f8a1165e..240b0147 100644 --- a/tests/index.test.jsx +++ b/tests/index.test.tsx @@ -68,10 +68,11 @@ describe("index.jsx", () => { test("handles missing root element by throwing error", async () => { // Mock getElementById to return null - document.getElementById.mockReturnValue(null); + (document.getElementById as jest.Mock).mockReturnValue(null); // Mock createRoot to throw an error when called with null - mockCreateRoot.mockImplementation((element) => { + mockCreateRoot.mockImplementation((...args) => { + const [element] = args as unknown as [HTMLElement | null]; if (!element) { throw new Error("Target container is not a DOM element."); } @@ -117,14 +118,14 @@ describe("index.jsx", () => { test("successfully loads CSS styles", async () => { // The CSS import is mocked, so we just verify the module loads without error - await expect(import("../src/index.jsx")).resolves.not.toThrow(); + await expect(import("../src/index.js")).resolves.not.toThrow(); }); test("calls createRoot and render in correct order", async () => { - const callOrder = []; + const callOrder: string[] = []; // Track call order - mockCreateRoot.mockImplementation((_element) => { + mockCreateRoot.mockImplementation((..._args) => { callOrder.push("createRoot"); return mockRoot; }); From 8e5356ae52ea0a84b5b8287edd51d5674f8aac66 Mon Sep 17 00:00:00 2001 From: QazCetelic Date: Mon, 6 Oct 2025 19:45:36 +0200 Subject: [PATCH 12/29] refactor(ui): Add types to tests 6 --- tests/{App.test.jsx => App.test.tsx} | 4 ++-- ...thod.test.jsx => EmailNotificationMethod.test.tsx} | 10 +++++----- ...d.test.jsx => PushoverNotificationMethod.test.tsx} | 10 +++++----- ...od.test.jsx => WebHookNotificationMethod.test.tsx} | 10 +++++----- .../{interval-mocks.js => interval-mocks.ts} | 11 ++++++----- 5 files changed, 23 insertions(+), 22 deletions(-) rename tests/{App.test.jsx => App.test.tsx} (99%) rename tests/components/notifications/{EmailNotificationMethod.test.jsx => EmailNotificationMethod.test.tsx} (83%) rename tests/components/notifications/{PushoverNotificationMethod.test.jsx => PushoverNotificationMethod.test.tsx} (72%) rename tests/components/notifications/{WebHookNotificationMethod.test.jsx => WebHookNotificationMethod.test.tsx} (74%) rename tests/testutils/{interval-mocks.js => interval-mocks.ts} (86%) diff --git a/tests/App.test.jsx b/tests/App.test.tsx similarity index 99% rename from tests/App.test.jsx rename to tests/App.test.tsx index e2da9215..c4661bed 100644 --- a/tests/App.test.jsx +++ b/tests/App.test.tsx @@ -105,13 +105,13 @@ beforeEach(() => { axiosMock.onPut("/api/v1/ui-preferences").reply(200, {}); // Mock window.location for React Router - delete window.location; + window.location = undefined as any; window.location = { origin: "http://localhost:3000", href: "http://localhost:3000/", pathname: "/", replace: vi.fn(), - }; + } as any; }); afterEach(() => { diff --git a/tests/components/notifications/EmailNotificationMethod.test.jsx b/tests/components/notifications/EmailNotificationMethod.test.tsx similarity index 83% rename from tests/components/notifications/EmailNotificationMethod.test.jsx rename to tests/components/notifications/EmailNotificationMethod.test.tsx index 0f68bb33..3dc22ec9 100644 --- a/tests/components/notifications/EmailNotificationMethod.test.jsx +++ b/tests/components/notifications/EmailNotificationMethod.test.tsx @@ -4,23 +4,23 @@ import { EmailNotificationMethod } from "../../../src/components/notifications/E import { fireEvent } from "@testing-library/react"; it("can set fields", async () => { - let ref = React.createRef(); + let ref = React.createRef(); const { getByTestId } = render(); - act(() => expect(ref.current.validate()).toBe(false)); + act(() => expect(ref.current!.validate()).toBe(false)); // required fireEvent.change(getByTestId("control-smtpServer"), { target: { value: "some-smtpServer" } }); fireEvent.change(getByTestId("control-smtpPort"), { target: { value: 25 } }); fireEvent.change(getByTestId("control-from"), { target: { value: "some-from@example.com" } }); fireEvent.change(getByTestId("control-to"), { target: { value: "some-to@example.com" } }); - expect(ref.current.validate()).toBe(true); + expect(ref.current!.validate()).toBe(true); // optional fireEvent.change(getByTestId("control-smtpUsername"), { target: { value: "some-username" } }); fireEvent.change(getByTestId("control-smtpPassword"), { target: { value: "some-password" } }); fireEvent.change(getByTestId("control-smtpIdentity"), { target: { value: "some-identity" } }); - expect(ref.current.validate()).toBe(true); + expect(ref.current!.validate()).toBe(true); - expect(ref.current.state).toStrictEqual({ + expect(ref.current!.state).toStrictEqual({ smtpServer: "some-smtpServer", smtpPort: 25, smtpUsername: "some-username", diff --git a/tests/components/notifications/PushoverNotificationMethod.test.jsx b/tests/components/notifications/PushoverNotificationMethod.test.tsx similarity index 72% rename from tests/components/notifications/PushoverNotificationMethod.test.jsx rename to tests/components/notifications/PushoverNotificationMethod.test.tsx index 51640dfe..c30fda40 100644 --- a/tests/components/notifications/PushoverNotificationMethod.test.jsx +++ b/tests/components/notifications/PushoverNotificationMethod.test.tsx @@ -4,18 +4,18 @@ import { PushoverNotificationMethod } from "../../../src/components/notification import { fireEvent } from "@testing-library/react"; it("can set fields", async () => { - let ref = React.createRef(); + let ref = React.createRef(); const { getByTestId } = render(); - act(() => expect(ref.current.validate()).toBe(false)); + act(() => expect(ref.current!.validate()).toBe(false)); // required fireEvent.change(getByTestId("control-appToken"), { target: { value: "some-appToken" } }); fireEvent.change(getByTestId("control-userKey"), { target: { value: "some-userKey" } }); - expect(ref.current.validate()).toBe(true); + expect(ref.current!.validate()).toBe(true); // optional - expect(ref.current.validate()).toBe(true); + expect(ref.current!.validate()).toBe(true); - expect(ref.current.state).toStrictEqual({ + expect(ref.current!.state).toStrictEqual({ appToken: "some-appToken", userKey: "some-userKey", format: "txt", diff --git a/tests/components/notifications/WebHookNotificationMethod.test.jsx b/tests/components/notifications/WebHookNotificationMethod.test.tsx similarity index 74% rename from tests/components/notifications/WebHookNotificationMethod.test.jsx rename to tests/components/notifications/WebHookNotificationMethod.test.tsx index 4fb0f75e..1551caa4 100644 --- a/tests/components/notifications/WebHookNotificationMethod.test.jsx +++ b/tests/components/notifications/WebHookNotificationMethod.test.tsx @@ -4,18 +4,18 @@ import { WebHookNotificationMethod } from "../../../src/components/notifications import { fireEvent } from "@testing-library/react"; it("can set fields", async () => { - let ref = React.createRef(); + let ref = React.createRef(); const { getByTestId } = render(); - act(() => expect(ref.current.validate()).toBe(false)); + act(() => expect(ref.current!.validate()).toBe(false)); // required fireEvent.change(getByTestId("control-endpoint"), { target: { value: "http://some-endpoint:12345" } }); - expect(ref.current.validate()).toBe(true); + expect(ref.current!.validate()).toBe(true); // optional fireEvent.change(getByTestId("control-headers"), { target: { value: "some:header\nanother:header" } }); - expect(ref.current.validate()).toBe(true); + expect(ref.current!.validate()).toBe(true); - expect(ref.current.state).toStrictEqual({ + expect(ref.current!.state).toStrictEqual({ endpoint: "http://some-endpoint:12345", method: "POST", format: "txt", diff --git a/tests/testutils/interval-mocks.js b/tests/testutils/interval-mocks.ts similarity index 86% rename from tests/testutils/interval-mocks.js rename to tests/testutils/interval-mocks.ts index 88d29b0a..ce48faca 100644 --- a/tests/testutils/interval-mocks.js +++ b/tests/testutils/interval-mocks.ts @@ -1,8 +1,9 @@ import { vi } from "vitest"; +import "@testing-library/jest-dom"; let intervalSpy; let clearIntervalSpy; -let intervalCallbacks = []; +let intervalCallbacks: { id: number, callback, delay }[] = []; let intervalId = 0; /** @@ -16,7 +17,7 @@ export function setupIntervalMocks() { intervalSpy = vi.spyOn(window, "setInterval").mockImplementation((callback, delay) => { const id = ++intervalId; intervalCallbacks.push({ id, callback, delay }); - return id; + return id as unknown as ReturnType; }); clearIntervalSpy = vi.spyOn(window, "clearInterval").mockImplementation((id) => { @@ -52,7 +53,7 @@ export function getIntervalMockSpies() { export async function triggerIntervals() { const { act } = await import("@testing-library/react"); await act(async () => { - intervalCallbacks.forEach(({ callback }) => { + intervalCallbacks.forEach(({ callback }: { callback: () => void }) => { callback(); }); }); @@ -60,9 +61,9 @@ export async function triggerIntervals() { /** * Helper function to wait for component to load and then trigger intervals - * @param {RegExp|string} expectedText - Text to wait for before triggering intervals + * @param expectedText - Text to wait for before triggering intervals */ -export async function waitForLoadAndTriggerIntervals(expectedText) { +export async function waitForLoadAndTriggerIntervals(expectedText: RegExp | string) { const { waitFor, screen, act } = await import("@testing-library/react"); // Wait for the component to load first From 180acd5ffde8692d2e0262bc54086a12c46c6f9c Mon Sep 17 00:00:00 2001 From: QazCetelic Date: Mon, 6 Oct 2025 19:49:41 +0200 Subject: [PATCH 13/29] refactor(ui): Add types to tests 7 --- .../{CLIEquivalent.test.jsx => CLIEquivalent.test.tsx} | 0 ...Breadcrumbs.test.jsx => DirectoryBreadcrumbs.test.tsx} | 6 +++--- .../{DirectoryItems.test.jsx => DirectoryItems.test.tsx} | 8 ++++---- .../{GoBackButton.test.jsx => GoBackButton.test.tsx} | 2 +- ...icationEditor.test.jsx => NotificationEditor.test.tsx} | 0 5 files changed, 8 insertions(+), 8 deletions(-) rename tests/components/{CLIEquivalent.test.jsx => CLIEquivalent.test.tsx} (100%) rename tests/components/{DirectoryBreadcrumbs.test.jsx => DirectoryBreadcrumbs.test.tsx} (98%) rename tests/components/{DirectoryItems.test.jsx => DirectoryItems.test.tsx} (97%) rename tests/components/{GoBackButton.test.jsx => GoBackButton.test.tsx} (94%) rename tests/components/notifications/{NotificationEditor.test.jsx => NotificationEditor.test.tsx} (100%) diff --git a/tests/components/CLIEquivalent.test.jsx b/tests/components/CLIEquivalent.test.tsx similarity index 100% rename from tests/components/CLIEquivalent.test.jsx rename to tests/components/CLIEquivalent.test.tsx diff --git a/tests/components/DirectoryBreadcrumbs.test.jsx b/tests/components/DirectoryBreadcrumbs.test.tsx similarity index 98% rename from tests/components/DirectoryBreadcrumbs.test.jsx rename to tests/components/DirectoryBreadcrumbs.test.tsx index 6e646b68..4a471d2f 100644 --- a/tests/components/DirectoryBreadcrumbs.test.jsx +++ b/tests/components/DirectoryBreadcrumbs.test.tsx @@ -3,7 +3,7 @@ import { render, screen, fireEvent, act } from "@testing-library/react"; import { describe, it, expect, beforeEach, vi } from "vitest"; import "@testing-library/jest-dom"; import { BrowserRouter } from "react-router-dom"; -import { DirectoryBreadcrumbs } from "../../src/components/DirectoryBreadcrumbs"; +import { DirectoryBreadcrumbs } from "../../src/components/DirectoryBreadcrumbs.js"; import { mockNavigate, resetRouterMocks, updateRouterMocks } from "../testutils/react-router-mock.jsx"; // Mock react-router-dom using unified helper @@ -29,7 +29,7 @@ describe("DirectoryBreadcrumbs", () => { // Should render the Breadcrumb container but no items const breadcrumb = document.querySelector(".breadcrumb"); expect(breadcrumb).toBeInTheDocument(); - expect(breadcrumb.children).toHaveLength(0); + expect(breadcrumb!.children).toHaveLength(0); }); it("renders single breadcrumb item", () => { @@ -160,7 +160,7 @@ describe("DirectoryBreadcrumbs", () => { // Click on the info icon to show tooltip await act(async () => { - fireEvent.click(infoIcon); + fireEvent.click(infoIcon!); }); // Check for tooltip content diff --git a/tests/components/DirectoryItems.test.jsx b/tests/components/DirectoryItems.test.tsx similarity index 97% rename from tests/components/DirectoryItems.test.jsx rename to tests/components/DirectoryItems.test.tsx index b1615908..ded6500d 100644 --- a/tests/components/DirectoryItems.test.jsx +++ b/tests/components/DirectoryItems.test.tsx @@ -3,8 +3,8 @@ import { render, screen } from "@testing-library/react"; import { describe, it, expect, beforeEach, vi } from "vitest"; import "@testing-library/jest-dom"; import { BrowserRouter } from "react-router-dom"; -import { DirectoryItems } from "../../src/components/DirectoryItems"; -import { UIPreferencesContext } from "../../src/contexts/UIPreferencesContext"; +import { DirectoryItems } from "../../src/components/DirectoryItems.js"; +import { UIPreferences, UIPreferencesContext } from "../../src/contexts/UIPreferencesContext.js"; // Mock react-router-dom Link component using unified helper vi.mock("react-router-dom", async () => { @@ -25,7 +25,7 @@ vi.mock("react-router-dom", async () => { // Helper function to render component with necessary providers const renderDirectoryItems = (props, contextOverrides = {}) => { - const defaultContext = { + const defaultContext: UIPreferences = { bytesStringBase2: false, pageSize: 10, theme: "light", @@ -256,7 +256,7 @@ describe("DirectoryItems", () => { const linkElement = dirLink.closest("a"); // Check that the link has the correct state data - const linkState = JSON.parse(linkElement.getAttribute("data-link-state")); + const linkState = JSON.parse(linkElement!.getAttribute("data-link-state")!); expect(linkState).toEqual({ label: "sub-folder", oid: "kdir888", diff --git a/tests/components/GoBackButton.test.jsx b/tests/components/GoBackButton.test.tsx similarity index 94% rename from tests/components/GoBackButton.test.jsx rename to tests/components/GoBackButton.test.tsx index 151724c1..c694bc95 100644 --- a/tests/components/GoBackButton.test.jsx +++ b/tests/components/GoBackButton.test.tsx @@ -3,7 +3,7 @@ import { render, screen, fireEvent } from "@testing-library/react"; import { BrowserRouter } from "react-router-dom"; import { vi } from "vitest"; import "@testing-library/jest-dom"; -import { GoBackButton } from "../../src/components/GoBackButton"; +import { GoBackButton } from "../../src/components/GoBackButton.js"; import { mockNavigate, resetRouterMocks } from "../testutils/react-router-mock.jsx"; // Mock react-router-dom using the unified helper diff --git a/tests/components/notifications/NotificationEditor.test.jsx b/tests/components/notifications/NotificationEditor.test.tsx similarity index 100% rename from tests/components/notifications/NotificationEditor.test.jsx rename to tests/components/notifications/NotificationEditor.test.tsx From b884e3c517611514601a98e30dc6a73b5ff5c8c9 Mon Sep 17 00:00:00 2001 From: QazCetelic Date: Mon, 6 Oct 2025 21:06:44 +0200 Subject: [PATCH 14/29] refactor(ui): Add types to tests 8 --- src/components/Logs.tsx | 10 ++- src/components/policy-editor/PolicyEditor.tsx | 67 ++++++++++++------- ...opiaTable.test.jsx => KopiaTable.test.tsx} | 29 ++++---- .../{Logs.test.jsx => Logs.test.tsx} | 8 +-- ...yEditor.test.jsx => PolicyEditor.test.tsx} | 6 +- 5 files changed, 73 insertions(+), 47 deletions(-) rename tests/components/{KopiaTable.test.jsx => KopiaTable.test.tsx} (96%) rename tests/components/{Logs.test.jsx => Logs.test.tsx} (97%) rename tests/components/{PolicyEditor.test.jsx => PolicyEditor.test.tsx} (92%) diff --git a/src/components/Logs.tsx b/src/components/Logs.tsx index 391b8b5e..8299c36e 100644 --- a/src/components/Logs.tsx +++ b/src/components/Logs.tsx @@ -6,13 +6,17 @@ import { redirect } from "../utils/uiutil"; import PropTypes from "prop-types"; import { ComponentChangeHandling, ChangeEventHandle } from "./types"; -export class Logs extends Component implements ComponentChangeHandling { +interface LogsProps { + taskID: string; +} + +export class Logs extends Component implements ComponentChangeHandling { handleChange: ChangeEventHandle; interval: number; messagesEndRef: React.RefObject; - constructor() { - super(); + constructor(props: LogsProps) { + super(props); this.state = { items: [], isLoading: false, diff --git a/src/components/policy-editor/PolicyEditor.tsx b/src/components/policy-editor/PolicyEditor.tsx index 5ee42ed5..ea15023c 100644 --- a/src/components/policy-editor/PolicyEditor.tsx +++ b/src/components/policy-editor/PolicyEditor.tsx @@ -42,11 +42,42 @@ import { SectionHeaderRow } from "./SectionHeaderRow"; import { ActionRowScript } from "./ActionRowScript"; import { ActionRowTimeout } from "./ActionRowTimeout"; import { ActionRowMode } from "./ActionRowMode"; -import PropTypes from "prop-types"; import { ChangeEventHandle, ComponentChangeHandling } from "../types"; -export class PolicyEditor extends Component implements ComponentChangeHandling { +type PolicyEditorProps = { + path?: string; + close: () => void; + embedded?: boolean; + isNew?: boolean; + params?: object; + navigate?: () => void; + location?: object; + userName?: string; + host?: string; +} & PolicyQueryParams; + +type PolicyEditorState = { + items: any[]; + isLoading: boolean; + error: Error | null; + algorithms?: any; + policy?: any; + resolved?: any; + resolvedError?: Error; + isNew?: boolean; + saving?: boolean; +} + +type Policy = { + files?: any; + compression?: any; + scheduling?: any; + actions?: any; +}; + +export class PolicyEditor extends Component implements ComponentChangeHandling { handleChange: ChangeEventHandle; + lastResolvedPolicy?: any; constructor() { super(); @@ -77,7 +108,7 @@ export class PolicyEditor extends Component implements ComponentChangeHandling { }); } - componentDidUpdate(prevProps) { + componentDidUpdate(prevProps: PolicyEditorProps, _prevState: PolicyEditorState, _snapshot: unknown) { if (sourceQueryStringParams(this.props) !== sourceQueryStringParams(prevProps)) { this.fetchPolicy(this.props); } @@ -89,7 +120,7 @@ export class PolicyEditor extends Component implements ComponentChangeHandling { } } - fetchPolicy(props) { + fetchPolicy(props: PolicyEditorProps) { axios .get(this.policyURL(props)) .then((result) => { @@ -114,7 +145,7 @@ export class PolicyEditor extends Component implements ComponentChangeHandling { }); } - resolvePolicy(props) { + resolvePolicy(props: PolicyQueryParams) { const u = "/api/v1/policy/resolve?" + sourceQueryStringParams(props); try { @@ -152,7 +183,7 @@ export class PolicyEditor extends Component implements ComponentChangeHandling { return l; } - let result = []; + const result = []; for (let i = 0; i < l.length; i++) { const s = l[i]; if (s === "") { @@ -177,7 +208,7 @@ export class PolicyEditor extends Component implements ComponentChangeHandling { } // clone and clean up policy before saving - let policy = JSON.parse(JSON.stringify(this.state.policy)); + const policy: Policy = JSON.parse(JSON.stringify(this.state.policy)); if (policy.files) { if (policy.files.ignore) { policy.files.ignore = removeEmpty(policy.files.ignore); @@ -214,7 +245,7 @@ export class PolicyEditor extends Component implements ComponentChangeHandling { return policy; } - sanitizeActions(actions, actionTypes) { + sanitizeActions(actions, actionTypes: string[]) { actionTypes.forEach((actionType) => { if (actions[actionType]) { if (actions[actionType].script === undefined || actions[actionType].script === "") { @@ -229,7 +260,7 @@ export class PolicyEditor extends Component implements ComponentChangeHandling { return actions; } - saveChanges(e) { + saveChanges(e: React.FormEvent | React.MouseEvent): void { e.preventDefault(); try { @@ -267,8 +298,8 @@ export class PolicyEditor extends Component implements ComponentChangeHandling { } } - policyURL(props) { - return "/api/v1/policy?" + sourceQueryStringParams(props); + policyURL(props: PolicyQueryParams) { + return `/api/v1/policy?${sourceQueryStringParams(props)}`; } isGlobal() { @@ -854,16 +885,4 @@ export class PolicyEditor extends Component implements ComponentChangeHandling { ); } -} - -PolicyEditor.propTypes = { - path: PropTypes.string, - close: PropTypes.func, - embedded: PropTypes.bool, - isNew: PropTypes.bool, - params: PropTypes.object.isRequired, - navigate: PropTypes.func.isRequired, - location: PropTypes.object.isRequired, - userName: PropTypes.string, - host: PropTypes.string, -}; +} \ No newline at end of file diff --git a/tests/components/KopiaTable.test.jsx b/tests/components/KopiaTable.test.tsx similarity index 96% rename from tests/components/KopiaTable.test.jsx rename to tests/components/KopiaTable.test.tsx index adaddc81..7060bfaf 100644 --- a/tests/components/KopiaTable.test.jsx +++ b/tests/components/KopiaTable.test.tsx @@ -5,21 +5,24 @@ import { expect, describe, it, vi, beforeEach } from "vitest"; import "@testing-library/jest-dom"; import KopiaTable from "../../src/components/KopiaTable"; -import { UIPreferencesContext, PAGE_SIZES } from "../../src/contexts/UIPreferencesContext"; +import { UIPreferencesContext, PAGE_SIZES, UIPreferences, PageSize } from "../../src/contexts/UIPreferencesContext"; // Test data and mock setup -const createMockContext = (pageSize = 10) => ({ - pageSize, - setPageSize: vi.fn(), - theme: "light", - bytesStringBase2: false, - defaultSnapshotViewAll: false, - fontSize: "fs-6", - setTheme: vi.fn(), - setByteStringBase: vi.fn(), - setDefaultSnapshotViewAll: vi.fn(), - setFontSize: vi.fn(), -}); +function createMockContext(pageSize: PageSize = 10) { + const pref: UIPreferences = { + pageSize, + setPageSize: vi.fn(), + theme: "light", + bytesStringBase2: false, + defaultSnapshotViewAll: false, + fontSize: "fs-6", + setTheme: vi.fn(), + setByteStringBase: vi.fn(), + setDefaultSnapshotViewAll: vi.fn(), + setFontSize: vi.fn(), + }; + return pref; +} const sampleColumns = [ { diff --git a/tests/components/Logs.test.jsx b/tests/components/Logs.test.tsx similarity index 97% rename from tests/components/Logs.test.jsx rename to tests/components/Logs.test.tsx index b79322e6..a2f8124f 100644 --- a/tests/components/Logs.test.jsx +++ b/tests/components/Logs.test.tsx @@ -101,7 +101,7 @@ describe("Logs Component", () => { // The component formats time as HH:MM:SS.mmm // For timestamp 1672531200.123, depending on timezone const timeElement = screen.getByText(/Test message/).parentElement; - expect(timeElement.textContent).toMatch(/\d{2}:\d{2}:\d{2}\.\d{3}/); + expect(timeElement!.textContent).toMatch(/\d{2}:\d{2}:\d{2}\.\d{3}/); }); }); @@ -207,7 +207,7 @@ describe("Logs Component", () => { }); // Clear the mock calls from initial render - Element.prototype.scrollIntoView.mockClear(); + (Element.prototype.scrollIntoView as jest.Mock).mockClear(); // Trigger the interval callback manually await triggerIntervals(); @@ -241,7 +241,7 @@ describe("Logs Component", () => { const initialCallCount = callCount; // Clear the mock calls from initial render - Element.prototype.scrollIntoView.mockClear(); + (Element.prototype.scrollIntoView as jest.Mock).mockClear(); // Trigger the interval callback manually await triggerIntervals(); @@ -307,7 +307,7 @@ describe("Logs Component", () => { const cell = container.querySelector("td.elide"); expect(cell).toBeTruthy(); - const title = cell.getAttribute("title"); + const title = cell!.getAttribute("title"); expect(title).toBeTruthy(); expect(title).toMatch(/\d{1,2}\/\d{1,2}\/\d{4}/); }); diff --git a/tests/components/PolicyEditor.test.jsx b/tests/components/PolicyEditor.test.tsx similarity index 92% rename from tests/components/PolicyEditor.test.jsx rename to tests/components/PolicyEditor.test.tsx index d477844e..0673a82c 100644 --- a/tests/components/PolicyEditor.test.jsx +++ b/tests/components/PolicyEditor.test.tsx @@ -118,8 +118,8 @@ it("e2e", async () => { , ); - await waitFor(() => expect(getByTestId("effective-retention.keepHourly").value).toBe("45")); - expect(getByTestId("effective-retention.keepLatest").value).toEqual("33"); + await waitFor(() => expect((getByTestId("effective-retention.keepHourly") as HTMLInputElement).value).toBe("45")); + expect((getByTestId("effective-retention.keepLatest") as HTMLInputElement).value).toEqual("33"); await waitFor(() => expect(getByTestId("definition-retention.keepLatest").innerHTML).toContain(`Directory: some-user@h1:some-path`), @@ -139,7 +139,7 @@ it("e2e", async () => { fireEvent.change(getByTestId("control-policy.retention.keepLatest"), { target: { value: "44" } }); // this will trigger resolve and will update effective field: "(Defined by this policy)" - await waitFor(() => expect(getByTestId("effective-retention.keepLatest").value).toBe("44")); + await waitFor(() => expect((getByTestId("effective-retention.keepLatest") as HTMLInputElement).value).toBe("44")); await waitFor(() => expect(getByTestId("definition-retention.keepLatest").innerHTML).toEqual("(Defined by this policy)"), ); From 5d648ddab93d2661a0cdabb32523ad5e42dab508 Mon Sep 17 00:00:00 2001 From: QazCetelic Date: Tue, 7 Oct 2025 18:56:38 +0200 Subject: [PATCH 15/29] refactor(ui): Add types to tests 9 --- src/components/PolicyEditorLink.tsx | 4 +-- src/components/policy-editor/PolicyEditor.tsx | 10 +++--- src/utils/policyutil.tsx | 20 ++++++----- ...ink.test.jsx => PolicyEditorLink.test.tsx} | 5 +-- ...tory.test.jsx => SetupRepository.test.tsx} | 0 ...test.jsx => SetupRepositoryAzure.test.tsx} | 10 +++--- ...B2.test.jsx => SetupRepositoryB2.test.tsx} | 10 +++--- ...jsx => SetupRepositoryFilesystem.test.tsx} | 8 ++--- ...S.test.jsx => SetupRepositoryGCS.test.tsx} | 10 +++--- ...est.jsx => SetupRepositoryRclone.test.tsx} | 10 +++--- ...S3.test.jsx => SetupRepositoryS3.test.tsx} | 14 ++++---- tests/utils/policyutil.test.ts | 34 +++++++++---------- 12 files changed, 69 insertions(+), 66 deletions(-) rename tests/components/{PolicyEditorLink.test.jsx => PolicyEditorLink.test.tsx} (96%) rename tests/components/{SetupRepository.test.jsx => SetupRepository.test.tsx} (100%) rename tests/components/{SetupRepositoryAzure.test.jsx => SetupRepositoryAzure.test.tsx} (82%) rename tests/components/{SetupRepositoryB2.test.jsx => SetupRepositoryB2.test.tsx} (76%) rename tests/components/{SetupRepositoryFilesystem.test.jsx => SetupRepositoryFilesystem.test.tsx} (69%) rename tests/components/{SetupRepositoryGCS.test.jsx => SetupRepositoryGCS.test.tsx} (77%) rename tests/components/{SetupRepositoryRclone.test.jsx => SetupRepositoryRclone.test.tsx} (71%) rename tests/components/{SetupRepositoryS3.test.jsx => SetupRepositoryS3.test.tsx} (80%) diff --git a/src/components/PolicyEditorLink.tsx b/src/components/PolicyEditorLink.tsx index 08451d77..6d5bd129 100644 --- a/src/components/PolicyEditorLink.tsx +++ b/src/components/PolicyEditorLink.tsx @@ -1,7 +1,7 @@ import React from "react"; import { Link } from "react-router-dom"; -import { PolicyQueryParams, PolicyTypeName, policyEditorURL } from "../utils/policyutil"; +import { PolicyKey, PolicyTypeName, policyEditorURL } from "../utils/policyutil"; -export function PolicyEditorLink(s: PolicyQueryParams) { +export function PolicyEditorLink(s: PolicyKey) { return {PolicyTypeName(s)}; } diff --git a/src/components/policy-editor/PolicyEditor.tsx b/src/components/policy-editor/PolicyEditor.tsx index ea15023c..3a625509 100644 --- a/src/components/policy-editor/PolicyEditor.tsx +++ b/src/components/policy-editor/PolicyEditor.tsx @@ -26,7 +26,7 @@ import { OptionalNumberField } from "../../forms/OptionalNumberField"; import { RequiredBoolean } from "../../forms/RequiredBoolean"; import { TimesOfDayList } from "../../forms/TimesOfDayList"; import { errorAlert, toAlgorithmOption } from "../../utils/uiutil"; -import { PolicyQueryParams, sourceQueryStringParams } from "../../utils/policyutil"; +import { PolicyKey, sourceQueryStringParams } from "../../utils/policyutil"; import { PolicyEditorLink } from "../PolicyEditorLink"; import { LabelColumn } from "./LabelColumn"; import { ValueColumn } from "./ValueColumn"; @@ -54,7 +54,7 @@ type PolicyEditorProps = { location?: object; userName?: string; host?: string; -} & PolicyQueryParams; +} & PolicyKey; type PolicyEditorState = { items: any[]; @@ -145,7 +145,7 @@ export class PolicyEditor extends Component) { +export function PolicyTypeName(s: PolicyKey) { if (!s.host && !s.userName) { return "Global Policy"; } @@ -68,16 +68,20 @@ export function PolicyTypeName(s: Partial) { return "Directory: " + s.userName + "@" + s.host + ":" + s.path; } -export interface PolicyQueryParams { - userName: string; - host: string; - path: string; +export interface PolicyKey { + userName?: string; + host?: string; + path?: string; }; -export function sourceQueryStringParams(src: PolicyQueryParams) { - return `userName=${encodeURIComponent(src.userName)}&host=${encodeURIComponent(src.host)}&path=${encodeURIComponent(src.path)}`; +export function sourceQueryStringParams(src: PolicyKey) { + // encodeURIComponent will in practice handle missing values too, but that is undefined behavior + const user = src.userName ? encodeURIComponent(src.userName) : ""; + const host = src.host ? encodeURIComponent(src.host) : ""; + const path = src.path ? encodeURIComponent(src.path) : ""; + return `userName=${user}&host=${host}&path=${path}`; } -export function policyEditorURL(s: PolicyQueryParams) { +export function policyEditorURL(s: PolicyKey) { return `/policies/edit?${sourceQueryStringParams(s)}`; } diff --git a/tests/components/PolicyEditorLink.test.jsx b/tests/components/PolicyEditorLink.test.tsx similarity index 96% rename from tests/components/PolicyEditorLink.test.jsx rename to tests/components/PolicyEditorLink.test.tsx index 449c1382..3b3566c0 100644 --- a/tests/components/PolicyEditorLink.test.jsx +++ b/tests/components/PolicyEditorLink.test.tsx @@ -10,7 +10,8 @@ vi.mock("react-router-dom", async () => { return createRouterMock()(); }); -import { PolicyEditorLink } from "../../src/components/PolicyEditorLink"; +import { PolicyEditorLink } from "../../src/components/PolicyEditorLink.js"; +import { PolicyKey } from "../../src/utils/policyutil.js"; // Helper to render components with router const renderWithRouter = (component) => { @@ -19,7 +20,7 @@ const renderWithRouter = (component) => { describe("PolicyEditorLink", () => { it("renders link with correct URL and text", () => { - const source = { + const source: PolicyKey = { userName: "john", host: "example.com", path: "/home/john", diff --git a/tests/components/SetupRepository.test.jsx b/tests/components/SetupRepository.test.tsx similarity index 100% rename from tests/components/SetupRepository.test.jsx rename to tests/components/SetupRepository.test.tsx diff --git a/tests/components/SetupRepositoryAzure.test.jsx b/tests/components/SetupRepositoryAzure.test.tsx similarity index 82% rename from tests/components/SetupRepositoryAzure.test.jsx rename to tests/components/SetupRepositoryAzure.test.tsx index 2da8797c..ca046793 100644 --- a/tests/components/SetupRepositoryAzure.test.jsx +++ b/tests/components/SetupRepositoryAzure.test.tsx @@ -4,22 +4,22 @@ import { SetupRepositoryAzure } from "../../src/components/SetupRepositoryAzure" import { fireEvent } from "@testing-library/react"; it("can set fields", async () => { - let ref = React.createRef(); + const ref = React.createRef(); const { getByTestId } = render(); - act(() => expect(ref.current.validate()).toBe(false)); + act(() => expect(ref.current!.validate()).toBe(false)); // required fireEvent.change(getByTestId("control-container"), { target: { value: "some-container" } }); fireEvent.change(getByTestId("control-storageAccount"), { target: { value: "some-storageAccount" } }); - expect(ref.current.validate()).toBe(true); + expect(ref.current!.validate()).toBe(true); // optional fireEvent.change(getByTestId("control-storageKey"), { target: { value: "some-storageKey" } }); fireEvent.change(getByTestId("control-sasToken"), { target: { value: "some-sas-token" } }); fireEvent.change(getByTestId("control-storageDomain"), { target: { value: "some-storage-domain" } }); fireEvent.change(getByTestId("control-prefix"), { target: { value: "some-prefix" } }); - expect(ref.current.validate()).toBe(true); + expect(ref.current!.validate()).toBe(true); - expect(ref.current.state).toStrictEqual({ + expect(ref.current!.state).toStrictEqual({ storageAccount: "some-storageAccount", container: "some-container", prefix: "some-prefix", diff --git a/tests/components/SetupRepositoryB2.test.jsx b/tests/components/SetupRepositoryB2.test.tsx similarity index 76% rename from tests/components/SetupRepositoryB2.test.jsx rename to tests/components/SetupRepositoryB2.test.tsx index a3a21445..5383b649 100644 --- a/tests/components/SetupRepositoryB2.test.jsx +++ b/tests/components/SetupRepositoryB2.test.tsx @@ -4,20 +4,20 @@ import { SetupRepositoryB2 } from "../../src/components/SetupRepositoryB2"; import { fireEvent } from "@testing-library/react"; it("can set fields", async () => { - let ref = React.createRef(); + const ref = React.createRef(); const { getByTestId } = render(); - act(() => expect(ref.current.validate()).toBe(false)); + act(() => expect(ref.current!.validate()).toBe(false)); // required fireEvent.change(getByTestId("control-bucket"), { target: { value: "some-bucket" } }); fireEvent.change(getByTestId("control-keyId"), { target: { value: "some-key-id" } }); fireEvent.change(getByTestId("control-key"), { target: { value: "some-key" } }); - expect(ref.current.validate()).toBe(true); + expect(ref.current!.validate()).toBe(true); // optional fireEvent.change(getByTestId("control-prefix"), { target: { value: "some-prefix" } }); - expect(ref.current.validate()).toBe(true); + expect(ref.current!.validate()).toBe(true); - expect(ref.current.state).toStrictEqual({ + expect(ref.current!.state).toStrictEqual({ bucket: "some-bucket", keyId: "some-key-id", key: "some-key", diff --git a/tests/components/SetupRepositoryFilesystem.test.jsx b/tests/components/SetupRepositoryFilesystem.test.tsx similarity index 69% rename from tests/components/SetupRepositoryFilesystem.test.jsx rename to tests/components/SetupRepositoryFilesystem.test.tsx index d051e506..f3dfd3c9 100644 --- a/tests/components/SetupRepositoryFilesystem.test.jsx +++ b/tests/components/SetupRepositoryFilesystem.test.tsx @@ -4,15 +4,15 @@ import { SetupRepositoryFilesystem } from "../../src/components/SetupRepositoryF import { fireEvent } from "@testing-library/react"; it("can set fields", async () => { - let ref = React.createRef(); + const ref = React.createRef(); const { getByTestId } = render(); - act(() => expect(ref.current.validate()).toBe(false)); + act(() => expect(ref.current!.validate()).toBe(false)); // required fireEvent.change(getByTestId("control-path"), { target: { value: "some-path" } }); - expect(ref.current.validate()).toBe(true); + expect(ref.current!.validate()).toBe(true); - expect(ref.current.state).toStrictEqual({ + expect(ref.current!.state).toStrictEqual({ path: "some-path", }); }); diff --git a/tests/components/SetupRepositoryGCS.test.jsx b/tests/components/SetupRepositoryGCS.test.tsx similarity index 77% rename from tests/components/SetupRepositoryGCS.test.jsx rename to tests/components/SetupRepositoryGCS.test.tsx index 19423e26..57f166ed 100644 --- a/tests/components/SetupRepositoryGCS.test.jsx +++ b/tests/components/SetupRepositoryGCS.test.tsx @@ -4,20 +4,20 @@ import { SetupRepositoryGCS } from "../../src/components/SetupRepositoryGCS"; import { fireEvent } from "@testing-library/react"; it("can set fields", async () => { - let ref = React.createRef(); + const ref = React.createRef(); const { getByTestId } = render(); - act(() => expect(ref.current.validate()).toBe(false)); + act(() => expect(ref.current!.validate()).toBe(false)); // required fireEvent.change(getByTestId("control-bucket"), { target: { value: "some-bucket" } }); - expect(ref.current.validate()).toBe(true); + expect(ref.current!.validate()).toBe(true); // optional fireEvent.change(getByTestId("control-prefix"), { target: { value: "some-prefix" } }); fireEvent.change(getByTestId("control-credentialsFile"), { target: { value: "some-credentials-file" } }); fireEvent.change(getByTestId("control-credentials"), { target: { value: "some-credentials" } }); - expect(ref.current.validate()).toBe(true); + expect(ref.current!.validate()).toBe(true); - expect(ref.current.state).toStrictEqual({ + expect(ref.current!.state).toStrictEqual({ bucket: "some-bucket", credentials: "some-credentials", credentialsFile: "some-credentials-file", diff --git a/tests/components/SetupRepositoryRclone.test.jsx b/tests/components/SetupRepositoryRclone.test.tsx similarity index 71% rename from tests/components/SetupRepositoryRclone.test.jsx rename to tests/components/SetupRepositoryRclone.test.tsx index 16222b3f..b85caea4 100644 --- a/tests/components/SetupRepositoryRclone.test.jsx +++ b/tests/components/SetupRepositoryRclone.test.tsx @@ -4,18 +4,18 @@ import { SetupRepositoryRclone } from "../../src/components/SetupRepositoryRclon import { fireEvent } from "@testing-library/react"; it("can set fields", async () => { - let ref = React.createRef(); + const ref = React.createRef(); const { getByTestId } = render(); - act(() => expect(ref.current.validate()).toBe(false)); + act(() => expect(ref.current!.validate()).toBe(false)); // required fireEvent.change(getByTestId("control-remotePath"), { target: { value: "myremote:path/to/repo" } }); - expect(ref.current.validate()).toBe(true); + expect(ref.current!.validate()).toBe(true); // optional fireEvent.change(getByTestId("control-rcloneExe"), { target: { value: "/usr/bin/rclone" } }); - expect(ref.current.validate()).toBe(true); + expect(ref.current!.validate()).toBe(true); - expect(ref.current.state).toStrictEqual({ + expect(ref.current!.state).toStrictEqual({ remotePath: "myremote:path/to/repo", rcloneExe: "/usr/bin/rclone", }); diff --git a/tests/components/SetupRepositoryS3.test.jsx b/tests/components/SetupRepositoryS3.test.tsx similarity index 80% rename from tests/components/SetupRepositoryS3.test.jsx rename to tests/components/SetupRepositoryS3.test.tsx index 3d7094d9..7a97f9b8 100644 --- a/tests/components/SetupRepositoryS3.test.jsx +++ b/tests/components/SetupRepositoryS3.test.tsx @@ -4,25 +4,25 @@ import { SetupRepositoryS3 } from "../../src/components/SetupRepositoryS3"; import { fireEvent } from "@testing-library/react"; it("can set fields", async () => { - let ref = React.createRef(); + const ref = React.createRef(); const { getByTestId } = render(); - act(() => expect(ref.current.validate()).toBe(false)); + act(() => expect(ref.current!.validate()).toBe(false)); // required fireEvent.change(getByTestId("control-bucket"), { target: { value: "some-bucket" } }); fireEvent.change(getByTestId("control-accessKeyID"), { target: { value: "some-accessKeyID" } }); fireEvent.change(getByTestId("control-secretAccessKey"), { target: { value: "some-secretAccessKey" } }); fireEvent.change(getByTestId("control-endpoint"), { target: { value: "some-endpoint" } }); - act(() => expect(ref.current.validate()).toBe(true)); + act(() => expect(ref.current!.validate()).toBe(true)); // optional fireEvent.click(getByTestId("control-doNotUseTLS")); fireEvent.click(getByTestId("control-doNotVerifyTLS")); fireEvent.change(getByTestId("control-prefix"), { target: { value: "some-prefix" } }); fireEvent.change(getByTestId("control-sessionToken"), { target: { value: "some-sessionToken" } }); fireEvent.change(getByTestId("control-region"), { target: { value: "some-region" } }); - act(() => expect(ref.current.validate()).toBe(true)); + act(() => expect(ref.current!.validate()).toBe(true)); - expect(ref.current.state).toStrictEqual({ + expect(ref.current!.state).toStrictEqual({ accessKeyID: "some-accessKeyID", bucket: "some-bucket", endpoint: "some-endpoint", @@ -36,6 +36,6 @@ it("can set fields", async () => { fireEvent.click(getByTestId("control-doNotUseTLS")); fireEvent.click(getByTestId("control-doNotVerifyTLS")); - expect(ref.current.state.doNotUseTLS).toBe(false); - expect(ref.current.state.doNotVerifyTLS).toBe(false); + expect(ref.current!.state.doNotUseTLS).toBe(false); + expect(ref.current!.state.doNotVerifyTLS).toBe(false); }); diff --git a/tests/utils/policyutil.test.ts b/tests/utils/policyutil.test.ts index 92d3defc..a9b12233 100644 --- a/tests/utils/policyutil.test.ts +++ b/tests/utils/policyutil.test.ts @@ -104,18 +104,17 @@ describe("sourceQueryStringParams", () => { expect(result).toContain("path=%2Fpath%20with%20spaces"); }); - // This shouldn't be possible with the current type definition and the expected fallback behavior is pointless since getSnapshotSourceFromURL in the backend doesn't support it. - // it("handles undefined or null values", () => { - // const source = { - // userName: undefined, - // host: "examplehost", - // path: null, - // }; - // const result = sourceQueryStringParams(source); - // expect(result).toContain("userName=undefined"); - // expect(result).toContain("host=examplehost"); - // expect(result).toContain("path=null"); - // }); + it("handles undefined values", () => { + const source = { + userName: undefined, + host: "examplehost", + path: undefined, + }; + const result = sourceQueryStringParams(source); + expect(result).toContain("userName="); + expect(result).toContain("host=examplehost"); + expect(result).toContain("path="); + }); it("handles empty values", () => { const source = { @@ -196,12 +195,11 @@ describe("policyEditorURL", () => { expect(url).toContain("path=%2Fpath%20with%20spaces"); }); - // This shouldn't be possible with the current type definition and the expected fallback behavior is pointless since getSnapshotSourceFromURL in the backend doesn't support it. - // it("handles empty source", () => { - // const source = {}; - // const url = policyEditorURL(source); - // expect(url).toBe("/policies/edit?userName=undefined&host=undefined&path=undefined"); - // }); + it("handles empty source", () => { + const source = {}; + const url = policyEditorURL(source); + expect(url).toBe("/policies/edit?userName=&host=&path="); + }); it("generates URL for global policy", () => { const source = { userName: "", host: "", path: "" }; From ed11c2e61ff20e537d9898fc5ec51876c10d261d Mon Sep 17 00:00:00 2001 From: QazCetelic Date: Tue, 7 Oct 2025 19:08:02 +0200 Subject: [PATCH 16/29] refactor(ui): Add types to tests 10 --- ....test.jsx => SetupRepositorySFTP.test.tsx} | 24 +++++----- ...est.jsx => SetupRepositoryServer.test.tsx} | 10 ++-- ...test.jsx => SetupRepositoryToken.test.tsx} | 8 ++-- ...est.jsx => SetupRepositoryWebDAV.test.tsx} | 10 ++-- ...n.test.jsx => SnapshotEstimation.test.tsx} | 47 +++++++++---------- 5 files changed, 49 insertions(+), 50 deletions(-) rename tests/components/{SetupRepositorySFTP.test.jsx => SetupRepositorySFTP.test.tsx} (77%) rename tests/components/{SetupRepositoryServer.test.jsx => SetupRepositoryServer.test.tsx} (73%) rename tests/components/{SetupRepositoryToken.test.jsx => SetupRepositoryToken.test.tsx} (69%) rename tests/components/{SetupRepositoryWebDAV.test.jsx => SetupRepositoryWebDAV.test.tsx} (74%) rename tests/components/{SnapshotEstimation.test.jsx => SnapshotEstimation.test.tsx} (95%) diff --git a/tests/components/SetupRepositorySFTP.test.jsx b/tests/components/SetupRepositorySFTP.test.tsx similarity index 77% rename from tests/components/SetupRepositorySFTP.test.jsx rename to tests/components/SetupRepositorySFTP.test.tsx index e28ba7a5..abe981fb 100644 --- a/tests/components/SetupRepositorySFTP.test.jsx +++ b/tests/components/SetupRepositorySFTP.test.tsx @@ -4,10 +4,10 @@ import { SetupRepositorySFTP } from "../../src/components/SetupRepositorySFTP"; import { fireEvent } from "@testing-library/react"; it("can set fields", async () => { - let ref = React.createRef(); + const ref = React.createRef(); const { getByTestId } = render(); - act(() => expect(ref.current.validate()).toBe(false)); + act(() => expect(ref.current!.validate()).toBe(false)); // required fireEvent.change(getByTestId("control-host"), { target: { value: "some-host" } }); fireEvent.change(getByTestId("control-port"), { target: { value: "22" } }); @@ -15,10 +15,10 @@ it("can set fields", async () => { fireEvent.change(getByTestId("control-username"), { target: { value: "some-username" } }); fireEvent.change(getByTestId("control-keyfile"), { target: { value: "some-keyfile" } }); fireEvent.change(getByTestId("control-knownHostsFile"), { target: { value: "some-knownHostsFile" } }); - act(() => expect(ref.current.validate()).toBe(true)); + act(() => expect(ref.current!.validate()).toBe(true)); // key file + known hosts file - expect(ref.current.state).toStrictEqual({ + expect(ref.current!.state).toStrictEqual({ host: "some-host", username: "some-username", keyfile: "some-keyfile", @@ -30,12 +30,12 @@ it("can set fields", async () => { // now enter key data instead of key file, make sure validation triggers along the way fireEvent.change(getByTestId("control-keyData"), { target: { value: "some-keyData" } }); - act(() => expect(ref.current.validate()).toBe(false)); + act(() => expect(ref.current!.validate()).toBe(false)); fireEvent.change(getByTestId("control-keyfile"), { target: { value: "" } }); - act(() => expect(ref.current.validate()).toBe(true)); + act(() => expect(ref.current!.validate()).toBe(true)); // key data + known hosts file - expect(ref.current.state).toStrictEqual({ + expect(ref.current!.state).toStrictEqual({ host: "some-host", username: "some-username", keyfile: "", @@ -47,17 +47,17 @@ it("can set fields", async () => { }); fireEvent.change(getByTestId("control-password"), { target: { value: "some-password" } }); - act(() => expect(ref.current.validate()).toBe(false)); + act(() => expect(ref.current!.validate()).toBe(false)); fireEvent.change(getByTestId("control-keyData"), { target: { value: "" } }); - act(() => expect(ref.current.validate()).toBe(true)); + act(() => expect(ref.current!.validate()).toBe(true)); fireEvent.change(getByTestId("control-knownHostsData"), { target: { value: "some-knownHostsData" } }); - act(() => expect(ref.current.validate()).toBe(false)); + act(() => expect(ref.current!.validate()).toBe(false)); fireEvent.change(getByTestId("control-knownHostsFile"), { target: { value: "" } }); - act(() => expect(ref.current.validate()).toBe(true)); + act(() => expect(ref.current!.validate()).toBe(true)); // known hosts data + password - expect(ref.current.state).toStrictEqual({ + expect(ref.current!.state).toStrictEqual({ host: "some-host", username: "some-username", password: "some-password", diff --git a/tests/components/SetupRepositoryServer.test.jsx b/tests/components/SetupRepositoryServer.test.tsx similarity index 73% rename from tests/components/SetupRepositoryServer.test.jsx rename to tests/components/SetupRepositoryServer.test.tsx index 440a50de..eaded96d 100644 --- a/tests/components/SetupRepositoryServer.test.jsx +++ b/tests/components/SetupRepositoryServer.test.tsx @@ -4,18 +4,18 @@ import { SetupRepositoryServer } from "../../src/components/SetupRepositoryServe import { fireEvent } from "@testing-library/react"; it("can set fields", async () => { - let ref = React.createRef(); + const ref = React.createRef(); const { getByTestId } = render(); - act(() => expect(ref.current.validate()).toBe(false)); + act(() => expect(ref.current!.validate()).toBe(false)); // required fireEvent.change(getByTestId("control-url"), { target: { value: "https://kopia.example.com:51515" } }); - expect(ref.current.validate()).toBe(true); + expect(ref.current!.validate()).toBe(true); // optional fireEvent.change(getByTestId("control-serverCertFingerprint"), { target: { value: "sha256:abcd1234567890" } }); - expect(ref.current.validate()).toBe(true); + expect(ref.current!.validate()).toBe(true); - expect(ref.current.state).toStrictEqual({ + expect(ref.current!.state).toStrictEqual({ url: "https://kopia.example.com:51515", serverCertFingerprint: "sha256:abcd1234567890", }); diff --git a/tests/components/SetupRepositoryToken.test.jsx b/tests/components/SetupRepositoryToken.test.tsx similarity index 69% rename from tests/components/SetupRepositoryToken.test.jsx rename to tests/components/SetupRepositoryToken.test.tsx index dacb1723..5b56cb6d 100644 --- a/tests/components/SetupRepositoryToken.test.jsx +++ b/tests/components/SetupRepositoryToken.test.tsx @@ -4,15 +4,15 @@ import { SetupRepositoryToken } from "../../src/components/SetupRepositoryToken" import { fireEvent } from "@testing-library/react"; it("can set fields", async () => { - let ref = React.createRef(); + const ref = React.createRef(); const { getByTestId } = render(); - act(() => expect(ref.current.validate()).toBe(false)); + act(() => expect(ref.current!.validate()).toBe(false)); // required fireEvent.change(getByTestId("control-token"), { target: { value: "some-token" } }); - expect(ref.current.validate()).toBe(true); + expect(ref.current!.validate()).toBe(true); - expect(ref.current.state).toStrictEqual({ + expect(ref.current!.state).toStrictEqual({ token: "some-token", }); }); diff --git a/tests/components/SetupRepositoryWebDAV.test.jsx b/tests/components/SetupRepositoryWebDAV.test.tsx similarity index 74% rename from tests/components/SetupRepositoryWebDAV.test.jsx rename to tests/components/SetupRepositoryWebDAV.test.tsx index 30b91d47..c6a474b1 100644 --- a/tests/components/SetupRepositoryWebDAV.test.jsx +++ b/tests/components/SetupRepositoryWebDAV.test.tsx @@ -4,21 +4,21 @@ import { SetupRepositoryWebDAV } from "../../src/components/SetupRepositoryWebDA import { fireEvent } from "@testing-library/react"; it("can set fields", async () => { - let ref = React.createRef(); + const ref = React.createRef(); const { getByTestId } = render(); - act(() => expect(ref.current.validate()).toBe(false)); + act(() => expect(ref.current!.validate()).toBe(false)); // required fireEvent.change(getByTestId("control-url"), { target: { value: "some-url" } }); - expect(ref.current.validate()).toBe(true); + expect(ref.current!.validate()).toBe(true); // optional fireEvent.change(getByTestId("control-username"), { target: { value: "some-username" } }); fireEvent.change(getByTestId("control-password"), { target: { value: "some-password" } }); - expect(ref.current.validate()).toBe(true); + expect(ref.current!.validate()).toBe(true); - expect(ref.current.state).toStrictEqual({ + expect(ref.current!.state).toStrictEqual({ url: "some-url", username: "some-username", password: "some-password", diff --git a/tests/components/SnapshotEstimation.test.jsx b/tests/components/SnapshotEstimation.test.tsx similarity index 95% rename from tests/components/SnapshotEstimation.test.jsx rename to tests/components/SnapshotEstimation.test.tsx index cd6e257d..b46f2f8b 100644 --- a/tests/components/SnapshotEstimation.test.jsx +++ b/tests/components/SnapshotEstimation.test.tsx @@ -3,13 +3,13 @@ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; import { render, screen, waitFor } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { MemoryRouter } from "react-router-dom"; -import { SnapshotEstimation } from "../../src/components/SnapshotEstimation"; -import { UIPreferencesContext } from "../../src/contexts/UIPreferencesContext"; -import { setupAPIMock } from "../testutils/api-mocks"; +import { SnapshotEstimation } from "../../src/components/SnapshotEstimation.js"; +import { UIPreferences, UIPreferencesContext } from "../../src/contexts/UIPreferencesContext.js"; +import { setupAPIMock } from "../testutils/api-mocks.js"; import "@testing-library/jest-dom"; import { resetRouterMocks, updateRouterMocks } from "../testutils/react-router-mock.jsx"; import PropTypes from "prop-types"; -import { setupIntervalMocks, cleanupIntervalMocks, waitForLoadAndTriggerIntervals } from "../testutils/interval-mocks"; +import { setupIntervalMocks, cleanupIntervalMocks, waitForLoadAndTriggerIntervals } from "../testutils/interval-mocks.js"; // Mock Logs component to avoid complex dependencies vi.mock("../../src/components/Logs", () => ({ @@ -49,18 +49,20 @@ vi.mock("../../src/utils/taskutil", async () => { }); // Create mock UI preferences context -const createMockUIContext = () => ({ - pageSize: 10, - bytesStringBase2: false, - defaultSnapshotViewAll: false, - theme: "light", - fontSize: "fs-6", - setTheme: vi.fn(), - setPageSize: vi.fn(), - setByteStringBase: vi.fn(), - setDefaultSnapshotViewAll: vi.fn(), - setFontSize: vi.fn(), -}); +function createMockUIContext(): UIPreferences { + return { + pageSize: 10, + bytesStringBase2: false, + defaultSnapshotViewAll: false, + theme: "light", + fontSize: "fs-6", + setTheme: vi.fn(), + setPageSize: vi.fn(), + setByteStringBase: vi.fn(), + setDefaultSnapshotViewAll: vi.fn(), + setFontSize: vi.fn(), + }; +} // Mock server let serverMock; @@ -266,8 +268,7 @@ describe("SnapshotEstimation", () => { serverMock.onGet("/api/v1/tasks/test-task-id").reply(200, task); - const uiContext = createMockUIContext(); - uiContext.bytesStringBase2 = false; + const uiContext = { ...createMockUIContext(), bytesStringBase2: false }; renderWithProviders(, uiContext); @@ -298,8 +299,7 @@ describe("SnapshotEstimation", () => { serverMock.onGet("/api/v1/tasks/test-task-id").reply(200, task); - const uiContext = createMockUIContext(); - uiContext.bytesStringBase2 = true; + const uiContext = { ...createMockUIContext(), bytesStringBase2: true }; renderWithProviders(, uiContext); @@ -368,7 +368,7 @@ describe("SnapshotEstimation", () => { describe("Cancel task functionality", () => { it("calls cancelTask when cancel button is clicked", async () => { - const { cancelTask } = await import("../../src/utils/taskutil"); + const { cancelTask } = await import("../../src/utils/taskutil.js"); const runningTask = { id: "test-task-id", @@ -592,7 +592,7 @@ describe("SnapshotEstimation", () => { describe("Error handling edge cases", () => { it("handles redirect on API error", async () => { - const { redirect } = await import("../../src/utils/uiutil"); + const { redirect } = await import("../../src/utils/uiutil.js"); serverMock.onGet("/api/v1/tasks/test-task-id").reply(401, { code: "NOT_CONNECTED", @@ -650,8 +650,7 @@ describe("SnapshotEstimation", () => { serverMock.onGet("/api/v1/tasks/test-task-id").reply(200, task); - const uiContext = createMockUIContext(); - uiContext.bytesStringBase2 = false; // Start with base 10 + const uiContext = { ...createMockUIContext(), bytesStringBase2: false }; const { rerender } = renderWithProviders(, uiContext); From 616281be602f7bac4031ed17814228ed6728115a Mon Sep 17 00:00:00 2001 From: QazCetelic Date: Tue, 7 Oct 2025 19:44:36 +0200 Subject: [PATCH 17/29] refactor(ui): Add types to tests 11 --- .../forms/{forms.test.jsx => forms.test.tsx} | 110 +++++++++--------- 1 file changed, 58 insertions(+), 52 deletions(-) rename tests/forms/{forms.test.jsx => forms.test.tsx} (90%) diff --git a/tests/forms/forms.test.jsx b/tests/forms/forms.test.tsx similarity index 90% rename from tests/forms/forms.test.jsx rename to tests/forms/forms.test.tsx index 57bda1d5..e3a3d276 100644 --- a/tests/forms/forms.test.jsx +++ b/tests/forms/forms.test.tsx @@ -1,25 +1,27 @@ -import { render, fireEvent } from "@testing-library/react"; +import { OptionalDirectory } from "../../src/forms/OptionalDirectory"; +import { OptionalBoolean } from "../../src/forms/OptionalBoolean"; +import { LogDetailSelector } from "../../src/forms/LogDetailSelector"; +import { RequiredBoolean } from "../../src/forms/RequiredBoolean"; +import { isInvalidNumber, stateProperty, validateRequiredFields, valueToNumber } from "../../src/forms"; +import { listToMultilineString, multilineStringToList, StringList } from "../../src/forms/StringList"; +import { fireEvent, render } from "@testing-library/react"; import React from "react"; -import { vi } from "vitest"; -import PropTypes from "prop-types"; -import "@testing-library/jest-dom"; -import { validateRequiredFields, handleChange, stateProperty, valueToNumber, isInvalidNumber } from "../../src/forms"; -import { StringList, listToMultilineString, multilineStringToList } from "../../src/forms/StringList"; -import { OptionalFieldNoLabel } from "../../src/forms/OptionalFieldNoLabel"; import { RequiredField } from "../../src/forms/RequiredField"; import { OptionalField } from "../../src/forms/OptionalField"; -import { TimesOfDayList } from "../../src/forms/TimesOfDayList"; +import { OptionalFieldNoLabel } from "../../src/forms/OptionalFieldNoLabel"; import { RequiredNumberField } from "../../src/forms/RequiredNumberField"; -import { RequiredDirectory } from "../../src/forms/RequiredDirectory"; import { OptionalNumberField } from "../../src/forms/OptionalNumberField"; -import { OptionalDirectory } from "../../src/forms/OptionalDirectory"; -import { OptionalBoolean } from "../../src/forms/OptionalBoolean"; -import { LogDetailSelector } from "../../src/forms/LogDetailSelector"; -import { RequiredBoolean } from "../../src/forms/RequiredBoolean"; +import { TimesOfDayList } from "../../src/forms/TimesOfDayList"; +import { RequiredDirectory } from "../../src/forms/RequiredDirectory"; +import { vi } from "vitest"; +import PropTypes from "prop-types"; +import { handleChange } from "../../src/forms"; // Mock component class to simulate React component behavior -class MockComponent extends React.Component { - state = {}; // Define state as class property +export class MockComponent extends React.Component { + state: { [key: string]: any } = {}; // Define state as class property + _isTestComponent: boolean; + handleChange: unknown; constructor(props) { super(props); @@ -46,7 +48,7 @@ class MockComponent extends React.Component { } // Test form component that uses all field types -function TestFormComponent({ component }) { +export function TestFormComponent({ component }) { return (
{RequiredField(component, "Required Text", "requiredText")} @@ -73,7 +75,7 @@ describe("Forms Utility Functions", () => { let component; beforeEach(() => { - component = new MockComponent(); + component = new MockComponent({}); }); describe("validateRequiredFields", () => { @@ -170,22 +172,26 @@ describe("StringList Component", () => { describe("multilineStringToList", () => { it("should convert multiline string to array", () => { - expect(multilineStringToList({ value: "line1\nline2\nline3" })).toEqual(["line1", "line2", "line3"]); + expect(multilineStringToList({ value: "line1\nline2\nline3" } as HTMLTextAreaElement)).toEqual([ + "line1", + "line2", + "line3", + ]); }); it("should return undefined for empty string", () => { - expect(multilineStringToList({ value: "" })).toBeUndefined(); + expect(multilineStringToList({ value: "" } as HTMLTextAreaElement)).toBeUndefined(); }); }); it("should render and handle changes", () => { - const component = new MockComponent(); + const component = new MockComponent({}); component.setTestState({ stringList: ["item1", "item2"] }); const { container } = render(
{StringList(component, "stringList")}
); - const textarea = container.querySelector('textarea[name="stringList"]'); - expect(textarea.value).toBe("item1\nitem2"); + const textarea = container.querySelector('textarea[name="stringList"]')!; + expect((textarea as HTMLTextAreaElement)!.value).toBe("item1\nitem2"); fireEvent.change(textarea, { target: { value: "new1\nnew2\nnew3" } }); expect(component.state.stringList).toEqual(["new1", "new2", "new3"]); @@ -196,7 +202,7 @@ describe("Form Field Components", () => { let component; beforeEach(() => { - component = new MockComponent(); + component = new MockComponent({}); }); describe("RequiredField", () => { @@ -204,10 +210,10 @@ describe("Form Field Components", () => { component.setTestState({ test: "" }); const { container } = render(
{RequiredField(component, "Test Label", "test")}
); - const input = container.querySelector('input[name="test"]'); + const input = container.querySelector('input[name="test"]')!; expect(input.classList.contains("is-invalid")).toBe(true); - const label = container.querySelector("label"); + const label = container.querySelector("label")!; expect(label.classList.contains("required")).toBe(true); expect(label.textContent).toBe("Test Label"); }); @@ -216,7 +222,7 @@ describe("Form Field Components", () => { component.setTestState({ test: "value" }); const { container } = render(
{RequiredField(component, "Test Label", "test")}
); - const input = container.querySelector('input[name="test"]'); + const input = container.querySelector('input[name="test"]')!; expect(input.classList.contains("is-invalid")).toBe(false); }); }); @@ -225,10 +231,10 @@ describe("Form Field Components", () => { it("should render without validation styling", () => { const { container } = render(
{OptionalField(component, "Optional Label", "optional")}
); - const input = container.querySelector('input[name="optional"]'); + const input = container.querySelector('input[name="optional"]')!; expect(input.classList.contains("is-invalid")).toBe(false); - const label = container.querySelector("label"); + const label = container.querySelector("label")!; expect(label.classList.contains("required")).toBe(false); }); }); @@ -247,7 +253,7 @@ describe("Form Field Components", () => { component.setTestState({ num: "abc" }); const { container } = render(
{RequiredNumberField(component, "Number", "num")}
); - const input = container.querySelector('input[name="num"]'); + const input = container.querySelector('input[name="num"]')!; expect(input.classList.contains("is-invalid")).toBe(true); }); @@ -255,7 +261,7 @@ describe("Form Field Components", () => { component.setTestState({ num: "123" }); const { container } = render(
{RequiredNumberField(component, "Number", "num")}
); - const input = container.querySelector('input[name="num"]'); + const input = container.querySelector('input[name="num"]')!; expect(input.classList.contains("is-invalid")).toBe(false); }); }); @@ -265,7 +271,7 @@ describe("Form Field Components", () => { component.setTestState({ optNum: "invalid" }); const { container } = render(
{OptionalNumberField(component, "Optional Number", "optNum")}
); - const input = container.querySelector('input[name="optNum"]'); + const input = container.querySelector('input[name="optNum"]')!; expect(input.classList.contains("is-invalid")).toBe(true); }); @@ -273,7 +279,7 @@ describe("Form Field Components", () => { component.setTestState({ optNum: "" }); const { container } = render(
{OptionalNumberField(component, "Optional Number", "optNum")}
); - const input = container.querySelector('input[name="optNum"]'); + const input = container.querySelector('input[name="optNum"]')!; expect(input.classList.contains("is-invalid")).toBe(false); }); }); @@ -283,14 +289,14 @@ describe("Form Field Components", () => { component.setTestState({ bool: true }); const { container } = render(
{RequiredBoolean(component, "Boolean Field", "bool")}
); - const checkbox = container.querySelector('input[type="checkbox"]'); + const checkbox = container.querySelector('input[type="checkbox"]')! as HTMLInputElement; expect(checkbox.checked).toBe(true); }); it("should handle checkbox changes", () => { const { container } = render(
{RequiredBoolean(component, "Boolean Field", "bool")}
); - const checkbox = container.querySelector('input[type="checkbox"]'); + const checkbox = container.querySelector('input[type="checkbox"]') as HTMLInputElement; fireEvent.click(checkbox); expect(component.state.bool).toBe(true); }); @@ -300,7 +306,7 @@ describe("Form Field Components", () => { it("should render select with three options", () => { const { container } = render(
{OptionalBoolean(component, "Optional Bool", "optBool", "Default")}
); - const select = container.querySelector("select"); + const select = container.querySelector("select") as HTMLSelectElement; const options = select.querySelectorAll("option"); expect(options).toHaveLength(3); expect(options[0].textContent).toBe("Default"); @@ -311,7 +317,7 @@ describe("Form Field Components", () => { it("should handle value changes", () => { const { container } = render(
{OptionalBoolean(component, "Optional Bool", "optBool", "Default")}
); - const select = container.querySelector("select"); + const select = container.querySelector("select") as HTMLSelectElement; fireEvent.change(select, { target: { value: "true" } }); expect(component.state.optBool).toBe(true); @@ -324,7 +330,7 @@ describe("Form Field Components", () => { it("should render with proper options", () => { const { container } = render(
{LogDetailSelector(component, "logLevel")}
); - const select = container.querySelector("select"); + const select = container.querySelector("select") as HTMLSelectElement; const options = select.querySelectorAll("option"); expect(options.length).toBeGreaterThan(10); expect(options[0].textContent).toBe("(inherit from parent)"); @@ -333,7 +339,7 @@ describe("Form Field Components", () => { it("should handle numeric values", () => { const { container } = render(
{LogDetailSelector(component, "logLevel")}
); - const select = container.querySelector("select"); + const select = container.querySelector("select") as HTMLSelectElement; fireEvent.change(select, { target: { value: "5" } }); expect(component.state.logLevel).toBe(5); }); @@ -349,14 +355,14 @@ describe("Form Field Components", () => { }); const { container } = render(
{TimesOfDayList(component, "times")}
); - const textarea = container.querySelector('textarea[name="times"]'); - expect(textarea.value).toBe("9:30\n17:00"); + const textarea = container.querySelector('textarea[name="times"]') as HTMLTextAreaElement; + expect(textarea!.value).toBe("9:30\n17:00"); }); it("should parse time strings correctly", () => { const { container } = render(
{TimesOfDayList(component, "times")}
); - const textarea = container.querySelector('textarea[name="times"]'); + const textarea = container.querySelector('textarea[name="times"]') as HTMLTextAreaElement; fireEvent.change(textarea, { target: { value: "09:30\n17:00\ninvalid" } }); expect(component.state.times).toEqual([{ hour: 9, min: 30 }, { hour: 17, min: 0 }, "invalid"]); @@ -368,17 +374,17 @@ describe("Form Field Components", () => { component.setTestState({ dir: "" }); const { container } = render(
{RequiredDirectory(component, "Required Dir", "dir")}
); - const input = container.querySelector('input[name="dir"]'); + const input = container.querySelector('input[name="dir"]')!; expect(input.classList.contains("is-invalid")).toBe(true); - const label = container.querySelector("label"); + const label = container.querySelector("label")!; expect(label.classList.contains("required")).toBe(true); }); it("should render OptionalDirectory without validation", () => { const { container } = render(
{OptionalDirectory(component, "Optional Dir", "dir")}
); - const input = container.querySelector('input[name="dir"]'); + const input = container.querySelector('input[name="dir"]')!; expect(input.classList.contains("is-invalid")).toBe(false); }); @@ -392,7 +398,7 @@ describe("Form Field Components", () => { it("should show button when kopiaUI is available", () => { // Mock window.kopiaUI const mockSelectDirectory = vi.fn(); - globalThis.window.kopiaUI = { selectDirectory: mockSelectDirectory }; + (globalThis.window as unknown as { kopiaUI }).kopiaUI = { selectDirectory: mockSelectDirectory }; const { container } = render(
{RequiredDirectory(component, "Dir", "dir")}
); @@ -400,7 +406,7 @@ describe("Form Field Components", () => { expect(button).toBeTruthy(); // Clean up - delete globalThis.window.kopiaUI; + delete (globalThis.window as unknown as { kopiaUI }).kopiaUI; }); }); }); @@ -409,7 +415,7 @@ describe("Integrated Form Component", () => { let component; beforeEach(() => { - component = new MockComponent(); + component = new MockComponent({}); }); it("should render all field types together", () => { @@ -453,20 +459,20 @@ describe("Integrated Form Component", () => { expect(component.state.requiredBool).toBe(true); // Test select fields - const optionalBoolSelect = container.querySelector('select[name="optionalBool"]'); + const optionalBoolSelect = container.querySelector('select[name="optionalBool"]')!; fireEvent.change(optionalBoolSelect, { target: { value: "true" } }); expect(component.state.optionalBool).toBe(true); - const logLevelSelect = container.querySelector('select[name="logLevel"]'); + const logLevelSelect = container.querySelector('select[name="logLevel"]')!; fireEvent.change(logLevelSelect, { target: { value: "5" } }); expect(component.state.logLevel).toBe(5); // Test textarea fields - const stringListTextarea = container.querySelector('textarea[name="stringList"]'); + const stringListTextarea = container.querySelector('textarea[name="stringList"]')!; fireEvent.change(stringListTextarea, { target: { value: "item1\nitem2" } }); expect(component.state.stringList).toEqual(["item1", "item2"]); - const timesTextarea = container.querySelector('textarea[name="timesOfDay"]'); + const timesTextarea = container.querySelector('textarea[name="timesOfDay"]')!; fireEvent.change(timesTextarea, { target: { value: "09:30\n17:00" } }); expect(component.state.timesOfDay).toEqual([ { hour: 9, min: 30 }, @@ -516,4 +522,4 @@ describe("Integrated Form Component", () => { expect(valueToNumber({ value: input })).toBe(expected); }); }); -}); +}); \ No newline at end of file From 6ab098d8207ba056fdb2395d64be84a3dd59f8c7 Mon Sep 17 00:00:00 2001 From: QazCetelic Date: Tue, 7 Oct 2025 21:02:24 +0200 Subject: [PATCH 18/29] refactor(ui): Add types to StringList tests --- ...tringList.test.jsx => StringList.test.tsx} | 64 ++++++++++--------- 1 file changed, 34 insertions(+), 30 deletions(-) rename tests/forms/{StringList.test.jsx => StringList.test.tsx} (74%) diff --git a/tests/forms/StringList.test.jsx b/tests/forms/StringList.test.tsx similarity index 74% rename from tests/forms/StringList.test.jsx rename to tests/forms/StringList.test.tsx index beb86076..51e111bd 100644 --- a/tests/forms/StringList.test.jsx +++ b/tests/forms/StringList.test.tsx @@ -1,18 +1,22 @@ import { render, act } from "@testing-library/react"; import React from "react"; -import PropTypes from "prop-types"; import { StringList } from "../../src/forms/StringList"; import { fireEvent } from "@testing-library/react"; import { listToMultilineString, multilineStringToList } from "../../src/forms/StringList"; // Mock component to simulate the form component that would use StringList -class MockFormComponent extends React.Component { - static propTypes = { - fieldName: PropTypes.string.isRequired, - initialState: PropTypes.object, - props: PropTypes.object, - }; +interface MockFormComponentProps { + fieldName: string; + initialState?: Record; + props?: Record; +} + +interface MockFormComponentState { + testField: unknown; + [key: string]: unknown; // This is here for handleChange to work with dynamic field names +} +class MockFormComponent extends React.Component { constructor(props) { super(props); this.state = props.initialState || {}; @@ -58,32 +62,32 @@ describe("listToMultilineString", () => { describe("multilineStringToList", () => { it("converts multiline string to array", () => { - const target = { value: "first\nsecond\nthird" }; + const target = { value: "first\nsecond\nthird" } as HTMLTextAreaElement; expect(multilineStringToList(target)).toEqual(["first", "second", "third"]); }); it("returns undefined for empty string", () => { - const target = { value: "" }; + const target = { value: "" } as HTMLTextAreaElement; expect(multilineStringToList(target)).toBeUndefined(); }); it("handles single line string", () => { - const target = { value: "single line" }; + const target = { value: "single line" } as HTMLTextAreaElement; expect(multilineStringToList(target)).toEqual(["single line"]); }); it("handles strings with empty lines", () => { - const target = { value: "first\n\nthird" }; + const target = { value: "first\n\nthird" } as HTMLTextAreaElement; expect(multilineStringToList(target)).toEqual(["first", "", "third"]); }); it("handles strings with carriage returns", () => { - const target = { value: "first\r\nsecond" }; + const target = { value: "first\r\nsecond" } as HTMLTextAreaElement; expect(multilineStringToList(target)).toEqual(["first\r", "second"]); }); it("handles trailing newline", () => { - const target = { value: "first\nsecond\n" }; + const target = { value: "first\nsecond\n" } as HTMLTextAreaElement; expect(multilineStringToList(target)).toEqual(["first", "second", ""]); }); }); @@ -92,7 +96,7 @@ describe("StringList component", () => { it("renders empty textarea when no value is set", () => { const { getByRole } = render(); - const textarea = getByRole("textbox"); + const textarea = getByRole("textbox") as HTMLTextAreaElement; expect(textarea.value).toBe(""); expect(textarea.name).toBe("testField"); }); @@ -104,25 +108,25 @@ describe("StringList component", () => { const { getByRole } = render(); - const textarea = getByRole("textbox"); + const textarea = getByRole("textbox") as HTMLTextAreaElement; expect(textarea.value).toBe("item 1\nitem 2\nitem 3"); }); it("handles onChange event and updates state", () => { - let ref = React.createRef(); + const ref = React.createRef(); const { getByRole } = render(); - const textarea = getByRole("textbox"); + const textarea = getByRole("textbox") as HTMLTextAreaElement; act(() => { fireEvent.change(textarea, { target: { value: "new item 1\nnew item 2" } }); }); - expect(ref.current.state.testField).toEqual(["new item 1", "new item 2"]); + expect(ref.current!.state.testField).toEqual(["new item 1", "new item 2"]); }); it("clears state when input is empty", () => { - let ref = React.createRef(); + const ref = React.createRef(); const initialState = { testField: ["existing", "items"], }; @@ -135,13 +139,13 @@ describe("StringList component", () => { fireEvent.change(textarea, { target: { value: "" } }); }); - expect(ref.current.state.testField).toBeUndefined(); + expect(ref.current!.state.testField).toBeUndefined(); }); it("has correct textarea attributes", () => { const { getByRole } = render(); - const textarea = getByRole("textbox"); + const textarea = getByRole("textbox") as HTMLTextAreaElement; expect(textarea.tagName).toBe("TEXTAREA"); expect(textarea.rows).toBe(5); expect(textarea.classList.contains("form-control-sm")).toBe(true); @@ -157,27 +161,27 @@ describe("StringList component", () => { const { getByTestId } = render(); - const textarea = getByTestId("string-list-input"); + const textarea = getByTestId("string-list-input") as HTMLTextAreaElement; expect(textarea.placeholder).toBe("Enter items"); expect(textarea.disabled).toBe(true); expect(textarea.classList.contains("custom-class")).toBe(true); }); it("handles component state updates", () => { - let ref = React.createRef(); + const ref = React.createRef(); const { getByRole } = render( , ); - let textarea = getByRole("textbox"); + let textarea = getByRole("textbox") as HTMLTextAreaElement; expect(textarea.value).toBe("initial"); // Update the component state act(() => { - ref.current.setState({ testField: ["updated", "list"] }); + ref.current!.setState({ testField: ["updated", "list"] }); }); - textarea = getByRole("textbox"); + textarea = getByRole("textbox") as HTMLTextAreaElement; expect(textarea.value).toBe("updated\nlist"); }); @@ -190,20 +194,20 @@ describe("StringList component", () => { const { getByRole } = render(); - const textarea = getByRole("textbox"); + const textarea = getByRole("textbox") as HTMLTextAreaElement; expect(textarea.value).toBe("nested\nvalue"); }); it("maintains proper Form.Group structure", () => { const { container } = render(); - const formGroup = container.querySelector(".col"); + const formGroup = container.querySelector(".col")!; expect(formGroup).toBeTruthy(); expect(formGroup.querySelector("textarea.form-control")).toBeTruthy(); }); it("preserves whitespace in list items", () => { - let ref = React.createRef(); + const ref = React.createRef(); const { getByRole } = render(); const textarea = getByRole("textbox"); @@ -212,6 +216,6 @@ describe("StringList component", () => { fireEvent.change(textarea, { target: { value: " item with spaces \n\ttabbed item\n normal item" } }); }); - expect(ref.current.state.testField).toEqual([" item with spaces ", "\ttabbed item", " normal item"]); + expect(ref.current!.state.testField).toEqual([" item with spaces ", "\ttabbed item", " normal item"]); }); }); From 61bf1adff5e638ad1c5c1871f4e696c64a9b7238 Mon Sep 17 00:00:00 2001 From: QazCetelic Date: Tue, 7 Oct 2025 21:03:58 +0200 Subject: [PATCH 19/29] refactor(ui): Add types to StringList tests II --- src/forms/StringList.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/forms/StringList.tsx b/src/forms/StringList.tsx index 749b39e9..e5db2607 100644 --- a/src/forms/StringList.tsx +++ b/src/forms/StringList.tsx @@ -4,7 +4,7 @@ import Col from "react-bootstrap/Col"; import { stateProperty } from "."; import { ComponentChangeHandling } from "src/components/types"; -export function listToMultilineString(v: any): string { +export function listToMultilineString(v: undefined | null | string[]): string { if (v) { return v.join("\n"); } From 8b37aa822d6aace845aec6f0ec8611df041adec2 Mon Sep 17 00:00:00 2001 From: QazCetelic Date: Tue, 7 Oct 2025 21:10:57 +0200 Subject: [PATCH 20/29] refactor(ui): Add types to TimeOfDayList tests --- ...yList.test.jsx => TimesOfDayList.test.tsx} | 79 +++++++++++-------- 1 file changed, 45 insertions(+), 34 deletions(-) rename tests/forms/{TimesOfDayList.test.jsx => TimesOfDayList.test.tsx} (76%) diff --git a/tests/forms/TimesOfDayList.test.jsx b/tests/forms/TimesOfDayList.test.tsx similarity index 76% rename from tests/forms/TimesOfDayList.test.jsx rename to tests/forms/TimesOfDayList.test.tsx index 34836368..0411542f 100644 --- a/tests/forms/TimesOfDayList.test.jsx +++ b/tests/forms/TimesOfDayList.test.tsx @@ -4,8 +4,19 @@ import PropTypes from "prop-types"; import { TimesOfDayList } from "../../src/forms/TimesOfDayList"; import { fireEvent } from "@testing-library/react"; +interface MockFormComponentProps { + fieldName: string; + initialState?: Record; + props?: Record; +} + +interface MockFormComponentState { + testField: unknown; + [key: string]: unknown; // This is here for handleChange to work with dynamic field names +} + // Mock component to simulate the form component that would use TimesOfDayList -class MockFormComponent extends React.Component { +class MockFormComponent extends React.Component { static propTypes = { fieldName: PropTypes.string.isRequired, initialState: PropTypes.object, @@ -33,7 +44,7 @@ describe("TimesOfDayList", () => { it("renders empty textarea when no times are set", () => { const { getByRole } = render(); - const textarea = getByRole("textbox"); + const textarea = getByRole("textbox") as HTMLTextAreaElement; expect(textarea.value).toBe(""); }); @@ -48,7 +59,7 @@ describe("TimesOfDayList", () => { const { getByRole } = render(); - const textarea = getByRole("textbox"); + const textarea = getByRole("textbox") as HTMLTextAreaElement; expect(textarea.value).toBe("9:30\n15:45\n23:00"); }); @@ -59,12 +70,12 @@ describe("TimesOfDayList", () => { const { getByRole } = render(); - const textarea = getByRole("textbox"); + const textarea = getByRole("textbox") as HTMLTextAreaElement; expect(textarea.value).toBe("9:30\ninvalid-time\n15:45"); }); it("parses valid time strings correctly", () => { - let ref = React.createRef(); + const ref = React.createRef(); const { getByRole } = render(); const textarea = getByRole("textbox"); @@ -73,7 +84,7 @@ describe("TimesOfDayList", () => { fireEvent.change(textarea, { target: { value: "9:30\n15:45\n23:00" } }); }); - expect(ref.current.state.testField).toEqual([ + expect(ref.current!.state.testField).toEqual([ { hour: 9, min: 30 }, { hour: 15, min: 45 }, { hour: 23, min: 0 }, @@ -81,7 +92,7 @@ describe("TimesOfDayList", () => { }); it("handles mixed valid and invalid time strings", () => { - let ref = React.createRef(); + const ref = React.createRef(); const { getByRole } = render(); const textarea = getByRole("textbox"); @@ -90,7 +101,7 @@ describe("TimesOfDayList", () => { fireEvent.change(textarea, { target: { value: "9:30\ninvalid\n15:45\n25:00\n12:5" } }); }); - expect(ref.current.state.testField).toEqual([ + expect(ref.current!.state.testField).toEqual([ { hour: 9, min: 30 }, "invalid", { hour: 15, min: 45 }, @@ -100,7 +111,7 @@ describe("TimesOfDayList", () => { }); it("validates hour range (0-23)", () => { - let ref = React.createRef(); + const ref = React.createRef(); const { getByRole } = render(); const textarea = getByRole("textbox"); @@ -109,7 +120,7 @@ describe("TimesOfDayList", () => { fireEvent.change(textarea, { target: { value: "0:00\n23:59\n24:00\n-1:00" } }); }); - expect(ref.current.state.testField).toEqual([ + expect(ref.current!.state.testField).toEqual([ { hour: 0, min: 0 }, { hour: 23, min: 59 }, "24:00", // Invalid - hour >= 24 @@ -118,16 +129,16 @@ describe("TimesOfDayList", () => { }); it("validates minute range (0-59)", () => { - let ref = React.createRef(); + const ref = React.createRef(); const { getByRole } = render(); - const textarea = getByRole("textbox"); + const textarea = getByRole("textbox") as HTMLTextAreaElement; act(() => { fireEvent.change(textarea, { target: { value: "12:00\n12:59\n12:60\n12:-1" } }); }); - expect(ref.current.state.testField).toEqual([ + expect(ref.current!.state.testField).toEqual([ { hour: 12, min: 0 }, { hour: 12, min: 59 }, "12:60", // Invalid - minute >= 60 @@ -136,16 +147,16 @@ describe("TimesOfDayList", () => { }); it("requires two-digit minute format for single digits", () => { - let ref = React.createRef(); + const ref = React.createRef(); const { getByRole } = render(); - const textarea = getByRole("textbox"); + const textarea = getByRole("textbox") as HTMLTextAreaElement; act(() => { fireEvent.change(textarea, { target: { value: "12:05\n12:5\n12:00\n12:9" } }); }); - expect(ref.current.state.testField).toEqual([ + expect(ref.current!.state.testField).toEqual([ { hour: 12, min: 5 }, // Valid - two digits "12:5", // Invalid - single digit minute { hour: 12, min: 0 }, // Valid - 00 is two digits @@ -154,7 +165,7 @@ describe("TimesOfDayList", () => { }); it("handles empty string input", () => { - let ref = React.createRef(); + const ref = React.createRef(); const { getByRole } = render(); const textarea = getByRole("textbox"); @@ -163,20 +174,20 @@ describe("TimesOfDayList", () => { fireEvent.change(textarea, { target: { value: "" } }); }); - expect(ref.current.state.testField).toBeUndefined(); + expect(ref.current!.state.testField).toBeUndefined(); }); it("handles whitespace and empty lines", () => { - let ref = React.createRef(); + const ref = React.createRef(); const { getByRole } = render(); - const textarea = getByRole("textbox"); + const textarea = getByRole("textbox") as HTMLTextAreaElement; act(() => { fireEvent.change(textarea, { target: { value: "9:30\n\n15:45\n \n23:00" } }); }); - expect(ref.current.state.testField).toEqual([ + expect(ref.current!.state.testField).toEqual([ { hour: 9, min: 30 }, "", { hour: 15, min: 45 }, @@ -196,21 +207,21 @@ describe("TimesOfDayList", () => { const { getByRole } = render(); - const textarea = getByRole("textbox"); + const textarea = getByRole("textbox") as HTMLTextAreaElement; expect(textarea.value).toBe("9:05\n12:00\n15:30"); }); it("preserves order of times", () => { - let ref = React.createRef(); + const ref = React.createRef(); const { getByRole } = render(); - const textarea = getByRole("textbox"); + const textarea = getByRole("textbox") as HTMLTextAreaElement; act(() => { fireEvent.change(textarea, { target: { value: "23:59\n00:01\n12:30\n06:15" } }); }); - expect(ref.current.state.testField).toEqual([ + expect(ref.current!.state.testField).toEqual([ { hour: 23, min: 59 }, { hour: 0, min: 1 }, { hour: 12, min: 30 }, @@ -219,7 +230,7 @@ describe("TimesOfDayList", () => { }); it("handles complex regex edge cases", () => { - let ref = React.createRef(); + const ref = React.createRef(); const { getByRole } = render(); const textarea = getByRole("textbox"); @@ -228,7 +239,7 @@ describe("TimesOfDayList", () => { fireEvent.change(textarea, { target: { value: "1:2:3\n12:\n:30\n12:30:00\n12.30" } }); }); - expect(ref.current.state.testField).toEqual([ + expect(ref.current!.state.testField).toEqual([ "1:2:3", // Invalid - matches regex but has extra number "12:", // Invalid - no minutes ":30", // Invalid - no hours @@ -246,7 +257,7 @@ describe("TimesOfDayList", () => { const { getByTestId } = render(); - const textarea = getByTestId("times-input"); + const textarea = getByTestId("times-input") as HTMLTextAreaElement; expect(textarea.placeholder).toBe("Enter times"); expect(textarea.disabled).toBe(true); }); @@ -254,7 +265,7 @@ describe("TimesOfDayList", () => { it("has correct textarea attributes", () => { const { getByRole } = render(); - const textarea = getByRole("textbox"); + const textarea = getByRole("textbox") as HTMLTextAreaElement; expect(textarea.tagName).toBe("TEXTAREA"); expect(textarea.rows).toBe(5); expect(textarea.name).toBe("testField"); @@ -263,26 +274,26 @@ describe("TimesOfDayList", () => { it("displays validation feedback message", () => { const { container } = render(); - const feedback = container.querySelector(".invalid-feedback"); + const feedback = container.querySelector(".invalid-feedback") as HTMLElement; expect(feedback).toBeTruthy(); expect(feedback.textContent).toBe("Invalid Times of Day"); }); it("updates display when component state changes", () => { - let ref = React.createRef(); + const ref = React.createRef(); const { getByRole } = render( , ); - let textarea = getByRole("textbox"); + let textarea = getByRole("textbox") as HTMLTextAreaElement; expect(textarea.value).toBe("9:30"); // Update the component state and rerender act(() => { - ref.current.setState({ testField: [{ hour: 15, min: 45 }] }); + ref.current!.setState({ testField: [{ hour: 15, min: 45 }] }); }); - textarea = getByRole("textbox"); + textarea = getByRole("textbox") as HTMLTextAreaElement; expect(textarea.value).toBe("15:45"); }); }); From 2894478c93156313d72aaf5ff085fec3117dafc2 Mon Sep 17 00:00:00 2001 From: QazCetelic Date: Wed, 8 Oct 2025 19:26:00 +0200 Subject: [PATCH 21/29] refactor(ui): Add types to uiutil-browser tests --- ...til-browser.test.js => uiutil-browser.test.ts} | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) rename tests/utils/{uiutil-browser.test.js => uiutil-browser.test.ts} (88%) diff --git a/tests/utils/uiutil-browser.test.js b/tests/utils/uiutil-browser.test.ts similarity index 88% rename from tests/utils/uiutil-browser.test.js rename to tests/utils/uiutil-browser.test.ts index 9d5c7b5f..3d88d5a2 100644 --- a/tests/utils/uiutil-browser.test.js +++ b/tests/utils/uiutil-browser.test.ts @@ -6,16 +6,21 @@ describe("redirect", () => { beforeAll(() => { // Mock window.location.replace - delete window.location; - window.location = { replace: vi.fn() }; + Object.defineProperty(window, "location", { + configurable: true, + value: { replace: vi.fn() }, + }); }); afterAll(() => { - window.location = originalLocation; + Object.defineProperty(window, "location", { + configurable: true, + value: originalLocation, + }); }); beforeEach(() => { - window.location.replace.mockClear(); + (window.location.replace as jest.Mock).mockClear(); }); it("redirects to /repo when error code is NOT_CONNECTED", () => { @@ -69,7 +74,7 @@ describe("errorAlert", () => { }); beforeEach(() => { - window.alert.mockClear(); + (window.alert as jest.Mock).mockClear(); }); it("shows error message from response data", () => { From 3b4ddbbcfa057168a98894bb782e98cfe70857f8 Mon Sep 17 00:00:00 2001 From: QazCetelic Date: Wed, 8 Oct 2025 19:43:00 +0200 Subject: [PATCH 22/29] refactor(ui): Add types to uiutil-components tests --- src/utils/uiutil.tsx | 8 ++++++-- ...ts.test.jsx => uiutil-components.test.tsx} | 19 ++++++++++++++++++- 2 files changed, 24 insertions(+), 3 deletions(-) rename tests/utils/{uiutil-components.test.jsx => uiutil-components.test.tsx} (88%) diff --git a/src/utils/uiutil.tsx b/src/utils/uiutil.tsx index 11364984..2cc4d1a9 100644 --- a/src/utils/uiutil.tsx +++ b/src/utils/uiutil.tsx @@ -3,7 +3,11 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import React from "react"; import { sizeDisplayName } from "./formatutils.js"; -export function sizeWithFailures(size?: number, summ, bytesStringBase2: boolean) { +export function sizeWithFailures( + size?: number, + summ?: { errors: { path: string; error: string; }[]; numFailed: number } | null, + bytesStringBase2?: boolean, +): "" | React.JSX.Element { if (size === undefined) { return ""; } @@ -31,7 +35,7 @@ export function sizeWithFailures(size?: number, summ, bytesStringBase2: boolean) /** * In case of an error, redirect to the repository selection - * @param {error} The error that was returned + * @param {error} e The error that was returned */ export function redirect(e) { if (e && e.response && e.response.data && e.response.data.code === "NOT_CONNECTED") { diff --git a/tests/utils/uiutil-components.test.jsx b/tests/utils/uiutil-components.test.tsx similarity index 88% rename from tests/utils/uiutil-components.test.jsx rename to tests/utils/uiutil-components.test.tsx index e3b67729..3e8ea277 100644 --- a/tests/utils/uiutil-components.test.jsx +++ b/tests/utils/uiutil-components.test.tsx @@ -4,19 +4,27 @@ import "@testing-library/jest-dom"; import { sizeWithFailures } from "../../src/utils/uiutil"; import { taskStatusSymbol } from "../../src/utils/taskutil"; +const sizeWithFailuresNoElement = new Error("sizeWithFailures did not return a React element, this is not expected behavior"); + describe("sizeWithFailures", () => { it("returns empty string for undefined size", () => { - expect(sizeWithFailures(undefined)).toBe(""); + expect(sizeWithFailures(undefined, undefined, false)).toBe(""); }); it("returns simple size display without errors", () => { const result = sizeWithFailures(1024, null, false); + if (typeof result === "string") { + throw sizeWithFailuresNoElement; + } expect(result.props.children).toBe("1 KB"); }); it("returns simple size display when no failures", () => { const summ = { errors: [], numFailed: 0 }; const result = sizeWithFailures(1024, summ, false); + if (typeof result === "string") { + throw sizeWithFailuresNoElement; + } expect(result.props.children).toBe("1 KB"); }); @@ -26,6 +34,9 @@ describe("sizeWithFailures", () => { numFailed: 1, }; const result = sizeWithFailures(1024, summ, false); + if (typeof result === "string") { + throw sizeWithFailuresNoElement; + } // Should be a span containing size, nbsp, and error icon expect(result.type).toBe("span"); @@ -43,6 +54,9 @@ describe("sizeWithFailures", () => { numFailed: 2, }; const result = sizeWithFailures(1024, summ, false); + if (typeof result === "string") { + throw sizeWithFailuresNoElement; + } expect(result.type).toBe("span"); // Check that error icon has the correct title format @@ -58,6 +72,9 @@ describe("sizeWithFailures", () => { numFailed: 1, }; const result = sizeWithFailures(1024, summ, false); + if (typeof result === "string") { + throw sizeWithFailuresNoElement; + } const errorIcon = result.props.children[2]; // Third element is the icon expect(errorIcon.props.title).toContain("Error: "); From cd7a172d6a6faeea64cc03c2a4f371fe62ce13ef Mon Sep 17 00:00:00 2001 From: QazCetelic Date: Wed, 8 Oct 2025 19:50:54 +0200 Subject: [PATCH 23/29] refactor(ui): Add types to react-router-mock tests --- ...-router-mock.jsx => react-router-mock.tsx} | 37 +++++++++++++++++-- 1 file changed, 33 insertions(+), 4 deletions(-) rename tests/testutils/{react-router-mock.jsx => react-router-mock.tsx} (83%) diff --git a/tests/testutils/react-router-mock.jsx b/tests/testutils/react-router-mock.tsx similarity index 83% rename from tests/testutils/react-router-mock.jsx rename to tests/testutils/react-router-mock.tsx index 49bcecc1..fc33767d 100644 --- a/tests/testutils/react-router-mock.jsx +++ b/tests/testutils/react-router-mock.tsx @@ -93,6 +93,17 @@ export function createRouterMock(options = {}) { searchParams = DEFAULT_STATE.searchParams, simple = false, components = {}, + }: { + location?: typeof DEFAULT_STATE.location; + params?: Record; + navigate?: typeof DEFAULT_STATE.navigate; + searchParams?: URLSearchParams | Record | string; + simple?: boolean; + components?: { + link?: boolean; + navLink?: boolean; + only?: boolean; + }; } = options; const { link: mockLink = true, navLink: mockNavLink = true, only: componentsOnly = false } = components; @@ -122,7 +133,7 @@ export function createRouterMock(options = {}) { // Component-only mock (minimal footprint) function createComponentOnlyMock({ mockLink, mockNavLink }) { return () => { - const mocks = {}; + const mocks: { Link?: typeof MockLink; NavLink?: typeof MockNavLink } = {}; if (mockLink) mocks.Link = MockLink; if (mockNavLink) mocks.NavLink = MockNavLink; return mocks; @@ -132,7 +143,14 @@ function createComponentOnlyMock({ mockLink, mockNavLink }) { // Simple mock (no actual implementation) function createSimpleMock({ navigate, mockLink, mockNavLink }) { return () => { - const mocks = { + const mocks: { + useNavigate: () => typeof navigate; + useLocation: () => ReturnType; + useParams: () => ReturnType; + useSearchParams: () => ReturnType; + Link?: typeof MockLink; + NavLink?: typeof MockNavLink; + } = { useNavigate: () => navigate, useLocation: () => mockUseLocation(), useParams: () => mockUseParams(), @@ -150,7 +168,14 @@ function createSimpleMock({ navigate, mockLink, mockNavLink }) { function createFullMock({ navigate, mockLink, mockNavLink }) { return async () => { const actual = await vi.importActual("react-router-dom"); - const mocks = { + const mocks: { + useNavigate: () => typeof navigate; + useLocation: () => ReturnType; + useParams: () => ReturnType; + useSearchParams: () => ReturnType; + Link?: typeof MockLink; + NavLink?: typeof MockNavLink; + } = { ...actual, useNavigate: () => navigate, useLocation: () => mockUseLocation(), @@ -185,7 +210,11 @@ export function resetRouterMocks() { * @param {Object} [state.params] - Params object to mock * @param {URLSearchParams|Object} [state.searchParams] - Search params to mock */ -export function updateRouterMocks(state = {}) { +export function updateRouterMocks(state: { + location?: typeof DEFAULT_STATE.location; + params?: Record; + searchParams?: URLSearchParams | Record | string; +} = {}) { if (state.location) { mockUseLocation.mockReturnValue({ ...DEFAULT_STATE.location, ...state.location }); } From 22b3030bc9e350a08a4b63144bd82c25f3e95b08 Mon Sep 17 00:00:00 2001 From: QazCetelic Date: Wed, 8 Oct 2025 20:22:59 +0200 Subject: [PATCH 24/29] refactor(ui): Add TypeScript to config files --- babel.config.js | 2 -- babel.config.ts | 2 ++ package-lock.json | 11 +++++++++++ package.json | 1 + vite.config.js => vite.config.ts | 2 +- 5 files changed, 15 insertions(+), 3 deletions(-) delete mode 100644 babel.config.js create mode 100644 babel.config.ts rename vite.config.js => vite.config.ts (97%) diff --git a/babel.config.js b/babel.config.js deleted file mode 100644 index 2f1045a4..00000000 --- a/babel.config.js +++ /dev/null @@ -1,2 +0,0 @@ -//babel.config.js -module.exports = { presets: ["@babel/preset-env"] }; diff --git a/babel.config.ts b/babel.config.ts new file mode 100644 index 00000000..3db1c64c --- /dev/null +++ b/babel.config.ts @@ -0,0 +1,2 @@ +//babel.config.ts +module.exports = { presets: ["@babel/preset-env"] }; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 24157527..661890d2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -38,6 +38,7 @@ "eslint-config-react": "^1.1.7", "eslint-plugin-react": "^7.37.5", "globals": "^16.3.0", + "jiti": "^2.6.1", "jsdom": "^26.1.0", "prettier": "^3.5.3", "typescript": "^5.8.3", @@ -5395,6 +5396,16 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", diff --git a/package.json b/package.json index f7b9350b..aab9874a 100644 --- a/package.json +++ b/package.json @@ -56,6 +56,7 @@ "eslint-config-react": "^1.1.7", "eslint-plugin-react": "^7.37.5", "globals": "^16.3.0", + "jiti": "^2.6.1", "jsdom": "^26.1.0", "prettier": "^3.5.3", "typescript": "^5.8.3", diff --git a/vite.config.js b/vite.config.ts similarity index 97% rename from vite.config.js rename to vite.config.ts index 9b4e31ef..6072b893 100644 --- a/vite.config.js +++ b/vite.config.ts @@ -20,7 +20,7 @@ export default defineConfig(() => { server: { port: 3000, host: "localhost", - https: false, + https: undefined, strictPort: true, open: process.env.VITE_KOPIA_ENDPOINT ? false : true, proxy: { From ceff5f2d9dee56e83e5187882962e8067ccb5011 Mon Sep 17 00:00:00 2001 From: QazCetelic Date: Wed, 8 Oct 2025 20:40:36 +0200 Subject: [PATCH 25/29] refactor(ui): Migrate UIPreferencesContext tests to TypeScript --- ...test.jsx => UIPreferencesContext.test.tsx} | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) rename tests/contexts/{UIPreferencesContext.test.jsx => UIPreferencesContext.test.tsx} (94%) diff --git a/tests/contexts/UIPreferencesContext.test.jsx b/tests/contexts/UIPreferencesContext.test.tsx similarity index 94% rename from tests/contexts/UIPreferencesContext.test.jsx rename to tests/contexts/UIPreferencesContext.test.tsx index 253b81f3..ad7bfce1 100644 --- a/tests/contexts/UIPreferencesContext.test.jsx +++ b/tests/contexts/UIPreferencesContext.test.tsx @@ -22,13 +22,13 @@ afterEach(() => { }); // Helper component to access context values -const TestComponent = ({ onMount }) => { +function TestComponent({ onMount }) { const preferences = useContext(UIPreferencesContext); React.useEffect(() => { onMount(preferences); }, [preferences, onMount]); return null; -}; +} describe("UIPreferencesContext", () => { describe("Default values", () => { @@ -41,7 +41,7 @@ describe("UIPreferencesContext", () => { let capturedPreferences; render( - + (capturedPreferences = prefs)} /> , ); @@ -74,7 +74,7 @@ describe("UIPreferencesContext", () => { let capturedPreferences; render( - + (capturedPreferences = prefs)} /> , ); @@ -100,7 +100,7 @@ describe("UIPreferencesContext", () => { let capturedPreferences; render( - + (capturedPreferences = prefs)} /> , ); @@ -129,7 +129,7 @@ describe("UIPreferencesContext", () => { let capturedPreferences; render( - + (capturedPreferences = prefs)} /> , ); @@ -162,7 +162,7 @@ describe("UIPreferencesContext", () => { let capturedPreferences; const { unmount } = render( - + (capturedPreferences = prefs)} /> , ); @@ -191,7 +191,7 @@ describe("UIPreferencesContext", () => { it("should update theme and sync with HTML classes", async () => { let capturedPreferences; render( - + (capturedPreferences = prefs)} /> , ); @@ -219,7 +219,7 @@ describe("UIPreferencesContext", () => { it("should update font size and sync with HTML classes", async () => { let capturedPreferences; render( - + (capturedPreferences = prefs)} /> , ); @@ -242,7 +242,7 @@ describe("UIPreferencesContext", () => { it("should update page size", async () => { let capturedPreferences; render( - + (capturedPreferences = prefs)} /> , ); @@ -263,7 +263,7 @@ describe("UIPreferencesContext", () => { it("should update bytesStringBase2", async () => { let capturedPreferences; render( - + (capturedPreferences = prefs)} /> , ); @@ -292,7 +292,7 @@ describe("UIPreferencesContext", () => { it("should update defaultSnapshotViewAll", async () => { let capturedPreferences; render( - + (capturedPreferences = prefs)} /> , ); @@ -337,7 +337,7 @@ describe("UIPreferencesContext", () => { axiosMock.onPut("/api/v1/ui-preferences").reply(200); render( - + {}} /> , ); @@ -371,7 +371,7 @@ describe("UIPreferencesContext", () => { let capturedPreferences; render( - + (capturedPreferences = prefs)} /> , ); From 5644c2c3f933fa911e9a3cf90cc57124866d697c Mon Sep 17 00:00:00 2001 From: QazCetelic Date: Sat, 18 Oct 2025 12:14:42 +0200 Subject: [PATCH 26/29] Remove comment "This is only mentioned here" --- src/components/policy-editor/UpcomingSnapshotTimes.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/policy-editor/UpcomingSnapshotTimes.tsx b/src/components/policy-editor/UpcomingSnapshotTimes.tsx index 3844c34b..004303ec 100644 --- a/src/components/policy-editor/UpcomingSnapshotTimes.tsx +++ b/src/components/policy-editor/UpcomingSnapshotTimes.tsx @@ -7,7 +7,6 @@ export function UpcomingSnapshotTimes(resolved: { schedulingError?: string; upco return null; } - // This is only mentioned here if (resolved.schedulingError) { return

{resolved.schedulingError}

; } From 86be3759c301c8520a44a1164decf7fcb22d7c93 Mon Sep 17 00:00:00 2001 From: QazCetelic Date: Sat, 18 Oct 2025 12:23:57 +0200 Subject: [PATCH 27/29] Get rid of linting errors by changing lint rules and removing imports --- eslint.config.mjs | 4 ++-- src/pages/SnapshotDirectory.tsx | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/eslint.config.mjs b/eslint.config.mjs index af90e0ae..3c87f11e 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -25,10 +25,10 @@ export default defineConfig([ }, { rules: { - "prefer-const": "warn", + "prefer-const": "off", "no-var": "warn", "@typescript-eslint/no-unused-vars": ["warn", { argsIgnorePattern: "^_", varsIgnorePattern: "^_" }], - "@typescript-eslint/no-explicit-any": "warn", + "@typescript-eslint/no-explicit-any": "off", }, }, pluginReact.configs.flat.recommended, diff --git a/src/pages/SnapshotDirectory.tsx b/src/pages/SnapshotDirectory.tsx index ec4b606d..65747e54 100644 --- a/src/pages/SnapshotDirectory.tsx +++ b/src/pages/SnapshotDirectory.tsx @@ -11,7 +11,6 @@ import { DirectoryItems } from "../components/DirectoryItems"; import { CLIEquivalent } from "../components/CLIEquivalent"; import { DirectoryBreadcrumbs } from "../components/DirectoryBreadcrumbs"; import PropTypes from "prop-types"; -import { ComponentChangeHandling, ChangeEventHandle } from "src/components/types"; class SnapshotDirectoryInternal extends Component { constructor() { From ae3d0920230fa45e90c769bf78aea0573ed44835 Mon Sep 17 00:00:00 2001 From: QazCetelic Date: Sun, 19 Oct 2025 13:44:40 +0200 Subject: [PATCH 28/29] Remove omitted parameters --- src/pages/Policies.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/Policies.tsx b/src/pages/Policies.tsx index 550fb279..5117e713 100644 --- a/src/pages/Policies.tsx +++ b/src/pages/Policies.tsx @@ -133,7 +133,7 @@ export class PoliciesInternal extends Component implements C return; } - const error = checkPolicyPath(this.state.policyPath/*, this.state.localHost, this.state.localUsername*/); + const error = checkPolicyPath(this.state.policyPath); if (error) { alert(error + "\nMust be either an absolute path, `user@host:/absolute/path`, `user@host` or `@host`. Use backslashes on Windows."); From b202030fdb0b6d50462439a28bc20a081cb7857e Mon Sep 17 00:00:00 2001 From: QazCetelic Date: Sun, 19 Oct 2025 13:46:40 +0200 Subject: [PATCH 29/29] Formatted code with prettier --- babel.config.ts | 2 +- global.d.ts | 2 +- src/components/policy-editor/PolicyEditor.tsx | 4 ++-- src/components/types.ts | 2 +- src/forms/OptionalBoolean.tsx | 7 ++++++- src/forms/OptionalField.tsx | 8 +++++++- src/forms/OptionalFieldNoLabel.tsx | 9 ++++++++- src/forms/OptionalNumberField.tsx | 7 ++++++- src/forms/RequiredField.tsx | 8 +++++++- src/forms/index.tsx | 2 +- src/pages/Policies.tsx | 4 +++- src/pages/SnapshotRestore.tsx | 7 +++++-- src/utils/formatutils.ts | 8 ++++++-- src/utils/policyutil.tsx | 2 +- src/utils/uiutil.tsx | 2 +- tests/components/SnapshotEstimation.test.tsx | 6 +++++- tests/forms/forms.test.tsx | 2 +- tests/pages/SnapshotCreate.test.tsx | 2 +- tests/testutils/interval-mocks.ts | 2 +- tests/testutils/react-router-mock.tsx | 12 +++++++----- tests/utils/uiutil-components.test.tsx | 4 +++- 21 files changed, 74 insertions(+), 28 deletions(-) diff --git a/babel.config.ts b/babel.config.ts index 3db1c64c..2fea1655 100644 --- a/babel.config.ts +++ b/babel.config.ts @@ -1,2 +1,2 @@ //babel.config.ts -module.exports = { presets: ["@babel/preset-env"] }; \ No newline at end of file +module.exports = { presets: ["@babel/preset-env"] }; diff --git a/global.d.ts b/global.d.ts index d47c59fa..9aa2ecd9 100644 --- a/global.d.ts +++ b/global.d.ts @@ -4,4 +4,4 @@ declare interface Window { selectDirectory?: (callback: (path: string) => void) => void; browseDirectory?; }; -} \ No newline at end of file +} diff --git a/src/components/policy-editor/PolicyEditor.tsx b/src/components/policy-editor/PolicyEditor.tsx index 3a625509..4a7b0368 100644 --- a/src/components/policy-editor/PolicyEditor.tsx +++ b/src/components/policy-editor/PolicyEditor.tsx @@ -66,7 +66,7 @@ type PolicyEditorState = { resolvedError?: Error; isNew?: boolean; saving?: boolean; -} +}; type Policy = { files?: any; @@ -885,4 +885,4 @@ export class PolicyEditor extends Component ); } -} \ No newline at end of file +} diff --git a/src/components/types.ts b/src/components/types.ts index 7aa9cbf3..e8d54294 100644 --- a/src/components/types.ts +++ b/src/components/types.ts @@ -4,4 +4,4 @@ export type ChangeEventHandle = (event: React.ChangeEvent, valueGetter?: (x export type ComponentChangeHandling = Component & { handleChange: ChangeEventHandle; -}; \ No newline at end of file +}; diff --git a/src/forms/OptionalBoolean.tsx b/src/forms/OptionalBoolean.tsx index 1b8f6650..49538ac3 100644 --- a/src/forms/OptionalBoolean.tsx +++ b/src/forms/OptionalBoolean.tsx @@ -16,7 +16,12 @@ function optionalBooleanValue(target: HTMLSelectElement): boolean | undefined { return undefined; } -export function OptionalBoolean(component: ComponentChangeHandling, label: string | null, name: string, defaultLabel?: string) { +export function OptionalBoolean( + component: ComponentChangeHandling, + label: string | null, + name: string, + defaultLabel?: string, +) { return ( {label && {label}} diff --git a/src/forms/OptionalField.tsx b/src/forms/OptionalField.tsx index 73d6d1f4..137539e1 100644 --- a/src/forms/OptionalField.tsx +++ b/src/forms/OptionalField.tsx @@ -4,7 +4,13 @@ import Col from "react-bootstrap/Col"; import { stateProperty } from "."; import { ComponentChangeHandling } from "src/components/types"; -export function OptionalField(component: ComponentChangeHandling, label: string, name: string, props = {}, helpText = null) { +export function OptionalField( + component: ComponentChangeHandling, + label: string, + name: string, + props = {}, + helpText = null, +) { return ( {label} diff --git a/src/forms/OptionalFieldNoLabel.tsx b/src/forms/OptionalFieldNoLabel.tsx index 28c8941e..33a086d7 100644 --- a/src/forms/OptionalFieldNoLabel.tsx +++ b/src/forms/OptionalFieldNoLabel.tsx @@ -5,7 +5,14 @@ import Col from "react-bootstrap/Col"; import { stateProperty } from "."; import { ComponentChangeHandling } from "src/components/types"; -export function OptionalFieldNoLabel(component: ComponentChangeHandling, label: string, name: string, props = {}, helpText = null, invalidFeedback = null) { +export function OptionalFieldNoLabel( + component: ComponentChangeHandling, + label: string, + name: string, + props = {}, + helpText = null, + invalidFeedback = null, +) { return ( {label && {label}} diff --git a/src/forms/RequiredField.tsx b/src/forms/RequiredField.tsx index f68f2c41..f6d2959e 100644 --- a/src/forms/RequiredField.tsx +++ b/src/forms/RequiredField.tsx @@ -4,7 +4,13 @@ import Col from "react-bootstrap/Col"; import { stateProperty } from "."; import { ComponentChangeHandling } from "src/components/types"; -export function RequiredField(component: ComponentChangeHandling, label: string, name: string, props = {}, helpText: string | null = null) { +export function RequiredField( + component: ComponentChangeHandling, + label: string, + name: string, + props = {}, + helpText: string | null = null, +) { return ( {label} diff --git a/src/forms/index.tsx b/src/forms/index.tsx index 4bd49d12..96a2c9e8 100644 --- a/src/forms/index.tsx +++ b/src/forms/index.tsx @@ -3,7 +3,7 @@ import { getDeepStateProperty, setDeepStateProperty } from "../utils/deepstate"; import { ComponentChangeHandling } from "src/components/types"; export function validateRequiredFields(component: ComponentChangeHandling, fields: string[]) { - const updateState: {[field: string]: any } = {}; + const updateState: { [field: string]: any } = {}; let failed = false; for (let i = 0; i < fields.length; i++) { diff --git a/src/pages/Policies.tsx b/src/pages/Policies.tsx index 5117e713..a36b175c 100644 --- a/src/pages/Policies.tsx +++ b/src/pages/Policies.tsx @@ -136,7 +136,9 @@ export class PoliciesInternal extends Component implements C const error = checkPolicyPath(this.state.policyPath); if (error) { - alert(error + "\nMust be either an absolute path, `user@host:/absolute/path`, `user@host` or `@host`. Use backslashes on Windows."); + alert( + `${error}\nMust be either an absolute path, \`user@host:/absolute/path\`, \`user@host\` or \`@host\`. Use backslashes on Windows.`, + ); return; } diff --git a/src/pages/SnapshotRestore.tsx b/src/pages/SnapshotRestore.tsx index ccf8cf10..32e0b946 100644 --- a/src/pages/SnapshotRestore.tsx +++ b/src/pages/SnapshotRestore.tsx @@ -57,9 +57,12 @@ interface SnapshotRestoreInternalState { minSizeForPlaceholder: number; restoreTask: string; destination?: string; -}; +} -export class SnapshotRestoreInternal extends Component implements ComponentChangeHandling { +export class SnapshotRestoreInternal + extends Component + implements ComponentChangeHandling +{ handleChange: ChangeEventHandle; constructor() { diff --git a/src/utils/formatutils.ts b/src/utils/formatutils.ts index 00ebb20b..6b55a2f0 100644 --- a/src/utils/formatutils.ts +++ b/src/utils/formatutils.ts @@ -59,7 +59,7 @@ export function objectLink(n: string) { return `/api/v1/objects/${n}`; } -export function formatOwnerName(s: { userName: string, host: string }): string { +export function formatOwnerName(s: { userName: string; host: string }): string { return `${s.userName}@${s.host}`; } @@ -208,7 +208,11 @@ export function formatMilliseconds(ms: number, useMultipleUnits = false): string ); } -export function formatDuration(from: undefined | null | number | string | Date, to?: number | string | Date, useMultipleUnits = false) { +export function formatDuration( + from: undefined | null | number | string | Date, + to?: number | string | Date, + useMultipleUnits = false, +) { if (!from) { return ""; } diff --git a/src/utils/policyutil.tsx b/src/utils/policyutil.tsx index 4b88f236..72c19f6b 100644 --- a/src/utils/policyutil.tsx +++ b/src/utils/policyutil.tsx @@ -72,7 +72,7 @@ export interface PolicyKey { userName?: string; host?: string; path?: string; -}; +} export function sourceQueryStringParams(src: PolicyKey) { // encodeURIComponent will in practice handle missing values too, but that is undefined behavior diff --git a/src/utils/uiutil.tsx b/src/utils/uiutil.tsx index 2cc4d1a9..0ea094a9 100644 --- a/src/utils/uiutil.tsx +++ b/src/utils/uiutil.tsx @@ -5,7 +5,7 @@ import { sizeDisplayName } from "./formatutils.js"; export function sizeWithFailures( size?: number, - summ?: { errors: { path: string; error: string; }[]; numFailed: number } | null, + summ?: { errors: { path: string; error: string }[]; numFailed: number } | null, bytesStringBase2?: boolean, ): "" | React.JSX.Element { if (size === undefined) { diff --git a/tests/components/SnapshotEstimation.test.tsx b/tests/components/SnapshotEstimation.test.tsx index b46f2f8b..c50d9f9e 100644 --- a/tests/components/SnapshotEstimation.test.tsx +++ b/tests/components/SnapshotEstimation.test.tsx @@ -9,7 +9,11 @@ import { setupAPIMock } from "../testutils/api-mocks.js"; import "@testing-library/jest-dom"; import { resetRouterMocks, updateRouterMocks } from "../testutils/react-router-mock.jsx"; import PropTypes from "prop-types"; -import { setupIntervalMocks, cleanupIntervalMocks, waitForLoadAndTriggerIntervals } from "../testutils/interval-mocks.js"; +import { + setupIntervalMocks, + cleanupIntervalMocks, + waitForLoadAndTriggerIntervals, +} from "../testutils/interval-mocks.js"; // Mock Logs component to avoid complex dependencies vi.mock("../../src/components/Logs", () => ({ diff --git a/tests/forms/forms.test.tsx b/tests/forms/forms.test.tsx index e3a3d276..2fd9812b 100644 --- a/tests/forms/forms.test.tsx +++ b/tests/forms/forms.test.tsx @@ -522,4 +522,4 @@ describe("Integrated Form Component", () => { expect(valueToNumber({ value: input })).toBe(expected); }); }); -}); \ No newline at end of file +}); diff --git a/tests/pages/SnapshotCreate.test.tsx b/tests/pages/SnapshotCreate.test.tsx index a7b4486b..e5dbac45 100644 --- a/tests/pages/SnapshotCreate.test.tsx +++ b/tests/pages/SnapshotCreate.test.tsx @@ -31,7 +31,7 @@ vi.mock("../../src/components/GoBackButton", () => ({ // Mock PolicyEditor with a simple implementation that tracks ref calls vi.mock("../../src/components/policy-editor/PolicyEditor", () => ({ - PolicyEditor: React.forwardRef((props: { embedded, path }, ref) => { + PolicyEditor: React.forwardRef((props: { embedded; path }, ref) => { React.useImperativeHandle(ref, () => ({ getAndValidatePolicy: vi.fn(() => ({ somePolicy: "data" })), })); diff --git a/tests/testutils/interval-mocks.ts b/tests/testutils/interval-mocks.ts index ce48faca..1592302f 100644 --- a/tests/testutils/interval-mocks.ts +++ b/tests/testutils/interval-mocks.ts @@ -3,7 +3,7 @@ import "@testing-library/jest-dom"; let intervalSpy; let clearIntervalSpy; -let intervalCallbacks: { id: number, callback, delay }[] = []; +let intervalCallbacks: { id: number; callback; delay }[] = []; let intervalId = 0; /** diff --git a/tests/testutils/react-router-mock.tsx b/tests/testutils/react-router-mock.tsx index fc33767d..cf7cecd9 100644 --- a/tests/testutils/react-router-mock.tsx +++ b/tests/testutils/react-router-mock.tsx @@ -210,11 +210,13 @@ export function resetRouterMocks() { * @param {Object} [state.params] - Params object to mock * @param {URLSearchParams|Object} [state.searchParams] - Search params to mock */ -export function updateRouterMocks(state: { - location?: typeof DEFAULT_STATE.location; - params?: Record; - searchParams?: URLSearchParams | Record | string; -} = {}) { +export function updateRouterMocks( + state: { + location?: typeof DEFAULT_STATE.location; + params?: Record; + searchParams?: URLSearchParams | Record | string; + } = {}, +) { if (state.location) { mockUseLocation.mockReturnValue({ ...DEFAULT_STATE.location, ...state.location }); } diff --git a/tests/utils/uiutil-components.test.tsx b/tests/utils/uiutil-components.test.tsx index 3e8ea277..6c35e978 100644 --- a/tests/utils/uiutil-components.test.tsx +++ b/tests/utils/uiutil-components.test.tsx @@ -4,7 +4,9 @@ import "@testing-library/jest-dom"; import { sizeWithFailures } from "../../src/utils/uiutil"; import { taskStatusSymbol } from "../../src/utils/taskutil"; -const sizeWithFailuresNoElement = new Error("sizeWithFailures did not return a React element, this is not expected behavior"); +const sizeWithFailuresNoElement = new Error( + "sizeWithFailures did not return a React element, this is not expected behavior", +); describe("sizeWithFailures", () => { it("returns empty string for undefined size", () => {