libs/shared/src/upload/ held a network adapter, an Elm machine, and two application-layer coordinators outside the folder-per-layer convention every other context follows. The dependency-cruiser rule carved an exception around the misplaced adapter instead of the violation being fixed. Move all five files to the layer each belongs to (git mv), update every import across 24 consumer files, then delete the carve-out clause from .dependency-cruiser.base.js. No export renamed, no file split, no spec content changed. Deleting the carve-out exposed a second, pre-existing rule violation: ui-not-infrastructure had never fired against upload.adapter.ts because its old path did not match /infrastructure/. Three UI components injected UploadAdapter directly for its one-line contentUrl() wrapper. Route each through the existing pure uploadContentUrl() function via the application layer (upload-controller's new previewUrlFor, OrgTemplateStore's new previewUrlFor) instead — the same idiom brief.store.ts already used. npm run ci passes; dep:check is clean for both apps with the carve-out gone. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
131 lines
5.2 KiB
TypeScript
131 lines
5.2 KiB
TypeScript
import { Component, computed, effect, inject } from '@angular/core';
|
|
import { PageShellComponent } from '@shared/layout/page-shell/page-shell.component';
|
|
import { AlertComponent } from '@shared/ui/alert/alert.component';
|
|
import { ButtonComponent } from '@shared/ui/button/button.component';
|
|
import { ASYNC } from '@shared/ui/async/async.component';
|
|
import { AccessStore } from '@shared/application/access.store';
|
|
import { OrgTemplateStore } from '@brief/application/org-template.store';
|
|
import { OrgTemplateEditorComponent } from '@brief/ui/org-template-editor/org-template-editor.component';
|
|
|
|
/** Page: thin container for the admin org-template editor (WP-26). Deny-by-default
|
|
capability gate (`orgtemplate:edit`) — a denial alert for non-admins, the editor
|
|
for admins. Loads once the capability resolves; wires store commands to the organism. */
|
|
@Component({
|
|
selector: 'app-org-template-page',
|
|
imports: [
|
|
PageShellComponent,
|
|
AlertComponent,
|
|
ButtonComponent,
|
|
...ASYNC,
|
|
OrgTemplateEditorComponent,
|
|
],
|
|
styles: [
|
|
`
|
|
.save {
|
|
color: var(--rhc-color-foreground-subtle);
|
|
font-size: 0.9em;
|
|
}
|
|
`,
|
|
],
|
|
template: `
|
|
<app-page-shell [heading]="heading" [intro]="intro" backLink="/brief">
|
|
@if (store.lastError(); as err) {
|
|
<app-alert type="error">{{ err }}</app-alert>
|
|
}
|
|
|
|
@if (!access.ready()) {
|
|
<!-- wait for /me before deciding — avoids flashing the denial to an admin -->
|
|
} @else if (!canEdit()) {
|
|
<app-alert type="error">{{ deniedText }}</app-alert>
|
|
} @else {
|
|
<app-async [data]="store.remoteData()">
|
|
<ng-template appAsyncError>
|
|
<app-alert type="error">{{ failedText }}</app-alert>
|
|
<app-button variant="secondary" (click)="reload()">{{ retryText }}</app-button>
|
|
</ng-template>
|
|
<ng-template appAsyncLoaded>
|
|
@if (store.draft(); as draft) {
|
|
<app-org-template-editor
|
|
[draft]="draft"
|
|
[logoUrl]="store.logoUrl()"
|
|
[uploadState]="store.uploadState()"
|
|
[subOrgs]="store.subOrgs()"
|
|
[selectedSubOrgId]="store.selectedSubOrgId()"
|
|
[history]="store.history()"
|
|
[publishedVersion]="store.publishedVersion()"
|
|
[unsentBriefs]="store.unsentBriefs()"
|
|
[draftValid]="store.draftValid()"
|
|
[busy]="store.busy()"
|
|
[pendingPublish]="store.pendingPublish()"
|
|
[saveText]="saveText()"
|
|
[previewUrlFor]="previewUrlFor"
|
|
(selectSubOrg)="store.selectSubOrg($event)"
|
|
(templateEdit)="
|
|
store.edit({ tag: 'FieldEdited', field: $event.field, value: $event.value })
|
|
"
|
|
(marginEdit)="
|
|
store.edit({ tag: 'MarginEdited', edge: $event.edge, value: $event.value })
|
|
"
|
|
(logoSelected)="store.onLogoSelected($event)"
|
|
(logoRemoved)="store.onLogoRemoved($event)"
|
|
(logoRetry)="store.onLogoRetry($event)"
|
|
(requestPublish)="store.requestPublish()"
|
|
(confirmPublish)="store.confirmPublish()"
|
|
(cancelPublish)="store.cancelPublish()"
|
|
(rollback)="store.rollback($event)"
|
|
(proefbrief)="store.proefbrief()"
|
|
/>
|
|
}
|
|
</ng-template>
|
|
</app-async>
|
|
}
|
|
</app-page-shell>
|
|
`,
|
|
})
|
|
export class OrgTemplatePage {
|
|
protected store = inject(OrgTemplateStore);
|
|
protected access = inject(AccessStore);
|
|
|
|
protected canEdit = computed(() => this.access.can('orgtemplate:edit'));
|
|
protected previewUrlFor = this.store.previewUrlFor;
|
|
|
|
protected heading = $localize`:@@orgTemplate.page.heading:Huisstijl beheren`;
|
|
protected intro = $localize`:@@orgTemplate.page.intro:Beheer per organisatieonderdeel het uiterlijk van de brief: logo, afzender, ondertekening, voettekst en marges.`;
|
|
protected deniedText = $localize`:@@orgTemplate.page.denied:U hebt geen rechten om organisatiesjablonen te beheren.`;
|
|
protected failedText = $localize`:@@orgTemplate.page.failed:Het sjabloon kon niet worden geladen.`;
|
|
protected retryText = $localize`:@@orgTemplate.page.retry:Opnieuw proberen`;
|
|
|
|
private savingText = $localize`:@@orgTemplate.page.saving:Concept opslaan…`;
|
|
private savedText = $localize`:@@orgTemplate.page.saved:Concept opgeslagen`;
|
|
private saveErrorText = $localize`:@@orgTemplate.page.saveError:Opslaan mislukt`;
|
|
protected saveText = computed(() => {
|
|
switch (this.store.saveState().tag) {
|
|
case 'Saving':
|
|
return this.savingText;
|
|
case 'Saved':
|
|
return this.savedText;
|
|
case 'Error':
|
|
return this.saveErrorText;
|
|
default:
|
|
return '';
|
|
}
|
|
});
|
|
|
|
private loadRequested = false;
|
|
constructor() {
|
|
// Load once the capability resolves to `allowed` (a 403 GET would be wasted
|
|
// otherwise). Depends only on `canEdit()` + a plain flag — never on the store
|
|
// model, so dispatching `Loading` inside `load()` can't retrigger this effect.
|
|
effect(() => {
|
|
if (this.canEdit() && !this.loadRequested) {
|
|
this.loadRequested = true;
|
|
void this.store.load();
|
|
}
|
|
});
|
|
}
|
|
|
|
protected reload() {
|
|
void this.store.load();
|
|
}
|
|
}
|