From 688c0955ca2ba6affbe013bb5c8ca4c42e16e205 Mon Sep 17 00:00:00 2001
From: Ayobami Akingbade
Date: Wed, 6 Dec 2023 20:01:05 +0100
Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat(form-settings):=20add=20defaul?=
=?UTF-8?q?t=20values=20for=20forms?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.github/workflows/codeql.yml | 2 -
src/backend/entities/entities.service.ts | 37 ++++++------
.../SchemaForm/_RenderFormInput.tsx | 13 ++++-
src/frontend/components/SchemaForm/index.tsx | 2 +
.../components/Form/FormCodeEditor/index.tsx | 1 +
.../design-system/components/Form/_types.ts | 1 +
src/frontend/docs/scripts/form-scripts.tsx | 24 +++++++-
src/frontend/lib/selection/index.ts | 4 ++
src/frontend/lib/selection/selection.spec.ts | 16 ++++++
src/frontend/views/data/Create/index.tsx | 30 +++++++++-
src/frontend/views/data/Details/_Layout.tsx | 2 +-
src/frontend/views/data/Table/index.tsx | 2 +-
src/frontend/views/data/Update/index.tsx | 4 +-
src/frontend/views/entity/Actions/Form.tsx | 5 --
src/frontend/views/entity/Fields/index.tsx | 2 +-
src/frontend/views/entity/Form/ScriptForm.tsx | 3 +
src/frontend/views/entity/Form/index.tsx | 57 +++++++++++++++++--
src/frontend/views/entity/Form/types.ts | 5 ++
src/frontend/views/entity/constants.ts | 10 ++--
.../views/settings/Entities/Selection.tsx | 4 +-
src/shared/configurations/constants.ts | 1 +
src/shared/form-schemas/types.ts | 2 +
.../lib/strings/__tests__/strings.spec.ts | 29 +++++++++-
src/shared/lib/strings/index.ts | 5 ++
src/shared/types/db.ts | 2 +-
25 files changed, 215 insertions(+), 48 deletions(-)
create mode 100644 src/frontend/views/entity/Form/types.ts
diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
index e59b2da94..286990db7 100644
--- a/.github/workflows/codeql.yml
+++ b/.github/workflows/codeql.yml
@@ -2,8 +2,6 @@
name: CodeQL
on:
- push:
- pull_request:
schedule:
- cron: "30 1 * * 0"
diff --git a/src/backend/entities/entities.service.ts b/src/backend/entities/entities.service.ts
index 224e45f01..336633a5b 100644
--- a/src/backend/entities/entities.service.ts
+++ b/src/backend/entities/entities.service.ts
@@ -132,31 +132,37 @@ export class EntitiesApiService implements IApplicationService {
}));
}
+ async getEntityValidRelations(
+ entity: string
+ ): Promise {
+ const [entityRelations, disabledEntities, hiddenEntity] = await Promise.all(
+ [
+ this.getEntityRelations(entity),
+ this._configurationApiService.show("disabled_entities"),
+ this._configurationApiService.show("hidden_entity_relations", entity),
+ ]
+ );
+
+ return entityRelations.filter(
+ ({ table }) =>
+ !disabledEntities.includes(table) && !hiddenEntity.includes(table)
+ );
+ }
+
async getEntityRelationsForUserRole(
entity: string,
userRole: string
): Promise {
- const [
- entityRelations,
- disabledEntities,
- entityLabels,
- entityOrders,
- hiddenEntity,
- ] = await Promise.all([
- this.getEntityRelations(entity),
- this._configurationApiService.show("disabled_entities"),
+ const [validRelations, entityLabels, entityOrders] = await Promise.all([
+ this.getEntityValidRelations(entity),
this._configurationApiService.show("entity_relations_labels", entity),
this._configurationApiService.show("entity_relations_order", entity),
- this._configurationApiService.show("hidden_entity_relations", entity),
]);
const allowedEntityRelation =
await this._rolesApiService.filterPermittedEntities(
userRole,
- entityRelations.filter(
- ({ table }) =>
- !disabledEntities.includes(table) && !hiddenEntity.includes(table)
- ),
+ validRelations,
"table"
);
@@ -173,8 +179,7 @@ export class EntitiesApiService implements IApplicationService {
table: relation.table,
label: entityLabels[relation.table],
type,
- field:
- type === "toOne" ? relation?.joinColumnOptions?.[0].name : undefined,
+ field: relation?.joinColumnOptions?.[0].name,
inverseToOneField:
relation?.joinColumnOptions?.[0].tag === "inverse"
? relation?.joinColumnOptions?.[0].referencedColumnName
diff --git a/src/frontend/components/SchemaForm/_RenderFormInput.tsx b/src/frontend/components/SchemaForm/_RenderFormInput.tsx
index 6786f54f0..d825b85ae 100644
--- a/src/frontend/components/SchemaForm/_RenderFormInput.tsx
+++ b/src/frontend/components/SchemaForm/_RenderFormInput.tsx
@@ -2,7 +2,6 @@ import { sluggify } from "shared/lib/strings";
import { ISchemaFormConfig } from "shared/form-schemas/types";
import { IColorableSelection } from "shared/types/ui";
import { FIELD_TYPES_CONFIG_MAP } from "shared/validations";
-import { ISharedFormInput } from "frontend/design-system/components/Form/_types";
import { FormInput } from "frontend/design-system/components/Form/FormInput";
import { FormNumberInput } from "frontend/design-system/components/Form/FormNumberInput";
import { FormSelect } from "frontend/design-system/components/Form/FormSelect";
@@ -14,15 +13,21 @@ import { FormTextArea } from "frontend/design-system/components/Form/FormTextAre
import { FormFileInput } from "frontend/design-system/components/Form/FormFileInput";
import { FormSelectButton } from "frontend/design-system/components/Form/FormSelectButton";
import { FormRichTextArea } from "frontend/design-system/components/Form/FormRichTextArea";
+import { FieldInputProps, FieldMetaState } from "react-final-form";
interface IProps {
type: keyof typeof FIELD_TYPES_CONFIG_MAP;
- renderProps: ISharedFormInput;
+ renderProps: {
+ input: FieldInputProps;
+ meta: FieldMetaState;
+ };
apiSelections?: ISchemaFormConfig["apiSelections"];
entityFieldSelections?: IColorableSelection[];
required: boolean;
disabled: boolean;
label: string;
+ placeholder?: string;
+ description?: string;
}
export function RenderFormInput({
@@ -33,11 +38,15 @@ export function RenderFormInput({
apiSelections,
required,
disabled,
+ description,
+ placeholder,
}: IProps) {
const formProps = {
label,
required,
disabled,
+ placeholder,
+ description,
...renderProps,
};
diff --git a/src/frontend/components/SchemaForm/index.tsx b/src/frontend/components/SchemaForm/index.tsx
index 1dfdd8fe4..c9665f6df 100644
--- a/src/frontend/components/SchemaForm/index.tsx
+++ b/src/frontend/components/SchemaForm/index.tsx
@@ -98,6 +98,8 @@ export function SchemaForm>({
required={bag.validations.some(
(validation) => validation.validationType === "required"
)}
+ placeholder={bag.placeholder}
+ description={bag.description}
apiSelections={bag.apiSelections}
label={bag.label || userFriendlyCase(field)}
entityFieldSelections={bag.selections}
diff --git a/src/frontend/design-system/components/Form/FormCodeEditor/index.tsx b/src/frontend/design-system/components/Form/FormCodeEditor/index.tsx
index a1161f71e..f56ee8bf8 100644
--- a/src/frontend/design-system/components/Form/FormCodeEditor/index.tsx
+++ b/src/frontend/design-system/components/Form/FormCodeEditor/index.tsx
@@ -55,6 +55,7 @@ export const FormCodeEditor: React.FC = (formInput) => {
highlight(code, languages[formInput.language || "javascript"])
}
disabled={formInput.disabled}
+ placeholder={formInput.placeholder}
textareaId={formInput.input.name}
padding={4}
style={{
diff --git a/src/frontend/design-system/components/Form/_types.ts b/src/frontend/design-system/components/Form/_types.ts
index 8298f0790..841a28296 100644
--- a/src/frontend/design-system/components/Form/_types.ts
+++ b/src/frontend/design-system/components/Form/_types.ts
@@ -5,6 +5,7 @@ export interface ISharedFormInput {
meta: FieldMetaState;
label?: string;
description?: string;
+ placeholder?: string;
required?: boolean;
disabled?: boolean;
sm?: true;
diff --git a/src/frontend/docs/scripts/form-scripts.tsx b/src/frontend/docs/scripts/form-scripts.tsx
index ca548182c..7b80a9bb0 100644
--- a/src/frontend/docs/scripts/form-scripts.tsx
+++ b/src/frontend/docs/scripts/form-scripts.tsx
@@ -54,10 +54,10 @@ export function FormScriptDocumentation(props: IDocumentationRootProps) {
can't make Promises or make network calls.{" "}
- We have two tabs where which do different things so let's start
+ We have three tabs where which do different things so let's start
with the first
- 1. Field State
+ 1. Field State
This allows you to hide or disable your form fields. Let's dive
straight into examples
@@ -132,7 +132,7 @@ return {
the database field name which is accountBalance
any other
label will not work.{" "}
- 2. Before Submit
+ 2. Before Submit
This tab enables you to do two different things.
1. Run custom validation
@@ -240,6 +240,24 @@ return {
createdById: JSON.parse($.auth.systemProfile).userId
}`}
/>
+
+
3. Initial Values
+
+ This tab simply allows you to set the initial values for the create form
+
+
+ You will not have access to any variables here, so no `$.anything`
);
}
diff --git a/src/frontend/lib/selection/index.ts b/src/frontend/lib/selection/index.ts
index 004ffce0d..022048bda 100644
--- a/src/frontend/lib/selection/index.ts
+++ b/src/frontend/lib/selection/index.ts
@@ -34,6 +34,10 @@ export function useStringSelections(key: string) {
const update = Object.fromEntries(items.map((item) => [item, true]));
setSelections({ ...selections, ...update });
},
+ setMultiple: (items: string[]) => {
+ const update = Object.fromEntries(items.map((item) => [item, true]));
+ setSelections(update);
+ },
deSelectMutiple: (items: string[]) => {
const update = Object.fromEntries(items.map((item) => [item, false]));
diff --git a/src/frontend/lib/selection/selection.spec.ts b/src/frontend/lib/selection/selection.spec.ts
index febd45420..1fbe44448 100644
--- a/src/frontend/lib/selection/selection.spec.ts
+++ b/src/frontend/lib/selection/selection.spec.ts
@@ -28,6 +28,22 @@ describe("useStringSelections", () => {
]);
});
+ it("should select multiple", async () => {
+ const { result, rerender } = renderHook(() => useStringSelections(""));
+
+ result.current.setMultiple(["foo", "bar"]);
+
+ rerender();
+
+ expect(result.current.allSelections).toEqual(["foo", "bar"]);
+
+ result.current.setMultiple(["baz", "quz", "foo"]);
+
+ rerender();
+
+ expect(result.current.allSelections).toEqual(["baz", "quz", "foo"]);
+ });
+
it("should deSelectMutiple multiple", async () => {
const { result, rerender } = renderHook(() => useStringSelections(""));
diff --git a/src/frontend/views/data/Create/index.tsx b/src/frontend/views/data/Create/index.tsx
index be54be3e6..571f2ffed 100644
--- a/src/frontend/views/data/Create/index.tsx
+++ b/src/frontend/views/data/Create/index.tsx
@@ -11,12 +11,29 @@ import {
} from "frontend/hooks/entity/entity.config";
import { useEntityDataCreationMutation } from "frontend/hooks/data/data.store";
import { useRouteParams } from "frontend/lib/routing/useRouteParam";
+import { useEntityConfiguration } from "frontend/hooks/configuration/configuration.store";
+import { evalJavascriptStringSafely } from "frontend/lib/script-runner";
import {
EntityActionTypes,
useEntityActionMenuItems,
} from "../../entity/constants";
import { BaseEntityForm } from "../_BaseEntityForm";
+const runInitialValuesScript = (
+ initialValuesScript: string
+): Record => {
+ if (!initialValuesScript) {
+ return {};
+ }
+
+ const response = evalJavascriptStringSafely<{}>(initialValuesScript, {});
+
+ if (typeof response !== "object") {
+ return {};
+ }
+ return response;
+};
+
export function EntityCreate() {
const routeParams = useRouteParams();
const entity = useEntitySlug();
@@ -26,7 +43,7 @@ export function EntityCreate() {
const actionItems = useEntityActionMenuItems([
EntityActionTypes.Create,
- EntityActionTypes.Types,
+ EntityActionTypes.Form,
]);
useSetPageDetails({
@@ -40,6 +57,15 @@ export function EntityCreate() {
const { backLink } = useNavigationStack();
+ const entityFormExtension = useEntityConfiguration(
+ "entity_form_extension",
+ entity
+ );
+
+ const scriptInitialValues = runInitialValuesScript(
+ entityFormExtension.data.initialValues
+ );
+
return (
@@ -53,7 +79,7 @@ export function EntityCreate() {
crudAction="create"
resetForm
buttonText={entityCrudConfig.FORM_LANG.CREATE}
- initialValues={routeParams}
+ initialValues={{ ...scriptInitialValues, ...routeParams }}
onSubmit={entityDataCreationMutation.mutateAsync}
hiddenColumns={hiddenCreateColumns}
/>
diff --git a/src/frontend/views/data/Details/_Layout.tsx b/src/frontend/views/data/Details/_Layout.tsx
index ede7e530c..7749cf907 100644
--- a/src/frontend/views/data/Details/_Layout.tsx
+++ b/src/frontend/views/data/Details/_Layout.tsx
@@ -43,7 +43,7 @@ export function DetailsLayout({
const actionItems = useEntityActionMenuItems(
[
EntityActionTypes.Details,
- EntityActionTypes.Types,
+ EntityActionTypes.Form,
EntityActionTypes.Labels,
],
childEntity
diff --git a/src/frontend/views/data/Table/index.tsx b/src/frontend/views/data/Table/index.tsx
index 1a121697f..cace40c8f 100644
--- a/src/frontend/views/data/Table/index.tsx
+++ b/src/frontend/views/data/Table/index.tsx
@@ -21,7 +21,7 @@ export function EntityTable() {
EntityActionTypes.Table,
EntityActionTypes.Diction,
EntityActionTypes.Labels,
- EntityActionTypes.Types,
+ EntityActionTypes.Form,
]);
useSetPageDetails({
diff --git a/src/frontend/views/data/Update/index.tsx b/src/frontend/views/data/Update/index.tsx
index 7501c5a51..bfa6716c7 100644
--- a/src/frontend/views/data/Update/index.tsx
+++ b/src/frontend/views/data/Update/index.tsx
@@ -33,7 +33,7 @@ export function EntityUpdate() {
const actionItems = useEntityActionMenuItems([
EntityActionTypes.Update,
- EntityActionTypes.Types,
+ EntityActionTypes.Form,
]);
useSetPageDetails({
@@ -58,7 +58,7 @@ export function EntityUpdate() {
title={entityCrudConfig.TEXT_LANG.EDIT}
description={
userHasPermission(USER_PERMISSIONS.CAN_CONFIGURE_APP)
- ? "For security reasons, Any data that is hidden in details view will not show up here, So rememeber to toggle on all fields there if you want to update them here"
+ ? `For security reasons, Any data that is hidden in details view will not show up here, So rememeber to toggle on all fields there if you want to update them here`
: undefined
}
backLink={backLink}
diff --git a/src/frontend/views/entity/Actions/Form.tsx b/src/frontend/views/entity/Actions/Form.tsx
index 0c8744c7a..e2505b057 100644
--- a/src/frontend/views/entity/Actions/Form.tsx
+++ b/src/frontend/views/entity/Actions/Form.tsx
@@ -121,11 +121,6 @@ export function ActionForm({
})),
},
...selectedImplementation,
- // TODO: Actions script i.e return false or return a loop to do many users
- // triggerLogic: {
- // type: "json",
- // validations: [],
- // },
};
if (currentView.type === "entity") {
delete fields.entity;
diff --git a/src/frontend/views/entity/Fields/index.tsx b/src/frontend/views/entity/Fields/index.tsx
index 439abfb62..a2f8f1f23 100644
--- a/src/frontend/views/entity/Fields/index.tsx
+++ b/src/frontend/views/entity/Fields/index.tsx
@@ -197,7 +197,7 @@ export function EntityFieldsSettings() {
/>
),
- label: ENTITY_FIELD_SETTINGS_TAB_LABELS.TYPES,
+ label: ENTITY_FIELD_SETTINGS_TAB_LABELS.FORM,
},
{
content: (
diff --git a/src/frontend/views/entity/Form/ScriptForm.tsx b/src/frontend/views/entity/Form/ScriptForm.tsx
index b3a688b79..c184abbca 100644
--- a/src/frontend/views/entity/Form/ScriptForm.tsx
+++ b/src/frontend/views/entity/Form/ScriptForm.tsx
@@ -14,6 +14,7 @@ interface IProps {
value: string;
onSubmit: (value: string) => Promise;
isLoading: boolean;
+ placeholder: string;
field: string;
error?: unknown;
}
@@ -25,6 +26,7 @@ export function ScriptForm({
onSubmit,
field,
error,
+ placeholder,
isLoading,
}: IProps) {
const scriptContext = useSchemaFormScriptContext("test");
@@ -41,6 +43,7 @@ export function ScriptForm({
type: "json",
label: "Script",
validations: [],
+ placeholder,
},
}}
onSubmit={async (data) => {
diff --git a/src/frontend/views/entity/Form/index.tsx b/src/frontend/views/entity/Form/index.tsx
index c2781a6c2..615d2752d 100644
--- a/src/frontend/views/entity/Form/index.tsx
+++ b/src/frontend/views/entity/Form/index.tsx
@@ -1,7 +1,6 @@
import { SLUG_LOADING_VALUE } from "frontend/lib/routing/constants";
import { useSetPageDetails } from "frontend/lib/routing/usePageDetails";
import { USER_PERMISSIONS } from "shared/constants/user";
-import { IFormExtension } from "frontend/components/SchemaForm/types";
import { MAKE_APP_CONFIGURATION_CRUD_CONFIG } from "frontend/hooks/configuration/configuration.constant";
import {
useEntityConfiguration,
@@ -13,10 +12,12 @@ import { DOCUMENTATION_LABEL } from "frontend/docs";
import { SectionBox } from "frontend/design-system/components/Section/SectionBox";
import { Tabs } from "frontend/design-system/components/Tabs";
import { useEntitySlug } from "frontend/hooks/entity/entity.config";
+import { Typo } from "frontend/design-system/primitives/Typo";
+import { Spacer } from "frontend/design-system/primitives/Spacer";
import { BaseEntitySettingsLayout } from "../_Base";
-
import { ENTITY_CONFIGURATION_VIEW } from "../constants";
import { ScriptForm } from "./ScriptForm";
+import { IEntityFormExtension } from "./types";
function useEntityFormView() {
const entity = useEntitySlug();
@@ -35,7 +36,7 @@ function useEntityFormView() {
const { error } = entityFormExtensionSettings;
const onScriptSubmit =
- (key: keyof IFormExtension) => async (value: string) => {
+ (key: keyof IEntityFormExtension) => async (value: string) => {
await upsertEntityFormExtensionSettingsMutation.mutateAsync({
...entityFormExtensionSettings.data,
[key]: value,
@@ -50,6 +51,19 @@ function useEntityFormView() {
onSubmit={onScriptSubmit("fieldsState")}
field="fieldsState"
error={error}
+ placeholder={`return {
+ canRegister: {
+ disabled: $.formValues.age < 18
+ },
+ accountBalance: {
+ hidden: JSON.parse($.auth.systemProfile).userId === $.routeParams.entityId
+ },
+ someField: {
+ disabled: someDisabledLogic,
+ hidden: someHiddenLogic,
+ },
+}
+ `}
/>
),
"Before Submit": (
@@ -59,6 +73,32 @@ function useEntityFormView() {
field="beforeSubmit"
onSubmit={onScriptSubmit("beforeSubmit")}
error={error}
+ placeholder={`if($.formValues.planet != "Earth") {
+ return "Only Aliens can submit this form"
+}
+
+return {
+ ...$.formValues,
+ slug: $.formValues.title.replaceAll(" ", "-").toLowerCase(),
+ createdById: JSON.parse($.auth.systemProfile).userId,
+ createdAt: new Date(),
+}
+ `}
+ />
+ ),
+ "Initial Values": (
+
),
};
@@ -91,7 +131,16 @@ export function EntityFormExtensionSettings() {
({
label: key,
- content: value,
+ content: (
+ <>
+
+ Click the 'Explain Form Scripts' at the top right
+ corner for more info on how this works
+
+
+ {value}
+ >
+ ),
}))}
/>
diff --git a/src/frontend/views/entity/Form/types.ts b/src/frontend/views/entity/Form/types.ts
new file mode 100644
index 000000000..25d3fad8c
--- /dev/null
+++ b/src/frontend/views/entity/Form/types.ts
@@ -0,0 +1,5 @@
+import { IFormExtension } from "frontend/components/SchemaForm/types";
+
+export interface IEntityFormExtension extends IFormExtension {
+ initialValues: string;
+}
diff --git a/src/frontend/views/entity/constants.ts b/src/frontend/views/entity/constants.ts
index 0d2d23a4c..e3e1b843f 100644
--- a/src/frontend/views/entity/constants.ts
+++ b/src/frontend/views/entity/constants.ts
@@ -10,7 +10,7 @@ export const ENTITY_CONFIGURATION_VIEW = "ENTITY_CONFIGURATION_VIEW";
export const ENTITY_FIELD_SETTINGS_TAB_LABELS = {
LABELS: "Labels",
- TYPES: "Types",
+ FORM: "Form",
ORDER: "Order",
};
@@ -27,7 +27,7 @@ export enum EntityActionTypes {
Create,
Table,
Details,
- Types,
+ Form,
Diction,
Labels,
}
@@ -48,12 +48,12 @@ const ENTITY_ACTION_BAG: Record<
tab: ENTITY_FIELD_SETTINGS_TAB_LABELS.LABELS,
}),
},
- [EntityActionTypes.Types]: {
- label: "Types Settings",
+ [EntityActionTypes.Form]: {
+ label: "Form Settings",
IconComponent: Settings,
link: (entity) =>
NAVIGATION_LINKS.ENTITY.CONFIG.FIELDS(entity, {
- tab: ENTITY_FIELD_SETTINGS_TAB_LABELS.TYPES,
+ tab: ENTITY_FIELD_SETTINGS_TAB_LABELS.FORM,
}),
},
[EntityActionTypes.Update]: {
diff --git a/src/frontend/views/settings/Entities/Selection.tsx b/src/frontend/views/settings/Entities/Selection.tsx
index 7adfdc091..dab11d4da 100644
--- a/src/frontend/views/settings/Entities/Selection.tsx
+++ b/src/frontend/views/settings/Entities/Selection.tsx
@@ -25,7 +25,7 @@ export function EntitiesSelection({
hiddenList,
crudConfig,
}: IProps) {
- const { toggleSelection, allSelections, selectMutiple, isSelected } =
+ const { toggleSelection, allSelections, setMultiple, isSelected } =
useStringSelections(`${selectionKey}--entities-selection`);
const [touched, setTouched] = useState(false);
@@ -33,7 +33,7 @@ export function EntitiesSelection({
const [isMakingRequest, setIsMakingRequest] = useState(false);
useEffect(() => {
- selectMutiple(hiddenList);
+ setMultiple(hiddenList);
}, [hiddenList]);
const formButton = (
diff --git a/src/shared/configurations/constants.ts b/src/shared/configurations/constants.ts
index 271ac2775..2a6d61d11 100644
--- a/src/shared/configurations/constants.ts
+++ b/src/shared/configurations/constants.ts
@@ -69,6 +69,7 @@ export const APP_CONFIGURATION_CONFIG = {
defaultValue: {
fieldsState: "",
beforeSubmit: "",
+ initialValues: "",
},
},
file_upload_settings: {
diff --git a/src/shared/form-schemas/types.ts b/src/shared/form-schemas/types.ts
index 4a7ad05b0..11a71c66d 100644
--- a/src/shared/form-schemas/types.ts
+++ b/src/shared/form-schemas/types.ts
@@ -10,6 +10,8 @@ export interface ISchemaFormConfig {
};
type: keyof typeof FIELD_TYPES_CONFIG_MAP;
label?: string;
+ placeholder?: string;
+ description?: string;
validations: IFieldValidationItem[];
}
diff --git a/src/shared/lib/strings/__tests__/strings.spec.ts b/src/shared/lib/strings/__tests__/strings.spec.ts
index 08fe9fbc4..1dc39614c 100644
--- a/src/shared/lib/strings/__tests__/strings.spec.ts
+++ b/src/shared/lib/strings/__tests__/strings.spec.ts
@@ -1,4 +1,4 @@
-import { pluralize } from "..";
+import { arrayToComaSeparatedString, pluralize } from "..";
describe("pluralize", () => {
it("should work correctly with singular forms", () => {
@@ -58,3 +58,30 @@ describe("pluralize", () => {
).toBe("0 complexities");
});
});
+
+describe("arrayToComaSeparatedString", () => {
+ it("should return single string for the list of one string", () => {
+ expect(arrayToComaSeparatedString(["Document"])).toBe("Document");
+ });
+
+ it("should return two words, separated with `and` for the list of two strings", () => {
+ expect(arrayToComaSeparatedString(["Document", "Document"])).toBe(
+ "Document and Document"
+ );
+ });
+
+ it("should return words, separated with `,` and the last word separated with `and`", () => {
+ expect(
+ arrayToComaSeparatedString(["Document", "Document", "Document"])
+ ).toBe("Document, Document and Document");
+
+ expect(
+ arrayToComaSeparatedString([
+ "Document",
+ "Document",
+ "Document",
+ "Document",
+ ])
+ ).toBe("Document, Document, Document and Document");
+ });
+});
diff --git a/src/shared/lib/strings/index.ts b/src/shared/lib/strings/index.ts
index f6cb8d17f..14dbcf48c 100644
--- a/src/shared/lib/strings/index.ts
+++ b/src/shared/lib/strings/index.ts
@@ -2,6 +2,11 @@ export function upperCaseFirstLetter(word: string): string {
return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
}
+export const arrayToComaSeparatedString = (list: string[]): string =>
+ [list.slice(0, -1).join(", "), list.slice(-1)[0]].join(
+ list.length < 2 ? "" : " and "
+ );
+
export function pluralize({
count,
singular,
diff --git a/src/shared/types/db.ts b/src/shared/types/db.ts
index cf4f040bd..96f2adfa5 100644
--- a/src/shared/types/db.ts
+++ b/src/shared/types/db.ts
@@ -1,6 +1,6 @@
interface IDBSchemaRelation {
table: string;
- relationType: string;
+ relationType: "ManyToOne" | "OneToMany" | "OneToOne" | "ManyToMany";
joinColumnOptions?: {
name: string;
referencedColumnName: string;