feat: workspace switcher, per-project filters and run stamping (stage 1)
Some checks failed
Security / Security check (push) Failing after 2s
Some checks failed
Security / Security check (push) Failing after 2s
Multi-project stage 1 against the shared hub (platform design docs/architecture/projects.md, § Studio): - ChainWorkspaceSwitcher in the Audit + Approvals AppBars: lists the registry (colour dot per project, shield for protected, honesty tooltip), 'All projects' stays reachable — a filter, not a jail. Selection is persisted and shared via the Workspace notifier. - Audit page: list query AND live stream re-scoped hub-side on switch. - Approvals page: pending + history scoped; the sidebar badge counts the active workspace's pending approvals. - Flow runs are stamped with the active workspace; a flow file carrying its own project: keeps it (file wins, CLI semantics). - Data layer: listProjects/ProjectRef; project fields on AuditEvent, PendingApproval(+Record), SavedFlow; project params through HubService. l10n DE+EN. Widget tests for the switcher contract. Visual verification (light+dark screenshots) still pending — the shared desktop was in active use; code paths are covered by flutter test (26 green). Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
This commit is contained in:
parent
7a38cd58aa
commit
2592a23cc0
14 changed files with 554 additions and 9 deletions
100
lib/data/workspace.dart
Normal file
100
lib/data/workspace.dart
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
// Workspace — the active project selection shared across pages.
|
||||
//
|
||||
// One switcher in the AppBar drives two things at once (see
|
||||
// docs/architecture/projects.md in the platform repo, § Studio):
|
||||
// the page filter (audit, approvals) AND the label new runs are
|
||||
// stamped with. "All projects" stays reachable — the selection is
|
||||
// a filter, not a jail. Sealed areas are NOT listed here; they are
|
||||
// their own hub instance and arrive with the stage-3 connection
|
||||
// switch.
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import 'hub.dart';
|
||||
|
||||
class Workspace extends ChangeNotifier {
|
||||
Workspace._();
|
||||
static final Workspace instance = Workspace._();
|
||||
|
||||
static const _prefsKey = 'workspace.active';
|
||||
|
||||
/// Registry projects (`general` first, as ordered by the hub).
|
||||
List<ProjectRef> _projects = const [];
|
||||
List<ProjectRef> get projects => _projects;
|
||||
|
||||
/// Active project slug; empty = all projects (no filter).
|
||||
String _activeSlug = '';
|
||||
String get activeSlug => _activeSlug;
|
||||
bool get isAll => _activeSlug.isEmpty;
|
||||
|
||||
/// The active project's registry entry, or null for "all".
|
||||
ProjectRef? get active {
|
||||
if (_activeSlug.isEmpty) return null;
|
||||
for (final p in _projects) {
|
||||
if (p.slug == _activeSlug) return p;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
bool _loaded = false;
|
||||
|
||||
/// Restore the persisted selection and pull the registry once.
|
||||
/// Safe to call repeatedly; only the first call restores.
|
||||
Future<void> ensureLoaded() async {
|
||||
if (!_loaded) {
|
||||
_loaded = true;
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
_activeSlug = prefs.getString(_prefsKey) ?? '';
|
||||
} catch (_) {
|
||||
// No persisted selection — start on "all projects".
|
||||
}
|
||||
}
|
||||
await refresh();
|
||||
}
|
||||
|
||||
/// Re-pull the registry from the hub. Soft-fails (keeps the
|
||||
/// last list) so a hub restart doesn't blank the switcher; an
|
||||
/// active slug that vanished from the registry stays selected —
|
||||
/// its historical rows remain filterable, which is the honest
|
||||
/// behaviour for an audit surface.
|
||||
Future<void> refresh() async {
|
||||
try {
|
||||
final fresh = await HubService.instance.listProjects();
|
||||
if (!listEquals(fresh, _projects)) {
|
||||
_projects = fresh;
|
||||
notifyListeners();
|
||||
}
|
||||
} catch (_) {
|
||||
// Hub unreachable — the pages already surface that state.
|
||||
}
|
||||
}
|
||||
|
||||
/// Test hook: seed registry + selection without a hub and mark
|
||||
/// the persisted state as restored so [ensureLoaded] won't
|
||||
/// overwrite the seed.
|
||||
@visibleForTesting
|
||||
void debugSeed({
|
||||
required List<ProjectRef> projects,
|
||||
required String active,
|
||||
}) {
|
||||
_projects = projects;
|
||||
_activeSlug = active;
|
||||
_loaded = true;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Switch the active workspace ('' = all projects) and persist.
|
||||
Future<void> setActive(String slug) async {
|
||||
if (slug == _activeSlug) return;
|
||||
_activeSlug = slug;
|
||||
notifyListeners();
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString(_prefsKey, slug);
|
||||
} catch (_) {
|
||||
// Persistence is best-effort; the in-memory switch stands.
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue