mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-04 07:19:52 +00:00
feat(studio): carry hfId on TimelineElement, wire through buildPatchTarget (R7, T5b) (#1299)
* feat(studio): carry hfId on TimelineElement, wire through buildPatchTarget (R7, T5b) * refactor(studio): extract readHfId helper, fix empty-string normalization, add comments (R7 review) - Extract readHfId(el) to domEditingLayers.ts — centralizes `?.trim() || undefined` normalisation; guards against empty-string data-hf-id reaching findTagByTarget - Wire readHfId into domEditingLayers.ts and useDomEditCommits.ts (the one site that still used `?? undefined` instead of `|| undefined`) - Re-export readHfId through domEditing.ts public API - Add readHfId unit tests: present, absent, empty-string, whitespace-only - Add comment on PatchTarget: runtime validation lives in findTagByTarget, type is docs-only - Suppress pre-existing unused re-exports in timelineDOM.ts (backward-compat re-exports brought into fallow scope by the T5b hfId changes) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(core): clear data-hf-id on split clone to prevent dual-match (R7 review) cloneNode(true) copies all attributes including data-hf-id. Without clearing it, both halves of a split share the same hf-id; the server's findByHfId picks the first match and silently patches the wrong clip. Remove the attribute from the clone so write-back re-mints a fresh id on the next preview load. Adds a test: splitElementInHtml — hfId clone isolation. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(studio): add hfId to DomEditLayerItem + getDomLayerPatchTarget return type (R7 review) - Add hfId to DomEditLayerItem interface (domEditingTypes.ts) so layer item construction in collectDomEditLayerItems compiles - Widen getDomLayerPatchTarget return type to include hfId + populate it from data-hf-id attribute (domEditingElement.ts) - Widen findDomEditSelectionTarget to check hfId-first when no id/selector - Widen Pick types in domEditOverlayGeometry.ts and useGsapScriptCommits.ts - Add hfId to buildMissingCompositionElements element construction - Add hfId-targeted test coverage in domEditing.test.ts, domEditOverlayGeometry.test.ts, timelineIframeHelpers.test.ts - Update hfIds.test.ts KNOWN LIMITATION labels — write-back landed in R7 T1-2 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
82c754f27a
commit
0923bc0787
@@ -173,6 +173,7 @@ export function buildDefaultDomEditTextField(base?: Partial<DomEditTextField>):
|
||||
// fallow-ignore-next-line complexity
|
||||
export function resolveDomEditCapabilities(args: {
|
||||
selector?: string;
|
||||
hfId?: string;
|
||||
tagName?: string;
|
||||
className?: string;
|
||||
inlineStyles: Record<string, string>;
|
||||
@@ -182,7 +183,7 @@ export function resolveDomEditCapabilities(args: {
|
||||
isMasterView: boolean;
|
||||
existsInSource?: boolean;
|
||||
}): DomEditCapabilities {
|
||||
if (!args.selector || args.isInsideLockedComposition) {
|
||||
if ((!args.selector && !args.hfId) || args.isInsideLockedComposition) {
|
||||
return {
|
||||
canSelect: !args.isInsideLockedComposition,
|
||||
canEditStyles: false,
|
||||
@@ -289,7 +290,7 @@ export function buildElementLabel(el: HTMLElement): string {
|
||||
async function probeSourceElement(
|
||||
projectId: string,
|
||||
sourceFile: string,
|
||||
target: { id?: string; selector?: string; selectorIndex?: number },
|
||||
target: { id?: string; hfId?: string; selector?: string; selectorIndex?: number },
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
const response = await fetch(
|
||||
@@ -321,7 +322,8 @@ export async function resolveDomEditSelection(
|
||||
let current: HTMLElement | null = getSelectionCandidate(startEl, options);
|
||||
while (current && current !== doc.body && current !== doc.documentElement) {
|
||||
const selector = buildStableSelector(current);
|
||||
if (!selector) {
|
||||
const hfId = readHfId(current);
|
||||
if (!selector && !hfId) {
|
||||
current = current.parentElement;
|
||||
continue;
|
||||
}
|
||||
@@ -330,13 +332,9 @@ export async function resolveDomEditSelection(
|
||||
current,
|
||||
options.activeCompositionPath,
|
||||
);
|
||||
const selectorIndex = getSelectorIndex(
|
||||
doc,
|
||||
current,
|
||||
selector,
|
||||
sourceFile,
|
||||
options.activeCompositionPath,
|
||||
);
|
||||
const selectorIndex = selector
|
||||
? getSelectorIndex(doc, current, selector, sourceFile, options.activeCompositionPath)
|
||||
: undefined;
|
||||
const compositionSrc =
|
||||
current.getAttribute("data-composition-src") ??
|
||||
current.getAttribute("data-composition-file") ??
|
||||
@@ -346,15 +344,18 @@ export async function resolveDomEditSelection(
|
||||
const textFields = collectDomEditTextFields(current);
|
||||
const isInsideLocked = Boolean(findClosestByAttribute(current, ["data-timeline-locked"]));
|
||||
let existsInSource: boolean | undefined;
|
||||
if (!options.skipSourceProbe && options.projectId && (current.id || selector)) {
|
||||
const probeTarget: { id?: string; selector?: string; selectorIndex?: number } = {};
|
||||
if (!options.skipSourceProbe && options.projectId && (current.id || selector || hfId)) {
|
||||
const probeTarget: { id?: string; hfId?: string; selector?: string; selectorIndex?: number } =
|
||||
{};
|
||||
if (current.id) probeTarget.id = current.id;
|
||||
if (hfId) probeTarget.hfId = hfId;
|
||||
if (selector) probeTarget.selector = selector;
|
||||
if (selectorIndex != null) probeTarget.selectorIndex = selectorIndex;
|
||||
existsInSource = await probeSourceElement(options.projectId, sourceFile, probeTarget);
|
||||
}
|
||||
const capabilities = resolveDomEditCapabilities({
|
||||
selector,
|
||||
hfId,
|
||||
tagName: current.tagName.toLowerCase(),
|
||||
className: current.className,
|
||||
inlineStyles,
|
||||
@@ -369,7 +370,7 @@ export async function resolveDomEditSelection(
|
||||
return {
|
||||
element: current,
|
||||
id: current.id || undefined,
|
||||
hfId: current.getAttribute("data-hf-id") ?? undefined,
|
||||
hfId,
|
||||
selector,
|
||||
selectorIndex,
|
||||
sourceFile,
|
||||
@@ -452,6 +453,7 @@ export function collectDomEditLayerItems(
|
||||
if (!root) return [];
|
||||
|
||||
const items: DomEditLayerItem[] = [];
|
||||
// fallow-ignore-next-line complexity
|
||||
const visit = (el: HTMLElement, depth: number) => {
|
||||
if (items.length >= maxItems) return;
|
||||
|
||||
@@ -465,6 +467,7 @@ export function collectDomEditLayerItems(
|
||||
depth,
|
||||
childCount: getDirectLayerChildren(el, options).length,
|
||||
id: target.id ?? undefined,
|
||||
hfId: target.hfId ?? undefined,
|
||||
selector: target.selector ?? undefined,
|
||||
selectorIndex: target.selectorIndex,
|
||||
sourceFile: target.sourceFile,
|
||||
@@ -536,10 +539,11 @@ export function getDomEditNonEditableReason(
|
||||
}
|
||||
|
||||
export function getDomEditTargetKey(
|
||||
selection: Pick<DomEditSelection, "id" | "selector" | "selectorIndex" | "sourceFile">,
|
||||
selection: Pick<DomEditSelection, "id" | "hfId" | "selector" | "selectorIndex" | "sourceFile">,
|
||||
): string {
|
||||
return [
|
||||
selection.sourceFile || "index.html",
|
||||
selection.hfId ?? "",
|
||||
selection.id ?? "",
|
||||
selection.selector ?? "",
|
||||
selection.selectorIndex ?? "",
|
||||
@@ -556,6 +560,10 @@ export function isTextEditableSelection(selection: DomEditSelection): boolean {
|
||||
|
||||
// buildElementAgentPrompt is in domEditingAgentPrompt.ts
|
||||
|
||||
export function readHfId(element: Element): string | undefined {
|
||||
return element.getAttribute("data-hf-id")?.trim() || undefined;
|
||||
}
|
||||
|
||||
export function buildDomEditPatchTarget(
|
||||
selection: Pick<DomEditSelection, "id" | "hfId" | "selector" | "selectorIndex">,
|
||||
): { id?: string | null; hfId?: string; selector?: string; selectorIndex?: number } {
|
||||
|
||||
Reference in New Issue
Block a user