From 3105c33e4a052d7f2c8e357907bbe935547da10d Mon Sep 17 00:00:00 2001 From: Chet Joswig Date: Sun, 15 Feb 2026 20:40:45 -0800 Subject: [PATCH 01/18] activity search form --- src/components/menus/AppMenu.svelte | 5 + src/components/search/Search.svelte | 20 ++++ src/components/search/SearchPanel.svelte | 119 +++++++++++++++++++++ src/components/search/SearchResults.svelte | 58 ++++++++++ src/routes/search/+layout.svelte | 14 +++ src/routes/search/+page.svelte | 13 +++ src/routes/search/+page.ts | 9 ++ src/stores/search.ts | 9 ++ src/types/activity.ts | 16 ++- src/utilities/effects.ts | 100 +++++++++++++++++ src/utilities/gql.ts | 27 +++++ 11 files changed, 389 insertions(+), 1 deletion(-) create mode 100644 src/components/search/Search.svelte create mode 100644 src/components/search/SearchPanel.svelte create mode 100644 src/components/search/SearchResults.svelte create mode 100644 src/routes/search/+layout.svelte create mode 100644 src/routes/search/+page.svelte create mode 100644 src/routes/search/+page.ts create mode 100644 src/stores/search.ts diff --git a/src/components/menus/AppMenu.svelte b/src/components/menus/AppMenu.svelte index 87c5a0fc33..39ed9f5eb2 100644 --- a/src/components/menus/AppMenu.svelte +++ b/src/components/menus/AppMenu.svelte @@ -21,6 +21,7 @@ Info, LogOut, Network, + Search as SearchIcon, Tags, } from 'lucide-svelte'; import PlanDevWordmarkDark from '../../assets/plandev-logo-dark.svg?component'; @@ -95,6 +96,10 @@ External Sources + + + Activity Search + diff --git a/src/components/search/Search.svelte b/src/components/search/Search.svelte new file mode 100644 index 0000000000..8bf554be01 --- /dev/null +++ b/src/components/search/Search.svelte @@ -0,0 +1,20 @@ + + + + + + + + + + + diff --git a/src/components/search/SearchPanel.svelte b/src/components/search/SearchPanel.svelte new file mode 100644 index 0000000000..041bca7419 --- /dev/null +++ b/src/components/search/SearchPanel.svelte @@ -0,0 +1,119 @@ + + + + + + + Search for activities across plans + + + +
+
+ + +
+
+ + +
+
+ + + + +
+
+ + +
+
+ +
+
+
+
diff --git a/src/components/search/SearchResults.svelte b/src/components/search/SearchResults.svelte new file mode 100644 index 0000000000..ded0f8d4ee --- /dev/null +++ b/src/components/search/SearchResults.svelte @@ -0,0 +1,58 @@ + + + + + + + Results + + + + + + diff --git a/src/routes/search/+layout.svelte b/src/routes/search/+layout.svelte new file mode 100644 index 0000000000..14cbc08a93 --- /dev/null +++ b/src/routes/search/+layout.svelte @@ -0,0 +1,14 @@ + + + + + + + + + diff --git a/src/routes/search/+page.svelte b/src/routes/search/+page.svelte new file mode 100644 index 0000000000..419849178d --- /dev/null +++ b/src/routes/search/+page.svelte @@ -0,0 +1,13 @@ + + + + + + + diff --git a/src/routes/search/+page.ts b/src/routes/search/+page.ts new file mode 100644 index 0000000000..ee8329053d --- /dev/null +++ b/src/routes/search/+page.ts @@ -0,0 +1,9 @@ +import type { PageLoad } from './$types'; + +export const load: PageLoad = async ({ parent }) => { + const { user } = await parent(); + + return { + user, + }; +}; diff --git a/src/stores/search.ts b/src/stores/search.ts new file mode 100644 index 0000000000..3faae0b04f --- /dev/null +++ b/src/stores/search.ts @@ -0,0 +1,9 @@ +import { writable, type Writable } from 'svelte/store'; +import type { ActivityDirectiveSearchResult } from '../types/activity'; + +/* Writeable. */ +export const searchColumns: Writable = writable('1fr 3px 1fr'); + +export const hasSearched: Writable = writable(false); + +export const searchResults: Writable = writable(null); diff --git a/src/types/activity.ts b/src/types/activity.ts index 5463c2bdea..79a46f8fd7 100644 --- a/src/types/activity.ts +++ b/src/types/activity.ts @@ -4,8 +4,9 @@ import type { PartialWith, UserId } from './app'; import type { ActivityDirectiveValidationFailures } from './errors'; import type { ExpansionRuleSlim } from './expansion'; import type { ArgumentsMap, ParametersMap } from './parameter'; +import type { PlanSchema } from './plan'; import type { ValueSchema } from './schema'; -import type { Tag } from './tags'; +import type { Tag, TagsInsertInput } from './tags'; export type ActivityType = { computed_attributes_value_schema: ValueSchema; @@ -131,3 +132,16 @@ export type PlanSnapshotActivity = Omit & { snapshot_id: number; }; + +export interface ActivityDirectiveSearchResult { + applied_preset: AppliedPreset['preset_applied']['name'] | null; + arguments: ArgumentsMap; + directive_id: number; + name: string; + plan: Pick; + plan_id: number; + tags: { + tag: TagsInsertInput; + }[]; + type: string; +} diff --git a/src/utilities/effects.ts b/src/utilities/effects.ts index cff59b230d..46743137b3 100644 --- a/src/utilities/effects.ts +++ b/src/utilities/effects.ts @@ -86,6 +86,7 @@ import type { ActivityDirectiveId, ActivityDirectiveInsertInput, ActivityDirectiveRevision, + ActivityDirectiveSearchResult, ActivityDirectiveSetInput, ActivityPreset, ActivityPresetId, @@ -7199,6 +7200,105 @@ const effects = { } }, + async searchActivities( + filterArgType: string, + filterActName: string, + filterArgs: [name: string, value: string | number | boolean][], + filterTagValue: string, + user: User | null, + ): Promise { + try { + const clauses = []; + if (filterArgType) { + clauses.push({ + type: { + _eq: filterArgType, + }, + }); + } + if (filterActName) { + clauses.push({ + name: { + _ilike: `%${filterActName}%`, + }, + }); + } + if (filterTagValue) { + clauses.push({ + tags: { + tag: { + name: { + _eq: filterTagValue, + }, + }, + }, + }); + } + + for (const [argName, argValue] of filterArgs) { + if (argName === '' && argValue === '') { + continue; + } else if (argName === '') { + clauses.push({ + arguments: { + _cast: { + String: { + _ilike: `%${argValue}%`, + }, + }, + }, + }); + } else if (argValue === '') { + clauses.push({ + arguments: { + _has_key: argName, + }, + }); + } else if (typeof argValue === 'string') { + clauses.push({ + arguments: { + _contains: { + [argName]: argValue, + }, + }, + }); + } else if (typeof argValue === 'number' || typeof argValue === 'boolean') { + clauses.push({ + _or: [ + { + arguments: { + _contains: { + [argName]: argValue, + }, + }, + }, + { + arguments: { + _contains: { + [argName]: argValue.toString(), + }, + }, + }, + ], + }); + } + } + + const data = await reqHasura( + gql.SEARCH_ACTIVITIES, + { searchFilter: { _and: clauses } }, + user, + ); + if (data.activity_directive) { + return data.activity_directive; + } + } catch (e) { + catchError('Search Failed', e as Error); + showFailureToast('Search Failed'); + } + return null; + }, + async sendActionSecretParameters( workspace: Workspace, secretParameters: any, diff --git a/src/utilities/gql.ts b/src/utilities/gql.ts index 83c85f426a..f293656d26 100644 --- a/src/utilities/gql.ts +++ b/src/utilities/gql.ts @@ -2052,6 +2052,33 @@ const gql = { } `, + SEARCH_ACTIVITIES: `#graphql + query SearchActivities($searchFilter: activity_directive_bool_exp!) { + ${Queries.ACTIVITY_DIRECTIVES}(where: $searchFilter) { + name + directive_id: id + type + plan_id + plan { + name + model_id + } + arguments + tags { + tag { + name + color + } + } + applied_preset { + preset_applied { + name + } + } + } + } + `, + SIMULATE: `#graphql query Simulate($planId: Int!, $force: Boolean!) { ${Queries.SIMULATE}(planId: $planId, force: $force) { From 3bbeaa67903c79654b0135c0116cc39a5531177a Mon Sep 17 00:00:00 2001 From: Chet Joswig Date: Mon, 16 Feb 2026 12:50:03 -0800 Subject: [PATCH 02/18] limit search results, define permissions, display preset --- src/components/search/SearchResults.svelte | 8 ++++++++ src/enums/gql.ts | 1 + src/types/activity.ts | 1 + src/utilities/effects.ts | 2 +- src/utilities/gql.ts | 21 +++++++++++---------- src/utilities/permissions.ts | 3 +++ 6 files changed, 25 insertions(+), 11 deletions(-) diff --git a/src/components/search/SearchResults.svelte b/src/components/search/SearchResults.svelte index ded0f8d4ee..eda32ff928 100644 --- a/src/components/search/SearchResults.svelte +++ b/src/components/search/SearchResults.svelte @@ -24,6 +24,14 @@ field: 'plan.name', headerName: 'Plan Name', }, + { + field: 'applied_preset.preset_applied.name', + headerName: 'Applied Preset', + }, + { + field: 'last_modified_at', + headerName: 'Last Modified', + }, ]; function getUrlForActivity(activity: ActivityDirectiveSearchResult): string { diff --git a/src/enums/gql.ts b/src/enums/gql.ts index 1961ab4770..e458af33ef 100644 --- a/src/enums/gql.ts +++ b/src/enums/gql.ts @@ -204,6 +204,7 @@ export enum Queries { SCHEDULING_SPECIFICATION = 'scheduling_specification_by_pk', SCHEDULING_SPECIFICATION_CONDITIONS = 'scheduling_specification_conditions', SCHEDULING_SPECIFICATION_GOALS = 'scheduling_specification_goals', + SEARCH_ACTIVITIES = 'search_activities', SEEN_SOURCES = 'seen_sources', SEQUENCE = 'sequence', SEQUENCE_ADAPTATION = 'sequence_adaptation', diff --git a/src/types/activity.ts b/src/types/activity.ts index 79a46f8fd7..992f298f49 100644 --- a/src/types/activity.ts +++ b/src/types/activity.ts @@ -137,6 +137,7 @@ export interface ActivityDirectiveSearchResult { applied_preset: AppliedPreset['preset_applied']['name'] | null; arguments: ArgumentsMap; directive_id: number; + last_modified_at: string; name: string; plan: Pick; plan_id: number; diff --git a/src/utilities/effects.ts b/src/utilities/effects.ts index 46743137b3..c26a7c5c72 100644 --- a/src/utilities/effects.ts +++ b/src/utilities/effects.ts @@ -7286,7 +7286,7 @@ const effects = { const data = await reqHasura( gql.SEARCH_ACTIVITIES, - { searchFilter: { _and: clauses } }, + { limit: 500, searchFilter: { _and: clauses } }, user, ); if (data.activity_directive) { diff --git a/src/utilities/gql.ts b/src/utilities/gql.ts index f293656d26..a2d3b17947 100644 --- a/src/utilities/gql.ts +++ b/src/utilities/gql.ts @@ -2053,28 +2053,29 @@ const gql = { `, SEARCH_ACTIVITIES: `#graphql - query SearchActivities($searchFilter: activity_directive_bool_exp!) { - ${Queries.ACTIVITY_DIRECTIVES}(where: $searchFilter) { - name + query SearchActivities($searchFilter: activity_directive_bool_exp!, $limit: Int!) { + ${Queries.ACTIVITY_DIRECTIVES}(where: $searchFilter, order_by: { start_offset: desc }, limit: $limit) { + applied_preset { + preset_applied { + name + } + } + arguments directive_id: id - type + name + last_modified_at plan_id plan { name model_id } - arguments tags { tag { name color } } - applied_preset { - preset_applied { - name - } - } + type } } `, diff --git a/src/utilities/permissions.ts b/src/utilities/permissions.ts index 0e6ea43465..85a5a28121 100644 --- a/src/utilities/permissions.ts +++ b/src/utilities/permissions.ts @@ -949,6 +949,9 @@ const queryPermissions: Record b const queries = [Queries.SCHEDULE]; return isUserAdmin(user) || (getPermission(queries, user) && getRolePlanPermission(queries, user, plan, model)); }, + SEARCH_ACTIVITIES: (user: User | null): boolean => { + return isUserAdmin(user) || getPermission([Queries.SEARCH_ACTIVITIES], user); + }, SIMULATE: (user: User | null, plan: PlanWithOwners, model: ModelWithOwner | null): boolean => { const queries = [Queries.SIMULATE]; return isUserAdmin(user) || (getPermission(queries, user) && getRolePlanPermission(queries, user, plan, model)); From 890cc55c14400d83a68a2888b0bed279af34133a Mon Sep 17 00:00:00 2001 From: Chet Joswig Date: Tue, 24 Feb 2026 15:10:12 -0800 Subject: [PATCH 03/18] search presets --- src/components/search/SearchPanel.svelte | 166 ++++++++++++++++++++--- src/types/model.ts | 2 + src/utilities/effects.ts | 22 +++ src/utilities/gql.ts | 17 +++ src/utilities/permissions.ts | 1 + 5 files changed, 186 insertions(+), 22 deletions(-) diff --git a/src/components/search/SearchPanel.svelte b/src/components/search/SearchPanel.svelte index 041bca7419..d73fd0a7f9 100644 --- a/src/components/search/SearchPanel.svelte +++ b/src/components/search/SearchPanel.svelte @@ -1,20 +1,76 @@ @@ -46,16 +141,44 @@
- - + +
+ (selectedModel = $models.find(model => model.id === v?.value))} + loop={false} + > + + + + + All Models + {#each orderedModels as model (model.id)} + + {model.name} +
(Version: {model.version})
+
+ {/each} +
+ +
+
+
+
+ +
@@ -93,17 +216,15 @@
- + +
+
+ +
+ +
Search
diff --git a/src/types/model.ts b/src/types/model.ts index 6c36cd2eca..37c1557936 100644 --- a/src/types/model.ts +++ b/src/types/model.ts @@ -34,6 +34,7 @@ export type ModelLog = { }; export type ModelSchema = { + activity_types: { name: string; parameters: ParametersMap }[]; constraint_specification: ConstraintModelSpecification[]; created_at: string; default_view_id: number | null; @@ -57,6 +58,7 @@ export type ModelSchema = { export type ModelSlim = Pick< Model, + | 'activity_types' | 'created_at' | 'description' | 'id' diff --git a/src/utilities/effects.ts b/src/utilities/effects.ts index c26a7c5c72..5196d3b080 100644 --- a/src/utilities/effects.ts +++ b/src/utilities/effects.ts @@ -7201,14 +7201,25 @@ const effects = { }, async searchActivities( + modelId: number | undefined, filterArgType: string, filterActName: string, filterArgs: [name: string, value: string | number | boolean][], filterTagValue: string, + filterPreset: string, user: User | null, ): Promise { try { const clauses = []; + if (modelId !== undefined && modelId !== null) { + clauses.push({ + plan: { + model_id: { + _eq: modelId, + }, + }, + }); + } if (filterArgType) { clauses.push({ type: { @@ -7234,6 +7245,17 @@ const effects = { }, }); } + if (filterPreset) { + clauses.push({ + applied_preset: { + preset_applied: { + name: { + _eq: filterPreset, + }, + }, + }, + }); + } for (const [argName, argValue] of filterArgs) { if (argName === '' && argValue === '') { diff --git a/src/utilities/gql.ts b/src/utilities/gql.ts index a2d3b17947..b6fbbb86db 100644 --- a/src/utilities/gql.ts +++ b/src/utilities/gql.ts @@ -2265,6 +2265,19 @@ const gql = { } `, + SUB_ACTIVITY_PRESETS_ALL: `#graphql + subscription SubActivityPresetsAll { + ${Queries.ACTIVITY_PRESETS} { + id + model_id + name + associated_activity_type + arguments + owner + } + } + `, + SUB_ACTIVITY_TYPES: `#graphql subscription SubActivityTypes($modelId: Int!) { ${Queries.ACTIVITY_TYPES}(where: { model_id: { _eq: $modelId } }, order_by: { name: asc }) { @@ -2751,6 +2764,10 @@ const gql = { SUB_MODELS: `#graphql subscription SubModels { models: ${Queries.MISSION_MODELS}(order_by: { name: asc }) { + activity_types { + name + parameters + } created_at description id diff --git a/src/utilities/permissions.ts b/src/utilities/permissions.ts index 85a5a28121..d38b57626c 100644 --- a/src/utilities/permissions.ts +++ b/src/utilities/permissions.ts @@ -971,6 +971,7 @@ const queryPermissions: Record b SUB_ACTIVITY_PRESETS: (user: User | null): boolean => { return isUserAdmin(user) || getPermission([Queries.ACTIVITY_PRESETS], user); }, + SUB_ACTIVITY_PRESETS_ALL: () => true, SUB_ACTIVITY_TYPES: () => true, SUB_ANCHOR_VALIDATION_STATUS: () => true, SUB_CHANNEL_DICTIONARIES: () => true, From c42c4001d71fc4887aee43eb058e19fa58b9225f Mon Sep 17 00:00:00 2001 From: Chet Joswig Date: Tue, 24 Feb 2026 15:22:55 -0800 Subject: [PATCH 04/18] updated test models --- src/components/plan/PlanMergeReview.test.ts | 1 + src/utilities/activities.test.ts | 1 + src/utilities/plan.test.ts | 3 +++ 3 files changed, 5 insertions(+) diff --git a/src/components/plan/PlanMergeReview.test.ts b/src/components/plan/PlanMergeReview.test.ts index e7b5f40067..3026170e4f 100644 --- a/src/components/plan/PlanMergeReview.test.ts +++ b/src/components/plan/PlanMergeReview.test.ts @@ -105,6 +105,7 @@ const mockInitialPlan: Plan = { id: 1, is_locked: true, model: { + activity_types: [], constraint_specification: [], created_at: '2023-02-16T00:00:00', default_view_id: 0, diff --git a/src/utilities/activities.test.ts b/src/utilities/activities.test.ts index 7cae3ac970..3be411ff7e 100644 --- a/src/utilities/activities.test.ts +++ b/src/utilities/activities.test.ts @@ -297,6 +297,7 @@ function getTestPlan(): Plan { id: 1, is_locked: false, model: { + activity_types: [], constraint_specification: [], created_at: '2006-07-11T00:00:00', default_view_id: 0, diff --git a/src/utilities/plan.test.ts b/src/utilities/plan.test.ts index 6e3bb81511..7387ccf0b3 100644 --- a/src/utilities/plan.test.ts +++ b/src/utilities/plan.test.ts @@ -41,6 +41,7 @@ describe('Plan utility', () => { id: 1, is_locked: false, model: { + activity_types: [], constraint_specification: [], created_at: '2024-01-01T00:00:00', default_view_id: 0, @@ -229,6 +230,7 @@ describe('Plan utility', () => { id: 1, is_locked: false, model: { + activity_types: [], constraint_specification: [], created_at: '2024-01-01T00:00:00', default_view_id: 0, @@ -339,6 +341,7 @@ describe('Plan utility', () => { id: 1, is_locked: false, model: { + activity_types: [], constraint_specification: [], created_at: '2024-01-01T00:00:00', default_view_id: 0, From 91eeafcc1d2cd31b02cd286230061a0a47e98c0b Mon Sep 17 00:00:00 2001 From: AaronPlave Date: Mon, 27 Apr 2026 09:25:37 -0700 Subject: [PATCH 05/18] Add pagination, filters, sorting, and deep linking --- src/components/search/Search.svelte | 14 +- src/components/search/SearchPanel.svelte | 443 +++++++++++++++------ src/components/search/SearchResults.svelte | 178 ++++++++- src/stores/search.ts | 10 +- src/types/activity.ts | 16 +- src/utilities/effects.ts | 159 ++++---- src/utilities/gql.ts | 31 +- 7 files changed, 612 insertions(+), 239 deletions(-) diff --git a/src/components/search/Search.svelte b/src/components/search/Search.svelte index 8bf554be01..18ea6e0c12 100644 --- a/src/components/search/Search.svelte +++ b/src/components/search/Search.svelte @@ -9,12 +9,22 @@ import SearchResults from './SearchResults.svelte'; export let user: User | null; + + let searchPanel: SearchPanel; + + function onPageChange(page: number) { + searchPanel?.onSearch(page); + } + + function onSortChange() { + searchPanel?.onSearch(0); + } - + - + diff --git a/src/components/search/SearchPanel.svelte b/src/components/search/SearchPanel.svelte index d73fd0a7f9..3295392ad3 100644 --- a/src/components/search/SearchPanel.svelte +++ b/src/components/search/SearchPanel.svelte @@ -1,11 +1,21 @@ - + Search for activities across plans -
-
- -
- (selectedModel = $models.find(model => model.id === v?.value))} - loop={false} + onSearch()} class="flex flex-col gap-3 p-2"> +
+ + (selectedModel = $models.find(model => model.id === v?.value))} + loop={false} + > + + + + - - - - - All Models - {#each orderedModels as model (model.id)} - - {model.name} -
(Version: {model.version})
-
- {/each} -
- -
-
-
-
- - -
-
- + All Models + {#each orderedModels as model (model.id)} + + {model.name} +
(Version: {model.version})
+
+ {/each} + + + + + +
+ + (filters = { ...filters, actType: e.detail[0]?.toString() ?? '' })} + selectedOptionValues={[filters.actType]} + /> +
+ +
+ -
-
- + + +
+ - +
+
+ -
-
- - -
-
- - -
-
- -
- -
+ sizeVariant="xs" + type="datetime-local" + /> + +
+ + +
+ +
+ + +
+ +
+ + (filters = { ...filters, planOwner: e.detail[0]?.toString() ?? '' })} + selectedOptionValues={[filters.planOwner]} + /> +
+ +
+ +
+ +
+ + +
diff --git a/src/components/search/SearchResults.svelte b/src/components/search/SearchResults.svelte index eda32ff928..98f45029ed 100644 --- a/src/components/search/SearchResults.svelte +++ b/src/components/search/SearchResults.svelte @@ -1,39 +1,102 @@ - Results +
+ Results + {#if $hasSearched && $searchTotalCount > 0} + + {startResult}-{endResult} of {$searchTotalCount.toLocaleString()} + + {/if} +
- +
+
+ +
+ + {#if $hasSearched && $searchTotalCount > PAGE_SIZE} +
+ + + + Page {currentPage + 1} of {totalPages.toLocaleString()} + + + +
+ {/if} +
diff --git a/src/stores/search.ts b/src/stores/search.ts index 3faae0b04f..06f8a80415 100644 --- a/src/stores/search.ts +++ b/src/stores/search.ts @@ -1,9 +1,17 @@ import { writable, type Writable } from 'svelte/store'; import type { ActivityDirectiveSearchResult } from '../types/activity'; +export const PAGE_SIZE = 50; + /* Writeable. */ -export const searchColumns: Writable = writable('1fr 3px 1fr'); +export const searchColumns: Writable = writable('1fr 3px 4fr'); export const hasSearched: Writable = writable(false); export const searchResults: Writable = writable(null); + +export const searchTotalCount: Writable = writable(0); + +export const searchCurrentPage: Writable = writable(0); + +export const searchOrderBy: Writable[]> = writable([{ last_modified_at: 'desc' }]); diff --git a/src/types/activity.ts b/src/types/activity.ts index 992f298f49..265609bf27 100644 --- a/src/types/activity.ts +++ b/src/types/activity.ts @@ -136,13 +136,27 @@ export type PlanSnapshotActivityDB = Omit; + plan: Pick; plan_id: number; + source_scheduling_goal_id: number | null; + start_offset: string; tags: { tag: TagsInsertInput; }[]; type: string; } + +export interface ActivitySearchResponse { + activity_directive: ActivityDirectiveSearchResult[]; + activity_directive_aggregate: { + aggregate: { + count: number; + }; + }; +} diff --git a/src/utilities/effects.ts b/src/utilities/effects.ts index 5196d3b080..a07c6ebd89 100644 --- a/src/utilities/effects.ts +++ b/src/utilities/effects.ts @@ -87,6 +87,7 @@ import type { ActivityDirectiveInsertInput, ActivityDirectiveRevision, ActivityDirectiveSearchResult, + ActivitySearchResponse, ActivityDirectiveSetInput, ActivityPreset, ActivityPresetId, @@ -7201,118 +7202,100 @@ const effects = { }, async searchActivities( - modelId: number | undefined, - filterArgType: string, - filterActName: string, - filterArgs: [name: string, value: string | number | boolean][], - filterTagValue: string, - filterPreset: string, - user: User | null, - ): Promise { + filters: { + actName: string; + actType: string; + args: [name: string, value: string | number | boolean][]; + createdBy: string; + lastModifiedAfter: string; + lastModifiedBefore: string; + modelId: number | undefined; + planName: string; + planOwner: string; + preset: string; + schedulerCreatedOnly: boolean; + tagValue: string; + }, + pagination: { + limit: number; + offset: number; + orderBy: Record[]; + }, + user: User | null, + ): Promise<{ results: ActivityDirectiveSearchResult[]; totalCount: number } | null> { try { const clauses = []; - if (modelId !== undefined && modelId !== null) { - clauses.push({ - plan: { - model_id: { - _eq: modelId, - }, - }, - }); + + if (filters.modelId !== undefined && filters.modelId !== null) { + clauses.push({ plan: { model_id: { _eq: filters.modelId } } }); } - if (filterArgType) { - clauses.push({ - type: { - _eq: filterArgType, - }, - }); + if (filters.actType) { + clauses.push({ type: { _eq: filters.actType } }); } - if (filterActName) { - clauses.push({ - name: { - _ilike: `%${filterActName}%`, - }, - }); + if (filters.actName) { + clauses.push({ name: { _ilike: `%${filters.actName}%` } }); } - if (filterTagValue) { - clauses.push({ - tags: { - tag: { - name: { - _eq: filterTagValue, - }, - }, - }, - }); + if (filters.tagValue) { + clauses.push({ tags: { tag: { name: { _eq: filters.tagValue } } } }); } - if (filterPreset) { - clauses.push({ - applied_preset: { - preset_applied: { - name: { - _eq: filterPreset, - }, - }, - }, - }); + if (filters.preset) { + clauses.push({ applied_preset: { preset_applied: { name: { _eq: filters.preset } } } }); + } + if (filters.createdBy) { + clauses.push({ created_by: { _eq: filters.createdBy } }); + } + if (filters.lastModifiedAfter) { + // datetime-local values (YYYY-MM-DDTHH:MM) are parsed as local time by JS Date + clauses.push({ last_modified_at: { _gte: new Date(filters.lastModifiedAfter).toISOString() } }); + } + if (filters.lastModifiedBefore) { + clauses.push({ last_modified_at: { _lte: new Date(filters.lastModifiedBefore).toISOString() } }); + } + if (filters.planName) { + clauses.push({ plan: { name: { _ilike: `%${filters.planName}%` } } }); + } + if (filters.planOwner) { + clauses.push({ plan: { owner: { _eq: filters.planOwner } } }); + } + if (filters.schedulerCreatedOnly) { + clauses.push({ source_scheduling_goal_id: { _is_null: false } }); } - for (const [argName, argValue] of filterArgs) { + for (const [argName, argValue] of filters.args) { if (argName === '' && argValue === '') { continue; } else if (argName === '') { - clauses.push({ - arguments: { - _cast: { - String: { - _ilike: `%${argValue}%`, - }, - }, - }, - }); + clauses.push({ arguments: { _cast: { String: { _ilike: `%${argValue}%` } } } }); } else if (argValue === '') { - clauses.push({ - arguments: { - _has_key: argName, - }, - }); + clauses.push({ arguments: { _has_key: argName } }); } else if (typeof argValue === 'string') { - clauses.push({ - arguments: { - _contains: { - [argName]: argValue, - }, - }, - }); + clauses.push({ arguments: { _contains: { [argName]: argValue } } }); } else if (typeof argValue === 'number' || typeof argValue === 'boolean') { clauses.push({ _or: [ - { - arguments: { - _contains: { - [argName]: argValue, - }, - }, - }, - { - arguments: { - _contains: { - [argName]: argValue.toString(), - }, - }, - }, + { arguments: { _contains: { [argName]: argValue } } }, + { arguments: { _contains: { [argName]: argValue.toString() } } }, ], }); } } - const data = await reqHasura( + const data: ActivitySearchResponse = (await reqHasura( gql.SEARCH_ACTIVITIES, - { limit: 500, searchFilter: { _and: clauses } }, + { + limit: pagination.limit, + offset: pagination.offset, + orderBy: pagination.orderBy, + searchFilter: { _and: clauses }, + }, user, - ); + )) as unknown as ActivitySearchResponse; + if (data.activity_directive) { - return data.activity_directive; + return { + results: data.activity_directive, + totalCount: data.activity_directive_aggregate?.aggregate?.count ?? 0, + }; } } catch (e) { catchError('Search Failed', e as Error); diff --git a/src/utilities/gql.ts b/src/utilities/gql.ts index b6fbbb86db..59c8191786 100644 --- a/src/utilities/gql.ts +++ b/src/utilities/gql.ts @@ -2053,30 +2053,53 @@ const gql = { `, SEARCH_ACTIVITIES: `#graphql - query SearchActivities($searchFilter: activity_directive_bool_exp!, $limit: Int!) { - ${Queries.ACTIVITY_DIRECTIVES}(where: $searchFilter, order_by: { start_offset: desc }, limit: $limit) { + query SearchActivities( + $searchFilter: activity_directive_bool_exp!, + $limit: Int!, + $offset: Int!, + $orderBy: [activity_directive_order_by!] + ) { + ${Queries.ACTIVITY_DIRECTIVES}(where: $searchFilter, order_by: $orderBy, limit: $limit, offset: $offset) { applied_preset { preset_applied { name } } arguments + created_at + created_by directive_id: id - name last_modified_at + last_modified_by + name plan_id plan { name model_id + owner + start_time + tags { + tag { + color + name + } + } } + source_scheduling_goal_id + start_offset tags { tag { - name color + name } } type } + activity_directive_aggregate(where: $searchFilter) { + aggregate { + count + } + } } `, From e602c9f5075efa0085bcc1ed03e62b09c402da87 Mon Sep 17 00:00:00 2001 From: AaronPlave Date: Mon, 27 Apr 2026 10:37:42 -0700 Subject: [PATCH 06/18] Improve activity argument UX --- src/components/search/SearchPanel.svelte | 55 +++++++++++++++++------- 1 file changed, 40 insertions(+), 15 deletions(-) diff --git a/src/components/search/SearchPanel.svelte b/src/components/search/SearchPanel.svelte index 3295392ad3..e1a3037265 100644 --- a/src/components/search/SearchPanel.svelte +++ b/src/components/search/SearchPanel.svelte @@ -1,6 +1,7 @@
- {#each Array(count) as _, index} - + {#each items as item, index} + {/each}
diff --git a/src/components/ui/SearchableDropdown.svelte b/src/components/ui/SearchableDropdown.svelte index 1d087be546..4a2bbedb9f 100644 --- a/src/components/ui/SearchableDropdown.svelte +++ b/src/components/ui/SearchableDropdown.svelte @@ -34,6 +34,7 @@ export let disabled: boolean = false; export let error: string | undefined = undefined; export let hasUpdatePermission: boolean = true; + export let loading: boolean = false; export let options: DropdownOptions = []; export let maxListHeight: string = '300px'; export let name: string | undefined = undefined; @@ -85,21 +86,45 @@ let searchFilter: string = ''; let selectedOptions: DropdownOptions = []; let maxWidth: number = 0; + let measureCanvasContext: CanvasRenderingContext2D | null = null; + let measureRef: HTMLSpanElement | undefined; - $: { - selectedOptions = []; - let maxOptionChars = 0; - options.forEach(option => { - if (selectedOptionValues.find(value => value === option.value)) { - selectedOptions.push(option); + function getMeasureContext(): CanvasRenderingContext2D | null { + if (typeof document === 'undefined') { + return null; + } + if (!measureCanvasContext) { + const canvas = document.createElement('canvas'); + measureCanvasContext = canvas.getContext('2d'); + } + return measureCanvasContext; + } + + function measureMaxOptionWidth(opts: DropdownOptions): number { + const ctx = getMeasureContext(); + if (!ctx || !measureRef) { + return 0; + } + // Read the actual rendered font from a span that mirrors the option's CSS class — + // canvas needs the font in its own string format. One DOM read per measurement pass, + // not per option. + const cs = getComputedStyle(measureRef); + ctx.font = `${cs.fontStyle} ${cs.fontWeight} ${cs.fontSize} ${cs.fontFamily}`; + let max = 0; + for (const opt of opts) { + const w = ctx.measureText(opt.display.toString()).width; + if (w > max) { + max = w; } - const optionCharacterLength = option.display.toString().length; - maxOptionChars = Math.max(maxOptionChars, optionCharacterLength); - }); - // avg char length + 48 padding for the rest of the menu - maxWidth = Math.max(50, maxOptionChars * 8 + 48); + } + return max; } + // Measure the widest option text and add room for the row chrome: + // px-3 (left) + icon 24 + gap 4 + px-3 (right) + scrollbar ~16 + small canvas-vs-browser buffer ~4 + // = 72px. Re-runs when `options` changes or once `measureRef` is bound on mount. + $: maxWidth = measureRef ? Math.max(50, Math.ceil(measureMaxOptionWidth(options)) + 72) : 50; + $: selectedOptions = options.filter(option => { return !!selectedOptionValues.find(value => value === option.value); }); @@ -174,6 +199,14 @@
+ +
- {label} -
o.value === selectedOptions[0].value) : undefined} + let:item let:index > - {@const displayedOption = displayedOptions[index]} + {@const displayedOption = item} {@const selected = !!selectedOptions.find(o => o.value === displayedOption.value) || (!!showPlaceholderOption && selectedOptions.length === 0 && index === 0)} From e9c1211728b12c17425bcbdcade5ea30f004610b Mon Sep 17 00:00:00 2001 From: AaronPlave Date: Mon, 27 Apr 2026 17:38:49 -0700 Subject: [PATCH 10/18] search: panel polish + results overlay Mission Model uses SearchableDropdown. Collapse pending-model state into a single selectedModelId (selectedModel derived). Inline subscription-error banner. Per-dropdown loading. Start Offset min/max filters. Argument Value defaults tooltip. Search always enabled, above Clear, sticky footer. 500ms-delayed spinner overlay over results; no-op local sort comparators avoid the flash before server-sorted data lands. SearchResults adopts the new persistColumnStateKey for column-state persistence. --- src/components/search/SearchPanel.svelte | 500 +++++++++++---------- src/components/search/SearchResults.svelte | 94 ++-- src/routes/search/+layout.svelte | 2 +- src/stores/search.ts | 2 + src/utilities/effects.ts | 8 + 5 files changed, 325 insertions(+), 281 deletions(-) diff --git a/src/components/search/SearchPanel.svelte b/src/components/search/SearchPanel.svelte index 878c6b15e8..1140ae5743 100644 --- a/src/components/search/SearchPanel.svelte +++ b/src/components/search/SearchPanel.svelte @@ -4,10 +4,12 @@ import { browser } from '$app/environment'; import { goto } from '$app/navigation'; import { page } from '$app/stores'; - import { Button, Input as InputStellar, Label, Select } from '@nasa-jpl/stellar-svelte'; + import { Button, Input as InputStellar, Label } from '@nasa-jpl/stellar-svelte'; + import { ChevronDown, CircleQuestionMark } from 'lucide-svelte'; import { models } from '../../stores/model'; import { hasSearched, + isSearching, PAGE_SIZE, searchCurrentPage, searchOrderBy, @@ -15,7 +17,7 @@ searchTotalCount, } from '../../stores/search'; import { gqlSubscribable } from '../../stores/subscribable'; - import { tags } from '../../stores/tags'; + import { tagsStore } from '../../stores/tags'; import { users } from '../../stores/user'; import type { ActivityPreset } from '../../types/activity'; import type { User } from '../../types/app'; @@ -27,15 +29,10 @@ import Panel from '../ui/Panel.svelte'; import SearchableDropdown from '../ui/SearchableDropdown.svelte'; import SectionTitle from '../ui/SectionTitle.svelte'; + import Tooltip from '../ui/Tooltip.svelte'; export let user: User | null; - let activityPresets: GqlSubscribable = gqlSubscribable( - gql.SUB_ACTIVITY_PRESETS_ALL, - {}, - [], - ); - // Consolidated filter state const DEFAULT_FILTERS = { actName: '', @@ -49,37 +46,54 @@ planOwner: '', preset: '', schedulerCreatedOnly: false, + startOffsetMax: '', + startOffsetMin: '', tagValue: '', }; + type FilterKey = keyof typeof DEFAULT_FILTERS; + // Map filter keys to URL param names where they differ const URL_PARAM_OVERRIDES: Partial> = { schedulerCreatedOnly: 'schedulerOnly', tagValue: 'tag', }; - type FilterKey = keyof typeof DEFAULT_FILTERS; - - function getParamName(key: FilterKey): string { - return URL_PARAM_OVERRIDES[key] ?? key; - } + const activityPresets: GqlSubscribable = gqlSubscribable( + gql.SUB_ACTIVITY_PRESETS_ALL, + {}, + [], + ); + const modelsLoading = models.loading; + const modelsError = models.error; + const tagsLoading = tagsStore.loading; + const tagsError = tagsStore.error; + const usersLoading = users.loading; + const usersError = users.error; + const presetsLoading = activityPresets.loading; + const presetsError = activityPresets.error; - let selectedModel: ModelSlim | undefined; + let argNameOptions: DropdownOptions = []; + let selectedModelId: number | undefined; let filters = { ...DEFAULT_FILTERS }; let initialized = false; - let pendingModelId: number | null = null; - let pendingSearch = false; - let orderedModels: ModelSlim[] = []; + let modelOptions: DropdownOptions = []; let tagOptions: DropdownOptions = []; let typeOptions: DropdownOptions = []; let presetOptions: DropdownOptions = []; let userOptions: DropdownOptions = []; - let argNameOptions: DropdownOptions = []; + + $: selectedModel = selectedModelId !== undefined ? $models.find(m => m.id === selectedModelId) : undefined; $: orderedModels = [...$models].sort(({ id: idA }, { id: idB }) => idB - idA); - $: tagOptions = [{ display: '', value: '' }, ...$tags.map(tag => ({ display: tag.name, value: tag.name }))]; + $: modelOptions = [ + { display: '', value: '' }, + ...orderedModels.map(m => ({ display: getDisplayNameForModel(m), value: m.id })), + ]; + + $: tagOptions = [{ display: '', value: '' }, ...$tagsStore.map(tag => ({ display: tag.name, value: tag.name }))]; $: userOptions = [ { display: '', value: '' }, @@ -130,32 +144,24 @@ ]; } - $: hasAnyFilter = - selectedModel !== undefined || - Object.entries(filters).some(([k, v]) => (k === 'schedulerCreatedOnly' ? v === true : v !== '')); - // Initialize from URL on first page load (browser only — SSR can't navigate) $: if (browser && $page.url) { initFromUrl(); } - // Resolve pending model ID once models subscription delivers data - $: if (pendingModelId !== null && $models.length > 0) { - selectedModel = $models.find(m => m.id === pendingModelId); - pendingModelId = null; - if (pendingSearch) { - pendingSearch = false; - onSearch(); - } - } + $: subscriptionError = $modelsError || $tagsError || $usersError || $presetsError || ''; // Keep URL in sync with current filter form state after init $: if (browser && initialized) { void filters; - void selectedModel; + void selectedModelId; updateUrl(); } + function getParamName(key: FilterKey): string { + return URL_PARAM_OVERRIDES[key] ?? key; + } + function initFromUrl() { if (initialized) { return; @@ -166,13 +172,7 @@ const modelIdParam = params.get('modelId'); if (modelIdParam) { - const id = parseInt(modelIdParam); - const found = $models.find(m => m.id === id); - if (found) { - selectedModel = found; - } else { - pendingModelId = id; - } + selectedModelId = parseInt(modelIdParam); } const updates: Partial = {}; @@ -186,18 +186,15 @@ filters = { ...filters, ...updates }; } - if (hasAnyFilter) { + if (selectedModelId !== undefined || Object.keys(updates).length > 0) { onSearch(); - } else if (pendingModelId !== null) { - // Model is the only filter; defer search until it resolves - pendingSearch = true; } } function updateUrl() { const params = new URLSearchParams(); - if (selectedModel) { - params.set('modelId', selectedModel.id.toString()); + if (selectedModelId !== undefined) { + params.set('modelId', selectedModelId.toString()); } for (const key of Object.keys(filters) as FilterKey[]) { const val = filters[key]; @@ -216,6 +213,7 @@ export async function onSearch(pageNumber: number = 0) { hasSearched.set(true); searchCurrentPage.set(pageNumber); + isSearching.set(true); const filterArgs: [name: string, value: string | number | boolean][] = []; if (filters.argName || filters.argValue) { @@ -231,39 +229,45 @@ } } - const result = await effects.searchActivities( - { - actName: filters.actName, - actType: filters.actType, - args: filterArgs, - createdBy: filters.createdBy, - lastModifiedAfter: filters.lastModifiedAfter, - lastModifiedBefore: filters.lastModifiedBefore, - modelId: selectedModel?.id, - planName: filters.planName, - planOwner: filters.planOwner, - preset: filters.preset, - schedulerCreatedOnly: filters.schedulerCreatedOnly, - tagValue: filters.tagValue, - }, - { - limit: PAGE_SIZE, - offset: pageNumber * PAGE_SIZE, - orderBy: $searchOrderBy, - }, - user, - ); - - if (result) { - searchResults.set(result.results); - searchTotalCount.set(result.totalCount); - } + try { + const result = await effects.searchActivities( + { + actName: filters.actName, + actType: filters.actType, + args: filterArgs, + createdBy: filters.createdBy, + lastModifiedAfter: filters.lastModifiedAfter, + lastModifiedBefore: filters.lastModifiedBefore, + modelId: selectedModelId, + planName: filters.planName, + planOwner: filters.planOwner, + preset: filters.preset, + schedulerCreatedOnly: filters.schedulerCreatedOnly, + startOffsetMax: filters.startOffsetMax, + startOffsetMin: filters.startOffsetMin, + tagValue: filters.tagValue, + }, + { + limit: PAGE_SIZE, + offset: pageNumber * PAGE_SIZE, + orderBy: $searchOrderBy, + }, + user, + ); - updateUrl(); + if (result) { + searchResults.set(result.results); + searchTotalCount.set(result.totalCount); + } + + updateUrl(); + } finally { + isSearching.set(false); + } } function clearFilters() { - selectedModel = undefined; + selectedModelId = undefined; filters = { ...DEFAULT_FILTERS }; hasSearched.set(false); searchResults.set([]); @@ -280,173 +284,205 @@ } - + Search for Activities Across Plans -
onSearch()} class="flex flex-col gap-3 p-2"> -
- - (selectedModel = $models.find(model => model.id === v?.value))} - loop={false} - > - onSearch()} class="flex h-full flex-col"> + {#if subscriptionError} +
+ Failed to load filter data: {subscriptionError} +
+ {/if} +
+
+ + { + const v = e.detail[0]; + selectedModelId = v === '' || v === undefined ? undefined : Number(v); + }} + selectedOptionValues={[selectedModelId ?? '']} > - - - + +
+ +
+ + (filters = { ...filters, actType: e.detail[0]?.toString() ?? '' })} + selectedOptionValues={[filters.actType]} > - All Models - {#each orderedModels as model (model.id)} - - {model.name} -
(Version: {model.version})
-
- {/each} - - - -
- -
- - (filters = { ...filters, actType: e.detail[0]?.toString() ?? '' })} - selectedOptionValues={[filters.actType]} - /> -
- -
- - -
- -
- - (filters = { ...filters, argName: e.detail[0]?.toString() ?? '' })} - selectedOptionValues={[filters.argName]} - /> -
-
- - -
- -
- - (filters = { ...filters, tagValue: e.detail[0]?.toString() ?? '' })} - selectedOptionValues={[filters.tagValue]} - /> -
- -
- - (filters = { ...filters, preset: e.detail[0]?.toString() ?? '' })} - selectedOptionValues={[filters.preset]} - /> -
- -
- - (filters = { ...filters, createdBy: e.detail[0]?.toString() ?? '' })} - selectedOptionValues={[filters.createdBy]} - /> -
- -
- - -
-
- - -
- -
- - -
- -
- - (filters = { ...filters, planOwner: e.detail[0]?.toString() ?? '' })} - selectedOptionValues={[filters.planOwner]} - /> -
- -
-
+ +
+ + - Scheduler-created only - +
+ +
+ + (filters = { ...filters, argName: e.detail[0]?.toString() ?? '' })} + selectedOptionValues={[filters.argName]} + > + + +
+
+ + +
+ +
+ + (filters = { ...filters, tagValue: e.detail[0]?.toString() ?? '' })} + selectedOptionValues={[filters.tagValue]} + > + + +
+ +
+ + (filters = { ...filters, preset: e.detail[0]?.toString() ?? '' })} + selectedOptionValues={[filters.preset]} + > + + +
+ +
+ + (filters = { ...filters, createdBy: e.detail[0]?.toString() ?? '' })} + selectedOptionValues={[filters.createdBy]} + > + + +
+ +
+ + +
+
+ + +
+ +
+ + +
+
+ + +
+ +
+ + +
+ +
+ + (filters = { ...filters, planOwner: e.detail[0]?.toString() ?? '' })} + selectedOptionValues={[filters.planOwner]} + > + + +
+ +
+ +
- -
- +
+
diff --git a/src/components/search/SearchResults.svelte b/src/components/search/SearchResults.svelte index bbaf12c1a9..2d88d99e4e 100644 --- a/src/components/search/SearchResults.svelte +++ b/src/components/search/SearchResults.svelte @@ -3,12 +3,19 @@ + + diff --git a/src/components/search/SearchPanel.svelte b/src/components/search/SearchPanel.svelte index 13570aacb2..2f932b2512 100644 --- a/src/components/search/SearchPanel.svelte +++ b/src/components/search/SearchPanel.svelte @@ -7,6 +7,7 @@ import { Button, Input as InputStellar, Label } from '@nasa-jpl/stellar-svelte'; import { ChevronDown, CircleQuestionMark } from 'lucide-svelte'; import { models } from '../../stores/model'; + import { schedulingGoalResponses } from '../../stores/scheduling'; import { hasSearched, isSearching, @@ -22,7 +23,7 @@ import { users } from '../../stores/user'; import type { ActivityPreset } from '../../types/activity'; import type { User } from '../../types/app'; - import type { DropdownOptions } from '../../types/dropdown'; + import type { DropdownOptions, SelectedDropdownOptionValue } from '../../types/dropdown'; import type { ModelSlim } from '../../types/model'; import type { GqlSubscribable } from '../../types/subscribable'; import effects from '../../utilities/effects'; @@ -37,16 +38,21 @@ // Consolidated filter state const DEFAULT_FILTERS = { actName: '', - actType: '', + actType: [] as string[], argName: '', argValue: '', + createdAfter: '', + createdBefore: '', createdBy: '', lastModifiedAfter: '', lastModifiedBefore: '', + lastModifiedBy: '', planName: '', planOwner: '', + planTag: '', preset: '', schedulerCreatedOnly: false, + schedulingGoalId: '', startOffsetMax: '', startOffsetMin: '', tagValue: '', @@ -73,10 +79,13 @@ const usersError = users.error; const presetsLoading = activityPresets.loading; const presetsError = activityPresets.error; + const goalsLoading = schedulingGoalResponses.loading; + const goalsError = schedulingGoalResponses.error; let argNameOptions: DropdownOptions = []; let selectedModelId: number | undefined; let filters = { ...DEFAULT_FILTERS }; + let goalOptions: DropdownOptions = []; let initialized = false; let orderedModels: ModelSlim[] = []; let modelOptions: DropdownOptions = []; @@ -131,7 +140,7 @@ const paramNames = new Set(); for (const model of sourceModels) { for (const type of model?.activity_types ?? []) { - if (filters.actType && type.name !== filters.actType) { + if (filters.actType.length > 0 && !filters.actType.includes(type.name)) { continue; } for (const paramName of Object.keys(type.parameters ?? {})) { @@ -145,12 +154,20 @@ ]; } + $: goalOptions = [ + { display: '', value: '' }, + ...[...$schedulingGoalResponses] + .sort((a, b) => a.name.localeCompare(b.name)) + .map(goal => ({ display: `${goal.name} (${goal.id})`, value: goal.id.toString() })), + ]; + // Initialize from URL on first page load (browser only — SSR can't navigate) $: if (browser && $page.url) { initFromUrl(); } - $: subscriptionError = $modelsError || $tagsError || $usersError || $presetsError || ''; + $: subscriptionError = + $modelsError || $tagsError || $usersError || $presetsError || $goalsError || ''; // Keep URL in sync with current filter form state after init $: if (browser && initialized) { @@ -163,6 +180,16 @@ return URL_PARAM_OVERRIDES[key] ?? key; } + function onActTypeChange(values: SelectedDropdownOptionValue[]) { + filters = { + ...filters, + actType: values + .filter((v): v is string | number => v !== null) + .map(v => String(v)) + .filter(s => s.length > 0), + }; + } + function initFromUrl() { if (initialized) { return; @@ -180,7 +207,16 @@ for (const key of Object.keys(DEFAULT_FILTERS) as FilterKey[]) { const val = params.get(getParamName(key)); if (val !== null) { - updates[key] = (typeof DEFAULT_FILTERS[key] === 'boolean' ? val === 'true' : val) as never; + const defaultVal = DEFAULT_FILTERS[key]; + let parsed: unknown; + if (typeof defaultVal === 'boolean') { + parsed = val === 'true'; + } else if (Array.isArray(defaultVal)) { + parsed = val.split(',').filter(s => s.length > 0); + } else { + parsed = val; + } + updates[key] = parsed as never; } } if (Object.keys(updates).length) { @@ -199,7 +235,11 @@ } for (const key of Object.keys(filters) as FilterKey[]) { const val = filters[key]; - if (val !== '' && val !== false) { + if (Array.isArray(val)) { + if (val.length > 0) { + params.set(getParamName(key), val.join(',')); + } + } else if (val !== '' && val !== false) { params.set(getParamName(key), val.toString()); } } @@ -236,14 +276,19 @@ actName: filters.actName, actType: filters.actType, args: filterArgs, + createdAfter: filters.createdAfter, + createdBefore: filters.createdBefore, createdBy: filters.createdBy, lastModifiedAfter: filters.lastModifiedAfter, lastModifiedBefore: filters.lastModifiedBefore, + lastModifiedBy: filters.lastModifiedBy, modelId: selectedModelId, planName: filters.planName, planOwner: filters.planOwner, + planTag: filters.planTag, preset: filters.preset, schedulerCreatedOnly: filters.schedulerCreatedOnly, + schedulingGoalId: filters.schedulingGoalId, startOffsetMax: filters.startOffsetMax, startOffsetMin: filters.startOffsetMin, tagValue: filters.tagValue, @@ -317,10 +362,11 @@
(filters = { ...filters, actType: e.detail[0]?.toString() ?? '' })} - selectedOptionValues={[filters.actType]} + on:change={e => onActTypeChange(e.detail)} + selectedOptionValues={filters.actType} > @@ -352,7 +398,7 @@
+
+ + (filters = { ...filters, lastModifiedBy: e.detail[0]?.toString() ?? '' })} + selectedOptionValues={[filters.lastModifiedBy]} + > + + +
+ +
+ + +
+
+ + +
+
+
+ + (filters = { ...filters, planTag: e.detail[0]?.toString() ?? '' })} + selectedOptionValues={[filters.planTag]} + > + + +
+ +
+ + (filters = { ...filters, schedulingGoalId: e.detail[0]?.toString() ?? '' })} + selectedOptionValues={[filters.schedulingGoalId]} + > + + +
+