diff --git a/oeq-ts-rest-api/package-lock.json b/oeq-ts-rest-api/package-lock.json
index bb74c51d8b..6d19c4d9e9 100644
--- a/oeq-ts-rest-api/package-lock.json
+++ b/oeq-ts-rest-api/package-lock.json
@@ -18,7 +18,6 @@
"monocle-ts": "2.3.13",
"newtype-ts": "0.3.5",
"query-string": "^7.0.0",
- "runtypes": "6.7.0",
"tough-cookie": "^4.0.0"
},
"devDependencies": {
@@ -4844,11 +4843,6 @@
"queue-microtask": "^1.2.2"
}
},
- "node_modules/runtypes": {
- "version": "6.7.0",
- "resolved": "https://registry.npmjs.org/runtypes/-/runtypes-6.7.0.tgz",
- "integrity": "sha512-3TLdfFX8YHNFOhwHrSJza6uxVBmBrEjnNQlNXvXCdItS0Pdskfg5vVXUTWIN+Y23QR09jWpSl99UHkA83m4uWA=="
- },
"node_modules/semver": {
"version": "7.5.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
@@ -9031,11 +9025,6 @@
"queue-microtask": "^1.2.2"
}
},
- "runtypes": {
- "version": "6.7.0",
- "resolved": "https://registry.npmjs.org/runtypes/-/runtypes-6.7.0.tgz",
- "integrity": "sha512-3TLdfFX8YHNFOhwHrSJza6uxVBmBrEjnNQlNXvXCdItS0Pdskfg5vVXUTWIN+Y23QR09jWpSl99UHkA83m4uWA=="
- },
"semver": {
"version": "7.5.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
diff --git a/oeq-ts-rest-api/package.json b/oeq-ts-rest-api/package.json
index e90a37dc45..889916dae8 100644
--- a/oeq-ts-rest-api/package.json
+++ b/oeq-ts-rest-api/package.json
@@ -38,7 +38,6 @@
"monocle-ts": "2.3.13",
"newtype-ts": "0.3.5",
"query-string": "^7.0.0",
- "runtypes": "6.7.0",
"tough-cookie": "^4.0.0"
},
"devDependencies": {
diff --git a/oeq-ts-rest-api/src/Common.ts b/oeq-ts-rest-api/src/Common.ts
index bd177cf7ab..110bde56fc 100644
--- a/oeq-ts-rest-api/src/Common.ts
+++ b/oeq-ts-rest-api/src/Common.ts
@@ -17,7 +17,6 @@
*/
import { pipe } from 'fp-ts/function';
import * as t from 'io-ts';
-import { Literal, Union } from 'runtypes';
import {
BaseEntityCodec,
BaseEntitySummaryCodec,
@@ -109,19 +108,6 @@ export const isBaseEntitySummaryArray = (
): instance is BaseEntitySummary[] =>
pipe(instance, validate(t.array(BaseEntitySummaryCodec)));
-export const ItemStatuses = Union(
- Literal('ARCHIVED'),
- Literal('DELETED'),
- Literal('DRAFT'),
- Literal('LIVE'),
- Literal('MODERATING'),
- Literal('PERSONAL'),
- Literal('REJECTED'),
- Literal('REVIEW'),
- Literal('SUSPENDED')
-);
-
-// todo: fix this type alias which is not in sync with the runtype. Jira ticket: OEQ-1438
export type ItemStatus =
| 'ARCHIVED'
| 'DELETED'
diff --git a/oeq-ts-rest-api/src/Search.ts b/oeq-ts-rest-api/src/Search.ts
index d5226f831c..e9af1461e2 100644
--- a/oeq-ts-rest-api/src/Search.ts
+++ b/oeq-ts-rest-api/src/Search.ts
@@ -16,7 +16,6 @@
* limitations under the License.
*/
import { stringify } from 'query-string';
-import { Literal, Union } from 'runtypes';
import { GET, HEAD, POST } from './AxiosInstance';
import type { i18nString, ItemStatus } from './Common';
import { SearchResultCodec, SearchResultItemRawCodec } from './gen/Search';
@@ -28,17 +27,6 @@ import { convertDateFields, validate } from './Utils';
*/
export type Must = [string, string[]];
-export const SortOrderRunTypes = Union(
- Literal('rank'),
- Literal('datemodified'),
- Literal('datecreated'),
- Literal('name'),
- Literal('rating'),
- Literal('task_lastaction'),
- Literal('task_submitted')
-);
-
-// todo: fix this type alias which is not in sync with the runtype. Jira ticket: OEQ-1438
export type SortOrder =
| 'rank'
| 'datemodified'
diff --git a/oeq-ts-rest-api/src/SearchSettings.ts b/oeq-ts-rest-api/src/SearchSettings.ts
index 768e4095ac..403fe520a7 100644
--- a/oeq-ts-rest-api/src/SearchSettings.ts
+++ b/oeq-ts-rest-api/src/SearchSettings.ts
@@ -15,19 +15,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import { Literal, Union } from 'runtypes';
import { GET, PUT } from './AxiosInstance';
import { SettingsCodec } from './gen/SearchSettings';
import type { SortOrder } from './Search';
import { validate } from './Utils';
-export const ContentIndexLevelRunTypes = Union(
- Literal(0),
- Literal(1),
- Literal(2)
-);
-
-// todo: fix this type alias which is not in sync with the runtype. Jira ticket: OEQ-1438
export type ContentIndexLevel = 0 | 1 | 2;
export interface Settings {
diff --git a/oeq-ts-rest-api/src/WizardControl.ts b/oeq-ts-rest-api/src/WizardControl.ts
index 14e6a9f96b..44c5e8071f 100644
--- a/oeq-ts-rest-api/src/WizardControl.ts
+++ b/oeq-ts-rest-api/src/WizardControl.ts
@@ -15,7 +15,6 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import { Literal, Union } from 'runtypes';
import type { UuidString } from './Common';
import type { SelectionRestriction, TermStorageFormat } from './Taxonomy';
import type {
@@ -24,23 +23,6 @@ import type {
WizardDateFormat,
} from './WizardCommonTypes';
-/**
- * Runtypes definition for Wizard control type.
- */
-export const RuntypesControlType = Union(
- Literal('calendar'),
- Literal('checkboxgroup'),
- Literal('editbox'),
- Literal('html'),
- Literal('listbox'),
- Literal('radiogroup'),
- Literal('shufflebox'),
- Literal('shufflelist'),
- Literal('termselector'),
- Literal('userselector')
-);
-
-// todo: fix this type alias which is not in sync with the runtype. Jira ticket: OEQ-1438
/**
* Supported Wizard Control types.
*/
diff --git a/oeq-ts-rest-api/src/index.ts b/oeq-ts-rest-api/src/index.ts
index 8e8effc0f2..5b542a4890 100644
--- a/oeq-ts-rest-api/src/index.ts
+++ b/oeq-ts-rest-api/src/index.ts
@@ -15,6 +15,10 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+import * as CommonCodec from './gen/Common';
+import * as SearchCodec from './gen/Search';
+import * as WizardControlCodec from './gen/WizardControl';
+
export * as Acl from './Acl';
export * as AdvancedSearch from './AdvancedSearch';
export * as Auth from './Auth';
@@ -44,3 +48,9 @@ export * as UserQuery from './UserQuery';
export * as Utils from './Utils';
export * as WizardCommonTypes from './WizardCommonTypes';
export * as WizardControl from './WizardControl';
+
+export const Codec = {
+ Common: CommonCodec,
+ Search: SearchCodec,
+ WizardControl: WizardControlCodec,
+};
diff --git a/react-front-end/__mocks__/ACLRecipientModule.mock.ts b/react-front-end/__mocks__/ACLRecipientModule.mock.ts
index 556c48abea..e5a610d6a4 100644
--- a/react-front-end/__mocks__/ACLRecipientModule.mock.ts
+++ b/react-front-end/__mocks__/ACLRecipientModule.mock.ts
@@ -21,7 +21,7 @@ import {
LOGGED_IN_USER_ROLE_ID,
} from "../tsrc/modules/ACLRecipientModule";
-export const ownerRecipient = {
+export const ownerRecipient: ACLRecipient = {
expression: "$OWNER",
type: "$OWNER",
};
@@ -32,7 +32,7 @@ export const ownerRecipientWithName: ACLRecipient = {
name: ownerRecipientHumanReadableExpression,
};
-export const everyoneRecipient = {
+export const everyoneRecipient: ACLRecipient = {
expression: "*",
type: "*",
};
@@ -43,7 +43,7 @@ export const everyoneRecipientWithName: ACLRecipient = {
name: everyoneRecipientHumanReadableExpression,
};
-export const user100Recipient = {
+export const user100Recipient: ACLRecipient = {
expression: "20483af2-fe56-4499-a54b-8d7452156895",
type: "U",
};
@@ -56,16 +56,16 @@ export const user100RecipientWithName = {
name: user100RecipientHumanReadableExpression,
};
-export const user200Recipient = {
+export const user200Recipient: ACLRecipient = {
expression: "f9ec8b09-cf64-44ff-8a0a-08a8f2f9272a",
type: "U",
};
-export const user200RecipientWithName = {
+export const user200RecipientWithName: ACLRecipient = {
...user200Recipient,
name: "Racheal Carlyle [user200]",
};
-export const user300Recipient = {
+export const user300Recipient: ACLRecipient = {
expression: "eb75a832-6533-4d72-93f4-2b7a1b108951",
type: "U",
};
@@ -74,26 +74,26 @@ export const user300RecipientWithName = {
name: "Yasmin Day [user300]",
};
-export const user400Recipient = {
+export const user400Recipient: ACLRecipient = {
expression: "1c2ff1d0-9040-4985-a450-0ff6422ba5ef",
type: "U",
};
-export const user400RecipientWithName = {
+export const user400RecipientWithName: ACLRecipient = {
...user400Recipient,
name: "Ronny Southgate [user400]",
};
-export const userAdminRecipient = {
+export const userAdminRecipient: ACLRecipient = {
expression: "75abbd62-d91c-4ce5-b4b5-339e0d44ac0e",
type: "U",
};
-export const userContentAdminRecipient = {
+export const userContentAdminRecipient: ACLRecipient = {
expression: "2",
type: "U",
};
-export const roleGuestRecipient = {
+export const roleGuestRecipient: ACLRecipient = {
expression: GUEST_USER_ROLE_ID,
type: "R",
};
@@ -105,14 +105,14 @@ export const roleGuestRecipientWithName = {
};
export const LOGGED_IN_USER_ROLE_NAME = "Logged In User Role";
-export const roleLoggedRecipientWithName = {
+export const roleLoggedRecipientWithName: ACLRecipient = {
expression: LOGGED_IN_USER_ROLE_ID,
type: "R",
name: LOGGED_IN_USER_ROLE_NAME,
};
export const roleLoggedRecipientRawExpression = `R:${LOGGED_IN_USER_ROLE_ID}`;
-export const role100RecipientWithName = {
+export const role100RecipientWithName: ACLRecipient = {
expression: "fda99983-9eda-440a-ac68-0f746173fdcb",
type: "R",
name: "role100",
@@ -124,7 +124,7 @@ export const role200RecipientWithName = {
name: "role200",
};
-export const groupStudentRecipient = {
+export const groupStudentRecipient: ACLRecipient = {
expression: "99806ac8-410e-4c60-b3ab-22575276f0f0",
type: "G",
};
@@ -133,36 +133,36 @@ export const groupStudentRecipientRawExpression =
export const groupStudentRecipientHumanReadableExpression =
"Engineering & Computer Science Students";
-export const groupStaffRecipient = {
+export const groupStaffRecipient: ACLRecipient = {
expression: "d0265a33-8f89-4cea-8a36-45fd3c4cf5a1",
type: "G",
};
-export const group100RecipientWithName = {
+export const group100RecipientWithName: ACLRecipient = {
expression: "303e758c-0051-4aea-9a8e-421f93ed9d1a",
type: "G",
name: "group100",
};
-export const group200RecipientWithName = {
+export const group200RecipientWithName: ACLRecipient = {
expression: "d7dd1907-5731-4244-9a65-e0e847f68604",
type: "G",
name: "group200",
};
-export const group300RecipientWithName = {
+export const group300RecipientWithName: ACLRecipient = {
expression: "f921a6e3-69a6-4ec4-8cf8-bc193beda5f6",
type: "G",
name: "group300",
};
-export const group400RecipientWithName = {
+export const group400RecipientWithName: ACLRecipient = {
expression: "a2576dea-bd5c-490b-a065-637068e1a4fb",
type: "G",
name: "group400",
};
-export const ssoMoodleRecipient = {
+export const ssoMoodleRecipient: ACLRecipient = {
expression: "moodle",
type: "T",
};
@@ -174,26 +174,26 @@ export const ssoMoodleRecipientWithName = {
};
// helper function to generate an IP recipient
-export const ipRecipient = (ip: string) => ({
+export const ipRecipient = (ip: string): ACLRecipient => ({
expression: ip,
type: "I",
});
// helper function to generate an IP recipient with name
-export const ipRecipientWithName = (ip: string) => ({
+export const ipRecipientWithName = (ip: string): ACLRecipient => ({
expression: ip,
type: "I",
name: `From ${ip}`,
});
// helper function to generate an refer recipient
-export const referRecipient = (refer: string) => ({
+export const referRecipient = (refer: string): ACLRecipient => ({
expression: refer,
type: "F",
});
// helper function to generate a referrer recipient with name
-export const referRecipientWithName = (refer: string) => ({
+export const referRecipientWithName = (refer: string): ACLRecipient => ({
...referRecipient(refer),
name: `From ${decodeURIComponent(refer)}`,
});
diff --git a/react-front-end/__tests__/tsrc/components/aclexpressionbuilder/ACLExpressionBuilder.test.tsx b/react-front-end/__tests__/tsrc/components/aclexpressionbuilder/ACLExpressionBuilder.test.tsx
index a9dd4f5dd1..1505089616 100644
--- a/react-front-end/__tests__/tsrc/components/aclexpressionbuilder/ACLExpressionBuilder.test.tsx
+++ b/react-front-end/__tests__/tsrc/components/aclexpressionbuilder/ACLExpressionBuilder.test.tsx
@@ -114,7 +114,7 @@ describe("", () => {
// delete recipient
await clickDeleteButtonForRecipient(
container,
- user200RecipientWithName.name
+ user200RecipientWithName.name!
);
// click ok button to see if the result is what we want
await userEvent.click(getByText(okLabel));
@@ -247,14 +247,14 @@ describe("", () => {
[
"group",
groupsRadioLabel,
- group100RecipientWithName.name,
+ group100RecipientWithName.name!,
searchGroup,
"G:303e758c-0051-4aea-9a8e-421f93ed9d1a",
],
[
"role",
rolesRadioLabel,
- role100RecipientWithName.name,
+ role100RecipientWithName.name!,
searchRole,
"R:fda99983-9eda-440a-ac68-0f746173fdcb",
],
@@ -446,7 +446,7 @@ describe("", () => {
// click add button
await userEvent.click(getByText(addLabel));
// wait for adding action
- await findByText(ipRecipient.name);
+ await findByText(ipRecipient.name!);
// click ok button to check the result
await userEvent.click(getByText(okLabel));
@@ -486,7 +486,7 @@ describe("", () => {
// click add button
await userEvent.click(getByText(addLabel));
// wait for adding action
- await findByText(httpReferrerRecipient.name);
+ await findByText(httpReferrerRecipient.name!);
// click ok button to check the result
await userEvent.click(getByText(okLabel));
diff --git a/react-front-end/__tests__/tsrc/modules/BrowserStorageModule.test.ts b/react-front-end/__tests__/tsrc/modules/BrowserStorageModule.test.ts
index 51c9578bfe..8ab6d38f39 100644
--- a/react-front-end/__tests__/tsrc/modules/BrowserStorageModule.test.ts
+++ b/react-front-end/__tests__/tsrc/modules/BrowserStorageModule.test.ts
@@ -15,7 +15,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import { Boolean } from "runtypes";
+import * as t from "io-ts";
import {
readDataFromStorage,
saveDataToStorage,
@@ -25,7 +25,7 @@ const mockGetItem = jest.spyOn(Storage.prototype, "getItem");
const mockSetItem = jest
.spyOn(Storage.prototype, "setItem")
.mockImplementation(jest.fn);
-const booleanValidator = Boolean.guard;
+const booleanValidator = t.boolean.is;
const mockConsoleError = jest.spyOn(console, "error");
const KEY = "test";
diff --git a/react-front-end/__tests__/tsrc/modules/SearchFacetsModule.test.ts b/react-front-end/__tests__/tsrc/modules/SearchFacetsModule.test.ts
index 45905dd8c8..efc96832dd 100644
--- a/react-front-end/__tests__/tsrc/modules/SearchFacetsModule.test.ts
+++ b/react-front-end/__tests__/tsrc/modules/SearchFacetsModule.test.ts
@@ -104,7 +104,7 @@ describe("SearchFacetsModule", () => {
firstName: "Test",
lastName: "Owner",
},
- status: OEQ.Common.ItemStatuses.alternatives.map((i) => i.value), // i.e. All statuses
+ status: OEQ.Codec.Common.ItemStatusCodec.types.map(({ value }) => value), // i.e. All statuses
sortOrder: undefined,
mimeTypes: mimeTypes,
});
diff --git a/react-front-end/__tests__/tsrc/util/Either.extended.test.ts b/react-front-end/__tests__/tsrc/util/Either.extended.test.ts
new file mode 100644
index 0000000000..7c5ef8e0f7
--- /dev/null
+++ b/react-front-end/__tests__/tsrc/util/Either.extended.test.ts
@@ -0,0 +1,37 @@
+/*
+ * Licensed to The Apereo Foundation under one or more contributor license
+ * agreements. See the NOTICE file distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * The Apereo Foundation licenses this file to you under the Apache License,
+ * Version 2.0, (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { pipe } from "fp-ts/function";
+import * as E from "../../../tsrc/util/Either.extended";
+
+describe("Either Extended", () => {
+ describe("getOrThrow", () => {
+ it("should return the right value if Either is right", () => {
+ const rightValue = "Right Value";
+ const result = pipe(E.right(rightValue), E.getOrThrow);
+
+ expect(result).toEqual(rightValue);
+ });
+
+ it("should throw an error if Either is left", () => {
+ const leftValue = "Error Message";
+ expect(() => pipe(E.left(leftValue), E.getOrThrow)).toThrow(
+ new Error(leftValue)
+ );
+ });
+ });
+});
diff --git a/react-front-end/__tests__/tsrc/util/match.test.ts b/react-front-end/__tests__/tsrc/util/match.test.ts
index 5fb969c676..9c780a94b8 100644
--- a/react-front-end/__tests__/tsrc/util/match.test.ts
+++ b/react-front-end/__tests__/tsrc/util/match.test.ts
@@ -16,7 +16,11 @@
* limitations under the License.
*/
import { identity } from "fp-ts/function";
-import { simpleMatch, simpleMatchD } from "../../../tsrc/util/match";
+import {
+ simpleMatch,
+ simpleMatchD,
+ simpleUnionMatch,
+} from "../../../tsrc/util/match";
describe("simpleMatch", () => {
const unmatched = (s: string | number): string =>
@@ -63,3 +67,33 @@ describe("simpleMatchDynamic", () => {
expect(testMatchD(knownMatch)).toMatch(`${knownMatch}`);
});
});
+
+describe("simpleUnionTypeMatch", () => {
+ type TestUnion = "hello" | "world" | 1;
+ const cases: Partial string>> = {
+ hello: () => "hello",
+ 1: () => "1",
+ };
+ const defaultValue = "default";
+ const buildDefault = jest.fn().mockReturnValue(defaultValue);
+ const testMatch = simpleUnionMatch(cases, buildDefault);
+
+ it("executes the function matching the provided literal type", () => {
+ const knownMatch = "hello";
+ expect(testMatch(knownMatch)).toMatch(knownMatch);
+ });
+
+ it("executes the default function if the literal type does not have any function to be executed", () => {
+ const knownMatch = "world";
+ const result = testMatch(knownMatch);
+ expect(buildDefault).toHaveBeenCalledTimes(1);
+ expect(result).toMatch(defaultValue);
+ });
+
+ it("throws an error if the literal type does not have any function to be executed", () => {
+ const noMatcher = simpleUnionMatch(cases);
+ expect(() => noMatcher("world")).toThrow(
+ "Missing matcher for literal type: world"
+ );
+ });
+});
diff --git a/react-front-end/package-lock.json b/react-front-end/package-lock.json
index 3c2bfa36ad..97f2151745 100644
--- a/react-front-end/package-lock.json
+++ b/react-front-end/package-lock.json
@@ -26,6 +26,7 @@
"history": "4.10.1",
"html-react-parser": "3.0.16",
"io-ts": "2.2.20",
+ "io-ts-types": "0.5.19",
"jspolyfill-array.prototype.find": "0.1.3",
"lodash": "4.17.21",
"luxon": "3.4.2",
@@ -40,7 +41,6 @@
"react-router": "5.3.4",
"react-router-dom": "5.3.4",
"react-router-hash-link": "2.4.3",
- "runtypes": "6.7.0",
"shallow-equal-object": "1.1.1",
"sprintf-js": "1.1.2",
"tinymce": "6.7.0",
@@ -113,7 +113,6 @@
"monocle-ts": "2.3.13",
"newtype-ts": "0.3.5",
"query-string": "^7.0.0",
- "runtypes": "6.7.0",
"tough-cookie": "^4.0.0"
},
"devDependencies": {
@@ -4724,10 +4723,6 @@
"queue-microtask": "^1.2.2"
}
},
- "../oeq-ts-rest-api/node_modules/runtypes": {
- "version": "6.6.0",
- "license": "MIT"
- },
"../oeq-ts-rest-api/node_modules/semver": {
"version": "7.3.8",
"dev": true,
@@ -19781,6 +19776,17 @@
"fp-ts": "^2.5.0"
}
},
+ "node_modules/io-ts-types": {
+ "version": "0.5.19",
+ "resolved": "https://registry.npmjs.org/io-ts-types/-/io-ts-types-0.5.19.tgz",
+ "integrity": "sha512-kQOYYDZG5vKre+INIDZbLeDJe+oM+4zLpUkjXyTMyUfoCpjJNyi29ZLkuEAwcPufaYo3yu/BsemZtbdD+NtRfQ==",
+ "peerDependencies": {
+ "fp-ts": "^2.0.0",
+ "io-ts": "^2.0.0",
+ "monocle-ts": "^2.0.0",
+ "newtype-ts": "^0.3.2"
+ }
+ },
"node_modules/ip": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ip/-/ip-2.0.0.tgz",
@@ -23181,6 +23187,15 @@
"integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
"dev": true
},
+ "node_modules/monocle-ts": {
+ "version": "2.3.13",
+ "resolved": "https://registry.npmjs.org/monocle-ts/-/monocle-ts-2.3.13.tgz",
+ "integrity": "sha512-D5Ygd3oulEoAm3KuGO0eeJIrhFf1jlQIoEVV2DYsZUMz42j4tGxgct97Aq68+F8w4w4geEnwFa8HayTS/7lpKQ==",
+ "peer": true,
+ "peerDependencies": {
+ "fp-ts": "^2.5.0"
+ }
+ },
"node_modules/mri": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz",
@@ -23330,6 +23345,16 @@
"integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==",
"dev": true
},
+ "node_modules/newtype-ts": {
+ "version": "0.3.5",
+ "resolved": "https://registry.npmjs.org/newtype-ts/-/newtype-ts-0.3.5.tgz",
+ "integrity": "sha512-v83UEQMlVR75yf1OUdoSFssjitxzjZlqBAjiGQ4WJaML8Jdc68LJ+BaSAXUmKY4bNzp7hygkKLYTsDi14PxI2g==",
+ "peer": true,
+ "peerDependencies": {
+ "fp-ts": "^2.0.0",
+ "monocle-ts": "^2.0.0"
+ }
+ },
"node_modules/nice-try": {
"version": "1.0.5",
"dev": true,
@@ -27071,11 +27096,6 @@
"queue-microtask": "^1.2.2"
}
},
- "node_modules/runtypes": {
- "version": "6.7.0",
- "resolved": "https://registry.npmjs.org/runtypes/-/runtypes-6.7.0.tgz",
- "integrity": "sha512-3TLdfFX8YHNFOhwHrSJza6uxVBmBrEjnNQlNXvXCdItS0Pdskfg5vVXUTWIN+Y23QR09jWpSl99UHkA83m4uWA=="
- },
"node_modules/safe-buffer": {
"version": "5.1.2",
"dev": true,
@@ -33210,7 +33230,6 @@
"query-string": "^7.0.0",
"rollup": "3.29.0",
"rollup-plugin-typescript2": "0.35.0",
- "runtypes": "6.7.0",
"tough-cookie": "^4.0.0",
"ts-jest": "29.1.1",
"tslib": "2.6.2",
@@ -36129,9 +36148,6 @@
"queue-microtask": "^1.2.2"
}
},
- "runtypes": {
- "version": "6.6.0"
- },
"semver": {
"version": "7.3.8",
"dev": true,
@@ -43722,6 +43738,12 @@
"version": "2.2.20",
"requires": {}
},
+ "io-ts-types": {
+ "version": "0.5.19",
+ "resolved": "https://registry.npmjs.org/io-ts-types/-/io-ts-types-0.5.19.tgz",
+ "integrity": "sha512-kQOYYDZG5vKre+INIDZbLeDJe+oM+4zLpUkjXyTMyUfoCpjJNyi29ZLkuEAwcPufaYo3yu/BsemZtbdD+NtRfQ==",
+ "requires": {}
+ },
"ip": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ip/-/ip-2.0.0.tgz",
@@ -46090,6 +46112,13 @@
"integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
"dev": true
},
+ "monocle-ts": {
+ "version": "2.3.13",
+ "resolved": "https://registry.npmjs.org/monocle-ts/-/monocle-ts-2.3.13.tgz",
+ "integrity": "sha512-D5Ygd3oulEoAm3KuGO0eeJIrhFf1jlQIoEVV2DYsZUMz42j4tGxgct97Aq68+F8w4w4geEnwFa8HayTS/7lpKQ==",
+ "peer": true,
+ "requires": {}
+ },
"mri": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz",
@@ -46192,6 +46221,13 @@
"integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==",
"dev": true
},
+ "newtype-ts": {
+ "version": "0.3.5",
+ "resolved": "https://registry.npmjs.org/newtype-ts/-/newtype-ts-0.3.5.tgz",
+ "integrity": "sha512-v83UEQMlVR75yf1OUdoSFssjitxzjZlqBAjiGQ4WJaML8Jdc68LJ+BaSAXUmKY4bNzp7hygkKLYTsDi14PxI2g==",
+ "peer": true,
+ "requires": {}
+ },
"nice-try": {
"version": "1.0.5",
"dev": true
@@ -48888,11 +48924,6 @@
"queue-microtask": "^1.2.2"
}
},
- "runtypes": {
- "version": "6.7.0",
- "resolved": "https://registry.npmjs.org/runtypes/-/runtypes-6.7.0.tgz",
- "integrity": "sha512-3TLdfFX8YHNFOhwHrSJza6uxVBmBrEjnNQlNXvXCdItS0Pdskfg5vVXUTWIN+Y23QR09jWpSl99UHkA83m4uWA=="
- },
"safe-buffer": {
"version": "5.1.2",
"dev": true
diff --git a/react-front-end/package.json b/react-front-end/package.json
index 085c442080..4b7dd58ed6 100644
--- a/react-front-end/package.json
+++ b/react-front-end/package.json
@@ -49,6 +49,7 @@
"history": "4.10.1",
"html-react-parser": "3.0.16",
"io-ts": "2.2.20",
+ "io-ts-types": "0.5.19",
"jspolyfill-array.prototype.find": "0.1.3",
"lodash": "4.17.21",
"luxon": "3.4.2",
@@ -63,7 +64,6 @@
"react-router": "5.3.4",
"react-router-dom": "5.3.4",
"react-router-hash-link": "2.4.3",
- "runtypes": "6.7.0",
"shallow-equal-object": "1.1.1",
"sprintf-js": "1.1.2",
"tinymce": "6.7.0",
diff --git a/react-front-end/tsrc/components/IPv4CIDRInput.tsx b/react-front-end/tsrc/components/IPv4CIDRInput.tsx
index a93ff95605..e34d578e3d 100644
--- a/react-front-end/tsrc/components/IPv4CIDRInput.tsx
+++ b/react-front-end/tsrc/components/IPv4CIDRInput.tsx
@@ -15,30 +15,34 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+import * as t from "io-ts";
import { Grid, TextField } from "@mui/material";
import { styled } from "@mui/material/styles";
import * as A from "fp-ts/Array";
+import * as E from "fp-ts/Either";
import { constant, constFalse, pipe } from "fp-ts/function";
import * as O from "fp-ts/Option";
import { not } from "fp-ts/Predicate";
import * as S from "fp-ts/string";
import * as React from "react";
import { createRef, RefObject, useState, useRef } from "react";
-import { Literal, Union } from "runtypes";
import * as N from "fp-ts/number";
import * as NEA from "fp-ts/NonEmptyArray";
import * as RA from "fp-ts/ReadonlyArray";
import * as RNEA from "fp-ts/ReadonlyNonEmptyArray";
+import { simpleUnionMatch } from "../util/match";
/**
* Runtypes definition for key code which need to be handled here.
*/
-const KeyCodeTypesUnion = Union(
- Literal("Enter"),
- Literal("Backspace"),
- Literal("Period"),
- Literal("NumpadDecimal")
-);
+const KeyCodeTypesUnion = t.union([
+ t.literal("Enter"),
+ t.literal("Backspace"),
+ t.literal("Period"),
+ t.literal("NumpadDecimal"),
+]);
+
+type KeyCodeTypes = t.TypeOf;
const PREFIX = "IPv4CIDRInput";
const classes = {
@@ -234,30 +238,31 @@ const IPv4CIDRInput = ({ value = "", onChange }: IPv4CIDRInputProps) => {
) =>
pipe(
event.code,
- O.fromPredicate(KeyCodeTypesUnion.guard),
- O.map(
- KeyCodeTypesUnion.match(
- (Enter) => focusInput(index + 1),
- (Backspace) => {
+ KeyCodeTypesUnion.decode,
+ E.fold(
+ console.error,
+ simpleUnionMatch({
+ Enter: () => focusInput(index + 1),
+ Backspace: () => {
if (S.isEmpty(inputValue)) {
focusInput(index - 1);
}
},
- (Period) => {
+ Period: () => {
if (!S.isEmpty(inputValue)) {
focusInput(index + 1);
// key `Period` will trigger focus event on current input, thus prevent it.
event.preventDefault();
}
},
- (NumpadDecimal) => {
+ NumpadDecimal: () => {
if (!S.isEmpty(inputValue)) {
focusInput(index + 1);
// key `NumpadDecimal` will trigger focus event on current input.
event.preventDefault();
}
- }
- )
+ },
+ })
)
);
diff --git a/react-front-end/tsrc/components/aclexpressionbuilder/ACLHTTPReferrerInput.tsx b/react-front-end/tsrc/components/aclexpressionbuilder/ACLHTTPReferrerInput.tsx
index d8c1aeca57..791df504a5 100644
--- a/react-front-end/tsrc/components/aclexpressionbuilder/ACLHTTPReferrerInput.tsx
+++ b/react-front-end/tsrc/components/aclexpressionbuilder/ACLHTTPReferrerInput.tsx
@@ -15,9 +15,9 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+import * as t from "io-ts";
import { FormControlLabel, Radio, RadioGroup, TextField } from "@mui/material";
import * as React from "react";
-import { Literal, Static, Union } from "runtypes";
import { languageStrings } from "../../util/langstrings";
/**
@@ -25,8 +25,8 @@ import { languageStrings } from "../../util/langstrings";
* Contain: Match referrers containing this value.
* Exact: Only match this exact referrer.
*/
-const ReferrerTypesUnion = Union(Literal("Contain"), Literal("Exact"));
-export type ReferrerType = Static;
+const ReferrerTypesUnion = t.union([t.literal("Contain"), t.literal("Exact")]);
+export type ReferrerType = t.TypeOf;
const {
aclExpressionBuilder: {
diff --git a/react-front-end/tsrc/components/aclexpressionbuilder/ACLHomePanel.tsx b/react-front-end/tsrc/components/aclexpressionbuilder/ACLHomePanel.tsx
index 8992c1c48f..91c77d476f 100644
--- a/react-front-end/tsrc/components/aclexpressionbuilder/ACLHomePanel.tsx
+++ b/react-front-end/tsrc/components/aclexpressionbuilder/ACLHomePanel.tsx
@@ -15,6 +15,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+import * as t from "io-ts";
import {
FormControl,
FormControlLabel,
@@ -23,12 +24,12 @@ import {
RadioGroup,
Radio,
} from "@mui/material";
+import * as E from "../../util/Either.extended";
import * as React from "react";
import * as OEQ from "@openequella/rest-api-client";
import { pipe } from "fp-ts/function";
import * as RSET from "fp-ts/ReadonlySet";
import { ChangeEvent, useState } from "react";
-import { Literal, Static, Union } from "runtypes";
import {
ACLRecipient,
groupToRecipient,
@@ -37,6 +38,7 @@ import {
userToRecipient,
} from "../../modules/ACLRecipientModule";
import { languageStrings } from "../../util/langstrings";
+import { simpleUnionMatch } from "../../util/match";
import GroupSearch from "../securityentitysearch/GroupSearch";
import RoleSearch from "../securityentitysearch/RoleSearch";
import UserSearch from "../securityentitysearch/UserSearch";
@@ -44,13 +46,13 @@ import UserSearch from "../securityentitysearch/UserSearch";
/**
* Runtypes definition for home panel search filter type.
*/
-const SearchFilterTypesUnion = Union(
- Literal("Users"),
- Literal("Groups"),
- Literal("Roles")
-);
+const SearchFilterTypesUnion = t.union([
+ t.literal("Users"),
+ t.literal("Groups"),
+ t.literal("Roles"),
+]);
-type SearchFilterType = Static;
+type SearchFilterType = t.TypeOf;
const {
aclExpressionBuilder: { type: typeLabel },
@@ -99,7 +101,12 @@ const ACLHomePanel = ({
useState("Users");
const handleSearchFilterChange = (event: ChangeEvent) =>
- setActiveSearchFilterType(SearchFilterTypesUnion.check(event.target.value));
+ pipe(
+ event.target.value,
+ SearchFilterTypesUnion.decode,
+ E.getOrThrow,
+ setActiveSearchFilterType
+ );
const handleOnAdded = (
selections: ReadonlySet,
@@ -127,7 +134,7 @@ const ACLHomePanel = ({
value={activeSearchFilterType}
onChange={handleSearchFilterChange}
>
- {SearchFilterTypesUnion.alternatives.map((searchType) => (
+ {SearchFilterTypesUnion.types.map((searchType) => (
{pipe(
activeSearchFilterType,
- SearchFilterTypesUnion.match(
- (Users) => (
+ simpleUnionMatch({
+ Users: () => (
@@ -155,9 +162,9 @@ const ACLHomePanel = ({
showHelpText
/>
),
- (Groups) => (
+ Groups: () => (
@@ -169,9 +176,9 @@ const ACLHomePanel = ({
showHelpText
/>
),
- (Roles) => (
+ Roles: () => (
@@ -184,8 +191,8 @@ const ACLHomePanel = ({
groupFilterEditable={false}
showHelpText
/>
- )
- )
+ ),
+ })
)}
);
diff --git a/react-front-end/tsrc/components/aclexpressionbuilder/ACLOtherPanel.tsx b/react-front-end/tsrc/components/aclexpressionbuilder/ACLOtherPanel.tsx
index cbc119518e..27c227dd6d 100644
--- a/react-front-end/tsrc/components/aclexpressionbuilder/ACLOtherPanel.tsx
+++ b/react-front-end/tsrc/components/aclexpressionbuilder/ACLOtherPanel.tsx
@@ -15,6 +15,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+import * as t from "io-ts";
import {
Button,
FormControl,
@@ -25,13 +26,12 @@ import {
SelectChangeEvent,
Typography,
} from "@mui/material";
-import * as E from "fp-ts/Either";
+import * as E from "../../util/Either.extended";
import { flow, identity, pipe } from "fp-ts/function";
import * as O from "fp-ts/Option";
import * as TE from "fp-ts/TaskEither";
import * as React from "react";
import { ReactNode, useCallback, useState } from "react";
-import { Literal, Static, Union } from "runtypes";
import { ACLEntityResolvers } from "../../modules/ACLEntityModule";
import {
ACLRecipient,
@@ -45,6 +45,7 @@ import { findGroupById } from "../../modules/GroupModule";
import { findRoleById } from "../../modules/RoleModule";
import { findUserById } from "../../modules/UserModule";
import { languageStrings } from "../../util/langstrings";
+import { simpleUnionMatch } from "../../util/match";
import IPv4CIDRInput from "../IPv4CIDRInput";
import ACLHTTPReferrerInput from "./ACLHTTPReferrerInput";
import ACLSSOMenu from "./ACLSSOMenu";
@@ -76,16 +77,16 @@ const {
/**
* Runtypes definition for ACL expression types in `other` panel.
*/
-const OtherACLTypesUnion = Union(
- Literal("Everyone"),
- Literal("Owner"),
- Literal("Logged"),
- Literal("Guest"),
- Literal("SSO"),
- Literal("IP"),
- Literal("Referrer")
-);
-type OtherACLType = Static;
+const OtherACLTypesUnion = t.union([
+ t.literal("Everyone"),
+ t.literal("Owner"),
+ t.literal("Logged"),
+ t.literal("Guest"),
+ t.literal("SSO"),
+ t.literal("IP"),
+ t.literal("Referrer"),
+]);
+type OtherACLType = t.TypeOf;
export interface ACLOtherPanelProps {
/**
@@ -125,20 +126,30 @@ const ACLOtherPanel = ({
const [ssoToken, setSSOToken] = useState("");
const handleAclTypeChanged = (event: SelectChangeEvent) =>
- setActiveACLType(OtherACLTypesUnion.check(event.target.value));
+ pipe(
+ event.target.value,
+ OtherACLTypesUnion.decode,
+ E.getOrThrow,
+ setActiveACLType
+ );
const handleAddButtonClicked = async () => {
// generate raw ACL expression string for corresponding recipient type
- const generateExpression = OtherACLTypesUnion.match(
- (everyone) => ACLRecipientTypes.Everyone,
- (owner) => ACLRecipientTypes.Owner,
- (logged) => `${ACLRecipientTypes.Role}:${LOGGED_IN_USER_ROLE_ID}`,
- (guest) => `${ACLRecipientTypes.Role}:${GUEST_USER_ROLE_ID}`,
- (sso) => (ssoToken ? `${ACLRecipientTypes.Sso}:${ssoToken}` : undefined),
- (ip) => (ipAddress ? `${ACLRecipientTypes.Ip}:${ipAddress}` : undefined),
- (referrer) =>
- httpReferrer ? `${ACLRecipientTypes.Refer}:${httpReferrer}` : undefined
- );
+ const generateExpression = simpleUnionMatch<
+ OtherACLType,
+ string | undefined
+ >({
+ Everyone: () => ACLRecipientTypes.Everyone,
+ Owner: () => ACLRecipientTypes.Owner,
+ Logged: () => `${ACLRecipientTypes.Role}:${LOGGED_IN_USER_ROLE_ID}`,
+ Guest: () => `${ACLRecipientTypes.Role}:${GUEST_USER_ROLE_ID}`,
+ SSO: () =>
+ ssoToken ? `${ACLRecipientTypes.Sso}:${ssoToken}` : undefined,
+ IP: () =>
+ ipAddress ? `${ACLRecipientTypes.Ip}:${ipAddress}` : undefined,
+ Referrer: () =>
+ httpReferrer ? `${ACLRecipientTypes.Refer}:${httpReferrer}` : undefined,
+ });
// create an ACLRecipient object
const optionRecipient: O.Option = pipe(
@@ -174,15 +185,19 @@ const ACLOtherPanel = ({
);
- const buildSelections = OtherACLTypesUnion.match(
- (Everyone) => selection(Everyone, everyoneLabel),
- (Owner) => selection(Owner, ownerLabel),
- (Logged) => selection(Logged, loggedLabel),
- (Guest) => selection(Guest, guestLabel),
- (Sso) => selection(Sso, ssoLabel),
- (Ip) => selection(Ip, ipLabel),
- (Referrer) => selection(Referrer, referrerLabel)
- );
+ const buildSelections = (aclType: OtherACLType): React.JSX.Element => {
+ const getLabel = simpleUnionMatch({
+ Everyone: () => everyoneLabel,
+ Owner: () => ownerLabel,
+ Logged: () => loggedLabel,
+ Guest: () => guestLabel,
+ SSO: () => ssoLabel,
+ IP: () => ipLabel,
+ Referrer: () => referrerLabel,
+ });
+
+ return pipe(aclType, getLabel, (label) => selection(aclType, label));
+ };
const OtherControl = ({
title,
@@ -204,15 +219,15 @@ const ACLOtherPanel = ({
* TODO: Get all SSO selects from API
* */
const buildControls = useCallback(
- () =>
+ (): React.JSX.Element =>
pipe(
activeACLType,
- OtherACLTypesUnion.match(
- (Everyone) => ,
- (Owner) => ,
- (Logged) => ,
- (Guest) => ,
- (Sso) => (
+ simpleUnionMatch({
+ Everyone: () => ,
+ Owner: () => ,
+ Logged: () => ,
+ Guest: () => ,
+ SSO: () => (
),
- (Ip) => (
+ IP: () => (
),
- (Referrer) => (
+ Referrer: () => (
- )
- )
+ ),
+ })
),
[activeACLType, ssoTokensProvider]
);
@@ -246,7 +261,7 @@ const ACLOtherPanel = ({
onChange={handleAclTypeChanged}
label={typeLabel}
>
- {OtherACLTypesUnion.alternatives.map((aclType) =>
+ {OtherACLTypesUnion.types.map((aclType) =>
buildSelections(aclType.value)
)}
diff --git a/react-front-end/tsrc/components/wizard/WizardHelper.tsx b/react-front-end/tsrc/components/wizard/WizardHelper.tsx
index b63f11a584..47b289adc2 100644
--- a/react-front-end/tsrc/components/wizard/WizardHelper.tsx
+++ b/react-front-end/tsrc/components/wizard/WizardHelper.tsx
@@ -15,6 +15,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+import * as t from "io-ts";
import * as OEQ from "@openequella/rest-api-client";
import * as A from "fp-ts/Array";
import * as E from "fp-ts/Either";
@@ -39,16 +40,6 @@ import { first } from "fp-ts/Semigroup";
import * as SEP from "fp-ts/Separated";
import * as S from "fp-ts/string";
import * as React from "react";
-import {
- Array as RuntypeArray,
- Boolean,
- Number,
- Optional,
- Record,
- Static,
- String,
- Union,
-} from "runtypes";
import {
buildVisibilityScript,
ScriptContext,
@@ -103,40 +94,35 @@ export interface WizardControlBasicProps {
/**
* Runtypes definition for ControlTarget.
*/
-export const RuntypesControlTarget = Record({
- /**
- * The 'fullPath's for the targetNode.
- */
- schemaNode: RuntypeArray(String),
- /**
- * The type of control that is being targeted.
- */
- type: OEQ.WizardControl.RuntypesControlType,
- /**
- * Whether to tokenise the value.
- */
- isValueTokenised: Optional(Boolean),
-});
+export const ControlTargetCodec = t.intersection([
+ t.type({
+ schemaNode: t.array(t.string),
+ type: OEQ.Codec.WizardControl.ControlTypeCodec,
+ }),
+ t.partial({
+ isValueTokenised: t.boolean,
+ }),
+]);
/**
* Used to loosely target what a value (typically a `ControlValue`) is being used for.
*/
-export type ControlTarget = Static;
+export type ControlTarget = t.TypeOf;
/**
* Runtypes definition for ControlValue.
*/
-export const RuntypesControlValue = Union(
- RuntypeArray(String),
- RuntypeArray(Number)
-);
+export const ControlValueCodec = t.union([
+ t.array(t.string),
+ t.array(t.number),
+]);
/**
* Convenience type for our way of storing the two main value types across our controls. Represents
* that some controls are textual, and some are numeric; and that some controls store more than one
* value.
*/
-export type ControlValue = Static;
+export type ControlValue = t.TypeOf;
/**
* Identifies a Wizard 'field' and specifies its value.
diff --git a/react-front-end/tsrc/modules/ACLExpressionModule.ts b/react-front-end/tsrc/modules/ACLExpressionModule.ts
index 240e0f9253..eb4d917582 100644
--- a/react-front-end/tsrc/modules/ACLExpressionModule.ts
+++ b/react-front-end/tsrc/modules/ACLExpressionModule.ts
@@ -15,6 +15,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+import * as t from "io-ts";
import * as A from "fp-ts/Array";
import * as E from "fp-ts/Either";
import { constant, flow, identity, pipe } from "fp-ts/function";
@@ -28,9 +29,9 @@ import { ReadonlyNonEmptyArray } from "fp-ts/ReadonlyNonEmptyArray";
import * as S from "fp-ts/string";
import * as T from "fp-ts/Task";
import * as TE from "fp-ts/TaskEither";
-import { Literal, Static, Union } from "runtypes";
import { v4 as uuidv4 } from "uuid";
import { languageStrings } from "../util/langstrings";
+import { simpleUnionMatch } from "../util/match";
import { pfTernary, pfTernaryTypeGuard } from "../util/pointfree";
import { ACLEntityResolversMulti, ACLEntityResolvers } from "./ACLEntityModule";
import {
@@ -54,21 +55,21 @@ const {
*
* `UNKNOWN` is only used as a placeholder while parsing an ACL recipient string.
*/
-export const ACLOperatorTypesUnion = Union(
- Literal("OR"),
- Literal("AND"),
- Literal("NOT"),
- Literal("UNKNOWN")
-);
+export const ACLOperatorTypesUnion = t.union([
+ t.literal("OR"),
+ t.literal("AND"),
+ t.literal("NOT"),
+ t.literal("UNKNOWN"),
+]);
-export type ACLOperatorType = Static;
+export type ACLOperatorType = t.TypeOf;
-export const getOperatorLabel = ACLOperatorTypesUnion.match(
- (OR) => orLabel,
- (AND) => andLabel,
- (NOT) => notLabel,
- (UNKNOWN) => UNKNOWN
-);
+export const getOperatorLabel = simpleUnionMatch({
+ OR: () => orLabel,
+ AND: () => andLabel,
+ NOT: () => notLabel,
+ UNKNOWN: () => "UNKNOWN",
+});
/**
* Represents the ACL Expression string.
@@ -256,7 +257,7 @@ const getChildrenCount = (expression: ACLExpression): number =>
* Tells whether the provide piece of text is an ACL Operator.
*/
const isACLOperator = (text: string): text is ACLOperatorType =>
- ACLOperatorTypesUnion.guard(text);
+ ACLOperatorTypesUnion.is(text);
/**
* Merge a given base ACL Expression with a new ACL Expression.
diff --git a/react-front-end/tsrc/modules/ACLRecipientModule.ts b/react-front-end/tsrc/modules/ACLRecipientModule.ts
index 2f43eca51f..ccfa971b0f 100644
--- a/react-front-end/tsrc/modules/ACLRecipientModule.ts
+++ b/react-front-end/tsrc/modules/ACLRecipientModule.ts
@@ -15,6 +15,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+import * as t from "io-ts";
import * as OEQ from "@openequella/rest-api-client";
import * as E from "fp-ts/Either";
import * as EQ from "fp-ts/Eq";
@@ -24,9 +25,22 @@ import * as ORD from "fp-ts/Ord";
import * as RNEA from "fp-ts/ReadonlyNonEmptyArray";
import * as S from "fp-ts/string";
import * as TE from "fp-ts/TaskEither";
-import { Literal, Static, Union } from "runtypes";
+import { simpleUnionMatch } from "../util/match";
import { ACLEntityResolvers } from "./ACLEntityModule";
+const ACLRecipientTypesUnion = t.union([
+ t.literal("U"),
+ t.literal("G"),
+ t.literal("R"),
+ t.literal("*"),
+ t.literal("$OWNER"),
+ t.literal("F"),
+ t.literal("I"),
+ t.literal("T"),
+]);
+
+export type ACLRecipientType = t.TypeOf;
+
/**
* ACL Recipient types:
* * user: U:xxx-xxx-xxx
@@ -38,7 +52,7 @@ import { ACLEntityResolvers } from "./ACLEntityModule";
* * IP: I:255.255.0.0%2F24
* * Sso: T:xxx
*/
-export const ACLRecipientTypes = {
+export const ACLRecipientTypes: { [prefix: string]: ACLRecipientType } = {
User: "U",
Group: "G",
Role: "R",
@@ -49,19 +63,6 @@ export const ACLRecipientTypes = {
Sso: "T",
};
-const ACLRecipientTypesUnion = Union(
- Literal(ACLRecipientTypes.User),
- Literal(ACLRecipientTypes.Group),
- Literal(ACLRecipientTypes.Role),
- Literal(ACLRecipientTypes.Everyone),
- Literal(ACLRecipientTypes.Owner),
- Literal(ACLRecipientTypes.Refer),
- Literal(ACLRecipientTypes.Ip),
- Literal(ACLRecipientTypes.Sso)
-);
-
-export type ACLRecipientType = Static;
-
/**
* Represents a recipient in the ACL expression.
*
@@ -162,13 +163,11 @@ const generateACLRecipientName =
resolveGroupProvider,
resolveRoleProvider,
}: ACLEntityResolvers) =>
- (recipient: ACLRecipient): TE.TaskEither => {
- const { type, expression } = recipient;
-
- return pipe(
+ ({ type, expression }: ACLRecipient): TE.TaskEither =>
+ pipe(
type,
- ACLRecipientTypesUnion.match(
- (User) =>
+ simpleUnionMatch>({
+ U: () =>
pipe(
TE.tryCatch(
() => resolveUserProvider(expression),
@@ -183,7 +182,7 @@ const generateACLRecipientName =
)
)
),
- (Group) =>
+ G: () =>
pipe(
TE.tryCatch(
() => resolveGroupProvider(expression),
@@ -193,7 +192,7 @@ const generateACLRecipientName =
(g: OEQ.UserQuery.GroupDetails | undefined) => g?.name
)
),
- (Role) =>
+ R: () =>
pipe(
TE.tryCatch(
() => resolveRoleProvider(expression),
@@ -203,14 +202,13 @@ const generateACLRecipientName =
(r: OEQ.UserQuery.RoleDetails | undefined) => r?.name
)
),
- (Everyone) => TE.right("Everyone"),
- ($OWNER) => TE.right("Owner"),
- (Ip) => TE.right("From " + decodeURIComponent(expression)),
- (Refer) => TE.right("From " + expression),
- (Sso) => TE.right("Token ID is " + expression)
- )
+ "*": () => TE.right("Everyone"),
+ $OWNER: () => TE.right("Owner"),
+ I: () => TE.right("From " + expression),
+ F: () => TE.right("From " + decodeURIComponent(expression)),
+ T: () => TE.right("Token ID is " + expression),
+ })
);
- };
/**
* Show the full raw expression string for an ACL Recipient.
@@ -245,7 +243,7 @@ const parseACLRecipientType = (
S.split(":"),
RNEA.head,
E.fromPredicate(
- ACLRecipientTypesUnion.guard,
+ ACLRecipientTypesUnion.is,
(invalid) => `Failed to parse recipient: ${invalid}`
)
);
diff --git a/react-front-end/tsrc/modules/LegacyContentModule.ts b/react-front-end/tsrc/modules/LegacyContentModule.ts
index f16f42ee95..e9a6ee9254 100644
--- a/react-front-end/tsrc/modules/LegacyContentModule.ts
+++ b/react-front-end/tsrc/modules/LegacyContentModule.ts
@@ -15,28 +15,28 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+import * as t from "io-ts";
import Axios from "axios";
-import { Literal, Static, Union } from "runtypes";
import { API_BASE_URL } from "../AppConfig";
import type { ChangeRoute } from "../legacycontent/LegacyContent";
import type { ScrapbookType } from "./ScrapbookModule";
const legacyMyResourcesUrl = `${API_BASE_URL}/content/submit/access/myresources.do`;
-export const ScrapbookLiteral = Literal("scrapbook");
-export const ModQueueLiteral = Literal("modqueue");
+export const ScrapbookLiteral = t.literal("scrapbook");
+export const ModQueueLiteral = t.literal("modqueue");
-export const LegacyMyResourcesRuntypes = Union(
- Literal("published"),
- Literal("draft"),
+export const LegacyMyResourcesCodec = t.union([
+ t.literal("published"),
+ t.literal("draft"),
ScrapbookLiteral,
ModQueueLiteral,
- Literal("archived"),
- Literal("all"),
- Literal("defaultValue")
-);
+ t.literal("archived"),
+ t.literal("all"),
+ t.literal("defaultValue"),
+]);
-export type LegacyMyResourcesTypes = Static;
+export type LegacyMyResourcesTypes = t.TypeOf;
/**
* Send a Legacy content request to trigger server side event 'contributeFromNewUI', which will
* return a route for accessing the Legacy Scrapbook creating page. To support the requirement
diff --git a/react-front-end/tsrc/modules/SearchFacetsModule.ts b/react-front-end/tsrc/modules/SearchFacetsModule.ts
index c30b8330bd..cfed3d3998 100644
--- a/react-front-end/tsrc/modules/SearchFacetsModule.ts
+++ b/react-front-end/tsrc/modules/SearchFacetsModule.ts
@@ -105,7 +105,7 @@ const convertSearchOptions: (
owner: owner?.id,
showall: isEqual(
status?.sort(),
- OEQ.Common.ItemStatuses.alternatives.map((i) => i.value).sort()
+ OEQ.Codec.Common.ItemStatusCodec.types.map(({ value }) => value).sort()
),
mimeTypes,
musts,
diff --git a/react-front-end/tsrc/modules/SearchModule.ts b/react-front-end/tsrc/modules/SearchModule.ts
index c930c0cc8f..95b0993764 100644
--- a/react-front-end/tsrc/modules/SearchModule.ts
+++ b/react-front-end/tsrc/modules/SearchModule.ts
@@ -16,14 +16,14 @@
* limitations under the License.
*/
+import * as t from "io-ts";
import * as OEQ from "@openequella/rest-api-client";
import * as A from "fp-ts/Array";
import * as E from "fp-ts/Either";
-import { flow, pipe } from "fp-ts/function";
+import { flow, pipe, constFalse } from "fp-ts/function";
import * as NEA from "fp-ts/NonEmptyArray";
import * as O from "fp-ts/Option";
import * as TE from "fp-ts/TaskEither";
-import { Literal, Static, Union } from "runtypes";
import { API_BASE_URL } from "../AppConfig";
import { DateRange, getISODateString } from "../util/Date";
import type { Collection } from "./CollectionsModule";
@@ -50,41 +50,39 @@ export const nonLiveStatus = (status: OEQ.Common.ItemStatus): boolean =>
* List of statuses which are considered non-live.
*/
export const nonLiveStatuses: OEQ.Common.ItemStatus[] =
- OEQ.Common.ItemStatuses.alternatives
- .map((status) => status.value)
+ OEQ.Codec.Common.ItemStatusCodec.types
+ .map(({ value }) => value)
.filter(nonLiveStatus);
/**
* All statuses except "DELETED".
*/
export const nonDeletedStatuses: OEQ.Common.ItemStatus[] =
- OEQ.Common.ItemStatuses.alternatives
- .map((status) => status.value)
+ OEQ.Codec.Common.ItemStatusCodec.types
+ .map(({ value }) => value)
.filter((status) => status !== "DELETED");
/**
* Function to check if the supplied SearchResultItem refers to a live Item.
+ * Item status returned from 'search2' is a lowercase string so convert it to uppercase.
*/
-export const isLiveItem = (item: OEQ.Search.SearchResultItem): boolean => {
- // Item status returned from 'search2' is a lowercase string so convert it to uppercase.
- const status = item.status.toUpperCase();
- return OEQ.Common.ItemStatuses.guard(status) && liveStatuses.includes(status);
-};
+export const isLiveItem = (item: OEQ.Search.SearchResultItem): boolean =>
+ pipe(
+ OEQ.Codec.Common.ItemStatusCodec.decode(item.status.toUpperCase()),
+ E.fold(constFalse, (s) => liveStatuses.includes(s))
+ );
-/**
- * A Runtypes object which represents three display modes: list, gallery-image and gallery-video.
- */
-export const DisplayModeRuntypes = Union(
- Literal("list"),
- Literal("gallery-image"),
- Literal("gallery-video")
-);
+export const DisplayModeCodec = t.union([
+ t.literal("list"),
+ t.literal("gallery-image"),
+ t.literal("gallery-video"),
+]);
/**
* Available modes for displaying search results.
* @see { @link DisplayModeRuntypes } for original definition.
*/
-export type DisplayMode = Static;
+export type DisplayMode = t.TypeOf;
/**
* Type of all search options on Search page
diff --git a/react-front-end/tsrc/myresources/MyResourcesPageHelper.tsx b/react-front-end/tsrc/myresources/MyResourcesPageHelper.tsx
index 17fde72556..b0f0468c6c 100644
--- a/react-front-end/tsrc/myresources/MyResourcesPageHelper.tsx
+++ b/react-front-end/tsrc/myresources/MyResourcesPageHelper.tsx
@@ -32,15 +32,12 @@ import { Location } from "history";
import { MD5 } from "object-hash";
import * as React from "react";
import { ReactNode } from "react";
-import { Literal, match, Static, Union, Unknown, when } from "runtypes";
import { getBaseUrl } from "../AppConfig";
import { TooltipIconButton } from "../components/TooltipIconButton";
import { buildStorageKey } from "../modules/BrowserStorageModule";
import {
- LegacyMyResourcesRuntypes,
+ LegacyMyResourcesCodec,
LegacyMyResourcesTypes,
- ModQueueLiteral,
- ScrapbookLiteral,
} from "../modules/LegacyContentModule";
import { getMimeTypeDefaultViewerDetails } from "../modules/MimeTypesModule";
import {
@@ -59,27 +56,27 @@ import { SortOrderOptions } from "../search/components/SearchOrderSelect";
import SearchResult from "../search/components/SearchResult";
import {
DehydratedSearchPageOptions,
- DehydratedSearchPageOptionsRunTypes,
+ DehydratedSearchPageOptionsCodec,
SearchPageOptions,
} from "../search/SearchPageHelper";
import { SearchPageSearchResult } from "../search/SearchPageReducer";
import { DateRangeFromString, getISODateString } from "../util/Date";
import { languageStrings } from "../util/langstrings";
-import { simpleMatch } from "../util/match";
+import { simpleMatch, simpleUnionMatch } from "../util/match";
import { pfSplitAt, pfTernaryTypeGuard } from "../util/pointfree";
export const PARAM_MYRESOURCES_TYPE = "myResourcesType";
-export const MyResourcesTypeRuntypes = Union(
- Literal("Published"),
- Literal("Drafts"),
- Literal("Scrapbook"),
- Literal("Moderation queue"),
- Literal("Archive"),
- Literal("All resources")
-);
+export const MyResourcesTypeRuntypes = t.union([
+ t.literal("Published"),
+ t.literal("Drafts"),
+ t.literal("Scrapbook"),
+ t.literal("Moderation queue"),
+ t.literal("Archive"),
+ t.literal("All resources"),
+]);
-export type MyResourcesType = Static;
+export type MyResourcesType = t.TypeOf;
/**
* Return a list of Item status that match the given MyResources type.
@@ -134,7 +131,7 @@ const getLegacyMyResourceType = (
O.chainEitherK(
flow(
E.fromPredicate(
- LegacyMyResourcesRuntypes.guard,
+ LegacyMyResourcesCodec.is,
(value) => `Invalid legacy my resources type: ${value}`
),
E.mapLeft(console.error)
@@ -175,7 +172,7 @@ const getSearchOptionsFromQueryParam = (
t.UnknownRecord.decode, // Type of the parsed result should be a record where keys and values are unknown.
E.map(processLastModifiedDateRange),
E.filterOrElseW(
- DehydratedSearchPageOptionsRunTypes.guard,
+ DehydratedSearchPageOptionsCodec.is,
constant(
"Parsed searchOptions is not a DehydratedSearchPageOptions - failed type check"
)
@@ -203,15 +200,15 @@ const getMyResourcesTypeFromLegacyQueryParam = (
params,
getLegacyMyResourceType,
O.map(
- LegacyMyResourcesRuntypes.match(
- (published) => "Published",
- (draft) => "Drafts",
- (scrapbook) => "Scrapbook",
- (modqueue) => "Moderation queue",
- (archived) => "Archive",
- (all) => "All resources",
- (defaultValue) => "Published"
- )
+ simpleUnionMatch({
+ published: constant("Published"),
+ draft: constant("Drafts"),
+ scrapbook: constant("Scrapbook"),
+ modqueue: constant("Moderation queue"),
+ archived: constant("Archive"),
+ all: constant("All resources"),
+ defaultValue: constant("Published"),
+ })
)
);
@@ -221,7 +218,7 @@ const getMyResourcesTypeFromNewUIQueryParam = (
): O.Option =>
pipe(
params.get(PARAM_MYRESOURCES_TYPE),
- O.fromPredicate(MyResourcesTypeRuntypes.guard)
+ O.fromPredicate(MyResourcesTypeRuntypes.is)
);
/**
@@ -261,7 +258,7 @@ const getSubStatusFromLegacyQueryParam = (
O.chainEitherK(
flow(
E.fromPredicate(
- OEQ.Common.ItemStatuses.guard,
+ OEQ.Codec.Common.ItemStatusCodec.is,
(value) => `Invalid Item status: ${value}`
),
E.mapLeft(console.error)
@@ -309,10 +306,12 @@ const getSortOrderFromLegacyQueryParam = (
params,
getLegacyMyResourceType,
O.map(
- match(
- when(ScrapbookLiteral, constant("sbsort")),
- when(ModQueueLiteral, constant("modsort")),
- when(Unknown, constant("sort"))
+ simpleUnionMatch(
+ {
+ modqueue: constant("modsort"),
+ scrapbook: constant("sbsort"),
+ },
+ constant("sort")
)
),
O.chain((queryString) => pipe(params.get(queryString), O.fromNullable)),
@@ -327,7 +326,7 @@ const getSortOrderFromLegacyQueryParam = (
O.chainEitherK(
flow(
E.fromPredicate(
- OEQ.Search.SortOrderRunTypes.guard,
+ OEQ.Codec.Search.SortOrderCodec.is,
(value) => `Invalid sort order: ${value}`
),
E.mapLeft(console.error)
diff --git a/react-front-end/tsrc/search/SearchPageHelper.ts b/react-front-end/tsrc/search/SearchPageHelper.ts
index 2c910fbbd6..e3e3f2a495 100644
--- a/react-front-end/tsrc/search/SearchPageHelper.ts
+++ b/react-front-end/tsrc/search/SearchPageHelper.ts
@@ -15,6 +15,8 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+import * as t from "io-ts";
+import * as td from "io-ts-types";
import * as OEQ from "@openequella/rest-api-client";
import * as A from "fp-ts/Array";
import * as E from "fp-ts/Either";
@@ -27,28 +29,13 @@ import * as TO from "fp-ts/TaskOption";
import { History, Location } from "history";
import { pick } from "lodash";
import { createContext } from "react";
-import {
- Array as RuntypeArray,
- Boolean,
- Guard,
- Literal,
- match,
- Number,
- Partial,
- Record,
- Static,
- String,
- Tuple,
- Union,
- Unknown,
-} from "runtypes";
import type {
FieldValueMap,
PathValueMap,
} from "../components/wizard/WizardHelper";
import {
- RuntypesControlTarget,
- RuntypesControlValue,
+ ControlTargetCodec,
+ ControlValueCodec,
} from "../components/wizard/WizardHelper";
import { NEW_SEARCH_PATH, routes } from "../mainui/routes";
import {
@@ -60,7 +47,7 @@ import {
Collection,
findCollectionsByUuid,
} from "../modules/CollectionsModule";
-import { LegacyMyResourcesRuntypes } from "../modules/LegacyContentModule";
+import { LegacyMyResourcesCodec } from "../modules/LegacyContentModule";
import {
buildSelectionSessionItemSummaryLink,
buildSelectionSessionSearchPageLink,
@@ -70,12 +57,12 @@ import { getMimeTypeFiltersById } from "../modules/SearchFilterSettingsModule";
import {
defaultSearchOptions,
DisplayMode,
- DisplayModeRuntypes,
+ DisplayModeCodec,
SearchOptions,
SearchOptionsFields,
} from "../modules/SearchModule";
import { findUserById } from "../modules/UserModule";
-import { DateRange, isDate } from "../util/Date";
+import { DateRange } from "../util/Date";
import { languageStrings } from "../util/langstrings";
import { simpleMatch } from "../util/match";
import { pfSlice, pfTernary } from "../util/pointfree";
@@ -341,52 +328,47 @@ export const defaultPagedSearchResult: OEQ.Search.SearchResult;
+const LegacySearchParamsCodec = t.union([
+ t.literal("dp"),
+ t.literal("ds"),
+ t.literal("dr"),
+ t.literal("q"),
+ t.literal("sort"),
+ t.literal("owner"),
+ t.literal("in"),
+ t.literal("mt"),
+ t.literal("_int.mimeTypes"),
+ t.literal("type"),
+ t.literal("doc"),
+]);
+
+type LegacyParams = t.TypeOf;
/**
* Represents the shape of data returned from generateQueryStringFromSearchOptions
*/
-export const DehydratedSearchPageOptionsRunTypes = Partial({
- query: String,
- rowsPerPage: Number,
- currentPage: Number,
- sortOrder: OEQ.Search.SortOrderRunTypes,
- collections: RuntypeArray(Record({ uuid: String })),
- rawMode: Boolean,
- lastModifiedDateRange: Partial({ start: Guard(isDate), end: Guard(isDate) }),
- owner: Record({ id: String }),
- status: RuntypeArray(OEQ.Common.ItemStatuses),
- selectedCategories: RuntypeArray(
- Record({
- id: Number,
- categories: RuntypeArray(String),
- })
- ),
- searchAttachments: Boolean,
- mimeTypeFilters: RuntypeArray(Record({ id: String })),
- displayMode: DisplayModeRuntypes,
- dateRangeQuickModeEnabled: Boolean,
- advFieldValue: RuntypeArray(
- Tuple(RuntypesControlTarget, RuntypesControlValue)
+export const DehydratedSearchPageOptionsCodec = t.partial({
+ query: t.string,
+ rowsPerPage: t.number,
+ currentPage: t.number,
+ sortOrder: OEQ.Codec.Search.SortOrderCodec,
+ collections: t.array(t.type({ uuid: t.string })),
+ rawMode: t.boolean,
+ lastModifiedDateRange: t.partial({ start: td.date, end: td.date }),
+ owner: t.type({ id: t.string }),
+ status: t.array(OEQ.Codec.Common.ItemStatusCodec),
+ selectedCategories: t.array(
+ t.type({ id: t.number, categories: t.array(t.string) })
),
+ searchAttachments: t.boolean,
+ mimeTypeFilters: t.array(t.type({ id: t.string })),
+ displayMode: DisplayModeCodec,
+ dateRangeQuickModeEnabled: t.boolean,
+ advFieldValue: t.array(t.tuple([ControlTargetCodec, ControlValueCodec])),
});
-export type DehydratedSearchPageOptions = Static<
- typeof DehydratedSearchPageOptionsRunTypes
+export type DehydratedSearchPageOptions = t.TypeOf<
+ typeof DehydratedSearchPageOptionsCodec
>;
/**
@@ -410,9 +392,7 @@ export const generateSearchPageOptionsFromQueryString = async (
// For all else, return `undefined`.
if (searchPageOptions) {
return await newSearchQueryToSearchPageOptions(searchPageOptions);
- } else if (
- Array.from(params.keys()).some((key) => LegacySearchParams.guard(key))
- ) {
+ } else if (Array.from(params.keys()).some(LegacySearchParamsCodec.is)) {
return await legacyQueryStringToSearchPageOptions(params);
}
return undefined;
@@ -435,28 +415,18 @@ export const generateQueryStringFromSearchPageOptions = (
"searchOptions",
JSON.stringify(
searchPageOptions,
- (key: string, value: object[] | undefined) => {
- return match(
- [
- Literal("collections"),
- () => value?.map((collection) => pick(collection, ["uuid"])),
- ],
- [Literal("owner"), () => (value ? pick(value, ["id"]) : undefined)],
- [
- Literal("mimeTypeFilters"),
- () => value?.map((filter) => pick(filter, ["id"])),
- ],
- // As we can get MIME types from filters, we can skip key "mimeTypes".
- [Literal("mimeTypes"), () => undefined],
- // Skip advancedSearchCriteria as we can build it from `advFieldValue`.
- [Literal("advancedSearchCriteria"), () => undefined],
- [
- Literal("advFieldValue"),
- () => pipe(value, O.fromNullable, O.map(Array.from), O.toUndefined),
- ],
- [Unknown, () => value ?? undefined]
- )(key);
- }
+ (key: string, value: object[] | undefined) =>
+ simpleMatch({
+ collections: () =>
+ value?.map((collection) => pick(collection, ["uuid"])),
+ owner: () => (value ? pick(value, ["id"]) : undefined),
+ mimeTypeFilters: () => value?.map((filter) => pick(filter, ["id"])),
+ mimeTypes: () => undefined, // As we can get MIME types from filters, we can skip key "mimeTypes".
+ advancedSearchCriteria: () => undefined, // Skip advancedSearchCriteria as we can build it from `advFieldValue`.
+ advFieldValue: () =>
+ pipe(value, O.fromNullable, O.map(Array.from), O.toUndefined),
+ _: () => value ?? undefined,
+ })(key)
)
);
return params.toString();
@@ -511,7 +481,7 @@ export const newSearchQueryToSearchPageOptions = async (
return value;
});
- if (!DehydratedSearchPageOptionsRunTypes.guard(parsedOptions)) {
+ if (!DehydratedSearchPageOptionsCodec.is(parsedOptions)) {
console.warn("Invalid search query params received. Using defaults.");
return defaultSearchPageOptions;
}
@@ -572,7 +542,7 @@ const getDisplayModeFromLegacyParams = (
_: (mode) => {
// Because Old UI also uses query string `type` for My resources type and Legacy
// My resources page does not have galleries, we always return "list".
- if (LegacyMyResourcesRuntypes.guard(mode)) {
+ if (LegacyMyResourcesCodec.is(mode)) {
return "list";
}
throw new TypeError(`Unknown Legacy display mode [${mode}]`);
@@ -696,8 +666,8 @@ export const legacyQueryStringToSearchPageOptions = async (
const owner = await getOwnerFromLegacyParams(getQueryParam("owner"));
const sortOrder = pipe(
getQueryParam("sort")?.toLowerCase(),
- O.fromPredicate(OEQ.Search.SortOrderRunTypes.guard),
- O.getOrElse(() => defaultSearchOptions.sortOrder)
+ OEQ.Codec.Search.SortOrderCodec.decode,
+ E.getOrElse(() => defaultSearchOptions.sortOrder)
);
const datePrimary = getQueryParam("dp");
@@ -756,7 +726,7 @@ export const RAW_MODE_STORAGE_KEY = "raw_mode";
* Read the value of wildcard mode from LocalStorage.
*/
export const getRawModeFromStorage = (): boolean =>
- readDataFromStorage(RAW_MODE_STORAGE_KEY, Boolean.guard) ??
+ readDataFromStorage(RAW_MODE_STORAGE_KEY, t.boolean.is) ??
defaultSearchOptions.rawMode;
export const writeRawModeToStorage = (value: boolean): void =>
diff --git a/react-front-end/tsrc/search/components/SearchOrderSelect.tsx b/react-front-end/tsrc/search/components/SearchOrderSelect.tsx
index 3633d1f6cc..6545a3af67 100644
--- a/react-front-end/tsrc/search/components/SearchOrderSelect.tsx
+++ b/react-front-end/tsrc/search/components/SearchOrderSelect.tsx
@@ -16,6 +16,8 @@
* limitations under the License.
*/
import { InputLabel, MenuItem, Select } from "@mui/material";
+import { pipe } from "fp-ts/function";
+import * as E from "../../util/Either.extended";
import * as React from "react";
import { languageStrings } from "../../util/langstrings";
import * as OEQ from "@openequella/rest-api-client";
@@ -76,7 +78,12 @@ export const SearchOrderSelect = ({
// If sortOrder is undefined, pass an empty string to select nothing.
value={value ?? ""}
onChange={(event) =>
- onChange(OEQ.Search.SortOrderRunTypes.check(event.target.value))
+ pipe(
+ event.target.value,
+ OEQ.Codec.Search.SortOrderCodec.decode,
+ E.getOrThrow,
+ onChange
+ )
}
variant="standard"
>
diff --git a/react-front-end/tsrc/settings/Search/components/DefaultSortOrderSetting.tsx b/react-front-end/tsrc/settings/Search/components/DefaultSortOrderSetting.tsx
index bcba80dfe5..83a8b4ba2d 100644
--- a/react-front-end/tsrc/settings/Search/components/DefaultSortOrderSetting.tsx
+++ b/react-front-end/tsrc/settings/Search/components/DefaultSortOrderSetting.tsx
@@ -17,6 +17,8 @@
*/
import { FormControl, MenuItem, OutlinedInput, Select } from "@mui/material";
import { styled } from "@mui/material/styles";
+import { pipe } from "fp-ts/function";
+import * as E from "../../../util/Either.extended";
import * as React from "react";
import { languageStrings } from "../../../util/langstrings";
import * as OEQ from "@openequella/rest-api-client";
@@ -46,9 +48,6 @@ export default function DefaultSortOrderSetting({
const { relevance, lastModified, dateCreated, title, userRating } =
languageStrings.settings.searching.searchPageSettings;
- const validateSortOrder = (value: unknown): OEQ.Search.SortOrder =>
- OEQ.Search.SortOrderRunTypes.check(value);
-
const options: [OEQ.Search.SortOrder, string][] = [
["rank", relevance],
["datemodified", lastModified],
@@ -62,7 +61,14 @@ export default function DefaultSortOrderSetting({