|
conditions
|
conditions: any
|
Type : any
|
|
Optional
|
|
|
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 };
}