Skip to content

Commit

Permalink
fix: fix readOnly behaviour in nested fields
Browse files Browse the repository at this point in the history
  • Loading branch information
chrisvxd committed Jan 10, 2025
1 parent c00ea00 commit f6ab512
Show file tree
Hide file tree
Showing 4 changed files with 173 additions and 83 deletions.
70 changes: 70 additions & 0 deletions packages/core/components/AutoField/context.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { createContext, PropsWithChildren, useContext, useMemo } from "react";

type NestedFieldContext = {
localName?: string;
readOnlyFields?: Partial<Record<string | number | symbol, boolean>>;
};

export const NestedFieldContext = createContext<NestedFieldContext>({});

export const useNestedFieldContext = () => {
const context = useContext(NestedFieldContext);

return {
...context,
readOnlyFields: context.readOnlyFields || {},
};
};

export const NestedFieldProvider = ({
children,
name,
subName,
wildcardName = name,
readOnlyFields,
}: PropsWithChildren<{
name: string;
subName: string;
wildcardName?: string;
readOnlyFields: Partial<Record<string | number | symbol, boolean>>;
}>) => {
const subPath = `${name}.${subName}`;
const wildcardSubPath = `${wildcardName}.${subName}`;

const subReadOnlyFields = useMemo(
() =>
Object.keys(readOnlyFields).reduce((acc, readOnlyKey) => {
const isLocal =
readOnlyKey.indexOf(subPath) > -1 ||
readOnlyKey.indexOf(wildcardSubPath) > -1;

if (isLocal) {
const subPathPattern = new RegExp(
`^(${name}|${wildcardName})\.`
.replace(/\[/g, "\\[")
.replace(/\]/g, "\\]")
.replace(/\./g, "\\.")
.replace(/\*/g, "\\*")
);

const localName = readOnlyKey.replace(subPathPattern, "");

return {
...acc,
[localName]: readOnlyFields[readOnlyKey],
};
}

return acc;
}, {}),
[name, subName, wildcardName, readOnlyFields]
);

return (
<NestedFieldContext.Provider
value={{ readOnlyFields: subReadOnlyFields, localName: subName }}
>
{children}
</NestedFieldContext.Provider>
);
};
75 changes: 43 additions & 32 deletions packages/core/components/AutoField/fields/ArrayField/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { useCallback, useEffect, useState } from "react";
import { DragIcon } from "../../../DragIcon";
import { ArrayState, ItemWithId } from "../../../../types";
import { useAppContext } from "../../../Puck/context";
import { NestedFieldProvider, useNestedFieldContext } from "../../context";
import { DragDropContext } from "../../../DragDropContext";

