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 @@ - + + + + + + + + + + + + + + + + + + +