Compare commits

...
Author SHA1 Message Date
copilot-swe-agent[bot] ed6b18b2a3 Initial plan 2025-11-06 23:19:09 +00:00
nessar da31c9c753 Refactor get method in CalendarSettingsService to throw an error if the setting ID doesn't exist 2025-11-07 00:12:02 +01:00
nessar bda3db2aa8 Skip initial execution in storedSignal effect to prevent unnecessary localStorage writes 2025-11-07 00:04:08 +01:00
nessar 7ffa57748b Enhance storedSignal function to accept an Injector for improved context handling in localStorage persistence 2025-11-06 23:59:26 +01:00
Théophyl HoguetandCopilot 275512d443 Update src/app/services/calendar-settings-service.ts
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-06 23:58:42 +01:00
nessar 3eb35854d4 Update typescript-eslint to version 8.46.3 and enhance error handling in storedSignal function 2025-11-06 23:49:14 +01:00
nessar b793019ba7 Refactor calendar settings service to utilize a new storedSignal implementation for localStorage persistence and restore the stored-signal utility 2025-11-06 23:40:18 +01:00
Théophyl HoguetandCopilot d70aef4962 Update src/app/services/calendar-settings-service.ts
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-06 23:33:47 +01:00
nessar 22993a02c5 Refactor calendar settings service to use a definitions map for settings and update related components to access settings via a getter method 2025-11-06 22:22:04 +01:00
nessar 2a60fef8ff Merge remote-tracking branch 'origin/main' into THoguet/issue5 2025-11-06 21:25:23 +01:00
nessar 3fce4c03b5 enregistrer les calendar settings dans le localstorage
Fixes #5
2025-11-06 21:23:55 +01:00
9 changed files with 199 additions and 36 deletions

No files matched your search

