refactor: replace PayloadDecoder and MetadataDecoder with unified Payload component#3299
refactor: replace PayloadDecoder and MetadataDecoder with unified Payload component#3299rossedfort wants to merge 24 commits intomainfrom
Conversation
…load component Consolidates two divergent payload display components into a single `<Payload />` component at `src/lib/components/payload.svelte`. Previously, `payload-decoder.svelte` (Svelte 5 runes, required a `children` snippet, always decoded full attribute trees) and `metadata-decoder.svelte` (Svelte 4 options API with slot syntax, decoded single payloads to truncated summary strings) served different display purposes but used inconsistent APIs and decoding paths across 65+ call sites. The new component unifies both under a single `mode` prop: - `code-block` (default): renders a CodeBlock with the decoded value, replacing all PayloadDecoder + CodeBlock snippet patterns - `summary`: truncates to 120 chars and renders a Badge, replacing all MetadataDecoder usages - `inline-truncated`: renders the compact .payload pre block used in event detail rows - `children` snippet: escape hatch for callers that need custom rendering (schedule input, timeline SVG text elements) Both old components used a single decoding path internally (cloneAllPotentialPayloadsWithCodec -> decodePayloadAttributes -> stringifyWithBigInt). MetadataDecoder previously used a separate decodeSingleReadablePayloadWithCodec path; the new component uses the same unified path for consistency. The component is written in Svelte 5 runes throughout and imports from $app/state instead of $app/stores, matching the newer direction of the codebase. Migrated files: - src/lib/components/event/event-card.svelte - src/lib/components/event/event-details-row.svelte (+ moved .payload CSS) - src/lib/components/event/event-metadata-expanded.svelte - src/lib/components/event/event-summary-row.svelte - src/lib/components/lines-and-dots/svg/group-details-text.svelte - src/lib/components/lines-and-dots/svg/timeline-graph-row.svelte - src/lib/components/schedule/schedule-form/schedule-input-payload.svelte - src/lib/components/standalone-activities/activity-input-and-outcome.svelte - src/lib/components/workflow/client-actions/update-confirmation-modal.svelte - src/lib/components/workflow/input-and-results-payload.svelte - src/lib/components/workflow/metadata/metadata-events.svelte - src/lib/components/workflow/pending-activity/pending-activity-card.svelte - src/lib/components/workflow/workflow-json-navigator.svelte - src/lib/pages/standalone-activity-details.svelte - src/lib/pages/standalone-activity-search-attributes.svelte - src/lib/pages/workflow-memo.svelte - src/lib/pages/workflow-metadata.svelte - src/lib/pages/workflow-search-attributes.svelte All 1730 tests pass.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
| let success; | ||
| let error = $state(''); | ||
| let loading = $state(false); | ||
| let failure = $state< |
There was a problem hiding this comment.
⚠️ Property 'failure' does not exist on type 'IOutcome | null | undefined'.
| let failure = $state< | ||
| UpdateWorkflowResponse['outcome']['failure'] | undefined | ||
| >(undefined); | ||
| let success = $state< |
There was a problem hiding this comment.
⚠️ Property 'success' does not exist on type 'IOutcome | null | undefined'.
|
Moves payload.svelte into src/lib/components/payload/ and adds a USAGE.md report documenting all 31 call sites grouped by feature area. Updates all 17 import paths accordingly.
… components Replace the 14-prop multi-mode payload.svelte with four single-purpose components: PayloadCodeBlock, PayloadSummary, PayloadDecoder, and PayloadInline. Each component carries only the props relevant to its render mode, eliminating dead props at call sites. Also updates the decode paths to use the renamed decode-payload.ts API (decodeEventAttributes, parsePayloadAttributes) following the #3302 cleanup.
Replace decodeEventAttributes with decodeUserMetadata — the correct decode path for a single raw Payload (Phase 2 codec only, no attribute tree walking). Also add onDecode callback prop, called after a successful decode, consistent with PayloadCodeBlock.
…lue utility Remove 3-way duplication of getInitialValue/onMount decode logic from payload-code-block, payload-decoder, and payload-inline by extracting getInitialPayloadValue and decodePayloadValue into src/lib/utilities/decode-payload-value.ts.
Components now re-decode when value or fieldName props change after mount, fixing the reactivity gap that caused stale decoded content when parent components updated their payload bindings.
| value: DecodableValue, | ||
| fieldName: string, | ||
| ): string => { | ||
| if (!value) return stringifyWithBigInt(value); |
There was a problem hiding this comment.
⚠️ Element implicitly has an 'any' type because expression of type 'string' can't be used to index type 'IPayloads | IPayload | WorkflowEvent | IUserMetadata | IWorkflowExecutionStartedEventAttributes | ... 59 more ... | Record<...>'.⚠️ Element implicitly has an 'any' type because expression of type 'string' can't be used to index type 'IPayloads | IPayload | WorkflowEvent | IUserMetadata | IWorkflowExecutionStartedEventAttributes | ... 59 more ... | Record<...>'.
| const convertedAttributes = await decodeEventAttributes( | ||
| value as PotentiallyDecodable | EventAttribute | WorkflowEvent | Memo, | ||
| ); | ||
| const decodedAttributes = parsePayloadAttributes( |
There was a problem hiding this comment.
⚠️ Argument of type 'WorkflowEvent | EventAttribute | IMemo | PotentiallyDecodable' is not assignable to parameter of type 'Optional<WorkflowEvent | EventAttribute | PotentiallyDecodable>'.
| ); | ||
| const decodedAttributes = parsePayloadAttributes( | ||
| convertedAttributes, | ||
| ) as object; |
There was a problem hiding this comment.
⚠️ Element implicitly has an 'any' type because expression of type 'string' can't be used to index type '{}'.
| const decodedAttributes = parsePayloadAttributes( | ||
| convertedAttributes, | ||
| ) as object; | ||
| const keyExists = fieldName && decodedAttributes?.[fieldName]; |
There was a problem hiding this comment.
⚠️ Element implicitly has an 'any' type because expression of type 'string' can't be used to index type '{}'.
| @@ -69,20 +73,22 @@ | |||
| </script> | |||
|
|
|||
| <div class="flex flex-col gap-1"> | |||
There was a problem hiding this comment.
⚠️ Type 'IPayloads | undefined' is not assignable to type 'object | PotentiallyDecodable'.
| @@ -17,22 +16,14 @@ | |||
| <div class="grid w-full grid-cols-2 gap-4 max-md:grid-cols-1"> | |||
| <div class="flex flex-col gap-2"> | |||
| <h5>Input</h5> | |||
There was a problem hiding this comment.
⚠️ Type 'IPayloads | undefined' is not assignable to type 'object | PotentiallyDecodable'.
| @@ -172,16 +172,7 @@ | |||
| {translate('workflows.heartbeat-details')} | |||
| </p> | |||
| {#key activity.attempt} | |||
There was a problem hiding this comment.
⚠️ Type 'IPayloads | null | undefined' is not assignable to type 'object | PotentiallyDecodable'.
| {/if} | ||
| {#if $activityExecution.info.header} | ||
| <div class="space-y-2"> | ||
| <p class="font-medium text-secondary">Header</p> |
There was a problem hiding this comment.
⚠️ Type '{ [k: string]: IPayload; } | null | undefined' is not assignable to type 'object | PotentiallyDecodable'.
| @@ -9,18 +8,7 @@ | |||
|
|
|||
| <div class="flex flex-col gap-2"> | |||
| {#if searchAttributes} | |||
There was a problem hiding this comment.
⚠️ Type 'Record<string, IPayload> | undefined' is not assignable to type 'object | PotentiallyDecodable'.
| @@ -85,15 +87,14 @@ export const getWorkflowStartedCompletedAndTaskFailedEvents = ( | |||
| } | |||
|
|
|||
There was a problem hiding this comment.
⚠️ Variable 'workflowStartedEvent' is used before being assigned.
| @@ -85,15 +87,14 @@ export const getWorkflowStartedCompletedAndTaskFailedEvents = ( | |||
| } | |||
|
|
|||
| if (workflowStartedEvent) { | |||
There was a problem hiding this comment.
⚠️ Type 'IPayloads | null' is not assignable to type 'IPayloads'.
| ); | ||
| null; | ||
| } | ||
|
|
There was a problem hiding this comment.
⚠️ Variable 'workflowCompletedEvent' is used before being assigned.
| } | ||
|
|
||
| if (workflowCompletedEvent) { | ||
| contAsNew = isWorkflowExecutionContinuedAsNewEvent(workflowCompletedEvent); |
There was a problem hiding this comment.
⚠️ Type 'IPayloads | (IWorkflowExecutionFailedEventAttributes & { type: "workflowExecutionFailedEventAttributes"; }) | (IWorkflowExecutionTimedOutEventAttributes & { ...; }) | (IWorkflowExecutionTerminatedEventAttributes & { ...; }) | (IWorkflowExecutionCanceledEventAttributes & { ...; }) | null | undefined' is not assignable to type 'IPayloads | (EventAttribute & IWorkflowExecutionCompletedEventAttributes & { type: "workflowExecutionCompletedEventAttributes"; }) | ... 4 more ... | (EventAttribute & ... 1 more ... & { ...; })'.
| results = getEventResult(workflowCompletedEvent); | ||
| } | ||
|
|
||
| return { |
There was a problem hiding this comment.
⚠️ Variable 'input' is used before being assigned.
| /> | ||
| </div> | ||
| {/snippet} | ||
|
|
There was a problem hiding this comment.
⚠️ Parameter 'key' implicitly has an 'any' type.⚠️ Parameter 'value' implicitly has an 'any' type.
rossnelson
left a comment
There was a problem hiding this comment.
Great work! I left a couple minor comments.
| startEvent?.attributes?.input, | ||
| )) as PotentiallyDecodable; | ||
| const decodedInput = await decodePayloadAndParseDataToJSON( | ||
| startEvent.attributes.input[0], |
| export const decodeLocalActivity = async ( | ||
| event: IterableEvent, | ||
| ): Promise<SummaryAttribute | undefined> => { | ||
| console.log(event); |
| class="overflow-hidden border border-subtle bg-code-block px-1 py-0.5 font-mono text-xs text-primary {className}" | ||
| > | ||
| <code> | ||
| <pre class="truncate">{decodedValue.slice(0, truncateAt)}</pre> |
| export const decodePayloadsAndParseDataToJSON = async ( | ||
| payloads: Payloads | null | undefined, | ||
| ): Promise<unknown[]> => { | ||
| const decoded = await decodePayloadsWithRemoteCodec(payloads.payloads); |
There was a problem hiding this comment.
The function signature implies that payloads could be null or undefined.
| const decoded = await decodePayloadsWithRemoteCodec(payloads.payloads); | |
| const decoded = await decodePayloadsWithRemoteCodec(payloads?.payloads); |
| $effect(() => { | ||
| if (!value) { | ||
| decodedValue = fallback; | ||
| return; | ||
| } | ||
| decodePayloadAndParseDataToJSON(value) | ||
| .then((result) => { | ||
| if (typeof result === 'string' && result) { | ||
| decodedValue = applyPrefix(result); | ||
| onDecode?.(decodedValue); | ||
| } else { | ||
| decodedValue = fallback; | ||
| } | ||
| }) | ||
| .catch(() => { | ||
| decodedValue = fallback; | ||
| }); | ||
| }); |
There was a problem hiding this comment.
Edge case, but there could be a race condition here if the component is re-rendered while still decoding. Maybe cancel on effect cleanup?




Description & motivation 💭
The monolithic component handled 3 rendering modes plus a headless decode mode through a single 14-prop interface, where most props were irrelevant to any given call site. This refactor replaces it with four focused components, each with a minimal prop surface.
New components
Shared decode utility
Extracted the repeated decodeEventAttributes → parsePayloadAttributes → stringifyWithBigInt pipeline from PayloadCodeBlock, PayloadDecoder, and PayloadInline into src/lib/utilities/decode-payload-value.ts. Exports decodePayloadValue and getInitialPayloadValue with a DecodableValue union type.
Correctness fixes
Screenshots (if applicable) 📸
Design Considerations 🎨
Testing 🧪
How was this tested 👻
Steps for others to test: 🚶🏽♂️🚶🏽♀️
pnpm dev,pnpm codec-server, andpnpm run-workflows.Optionally in addition to the above:
git worktree add /path/to/worktree main,cd /path/to/worktree,cp /path/to/ui/.env /path/to/worktree,pnpm i.VITE_APIenv variable in.envto"http://localhost:8081"preview.portinvite.config.tsto3031.pnpm build:localandpnpm preview:localhttp://localhost:3001to spot-check differences betweenmainand this branch.Checklists
Draft Checklist
Merge Checklist
Issue(s) closed
Docs
Any docs updates needed?