From 1ce629a10f12bcc5fcb82fe2b1eb0c0fcd3d6695 Mon Sep 17 00:00:00 2001 From: Malcolm Nixon Date: Mon, 20 Jul 2026 18:54:48 -0400 Subject: [PATCH] Add type-filter chips and text search to Expose Targets picker The Custom View Builder's AvailableExposeTargets list is a single flat, alphabetically-sorted list of every exposable qualified name in the workspace. For large models this list grows to many pages with no way to narrow it down, forcing users to scroll extensively to find the element they want to expose. This change computes a human-readable type label for each candidate element (from SysmlDefinitionNode.DefinitionKeyword, SysmlFeatureNode.FeatureKeyword, literal labels for package/dependency/ satisfy nodes, and a defensive fallback for any future node kind), and adds a filter UI above the existing list: removable type-filter chips (OR semantics across chips), a + button with a flyout to add new type filters, and a text box for case-insensitive substring search (ANDed with the type filter). The list defaults to filtering by part since that's the most common expose target, while still allowing users to widen or change the filter as needed. The existing ListBox keeps its name and selection/double-click wiring unchanged, now bound to the filtered DisplayedExposeTargets instead of the raw AvailableExposeTargets, so existing add-target behavior is unaffected. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../view-builder-dialog.md | 61 ++++- .../view-builder-dialog.yaml | 22 ++ docs/user_guide/getting_started.md | 29 +- .../view-builder-dialog.md | 48 ++++ .../ViewBuilderDialogView.axaml | 34 ++- .../ViewBuilderDialogView.axaml.cs | 39 +++ .../ViewBuilderDialogViewModel.cs | 202 +++++++++++++- .../ViewBuilderDialogViewModelTests.cs | 251 ++++++++++++++++++ 8 files changed, 667 insertions(+), 19 deletions(-) diff --git a/docs/design/sysml2-workbench/app-shell-subsystem/view-builder-dialog.md b/docs/design/sysml2-workbench/app-shell-subsystem/view-builder-dialog.md index 643e9e4..524f639 100644 --- a/docs/design/sysml2-workbench/app-shell-subsystem/view-builder-dialog.md +++ b/docs/design/sysml2-workbench/app-shell-subsystem/view-builder-dialog.md @@ -37,7 +37,46 @@ derived from, any tab tracked by `Shell`. by the "Expose Targets" tab's add-target picker, drawn from `Shell.CurrentWorkspace.Workspace.Declarations`, excluding stdlib names and node kinds that would fail `ViewDefinitionModel` validation (views, -viewpoints, imports, metadata, transitions, connections). +viewpoints, imports, metadata, transitions, connections). Remains the +master, unfiltered, sorted list; `DisplayedExposeTargets` (below) is what the +picker `ListBox` actually binds to. + +**AvailableExposeTargetTypeLabels**: `IReadOnlyList` — every distinct +human-readable "type label" present among the current `AvailableExposeTargets` +candidates, sorted ordinally. Each candidate's label is computed from its +underlying `SysmlNode` subtype: a `SysmlDefinitionNode` yields its +`DefinitionKeyword` (e.g. `"part def"`), a `SysmlFeatureNode` yields its +`FeatureKeyword` (e.g. `"part"`), and `SysmlPackageNode`/`SysmlDependencyNode`/ +`SysmlSatisfyNode` yield the fixed literals `"package"`/`"dependency"`/ +`"satisfy"` respectively (none of these three expose a keyword-like property +of their own). Any other node kind not already excluded from the picker falls +back to a defensively derived label (its runtime type name with a leading +`Sysml` and/or trailing `Node` stripped, lowercased), so an unrecognized +future node kind is labeled reasonably rather than crashing or silently +vanishing from the picker. + +**ActiveExposeTypeFilters**: `ObservableCollection` — the type-label +chips currently applied over the picker, combined with OR semantics (an item +is shown when its type label is any one of these); empty means no type +restriction (every candidate's type is shown). `RefreshFromWorkspace` +pre-populates this with just `"part"` when that label is present in the +current workspace, since narrowing to part usages is the most common +starting point; otherwise it starts empty. Mutated only through +`AddExposeTypeFilter`/`RemoveExposeTypeFilter` (or, defensively, any other +direct mutation, which is also observed) rather than replaced wholesale, so +the view's chip-row `ItemsControl` can bind to this instance directly. + +**ExposeTargetSearchText**: `string?` — the picker's free-text search box +value, two-way bound from the view. A non-empty value narrows +`DisplayedExposeTargets` to qualified names containing it as a +case-insensitive substring, applied with AND semantics against whatever the +active type filter already narrowed to. + +**DisplayedExposeTargets**: `IReadOnlyList` — the picker `ListBox`'s +actual data source: `AvailableExposeTargets`, narrowed first by +`ActiveExposeTypeFilters` (OR semantics) and then by `ExposeTargetSearchText` +(AND semantics), preserving the master list's ordinal sort order. Recomputed +whenever any of the three inputs changes. **StatusMessage**: `string?` — set when `RenderPreview` or `TryCommit` fails, so the view can surface the failure inline without a modal message box. @@ -48,15 +87,27 @@ unusable, target-less picker. #### Key Methods -**RefreshFromWorkspace**: Refreshes `AvailableExposeTargets` and -`IsWorkspaceEmpty` from the shell's current workspace state. +**RefreshFromWorkspace**: Refreshes `AvailableExposeTargets`, +`AvailableExposeTargetTypeLabels`, `ActiveExposeTypeFilters`, +`DisplayedExposeTargets`, and `IsWorkspaceEmpty` from the shell's current +workspace state. - *Parameters*: none. -- *Returns*: `void` — both properties update in place. +- *Returns*: `void` — all listed properties update in place. - *Postconditions*: Called once at construction; the dialog is short-lived (opened, edited, closed) so it does not itself subscribe to `MainWindowShell.SourcesChanged` the way the old long-lived panel view - model did. + model did. Re-applies the `"part"`-default chip rule fresh each call. + +**AddExposeTypeFilter** / **RemoveExposeTypeFilter**: Add or remove a type +label from `ActiveExposeTypeFilters` and recompute `DisplayedExposeTargets`. + +- *Parameters*: the type label to add or remove. +- *Returns*: `void` — `ActiveExposeTypeFilters` and `DisplayedExposeTargets` + update in place. +- *Postconditions*: `AddExposeTypeFilter` is dedupe-safe (adding an + already-active label is a no-op beyond the recompute); `RemoveExposeTypeFilter` + no-ops when the label is not currently active, rather than throwing. **AddExposeTarget** / **RemoveExposeTarget** / **SetExposeRecursionKind** / **SetExposeBracketFilter**: Port the deleted `CustomViewBuilderToolViewModel`'s diff --git a/docs/reqstream/sysml2-workbench/app-shell-subsystem/view-builder-dialog.yaml b/docs/reqstream/sysml2-workbench/app-shell-subsystem/view-builder-dialog.yaml index 2f54216..25ed776 100644 --- a/docs/reqstream/sysml2-workbench/app-shell-subsystem/view-builder-dialog.yaml +++ b/docs/reqstream/sysml2-workbench/app-shell-subsystem/view-builder-dialog.yaml @@ -81,3 +81,25 @@ sections: composition rather than resuming a previous, possibly-cancelled session. tests: - 'Construction_FreshInstance_DoesNotCarryOverPriorInstanceSelections' + + - id: 'SysML2Workbench-AppShellSubsystem-ViewBuilderDialog-FilterExposeTargetsByTypeAndText' + title: 'The ViewBuilderDialog shall let the user narrow the expose-target picker by one or more + type-label chips (OR semantics, defaulting to a single "part" chip when available, or no + restriction when empty) combined with a case-insensitive name-substring search (AND semantics), + computing each candidate''s type label from its underlying SysmlNode subtype.' + justification: | + A workspace can contain far more candidate elements than fit comfortably in the picker + ListBox; letting the user narrow by kind (part def, part, package, etc.) and/or name text + makes finding the right expose target practical without scrolling through the entire + workspace, while the "part" default keeps the picker usefully narrowed out of the box. + tests: + - 'RefreshFromWorkspace_ComputesTypeLabelsPerNodeKind' + - 'RefreshFromWorkspace_AvailableExposeTargetTypeLabels_IsDistinctAndSorted' + - 'Construction_PartLabelPresent_DefaultsActiveExposeTypeFiltersToPart' + - 'Construction_PartLabelAbsent_ActiveExposeTypeFiltersStartsEmpty' + - 'DisplayedExposeTargets_EmptyChips_ShowsAllTypes' + - 'DisplayedExposeTargets_MultipleChips_AppliesOrSemantics' + - 'DisplayedExposeTargets_TypeFilterAndTextSearch_CombineWithAndSemantics' + - 'AddExposeTypeFilter_DuplicateLabel_DoesNotAddSecondChip' + - 'RemoveExposeTypeFilter_RemovesChipAndNoOpsWhenAbsent' + - 'ExposeTargetSearchTextChanged_RecomputesDisplayedExposeTargets' diff --git a/docs/user_guide/getting_started.md b/docs/user_guide/getting_started.md index 9b19adc..56150a0 100644 --- a/docs/user_guide/getting_started.md +++ b/docs/user_guide/getting_started.md @@ -103,16 +103,25 @@ to it. 1. On the **View Kind** tab, pick a kind (General, Interconnection, State Transition, Action Flow, Sequence, or Grid). -2. On the **Expose Targets** tab, pick an element or package from the loaded - workspace and click **Add** to add it to the exposed-targets list. Repeat - for every element or package you want exposed - custom views support - multiple exposed elements, just like a hand-written SysML view. For each - row, choose its **recursion kind** - *This element only*, *This element + - everything below (::\*\*)* (the default), *Direct children only (::\*)*, - or *All descendants, not itself (::\*::\*\*)* - and optionally enter a - **bracket-filter expression** (enabled only for the two recursive kinds) - to narrow that target's exposed membership. Click **Remove** to drop a - row entirely. +2. On the **Expose Targets** tab, narrow the picker if needed, then pick an + element or package and click **Add** to add it to the exposed-targets + list. The picker starts filtered to a single **part** type chip (the most + common starting point); click **✕** on a chip to remove it, or click **+** + to open a flyout listing every other type label present in the workspace + (for example *part def*, *package*, *dependency*) and pick one to add + another chip - active chips combine so an element matching any one of + them is shown, and removing every chip lifts the type restriction + entirely. Use the **Filter by name...** search box alongside the chips to + further narrow the list to qualified names containing the typed text, + case-insensitively; the type-chip and name-search filters combine + together. Repeat **Add** for every element or package you want exposed - + custom views support multiple exposed elements, just like a hand-written + SysML view. For each row, choose its **recursion kind** - *This element + only*, *This element + everything below (::\*\*)* (the default), *Direct + children only (::\*)*, or *All descendants, not itself (::\*::\*\*)* - and + optionally enter a **bracket-filter expression** (enabled only for the two + recursive kinds) to narrow that target's exposed membership. Click + **Remove** to drop a row entirely. 3. On the **Filter & Name** tab, optionally enter a **Filter Expression** to narrow what is included, and optionally enter a **View Name** - otherwise a default name is used. diff --git a/docs/verification/sysml2-workbench/app-shell-subsystem/view-builder-dialog.md b/docs/verification/sysml2-workbench/app-shell-subsystem/view-builder-dialog.md index 0694b55..73a6e48 100644 --- a/docs/verification/sysml2-workbench/app-shell-subsystem/view-builder-dialog.md +++ b/docs/verification/sysml2-workbench/app-shell-subsystem/view-builder-dialog.md @@ -100,3 +100,51 @@ filter expression, display name) that is never committed leaves the shell's `Ope over the same shell right after a prior instance was edited, starts from a completely clean `Definition` - no view kind, expose targets, filter expression, or display name carried over. Verified by `ViewBuilderDialogViewModelTests.Construction_FreshInstance_DoesNotCarryOverPriorInstanceSelections`. + +**RefreshFromWorkspace_ComputesTypeLabelsPerNodeKind**: Constructing the view model over a workspace containing a +`part def`, a `part` usage, and a bare `package` computes the correct type label for each - `"part def"`, +`"part"`, and `"package"` respectively - in `AvailableExposeTargetTypeLabels`. Verified by +`ViewBuilderDialogViewModelTests.RefreshFromWorkspace_ComputesTypeLabelsPerNodeKind`. + +**RefreshFromWorkspace_AvailableExposeTargetTypeLabels_IsDistinctAndSorted**: `AvailableExposeTargetTypeLabels` +contains no duplicate labels and is sorted ordinally. Verified by +`ViewBuilderDialogViewModelTests.RefreshFromWorkspace_AvailableExposeTargetTypeLabels_IsDistinctAndSorted`. + +**Construction_PartLabelPresent_DefaultsActiveExposeTypeFiltersToPart**: Constructing the view model over a +workspace where `"part"` is an available type label pre-populates `ActiveExposeTypeFilters` with exactly that one +chip. Verified by +`ViewBuilderDialogViewModelTests.Construction_PartLabelPresent_DefaultsActiveExposeTypeFiltersToPart`. + +**Construction_PartLabelAbsent_ActiveExposeTypeFiltersStartsEmpty**: Constructing the view model over a workspace +with no `"part"`-labeled elements leaves `ActiveExposeTypeFilters` empty, applying no type restriction by default. +Verified by +`ViewBuilderDialogViewModelTests.Construction_PartLabelAbsent_ActiveExposeTypeFiltersStartsEmpty`. + +**DisplayedExposeTargets_EmptyChips_ShowsAllTypes**: Removing the only active type-filter chip leaves +`DisplayedExposeTargets` equal to the full `AvailableExposeTargets` list, confirming empty chips means no type +restriction. Verified by +`ViewBuilderDialogViewModelTests.DisplayedExposeTargets_EmptyChips_ShowsAllTypes`. + +**DisplayedExposeTargets_MultipleChips_AppliesOrSemantics**: Activating two type-filter chips (`"part def"` and +`"package"`) shows candidates matching either label while excluding a candidate whose label matches neither, +confirming OR semantics across chips. Verified by +`ViewBuilderDialogViewModelTests.DisplayedExposeTargets_MultipleChips_AppliesOrSemantics`. + +**DisplayedExposeTargets_TypeFilterAndTextSearch_CombineWithAndSemantics**: With two type-filter chips active, +setting `ExposeTargetSearchText` to a case-insensitive substring further narrows `DisplayedExposeTargets` to only +the candidates matching both the type filter and the text search, confirming AND semantics between the two +filters. Verified by +`ViewBuilderDialogViewModelTests.DisplayedExposeTargets_TypeFilterAndTextSearch_CombineWithAndSemantics`. + +**AddExposeTypeFilter_DuplicateLabel_DoesNotAddSecondChip**: Calling `AddExposeTypeFilter` twice with the same +label results in exactly one chip in `ActiveExposeTypeFilters`, confirming dedupe-safe behavior. Verified by +`ViewBuilderDialogViewModelTests.AddExposeTypeFilter_DuplicateLabel_DoesNotAddSecondChip`. + +**RemoveExposeTypeFilter_RemovesChipAndNoOpsWhenAbsent**: Removing the default `"part"` chip clears +`ActiveExposeTypeFilters`, and a subsequent removal of a label that is not currently active is a no-op rather +than a throw. Verified by +`ViewBuilderDialogViewModelTests.RemoveExposeTypeFilter_RemovesChipAndNoOpsWhenAbsent`. + +**ExposeTargetSearchTextChanged_RecomputesDisplayedExposeTargets**: Setting `ExposeTargetSearchText` alone (with +no other method call) live-recomputes `DisplayedExposeTargets` to the matching subset. Verified by +`ViewBuilderDialogViewModelTests.ExposeTargetSearchTextChanged_RecomputesDisplayedExposeTargets`. diff --git a/src/DemaConsulting.SysML2Workbench/AppShellSubsystem/ViewBuilderDialogView.axaml b/src/DemaConsulting.SysML2Workbench/AppShellSubsystem/ViewBuilderDialogView.axaml index 8b3b144..0cff14f 100644 --- a/src/DemaConsulting.SysML2Workbench/AppShellSubsystem/ViewBuilderDialogView.axaml +++ b/src/DemaConsulting.SysML2Workbench/AppShellSubsystem/ViewBuilderDialogView.axaml @@ -49,7 +49,39 @@ - + + + + + + + + + + + + + + + + + + +