From a806734532a59c02cd5311901b19bbf7f7f732ea Mon Sep 17 00:00:00 2001 From: Laura Whitaker Date: Tue, 6 Oct 2026 21:24:03 -0700 Subject: [PATCH 1/4] fix(activities): show custom search attribute values in the activities table Custom search attribute columns on the standalone activities list were always empty: the table cell had no branch for them, and list items carried undecoded payloads. - Decode search attributes when loading the activities list (ActivityExecutionListInfo), reusing toSearchAttributes from the workflow model - Share custom search attribute rendering between the workflows and activities tables via isFilterableCustomSearchAttribute and a new table/search-attribute-value component - Read customSearchAttributes reactively in both cells so columns fill in once the namespace's search attributes load - Reuse toSearchAttributes in the activity workers tab - Remove the now-unused isCustomSearchAttribute Co-Authored-By: Claude Opus 5.5 (1M context) --- ...tivities-summary-configurable-table.svelte | 6 +- .../batch-actions.svelte | 4 +- .../table-body-cell.svelte | 35 ++++++++-- .../table-header-row.svelte | 6 +- .../table-row.svelte | 4 +- .../table/search-attribute-value.svelte | 38 +++++++++++ .../table-body-cell.svelte | 41 ++++-------- src/lib/models/workflow-execution.ts | 2 +- src/lib/pages/standalone-activities.svelte | 18 ++--- .../pages/standalone-activity-workers.svelte | 17 ++--- src/lib/services/activity-batch-service.ts | 6 +- .../services/standalone-activities.test.ts | 42 ++++++++++++ src/lib/services/standalone-activities.ts | 13 +++- src/lib/stores/search-attributes.test.ts | 67 +++++++++++++------ src/lib/stores/search-attributes.ts | 14 ++-- src/lib/types/activity-execution.ts | 12 +++- src/lib/utilities/delayed-activities.ts | 4 +- 17 files changed, 233 insertions(+), 96 deletions(-) create mode 100644 src/lib/components/table/search-attribute-value.svelte diff --git a/src/lib/components/standalone-activities/activities-summary-configurable-table.svelte b/src/lib/components/standalone-activities/activities-summary-configurable-table.svelte index 784ba1f61f..3ac7433812 100644 --- a/src/lib/components/standalone-activities/activities-summary-configurable-table.svelte +++ b/src/lib/components/standalone-activities/activities-summary-configurable-table.svelte @@ -23,7 +23,7 @@ resizeColumn, TABLE_TYPE, } from '$lib/stores/configurable-table-columns'; - import type { ActivityExecutionInfo } from '$lib/types/activity-execution'; + import type { ActivityExecutionListInfo } from '$lib/types/activity-execution'; import { getBatchSelectionTargets, getPageSelectionStatus, @@ -58,7 +58,7 @@ standaloneActivityBulkActionsEnabled(page) && $supportsAdvancedVisibility, ); - let visiblePaginatedItems: ActivityExecutionInfo[] = $state([]); + let visiblePaginatedItems: ActivityExecutionListInfo[] = $state([]); const pageSelectionStatus: PageSelectionStatus = $derived( getPageSelectionStatus( @@ -72,7 +72,7 @@ const handleSelectPage = ( isSelected: boolean, - activities: ActivityExecutionInfo[], + activities: ActivityExecutionListInfo[], ) => { selectActivities(isSelected, activities); prevClickedIndex = null; diff --git a/src/lib/components/standalone-activities/activities-summary-configurable-table/batch-actions.svelte b/src/lib/components/standalone-activities/activities-summary-configurable-table/batch-actions.svelte index bf7db83c3a..8653749a07 100644 --- a/src/lib/components/standalone-activities/activities-summary-configurable-table/batch-actions.svelte +++ b/src/lib/components/standalone-activities/activities-summary-configurable-table/batch-actions.svelte @@ -11,11 +11,11 @@ type ActivityBatchOperationContext, } from '$lib/pages/standalone-activities.svelte'; import { activitiesQuery, activityCount } from '$lib/stores/activities'; - import type { ActivityExecutionInfo } from '$lib/types/activity-execution'; + import type { ActivityExecutionListInfo } from '$lib/types/activity-execution'; import { standaloneActivityBulkActionsEnabled } from '$lib/utilities/standalone-activities-commands-disabled'; type Props = { - activities: ActivityExecutionInfo[]; + activities: ActivityExecutionListInfo[]; }; let { activities }: Props = $props(); diff --git a/src/lib/components/standalone-activities/activities-summary-configurable-table/table-body-cell.svelte b/src/lib/components/standalone-activities/activities-summary-configurable-table/table-body-cell.svelte index f535a9111f..2a2d366a1c 100644 --- a/src/lib/components/standalone-activities/activities-summary-configurable-table/table-body-cell.svelte +++ b/src/lib/components/standalone-activities/activities-summary-configurable-table/table-body-cell.svelte @@ -5,9 +5,14 @@ import { page } from '$app/state'; import ActivityStatusBadge from '$lib/components/standalone-activities/activity-status-badge.svelte'; + import SearchAttributeValue from '$lib/components/table/search-attribute-value.svelte'; import Timestamp from '$lib/components/timestamp.svelte'; import type { ConfigurableTableHeader } from '$lib/stores/configurable-table-columns'; - import type { ActivityExecutionInfo } from '$lib/types/activity-execution'; + import { + customSearchAttributes, + isFilterableCustomSearchAttribute, + } from '$lib/stores/search-attributes'; + import type { ActivityExecutionListInfo } from '$lib/types/activity-execution'; import { COLUMN_WIDTH_CLAMP_CLASSES, columnWidthStyle, @@ -21,7 +26,7 @@ type Props = { column: ConfigurableTableHeader; - activity: ActivityExecutionInfo; + activity: ActivityExecutionListInfo; }; let { column, activity }: Props = $props(); @@ -30,6 +35,17 @@ const filterableLabels = ['Activity ID', 'Run ID', 'Type', 'Task Queue']; + const customSearchAttributeValue = $derived( + activity.searchAttributes?.indexedFields?.[label], + ); + const isCustomKeywordOrTextAttribute = $derived( + isFilterableCustomSearchAttribute( + $customSearchAttributes, + label, + customSearchAttributeValue, + ), + ); + const className = $derived( twMerge( 'h-8 whitespace-nowrap', @@ -43,7 +59,7 @@ {#snippet renderFilterableTableCell( filterableCellProps: Pick< ComponentProps, - 'attribute' | 'value' | 'href' + 'attribute' | 'value' | 'href' | 'type' >, )} {/snippet} -{#if filterableLabels.includes(label)} +{#if filterableLabels.includes(label) || isCustomKeywordOrTextAttribute} {#if label === 'Activity ID'} {@render renderFilterableTableCell({ attribute: 'ActivityId', @@ -85,6 +101,12 @@ attribute: 'TaskQueue', value: activity.taskQueue ?? '', })} + {:else if isCustomKeywordOrTextAttribute} + {@render renderFilterableTableCell({ + attribute: label, + value: customSearchAttributeValue ?? '', + type: $customSearchAttributes[label], + })} {/if} {:else} @@ -105,6 +127,11 @@ {activity.stateTransitionCount ?? ''} {:else if label === 'Execution Time'} + {:else if label in $customSearchAttributes && customSearchAttributeValue !== undefined} + {/if} {/if} diff --git a/src/lib/components/standalone-activities/activities-summary-configurable-table/table-header-row.svelte b/src/lib/components/standalone-activities/activities-summary-configurable-table/table-header-row.svelte index 62eed514c3..9d3b50d34a 100644 --- a/src/lib/components/standalone-activities/activities-summary-configurable-table/table-header-row.svelte +++ b/src/lib/components/standalone-activities/activities-summary-configurable-table/table-header-row.svelte @@ -7,20 +7,20 @@ ACTIVITY_BATCH_OPERATION_CONTEXT, type ActivityBatchOperationContext, } from '$lib/pages/standalone-activities.svelte'; - import type { ActivityExecutionInfo } from '$lib/types/activity-execution'; + import type { ActivityExecutionListInfo } from '$lib/types/activity-execution'; import type { PageSelectionStatus } from '$lib/utilities/batch-selection'; import BatchActions from './batch-actions.svelte'; type Props = { - activities?: ActivityExecutionInfo[]; + activities?: ActivityExecutionListInfo[]; empty?: boolean; columnsCount?: number; showBatchActions?: boolean; pageSelectionStatus?: PageSelectionStatus; onSelectPage?: ( selected: boolean, - activities: ActivityExecutionInfo[], + activities: ActivityExecutionListInfo[], ) => void; children?: Snippet; }; diff --git a/src/lib/components/standalone-activities/activities-summary-configurable-table/table-row.svelte b/src/lib/components/standalone-activities/activities-summary-configurable-table/table-row.svelte index 3ce3d83e3c..5cc0e8aec8 100644 --- a/src/lib/components/standalone-activities/activities-summary-configurable-table/table-row.svelte +++ b/src/lib/components/standalone-activities/activities-summary-configurable-table/table-row.svelte @@ -11,11 +11,11 @@ type ActivityBatchOperationContext, activityKey, } from '$lib/pages/standalone-activities.svelte'; - import type { ActivityExecutionInfo } from '$lib/types/activity-execution'; + import type { ActivityExecutionListInfo } from '$lib/types/activity-execution'; import { standaloneActivityWriteActionsDisabled } from '$lib/utilities/standalone-activities-commands-disabled'; interface Props { - activity?: ActivityExecutionInfo; + activity?: ActivityExecutionListInfo; empty?: boolean; showBatchActions?: boolean; onClickBatchSelect?: (e: MouseEvent) => void; diff --git a/src/lib/components/table/search-attribute-value.svelte b/src/lib/components/table/search-attribute-value.svelte new file mode 100644 index 0000000000..2de064e1a2 --- /dev/null +++ b/src/lib/components/table/search-attribute-value.svelte @@ -0,0 +1,38 @@ + + +{#if type === SEARCH_ATTRIBUTE_TYPE.DATETIME && typeof value === 'string'} + +{:else if type === SEARCH_ATTRIBUTE_TYPE.BOOL} + +{:else} + + {truncate ? truncateValue(displayValue) : displayValue} + +{/if} diff --git a/src/lib/components/workflow/workflows-summary-configurable-table/table-body-cell.svelte b/src/lib/components/workflow/workflows-summary-configurable-table/table-body-cell.svelte index 4b8a4b773d..6ba266bb16 100644 --- a/src/lib/components/workflow/workflows-summary-configurable-table/table-body-cell.svelte +++ b/src/lib/components/workflow/workflows-summary-configurable-table/table-body-cell.svelte @@ -4,20 +4,17 @@ import { page } from '$app/state'; + import SearchAttributeValue from '$lib/components/table/search-attribute-value.svelte'; import Timestamp from '$lib/components/timestamp.svelte'; import WorkflowStatusBadge from '$lib/components/workflow/workflow-status-badge.svelte'; import Tooltip from '$lib/holocene/tooltip.svelte'; - import { Badge } from '$lib/io/badge'; import type { ConfigurableTableHeader } from '$lib/stores/configurable-table-columns'; import { customSearchAttributes, - isCustomSearchAttribute, + isFilterableCustomSearchAttribute, workflowIncludesSearchAttribute, } from '$lib/stores/search-attributes'; - import { - SEARCH_ATTRIBUTE_TYPE, - type WorkflowExecution, - } from '$lib/types/workflows'; + import type { WorkflowExecution } from '$lib/types/workflows'; import { COLUMN_WIDTH_CLAMP_CLASSES, columnWidthStyle, @@ -54,10 +51,11 @@ const { label, width } = $derived(column); const namespace = $derived(page.params.namespace); const isCustomKeywordOrTextAttribute = $derived( - isCustomSearchAttribute(label) && - ($customSearchAttributes[label] === SEARCH_ATTRIBUTE_TYPE.KEYWORD || - $customSearchAttributes[label] === SEARCH_ATTRIBUTE_TYPE.TEXT) && - typeof workflow.searchAttributes?.indexedFields?.[label] === 'string', + isFilterableCustomSearchAttribute( + $customSearchAttributes, + label, + workflow.searchAttributes?.indexedFields?.[label], + ), ); const filterableLabels = [ @@ -245,23 +243,12 @@ {/if} {:else if label === 'Change Version'} {workflow.searchAttributes?.indexedFields?.TemporalChangeVersion} - {:else if isCustomSearchAttribute(label) && workflowIncludesSearchAttribute(workflow, label)} - {@const content = workflow.searchAttributes?.indexedFields?.[label]} - {#if $customSearchAttributes[label] === SEARCH_ATTRIBUTE_TYPE.DATETIME && typeof content === 'string'} - - {:else if $customSearchAttributes[label] === SEARCH_ATTRIBUTE_TYPE.BOOL} - - {:else} - - {truncate ? truncateValue(content) : content} - - {/if} + {:else if label in $customSearchAttributes && workflowIncludesSearchAttribute(workflow, label)} + {/if} {/if} diff --git a/src/lib/models/workflow-execution.ts b/src/lib/models/workflow-execution.ts index c8453ae4f6..1e687340ef 100644 --- a/src/lib/models/workflow-execution.ts +++ b/src/lib/models/workflow-execution.ts @@ -67,7 +67,7 @@ const toCallbacks = (callbacks?: Callbacks): Callback[] => { }); }; -const toSearchAttributes = ( +export const toSearchAttributes = ( apiSearchAttributes: WorkflowSearchAttributes, ): DecodedWorkflowSearchAttributes => { if (!apiSearchAttributes || !apiSearchAttributes.indexedFields) return {}; diff --git a/src/lib/pages/standalone-activities.svelte b/src/lib/pages/standalone-activities.svelte index 42dddbc91e..660098ec73 100644 --- a/src/lib/pages/standalone-activities.svelte +++ b/src/lib/pages/standalone-activities.svelte @@ -1,27 +1,27 @@ @@ -120,9 +120,9 @@ selectItems: selectActivities, handleSelectAll, reset: resetSelection, - } = createBatchSelection(activityKey); + } = createBatchSelection(activityKey); - const isRunning = (activity: ActivityExecutionInfo) => + const isRunning = (activity: ActivityExecutionListInfo) => activity.status === 'ACTIVITY_EXECUTION_STATUS_RUNNING'; const cancelableActivities = derivedStore(selectedActivities, (activities) => diff --git a/src/lib/pages/standalone-activity-workers.svelte b/src/lib/pages/standalone-activity-workers.svelte index 61ccb11d1a..d932d109ca 100644 --- a/src/lib/pages/standalone-activity-workers.svelte +++ b/src/lib/pages/standalone-activity-workers.svelte @@ -1,12 +1,9 @@ ({ +}: ActivityExecutionListInfo): Execution => ({ type: ExecutionType.EXECUTION_TYPE_ACTIVITY, businessId: activityId ?? '', runId: runId ?? '', diff --git a/src/lib/services/standalone-activities.test.ts b/src/lib/services/standalone-activities.test.ts index 9c3110394b..32e3918d32 100644 --- a/src/lib/services/standalone-activities.test.ts +++ b/src/lib/services/standalone-activities.test.ts @@ -4,6 +4,7 @@ import type { StandaloneActivityFormData } from '$lib/components/standalone-acti import { toActivityCallbacks, + toActivityExecutionListInfo, toStartActivityExecutionRequest, } from './standalone-activities'; @@ -103,3 +104,44 @@ describe('toActivityCallbacks', () => { ).toEqual([]); }); }); + +describe('toActivityExecutionListInfo', () => { + it('decodes search attribute payloads', () => { + const activity = toActivityExecutionListInfo({ + status: 'ACTIVITY_EXECUTION_STATUS_RUNNING', + scheduleToCloseTimeout: '', + scheduleToStartTimeout: '', + startToCloseTimeout: '', + heartbeatTimeout: '', + stateTransitionCount: '', + currentRetryInterval: '', + searchAttributes: { + indexedFields: { + CustomKeyword: { + metadata: { encoding: btoa('json/plain') }, + data: btoa('"keyword-value"'), + }, + }, + }, + }); + + expect(activity.searchAttributes.indexedFields).toEqual({ + CustomKeyword: 'keyword-value', + }); + }); + + it('returns empty search attributes when there are none', () => { + const activity = toActivityExecutionListInfo({ + status: 'ACTIVITY_EXECUTION_STATUS_RUNNING', + scheduleToCloseTimeout: '', + scheduleToStartTimeout: '', + startToCloseTimeout: '', + heartbeatTimeout: '', + stateTransitionCount: '', + currentRetryInterval: '', + searchAttributes: {}, + }); + + expect(activity.searchAttributes).toEqual({}); + }); +}); diff --git a/src/lib/services/standalone-activities.ts b/src/lib/services/standalone-activities.ts index a51fa9b34b..9e1df910f5 100644 --- a/src/lib/services/standalone-activities.ts +++ b/src/lib/services/standalone-activities.ts @@ -15,11 +15,13 @@ import { isPayloadInputEncodingType, type PayloadInputEncoding, } from '$lib/models/payload-encoding'; +import { toSearchAttributes } from '$lib/models/workflow-execution'; import { activityError } from '$lib/stores/activities'; import type { ActivityOptions, Payload, SearchAttribute } from '$lib/types'; import type { ActivityExecution, ActivityExecutionInfo, + ActivityExecutionListInfo, StartActivityExecutionRequest, } from '$lib/types/activity-execution'; import type { Callback } from '$lib/types/nexus'; @@ -101,10 +103,17 @@ export interface StartStandaloneActivityResponse { started: boolean; } +export const toActivityExecutionListInfo = ( + activity: ActivityExecutionInfo, +): ActivityExecutionListInfo => ({ + ...activity, + searchAttributes: toSearchAttributes(activity.searchAttributes), +}); + export type PaginatedActivitiesPromise = ( pageSize?: number, token?: string, -) => Promise<{ items: ActivityExecutionInfo[]; nextPageToken: string }>; +) => Promise<{ items: ActivityExecutionListInfo[]; nextPageToken: string }>; export const fetchPaginatedActivities = async ( namespace: string, @@ -134,7 +143,7 @@ export const fetchPaginatedActivities = async ( }).then((response) => { const { executions = [], nextPageToken = '' } = response || {}; return { - items: executions, + items: executions.map(toActivityExecutionListInfo), nextPageToken: nextPageToken ? String(nextPageToken) : '', }; }); diff --git a/src/lib/stores/search-attributes.test.ts b/src/lib/stores/search-attributes.test.ts index 998318d52f..819676c762 100644 --- a/src/lib/stores/search-attributes.test.ts +++ b/src/lib/stores/search-attributes.test.ts @@ -1,30 +1,11 @@ import { describe, expect, test } from 'vitest'; import { - allSearchAttributes, - isCustomSearchAttribute, + isFilterableCustomSearchAttribute, workflowIncludesSearchAttribute, } from './search-attributes'; describe('search attributes store', () => { - describe('isCustomSearchAttribute', () => { - test('returns false when the key is not a custom search attribute', () => { - allSearchAttributes.set({ - systemAttributes: { WorkflowType: 'Keyword' }, - customAttributes: {}, - }); - expect(isCustomSearchAttribute('CustomBool')).toBe(false); - }); - - test('returns true when the key is a custom search attribute', () => { - allSearchAttributes.set({ - systemAttributes: { WorkflowType: 'Keyword' }, - customAttributes: { CustomBool: 'Bool' }, - }); - expect(isCustomSearchAttribute('CustomBool')).toBe(true); - }); - }); - describe('workflowIncludesSearchAttribute', () => { test('returns true when the search attribute is defined on the workflow', () => { const mockWorkflow = { @@ -79,4 +60,50 @@ describe('search attributes store', () => { ); }); }); + + describe('isFilterableCustomSearchAttribute', () => { + const customAttributes = { + CustomKeyword: 'Keyword', + CustomText: 'Text', + CustomInt: 'Int', + } as const; + + test('returns true for a string Keyword value', () => { + expect( + isFilterableCustomSearchAttribute( + customAttributes, + 'CustomKeyword', + 'value', + ), + ).toBe(true); + }); + + test('returns true for a string Text value', () => { + expect( + isFilterableCustomSearchAttribute(customAttributes, 'CustomText', 'a'), + ).toBe(true); + }); + + test('returns false for a non-string value', () => { + expect( + isFilterableCustomSearchAttribute( + customAttributes, + 'CustomKeyword', + undefined, + ), + ).toBe(false); + }); + + test('returns false for other types', () => { + expect( + isFilterableCustomSearchAttribute(customAttributes, 'CustomInt', '1'), + ).toBe(false); + }); + + test('returns false for unknown attributes', () => { + expect( + isFilterableCustomSearchAttribute(customAttributes, 'Unknown', 'a'), + ).toBe(false); + }); + }); }); diff --git a/src/lib/stores/search-attributes.ts b/src/lib/stores/search-attributes.ts index 59531f5834..d8eded04af 100644 --- a/src/lib/stores/search-attributes.ts +++ b/src/lib/stores/search-attributes.ts @@ -1,4 +1,4 @@ -import { derived, get, readable, type Readable, writable } from 'svelte/store'; +import { derived, readable, type Readable, writable } from 'svelte/store'; import { z } from 'zod/v3'; @@ -85,10 +85,14 @@ export const customSearchAttributeOptions: Readable< })); }); -export const isCustomSearchAttribute = (key: string) => { - const customSearchAttrs = get(customSearchAttributes); - return key in customSearchAttrs; -}; +export const isFilterableCustomSearchAttribute = ( + customAttributes: SearchAttributes, + attribute: string, + value: unknown, +): value is string => + typeof value === 'string' && + (customAttributes[attribute] === SEARCH_ATTRIBUTE_TYPE.KEYWORD || + customAttributes[attribute] === SEARCH_ATTRIBUTE_TYPE.TEXT); export const workflowIncludesSearchAttribute = ( workflow: WorkflowExecution, diff --git a/src/lib/types/activity-execution.ts b/src/lib/types/activity-execution.ts index 8a3a008bc1..7652716475 100644 --- a/src/lib/types/activity-execution.ts +++ b/src/lib/types/activity-execution.ts @@ -3,7 +3,10 @@ import type { temporal } from '@temporalio/proto'; import type { Callback } from '$lib/types/nexus'; import type { Failure, Payloads } from '.'; -import type { WorkflowSearchAttributes } from './workflows'; +import type { + DecodedWorkflowSearchAttributes, + WorkflowSearchAttributes, +} from './workflows'; // Enum values arrive over REST/JSON as their SCREAMING_SNAKE names, so these are // string unions of the proto enum keys (not the numeric proto enum). Keying off @@ -99,6 +102,13 @@ export interface ActivityExecutionInfo extends Omit< | null; } +export type ActivityExecutionListInfo = Omit< + ActivityExecutionInfo, + 'searchAttributes' +> & { + searchAttributes: DecodedWorkflowSearchAttributes; +}; + export interface ActivityExecution { runId: string; info: ActivityExecutionInfo; diff --git a/src/lib/utilities/delayed-activities.ts b/src/lib/utilities/delayed-activities.ts index 8e2bba98f3..dd62c538c0 100644 --- a/src/lib/utilities/delayed-activities.ts +++ b/src/lib/utilities/delayed-activities.ts @@ -1,6 +1,8 @@ import type { ActivityExecutionInfo } from '$lib/types/activity-execution'; import { isFuture } from '$lib/utilities/format-date'; -export const isActivityDelayed = (activity: ActivityExecutionInfo): boolean => { +export const isActivityDelayed = ( + activity: Pick, +): boolean => { return !!activity.executionTime && isFuture(activity.executionTime); }; From c1c352d07ed2eb60d341feb540dd96fe9d389bf2 Mon Sep 17 00:00:00 2001 From: Laura Whitaker Date: Wed, 7 Oct 2026 10:04:31 -0700 Subject: [PATCH 2/4] fix(nexus-operations, schedules): show custom search attribute values in tables - Decode search attributes when loading the standalone Nexus operations list, and render custom search attribute columns in its table cell, with filter and copy buttons for Keyword and Text values - Render custom search attribute values in the schedules table with the shared SearchAttributeValue component, decoding without mutating the schedule - Accept the proto SearchAttribute type in toSearchAttributes - Remove standalone-activities.test.ts Co-Authored-By: Claude Opus 5.5 (1M context) --- .../schedules-list/schedules-table-row.svelte | 16 +- .../table-body-cell.svelte | 30 +++- src/lib/models/workflow-execution.ts | 3 +- .../services/standalone-activities.test.ts | 147 ------------------ src/lib/services/standalone-activities.ts | 2 +- .../services/standalone-nexus-operations.ts | 13 +- src/lib/types/nexus-operation-execution.ts | 14 +- 7 files changed, 66 insertions(+), 159 deletions(-) delete mode 100644 src/lib/services/standalone-activities.test.ts diff --git a/src/lib/components/schedule/schedules-list/schedules-table-row.svelte b/src/lib/components/schedule/schedules-list/schedules-table-row.svelte index 3629418f07..eafdf2c8aa 100644 --- a/src/lib/components/schedule/schedules-list/schedules-table-row.svelte +++ b/src/lib/components/schedule/schedules-list/schedules-table-row.svelte @@ -4,17 +4,19 @@ import { goto } from '$app/navigation'; import { page } from '$app/state'; + import SearchAttributeValue from '$lib/components/table/search-attribute-value.svelte'; import Timestamp from '$lib/components/timestamp.svelte'; import WorkflowStatusBadge from '$lib/components/workflow/workflow-status-badge.svelte'; import Link from '$lib/holocene/link.svelte'; import TableCellWithFilterOrCopyButtons from '$lib/holocene/table/table-cell-with-filter-or-copy-buttons.svelte'; import { translate } from '$lib/i18n/translate'; + import { toSearchAttributes } from '$lib/models/workflow-execution'; import type { ConfigurableTableHeader } from '$lib/stores/configurable-table-columns'; + import { customSearchAttributes } from '$lib/stores/search-attributes'; import { COLUMN_WIDTH_CLAMP_CLASSES, columnWidthStyle, } from '$lib/utilities/column-width'; - import { parsePayloadAttributes } from '$lib/utilities/decode-payload'; import { routeForSchedule, routeForWorkflow, @@ -36,9 +38,8 @@ const status = $derived(schedule?.info?.paused ? 'Paused' : 'Running'); const spec = $derived(schedule?.info?.spec); - const searchAttributes = $derived(schedule?.searchAttributes ?? {}); - const decodedAttributes = $derived( - parsePayloadAttributes({ searchAttributes }), + const decodedSearchAttributes = $derived( + toSearchAttributes(schedule?.searchAttributes ?? {}).indexedFields ?? {}, ); const sortRecentActions = (recentActions: ScheduleActionResult[]) => { @@ -142,7 +143,12 @@ {:else} - {decodedAttributes?.searchAttributes?.indexedFields?.[label] ?? ''} + {#if decodedSearchAttributes[label] !== undefined} + + {/if} {/if} {/each} diff --git a/src/lib/components/standalone-nexus-operations/nexus-operations-summary-configurable-table/table-body-cell.svelte b/src/lib/components/standalone-nexus-operations/nexus-operations-summary-configurable-table/table-body-cell.svelte index c031b3e4c7..7dba143ede 100644 --- a/src/lib/components/standalone-nexus-operations/nexus-operations-summary-configurable-table/table-body-cell.svelte +++ b/src/lib/components/standalone-nexus-operations/nexus-operations-summary-configurable-table/table-body-cell.svelte @@ -3,8 +3,13 @@ import { page } from '$app/state'; + import SearchAttributeValue from '$lib/components/table/search-attribute-value.svelte'; import Timestamp from '$lib/components/timestamp.svelte'; import type { ConfigurableTableHeader } from '$lib/stores/configurable-table-columns'; + import { + customSearchAttributes, + isFilterableCustomSearchAttribute, + } from '$lib/stores/search-attributes'; import type { NexusOperationExecutionListInfo } from '$lib/types/nexus-operation-execution'; import { COLUMN_WIDTH_CLAMP_CLASSES, @@ -51,9 +56,20 @@ 'Service', 'Operation', ]; + + const customSearchAttributeValue = $derived( + operation.searchAttributes?.indexedFields?.[label], + ); + const isCustomKeywordOrTextAttribute = $derived( + isFilterableCustomSearchAttribute( + $customSearchAttributes, + label, + customSearchAttributeValue, + ), + ); -{#if filterableLabels.includes(label)} +{#if filterableLabels.includes(label) || isCustomKeywordOrTextAttribute} + {:else if isCustomKeywordOrTextAttribute} + {/if} {:else} @@ -129,6 +152,11 @@ {/if} {:else if label === 'State Transitions'} {operation.stateTransitionCount ?? ''} + {:else if label in $customSearchAttributes && customSearchAttributeValue !== undefined} + {/if} {/if} diff --git a/src/lib/models/workflow-execution.ts b/src/lib/models/workflow-execution.ts index 1e687340ef..e846bfeb7d 100644 --- a/src/lib/models/workflow-execution.ts +++ b/src/lib/models/workflow-execution.ts @@ -1,5 +1,6 @@ import { mapValues } from 'es-toolkit'; +import type { SearchAttribute } from '$lib/types'; import type { Callbacks, PendingActivity, @@ -68,7 +69,7 @@ const toCallbacks = (callbacks?: Callbacks): Callback[] => { }; export const toSearchAttributes = ( - apiSearchAttributes: WorkflowSearchAttributes, + apiSearchAttributes: WorkflowSearchAttributes | SearchAttribute, ): DecodedWorkflowSearchAttributes => { if (!apiSearchAttributes || !apiSearchAttributes.indexedFields) return {}; const decoded = mapValues(apiSearchAttributes.indexedFields, (payload) => diff --git a/src/lib/services/standalone-activities.test.ts b/src/lib/services/standalone-activities.test.ts deleted file mode 100644 index 32e3918d32..0000000000 --- a/src/lib/services/standalone-activities.test.ts +++ /dev/null @@ -1,147 +0,0 @@ -import { describe, expect, it } from 'vitest'; - -import type { StandaloneActivityFormData } from '$lib/components/standalone-activities/start-standalone-activity-form/types'; - -import { - toActivityCallbacks, - toActivityExecutionListInfo, - toStartActivityExecutionRequest, -} from './standalone-activities'; - -const baseFormData: StandaloneActivityFormData = { - identity: 'test', - namespace: 'default', - activityId: 'activity-1', - taskQueue: 'task-queue', - activityType: 'MyActivity', - startToCloseTimeout: '', - scheduleToCloseTimeout: '', - scheduleToStartTimeout: '', - input: '', - encoding: 'json/plain', - messageType: '', - searchAttributes: [], - summary: '', - details: '', - initialInterval: '', - backoffCoefficient: '', - maximumInterval: '', - maximumAttempts: '', -}; - -describe('toStartActivityExecutionRequest', () => { - it('omits retry policy numbers left empty', async () => { - const request = await toStartActivityExecutionRequest(baseFormData); - - expect(request.retryPolicy).toEqual({}); - }); - - it('coerces populated retry policy numbers to numbers', async () => { - const request = await toStartActivityExecutionRequest({ - ...baseFormData, - backoffCoefficient: '2', - maximumAttempts: '5', - }); - - expect(request.retryPolicy?.backoffCoefficient).toBe(2); - expect(request.retryPolicy?.maximumAttempts).toBe(5); - }); - - it('omits timeouts left empty and forwards provided ones', async () => { - const empty = await toStartActivityExecutionRequest(baseFormData); - - expect(empty.startToCloseTimeout).toBeUndefined(); - expect(empty.scheduleToCloseTimeout).toBeUndefined(); - - const provided = await toStartActivityExecutionRequest({ - ...baseFormData, - startToCloseTimeout: '30s', - }); - - expect(provided.startToCloseTimeout).toBe('30s'); - }); -}); - -describe('toActivityCallbacks', () => { - const activityCallbacks = [ - { - trigger: {}, - info: { - callback: { - nexus: { url: 'http://callback' }, - links: [ - { - workflowEvent: { - namespace: 'caller-ns', - workflowId: 'caller-wf', - runId: 'caller-run', - eventRef: { eventId: '7' }, - }, - }, - ], - }, - state: 'CALLBACK_STATE_SUCCEEDED', - attempt: 2, - }, - }, - ] as unknown as Parameters[0]; - - it('extracts nested info and makes state readable', () => { - const [callback] = toActivityCallbacks(activityCallbacks); - - expect(callback.state).toBe('Succeeded'); - expect(callback.attempt).toBe(2); - expect(callback.callback?.links).toHaveLength(1); - expect(callback.callback?.nexus?.url).toBe('http://callback'); - }); - - it('skips callbacks without info and handles empty input', () => { - expect(toActivityCallbacks(undefined)).toEqual([]); - expect( - toActivityCallbacks([{}] as unknown as Parameters< - typeof toActivityCallbacks - >[0]), - ).toEqual([]); - }); -}); - -describe('toActivityExecutionListInfo', () => { - it('decodes search attribute payloads', () => { - const activity = toActivityExecutionListInfo({ - status: 'ACTIVITY_EXECUTION_STATUS_RUNNING', - scheduleToCloseTimeout: '', - scheduleToStartTimeout: '', - startToCloseTimeout: '', - heartbeatTimeout: '', - stateTransitionCount: '', - currentRetryInterval: '', - searchAttributes: { - indexedFields: { - CustomKeyword: { - metadata: { encoding: btoa('json/plain') }, - data: btoa('"keyword-value"'), - }, - }, - }, - }); - - expect(activity.searchAttributes.indexedFields).toEqual({ - CustomKeyword: 'keyword-value', - }); - }); - - it('returns empty search attributes when there are none', () => { - const activity = toActivityExecutionListInfo({ - status: 'ACTIVITY_EXECUTION_STATUS_RUNNING', - scheduleToCloseTimeout: '', - scheduleToStartTimeout: '', - startToCloseTimeout: '', - heartbeatTimeout: '', - stateTransitionCount: '', - currentRetryInterval: '', - searchAttributes: {}, - }); - - expect(activity.searchAttributes).toEqual({}); - }); -}); diff --git a/src/lib/services/standalone-activities.ts b/src/lib/services/standalone-activities.ts index 9e1df910f5..f59663fe71 100644 --- a/src/lib/services/standalone-activities.ts +++ b/src/lib/services/standalone-activities.ts @@ -103,7 +103,7 @@ export interface StartStandaloneActivityResponse { started: boolean; } -export const toActivityExecutionListInfo = ( +const toActivityExecutionListInfo = ( activity: ActivityExecutionInfo, ): ActivityExecutionListInfo => ({ ...activity, diff --git a/src/lib/services/standalone-nexus-operations.ts b/src/lib/services/standalone-nexus-operations.ts index b0d7fa9efe..4a974dab24 100644 --- a/src/lib/services/standalone-nexus-operations.ts +++ b/src/lib/services/standalone-nexus-operations.ts @@ -2,6 +2,7 @@ import { isPayloadInputEncodingType, type PayloadInputEncoding, } from '$lib/models/payload-encoding'; +import { toSearchAttributes } from '$lib/models/workflow-execution'; import { nexusOperationError } from '$lib/stores/nexus-operations'; import type { SearchAttributesSchema } from '$lib/stores/search-attributes'; import type { @@ -15,6 +16,7 @@ import type { NexusOperationExecution, NexusOperationExecutionInfo, NexusOperationExecutionListInfo, + NexusOperationExecutionListInfoAPIResponse, StartNexusOperationRequest, } from '$lib/types/nexus-operation-execution'; import { decodePayloadAndParseDataToJSON } from '$lib/utilities/decode-payload'; @@ -30,7 +32,7 @@ import { import { routeForApi } from '$lib/utilities/route-for-api'; export type ListNexusOperationsResponse = { - operations: NexusOperationExecutionListInfo[]; + operations: NexusOperationExecutionListInfoAPIResponse[]; nextPageToken: string; }; @@ -53,6 +55,13 @@ const emptyNexusOperationExecution: NexusOperationExecution = { info: emptyNexusOperationExecutionInfo, }; +const toNexusOperationExecutionListInfo = ( + operation: NexusOperationExecutionListInfoAPIResponse, +): NexusOperationExecutionListInfo => ({ + ...operation, + searchAttributes: toSearchAttributes(operation.searchAttributes ?? {}), +}); + export type PaginatedNexusOperationsPromise = ( pageSize?: number, token?: string, @@ -90,7 +99,7 @@ export const fetchPaginatedNexusOperations = async ( }).then((response) => { const { operations = [], nextPageToken = '' } = response || {}; return { - items: operations, + items: operations.map(toNexusOperationExecutionListInfo), nextPageToken: nextPageToken ? String(nextPageToken) : '', }; }); diff --git a/src/lib/types/nexus-operation-execution.ts b/src/lib/types/nexus-operation-execution.ts index 2b174fb0e8..72a561c57a 100644 --- a/src/lib/types/nexus-operation-execution.ts +++ b/src/lib/types/nexus-operation-execution.ts @@ -6,7 +6,10 @@ import type { NexusOperationIdReusePolicy, StartNexusOperationExecutionRequest, } from '.'; -import type { WorkflowSearchAttributes } from './workflows'; +import type { + DecodedWorkflowSearchAttributes, + WorkflowSearchAttributes, +} from './workflows'; export type NexusOperationExecutionStatus = keyof typeof temporal.api.enums.v1.NexusOperationExecutionStatus; @@ -82,9 +85,16 @@ export interface NexusOperationExecutionListInfo extends Omit< closeTime: string; stateTransitionCount: string; executionDuration: string; - searchAttributes: WorkflowSearchAttributes; + searchAttributes: DecodedWorkflowSearchAttributes; } +export type NexusOperationExecutionListInfoAPIResponse = Omit< + NexusOperationExecutionListInfo, + 'searchAttributes' +> & { + searchAttributes?: WorkflowSearchAttributes; +}; + export interface NexusOperationExecution extends Omit< DescribeNexusOperationResponse, 'info' | 'longPollToken' From b202d72f431a4ee15825f260e5e77081d4fa0dc3 Mon Sep 17 00:00:00 2001 From: Laura Whitaker Date: Wed, 7 Oct 2026 10:09:09 -0700 Subject: [PATCH 3/4] refactor(search-attributes): move toSearchAttributes to decode-payload as parseSearchAttributes The helper is now shared by the workflows, activities, Nexus operations and schedules tables and the activity workers tab, so it no longer belongs in the workflow model. It sits next to parseRawPayloadToJSON and parsePayloadAttributes, and is renamed to match the file's "parse" (no codec) naming. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../schedules-list/schedules-table-row.svelte | 4 +-- src/lib/models/workflow-execution.ts | 22 ++----------- .../pages/standalone-activity-workers.svelte | 4 +-- src/lib/services/standalone-activities.ts | 8 +++-- .../services/standalone-nexus-operations.ts | 8 +++-- src/lib/utilities/decode-payload.test.ts | 33 +++++++++++++++++++ src/lib/utilities/decode-payload.ts | 21 +++++++++++- 7 files changed, 69 insertions(+), 31 deletions(-) diff --git a/src/lib/components/schedule/schedules-list/schedules-table-row.svelte b/src/lib/components/schedule/schedules-list/schedules-table-row.svelte index eafdf2c8aa..9926e5cb71 100644 --- a/src/lib/components/schedule/schedules-list/schedules-table-row.svelte +++ b/src/lib/components/schedule/schedules-list/schedules-table-row.svelte @@ -10,13 +10,13 @@ import Link from '$lib/holocene/link.svelte'; import TableCellWithFilterOrCopyButtons from '$lib/holocene/table/table-cell-with-filter-or-copy-buttons.svelte'; import { translate } from '$lib/i18n/translate'; - import { toSearchAttributes } from '$lib/models/workflow-execution'; import type { ConfigurableTableHeader } from '$lib/stores/configurable-table-columns'; import { customSearchAttributes } from '$lib/stores/search-attributes'; import { COLUMN_WIDTH_CLAMP_CLASSES, columnWidthStyle, } from '$lib/utilities/column-width'; + import { parseSearchAttributes } from '$lib/utilities/decode-payload'; import { routeForSchedule, routeForWorkflow, @@ -39,7 +39,7 @@ const status = $derived(schedule?.info?.paused ? 'Paused' : 'Running'); const spec = $derived(schedule?.info?.spec); const decodedSearchAttributes = $derived( - toSearchAttributes(schedule?.searchAttributes ?? {}).indexedFields ?? {}, + parseSearchAttributes(schedule?.searchAttributes ?? {}).indexedFields ?? {}, ); const sortRecentActions = (recentActions: ScheduleActionResult[]) => { diff --git a/src/lib/models/workflow-execution.ts b/src/lib/models/workflow-execution.ts index e846bfeb7d..8545b92b65 100644 --- a/src/lib/models/workflow-execution.ts +++ b/src/lib/models/workflow-execution.ts @@ -1,6 +1,3 @@ -import { mapValues } from 'es-toolkit'; - -import type { SearchAttribute } from '$lib/types'; import type { Callbacks, PendingActivity, @@ -11,14 +8,12 @@ import type { } from '$lib/types/events'; import type { Callback } from '$lib/types/nexus'; import type { - DecodedWorkflowSearchAttributes, ListWorkflowExecutionsResponse, MostRecentWorkflowVersionStamp, WorkflowExecution, WorkflowExecutionAPIResponse, - WorkflowSearchAttributes, } from '$lib/types/workflows'; -import { parseRawPayloadToJSON } from '$lib/utilities/decode-payload'; +import { parseSearchAttributes } from '$lib/utilities/decode-payload'; import { toCallbackStateReadable, toPendingActivityStateReadable, @@ -68,19 +63,6 @@ const toCallbacks = (callbacks?: Callbacks): Callback[] => { }); }; -export const toSearchAttributes = ( - apiSearchAttributes: WorkflowSearchAttributes | SearchAttribute, -): DecodedWorkflowSearchAttributes => { - if (!apiSearchAttributes || !apiSearchAttributes.indexedFields) return {}; - const decoded = mapValues(apiSearchAttributes.indexedFields, (payload) => - parseRawPayloadToJSON(payload), - ) as Record; - - return { - indexedFields: decoded, - }; -}; - const getStartDelay = ({ executionTime, startTime, @@ -101,7 +83,7 @@ export const toWorkflowExecution = ( response?: WorkflowExecutionAPIResponse, ): WorkflowExecution => { const info = response?.workflowExecutionInfo; - const searchAttributes = toSearchAttributes(info?.searchAttributes ?? {}); + const searchAttributes = parseSearchAttributes(info?.searchAttributes ?? {}); const memo = info?.memo ?? {}; const name = info?.type?.name ?? ''; const id = info?.execution?.workflowId ?? ''; diff --git a/src/lib/pages/standalone-activity-workers.svelte b/src/lib/pages/standalone-activity-workers.svelte index d932d109ca..a1bd3ea80e 100644 --- a/src/lib/pages/standalone-activity-workers.svelte +++ b/src/lib/pages/standalone-activity-workers.svelte @@ -2,8 +2,8 @@ import { page } from '$app/state'; import WorkersTable from '$lib/components/workers/workers-table/task-queue-workers-table.svelte'; - import { toSearchAttributes } from '$lib/models/workflow-execution'; import { activityWorkerCount } from '$lib/stores/activities'; + import { parseSearchAttributes } from '$lib/utilities/decode-payload'; import { activityExecution } from '$lib/utilities/standalone-activity-poller.svelte'; interface Props { @@ -20,7 +20,7 @@ ); const decodedSearchAttributes = $derived( - toSearchAttributes(searchAttributes ?? {}).indexedFields ?? {}, + parseSearchAttributes(searchAttributes ?? {}).indexedFields ?? {}, ); diff --git a/src/lib/services/standalone-activities.ts b/src/lib/services/standalone-activities.ts index f59663fe71..030a07ee20 100644 --- a/src/lib/services/standalone-activities.ts +++ b/src/lib/services/standalone-activities.ts @@ -15,7 +15,6 @@ import { isPayloadInputEncodingType, type PayloadInputEncoding, } from '$lib/models/payload-encoding'; -import { toSearchAttributes } from '$lib/models/workflow-execution'; import { activityError } from '$lib/stores/activities'; import type { ActivityOptions, Payload, SearchAttribute } from '$lib/types'; import type { @@ -26,7 +25,10 @@ import type { } from '$lib/types/activity-execution'; import type { Callback } from '$lib/types/nexus'; import { activityOptionsUpdateMask } from '$lib/utilities/activity-options-update-mask'; -import { decodePayloadAndParseDataToJSON } from '$lib/utilities/decode-payload'; +import { + decodePayloadAndParseDataToJSON, + parseSearchAttributes, +} from '$lib/utilities/decode-payload'; import { encodePayloads, setSearchAttributes, @@ -107,7 +109,7 @@ const toActivityExecutionListInfo = ( activity: ActivityExecutionInfo, ): ActivityExecutionListInfo => ({ ...activity, - searchAttributes: toSearchAttributes(activity.searchAttributes), + searchAttributes: parseSearchAttributes(activity.searchAttributes), }); export type PaginatedActivitiesPromise = ( diff --git a/src/lib/services/standalone-nexus-operations.ts b/src/lib/services/standalone-nexus-operations.ts index 4a974dab24..e924ad08cc 100644 --- a/src/lib/services/standalone-nexus-operations.ts +++ b/src/lib/services/standalone-nexus-operations.ts @@ -2,7 +2,6 @@ import { isPayloadInputEncodingType, type PayloadInputEncoding, } from '$lib/models/payload-encoding'; -import { toSearchAttributes } from '$lib/models/workflow-execution'; import { nexusOperationError } from '$lib/stores/nexus-operations'; import type { SearchAttributesSchema } from '$lib/stores/search-attributes'; import type { @@ -19,7 +18,10 @@ import type { NexusOperationExecutionListInfoAPIResponse, StartNexusOperationRequest, } from '$lib/types/nexus-operation-execution'; -import { decodePayloadAndParseDataToJSON } from '$lib/utilities/decode-payload'; +import { + decodePayloadAndParseDataToJSON, + parseSearchAttributes, +} from '$lib/utilities/decode-payload'; import { encodePayloads, setSearchAttributes, @@ -59,7 +61,7 @@ const toNexusOperationExecutionListInfo = ( operation: NexusOperationExecutionListInfoAPIResponse, ): NexusOperationExecutionListInfo => ({ ...operation, - searchAttributes: toSearchAttributes(operation.searchAttributes ?? {}), + searchAttributes: parseSearchAttributes(operation.searchAttributes ?? {}), }); export type PaginatedNexusOperationsPromise = ( diff --git a/src/lib/utilities/decode-payload.test.ts b/src/lib/utilities/decode-payload.test.ts index ddbd60e52d..67f9f0c9a4 100644 --- a/src/lib/utilities/decode-payload.test.ts +++ b/src/lib/utilities/decode-payload.test.ts @@ -7,6 +7,7 @@ import { decodeEventAttributes, parsePayloadAttributes, parseRawPayloadToJSON, + parseSearchAttributes, } from './decode-payload'; import { dataConvertedWorkflowStartedEvent, @@ -245,6 +246,38 @@ describe('parseRawPayloadToJSON with returnDataOnly = false', () => { }); }); +describe('parseSearchAttributes', () => { + it('parses indexed field payloads', () => { + expect( + parseSearchAttributes({ + indexedFields: { + CustomKeyword: { + metadata: { encoding: btoa('json/plain') }, + data: btoa('"keyword-value"'), + }, + CustomKeywordList: { + metadata: { encoding: btoa('json/plain') }, + data: btoa('["a","b"]'), + }, + }, + }), + ).toEqual({ + indexedFields: { + CustomKeyword: 'keyword-value', + CustomKeywordList: ['a', 'b'], + }, + }); + }); + + it('returns an empty object when there are no indexed fields', () => { + expect(parseSearchAttributes({})).toEqual({}); + }); + + it('returns an empty object when indexed fields are null', () => { + expect(parseSearchAttributes({ indexedFields: null })).toEqual({}); + }); +}); + describe('parsePayloadAttributes', () => { it('Should parsePayloadAttributes searchAttributes with indexedFields', () => { const payload = { diff --git a/src/lib/utilities/decode-payload.ts b/src/lib/utilities/decode-payload.ts index c7c00cc2a6..4799664a34 100644 --- a/src/lib/utilities/decode-payload.ts +++ b/src/lib/utilities/decode-payload.ts @@ -1,9 +1,15 @@ +import { mapValues } from 'es-toolkit'; + import { decodePayloadsWithCodec as callCodecEndpoint } from '$lib/services/data-encoder'; import type { DownloadEventHistorySetting } from '$lib/stores/events'; import { schemaForMessageType } from '$lib/system-nexus-endpoints'; -import type { Memo, Payload, Payloads } from '$lib/types'; +import type { Memo, Payload, Payloads, SearchAttribute } from '$lib/types'; import type { EventAttribute, WorkflowEvent } from '$lib/types/events'; import type { Optional, Replace } from '$lib/types/global'; +import type { + DecodedWorkflowSearchAttributes, + WorkflowSearchAttributes, +} from '$lib/types/workflows'; import { atob } from './atob'; import { @@ -162,6 +168,19 @@ export function parseRawPayloadToJSON( return payload; } +export const parseSearchAttributes = ( + apiSearchAttributes: WorkflowSearchAttributes | SearchAttribute, +): DecodedWorkflowSearchAttributes => { + if (!apiSearchAttributes || !apiSearchAttributes.indexedFields) return {}; + const decoded = mapValues(apiSearchAttributes.indexedFields, (payload) => + parseRawPayloadToJSON(payload), + ) as Record; + + return { + indexedFields: decoded, + }; +}; + /** * Phase 1 — synchronous, no network. * From 0a8cd982f52de24e72a625ce89117b4c045ff58a Mon Sep 17 00:00:00 2001 From: Laura Whitaker Date: Wed, 7 Oct 2026 10:29:30 -0700 Subject: [PATCH 4/4] Add null check for custom search attribute values --- .../table/search-attribute-value.svelte | 26 ++++++++++++------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/src/lib/components/table/search-attribute-value.svelte b/src/lib/components/table/search-attribute-value.svelte index 2de064e1a2..dfb9fdc8c7 100644 --- a/src/lib/components/table/search-attribute-value.svelte +++ b/src/lib/components/table/search-attribute-value.svelte @@ -20,19 +20,27 @@ let { value, type, truncate = false }: Props = $props(); const displayValue = $derived( - Array.isArray(value) ? value.join(', ') : String(value ?? ''), + Array.isArray(value) ? value.join(', ') : String(value), ); const hideTooltip = $derived( !truncate || truncateValue(displayValue).length <= TRUNCATE_LENGTH, ); -{#if type === SEARCH_ATTRIBUTE_TYPE.DATETIME && typeof value === 'string'} - -{:else if type === SEARCH_ATTRIBUTE_TYPE.BOOL} - -{:else} - - {truncate ? truncateValue(displayValue) : displayValue} - +{#if value != null} + {#if type === SEARCH_ATTRIBUTE_TYPE.DATETIME && typeof value === 'string'} + + {:else if type === SEARCH_ATTRIBUTE_TYPE.BOOL} + + {:else} + + {truncate ? truncateValue(displayValue) : displayValue} + + {/if} {/if}