diff --git a/docs/migration-doc.md b/docs/migration-doc.md
index 9b40d29c..5f774de0 100644
--- a/docs/migration-doc.md
+++ b/docs/migration-doc.md
@@ -637,9 +637,54 @@ Each component toggle below documents a single commit.
+
+CircularHeatmapComponent (d7abc966 )
+
+- Full signal migration: `filtersTeams`, `filtersTeamGroups`, `teamGroups`, `allSectors`, `selectedSector`, `showOverlay`, `showFilters`, `showActivityCard`, `showActivityDetails`, and `dataStore` converted from plain properties to `signal()`.
+- `hasTeamsFilter` derived as `computed()` from `filtersTeams` (no manual bookkeeping)
+- Replaced `ngOnInit` + `requestAnimationFrame` hack with `afterNextRender` (Angular 17+ DOM-ready API).
+- Replaced `themeService.theme$` RxJS subscription with an `effect()` tracking the `ThemeService.theme` signal for reactive heatmap repaint.
+- Dissolved `destroy$` Subject / `takeUntil` / `OnDestroy`: teardown handled by `DestroyRef` (`destroyRef.onDestroy()` and `takeUntilDestroyed()`).
+- Extracted `readThemeColors()` helper to DRY up CSS custom property reads (previously duplicated in init and theme subscriber).
+- Group chip filter switched from listbox-level `(change)` to per-chip `(selectionChange)` with `MatChipSelectionChange` — prevents deselection of the active group chip via a three-tier guard (`isUserInput` check → programmatic deselection ignore → already-active check).
+- **Bug fix:** Replaced `equalArray()` with set-based comparison for group highlight, fixing order-sensitive mismatch between chip/DOM order and YAML declaration order.
+- **Bug fix:** All filter mutations now create new object references via `.set()`, fixing the `keyvalue` pipe caching bug that caused group-selected teams to be silently dropped on subsequent single-chip clicks.
+- **Bug fix:** Group chip deselection no longer leaves stale team selections — the per-chip `(selectionChange)` handler prevents visual deselection entirely.
+- D3 click/hover handlers updated to use `signal.set()` for `selectedSector`, `showActivityCard`.
+- Template updated: all signal properties called with `()` in bindings.
+- **Files:** `circular-heatmap.component.ts`, `circular-heatmap.component.html`
-
+
+
+
+
+MappingComponent, SettingsComponent & TeamsComponent (d0089a97 )
+
+**MappingComponent**
+- `allTeams`, `dataStore`, and `searchTerms` converted from plain properties to `signal()`.
+- Search-term chip handlers (`removeSearchTerm`, `clearFilter`) and `onSearchKeydown` now use `.update()` / `.set()` instead of direct array mutation.
+- `setYamlData()` writes `dataStore` and `allTeams` via `.set()`.
+- Template updated: `searchTerms()`, `allTeams()`, `dataStore()` called as functions, including the optional-chained `dataStore().progressStore?.getTeamActivityTitle(...)`.
+
+**SettingsComponent**
+- `meta`, `dataStoreMaxLevel`, `selectedMaxLevel`, `selectedMaxLevelCaption`, `editingProgressDefinitions`, `remoteReleaseCheck`, and `selectedDateFormat` converted to `signal()`.
+- Removed unused `GithubReleaseInfo` import and dead `checkingLatest` / `latestReleaseInfo` / `latestCheckError` / `isNewerAvailable` / `latestDownloadUrl` / `latestReleasePublishedDate` fields. These were leftover state never wired to the template, superseded by `remoteReleaseCheck`.
+- `checkForLatestRelease()` rewritten to build the release-check result via local variables (`latestRelease`, `checkError`) and commit them in a single `.update()`/`.set()` call per branch, instead of mutating individual `remoteReleaseCheck` fields in place across try/catch/finally.
+- `onMaxLevelChange`, `updateMaxLevelCaption`, `toggleProgressDefinitionsEdit`, `saveProgressDefinitions`, `resetProgressDefinitions` updated to read/write via signal `()`/`.set()`/`.update()`.
+- Template updated: all signal properties called with `()`, including `meta()?.activityMeta?.getDsommVersion()` and `remoteReleaseCheck().latestRelease?.tagName`-style chains.
+- Spec updated: `component.meta.set({...})` and `component.selectedMaxLevel()` assertions.
+
+**TeamsComponent**
+- `dataStore`, `canEdit`, `teams`, `teamGroups`, `progressTitleImplemented`, `infoTitle`, `infoTeams`, `info`, `allColumnNames`, and `progressColumnNames` converted to `signal()`.
+- `onSelectionChanged` and `onTeamsChanged` reworked to derive a new `info`/`currentInfo` object and commit it via `.set()`, rather than mutating the existing record in place (avoids the same stale-reference class of bug fixed in the heatmap migration).
+- `setYamlData()`, `updateColumnNames()`, `onExportTeamGroups()`, `onResetTeamGroups()`, `makeTeamSummary()` updated to read signals via `()`.
+- Template updated: `teams()`, `teamGroups()`, `canEdit()`, `dataStore()`, `infoTitle()`, `infoTeams()`, `info()`, `allColumnNames()`, `progressColumnNames()` called as functions throughout.
+- Spec updated: `component.teams()`, `component.teamGroups()` assertions.
+
+- **Files:** `mapping.component.ts`, `mapping.component.html`, `settings.component.ts`, `settings.component.html`, `settings.component.spec.ts`, `teams.component.ts`, `teams.component.html`, `teams.component.spec.ts` (8 files)
+
+
---
@@ -648,7 +693,7 @@ Each component toggle below documents a single commit.
| # | Area | Issue | Priority | Notes |
|---|-----------|-------|----------|-------|
| 1 | `xlsx` (SheetJS) dependency | Prototype pollution + ReDoS vulnerability, no upstream fix available (maintainers stopped publishing security patches) | Medium | Requires code changes wherever `xlsx` is imported for spreadsheet export. Options: replace with `exceljs` or `xlsx-js-style` (community fork), or accept risk if only used for non-sensitive data export. Needs a dedicated PR. |
-| 2 | `CircularHeatmapComponent` | • **Order-sensitive group highlight:** `equalArray` compares chip/DOM order against YAML declaration order — mismatched order prevents highlighting. Fix: set-based comparison. • **Group selection wipe:** `toggleTeamGroupFilter` mutates `filtersTeams` in place, but `keyvalue` pipe caches on object reference — a subsequent single-chip click drops group-selected teams. Fix: signal-based state. • **Stale group deselection:** Deselecting a group chip visually deselects it but doesn't clear the underlying team selection. • **Layout shift on scroll:** Heatmap shifts vertically at certain viewport widths. | High | All issues will be fixed in one upcoming PR — migrating `CircularHeatmapComponent` to Signals (as `MatrixComponent` already does). |
+| 2 | `CircularHeatmapComponent` | • **Layout shift on scroll:** Heatmap shifts vertically at certain viewport widths. | Low | Remaining issue after signal migration. The three chip/filter bugs (order-sensitive group highlight, group selection wipe, stale group deselection) were resolved in the `CircularHeatmapComponent` signal migration. |
| 3 | Logging | Replace `console.log()` and boolean `environment.production` checks with a proper logging library using log-level feature toggles. Preferred Library: [Winston](https://github.com/winstonjs/winston). | Low | Discussed in team meeting. |
| 4 | Test Runner | • **Deprecated subdependencies:** Karma pulls in `glob@7.2.3`, `inflight@1.0.6`, and `rimraf@3.0.2` — all deprecated, cluttering `pnpm install` with warnings. • **pnpm incompatibility:** Karma's Webpack-based builder (`@angular-devkit/build-angular:karma`) cannot resolve transitive dependencies (e.g. `@babel/runtime`) under pnpm's strict symlinked `node_modules`. Currently requires `node-linker=hoisted` in `.npmrc` as a workaround, defeating pnpm's strictness benefits. • **Modern tooling alignment:** Build/serve already use esbuild/Vite via `@angular/build`. Tests are the last piece still on the legacy Webpack pipeline. Migrating would allow dropping `@angular-devkit/build-angular` entirely. | High | Karma is deprecated. Migrate to a Vite-based test runner (e.g. Vitest or `@angular/build` native test support). |
diff --git a/src/app/pages/circular-heatmap/circular-heatmap.component.html b/src/app/pages/circular-heatmap/circular-heatmap.component.html
index 7debf161..8527c78a 100644
--- a/src/app/pages/circular-heatmap/circular-heatmap.component.html
+++ b/src/app/pages/circular-heatmap/circular-heatmap.component.html
@@ -2,12 +2,12 @@
-
- @if (showActivityDetails) {
+
+ @if (showActivityDetails()) {
@@ -28,14 +28,17 @@ Nothing to show
- keyboard_arrow_up
- filter_alt
+ keyboard_arrow_up
+ filter_alt
-
+
Team Group Filter
-
- @for (group of filtersTeamGroups | keyvalue: unsorted; track group.key) {
-
+
+ @for (group of filtersTeamGroups() | keyvalue: unsorted; track group.key) {
+
{{ group.key }}
}
@@ -43,7 +46,7 @@ Nothing to show
Team Filter
- @for (team of filtersTeams | keyvalue; track team.key) {
+ @for (team of filtersTeams() | keyvalue; track team.key) {
{{ team.key }}
@@ -52,14 +55,14 @@ Nothing to show
- @if (showActivityCard) {
+ @if (showActivityCard()) {
- {{ showActivityCard.dimension }}
- Level {{ showActivityCard.level }}
+ {{ showActivityCard()?.dimension }}
+ Level {{ showActivityCard()?.level }}
@for (
- activity of showActivityCard.activities;
+ activity of showActivityCard()?.activities;
track activity;
let activityIndex = $index
) {
@@ -83,8 +86,8 @@ Nothing to show
- @for (team of filtersTeams | keyvalue; track team) {
- @if (team.value || !hasTeamsFilter) {
+ @for (team of filtersTeams() | keyvalue; track team) {
+ @if (team.value || !hasTeamsFilter()) {
{{ team.key }}:
= {};
- filtersTeamGroups: Record = {};
- teamGroups: TeamGroups = {};
- hasTeamsFilter: boolean = false;
maxLevel: number = 0;
dimensionLabels: string[] = [];
progressStates: string[] = [];
- allSectors: Sector[] = [];
- selectedSector: Sector | null = null;
- theme: string;
theme_colors!: Record;
- private destroy$ = new Subject();
+ // ── Signals ──
+ readonly showOverlay = signal(false);
+ readonly showFilters = signal(true);
+ readonly showActivityCard = signal(null);
+ readonly showActivityDetails = signal(null);
+ readonly dataStore = signal(null);
+ readonly filtersTeams = signal>({});
+ readonly filtersTeamGroups = signal>({});
+ readonly teamGroups = signal({});
+ readonly allSectors = signal([]);
+ readonly selectedSector = signal(null);
+
+ // ── Computed ──
+ readonly hasTeamsFilter = computed(() => Object.values(this.filtersTeams()).some(v => v));
constructor() {
- this.theme = this.themeService.getTheme();
- }
-
- ngOnInit(): void {
- requestAnimationFrame(() => {
- // Now the DOM has the correct class and CSS vars are live
- console.log(`${perfNow()}s: ngOnInit: Initial theme:`, this.theme);
- const css = getComputedStyle(document.body);
- this.theme_colors = {
- background: css.getPropertyValue('--heatmap-background').trim(),
- filled: css.getPropertyValue('--heatmap-filled').trim(),
- disabled: css.getPropertyValue('--heatmap-disabled').trim(),
- cursor: css.getPropertyValue('--heatmap-cursor-hover').trim(),
- stroke: css.getPropertyValue('--heatmap-stroke').trim(),
- };
- console.debug(`${perfNow()}s: ngOnInit: Heatmap theme colors:`, this.theme_colors);
- if (!this.theme_colors['background'] || !this.theme_colors['filled']) {
- console.debug(css);
- }
+ this.destroyRef.onDestroy(() => this.titleService.clearTitle());
+
+ // Wait for DOM to be ready, then load data and draw the heatmap
+ afterNextRender(() => {
+ this.readThemeColors();
console.log(`${perfNow()}: Heat: Loading yamls...`);
- // Ensure that Levels and Teams load before MaturityData
- // using promises, since ngOnInit does not support async/await
this.loader
.load()
.then((dataStore: DataStore) => {
@@ -136,12 +128,13 @@ export class CircularHeatmapComponent implements OnInit, OnDestroy {
throw Error('No progressStore available');
}
- this.filtersTeams = this.buildFilters(dataStore.meta?.teams as string[]);
+ this.filtersTeams.set(this.buildFilters(dataStore.meta?.teams as string[]));
// Insert key: 'All' with value: [], in the first position of the meta.teamGroups Record
const allTeamsGroupName: string = dataStore.getMetaString('allTeamsGroupName') || 'All';
- this.teamGroups = { [allTeamsGroupName]: [], ...(dataStore.meta?.teamGroups || {}) };
- this.filtersTeamGroups = this.buildFilters(Object.keys(this.teamGroups));
- this.filtersTeamGroups[allTeamsGroupName] = true;
+ this.teamGroups.set({ [allTeamsGroupName]: [], ...(dataStore.meta?.teamGroups || {}) });
+ const groupFilters = this.buildFilters(Object.keys(this.teamGroups()));
+ groupFilters[allTeamsGroupName] = true;
+ this.filtersTeamGroups.set(groupFilters);
let progressDefinition: ProgressDefinitions = dataStore.meta?.progressDefinition || {};
this.sectorService.init(
@@ -155,7 +148,12 @@ export class CircularHeatmapComponent implements OnInit, OnDestroy {
this.setYamlData(dataStore);
// For now, just draw the sectors (no activities yet)
- this.loadCircularHeatMap('#chart', this.allSectors, this.dimensionLabels, this.maxLevel);
+ this.loadCircularHeatMap(
+ '#chart',
+ this.allSectors(),
+ this.dimensionLabels,
+ this.maxLevel
+ );
console.log(`${perfNow()}: Page loaded: Circular Heatmap`);
// Check if there's a URL fragment and open the corresponding activity
@@ -168,36 +166,36 @@ export class CircularHeatmapComponent implements OnInit, OnDestroy {
}
});
});
- // Reactively handle theme changes (if user toggles later)
- this.themeService.theme$.pipe(takeUntil(this.destroy$)).subscribe((theme: string) => {
- console.log(`${perfNow()}s: themeService.pipe: Theme changed to:`, theme);
- const css = getComputedStyle(document.body);
- this.theme_colors = {
- background: css.getPropertyValue('--heatmap-background').trim(),
- filled: css.getPropertyValue('--heatmap-filled').trim(),
- disabled: css.getPropertyValue('--heatmap-disabled').trim(),
- cursor: css.getPropertyValue('--heatmap-cursor-hover').trim(),
- stroke: css.getPropertyValue('--heatmap-stroke').trim(),
- };
- console.debug(`${perfNow()}s: themeService.pipe: Heatmap theme colors:`, this.theme_colors);
- // Repaint segments with new theme
- this.reColorHeatmap();
+ // Reactively repaint heatmap on theme changes
+ effect(() => {
+ const theme = this.themeService.theme(); // tracked dependency
+ this.readThemeColors();
+ console.log(`${perfNow()}s: theme effect: Theme changed to:`, theme);
+ if (this.allSectors().length > 0) {
+ this.reColorHeatmap();
+ }
});
}
- ngOnDestroy(): void {
- this.titleService.clearTitle();
- this.destroy$.next();
- this.destroy$.complete();
+ private readThemeColors(): void {
+ const css = getComputedStyle(document.body);
+ this.theme_colors = {
+ background: css.getPropertyValue('--heatmap-background').trim(),
+ filled: css.getPropertyValue('--heatmap-filled').trim(),
+ disabled: css.getPropertyValue('--heatmap-disabled').trim(),
+ cursor: css.getPropertyValue('--heatmap-cursor-hover').trim(),
+ stroke: css.getPropertyValue('--heatmap-stroke').trim(),
+ };
+ console.debug(`${perfNow()}s: readThemeColors:`, this.theme_colors);
}
checkUrlFragmentForActivity() {
// Check if there's a URL fragment that might be an activity UUID
this.route.fragment
- .pipe(takeUntil(this.destroy$), distinctUntilChanged())
+ .pipe(takeUntilDestroyed(this.destroyRef), distinctUntilChanged())
.subscribe(fragment => {
- if (fragment && this.dataStore) {
+ if (fragment && this.dataStore()) {
this.navigateToActivityByUuid(fragment);
}
});
@@ -208,18 +206,17 @@ export class CircularHeatmapComponent implements OnInit, OnDestroy {
}
setYamlData(dataStore: DataStore) {
- this.dataStore = dataStore;
+ this.dataStore.set(dataStore);
this.maxLevel = this.settings?.getMaxLevel() || dataStore.getMaxLevel();
this.dimensionLabels = dataStore?.activityStore?.getAllDimensionNames() || [];
// Prepare all sectors: one for each (dimension, level) pair
- this.allSectors = [];
+ const sectors: Sector[] = [];
for (let level = 1; level <= this.maxLevel; level++) {
- // let DEBUG_DIM_INDEX = 0;
for (let dimName of this.dimensionLabels) {
const activities: Activity[] =
dataStore?.activityStore?.getActivities(dimName, level) || [];
- this.allSectors.push({
+ sectors.push({
dimension: dimName,
// dimensionIndex: DEBUG_DIM_INDEX++,
level: level,
@@ -227,6 +224,7 @@ export class CircularHeatmapComponent implements OnInit, OnDestroy {
});
}
}
+ this.allSectors.set(sectors);
}
buildFilters(names: string[]): Record {
@@ -239,23 +237,31 @@ export class CircularHeatmapComponent implements OnInit, OnDestroy {
return filters;
}
- toggleTeamGroupFilter(event: MatChipListboxChange) {
- const teamGroup = event.value;
- if (!teamGroup) return;
+ onGroupChipChange(event: MatChipSelectionChange, groupKey: string) {
+ if (!event.selected && event.isUserInput) {
+ event.source.select();
+ return;
+ }
- console.log(`${perfNow()}: Heat: Chip flip Group '${teamGroup}'`);
+ if (!event.selected || this.filtersTeamGroups()[groupKey]) return;
- Object.keys(this.filtersTeamGroups).forEach(key => {
- this.filtersTeamGroups[key] = key === teamGroup;
+ console.log(`${perfNow()}: Heat: Chip flip Group '${groupKey}'`);
+
+ const newGroupFilters: Record = {};
+ Object.keys(this.filtersTeamGroups()).forEach(key => {
+ newGroupFilters[key] = key === groupKey;
});
+ this.filtersTeamGroups.set(newGroupFilters);
+ const groups = this.teamGroups();
const selectedTeams: TeamName[] = [];
- Object.keys(this.filtersTeams).forEach(key => {
- this.filtersTeams[key] = this.teamGroups[teamGroup]?.includes(key) || false;
- if (this.filtersTeams[key]) selectedTeams.push(key);
+ const newTeamFilters: Record = {};
+ Object.keys(this.filtersTeams()).forEach(key => {
+ newTeamFilters[key] = groups[groupKey]?.includes(key) || false;
+ if (newTeamFilters[key]) selectedTeams.push(key);
});
+ this.filtersTeams.set(newTeamFilters);
this.sectorService.setVisibleTeams(selectedTeams);
- this.hasTeamsFilter = Object.values(this.filtersTeams).some(v => v === true);
this.reColorHeatmap();
}
@@ -263,36 +269,48 @@ export class CircularHeatmapComponent implements OnInit, OnDestroy {
const selectedTeams: string[] = event.value || [];
console.log(`${perfNow()}: Heat: Team filter changed: [${selectedTeams.join(', ')}]`);
- Object.keys(this.filtersTeams).forEach(key => {
- this.filtersTeams[key] = selectedTeams.includes(key);
+ const newTeamFilters: Record = {};
+ Object.keys(this.filtersTeams()).forEach(key => {
+ newTeamFilters[key] = selectedTeams.includes(key);
});
+ this.filtersTeams.set(newTeamFilters);
- this.hasTeamsFilter = selectedTeams.length > 0;
this.sectorService.setVisibleTeams(selectedTeams);
- Object.keys(this.teamGroups || {}).forEach(group => {
- this.filtersTeamGroups[group] = equalArray(selectedTeams, this.teamGroups[group]);
+ // Set-based comparison for group highlight (fixes order-sensitive bug)
+ const selectedSet = new Set(selectedTeams);
+ const groups = this.teamGroups();
+ const newGroupFilters: Record = {};
+ Object.keys(groups).forEach(group => {
+ const groupTeams = groups[group];
+ newGroupFilters[group] =
+ groupTeams.length > 0 &&
+ groupTeams.length === selectedSet.size &&
+ groupTeams.every(t => selectedSet.has(t));
});
- this.filtersTeamGroups = { ...this.filtersTeamGroups };
+ this.filtersTeamGroups.set(newGroupFilters);
this.reColorHeatmap();
}
getTeamProgressState(activityUuid: string, teamName: string): string {
- return this.dataStore?.progressStore?.getTeamActivityTitle(activityUuid, teamName) || '';
+ return this.dataStore()?.progressStore?.getTeamActivityTitle(activityUuid, teamName) || '';
}
getBackedupTeamProgressState(activityUuid: string, teamName: string): string {
- return this.dataStore?.progressStore?.getTeamActivityTitle(activityUuid, teamName, true) || '';
+ return (
+ this.dataStore()?.progressStore?.getTeamActivityTitle(activityUuid, teamName, true) || ''
+ );
}
onProgressChange(activityUuid: Uuid, teamName: TeamName, newProgress: ProgressTitle) {
- if (!this.dataStore || !this.dataStore.progressStore || !this.dataStore.activityStore) {
+ const ds = this.dataStore();
+ if (!ds || !ds.progressStore || !ds.activityStore) {
throw Error('Data store or progress store is not initialized.');
}
- this.dataStore.progressStore.setTeamActivityProgressState(activityUuid, teamName, newProgress);
- let activity: Activity = this.dataStore.activityStore.getActivityByUuid(activityUuid);
+ ds.progressStore.setTeamActivityProgressState(activityUuid, teamName, newProgress);
+ let activity: Activity = ds.activityStore.getActivityByUuid(activityUuid);
let index =
this.dimensionLabels.indexOf(activity.dimension) +
this.dimensionLabels.length * (activity.level - 1);
@@ -312,7 +330,7 @@ export class CircularHeatmapComponent implements OnInit, OnDestroy {
onDependencyClicked(activityName: string) {
console.log(`${perfNow()}: Heat: Dependency clicked: '${activityName}'`);
- const activity = this.dataStore?.activityStore?.getActivityByName(activityName);
+ const activity = this.dataStore()?.activityStore?.getActivityByName(activityName);
if (activity?.uuid) {
this.navigateToActivityByUuid(activity.uuid);
}
@@ -377,16 +395,17 @@ export class CircularHeatmapComponent implements OnInit, OnDestroy {
.on('click', function () {
var clickedId = d3.select(this).attr('id');
var index = parseInt(clickedId.replace('index-', ''));
- _self.selectedSector = dataset[index]; // Store selected sector for details
+ const sector = dataset[index];
+ _self.selectedSector.set(sector);
// Assign showActivityCard to the sector if it has activities, else null
- if (_self.selectedSector?.activities?.length) {
+ if (sector?.activities?.length) {
_self.setSectorCursor(svg, '#selected', clickedId);
- _self.showActivityCard = _self.selectedSector;
- console.log(`${perfNow()}: Heat: Clicked sector: '${_self.selectedSector.dimension}' Level: ${_self.selectedSector.level}`); // eslint-disable-line
+ _self.showActivityCard.set(sector);
+ console.log(`${perfNow()}: Heat: Clicked sector: '${sector.dimension}' Level: ${sector.level}`); // eslint-disable-line
} else {
- _self.showActivityCard = null;
+ _self.showActivityCard.set(null);
_self.setSectorCursor(svg, '#selected', '');
- console.log(`${perfNow()}: Heat: Clicked disabled sector: '${_self?.selectedSector?.dimension}' Level: ${_self?.selectedSector?.level}`); // eslint-disable-line
+ console.log(`${perfNow()}: Heat: Clicked disabled sector: '${sector?.dimension}' Level: ${sector?.level}`); // eslint-disable-line
}
})
.on('mouseover', function () {
@@ -634,26 +653,28 @@ export class CircularHeatmapComponent implements OnInit, OnDestroy {
openActivityDetails(uuid: string) {
// Find the activity in the selected sector
- if (!this.dataStore || !this.dataStore.activityStore) {
+ const ds = this.dataStore();
+ if (!ds || !ds.activityStore) {
console.error(`Data store is not initialized. Cannot open activity ${uuid}`);
return;
}
- if (!this.showActivityCard || !this.showActivityCard.activities) {
- this.showOverlay = true;
+ const card = this.showActivityCard();
+ if (!card || !card.activities) {
+ this.showOverlay.set(true);
return;
}
- const activity: Activity = this.dataStore.activityStore.getActivityByUuid(uuid);
+ const activity: Activity = ds.activityStore.getActivityByUuid(uuid);
if (!activity) {
- this.showOverlay = true;
+ this.showOverlay.set(true);
return;
}
// Prepare navigationExtras and details
/* eslint-disable */
console.log(`${perfNow()}: Heat: Open Overlay: '${activity.name}'`);
- this.showActivityDetails = activity;
- this.showOverlay = true;
+ this.showActivityDetails.set(activity);
+ this.showOverlay.set(true);
// Update URL with activity UUID as fragment
if (activity.uuid) {
@@ -668,19 +689,20 @@ export class CircularHeatmapComponent implements OnInit, OnDestroy {
navigateToActivityByUuid(uuid: string) {
console.log(`${perfNow()}: Heat: Attempting to open activity with UUID: ${uuid}`);
- if (!this.dataStore || !this.dataStore.activityStore) {
+ const ds = this.dataStore();
+ if (!ds || !ds.activityStore) {
console.error('Data store is not initialized. Cannot open activity by UUID');
return;
}
- const activity: Activity = this.dataStore.activityStore.getActivityByUuid(uuid);
- const sector = this.allSectors.find(s => s.activities.some(a => a.uuid === uuid));
+ const activity: Activity = ds.activityStore.getActivityByUuid(uuid);
+ const sector = this.allSectors().find(s => s.activities.some(a => a.uuid === uuid));
if (activity && sector) {
- this.selectedSector = sector;
- this.showActivityCard = sector;
+ this.selectedSector.set(sector);
+ this.showActivityCard.set(sector);
this.openActivityDetails(activity.uuid);
} else {
// Only close the overlay, do not update the URL
- this.showOverlay = false;
+ this.showOverlay.set(false);
console.warn(`Heat: Activity with UUID ${uuid} not found.`);
}
}
@@ -692,16 +714,17 @@ export class CircularHeatmapComponent implements OnInit, OnDestroy {
fragment: undefined,
queryParamsHandling: 'preserve',
});
- this.showOverlay = false;
+ this.showOverlay.set(false);
}
toggleFilters() {
- this.showFilters = !this.showFilters;
+ this.showFilters.update(v => !v);
}
reColorHeatmap() {
- console.debug(`${perfNow()}s: Recoloring heatmap of ${this.allSectors.length} sectors`);
- for (let index = 0; index < this.allSectors.length; index++) {
+ const sectors = this.allSectors();
+ console.debug(`${perfNow()}s: Recoloring heatmap of ${sectors.length} sectors`);
+ for (let index = 0; index < sectors.length; index++) {
this.recolorSector(index);
}
}
@@ -712,7 +735,7 @@ export class CircularHeatmapComponent implements OnInit, OnDestroy {
.domain([0, 1])
.range([this.theme_colors['background'], this.theme_colors['filled']]);
- let progressValue: number = this.getSectorProgress(this.allSectors[index]);
+ let progressValue: number = this.getSectorProgress(this.allSectors()[index]);
if (progressValue) console.debug(`${perfNow()}s: recolorSector #${index} sector: ${progressValue.toFixed(2)} (${this.theme_colors['filled']})`); // eslint-disable-line
d3.select('#index-' + index).attr(
@@ -723,33 +746,35 @@ export class CircularHeatmapComponent implements OnInit, OnDestroy {
exportTeamProgress() {
console.log(`${perfNow()}: Exporting teams and groups`);
+ const ds = this.dataStore();
- let yamlStr: string | null = this.dataStore?.progressStore?.asYamlString() || null;
+ let yamlStr: string | null = ds?.progressStore?.asYamlString() || null;
if (!yamlStr) {
this.displayMessage(new DialogInfo('No team progress data available', 'Export Error'));
return;
}
- downloadYamlFile(yamlStr, this.dataStore?.meta?.teamProgressFile || 'team-progress.yaml');
+ downloadYamlFile(yamlStr, ds?.meta?.teamProgressFile || 'team-progress.yaml');
}
exportTeamEvidences() {
console.log(`${perfNow()}: Exporting team evidence`);
+ const ds = this.dataStore();
- let yamlStr: string | null = this.dataStore?.evidenceStore?.asYamlString() || null;
+ let yamlStr: string | null = ds?.evidenceStore?.asYamlString() || null;
if (!yamlStr) {
this.displayMessage(new DialogInfo('No team evidence data available', 'Export Error'));
return;
}
- downloadYamlFile(yamlStr, this.dataStore?.meta?.teamEvidenceFile || 'team-evidence.yaml');
+ downloadYamlFile(yamlStr, ds?.meta?.teamEvidenceFile || 'team-evidence.yaml');
}
async deleteLocalTeamsProgress() {
let buttonClicked: string = await this.displayDeleteLocalFilesDialog('progress');
if (buttonClicked === 'Delete') {
- this.dataStore?.progressStore?.deleteBrowserStoredTeamProgress();
+ this.dataStore()?.progressStore?.deleteBrowserStoredTeamProgress();
location.reload(); // Make sure all load routines are initialized
}
}
@@ -758,7 +783,7 @@ export class CircularHeatmapComponent implements OnInit, OnDestroy {
let buttonClicked: string = await this.displayDeleteLocalFilesDialog('evidence');
if (buttonClicked === 'Delete') {
- this.dataStore?.evidenceStore?.deleteBrowserStoredEvidence();
+ this.dataStore()?.evidenceStore?.deleteBrowserStoredEvidence();
location.reload(); // Make sure all load routines are initialized
}
}
@@ -804,12 +829,13 @@ export class CircularHeatmapComponent implements OnInit, OnDestroy {
}
openAddEvidenceModal(activityUuid: string): void {
- const teams = this.dataStore?.meta?.teams || [];
+ const ds = this.dataStore();
+ const teams = ds?.meta?.teams || [];
const dialogData: AddEvidenceModalData = {
activityUuid,
allTeams: teams,
- teamGroups: this.teamGroups,
+ teamGroups: this.teamGroups(),
};
const dialogRef = this.dialog.open(AddEvidenceModalComponent, {
@@ -818,8 +844,8 @@ export class CircularHeatmapComponent implements OnInit, OnDestroy {
});
dialogRef.afterClosed().subscribe(result => {
- if (result && result.entry && this.dataStore?.evidenceStore) {
- this.dataStore.evidenceStore.addEvidence(result.activityUuid, result.entry);
+ if (result && result.entry && ds?.evidenceStore) {
+ ds.evidenceStore.addEvidence(result.activityUuid, result.entry);
console.log(`${perfNow()}: Evidence added for activity ${result.activityUuid}`);
}
});
diff --git a/src/app/pages/mapping/mapping.component.html b/src/app/pages/mapping/mapping.component.html
index a53e27b4..b82df000 100644
--- a/src/app/pages/mapping/mapping.component.html
+++ b/src/app/pages/mapping/mapping.component.html
@@ -4,7 +4,7 @@
Search
- @for (term of searchTerms; track term) {
+ @for (term of searchTerms(); track term) {
{{ term }}
cancel
@@ -19,7 +19,7 @@
placeholder="Type and press Enter..."
[matChipInputAddOnBlur]="false" />
- @if (searchTerms.length) {
+ @if (searchTerms().length) {
close
@@ -114,7 +114,7 @@
SAMM
ISO 27001:2017
ISO 27001:2022
- @for (team of allTeams; track team) {
+ @for (team of allTeams(); track team) {
{{ team + ' - Status' }}
@@ -209,10 +209,10 @@
{{ item.ISO22 | slice: 0 : 32767 }}
}
- @for (team of allTeams; track team) {
+ @for (team of allTeams(); track team) {
- @if (dataStore.progressStore) {
- {{ dataStore.progressStore.getTeamActivityTitle(item.uuid, team) }}
+ @if (dataStore().progressStore) {
+ {{ dataStore().progressStore?.getTeamActivityTitle(item.uuid, team) }}
}
}
diff --git a/src/app/pages/mapping/mapping.component.ts b/src/app/pages/mapping/mapping.component.ts
index 4d1879aa..06614d76 100644
--- a/src/app/pages/mapping/mapping.component.ts
+++ b/src/app/pages/mapping/mapping.component.ts
@@ -1,4 +1,12 @@
-import { Component, OnInit, AfterViewInit, ViewChild, ElementRef, inject } from '@angular/core';
+import {
+ Component,
+ OnInit,
+ AfterViewInit,
+ ViewChild,
+ ElementRef,
+ inject,
+ signal,
+} from '@angular/core';
import { MatTableDataSource, MatTableModule } from '@angular/material/table';
import { MatSort, MatSortModule } from '@angular/material/sort';
import { COMMA, ENTER } from '@angular/cdk/keycodes';
@@ -84,7 +92,7 @@ export class MappingComponent implements OnInit, AfterViewInit {
knowledgeLabels: string[] = [];
generalLabels: string[] = [];
- allTeams: string[] = [];
+ allTeams = signal([]);
displayedColumns: string[] = [
'dimension',
'subDimension',
@@ -98,9 +106,9 @@ export class MappingComponent implements OnInit, AfterViewInit {
@ViewChild('chipInput') chipInput!: ElementRef;
@ViewChild(MatSort, { static: false }) sort!: MatSort;
- dataStore: DataStore = new DataStore();
+ dataStore = signal(new DataStore());
- searchTerms: string[] = [];
+ searchTerms = signal([]);
searchCtrl = new FormControl('');
ngOnInit(): void {
@@ -138,8 +146,8 @@ export class MappingComponent implements OnInit, AfterViewInit {
}
setYamlData(dataStore: DataStore) {
- this.dataStore = dataStore;
- this.allTeams = dataStore.meta?.teams || [];
+ this.dataStore.set(dataStore);
+ this.allTeams.set(dataStore.meta?.teams || []);
this.allMappings = this.transformDataStore(dataStore);
this.dataSource.data = this.allMappings;
}
@@ -193,31 +201,31 @@ export class MappingComponent implements OnInit, AfterViewInit {
const input = event.target as HTMLInputElement;
const value = input.value.trim();
if (event.key === 'Enter' && value) {
- if (!this.searchTerms.includes(value.toLowerCase())) {
- this.searchTerms.push(value.toLowerCase());
+ if (!this.searchTerms().includes(value.toLowerCase())) {
+ this.searchTerms.update(terms => [...terms, value.toLowerCase()]);
this.updateFilter();
}
input.value = '';
this.searchCtrl.setValue('');
- } else if (!value && this.searchTerms.length === 0) {
+ } else if (!value && this.searchTerms().length === 0) {
this.dataSource.filter = '';
}
}
removeSearchTerm(term: string) {
- this.searchTerms = this.searchTerms.filter(t => t !== term);
+ this.searchTerms.update(terms => terms.filter(t => t !== term));
this.updateFilter();
}
clearFilter() {
console.log(`${perfNow()}: Mapping: Clear search filter`);
- this.searchTerms = [];
+ this.searchTerms.set([]);
this.dataSource.filter = '';
this.searchCtrl.setValue('');
}
updateFilter() {
- this.dataSource.filter = this.searchTerms.join(SEPARATOR);
+ this.dataSource.filter = this.searchTerms().join(SEPARATOR);
console.log(
`${perfNow()}: Mapping: Search filter: ${this.dataSource.filter?.replace(SEPARATOR, ', ')}`
);
diff --git a/src/app/pages/settings/settings.component.html b/src/app/pages/settings/settings.component.html
index 05e3bc09..54f0281b 100644
--- a/src/app/pages/settings/settings.component.html
+++ b/src/app/pages/settings/settings.component.html
@@ -27,13 +27,13 @@
id="maxLevelSlider"
showTickMarks
min="1"
- [max]="dataStoreMaxLevel || 5"
+ [max]="dataStoreMaxLevel() || 5"
step="1"
class="max-slider"
#ngSlider
>
- {{ selectedMaxLevelCaption }}
+ {{ selectedMaxLevelCaption() }}
@@ -50,10 +50,10 @@
- @if (editingProgressDefinitions) {
+ @if (editingProgressDefinitions()) {
@for (definition of definitionsFormArray.controls; track definition; let i = $index) {
@@ -146,7 +146,7 @@ Progress Definitions
}
}
- @if (editingProgressDefinitions) {
+ @if (editingProgressDefinitions()) {
Define custom progression stages that match your team's workflow. Choose names and
@@ -187,7 +187,7 @@
About the DSOMM Model
While the application is the tool for assessing and tracking DevSecOps maturity, i.e. using
the model.
- @if (meta.activityMeta?.getDsommVersion()) {
+ @if (meta()?.activityMeta?.getDsommVersion()) {
The current version of the DSOMM model in use is:
@@ -195,24 +195,24 @@
About the DSOMM Model
Model version
Release date
- {{ meta.activityMeta?.getDsommVersion() || '' }}
+ {{ meta()?.activityMeta?.getDsommVersion() || '' }}
- {{ dateFormat(meta.activityMeta?.getDsommReleaseDate()) }}
+ {{ dateFormat(meta()?.activityMeta?.getDsommReleaseDate()) }}
- @if (remoteReleaseCheck.isNewerAvailable === false) {
+ @if (remoteReleaseCheck().isNewerAvailable === false) {
Up to date
}
- @if (remoteReleaseCheck.isNewerAvailable === true) {
+ @if (remoteReleaseCheck().isNewerAvailable === true) {
- {{ remoteReleaseCheck.latestRelease?.tagName }}
+ {{ remoteReleaseCheck().latestRelease?.tagName }}
- {{ dateFormat(remoteReleaseCheck.latestRelease?.publishedAt) }}
+ {{ dateFormat(remoteReleaseCheck().latestRelease?.publishedAt) }}
- @if (meta.activityFiles.length > 1) {
+ @if (meta()?.activityFiles && meta()!.activityFiles.length > 1) {
This application uses the official DSOMM Model above, with some local customizations.
The following files are used to describe the model in use:
}
}
- @if (!meta.activityMeta?.getDsommVersion()) {
+ @if (!meta()?.activityMeta?.getDsommVersion()) {
This application runs on a customized version of the DSOMM model:
- @for (file of meta.activityFiles; track file) {
+ @for (file of meta()?.activityFiles || []; track file) {
{{ file.replace('assets/YAML/', '') }}
diff --git a/src/app/pages/settings/settings.component.spec.ts b/src/app/pages/settings/settings.component.spec.ts
index da9ee765..2694cc1d 100644
--- a/src/app/pages/settings/settings.component.spec.ts
+++ b/src/app/pages/settings/settings.component.spec.ts
@@ -76,12 +76,12 @@ describe('SettingsComponent', () => {
beforeEach(fakeAsync(() => {
fixture = TestBed.createComponent(SettingsComponent);
component = fixture.componentInstance;
- component.meta = {
+ component.meta.set({
activityMeta: null,
activityFiles: [],
progressDefinition: {},
saveProgressDefinition: jasmine.createSpy('saveProgressDefinition'),
- } as any;
+ } as any);
fixture.detectChanges();
tick();
@@ -95,14 +95,14 @@ describe('SettingsComponent', () => {
it('should update max level settings correctly', fakeAsync(() => {
component.onMaxLevelChange(3);
- expect(component.selectedMaxLevel).toBe(3);
+ expect(component.selectedMaxLevel()).toBe(3);
expect(settingsService.setMaxLevel).toHaveBeenCalledWith(3);
}));
it('should handle max level reset to default', fakeAsync(() => {
component.onMaxLevelChange(5);
- expect(component.selectedMaxLevel).toBe(5);
+ expect(component.selectedMaxLevel()).toBe(5);
// Remove localStorage when settings' maxLevel is set to activity's maxLevel
expect(settingsService.setMaxLevel).toHaveBeenCalledWith(null);
diff --git a/src/app/pages/settings/settings.component.ts b/src/app/pages/settings/settings.component.ts
index 9080f56c..fdecb04a 100644
--- a/src/app/pages/settings/settings.component.ts
+++ b/src/app/pages/settings/settings.component.ts
@@ -1,4 +1,4 @@
-import { Component, OnInit, inject } from '@angular/core';
+import { Component, OnInit, inject, signal } from '@angular/core';
import {
FormBuilder,
FormGroup,
@@ -9,7 +9,7 @@ import {
ReactiveFormsModule,
} from '@angular/forms';
import { SettingsService } from '../../service/settings/settings.service';
-import { GithubService, GithubReleaseInfo } from 'src/app/service/settings/github.service';
+import { GithubService } from 'src/app/service/settings/github.service';
import { LoaderService } from 'src/app/service/loader/data-loader.service';
import { DataStore } from 'src/app/model/data-store';
import { ProgressDefinitions } from 'src/app/model/types';
@@ -82,22 +82,22 @@ export class SettingsComponent implements OnInit {
modal = inject(ModalMessageComponent);
private githubService = inject(GithubService);
- meta!: MetaStore;
+ meta = signal(null);
progressStore!: ProgressStore;
- dataStoreMaxLevel!: number;
- selectedMaxLevel!: number;
- selectedMaxLevelCaption: String = '';
+ dataStoreMaxLevel = signal(5);
+ selectedMaxLevel = signal(5);
+ selectedMaxLevelCaption = signal('');
progressDefinitionsForm!: FormGroup<{
definitions: FormArray>;
}>;
tempProgressDefinitions: ProgressDefinitions = {};
- editingProgressDefinitions: boolean = false;
- remoteReleaseCheck: RemoteReleaseCheck = {
+ editingProgressDefinitions = signal(false);
+ remoteReleaseCheck = signal({
isChecking: false,
isNewerAvailable: null,
latestRelease: null,
latestCheckError: null,
- };
+ });
private BROWSER_LOCALE = 'BROWSER';
dateFormats = [
@@ -110,15 +110,7 @@ export class SettingsComponent implements OnInit {
{ value: 'ja' },
{ value: 'hu' },
];
- selectedDateFormat: string = this.BROWSER_LOCALE;
-
- // GitHub release check state
- checkingLatest: boolean = false;
- latestReleaseInfo: GithubReleaseInfo | null = null;
- latestCheckError: string | null = null;
- isNewerAvailable: boolean | null = null;
- latestDownloadUrl: string | null = null;
- latestReleasePublishedDate: Date | null = null;
+ selectedDateFormat = signal(this.BROWSER_LOCALE);
ngOnInit(): void {
this.initialize();
@@ -138,36 +130,47 @@ export class SettingsComponent implements OnInit {
}
async checkForLatestRelease(): Promise {
- this.remoteReleaseCheck.isChecking = true;
- this.remoteReleaseCheck.isNewerAvailable = null;
- this.remoteReleaseCheck.latestRelease = null;
- this.remoteReleaseCheck.latestCheckError = null;
-
+ this.remoteReleaseCheck.update(state => ({
+ ...state,
+ isChecking: true,
+ isNewerAvailable: null,
+ latestRelease: null,
+ latestCheckError: null,
+ }));
+
+ let latestRelease: RemoteReleaseInfo | null = null;
try {
- this.remoteReleaseCheck.latestRelease = await this.githubService.getLatestRelease();
+ latestRelease = await this.githubService.getLatestRelease();
} catch (err: any) {
console.warn('Error checking latest DSOMM release', err);
- this.remoteReleaseCheck.latestCheckError = err?.message || 'Failed to check latest release';
+ this.remoteReleaseCheck.update(state => ({
+ ...state,
+ isChecking: false,
+ latestCheckError: err?.message || 'Failed to check latest release',
+ }));
return;
- } finally {
- this.remoteReleaseCheck.isChecking = false;
}
- if (!this.remoteReleaseCheck.latestRelease) {
- this.remoteReleaseCheck.latestCheckError =
- 'Error: No release information received from Github';
+ if (!latestRelease) {
+ this.remoteReleaseCheck.update(state => ({
+ ...state,
+ isChecking: false,
+ latestCheckError: 'Error: No release information received from Github',
+ }));
} else {
- const remote = this.remoteReleaseCheck.latestRelease;
+ const remote = latestRelease;
+ const meta = this.meta();
const remoteTag = (remote && remote.tagName?.replace(/^v/, '')) || '';
- const localTag = this.meta?.activityMeta?.getDsommVersion()?.replace(/^v/, '') || '';
+ const localTag = meta?.activityMeta?.getDsommVersion()?.replace(/^v/, '') || '';
const remoteDate =
remote && remote.publishedAt && new Date(remote.publishedAt.toDateString());
- const localDate = this.meta?.activityMeta?.getDsommReleaseDate();
+ const localDate = meta?.activityMeta?.getDsommReleaseDate();
// Prefer version tag comparison, fallback to published date comparison
let newer = false;
+ let checkError: string | null = null;
if (remoteTag && localTag && remoteDate && localDate) {
newer = remoteTag !== localTag || remoteDate > localDate;
} else {
@@ -179,15 +182,20 @@ export class SettingsComponent implements OnInit {
if (!localTag) tmp.push('local model version');
if (!remoteDate) tmp.push('DSOMM model date');
if (!localDate) tmp.push('local model date');
- this.remoteReleaseCheck.latestCheckError = `Could not determine ${tmp.join(', ')}`; // eslint-disable-line
- console.warn('ERROR: ' + this.remoteReleaseCheck.latestCheckError);
+ checkError = `Could not determine ${tmp.join(', ')}`; // eslint-disable-line
+ console.warn('ERROR: ' + checkError);
}
- this.remoteReleaseCheck.isNewerAvailable = newer;
+ this.remoteReleaseCheck.set({
+ isChecking: false,
+ isNewerAvailable: newer,
+ latestRelease: remote,
+ latestCheckError: checkError,
+ });
}
}
initialize(): void {
- this.selectedDateFormat = this.settings.getDateFormat() || this.BROWSER_LOCALE;
+ this.selectedDateFormat.set(this.settings.getDateFormat() || this.BROWSER_LOCALE);
// Init dates
let date: Date = new Date();
@@ -202,8 +210,8 @@ export class SettingsComponent implements OnInit {
}
setYamlData(dataStore: DataStore): void {
- this.dataStoreMaxLevel = dataStore.getMaxLevel();
- this.selectedMaxLevel = this.settings.getMaxLevel() || this.dataStoreMaxLevel;
+ this.dataStoreMaxLevel.set(dataStore.getMaxLevel());
+ this.selectedMaxLevel.set(this.settings.getMaxLevel() || this.dataStoreMaxLevel());
this.updateMaxLevelCaption();
if (dataStore.progressStore) {
@@ -212,34 +220,38 @@ export class SettingsComponent implements OnInit {
// Load progress definitions
if (dataStore.meta) {
- this.meta = dataStore.meta;
- this.tempProgressDefinitions = deepCopy(this.meta.progressDefinition);
+ this.meta.set(dataStore.meta);
+ this.tempProgressDefinitions = deepCopy(dataStore.meta.progressDefinition);
}
}
onDateFormatChange(): void {
- let value: any = this.selectedDateFormat == 'null' ? null : this.selectedDateFormat;
+ const fmt = this.selectedDateFormat();
+ let value: any = fmt == 'null' ? null : fmt;
this.settings.setDateFormat(value);
}
onMaxLevelChange(value: number | null): void {
- if (value == null) value = this.dataStoreMaxLevel;
- if (value == this.dataStoreMaxLevel) {
+ const maxLevel = this.dataStoreMaxLevel();
+ if (value == null) value = maxLevel;
+ if (value == maxLevel) {
this.settings.setMaxLevel(null);
} else {
this.settings.setMaxLevel(value);
}
- this.selectedMaxLevel = value;
+ this.selectedMaxLevel.set(value);
this.updateMaxLevelCaption();
}
// === Max Level ===
updateMaxLevelCaption(): void {
- if (this.selectedMaxLevel == this.dataStoreMaxLevel) {
- this.selectedMaxLevelCaption = 'All maturity levels';
+ const level = this.selectedMaxLevel();
+ const maxLevel = this.dataStoreMaxLevel();
+ if (level == maxLevel) {
+ this.selectedMaxLevelCaption.set('All maturity levels');
} else {
- if (this.selectedMaxLevel == 1) this.selectedMaxLevelCaption = `Maturity level 1 only`;
- else this.selectedMaxLevelCaption = `Maturity levels 1-${this.selectedMaxLevel} only`;
+ if (level == 1) this.selectedMaxLevelCaption.set('Maturity level 1 only');
+ else this.selectedMaxLevelCaption.set(`Maturity levels 1-${level} only`);
}
}
@@ -356,7 +368,7 @@ export class SettingsComponent implements OnInit {
this.tempProgressDefinitions = this.sortObjectByScore(newProgressDefinitions);
// Save the new progress definitions to MetaStore and localStorage
- this.meta.saveProgressDefinition(this.tempProgressDefinitions);
+ this.meta()!.saveProgressDefinition(this.tempProgressDefinitions);
// Reinitialize the ProgressStore with the new definitions
this.progressStore.init(this.tempProgressDefinitions);
@@ -364,18 +376,18 @@ export class SettingsComponent implements OnInit {
// Save progress data to localStorage
this.progressStore.saveToLocalStorage();
- this.editingProgressDefinitions = false;
+ this.editingProgressDefinitions.set(false);
this.updateProgressDefinitionsForm();
}
resetProgressDefinitions(): void {
- this.tempProgressDefinitions = deepCopy(this.meta.progressDefinition);
- this.editingProgressDefinitions = false;
+ this.tempProgressDefinitions = deepCopy(this.meta()!.progressDefinition);
+ this.editingProgressDefinitions.set(false);
this.updateProgressDefinitionsForm();
}
toggleProgressDefinitionsEdit(): void {
- this.editingProgressDefinitions = !this.editingProgressDefinitions;
+ this.editingProgressDefinitions.update(v => !v);
}
getFormGroupValue(control: AbstractControl, field: string): any {
diff --git a/src/app/pages/teams/teams.component.html b/src/app/pages/teams/teams.component.html
index 6cd05277..915c28af 100644
--- a/src/app/pages/teams/teams.component.html
+++ b/src/app/pages/teams/teams.component.html
@@ -1,13 +1,13 @@
- @if (dataStore.meta?.hasLocalStorage) {
+ @if (dataStore().meta?.hasLocalStorage) {