Skip to content

Commit

Permalink
feat: add a JSON editor
Browse files Browse the repository at this point in the history
  • Loading branch information
casperiv0 committed Nov 12, 2023
1 parent 0ba8b94 commit 2a4eec3
Show file tree
Hide file tree
Showing 10 changed files with 186 additions and 30 deletions.
3 changes: 2 additions & 1 deletion apps/client/locales/en/values.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,8 @@
"warningNotApplicable": "Warning Not Applicable",
"isPrimary": "Is Primary",
"isDispositionDescription": "A disposition code can be used when a call is closed. This will be used to determine the outcome of the call.",
"isDisposition": "Is Disposition"
"isDisposition": "Is Disposition",
"mustBeValidJson": "Must be valid JSON"
},
"PENAL_CODE_GROUP": {
"MANAGE": "Manage Penal Code Groups",
Expand Down
29 changes: 27 additions & 2 deletions apps/client/src/components/admin/values/ManageValueModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ function createInitialValues(options: CreateInitialValuesOptions) {

extraFields:
value && (isDivisionValue(value) || isDepartmentValue(value))
? JSON.stringify(value.extraFields)
? safelyStringifyJSON(value.extraFields)
: "null",

departmentLinks: value && isDepartmentValue(value) ? value.links ?? [] : [],
Expand Down Expand Up @@ -194,14 +194,19 @@ export function ManageValueModal({ onCreate, onUpdate, type, value }: Props) {
values: typeof INITIAL_VALUES,
helpers: FormikHelpers<typeof INITIAL_VALUES>,
) {
if (safelyParseJSON(values.extraFields) === false) {
helpers.setFieldError("extraFields", tValues("mustBeValidJson"));
return;
}

const data = {
...values,
whatPages: values.whatPages,
departments: values.departments,
divisions: values.divisions,
officerRankDepartments: values.officerRankDepartments,
trimLevels: values.trimLevels,
extraFields: JSON.parse(values.extraFields),
extraFields: safelyParseJSON(values.extraFields),
};

if (value) {
Expand Down Expand Up @@ -450,3 +455,23 @@ export function ManageValueModal({ onCreate, onUpdate, type, value }: Props) {
</Modal>
);
}

function safelyParseJSON(json: string) {
if (!json) return null;

try {
return JSON.parse(json);
} catch {
return false;
}
}

function safelyStringifyJSON(json: string | null) {
if (!json) return "null";

try {
return JSON.stringify(json, null, 4);
} catch {
return "null";
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { SelectField, SwitchField, TextField } from "@snailycad/ui";
import { JsonEditor, SelectField, SwitchField, TextField } from "@snailycad/ui";
import { useFormikContext } from "formik";
import { DepartmentType, ValueType } from "@snailycad/types";
import { useValues } from "context/ValuesContext";
Expand All @@ -7,6 +7,7 @@ import { ValueSelectField } from "components/form/inputs/value-select-field";
import { CALLSIGN_TEMPLATE_VARIABLES } from "components/admin/manage/cad-settings/misc-features/template-tab";
import { DepartmentLinksSection } from "./department-links-section";
import type { ManageValueFormValues } from "../ManageValueModal";
import { FormField } from "components/form/FormField";

export const DEPARTMENT_LABELS = {
[DepartmentType.LEO]: "LEO",
Expand Down Expand Up @@ -96,17 +97,12 @@ export function DepartmentFields() {
</SwitchField>
) : null}

<TextField
description={t("extraFieldsDescription")}
isTextarea
isOptional
errorMessage={errors.extraFields as string}
label={t("extraFields")}
name="extraFields"
onChange={(value) => setFieldValue("extraFields", value)}
value={values.extraFields}
placeholder="JSON"
/>
<FormField optional errorMessage={errors.extraFields as string} label={t("extraFields")}>
<JsonEditor
value={values.extraFields}
onChange={(value) => setFieldValue("extraFields", value)}
/>
</FormField>

<DepartmentLinksSection />
</>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { TextField } from "@snailycad/ui";
import { JsonEditor, TextField } from "@snailycad/ui";
import { useValues } from "context/ValuesContext";
import { useFormikContext } from "formik";
import { useTranslations } from "use-intl";
import { ValueSelectField } from "components/form/inputs/value-select-field";
import { ValueType } from "@snailycad/types";
import type { ManageValueFormValues } from "../ManageValueModal";
import { FormField } from "components/form/FormField";

export function DivisionFields() {
const { values, errors, setFieldValue } = useFormikContext<ManageValueFormValues>();
Expand Down Expand Up @@ -46,17 +47,12 @@ export function DivisionFields() {
value={values.pairedUnitTemplate}
/>

<TextField
description={t("extraFieldsDescription")}
isTextarea
isOptional
errorMessage={errors.extraFields as string}
label={t("extraFields")}
name="extraFields"
onChange={(value) => setFieldValue("extraFields", value)}
value={values.extraFields}
placeholder="JSON"
/>
<FormField optional errorMessage={errors.extraFields as string} label={t("extraFields")}>
<JsonEditor
value={values.extraFields}
onChange={(value) => setFieldValue("extraFields", value)}
/>
</FormField>
</>
);
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import type { AnyValue } from "@snailycad/types";
import { SelectField } from "@snailycad/ui";
import { JsonEditor, SelectField } from "@snailycad/ui";
import { isEmergencyVehicleValue } from "@snailycad/utils";
import { useValues } from "context/ValuesContext";
import { useFormikContext } from "formik";
import { useFeatureEnabled } from "hooks/useFeatureEnabled";
import { useTranslations } from "use-intl";
import type { ManageValueFormValues } from "../ManageValueModal";
import { FormField } from "components/form/FormField";

export function useDefaultDivisions() {
const { division } = useValues();
Expand All @@ -22,7 +23,7 @@ export function useDefaultDivisions() {
}

export function EmergencyVehicleFields() {
const { values, setFieldValue } = useFormikContext<ManageValueFormValues>();
const { values, errors, setFieldValue } = useFormikContext<ManageValueFormValues>();
const { division, department } = useValues();
const { DIVISIONS } = useFeatureEnabled();
const t = useTranslations("Values");
Expand Down Expand Up @@ -54,6 +55,13 @@ export function EmergencyVehicleFields() {
selectedKeys={values.divisions}
/>
) : null}

<FormField optional errorMessage={errors.extraFields as string} label={t("extraFields")}>
<JsonEditor
value={values.extraFields}
onChange={(value) => setFieldValue("extraFields", value)}
/>
</FormField>
</>
);
}
2 changes: 2 additions & 0 deletions packages/ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"@storybook/test-runner": "^0.13.0",
"@storybook/testing-library": "^0.2.2",
"autoprefixer": "^10.4.16",
"monaco-editor": "^0.44.0",
"postcss": "^8.4.31",
"prop-types": "^15.8.1",
"storybook": "^7.5.2",
Expand Down Expand Up @@ -60,6 +61,7 @@
"dependencies": {
"@casperiv/useful": "^3.0.0",
"@internationalized/date": "^3.5.0",
"@monaco-editor/react": "^4.6.0",
"@radix-ui/react-accordion": "^1.1.2",
"@radix-ui/react-context-menu": "^2.1.5",
"@radix-ui/react-dropdown-menu": "^2.0.6",
Expand Down
38 changes: 38 additions & 0 deletions packages/ui/src/components/editors/json-editor.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import Editor from "@monaco-editor/react";

export interface JsonEditorProps {
value: string;
onChange(value: string | undefined): void;
}

export function JsonEditor(props: JsonEditorProps) {
return (
<div className="overflow-hidden rounded-md border-2 border-secondary">
<Editor
loading={<SkeletonEditorLoading />}
theme="vs-dark"
height="200px"
language="json"
value={props.value}
onChange={props.onChange}
/>
</div>
);
}

function SkeletonEditorLoading() {
return (
<div
aria-label="Loading Editor..."
className="h-[200px] w-full bg-secondary animate-pulse p-6 flex flex-col gap-y-1.5"
>
<span className="block bg-primary h-3 w-full animate-pulse rounded" />
<span className="block bg-primary h-3 w-full animate-pulse rounded" />
<span className="block bg-primary h-3 w-full animate-pulse rounded" />
<span className="block bg-primary h-3 w-full animate-pulse rounded" />
<span className="block bg-primary h-3 w-full animate-pulse rounded" />
<span className="block bg-primary h-3 w-full animate-pulse rounded" />
<span className="block bg-primary h-3 w-full animate-pulse rounded" />
</div>
);
}
1 change: 1 addition & 0 deletions packages/ui/src/components/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,4 @@ export * from "./form-row";
export * from "./full-date";
export * from "./infofield";
export * from "./status";
export * from "./editors/json-editor";
22 changes: 22 additions & 0 deletions packages/ui/src/components/stories/editors/json-editor.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import * as React from "react";
import type { Meta, StoryObj } from "@storybook/react";
import { JsonEditor } from "../../editors/json-editor";

const meta = {
title: "Editors/JSON Editor",
component: JsonEditor,
tags: ["autodocs"],
} satisfies Meta<typeof JsonEditor>;

export default meta;
type Story = StoryObj<typeof JsonEditor>;

function DefaultRenderer() {
const [value, setValue] = React.useState<string | undefined>("");

return <JsonEditor value={value ?? ""} onChange={setValue} />;
}

export const Default: Story = {
render: () => <DefaultRenderer />,
};
69 changes: 68 additions & 1 deletion pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 2a4eec3

Please sign in to comment.