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
729cc3f4cf Refactor calendar settings dialog to use dynamic settings from service (#30)
* Refactor calendar settings dialog to use dynamic settings from service

* Update src/app/components/dialog/calendar-settings-dialog/calendar-settings-dialog.html

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Add type definition for calendar settings array (#35)

* Initial plan

* Add type definition for calendar settings array

Co-authored-by: THoguet <61246905+THoguet@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: THoguet <61246905+THoguet@users.noreply.github.com>

* use inject

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
2025-11-06 19:54:43 +01:00
dependabot[bot] f2b4d36709 Bump @types/node from 20.19.24 to 24.10.0 (#27)
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 20.19.24 to 24.10.0.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 24.10.0
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-04 00:22:44 +01:00
dependabot[bot] 104bdb273d Bump jasmine-core in the development-dependencies group (#26)
Bumps the development-dependencies group with 1 update: [jasmine-core](https://github.com/jasmine/jasmine).


Updates `jasmine-core` from 5.9.0 to 5.12.1
- [Release notes](https://github.com/jasmine/jasmine/releases)
- [Changelog](https://github.com/jasmine/jasmine/blob/main/RELEASE.md)
- [Commits](https://github.com/jasmine/jasmine/compare/v5.9.0...v5.12.1)

---
updated-dependencies:
- dependency-name: jasmine-core
  dependency-version: 5.12.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-04 00:22:33 +01:00
Théophyl Hoguet 000f37ec56 Change Trivy image reference in workflow 2025-11-04 00:19:01 +01:00
Théophyl Hoguet a2b5329716 Update image reference for Trivy vulnerability scanner 2025-11-04 00:16:37 +01:00
Théophyl Hoguet cef670955a Update image reference for Trivy vulnerability scan 2025-11-04 00:12:19 +01:00
Théophyl Hoguet 2027b412b3 Update image reference for Trivy scanner step 2025-11-04 00:09:34 +01:00
Théophyl Hoguet 6556dd7cf2 Update image reference for Trivy vulnerability scan 2025-11-04 00:02:57 +01:00
CopilotandTHoguet f7bd101fb7 Modernize CI/CD pipeline with comprehensive automation (#25)
* Initial plan

* Add comprehensive CI/CD workflows and tooling

- Add PR validation workflow with lint, format, type check, test, and build steps
- Improve Docker build workflow with caching, security scanning, and manual trigger
- Add CodeQL security analysis workflow
- Add Dependabot configuration for automated dependency updates
- Configure ESLint with TypeScript support
- Add npm scripts for linting and formatting
- Format code with Prettier
- Fix linting issues in source code

Co-authored-by: THoguet <61246905+THoguet@users.noreply.github.com>

* Update README and fix build configuration

- Add CI/CD documentation to README
- Document code quality tools and scripts
- Disable font optimization in production build to avoid network dependency
- Add build cache configuration for GitHub Actions

Co-authored-by: THoguet <61246905+THoguet@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: THoguet <61246905+THoguet@users.noreply.github.com>
2025-11-03 21:09:09 +01:00
11 changed files with 228 additions and 55 deletions

No files matched your search

+2 -1
View File
@@ -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'
+13 -13
View File
@@ -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
View File
@@ -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();
+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');
});
});
+62 -3
View File
@@ -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 ?? '';
}
}
+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;
}