const getClassName = getClassNameFactory("ArrayField", styles);
Expand All @@ -26,8 +27,7 @@ export const ArrayField = ({
Label = (props) => <div {...props} />,
}: FieldPropsInternal) => {
const { state, setUi, selectedItem, getPermissions } = useAppContext();

const readOnlyFields = selectedItem?.readOnly || {};
const { readOnlyFields, localName = name } = useNestedFieldContext();

const value: object[] = _value;

Expand Down Expand Up @@ -295,44 +295,55 @@ export const ArrayField = ({
<div className={getClassNameItem("body")}>
<fieldset className={getClassNameItem("fieldset")}>
{Object.keys(field.arrayFields!).map(
(fieldName) => {
const subField =
field.arrayFields![fieldName];
(subName) => {
const subField = field.arrayFields![subName];

const indexName = `${name}[${i}]`;
const subPath = `${indexName}.${subName}`;

const subFieldName = `${name}[${i}].${fieldName}`;
const wildcardFieldName = `${name}[*].${fieldName}`;
const localIndexName = `${localName}[${i}]`;
const localWildcardName = `${localName}[*]`;
const localSubPath = `${localIndexName}.${subName}`;
const localWildcardSubPath = `${localWildcardName}.${subName}`;

const subReadOnly = forceReadOnly
? forceReadOnly
: typeof readOnlyFields[subFieldName] !==
: typeof readOnlyFields[subPath] !==
"undefined"
? readOnlyFields[subFieldName]
: readOnlyFields[wildcardFieldName];
? readOnlyFields[localSubPath]
: readOnlyFields[localWildcardSubPath];

const label = subField.label || fieldName;
const label = subField.label || subName;

return (
<AutoFieldPrivate
key={subFieldName}
name={subFieldName}
label={label}
id={`${_arrayId}_${fieldName}`}
readOnly={subReadOnly}
field={{
...subField,
label, // May be used by custom fields
}}
value={data[fieldName]}
onChange={(val, ui) => {
onChange(
replace(value, i, {
...data,
[fieldName]: val,
}),
ui
);
}}
/>
<NestedFieldProvider
key={subPath}
name={localIndexName}
wildcardName={localWildcardName}
subName={subName}
readOnlyFields={readOnlyFields}
>
<AutoFieldPrivate
name={subPath}
label={label}
id={`${_arrayId}_${subName}`}
readOnly={subReadOnly}
field={{
...subField,
label, // May be used by custom fields
}}
value={data[subName]}
onChange={(val, ui) => {
onChange(
replace(value, i, {
...data,
[subName]: val,
}),
ui
);
}}
/>
</NestedFieldProvider>
);
}
)}
Expand Down
74 changes: 36 additions & 38 deletions packages/core/components/AutoField/fields/ObjectField/index.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,8 @@
import getClassNameFactory from "../../../../lib/get-class-name-factory";
import styles from "./styles.module.css";
import { MoreVertical } from "lucide-react";
import {
AutoFieldPrivate,
FieldLabelInternal,
FieldPropsInternal,
} from "../..";
import { useAppContext } from "../../../Puck/context";
import { AutoFieldPrivate, FieldPropsInternal } from "../..";
import { NestedFieldProvider, useNestedFieldContext } from "../../context";

const getClassName = getClassNameFactory("ObjectField", styles);

Expand All @@ -20,14 +16,12 @@ export const ObjectField = ({
readOnly,
id,
}: FieldPropsInternal) => {
const { selectedItem } = useAppContext();
const { readOnlyFields, localName = name } = useNestedFieldContext();

if (field.type !== "object" || !field.objectFields) {
return null;
}

const readOnlyFields = selectedItem?.readOnly || {};

const data = value || {};

return (
Expand All @@ -39,42 +33,46 @@ export const ObjectField = ({
>
<div className={getClassName()}>
<fieldset className={getClassName("fieldset")}>
{Object.keys(field.objectFields!).map((fieldName) => {
const subField = field.objectFields![fieldName];
{Object.keys(field.objectFields!).map((subName) => {
const subField = field.objectFields![subName];

const subFieldName = `${name}.${fieldName}`;
const wildcardFieldName = `${name}.${fieldName}`;
const subPath = `${name}.${subName}`;
const localSubPath = `${localName || name}.${subName}`;

const subReadOnly = readOnly
? readOnly
: typeof readOnlyFields[subFieldName] !== "undefined"
? readOnlyFields[subFieldName]
: readOnlyFields[wildcardFieldName];
: readOnlyFields[localSubPath];

const label = subField.label || fieldName;
const label = subField.label || subName;

return (
<AutoFieldPrivate
key={subFieldName}
name={subFieldName}
label={label}
id={`${id}_${fieldName}`}
readOnly={subReadOnly}
field={{
...subField,
label, // May be used by custom fields
}}
value={data[fieldName]}
onChange={(val, ui) => {
onChange(
{
...data,
[fieldName]: val,
},
ui
);
}}
/>
<NestedFieldProvider
key={subPath}
name={localName || id}
subName={subName}
readOnlyFields={readOnlyFields}
>
<AutoFieldPrivate
name={subPath}
label={subPath}
id={`${id}_${subName}`}
readOnly={subReadOnly}
field={{
...subField,
label, // May be used by custom fields
}}
value={data[subName]}
onChange={(val, ui) => {
onChange(
{
...data,
[subName]: val,
},
ui
);
}}
/>
</NestedFieldProvider>
);
})}
</fieldset>
Expand Down
37 changes: 24 additions & 13 deletions packages/core/components/AutoField/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ import {
ReactElement,
ReactNode,
useCallback,
useContext,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import {
Expand All @@ -25,6 +25,7 @@ import { useDebouncedCallback } from "use-debounce";
import { ObjectField } from "./fields/ObjectField";
import { useAppContext } from "../Puck/context";
import { useSafeId } from "../../lib/use-safe-id";
import { NestedFieldContext } from "./context";

const getClassName = getClassNameFactory("Input", styles);
const getClassNameWrapper = getClassNameFactory("InputWrapper", styles);
Expand Down Expand Up @@ -128,7 +129,7 @@ function AutoFieldInternal<
Label?: React.FC<FieldLabelPropsInternal>;
}
) {
const { dispatch, overrides } = useAppContext();
const { dispatch, overrides, selectedItem } = useAppContext();

const { id, Label = FieldLabelInternal } = props;

Expand Down Expand Up @@ -216,20 +217,30 @@ function AutoFieldInternal<

const Render = render[field.type] as (props: FieldProps) => ReactElement;

const nestedFieldContext = useContext(NestedFieldContext);

return (
<div
className={getClassNameWrapper()}
onFocus={onFocus}
onBlur={onBlur}
onClick={(e) => {
// Prevent propagation of any click events to parent field.
// For example, a field within an array may bubble an event
// and fail to stop propagation.
e.stopPropagation();
<NestedFieldContext.Provider
value={{
readOnlyFields:
nestedFieldContext.readOnlyFields || selectedItem?.readOnly || {},
localName: nestedFieldContext.localName,
}}
>
<Render {...mergedProps}>{children}</Render>
</div>
<div
className={getClassNameWrapper()}
onFocus={onFocus}
onBlur={onBlur}
onClick={(e) => {
// Prevent propagation of any click events to parent field.
// For example, a field within an array may bubble an event
// and fail to stop propagation.
e.stopPropagation();
}}
>
<Render {...mergedProps}>{children}</Render>
</div>
</NestedFieldContext.Provider>
);
}

Expand Down

0 comments on commit f6ab512

Please sign in to comment.