fix: Improve Actions V2 Texts and reenable in settings (#9814)

# Which Problems Are Solved
This pr includes improved texts to make the usage of Actions V2 more
easy.
Since the removal of the Actions V2 Feature Flag we removed the code
that checks if it's enabled in the settings sidenav.

# How the Problems Are Solved
Added new texts to translations. Removed sidenav logic that checks for
Actions V2 Feature Flag

# Additional Context

- Part of #7248 
- Part of #9688

---------

Co-authored-by: Max Peintner <[email protected]>
Co-authored-by: Max Peintner <[email protected]>
This commit is contained in:
Ramon
2025-04-29 11:25:49 +00:00
committed by GitHub
co-authored by Max Peintner Max Peintner
parent ce823c9176
commit d930a09cb0
39 changed files with 249 additions and 189 deletions
@@ -56,9 +56,9 @@ export class ActionsTwoActionsTableComponent {
return executions.map((execution) => {
const mappedTargets = execution.targets.map((target) => {
const targetType = targetsMap.get(target.type.value);
const targetType = targetsMap.get(target);
if (!targetType) {
throw new Error(`Target with id ${target.type.value} not found`);
throw new Error(`Target with id ${target} not found`);
}
return targetType;
});
@@ -1,4 +1,7 @@
<h2>{{ 'ACTIONSTWO.EXECUTION.TITLE' | translate }}</h2>
<cnsl-info-section [type]="InfoSectionType.ALERT">
{{ 'ACTIONSTWO.BETA_NOTE' | translate }}
</cnsl-info-section>
<p class="cnsl-secondary-text">{{ 'ACTIONSTWO.EXECUTION.DESCRIPTION' | translate }}</p>
<cnsl-actions-two-actions-table
@@ -15,6 +15,7 @@ import { MatDialog } from '@angular/material/dialog';
import { MessageInitShape } from '@bufbuild/protobuf';
import { SetExecutionRequestSchema } from '@zitadel/proto/zitadel/action/v2beta/action_service_pb';
import { Target } from '@zitadel/proto/zitadel/action/v2beta/target_pb';
import { InfoSectionType } from '../../info-section/info-section.component';
@Component({
selector: 'cnsl-actions-two-actions',
@@ -110,4 +111,6 @@ export class ActionsTwoActionsComponent {
this.toast.showError(error);
}
}
protected readonly InfoSectionType = InfoSectionType;
}
@@ -10,12 +10,7 @@ import {
} from './actions-two-add-action-condition/actions-two-add-action-condition.component';
import { ActionsTwoAddActionTargetComponent } from './actions-two-add-action-target/actions-two-add-action-target.component';
import { CommonModule } from '@angular/common';
import {
Condition,
Execution,
ExecutionTargetType,
ExecutionTargetTypeSchema,
} from '@zitadel/proto/zitadel/action/v2beta/execution_pb';
import { Condition, Execution } from '@zitadel/proto/zitadel/action/v2beta/execution_pb';
import { Subject } from 'rxjs';
import { SetExecutionRequestSchema } from '@zitadel/proto/zitadel/action/v2beta/action_service_pb';
@@ -27,11 +22,8 @@ enum Page {
export type CorrectlyTypedCondition = Condition & { conditionType: Extract<Condition['conditionType'], { case: string }> };
type CorrectlyTypedTargets = { type: Extract<ExecutionTargetType['type'], { case: 'target' }> };
export type CorrectlyTypedExecution = Omit<Execution, 'targets' | 'condition'> & {
export type CorrectlyTypedExecution = Omit<Execution, 'condition'> & {
condition: CorrectlyTypedCondition;
targets: CorrectlyTypedTargets[];
};
export const correctlyTypeExecution = (execution: Execution): CorrectlyTypedExecution => {
@@ -48,9 +40,6 @@ export const correctlyTypeExecution = (execution: Execution): CorrectlyTypedExec
return {
...execution,
condition,
targets: execution.targets
.map(({ type }) => ({ type }))
.filter((target): target is CorrectlyTypedTargets => target.type.case === 'target'),
};
};
@@ -81,7 +70,7 @@ export class ActionTwoAddActionDialogComponent {
protected readonly typeSignal = signal<ConditionType>('request');
protected readonly conditionSignal = signal<MessageInitShape<typeof SetExecutionRequestSchema>['condition']>(undefined);
protected readonly targetsSignal = signal<MessageInitShape<typeof ExecutionTargetTypeSchema>[]>([]);
protected readonly targetsSignal = signal<string[]>([]);
protected readonly continueSubject = new Subject<void>();
@@ -112,7 +101,7 @@ export class ActionTwoAddActionDialogComponent {
this.targetsSignal.set(data.execution.targets);
this.typeSignal.set(data.execution.condition.conditionType.case);
this.conditionSignal.set(data.execution.condition);
this.preselectedTargetIds = data.execution.targets.map((target) => target.type.value);
this.preselectedTargetIds = data.execution.targets;
this.page.set(Page.Target); // Set the initial page based on the provided execution data
}
@@ -1,4 +1,4 @@
<form class="form-grid" [formGroup]="form()" (ngSubmit)="submit()">
<form *ngIf="form$ | async as form" class="form-grid" [formGroup]="form" (ngSubmit)="submit()">
<p class="target-description">{{ 'ACTIONSTWO.EXECUTION.DIALOG.TARGET.DESCRIPTION' | translate }}</p>
<cnsl-form-field class="full-width">
@@ -8,9 +8,9 @@
#trigger="matAutocompleteTrigger"
#input
type="text"
[formControl]="form().controls.autocomplete"
[formControl]="form.controls.autocomplete"
[matAutocomplete]="autoservice"
(keydown.enter)="handleEnter($event); input.blur(); trigger.closePanel()"
(keydown.enter)="handleEnter($event, form); input.blur(); trigger.closePanel()"
/>
<mat-autocomplete #autoservice="matAutocomplete">
<mat-option *ngIf="targets().state === 'loading'" class="is-loading">
@@ -19,7 +19,7 @@
<mat-option
*ngFor="let target of selectableTargets(); trackBy: trackTarget"
#option
(click)="addTarget(target); option.deselect()"
(click)="addTarget(target, form); option.deselect()"
[value]="target.name"
>
{{ target.name }}
@@ -27,7 +27,7 @@
</mat-autocomplete>
</cnsl-form-field>
<table mat-table cdkDropList (cdkDropListDropped)="drop($event)" [dataSource]="dataSource" [trackBy]="trackTarget">
<table mat-table cdkDropList (cdkDropListDropped)="drop($event, form)" [dataSource]="dataSource" [trackBy]="trackTarget">
<ng-container matColumnDef="order">
<th mat-header-cell *matHeaderCellDef>Reorder</th>
<td mat-cell *cnslCellDef="let row; let i = index; dataSource: dataSource">
@@ -48,7 +48,7 @@
actions
matTooltip="{{ 'ACTIONS.REMOVE' | translate }}"
color="warn"
(click)="removeTarget(i)"
(click)="removeTarget(i, form)"
mat-icon-button
>
<i class="las la-trash"></i>
@@ -65,7 +65,7 @@
{{ 'ACTIONS.BACK' | translate }}
</button>
<span class="fill-space"></span>
<button color="primary" [disabled]="form().invalid" mat-raised-button type="submit">
<button color="primary" [disabled]="form.invalid" mat-raised-button type="submit">
{{ 'ACTIONS.CONTINUE' | translate }}
</button>
</div>
@@ -14,7 +14,7 @@ import { RouterModule } from '@angular/router';
import { TranslateModule } from '@ngx-translate/core';
import { MatButtonModule } from '@angular/material/button';
import { FormBuilder, FormControl, FormsModule, ReactiveFormsModule } from '@angular/forms';
import { ReplaySubject, switchMap } from 'rxjs';
import { ObservedValueOf, ReplaySubject, shareReplay, switchMap } from 'rxjs';
import { MatRadioModule } from '@angular/material/radio';
import { ActionService } from 'src/app/services/action.service';
import { ToastService } from 'src/app/services/toast.service';
@@ -23,14 +23,13 @@ import { InputModule } from 'src/app/modules/input/input.module';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MessageInitShape } from '@bufbuild/protobuf';
import { Target } from '@zitadel/proto/zitadel/action/v2beta/target_pb';
import { ExecutionTargetTypeSchema } from '@zitadel/proto/zitadel/action/v2beta/execution_pb';
import { MatSelectModule } from '@angular/material/select';
import { ActionConditionPipeModule } from 'src/app/pipes/action-condition-pipe/action-condition-pipe.module';
import { MatTableDataSource, MatTableModule } from '@angular/material/table';
import { startWith } from 'rxjs/operators';
import { map, startWith } from 'rxjs/operators';
import { TypeSafeCellDefModule } from 'src/app/directives/type-safe-cell-def/type-safe-cell-def.module';
import { CdkDrag, CdkDragDrop, CdkDropList, moveItemInArray } from '@angular/cdk/drag-drop';
import { toObservable, toSignal } from '@angular/core/rxjs-interop';
import { toSignal } from '@angular/core/rxjs-interop';
import { minArrayLengthValidator } from '../../../form-field/validators/validators';
import { ProjectRoleChipModule } from '../../../project-role-chip/project-role-chip.module';
import { MatTooltipModule } from '@angular/material/tooltip';
@@ -72,11 +71,12 @@ export class ActionsTwoAddActionTargetComponent {
}
@Output() public readonly back = new EventEmitter<void>();
@Output() public readonly continue = new EventEmitter<MessageInitShape<typeof ExecutionTargetTypeSchema>[]>();
@Output() public readonly continue = new EventEmitter<string[]>();
private readonly preselectedTargetIds$ = new ReplaySubject<string[]>(1);
protected readonly form: ReturnType<typeof this.buildForm>;
protected readonly form$: ReturnType<typeof this.buildForm>;
protected readonly targets: ReturnType<typeof this.listTargets>;
private readonly selectedTargetIds: Signal<string[]>;
protected readonly selectableTargets: Signal<Target[]>;
@@ -87,26 +87,27 @@ export class ActionsTwoAddActionTargetComponent {
private readonly actionService: ActionService,
private readonly toast: ToastService,
) {
this.form = this.buildForm();
this.form$ = this.buildForm().pipe(shareReplay({ refCount: true, bufferSize: 1 }));
this.targets = this.listTargets();
this.selectedTargetIds = this.getSelectedTargetIds(this.form);
this.selectableTargets = this.getSelectableTargets(this.targets, this.selectedTargetIds);
this.selectedTargetIds = this.getSelectedTargetIds(this.form$);
this.selectableTargets = this.getSelectableTargets(this.targets, this.selectedTargetIds, this.form$);
this.dataSource = this.getDataSource(this.targets, this.selectedTargetIds);
}
private buildForm() {
const preselectedTargetIds = toSignal(this.preselectedTargetIds$, { initialValue: [] as string[] });
return computed(() => {
return this.fb.group({
autocomplete: new FormControl('', { nonNullable: true }),
selectedTargetIds: new FormControl(preselectedTargetIds(), {
nonNullable: true,
validators: [minArrayLengthValidator(1)],
}),
});
});
return this.preselectedTargetIds$.pipe(
startWith([] as string[]),
map((preselectedTargetIds) => {
return this.fb.group({
autocomplete: new FormControl('', { nonNullable: true }),
selectedTargetIds: new FormControl(preselectedTargetIds, {
nonNullable: true,
validators: [minArrayLengthValidator(1)],
}),
});
}),
);
}
private listTargets() {
@@ -129,25 +130,35 @@ export class ActionsTwoAddActionTargetComponent {
return computed(targetsSignal);
}
private getSelectedTargetIds(form: typeof this.form) {
const selectedTargetIds$ = toObservable(form).pipe(
startWith(form()),
switchMap((form) => {
const { selectedTargetIds } = form.controls;
private getSelectedTargetIds(form$: typeof this.form$) {
const selectedTargetIds$ = form$.pipe(
switchMap(({ controls: { selectedTargetIds } }) => {
return selectedTargetIds.valueChanges.pipe(startWith(selectedTargetIds.value));
}),
);
return toSignal(selectedTargetIds$, { requireSync: true });
}
private getSelectableTargets(targets: typeof this.targets, selectedTargetIds: Signal<string[]>) {
return computed(() => {
private getSelectableTargets(targets: typeof this.targets, selectedTargetIds: Signal<string[]>, form$: typeof this.form$) {
const autocomplete$ = form$.pipe(
switchMap(({ controls: { autocomplete } }) => {
return autocomplete.valueChanges.pipe(startWith(autocomplete.value));
}),
);
const autocompleteSignal = toSignal(autocomplete$, { requireSync: true });
const unselectedTargets = computed(() => {
const targetsCopy = new Map(targets().targets);
for (const selectedTargetId of selectedTargetIds()) {
targetsCopy.delete(selectedTargetId);
}
return Array.from(targetsCopy.values());
});
return computed(() => {
const autocomplete = autocompleteSignal().toLowerCase();
return unselectedTargets().filter(({ name }) => name.toLowerCase().includes(autocomplete));
});
}
private getDataSource(targetsSignal: typeof this.targets, selectedTargetIdsSignal: Signal<string[]>) {
@@ -178,46 +189,39 @@ export class ActionsTwoAddActionTargetComponent {
return dataSource;
}
protected addTarget(target: Target) {
const { selectedTargetIds } = this.form().controls;
protected addTarget(target: Target, form: ObservedValueOf<typeof this.form$>) {
const { selectedTargetIds } = form.controls;
selectedTargetIds.setValue([target.id, ...selectedTargetIds.value]);
this.form().controls.autocomplete.setValue('');
form.controls.autocomplete.setValue('');
}
protected removeTarget(index: number) {
const { selectedTargetIds } = this.form().controls;
protected removeTarget(index: number, form: ObservedValueOf<typeof this.form$>) {
const { selectedTargetIds } = form.controls;
const data = [...selectedTargetIds.value];
data.splice(index, 1);
selectedTargetIds.setValue(data);
}
protected drop(event: CdkDragDrop<undefined>) {
const { selectedTargetIds } = this.form().controls;
protected drop(event: CdkDragDrop<undefined>, form: ObservedValueOf<typeof this.form$>) {
const { selectedTargetIds } = form.controls;
const data = [...selectedTargetIds.value];
moveItemInArray(data, event.previousIndex, event.currentIndex);
selectedTargetIds.setValue(data);
}
protected handleEnter(event: Event) {
protected handleEnter(event: Event, form: ObservedValueOf<typeof this.form$>) {
const selectableTargets = this.selectableTargets();
if (selectableTargets.length !== 1) {
return;
}
event.preventDefault();
this.addTarget(selectableTargets[0]);
this.addTarget(selectableTargets[0], form);
}
protected submit() {
const selectedTargets = this.selectedTargetIds().map((value) => ({
type: {
case: 'target' as const,
value,
},
}));
this.continue.emit(selectedTargets);
this.continue.emit(this.selectedTargetIds());
}
protected trackTarget(_: number, target: Target) {
@@ -26,6 +26,9 @@
{{ 'ACTIONSTWO.TARGET.CREATE.TYPES.' + type | translate }}
</mat-option>
</mat-select>
<span class="name-hint cnsl-secondary-text types-description">
{{ 'ACTIONSTWO.TARGET.CREATE.TYPES_DESCRIPTION' | translate }}
</span>
</cnsl-form-field>
<cnsl-form-field class="full-width">
@@ -23,3 +23,7 @@
.name-hint {
font-size: 12px;
}
.types-description {
white-space: pre-line;
}
@@ -1,4 +1,7 @@
<h2>{{ 'ACTIONSTWO.TARGET.TITLE' | translate }}</h2>
<cnsl-info-section [type]="InfoSectionType.ALERT">
{{ 'ACTIONSTWO.BETA_NOTE' | translate }}
</cnsl-info-section>
<p class="cnsl-secondary-text">{{ 'ACTIONSTWO.TARGET.DESCRIPTION' | translate }}</p>
<cnsl-actions-two-targets-table
@@ -12,6 +12,7 @@ import {
CreateTargetRequestSchema,
UpdateTargetRequestSchema,
} from '@zitadel/proto/zitadel/action/v2beta/action_service_pb';
import { InfoSectionType } from '../../info-section/info-section.component';
@Component({
selector: 'cnsl-actions-two-targets',
@@ -76,7 +77,8 @@ export class ActionsTwoTargetsComponent {
if ('id' in request) {
await this.actionService.updateTarget(request);
} else {
await this.actionService.createTarget(request);
const resp = await this.actionService.createTarget(request);
console.log(`Your singing key: ${resp.signingKey}`);
}
await new Promise((res) => setTimeout(res, 1000));
@@ -86,4 +88,6 @@ export class ActionsTwoTargetsComponent {
this.toast.showError(error);
}
}
protected readonly InfoSectionType = InfoSectionType;
}
@@ -20,6 +20,7 @@ import { ProjectRoleChipModule } from '../project-role-chip/project-role-chip.mo
import { ActionConditionPipeModule } from 'src/app/pipes/action-condition-pipe/action-condition-pipe.module';
import { MatSelectModule } from '@angular/material/select';
import { MatIconModule } from '@angular/material/icon';
import { InfoSectionModule } from '../info-section/info-section.module';
@NgModule({
declarations: [
@@ -47,6 +48,7 @@ import { MatIconModule } from '@angular/material/icon';
TypeSafeCellDefModule,
ProjectRoleChipModule,
ActionConditionPipeModule,
InfoSectionModule,
],
exports: [ActionsTwoActionsComponent, ActionsTwoTargetsComponent, ActionsTwoTargetsTableComponent],
})
@@ -231,6 +231,7 @@ export const ACTIONS: SidenavSetting = {
// todo: figure out roles
[PolicyComponentServiceType.ADMIN]: ['iam.policy.read'],
},
beta: true,
};
export const ACTIONS_TARGETS: SidenavSetting = {
@@ -241,4 +242,5 @@ export const ACTIONS_TARGETS: SidenavSetting = {
// todo: figure out roles
[PolicyComponentServiceType.ADMIN]: ['iam.policy.read'],
},
beta: true,
};
@@ -28,6 +28,7 @@
[attr.data-e2e]="'sidenav-element-' + setting.id"
>
<span>{{ setting.i18nKey | translate }}</span>
<span class="state" *ngIf="setting?.beta">{{ 'SETTINGS.BETA' | translate }}</span>
<mat-icon *ngIf="setting.showWarn" class="warn-icon" svgIcon="mdi_shield_alert"></mat-icon>
</button>
</ng-container>
@@ -90,6 +90,10 @@
flex-shrink: 0;
}
.state {
margin-left: 0.5rem;
}
&:hover {
span {
opacity: 1;
@@ -11,6 +11,7 @@ export interface SidenavSetting {
[PolicyComponentServiceType.ADMIN]?: string[];
};
showWarn?: boolean;
beta?: boolean;
}
@Component({