Repository navigation
feat(visual-builder): disable fields of entries the editor has restricted (DRFT-913) #663
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
65a722e
354956d
6fc7e27
7c5d60c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -43,6 +43,8 @@ import { EntryPermissions } from "../utils/getEntryPermissions"; | |
| import { FieldLocationAppList } from "./FieldLocationAppList"; | ||
| import { FieldLocationIcon } from "./FieldLocationIcon"; | ||
| import { WorkflowStageDetails } from "../utils/getWorkflowStageDetails"; | ||
| import { getEntryEditRestrictionForField } from "../utils/fieldLockIndicator"; | ||
| import { subscribeEntryFieldLockInfo } from "../utils/fieldLockStore"; | ||
| import { ResolvedVariantPermissions } from "../utils/getResolvedVariantPermissions"; | ||
| import { isCustomFieldMultipleInstance as checkIsCustomFieldMultipleInstance } from "../utils/isCustomFieldMultipleInstance"; | ||
|
|
||
|
|
@@ -153,7 +155,17 @@ function FieldToolbarComponent( | |
| let isWholeMultipleField = false; | ||
| const APP_LIST_MIN_WIDTH = 230; | ||
|
|
||
| let disableFieldActions = false; | ||
| const [restrictionReason, setRestrictionReason] = useState(() => | ||
| getEntryEditRestrictionForField(fieldMetadata) | ||
| ); | ||
| useEffect(() => { | ||
| const update = () => | ||
| setRestrictionReason(getEntryEditRestrictionForField(fieldMetadata)); | ||
| update(); | ||
| return subscribeEntryFieldLockInfo(update); | ||
| }, [fieldMetadata]); | ||
|
|
||
| let disableFieldActions = Boolean(restrictionReason); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit. The other consumers of this value render it, and the buttons here already carry a Generated by Claude Code |
||
| let isCustomFieldMultipleInstance = false; | ||
| let isCustomFieldWholeMultiple = false; | ||
| if (fieldSchema) { | ||
|
|
@@ -167,7 +179,7 @@ function FieldToolbarComponent( | |
| entryPermissions, | ||
| entryWorkflowStageDetails, | ||
| ); | ||
| disableFieldActions = isDisabled; | ||
| disableFieldActions = isDisabled || Boolean(restrictionReason); | ||
|
|
||
| fieldType = getFieldType(fieldSchema); | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| import visualBuilderPostMessage from "../utils/visualBuilderPostMessage"; | ||
| import { VisualBuilderPostMessageEvents } from "../utils/types/postMessage.types"; | ||
| import { | ||
| setEntryEditRestriction, | ||
| toEntryEditRestriction, | ||
| } from "../utils/fieldLockStore"; | ||
|
|
||
| interface EntryEditRestrictionUpdateEvent { | ||
| data: { | ||
| entryUid: string; | ||
| locale: string; | ||
| variantUid?: string; | ||
| restriction: unknown; | ||
| }; | ||
| } | ||
|
|
||
| /** | ||
| * Mirrors the parent's per-entry edit restriction (older version open, unlocalized, ...) so the | ||
| * entry's fields render disabled with the matching reason. Null clears the scope. | ||
| */ | ||
| export function useEntryEditRestrictionUpdateEvent(): void { | ||
| visualBuilderPostMessage?.on( | ||
| VisualBuilderPostMessageEvents.ENTRY_EDIT_RESTRICTION_UPDATE, | ||
| (event: EntryEditRestrictionUpdateEvent) => { | ||
| const { entryUid, locale, variantUid, restriction } = event.data; | ||
| setEntryEditRestriction( | ||
| { entryUid, locale, variantUid }, | ||
| toEntryEditRestriction(restriction) | ||
| ); | ||
| } | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -21,7 +21,10 @@ import { VisualBuilderPostMessageEvents } from "../utils/types/postMessage.types | |
| import { VisualBuilder } from ".."; | ||
| import { FieldSchemaMap } from "../utils/fieldSchemaMap"; | ||
| import { isFieldDisabled } from "../utils/isFieldDisabled"; | ||
| import { getPeerLockForField } from "../utils/fieldLockIndicator"; | ||
| import { | ||
| getEntryEditRestrictionForField, | ||
| getPeerLockForField, | ||
| } from "../utils/fieldLockIndicator"; | ||
| import EventListenerHandlerParams from "./types"; | ||
| import { toggleHighlightedCommentIconDisplay } from "../generators/generateHighlightedComment"; | ||
| import { VB_EmptyBlockParentClass } from "../.."; | ||
|
|
@@ -187,7 +190,8 @@ export async function handleBuilderInteraction( | |
| // gate runs before the post message so a click on a peer-locked field is a | ||
| // true no-op: posting MOUSE_CLICK with its fieldMetadata makes the host read | ||
| // it as a fresh selection and cancel the current user's own pending lock | ||
| // release. | ||
| // release. An entry-wide restriction does not stop here: the click still opens | ||
| // the form (the way back to the latest version), and the field shows disabled. | ||
|
Comment on lines
+193
to
+194
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Blocker. Letting the click through means Peer locks never hit this because the gate above returns before the toolbar is built. Restrictions deliberately do not, so the toolbar needs the same treatment the inline-edit path got: fold Generated by Claude Code |
||
| if (eventDetails && getPeerLockForField(eventDetails.fieldMetadata)) { | ||
| return; | ||
| } | ||
|
|
@@ -438,7 +442,7 @@ async function handleFieldSchemaAndIndividualFields( | |
| entryAcl, | ||
| entryWorkflowStageDetails | ||
| ); | ||
| if (isDisabled) { | ||
| if (isDisabled || getEntryEditRestrictionForField(fieldMetadata)) { | ||
| addOverlay({ | ||
| overlayWrapper: params.overlayWrapper, | ||
| resizeObserver: params.resizeObserver, | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Blocker. This subscription is never torn down, so it outlives the toolbar that created it.
appendFieldToolbarrenders this component withrender(<FieldToolbarComponent .../>, wrapper), andremoveFieldToolbartears it down withtoolbar.innerHTML = "". That removes the DOM but never unmounts the preact tree, so effect cleanups on this component do not run. TheDELETE_INSTANCEeffect lower in this file shows the consequence already being worked around: itsreturn () => event?.unregister()is dead, which is whyremoveFieldToolbarre-unregisters those three events by hand through@ts-expect-errorcalls into a private handler map.The listener registered here has no such counterpart, so every toolbar creation adds one more entry to
lockListenersthat is never removed. A toolbar is built on each focus, and the hover path inappendFieldToolbarskips the "already present" guard, so the set grows across an editing session. Each dead listener retains this component'sfieldMetadataand props, including the editable element, so detached DOM is retained with it, and every later restriction or lock update callssetRestrictionReasonon all of them.notifyLockListenerscatches and logs listener errors at debug level, so none of this surfaces.Fix it the way the postMessage listeners are handled: keep the unsubscribe where
removeFieldToolbarcan reach it and call it during teardown, or move the subscription out of the component so its lifetime is not tied to an unmount that never happens. Unmounting properly inremoveFieldToolbarwithrender(null, container)would fix this and the deadDELETE_INSTANCEcleanup together, but it needs the container reference thatappendFieldToolbarcurrently drops.Generated by Claude Code