Skip to content
Merged
Show file tree
Hide file tree
Changes from 30 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
0e10bed
Fix eslint warning
tegan-temporal Apr 22, 2026
10e370b
Migrate to Svelte 5 syntax
tegan-temporal Apr 24, 2026
6a0adfa
Add multiselect via shift key
tegan-temporal Apr 24, 2026
6c85195
Clear prev index after page selected or all selected trigger
tegan-temporal Apr 24, 2026
1503ae4
Make onClickBatchSelect optional, disable checkbox if child
tegan-temporal Apr 24, 2026
de8381b
Do not set child disabled
tegan-temporal Apr 24, 2026
e19f4ab
Support shift+click select within child workflows
tegan-temporal Apr 24, 2026
a6a3ea3
Update shift+click logic. Instead of multiple scopes, treat all visib…
tegan-temporal Apr 27, 2026
d187a05
Remove console log
tegan-temporal Apr 27, 2026
867f2cf
Fixup type
tegan-temporal Apr 27, 2026
0c4489f
Add tests
tegan-temporal Apr 27, 2026
65c88eb
Account for prevClickedRow being nullish
tegan-temporal Apr 27, 2026
bc3a849
Fix type
tegan-temporal Apr 27, 2026
6c7e94c
Fix some warnings
tegan-temporal Apr 27, 2026
48bc207
Fix type
tegan-temporal Apr 27, 2026
045a270
Fix type for onClickBatchSelect
tegan-temporal Apr 27, 2026
14a52aa
Fix type check
tegan-temporal Apr 27, 2026
ddb8af5
Use early return
tegan-temporal Apr 27, 2026
8c9e977
More warning fixes
tegan-temporal Apr 27, 2026
47f5f4b
Undo prop type change
tegan-temporal Apr 27, 2026
a507c61
Fix warning
tegan-temporal Apr 27, 2026
49e9421
Default query to empty string
tegan-temporal Apr 27, 2026
6c65863
Move comment to inside handler :\
tegan-temporal Apr 27, 2026
8204eaa
Move isChecked higher
tegan-temporal Apr 27, 2026
a7e605a
Fix race condition
tegan-temporal Apr 27, 2026
5477089
Fix the fix :P
tegan-temporal Apr 27, 2026
42959e5
Use runId as key
tegan-temporal Apr 27, 2026
2341487
use runId for check
tegan-temporal Apr 27, 2026
c074f6c
Show root rows as checked if allSelected
tegan-temporal Apr 28, 2026
4a645c6
Use map instead of set so we rely on runId for equality
tegan-temporal Apr 28, 2026
f5a8179
Fix header checkmark status
tegan-temporal Apr 28, 2026
5e7826f
Merge branch 'main' into DT-3657-support-shift-click-for-bulk-selecti…
tegan-temporal Apr 30, 2026
60f59e7
Address PR comments
tegan-temporal Apr 30, 2026
fa8c671
Apply suggestions from code review
tegan-temporal May 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
<script lang="ts">
import { page } from '$app/stores';
import { SvelteMap, SvelteSet } from 'svelte/reactivity';

import { getContext, type Snippet } from 'svelte';

import { page } from '$app/state';

