Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/main' into bugfix/3216-jq-fromdate
Browse files Browse the repository at this point in the history
  • Loading branch information
twschiller committed May 6, 2022
2 parents c8a2fd0 + 52ec942 commit c53cd0f
Show file tree
Hide file tree
Showing 14 changed files with 266 additions and 158 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,10 @@ const TemplateToggleWidget: React.VFC<TemplateToggleWidgetProps> = ({
inputModeOptions.some((option) => option.value === "string")
) {
onModeChange("string");
} else if (
inputModeOptions.some((option) => option.value === "number")
) {
onModeChange("number");
} else if (
inputModeOptions.some((option) => option.value === "var")
) {
Expand Down
22 changes: 18 additions & 4 deletions src/components/form/Form.module.scss
Original file line number Diff line number Diff line change
@@ -1,7 +1,21 @@
$color-danger: #fc3939;
$color-danger-active: #e50303;
/*
* Copyright (C) 2022 PixieBrix, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

.status {
color: $color-danger;
margin-bottom: 1rem;
border: none;
border-radius: 0;
}
41 changes: 39 additions & 2 deletions src/components/form/Form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,11 @@
import styles from "./Form.module.scss";

import React, { ReactElement } from "react";
import { Button, Form as BootstrapForm } from "react-bootstrap";
import { Alert, Button, Form as BootstrapForm } from "react-bootstrap";
import { Formik, FormikHelpers, FormikValues } from "formik";
import * as yup from "yup";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faExclamationTriangle } from "@fortawesome/free-solid-svg-icons";

export type OnSubmit<TValues = FormikValues> = (
values: TValues,
Expand All @@ -43,13 +45,46 @@ export type RenderSubmit = (state: {
export type RenderStatus = (state: { status: string }) => ReactElement;

type FormProps = {
/**
* The starting formik field values for the form
*/
initialValues: FormikValues;

/**
* The yup validation schema for the form
*/
validationSchema: yup.AnyObjectSchema;

/**
* Should the form be validated on component mount?
*/
validateOnMount?: boolean;

/**
* (from Formik): Should Formik reset the form when new initialValues change?
*/
enableReinitialize?: boolean;

/**
* The render function for the body of the form
*/
renderBody?: RenderBody;

/**
* The render function for the submit button (and any other co-located buttons)
*/
renderSubmit?: RenderSubmit;

/**
* The render function for the top-level form status message
*
* Note: This currently defaults to an "error" style layout
*/
renderStatus?: RenderStatus;

/**
* The submission handler for the form
*/
onSubmit: OnSubmit;
};

Expand All @@ -60,7 +95,9 @@ const defaultRenderSubmit: RenderSubmit = ({ isSubmitting, isValid }) => (
);

const defaultRenderStatus: RenderStatus = ({ status }) => (
<div className={styles.status}>{status}</div>
<Alert variant="danger" className={styles.status}>
<FontAwesomeIcon icon={faExclamationTriangle} /> {status}
</Alert>
);

const Form: React.FC<FormProps> = ({
Expand Down
9 changes: 4 additions & 5 deletions src/components/formBuilder/FormBuilder.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

import { IBlock } from "@/core";
import { getExampleBlockConfig } from "@/pageEditor/tabs/editTab/exampleBlockConfigs";
import { getExampleBlockConfig } from "@/pageEditor/exampleBlockConfigs";
import {
createFormikTemplate,
fireTextInput,
Expand All @@ -37,9 +36,9 @@ let defaultFieldName: string;

beforeAll(() => {
registerDefaultWidgets();
const { schema, uiSchema } = getExampleBlockConfig({
id: validateRegistryId("@pixiebrix/form"),
} as IBlock);
const { schema, uiSchema } = getExampleBlockConfig(
validateRegistryId("@pixiebrix/form")
);
exampleFormSchema = {
schema,
uiSchema,
Expand Down
60 changes: 30 additions & 30 deletions src/components/formBuilder/edit/FormEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,9 @@ import {
DEFAULT_FIELD_TYPE,
generateNewPropertyName,
moveStringInArray,
normalizeSchema,
normalizeUiOrder,
replaceStringInArray,
updateRjsfSchemaWithDefaultsIfNeeded,
} from "@/components/formBuilder/formBuilderHelpers";
import { UI_ORDER } from "@/components/formBuilder/schemaFieldNames";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
Expand Down Expand Up @@ -72,14 +72,6 @@ const FormEditor: React.FC<FormEditorProps> = ({

const { schema, uiSchema } = rjsfSchema;

useEffect(() => {
// Set default values if needed
const nextRjsfSchema = updateRjsfSchemaWithDefaultsIfNeeded(rjsfSchema);
if (nextRjsfSchema !== null) {
setRjsfSchema(nextRjsfSchema);
}
}, [rjsfSchema, setRjsfSchema]);

// Select the active field when FormEditor field changes
useEffect(
() => {
Expand Down Expand Up @@ -118,11 +110,7 @@ const FormEditor: React.FC<FormEditorProps> = ({
return { titleFieldProps, descriptionFieldProps };
}, [name]);

if (!schema || !uiSchema) {
return null;
}

const propertyKeys = Object.keys(schema.properties ?? {});
const propertyKeys = Object.keys(schema?.properties ?? {});

const addProperty = () => {
const propertyName = generateNewPropertyName(propertyKeys);
Expand All @@ -145,12 +133,16 @@ const FormEditor: React.FC<FormEditorProps> = ({
);

const nextRjsfSchema = produce(rjsfSchema, (draft) => {
draft.uiSchema[UI_ORDER] = nextUiOrder;
if (!draft.schema.properties) {
draft.schema.properties = {};
draft.schema = normalizeSchema(schema);
// eslint-disable-next-line security/detect-object-injection -- prop name is generated
draft.schema.properties[propertyName] = newProperty;

if (!uiSchema) {
draft.uiSchema = {};
}

draft.schema.properties[propertyName] = newProperty;
// eslint-disable-next-line security/detect-object-injection -- prop name is a constant
draft.uiSchema[UI_ORDER] = nextUiOrder;
});
setRjsfSchema(nextRjsfSchema);
setActiveField(propertyName);
Expand All @@ -176,36 +168,44 @@ const FormEditor: React.FC<FormEditorProps> = ({
setActiveField(nextActiveField);

const nextRjsfSchema = produce(rjsfSchema, (draft) => {
draft.schema = normalizeSchema(schema);

if (schema.required?.length > 0) {
draft.schema.required = replaceStringInArray(
schema.required,
propertyToRemove
);
}

draft.uiSchema[UI_ORDER] = nextUiOrder;
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete,security/detect-object-injection
delete draft.schema.properties[propertyToRemove];
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete

if (!uiSchema) {
draft.uiSchema = {};
}

// eslint-disable-next-line security/detect-object-injection -- prop name is a constant
draft.uiSchema[UI_ORDER] = nextUiOrder;
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete,security/detect-object-injection
delete draft.uiSchema[propertyToRemove];
});

setRjsfSchema(nextRjsfSchema);
};

// There's always at least 1 item in uiOrder array, "*".
// The uiOrder field may not be initialized yet
const order = uiOrder ?? ["*"];
const canMoveUp =
Boolean(activeField) &&
(uiOrder?.length > 2
? uiOrder[0] !== activeField
(order.length > 2
? order[0] !== activeField
: propertyKeys[0] !== activeField);
const canMoveDown =
Boolean(activeField) &&
(uiOrder?.length === propertyKeys.length + 1
? uiOrder[uiOrder.length - 2] !== activeField
: Array.isArray(uiOrder) &&
findLast(propertyKeys, (key) => !uiOrder.includes(key)) !==
activeField);
(order.length === propertyKeys.length + 1
? order[order.length - 2] !== activeField
: Array.isArray(order) &&
findLast(propertyKeys, (key) => !order.includes(key)) !== activeField);

return (
<>
Expand Down Expand Up @@ -242,7 +242,7 @@ const FormEditor: React.FC<FormEditorProps> = ({
</Col>
</Row>

{activeField && Boolean(schema.properties?.[activeField]) && (
{activeField && Boolean(schema?.properties?.[activeField]) && (
<FieldEditor
name={name}
propertyName={activeField}
Expand Down
95 changes: 41 additions & 54 deletions src/components/formBuilder/formBuilderHelpers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,17 @@ import {
DEFAULT_FIELD_TYPE,
MINIMAL_SCHEMA,
MINIMAL_UI_SCHEMA,
normalizeSchema,
normalizeUiOrder,
produceSchemaOnPropertyNameChange,
produceSchemaOnUiTypeChange,
replaceStringInArray,
stringifyUiType,
updateRjsfSchemaWithDefaultsIfNeeded,
validateNextPropertyName,
} from "./formBuilderHelpers";
import { RJSFSchema } from "./formBuilderTypes";
import { initRenamingCases } from "./formEditor.testCases";
import { UI_ORDER, UI_WIDGET } from "./schemaFieldNames";
import { UI_WIDGET } from "./schemaFieldNames";

describe("replaceStringInArray", () => {
let array: string[];
Expand Down Expand Up @@ -66,58 +66,6 @@ describe("replaceStringInArray", () => {
});
});

describe("updateRjsfSchemaWithDefaultsIfNeeded", () => {
test("accepts the minimal schema", () => {
const rjsfSchema: RJSFSchema = {
schema: MINIMAL_SCHEMA,
uiSchema: MINIMAL_UI_SCHEMA,
};

const nextRjsfSchema = updateRjsfSchemaWithDefaultsIfNeeded(rjsfSchema);
expect(nextRjsfSchema).toBeNull();
});

test("init schema and ui schema", () => {
const nextRjsfSchema = updateRjsfSchemaWithDefaultsIfNeeded(
{} as RJSFSchema
);
expect(nextRjsfSchema.schema).toEqual(MINIMAL_SCHEMA);
expect(nextRjsfSchema.uiSchema).toEqual(MINIMAL_UI_SCHEMA);
});

test("accepts the schema it created", () => {
const rjsfSchema = updateRjsfSchemaWithDefaultsIfNeeded({} as RJSFSchema);
const nextRjsfSchema = updateRjsfSchemaWithDefaultsIfNeeded(rjsfSchema);
expect(nextRjsfSchema).toBeNull();
});

test("init ui order", () => {
const rjsfSchema: RJSFSchema = {
schema: MINIMAL_SCHEMA,
uiSchema: {},
};

const nextRjsfSchema = updateRjsfSchemaWithDefaultsIfNeeded(rjsfSchema);
expect(nextRjsfSchema.uiSchema[UI_ORDER]).toEqual(["*"]);
});

test.each([null, false, true, "firstName"])(
"fixes required field when it's %s",
(requiredFieldValue: any) => {
const rjsfSchema: RJSFSchema = {
schema: {
...MINIMAL_SCHEMA,
required: requiredFieldValue,
},
uiSchema: MINIMAL_UI_SCHEMA,
};

const nextRjsfSchema = updateRjsfSchemaWithDefaultsIfNeeded(rjsfSchema);
expect(nextRjsfSchema.schema.required).toEqual([]);
}
);
});

describe("produceSchemaOnPropertyNameChange", () => {
test.each(initRenamingCases())(
"renaming a field",
Expand Down Expand Up @@ -185,6 +133,45 @@ describe("validateNextPropertyName", () => {
});
});

describe("normalizeSchema", () => {
test("init schema", () => {
// eslint-disable-next-line unicorn/no-useless-undefined
const actual = normalizeSchema(undefined);
expect(actual).toStrictEqual(MINIMAL_SCHEMA);
});

test("add properties", () => {
const actual = normalizeSchema({
type: "object",
});
expect(actual).toStrictEqual({
type: "object",
properties: {},
});
});

test("fix required", () => {
const actual = normalizeSchema({
type: "object",
properties: {
foo: {
type: "string",
},
},
required: null,
});
expect(actual).toStrictEqual({
type: "object",
properties: {
foo: {
type: "string",
},
},
required: [],
});
});
});

describe("normalizeUiOrder", () => {
test("init uiOrder", () => {
const actual = normalizeUiOrder(["propA", "propB"], []);
Expand Down
Loading

0 comments on commit c53cd0f

Please sign in to comment.