diff --git a/src/app/component/activity-description/activity-description.component.html b/src/app/component/activity-description/activity-description.component.html index 96d370e73..1c4f614b1 100644 --- a/src/app/component/activity-description/activity-description.component.html +++ b/src/app/component/activity-description/activity-description.component.html @@ -329,7 +329,10 @@
- No teams have started this activity yet + No {{ settings.getTeamLabelPlural() | lowercase }} have started this activity + yet @@ -347,7 +350,10 @@No teams have started implementing this activity yet.
++ No {{ settings.getTeamLabelPlural() | lowercase }} have started implementing this activity + yet. +
- Select which teams to include in the {{ type === 'report-config' ? 'report' : 'evidence' }}. + Select which {{ settings.getTeamLabelPlural() | lowercase }} to include in the + {{ type === 'report-config' ? 'report' : 'evidence' }}.
+ Customize the terminology to how you organize your "teams" and how you group them (e.g. 'Apps' + and 'Portfolios'). +
+| Team | ++ {{ settings.getTeamLabel() }} + | {{ element?.team }} | diff --git a/src/app/pages/teams/teams.component.ts b/src/app/pages/teams/teams.component.ts index c16b10b6f..5f238c8f8 100644 --- a/src/app/pages/teams/teams.component.ts +++ b/src/app/pages/teams/teams.component.ts @@ -161,7 +161,12 @@ export class TeamsComponent implements OnInit, AfterViewInit { if (!yamlStr) { this.displayMessage( - new DialogInfo('No team and groups names stored locally in the browser', 'Export Error') + new DialogInfo( + `No ${this.settings.getTeamLabel().toLowerCase()} and ${this.settings + .getGroupLabelPlural() + .toLowerCase()} names stored locally in the browser`, + 'Export Error' + ) ); return; } @@ -182,7 +187,9 @@ export class TeamsComponent implements OnInit, AfterViewInit { return new Promise((resolve, reject) => { let title: string = 'Delete local browser data'; let message: string = - 'Do you want to reset all team and group names?' + + `Do you want to reset all ${this.settings.getTeamLabel().toLowerCase()} and ${this.settings + .getGroupLabel() + .toLowerCase()} names?` + '\n\nThis will revert the names to the names stored in the yaml file on the server.'; let buttons: string[] = ['Cancel', 'Delete']; this.modal diff --git a/src/app/service/loader/data-loader.service.ts b/src/app/service/loader/data-loader.service.ts index afe580015..89e869e42 100644 --- a/src/app/service/loader/data-loader.service.ts +++ b/src/app/service/loader/data-loader.service.ts @@ -13,6 +13,7 @@ import { } from 'src/app/model/activity-store'; import { DataStore } from 'src/app/model/data-store'; import { NotificationService } from '../notification.service'; +import { SettingsService } from '../settings/settings.service'; export class DataValidationError extends Error { constructor(message: string) { @@ -38,7 +39,8 @@ export class LoaderService { constructor( private yamlService: YamlService, private githubService: GithubService, - private notificationService: NotificationService + private notificationService: NotificationService, + private settingsService: SettingsService ) { this.DSOMM_MODEL_URL = this.githubService.getDsommModelUrl() + '/tree/main/generated'; } @@ -62,6 +64,9 @@ export class LoaderService { this.dataStore.meta = await this.loadMeta(); this.dataStore.progressStore?.init(this.dataStore.meta.progressDefinition); + // Initialize SettingsService with meta.yaml defaults for team/group terminology + this.settingsService.initFromMeta(this.dataStore.getMetaStrings()); + // Then load activities this.dataStore.addActivities(await this.loadActivities(this.dataStore.meta)); diff --git a/src/app/service/settings/settings.service.spec.ts b/src/app/service/settings/settings.service.spec.ts index 14962f813..d0a7b1d1e 100644 --- a/src/app/service/settings/settings.service.spec.ts +++ b/src/app/service/settings/settings.service.spec.ts @@ -1,16 +1,13 @@ import { TestBed } from '@angular/core/testing'; -import { SettingsService } from './settings.service'; +import { SettingsService, LabelParts } from './settings.service'; describe('SettingsService', () => { let service: SettingsService; - let localStorageSpy: any; beforeEach(() => { - // Clear all mocks and create fresh instance for each test localStorage.clear(); TestBed.configureTestingModule({}); service = TestBed.inject(SettingsService); - localStorageSpy = spyOn(localStorage, 'setItem').and.callThrough(); }); afterEach(() => { @@ -21,22 +18,124 @@ describe('SettingsService', () => { expect(service).toBeTruthy(); }); - describe('General Settings Operations', () => { - it('should handle empty string settings', () => { - service.saveSettings('test.key', ''); - expect(localStorage.getItem('test.key')).toBeNull(); + describe('parseLabelParts', () => { + it('should parse a simple label without pipe', () => { + const result: LabelParts = SettingsService.parseLabelParts('Team'); + expect(result).toEqual({ singular: 'Team', plural: 'Teams' }); }); - it('should handle empty array settings', () => { - service.saveSettings('test.key', []); - expect(localStorage.getItem('test.key')).toBeNull(); + it('should parse a pipe-separated label', () => { + const result: LabelParts = SettingsService.parseLabelParts('Industry|Industries'); + expect(result).toEqual({ singular: 'Industry', plural: 'Industries' }); }); - it('should handle empty object settings', () => { - service.saveSettings('test.key', {}); - expect(localStorage.getItem('test.key')).toBeNull(); + it('should handle empty string', () => { + const result: LabelParts = SettingsService.parseLabelParts(''); + expect(result).toEqual({ singular: '', plural: '' }); }); + it('should handle pipe with empty plural (fallback to +s)', () => { + const result: LabelParts = SettingsService.parseLabelParts('App|'); + expect(result).toEqual({ singular: 'App', plural: 'Apps' }); + }); + + it('should trim whitespace', () => { + const result: LabelParts = SettingsService.parseLabelParts(' Client | Clients '); + expect(result).toEqual({ singular: 'Client', plural: 'Clients' }); + }); + }); + + describe('encodeLabelParts', () => { + it('should encode without pipe when plural is default +s', () => { + expect(SettingsService.encodeLabelParts('Team', 'Teams')).toBe('Team'); + }); + + it('should encode with pipe when plural is custom', () => { + expect(SettingsService.encodeLabelParts('Industry', 'Industries')).toBe( + 'Industry|Industries' + ); + }); + + it('should encode without pipe when plural is empty', () => { + expect(SettingsService.encodeLabelParts('App', '')).toBe('App'); + }); + }); + + describe('initFromMeta', () => { + it('should set meta defaults for team and group', () => { + service.initFromMeta({ + team: 'App', + group: 'Portfolio|Portfolios', + allTeamsGroupName: 'All', + labels: [], + maturityLevels: [], + knowledgeLabels: [], + }); + // Without any localStorage override, getters should return meta defaults + expect(service.getTeamLabel()).toBe('App'); + expect(service.getTeamLabelPlural()).toBe('Apps'); + expect(service.getGroupLabel()).toBe('Portfolio'); + expect(service.getGroupLabelPlural()).toBe('Portfolios'); + }); + + it('should be overridden by localStorage values', () => { + service.initFromMeta({ + team: 'App', + group: 'Portfolio', + allTeamsGroupName: 'All', + labels: [], + maturityLevels: [], + knowledgeLabels: [], + }); + service.setTeamLabel('Client', 'Clients'); + expect(service.getTeamLabel()).toBe('Client'); + expect(service.getTeamLabelPlural()).toBe('Clients'); + }); + }); + + describe('Consolidated labels round-trip', () => { + it('should store and retrieve team label', () => { + service.setTeamLabel('App'); + expect(service.getTeamLabel()).toBe('App'); + expect(service.getTeamLabelPlural()).toBe('Apps'); + }); + + it('should store and retrieve team label with custom plural', () => { + service.setTeamLabel('Category', 'Categories'); + expect(service.getTeamLabel()).toBe('Category'); + expect(service.getTeamLabelPlural()).toBe('Categories'); + }); + + it('should store and retrieve group label', () => { + service.setGroupLabel('Division'); + expect(service.getGroupLabel()).toBe('Division'); + expect(service.getGroupLabelPlural()).toBe('Divisions'); + }); + + it('should store and retrieve group label with custom plural', () => { + service.setGroupLabel('Industry', 'Industries'); + expect(service.getGroupLabel()).toBe('Industry'); + expect(service.getGroupLabelPlural()).toBe('Industries'); + }); + + it('should use localStorage key settings.labels', () => { + service.setTeamLabel('Foo'); + service.setGroupLabel('Bar', 'Bars'); + const stored = JSON.parse(localStorage.getItem('settings.labels')!); + expect(stored.team).toBe('Foo'); + expect(stored.group).toBe('Bar'); + }); + + it('should clear labels from localStorage when reset to default', () => { + service.setTeamLabel('Custom'); + expect(localStorage.getItem('settings.labels')).not.toBeNull(); + service.setTeamLabel('Team'); + service.setGroupLabel('Group'); + expect(localStorage.getItem('settings.labels')).toBeNull(); + }); + }); + + describe('Settings helpers', () => { it('should properly store and retrieve number settings', () => { localStorage.setItem('test.key', '42'); expect(service.getSettingsNumber('test.key')).toBe(42); @@ -45,15 +144,5 @@ describe('SettingsService', () => { it('should return null for non-existent number settings', () => { expect(service.getSettingsNumber('nonexistent.key')).toBeNull(); }); - - it('should handle complex object settings', () => { - const complexObj = { - key1: 'value1', - key2: 42, - nested: { prop: true }, - }; - service.saveSettings('test.complex', complexObj); - expect(service.getSettings('test.complex')).toEqual(complexObj); - }); }); }); diff --git a/src/app/service/settings/settings.service.ts b/src/app/service/settings/settings.service.ts index 89fe9bce8..db45028d4 100644 --- a/src/app/service/settings/settings.service.ts +++ b/src/app/service/settings/settings.service.ts @@ -1,4 +1,15 @@ import { Injectable } from '@angular/core'; +import { MetaStrings } from 'src/app/model/meta-store'; + +export interface LabelParts { + singular: string; + plural: string; +} + +export interface Labels { + team: string; + group: string; +} @Injectable({ providedIn: 'root', @@ -6,9 +17,171 @@ import { Injectable } from '@angular/core'; export class SettingsService { private readonly KEY_DATEFORMAT = 'settings.dateformat'; private readonly KEY_MAX_LEVEL = 'settings.maxlevel'; + private readonly KEY_LABELS = 'settings.labels'; private dateformat: string | null = null; private maxLevel: number | null = null; + private _labels: Labels | null = null; + + // Server-level defaults from meta.yaml (set via initFromMeta) + private _metaTeam: LabelParts = { singular: 'Team', plural: 'Teams' }; + private _metaGroup: LabelParts = { singular: 'Group', plural: 'Groups' }; + private _metaInitialized: boolean = false; + + constructor() {} + + // --- Meta initialization --- + + /** + * Called by LoaderService after loading meta.yaml to set server-level defaults. + */ + initFromMeta(metaStrings: MetaStrings): void { + if (metaStrings.team) { + this._metaTeam = SettingsService.parseLabelParts(metaStrings.team); + } + if (metaStrings.group) { + this._metaGroup = SettingsService.parseLabelParts(metaStrings.group); + } + this._metaInitialized = true; + // Reset cached labels so next get reads fresh values with new defaults + this._labels = null; + } + + // --- Meta defaults (for placeholder display) --- + + getMetaTeamLabel(): LabelParts { + return this._metaTeam; + } + + getMetaGroupLabel(): LabelParts { + return this._metaGroup; + } + + // --- Label parsing --- + + /** + * Parse a |-split label string into singular and plural parts. + * "Industry|Industries" => { singular: "Industry", plural: "Industries" } + * "Team" => { singular: "Team", plural: "Teams" } + */ + static parseLabelParts(value: string): LabelParts { + if (!value || value.trim().length === 0) { + return { singular: '', plural: '' }; + } + const parts = value.split('|'); + const singular = parts[0].trim(); + const plural = + parts.length > 1 && parts[1].trim().length > 0 ? parts[1].trim() : singular + 's'; + return { singular, plural }; + } + + /** + * Encode singular + plural into a |-split string. + * If plural === singular + 's', omit the plural part (store just singular). + */ + static encodeLabelParts(singular: string, plural: string): string { + if (!singular || singular.trim().length === 0) return ''; + singular = singular.trim(); + plural = plural.trim(); + if (!plural || plural === singular + 's') { + return singular; + } + return `${singular}|${plural}`; + } + + // --- Labels (consolidated) --- + + private loadLabels(): Labels { + if (this._labels !== null) return this._labels; + + const stored = localStorage.getItem(this.KEY_LABELS); + if (stored) { + try { + this._labels = JSON.parse(stored); + return this._labels!; + } catch { + // Invalid JSON — ignore + } + } + this._labels = { team: '', group: '' }; + return this._labels; + } + + private saveLabels(labels: Labels): void { + this._labels = labels; + // Remove from storage if both are empty/default + if (!labels.team && !labels.group) { + localStorage.removeItem(this.KEY_LABELS); + } else { + localStorage.setItem(this.KEY_LABELS, JSON.stringify(labels)); + } + } + + // --- Team label --- + + getTeamLabel(): string { + const labels = this.loadLabels(); + if (labels.team) { + return SettingsService.parseLabelParts(labels.team).singular; + } + return this._metaTeam.singular || 'Team'; + } + + getTeamLabelPlural(): string { + const labels = this.loadLabels(); + if (labels.team) { + return SettingsService.parseLabelParts(labels.team).plural; + } + return this._metaTeam.plural || 'Teams'; + } + + setTeamLabel(singular: string | null, plural?: string | null): void { + const labels = this.loadLabels(); + const s = singular?.trim() || ''; + const p = plural?.trim() || ''; + + // If the value matches the meta default, don't store it + if (s === this._metaTeam.singular && (!p || p === this._metaTeam.plural)) { + labels.team = ''; + } else { + labels.team = s ? SettingsService.encodeLabelParts(s, p) : ''; + } + this.saveLabels(labels); + } + + // --- Group label --- + + getGroupLabel(): string { + const labels = this.loadLabels(); + if (labels.group) { + return SettingsService.parseLabelParts(labels.group).singular; + } + return this._metaGroup.singular || 'Group'; + } + + getGroupLabelPlural(): string { + const labels = this.loadLabels(); + if (labels.group) { + return SettingsService.parseLabelParts(labels.group).plural; + } + return this._metaGroup.plural || 'Groups'; + } + + setGroupLabel(singular: string | null, plural?: string | null): void { + const labels = this.loadLabels(); + const s = singular?.trim() || ''; + const p = plural?.trim() || ''; + + // If the value matches the meta default, don't store it + if (s === this._metaGroup.singular && (!p || p === this._metaGroup.plural)) { + labels.group = ''; + } else { + labels.group = s ? SettingsService.encodeLabelParts(s, p) : ''; + } + this.saveLabels(labels); + } + + // --- Date format --- getDateFormat(): string | null { if (this.dateformat == null) { @@ -22,6 +195,8 @@ export class SettingsService { this.saveSettings(this.KEY_DATEFORMAT, format); } + // --- Max level --- + getMaxLevel(): number | null { if (this.maxLevel == null) { this.maxLevel = this.getSettingsNumber(this.KEY_MAX_LEVEL); @@ -34,6 +209,8 @@ export class SettingsService { this.saveSettings(this.KEY_MAX_LEVEL, maxLevel); } + // --- Generic settings helpers --- + getSettingsNumber(key: string): number | null { let setting: string | null = localStorage.getItem(key); if (setting == null) { diff --git a/src/assets/YAML/meta.yaml b/src/assets/YAML/meta.yaml index 13d6afecd..289e14508 100644 --- a/src/assets/YAML/meta.yaml +++ b/src/assets/YAML/meta.yaml @@ -37,6 +37,9 @@ activityFiles: lang: en strings: en: + # Optional: customize terminology (use 'singular|plural' for irregular plurals, e.g. 'Industry|Industries') + team: 'Team' + group: 'Group' allTeamsGroupName: 'All teams' maturityLevels: [
|---|