+1 -1
View File
@@ -46,7 +46,7 @@
"karma-jasmine-html-reporter": "~2.1.0",
"prettier": "^3.6.2",
"typescript": "~5.9.2",
"typescript-eslint": "^8.19.1"
"typescript-eslint": "^8.46.3"
}
},
"node_modules/@acemir/cssom": {
+1 -1
View File
@@ -65,6 +65,6 @@
"karma-jasmine-html-reporter": "~2.1.0",
"prettier": "^3.6.2",
"typescript": "~5.9.2",
"typescript-eslint": "^8.19.1"
"typescript-eslint": "^8.46.3"
}
}
@@ -2,7 +2,7 @@
<h3>{{ getMonthName() }} {{ year }}</h3>
<div>
<div class="row header">
@if (calendarSettingsService.showWeekNumbers()) {
@if (calendarSettingsService.get('showWeekNumbers')()) {
<p class="day empty week-number"></p>
}
<p class="day">L</p>
@@ -16,7 +16,7 @@
<div class="cols">
@for (week of weeksInAMonth; track $index) {
<div class="row week">
@if (calendarSettingsService.showWeekNumbers()) {
@if (calendarSettingsService.get('showWeekNumbers')()) {
<p
class="day week-number"
[class.start]="$index === 0"
@@ -35,7 +35,7 @@ export class CalendarYear {
effect(() => {
this.selectedDates = this.calendarService.getSelectedDatesForYear();
const vacationData = this.userInput.vacationNumberSignal();
const c = this.calendarSettingsService.samediMalin();
const c = this.calendarSettingsService.get('samediMalin')();
console.log('Samedi Malin setting:', c);
console.log('Optimizing vacations with data:', vacationData);
this.isLoading.set(true);
@@ -4,15 +4,15 @@
<p>Configure your calendar settings here.</p>
</div>
<div style="display: flex; flex-direction: column; gap: 1rem">
@for (signal of calendarSettingsService.settings; track signal.id) {
@for (setting of calendarSettingsService.settingsArray; track setting.id) {
<div style="align-self: flex-start">
<mat-slide-toggle
[id]="signal.id"
[name]="signal.id"
[checked]="signal.signal()"
(change)="signal.signal.set($event.checked)"
[id]="setting.id"
[name]="setting.id"
[checked]="setting.signal()"
(change)="setting.signal.set($event.checked)"
>
{{ signal.description }}
{{ setting.description }}
</mat-slide-toggle>
</div>
}
+1 -1
View File
@@ -540,7 +540,7 @@ export class SelectedDates implements SelectedDateInterface {
calendarSettingsService: CalendarSettingsService,
calendarService: CalendarService,
): void {
if (calendarSettingsService.samediMalin()) this.samediMalin(vacationsNumber);
if (calendarSettingsService.get('samediMalin')()) this.samediMalin(vacationsNumber);
while (vacationsNumber.cp > 0) {
const result = this.lookForVacation(DayType.CP, calendarService);
if (result.heuristic === SelectedDates.NO_VALID_STRATEGY_HEURISTIC) {
@@ -17,17 +17,29 @@ describe('CalendarSettingsService', () => {
});
it('should initialize with default values', () => {
expect(service.showWeekNumbers()).toBe(false);
expect(service.samediMalin()).toBe(true);
expect(service.get('showWeekNumbers')()).toBe(false);
expect(service.get('samediMalin')()).toBe(true);
});
it('should allow updating showWeekNumbers', () => {
service.showWeekNumbers.set(true);
expect(service.showWeekNumbers()).toBe(true);
service.get('showWeekNumbers').set(true);
expect(service.get('showWeekNumbers')()).toBe(true);
});
it('should allow updating samediMalin', () => {
service.samediMalin.set(false);
expect(service.samediMalin()).toBe(false);
service.get('samediMalin').set(false);
expect(service.get('samediMalin')()).toBe(false);
});
it('should provide settings array for UI iteration', () => {
const settingsArray = service.settingsArray;
expect(settingsArray.length).toBe(2);
expect(settingsArray[0].id).toBe('showWeekNumbers');
expect(settingsArray[1].id).toBe('samediMalin');
});
it('should return correct descriptions', () => {
expect(service.getDescription('showWeekNumbers')).toBe('Afficher les numéros de semaine');
expect(service.getDescription('samediMalin')).toBe('Activer le Samedi Malin');
});
});
+58 -18
View File
@@ -1,8 +1,8 @@
import { Injectable, signal, WritableSignal } from '@angular/core';
import { Injectable, WritableSignal, inject, Injector } from '@angular/core';
import { storedSignal } from '../shared/utils/stored-signal';
interface CalendarSetting {
signal: WritableSignal<boolean>;
id: string;
interface SettingDefinition {
defaultValue: boolean;
description: string;
}
@@ -10,19 +10,59 @@ interface CalendarSetting {
providedIn: 'root',
})
export class CalendarSettingsService {
readonly showWeekNumbers = signal(false);
readonly samediMalin = signal(true);
private readonly injector = inject(Injector);
readonly settings: readonly CalendarSetting[] = [
{
signal: this.showWeekNumbers,
id: 'showWeekNumbers',
description: 'Afficher les numéros de semaine',
},
{
signal: this.samediMalin,
id: 'samediMalin',
description: 'Activer le Samedi Malin',
},
];
private readonly definitions = new Map<string, SettingDefinition>([
[
'showWeekNumbers',
{
defaultValue: false,
description: 'Afficher les numéros de semaine',
},
],
[
'samediMalin',
{
defaultValue: true,
description: 'Activer le Samedi Malin',
},
],
]);
private readonly signals = new Map<string, WritableSignal<boolean>>();
readonly settingsArray: ReadonlyArray<{
id: string;
signal: WritableSignal<boolean>;
description: string;
}>;
constructor() {
for (const [id, def] of this.definitions) {
const storageKey = `calendarSettings.${id}`;
this.signals.set(id, storedSignal(storageKey, def.defaultValue, this.injector));
}
this.settingsArray = Array.from(this.definitions.entries()).map(([id, def]) => ({
id,
signal: this.signals.get(id)!,
description: def.description,
}));
}
/**
* Returns the signal for the given setting ID.
* @throws Error if the setting ID doesn't exist
*/
get(id: string): WritableSignal<boolean> {
const signal = this.signals.get(id);
if (!signal) {
throw new Error(`Setting with id "${id}" does not exist`);
}
return signal;
}
getDescription(id: string): string {
return this.definitions.get(id)?.description ?? '';
}
}
+111
View File
@@ -0,0 +1,111 @@
import { signal, effect, WritableSignal, Injector, runInInjectionContext } from '@angular/core';
/**
* Checks if localStorage is available in the current environment.
* @returns true if localStorage is accessible, false otherwise
*/
function isLocalStorageAvailable(): boolean {
try {
return typeof localStorage !== 'undefined' && localStorage !== null;
} catch {
return false;
}
}
/**
* Creates a signal that automatically persists its value to localStorage.
*
* The signal will:
* - Initialize with the stored value from localStorage if available
* - Fall back to the provided initial value if no stored value exists or parsing fails
* - Automatically sync changes to localStorage with debouncing (300ms delay)
* - Work safely in SSR and environments where localStorage is unavailable
*
* @template T - The type of value stored in the signal (must be JSON-serializable)
* @param key - The localStorage key used to store the value
* @param initialValue - The default value used when no stored value exists
* @param injector - The Angular injector (use inject(Injector) to get current context)
* @param debounceMs - Optional debounce delay in milliseconds (default: 300ms)
* @returns A writable signal that persists to localStorage
*
* @example
* ```typescript
* const injector = inject(Injector);
* const darkMode = storedSignal('theme.darkMode', false, injector);
* darkMode.set(true); // Automatically saved to localStorage after 300ms
* console.log(darkMode()); // true
* ```
*
* @remarks
* - Requires an Injector to create the effect in the proper context
* - Values must be JSON-serializable (no functions, circular references, etc.)
* - In SSR or when localStorage is unavailable, the signal works normally but won't persist
* - Corrupted localStorage data is automatically cleared and reset to initial value
* - localStorage writes are debounced to improve performance with frequent updates
*/
export function storedSignal<T>(
key: string,
initialValue: T,
injector: Injector,
debounceMs = 300,
): WritableSignal<T> {
let parsedValue = initialValue;
if (isLocalStorageAvailable()) {
try {
const storedValue = localStorage.getItem(key);
if (storedValue) {
parsedValue = JSON.parse(storedValue) as T;
}
} catch (error) {
console.warn(
`Failed to parse stored value for key "${key}". Using initial value.`,
error,
);
try {
localStorage.removeItem(key);
} catch {
// Ignore removal errors
}
}
}
const s = signal(parsedValue);
let timeoutId: ReturnType<typeof setTimeout> | null = null;
let isFirstRun = true;
runInInjectionContext(injector, () => {
effect(() => {
const value = s();
// Skip the first execution to avoid writing back what we just read
if (isFirstRun) {
isFirstRun = false;
return;
}
// Clear any pending timeout
if (timeoutId !== null) {
clearTimeout(timeoutId);
}
// Debounce the localStorage write
timeoutId = setTimeout(() => {
if (isLocalStorageAvailable()) {
try {
localStorage.setItem(key, JSON.stringify(value));
} catch (error) {
console.warn(
`Failed to save value for key "${key}" to localStorage.`,
error,
);
}
}
timeoutId = null;
}, debounceMs);
});
});
return s;
}