import TableEmptyState from '$lib/components/workflow/workflows-summary-configurable-table/table-empty-state.svelte';
import Button from '$lib/holocene/button.svelte';
Expand All @@ -8,6 +12,10 @@
import PaginatedTable from '$lib/holocene/table/paginated-table/api-paginated.svelte';
import Tooltip from '$lib/holocene/tooltip.svelte';
import { translate } from '$lib/i18n/translate';
import {
BATCH_OPERATION_CONTEXT,
type BatchOperationContext,
} from '$lib/pages/workflows-with-new-search.svelte';
import {
fetchAllChildWorkflows,
fetchPaginatedWorkflows,
Expand All @@ -24,73 +32,150 @@
import TableHeaderRow from './workflows-summary-configurable-table/table-header-row.svelte';
import TableRow from './workflows-summary-configurable-table/table-row.svelte';

export let onClickConfigure: () => void;
interface Props {
onClickConfigure: () => void;
cloud?: Snippet;
}

$: ({ namespace } = $page.params);
$: baseColumns = $configurableTableColumns?.[namespace]?.workflows ?? [];
$: query = $page.url.searchParams.get('query');
let { onClickConfigure, cloud }: Props = $props();

$: hasVersioningFilter =
query?.includes('TemporalWorkerDeploymentVersion') ?? false;
$: hasVersioningBehaviorColumn = baseColumns.some(
(col) => col.label === 'Versioning Behavior',
const { allSelected, pageSelected, selectWorkflows } =
getContext<BatchOperationContext>(BATCH_OPERATION_CONTEXT);

const namespace = $derived(page.params.namespace);
const baseColumns = $derived(
$configurableTableColumns?.[namespace]?.workflows ?? [],
);
$: columns =
const query = $derived(page.url.searchParams.get('query'));

const hasVersioningFilter = $derived(
query?.includes('TemporalWorkerDeploymentVersion') ?? false,
);
const hasVersioningBehaviorColumn = $derived(
baseColumns.some((col) => col.label === 'Versioning Behavior'),
);
const columns = $derived(
hasVersioningFilter && !hasVersioningBehaviorColumn
? [...baseColumns, { label: 'Versioning Behavior' }]
: baseColumns;
: baseColumns,
);

let childrenIds: {
workflowId: string;
runId: string;
children: WorkflowExecution[];
}[] = [];
const visibleChildrenMap = new SvelteMap<string, WorkflowExecution[]>();

const clearChildren = () => {
childrenIds = [];
};
$effect(() => {
void $refresh;
void query;
visibleChildrenMap.clear();
Comment thread
tegan-temporal marked this conversation as resolved.
});

$: ($refresh, query, clearChildren());
const inFlightChildRequests = new SvelteSet<string>();
const toggleChildrenVisibility = async (workflow: WorkflowExecution) => {
const visibleChildren = visibleChildrenMap.get(workflow.runId);

const viewChildren = async (workflow: WorkflowExecution) => {
if (childrenActive(workflow)) {
childrenIds = childrenIds.filter(
(id) => id.workflowId !== workflow.id && id.runId !== workflow.runId,
);
} else {
if (visibleChildren?.length) {
// we are collapsing the children so if there is an inflight request
// we don't want it's resolution to reopen the children.
Comment thread
tegan-temporal marked this conversation as resolved.
Outdated
inFlightChildRequests.delete(workflow.runId);

visibleChildrenMap.delete(workflow.runId);
// deselect children when collapsing
selectWorkflows(false, visibleChildren);

// clear prevClickedRow if row is collapsing
if (
prevClickedRow?.rowType === 'child' &&
prevClickedRow.parentRow.value.runId === workflow.runId
) {
prevClickedRow = prevClickedRow.parentRow;
}

return;
}

if (inFlightChildRequests.has(workflow.runId)) return;

inFlightChildRequests.add(workflow.runId);
try {
const children = await fetchAllChildWorkflows(
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps out of scope for this PR and probably not causing any bugs atm, but wondering what you think about having some kind of AbortSignal for fetchAllChildWorkflows. Seems like it might be possible to expand, collapse, and then expand child workflows again before the first fetch has resolved.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah an abort signal would be nice. I didn't want to go down that rabbit hole for this PR though.

namespace,
workflow.id,
workflow.runId,
);
childrenIds = [
{ workflowId: workflow.id, runId: workflow.runId, children },
...childrenIds,
];
}
};

$: childrenActive = (workflow: WorkflowExecution) => {
return childrenIds.find(
(id) => id.workflowId === workflow.id && id.runId === workflow.runId,
);
if (inFlightChildRequests.has(workflow.runId)) {
visibleChildrenMap.set(workflow.runId, children);
}
} finally {
inFlightChildRequests.delete(workflow.runId);
}
};

$: onFetch = () => fetchPaginatedWorkflows(namespace, query);
const onFetch = $derived(() =>
fetchPaginatedWorkflows(
namespace,
// query can be null but this function signature only accepts undefined | string
query ?? undefined,
),
);

$: dense = $tableDensity === 'dense';
const dense = $derived($tableDensity === 'dense');

const setTableDensity = () => {
$tableDensity = dense ? 'comfortable' : 'dense';
viewFeature('tableDensity');
};

let visibleItems: WorkflowExecution[] = $state([]);
Comment thread
tegan-temporal marked this conversation as resolved.
Outdated

type VisibleRow =
| {
rowType: 'root';
childCount: number;
value: WorkflowExecution;
}
| {
rowType: 'child';
parentRow: Extract<VisibleRow, { rowType: 'root' }>;
value: WorkflowExecution;
};
const visibleRows: VisibleRow[] = $derived.by(() => {
return visibleItems.flatMap((workflow) => {
const visibleChildren = visibleChildrenMap.get(workflow.runId) ?? [];

const rootRow = {
rowType: 'root' as const,
childCount: visibleChildren.length,
value: workflow,
};

return [
rootRow,
...visibleChildren.map((c) => ({
rowType: 'child' as const,
parentRow: rootRow,
value: c,
})),
];
});
});

let prevClickedRow = $state<VisibleRow | null>(null);

$effect(() => {
void visibleItems;
void $allSelected;
void $pageSelected;
prevClickedRow = null;
});
</script>

{#key [namespace, query, $refresh]}
<PaginatedTable
total={$workflowCount.count}
{onFetch}
let:visibleItems
onItemsChange={(items) => {
visibleItems = items;
}}
aria-label={translate('common.workflows')}
pageSizeSelectLabel={translate('common.per-page')}
nextButtonLabel={translate('common.next')}
Expand All @@ -105,37 +190,67 @@
columnsCount={columns.length}
empty={visibleItems.length === 0}
slot="headers"
let:visibleItems
workflows={visibleItems}
>
{#each columns as column}
{#each columns as column (column)}
Comment thread
tegan-temporal marked this conversation as resolved.
Outdated
<TableHeaderCell {column} />
{/each}
</TableHeaderRow>
{#each visibleItems as workflow (`${workflow.id}:${workflow.runId}`)}
{#each visibleRows as row, visibleRowIndex (row.value.runId)}
{@const isChildRow = row.rowType === 'child'}
<TableRow
{workflow}
{viewChildren}
childCount={childrenActive(workflow)?.children.length}
workflow={row.value}
toggleChildrenVisibility={(workflow) => {
toggleChildrenVisibility(workflow);
Comment thread
tegan-temporal marked this conversation as resolved.
Outdated
}}
childCount={!isChildRow && row.childCount > 0
? row.childCount
: undefined}
child={isChildRow}
onClickBatchSelect={(event) => {
// this is required due to how the underlying Checkbox component
// get's it's onclick type from svelte event forwarding. It does not
Comment thread
tegan-temporal marked this conversation as resolved.
Outdated
// know what the current event target type is a checkbox input
if (!(event.currentTarget instanceof HTMLInputElement)) {
return;
}

const isChecked = event.currentTarget.checked;

let targetedWorkflows = [row.value];

const prevClickedRowIndex = visibleRows.findIndex(
(r) => r.value.runId === prevClickedRow?.value.runId,
);

if (event.shiftKey && prevClickedRowIndex >= 0) {
const rangeStartInclusive = Math.min(
prevClickedRowIndex,
visibleRowIndex,
);
const rangeEndInclusive = Math.max(
prevClickedRowIndex,
visibleRowIndex,
);

// end of the slice range is exclusive, so add 1 to include the full range
targetedWorkflows = visibleRows
.slice(rangeStartInclusive, rangeEndInclusive + 1)
.map((r) => r.value);
}

selectWorkflows(isChecked, targetedWorkflows);

prevClickedRow = row;
}}
>
{#each columns as column}
<TableBodyCell {workflow} {column} truncate={dense} />
{#each columns as column (column)}
Comment thread
tegan-temporal marked this conversation as resolved.
Outdated
<TableBodyCell workflow={row.value} {column} truncate={dense} />
{/each}
</TableRow>
{#if childrenActive(workflow)}
{#each childrenActive(workflow).children as child (`${child.id}:${child.runId}`)}
<TableRow workflow={child} child>
{#each columns as column}
<TableBodyCell workflow={child} {column} truncate={dense} />
{/each}
</TableRow>
{/each}
{/if}
{/each}
<svelte:fragment slot="empty">
<TableEmptyState>
<slot name="cloud" slot="cloud" />
</TableEmptyState>
<TableEmptyState {cloud} />
</svelte:fragment>
<svelte:fragment slot="actions-end-additional" let:visibleItems let:page>
<Tooltip
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,20 @@

import StartWorkflowButton from '../start-workflow-button.svelte';

export let workflow: WorkflowExecution | undefined = undefined;
export let workflow: WorkflowExecution;
export let empty = false;
export let viewChildren: (workflow?: WorkflowExecution) => void = () => {};
export let toggleChildrenVisibility: (
workflow: WorkflowExecution,
) => void = () => {};
export let childCount: number | undefined = undefined;
export let child = false;

const { allSelected, selectedWorkflows } = getContext<BatchOperationContext>(
BATCH_OPERATION_CONTEXT,
);

export let onClickBatchSelect: (e: MouseEvent) => void = () => {};

$: ({ namespace } = $page.params);

$: parentWorkflows =
Expand All @@ -40,6 +44,10 @@
});

$: childrenShown = childCount !== undefined;

$: checked =
($allSelected && !child) ||
$selectedWorkflows.some((selected) => selected.runId === workflow.runId);
</script>

<tr
Expand All @@ -51,9 +59,11 @@
{#if !empty && $supportsBulkActions}
<td class="relative">
<Checkbox
data-testid="batch-checkbox"
{label}
labelHidden
bind:group={$selectedWorkflows}
on:click={onClickBatchSelect}
{checked}
value={workflow}
disabled={$allSelected}
aria-label={label}
Expand All @@ -80,11 +90,11 @@
<Button
size="xs"
variant={childrenShown ? 'primary' : 'ghost'}
on:click={() => viewChildren(workflow)}
on:click={() => toggleChildrenVisibility(workflow)}
class={$tableDensity === 'dense' ? 'mt-1 h-5 w-5' : ''}
>
<Tooltip
text={childrenShown
text={childrenShown && childCount != null
Comment thread
tegan-temporal marked this conversation as resolved.
Outdated
? translate('workflows.children', { count: childCount })
: translate('workflows.show-children')}
topLeft
Expand Down
Loading
Loading