Compare commits
20
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ed6b18b2a3 | ||
|
|
da31c9c753 | ||
|
|
bda3db2aa8 | ||
|
|
7ffa57748b | ||
|
|
275512d443 | ||
|
|
3eb35854d4 | ||
|
|
b793019ba7 | ||
|
|
d70aef4962 | ||
|
|
22993a02c5 | ||
|
|
2a60fef8ff | ||
|
|
3fce4c03b5 | ||
|
|
729cc3f4cf | ||
|
|
f2b4d36709 | ||
|
|
104bdb273d | ||
|
|
000f37ec56 | ||
|
|
a2b5329716 | ||
|
|
cef670955a | ||
|
|
2027b412b3 | ||
|
|
6556dd7cf2 | ||
|
|
f7bd101fb7 |
No files matched your search
@@ -41,6 +41,7 @@ jobs:
|
||||
type=raw,value=latest,enable={{is_default_branch}}
|
||||
|
||||
- name: Build and push
|
||||
id: build
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
@@ -53,7 +54,7 @@ jobs:
|
||||
- name: Run Trivy vulnerability scanner
|
||||
uses: aquasecurity/trivy-action@0.27.0
|
||||
with:
|
||||
image-ref: ghcr.io/${{ github.repository }}:sha-${{ github.sha }}
|
||||
image-ref: ghcr.io/thoguet/vac-optimizer@${{ steps.build.outputs.digest }}
|
||||
format: 'sarif'
|
||||
output: 'trivy-results.sarif'
|
||||
|
||||
|
||||
Generated
+13
-13
@@ -32,11 +32,11 @@
|
||||
"@types/express": "^5.0.1",
|
||||
"@types/jasmine": "~5.1.0",
|
||||
"@types/jest": "^30.0.0",
|
||||
"@types/node": "^20.17.19",
|
||||
"@types/node": "^24.10.0",
|
||||
"eslint": "^9.38.0",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-prettier": "^5.5.4",
|
||||
"jasmine-core": "~5.9.0",
|
||||
"jasmine-core": "~5.12.1",
|
||||
"jest": "^30.2.0",
|
||||
"jest-preset-angular": "^15.0.3",
|
||||
"karma": "~6.4.0",
|
||||
@@ -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": {
|
||||
@@ -5076,13 +5076,13 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "20.19.24",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.24.tgz",
|
||||
"integrity": "sha512-FE5u0ezmi6y9OZEzlJfg37mqqf6ZDSF2V/NLjUyGrR9uTZ7Sb9F7bLNZ03S4XVUNRWGA7Ck4c1kK+YnuWjl+DA==",
|
||||
"version": "24.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.0.tgz",
|
||||
"integrity": "sha512-qzQZRBqkFsYyaSWXuEHc2WR9c0a0CXwiE5FWUvn7ZM+vdy1uZLfCunD38UzhuB7YN/J11ndbDBcTmOdxJo9Q7A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.21.0"
|
||||
"undici-types": "~7.16.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/qs": {
|
||||
@@ -9150,9 +9150,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/jasmine-core": {
|
||||
"version": "5.9.0",
|
||||
"resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-5.9.0.tgz",
|
||||
"integrity": "sha512-OMUvF1iI6+gSRYOhMrH4QYothVLN9C3EJ6wm4g7zLJlnaTl8zbaPOr0bTw70l7QxkoM7sVFOWo83u9B2Fe2Zng==",
|
||||
"version": "5.12.1",
|
||||
"resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-5.12.1.tgz",
|
||||
"integrity": "sha512-P/UbRZ0LKwXe7wEpwDheuhunPwITn4oPALhrJEQJo6756EwNGnsK/TSQrWojBB4cQDQ+VaxWYws9tFNDuiMh2Q==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
@@ -14349,9 +14349,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||
"version": "7.16.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
|
||||
"integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
|
||||
+3
-3
@@ -51,11 +51,11 @@
|
||||
"@types/express": "^5.0.1",
|
||||
"@types/jasmine": "~5.1.0",
|
||||
"@types/jest": "^30.0.0",
|
||||
"@types/node": "^20.17.19",
|
||||
"@types/node": "^24.10.0",
|
||||
"eslint": "^9.38.0",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-prettier": "^5.5.4",
|
||||
"jasmine-core": "~5.9.0",
|
||||
"jasmine-core": "~5.12.1",
|
||||
"jest": "^30.2.0",
|
||||
"jest-preset-angular": "^15.0.3",
|
||||
"karma": "~6.4.0",
|
||||
@@ -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,26 +4,18 @@
|
||||
<p>Configure your calendar settings here.</p>
|
||||
</div>
|
||||
<div style="display: flex; flex-direction: column; gap: 1rem">
|
||||
<div style="align-self: flex-start">
|
||||
<mat-slide-toggle
|
||||
id="showWeekNumbers"
|
||||
name="showWeekNumbers"
|
||||
[checked]="calendarSettingsService.showWeekNumbers()"
|
||||
(change)="calendarSettingsService.showWeekNumbers.set($event.checked)"
|
||||
>
|
||||
Afficher les numéros de semaine
|
||||
</mat-slide-toggle>
|
||||
</div>
|
||||
<div style="align-self: flex-start">
|
||||
<mat-slide-toggle
|
||||
id="samediMalin"
|
||||
name="samediMalin"
|
||||
[checked]="calendarSettingsService.samediMalin()"
|
||||
(change)="calendarSettingsService.samediMalin.set($event.checked)"
|
||||
>
|
||||
Activer le Samedi Malin
|
||||
</mat-slide-toggle>
|
||||
</div>
|
||||
@for (setting of calendarSettingsService.settingsArray; track setting.id) {
|
||||
<div style="align-self: flex-start">
|
||||
<mat-slide-toggle
|
||||
[id]="setting.id"
|
||||
[name]="setting.id"
|
||||
[checked]="setting.signal()"
|
||||
(change)="setting.signal.set($event.checked)"
|
||||
>
|
||||
{{ setting.description }}
|
||||
</mat-slide-toggle>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<button mat-button (click)="closeDialog()">Fermer</button>
|
||||
</div>
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { Component, inject } from '@angular/core';
|
||||
import { MatButton } from '@angular/material/button';
|
||||
import { MatDialogRef } from '@angular/material/dialog';
|
||||
import { CalendarSettingsService } from '../../../services/calendar-settings-service';
|
||||
@@ -11,10 +11,8 @@ import { MatSlideToggle } from '@angular/material/slide-toggle';
|
||||
styleUrl: './calendar-settings-dialog.scss',
|
||||
})
|
||||
export class CalendarSettingsDialog {
|
||||
constructor(
|
||||
public dialogRef: MatDialogRef<CalendarSettingsDialog>,
|
||||
protected calendarSettingsService: CalendarSettingsService,
|
||||
) {}
|
||||
public readonly dialogRef = inject<MatDialogRef<CalendarSettingsDialog>>(MatDialogRef);
|
||||
protected readonly calendarSettingsService = inject(CalendarSettingsService);
|
||||
|
||||
closeDialog() {
|
||||
this.dialogRef.close();
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -1,9 +1,68 @@
|
||||
import { Injectable, signal } from '@angular/core';
|
||||
import { Injectable, WritableSignal, inject, Injector } from '@angular/core';
|
||||
import { storedSignal } from '../shared/utils/stored-signal';
|
||||
|
||||
interface SettingDefinition {
|
||||
defaultValue: boolean;
|
||||
description: string;
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class CalendarSettingsService {
|
||||
readonly showWeekNumbers = signal(false);
|
||||
readonly samediMalin = signal(true);
|
||||
private readonly injector = inject(Injector);
|
||||
|
||||
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 ?? '';
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
Reference in new issue
Block a user