Description
Dialog to visually edit the conditions restricting one permission
(e.g. "user_app can read Children only where center is X").
Closes with the new conditions object,
null to remove all conditions or undefined when cancelled.
Example
Methods
|
onConditionsChange
|
onConditionsChange(conditions: any)
|
|
|
Parameters :
| Name |
Type |
Optional |
| conditions |
any
|
No
|
|
|
removeConditions
|
removeConditions()
|
|
|
|
|
|
Readonly
combinator
|
Type : unknown
|
Default value : signal<"any" | "all">(
Array.isArray(this.data.conditions?.$or) ? "any" : "all",
)
|
|
|
whether all rows must match ("all", mongo implicit and) or any row ("any", $or)
|
|
Readonly
combinatorHint
|
Type : unknown
|
Default value : computed(() =>
this.combinator() === "any"
? $localize`Records match if any one of the conditions applies ("or" conditions).`
: $localize`Records match only if all conditions apply ("and" conditions).`,
)
|
|
|
|
Readonly
conditionSentence
|
Type : string
|
Default value : this.buildConditionSentence()
|
|
|
Full " can only where…" sentence as a single
localized message per action, so translators can reorder role/entity.
|
|
editorConditions
|
Type : any
|
Default value : toEditorFormat(this.data.conditions)
|
|
|
Working state in the { $or: [...] } row format of the conditions editor.
Deliberately not a signal: the conditions editor mutates this object in place
and the template does not need to react to its changes.
|
|
Readonly
entityConstructor
|
Type : EntityConstructor | undefined
|
Default value : this.entityRegistry.has(this.data.subject)
? this.entityRegistry.get(this.data.subject)
: undefined
|
|
|
|
Readonly
entityLabel
|
Type : string
|
Default value : this.entityConstructor?.label ?? this.data.subject
|
|
|
|
Readonly
hadConditions
|
Type : unknown
|
Default value : !!this.data.conditions
|
|
|
import {
ChangeDetectionStrategy,
Component,
computed,
inject,
signal,
} from "@angular/core";
import { MatButtonModule } from "@angular/material/button";
import { MatButtonToggleModule } from "@angular/material/button-toggle";
import {
MAT_DIALOG_DATA,
MatDialogModule,
MatDialogRef,
} from "@angular/material/dialog";
import { MatTooltipModule } from "@angular/material/tooltip";
import { ConditionsEditorComponent } from "../../../common-components/conditions-editor/conditions-editor.component";
import { DialogCloseComponent } from "../../../common-components/dialog-close/dialog-close.component";
import { EntityRegistry } from "../../../entity/database-entity.decorator";
import { EntityConstructor } from "../../../entity/model/entity";
import { EntityActionPermission } from "../../../permissions/permission-types";
import { roleDisplayName } from "../../../permissions/reserved-roles";
export interface PermissionConditionDialogData {
roleName: string;
action: EntityActionPermission;
subject: string;
conditions?: any;
}
/**
* Dialog to visually edit the conditions restricting one permission
* (e.g. "user_app can read Children only where center is X").
*
* Closes with the new conditions object,
* null to remove all conditions or undefined when cancelled.
*/
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: "app-permission-condition-dialog",
imports: [
MatDialogModule,
MatButtonModule,
MatButtonToggleModule,
MatTooltipModule,
ConditionsEditorComponent,
DialogCloseComponent,
],
templateUrl: "./permission-condition-dialog.component.html",
})
export class PermissionConditionDialogComponent {
private readonly dialogRef = inject(
MatDialogRef<PermissionConditionDialogComponent>,
);
private readonly entityRegistry = inject(EntityRegistry);
readonly data: PermissionConditionDialogData = inject(MAT_DIALOG_DATA);
readonly entityConstructor: EntityConstructor | undefined =
this.entityRegistry.has(this.data.subject)
? this.entityRegistry.get(this.data.subject)
: undefined;
/** whether all rows must match ("all", mongo implicit and) or any row ("any", $or) */
readonly combinator = signal<"any" | "all">(
Array.isArray(this.data.conditions?.$or) ? "any" : "all",
);
readonly hadConditions = !!this.data.conditions;
/**
* Working state in the { $or: [...] } row format of the conditions editor.
* Deliberately not a signal: the conditions editor mutates this object in place
* and the template does not need to react to its changes.
*/
editorConditions: any = toEditorFormat(this.data.conditions);
readonly entityLabel: string =
this.entityConstructor?.label ?? this.data.subject;
readonly combinatorHint = computed(() =>
this.combinator() === "any"
? $localize`Records match if any one of the conditions applies ("or" conditions).`
: $localize`Records match only if all conditions apply ("and" conditions).`,
);
/**
* Full "<role> can <action> <entity> only where…" sentence as a single
* localized message per action, so translators can reorder role/entity.
*/
readonly conditionSentence: string = this.buildConditionSentence();
private buildConditionSentence(): string {
// reserved roles are referred to by their readable name (e.g. "Default")
const role = roleDisplayName(this.data.roleName);
const entity = this.entityLabel;
switch (this.data.action) {
case "read":
return $localize`:permission condition sentence:${role} can read ${entity} only where…`;
case "create":
return $localize`:permission condition sentence:${role} can create ${entity} only where…`;
case "update":
return $localize`:permission condition sentence:${role} can update ${entity} only where…`;
case "delete":
return $localize`:permission condition sentence:${role} can delete ${entity} only where…`;
case "manage":
return $localize`:permission condition sentence:${role} can manage ${entity} only where…`;
}
}
onConditionsChange(conditions: any) {
this.editorConditions = conditions;
}
apply() {
const rows: any[] = (this.editorConditions?.$or ?? []).filter(
(row: any) =>
row &&
typeof row === "object" &&
Object.keys(row).length > 0 &&
Object.values(row).every((v) => v !== null && v !== undefined),
);
if (rows.length === 0) {
this.dialogRef.close(null);
} else if (this.combinator() === "any") {
this.dialogRef.close({ $or: rows });
} else {
this.dialogRef.close(mergeToAllConditions(rows));
}
}
removeConditions() {
this.dialogRef.close(null);
}
cancel() {
this.dialogRef.close(undefined);
}
}
/**
* Convert any stored conditions shape into the { $or: [...] } row format
* that the conditions editor works with.
*
* Deep-copies the input so the editor (which mutates rows in place) cannot
* touch the matrix model: cancelling the dialog must leave the model untouched.
*/
function toEditorFormat(conditions: any): any {
if (!conditions || typeof conditions !== "object") {
return {};
}
const copy = structuredClone(conditions);
if (Array.isArray(copy.$or)) {
return { $or: copy.$or };
}
if (Array.isArray(copy.$and)) {
return { $or: copy.$and };
}
// merged plain object: one row per key
return {
$or: Object.entries(copy).map(([key, value]) => ({ [key]: value })),
};
}
/**
* Combine rows into "all must match" conditions:
* a single merged object if keys are unique, otherwise an explicit $and.
*/
function mergeToAllConditions(rows: any[]): any {
const keys = rows.flatMap((row) => Object.keys(row));
if (new Set(keys).size === keys.length) {
return Object.assign({}, ...rows);
}
return { $and: rows };
}
<h2 mat-dialog-title>
@if (hadConditions) {
<span i18n>Edit Condition</span>
} @else {
<span i18n>Add Condition</span>
}
<app-dialog-close mat-dialog-close></app-dialog-close>
</h2>
<mat-dialog-content>
<p>{{ conditionSentence }}</p>
<!-- the combinator comes before the conditions it applies to, so the dialog
reads as one sentence: "<role> can read <type> only where …
[Any|All] of the following conditions match: …" -->
<div class="flex-row align-center gap-regular margin-bottom-regular">
<mat-button-toggle-group
[value]="combinator()"
(change)="combinator.set($event.value)"
hideSingleSelectionIndicator
>
<mat-button-toggle
value="any"
matTooltip='A record matches as soon as one of the conditions below applies ("or").'
i18n-matTooltip
i18n
>
Any
</mat-button-toggle>
<mat-button-toggle
value="all"
matTooltip='A record matches only if every one of the conditions below applies ("and").'
i18n-matTooltip
i18n
>
All
</mat-button-toggle>
</mat-button-toggle-group>
<span i18n>of the following conditions match</span>
</div>
<app-conditions-editor
[conditions]="editorConditions"
[entityConstructor]="entityConstructor"
[hint]="combinatorHint()"
[showInternalIdField]="true"
(conditionsChange)="onConditionsChange($event)"
></app-conditions-editor>
</mat-dialog-content>
<mat-dialog-actions align="end">
@if (hadConditions) {
<button mat-button color="warn" (click)="removeConditions()" i18n>
Remove condition
</button>
}
<button mat-stroked-button (click)="cancel()" i18n>Cancel</button>
<button mat-raised-button color="accent" (click)="apply()" i18n>
Apply Condition
</button>
</mat-dialog-actions>
Legend
Html element with directive