Manage user space users #15
Some checks failed
Test API before pr merge / test-lint (pull_request) Successful in 11s
Test before pr merge / test-translation-lint (pull_request) Successful in 40s
Test before pr merge / test-lint (pull_request) Failing after 44s
Test before pr merge / test-before-merge (pull_request) Failing after 1m35s
Some checks failed
Test API before pr merge / test-lint (pull_request) Successful in 11s
Test before pr merge / test-translation-lint (pull_request) Successful in 40s
Test before pr merge / test-lint (pull_request) Failing after 44s
Test before pr merge / test-before-merge (pull_request) Failing after 1m35s
This commit is contained in:
parent
bcd79b18ab
commit
4c0cc7faed
@ -60,7 +60,7 @@ class MutationFieldBuilder(FieldBuilderABC):
|
||||
|
||||
def with_input(
|
||||
self,
|
||||
input_type: Type[Union[InputABC, str, int, bool]],
|
||||
input_type: Type[Union[InputABC, str, int, bool, list]],
|
||||
input_key: str = "input",
|
||||
) -> Self:
|
||||
self._input_type = input_type
|
||||
|
@ -74,6 +74,8 @@ type UserSpaceMutation {
|
||||
update(input: UserSpaceUpdateInput!): UserSpace
|
||||
delete(id: Int!): Boolean
|
||||
restore(id: Int!): Boolean
|
||||
|
||||
inviteUsers(emails: [String!]!): Boolean
|
||||
}
|
||||
|
||||
input UserSpaceCreateInput {
|
||||
|
@ -3,6 +3,7 @@ from api_graphql.abc.mutation_abc import MutationABC
|
||||
from api_graphql.field.mutation_field_builder import MutationFieldBuilder
|
||||
from api_graphql.input.user_space_create_input import UserSpaceCreateInput
|
||||
from api_graphql.input.user_space_update_input import UserSpaceUpdateInput
|
||||
from api_graphql.service.query_context import QueryContext
|
||||
from core.logger import APILogger
|
||||
from core.string import first_to_lower
|
||||
from data.schemas.administration.user_dao import userDao
|
||||
@ -26,9 +27,11 @@ class UserSpaceMutation(MutationABC):
|
||||
.with_change_broadcast(
|
||||
f"{first_to_lower(self.name.replace("Mutation", ""))}Change"
|
||||
)
|
||||
.with_require_any_permission([Permissions.user_spaces_create])
|
||||
)
|
||||
|
||||
async def _xzy(ctx: QueryContext):
|
||||
pass
|
||||
|
||||
self.field(
|
||||
MutationFieldBuilder("update")
|
||||
.with_resolver(self.resolve_update)
|
||||
@ -36,7 +39,7 @@ class UserSpaceMutation(MutationABC):
|
||||
.with_change_broadcast(
|
||||
f"{first_to_lower(self.name.replace("Mutation", ""))}Change"
|
||||
)
|
||||
.with_require_any_permission([Permissions.user_spaces_update])
|
||||
.with_require_any([Permissions.user_spaces_update], [_xzy])
|
||||
)
|
||||
|
||||
self.field(
|
||||
@ -45,7 +48,7 @@ class UserSpaceMutation(MutationABC):
|
||||
.with_change_broadcast(
|
||||
f"{first_to_lower(self.name.replace("Mutation", ""))}Change"
|
||||
)
|
||||
.with_require_any_permission([Permissions.user_spaces_delete])
|
||||
.with_require_any([Permissions.user_spaces_delete], [_xzy])
|
||||
)
|
||||
|
||||
self.field(
|
||||
@ -57,6 +60,18 @@ class UserSpaceMutation(MutationABC):
|
||||
.with_require_any_permission([Permissions.user_spaces_delete])
|
||||
)
|
||||
|
||||
self.field(
|
||||
MutationFieldBuilder("inviteUsers")
|
||||
.with_resolver(self.resolve_invite_users)
|
||||
.with_input(list, "emails")
|
||||
.with_change_broadcast(
|
||||
f"{first_to_lower(self.name.replace("Mutation", ""))}Change"
|
||||
)
|
||||
.with_require_any(
|
||||
[Permissions.user_spaces_create, Permissions.user_spaces_update], [_xzy]
|
||||
)
|
||||
)
|
||||
|
||||
async def resolve_create(self, obj: UserSpaceCreateInput, *_):
|
||||
logger.debug(f"create user_space: {obj.__dict__}")
|
||||
|
||||
@ -88,9 +103,7 @@ class UserSpaceMutation(MutationABC):
|
||||
|
||||
user_space = await userSpaceDao.get_by_id(obj.id)
|
||||
if obj.name is not None:
|
||||
already_exists = await userSpaceDao.find_by(
|
||||
{UserSpace.name: obj.name, UserSpace.id: {"ne": obj.id}}
|
||||
)
|
||||
already_exists = await userSpaceDao.find_by({UserSpace.name: obj.name})
|
||||
if len(already_exists) > 0:
|
||||
raise ValueError(f"UserSpace {obj.name} already exists")
|
||||
|
||||
@ -123,3 +136,6 @@ class UserSpaceMutation(MutationABC):
|
||||
user_space = await userSpaceDao.get_by_id(id)
|
||||
await userSpaceDao.restore(user_space)
|
||||
return True
|
||||
|
||||
async def resolve_invite_users(self, emails: list[str], *_):
|
||||
pass
|
||||
|
@ -17,5 +17,5 @@ class UserSpaceQuery(DbModelQueryABC):
|
||||
self.set_history_reference_dao(shortUrlDao, "userspaceid")
|
||||
|
||||
@staticmethod
|
||||
async def _get_users(group: UserSpace, *_):
|
||||
return await userSpaceDao.get(group.id)
|
||||
async def _get_users(space: UserSpace, *_):
|
||||
return await userSpaceDao.get_users(space.id)
|
||||
|
@ -10,8 +10,8 @@ class UserSpaceUserDao(DbModelDaoABC[UserSpaceUser]):
|
||||
DbModelDaoABC.__init__(
|
||||
self, __name__, UserSpaceUser, "public.user_spaces_users"
|
||||
)
|
||||
self.attribute(UserSpaceUser.user_space_id, str)
|
||||
self.attribute(UserSpaceUser.user_id, str)
|
||||
self.attribute(UserSpaceUser.user_space_id, int)
|
||||
self.attribute(UserSpaceUser.user_id, int)
|
||||
|
||||
async def get_by_user_space_id(
|
||||
self, user_space_id: str, with_deleted=False
|
||||
|
@ -78,6 +78,7 @@ class DataPrivacyService:
|
||||
|
||||
# Anonymize internal data
|
||||
await user.anonymize()
|
||||
await userDao.delete(user)
|
||||
|
||||
# Anonymize external data
|
||||
try:
|
||||
|
@ -20,6 +20,7 @@ export const tokenInterceptor: HttpInterceptorFn = (req, next) => {
|
||||
return next(req);
|
||||
}
|
||||
|
||||
const auth = inject(AuthService);
|
||||
return from(keycloak.getToken()).pipe(
|
||||
switchMap(token => {
|
||||
if (!token) {
|
||||
@ -46,7 +47,6 @@ export const tokenInterceptor: HttpInterceptorFn = (req, next) => {
|
||||
);
|
||||
}),
|
||||
catchError(() => {
|
||||
const auth = inject(AuthService);
|
||||
auth.logout().then();
|
||||
return next(req);
|
||||
})
|
||||
|
@ -27,5 +27,23 @@
|
||||
type="text"
|
||||
formControlName="name"/>
|
||||
</div>
|
||||
<div class="divider"></div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<p class="font-bold label">{{ 'user_space.assign_users' | translate }}</p>
|
||||
<div *ngIf="node.users && node.users.length > 0" class="flex flex-wrap gap-1">
|
||||
<div *ngFor="let user of node.users">
|
||||
<p-chip [label]="user.username" [removable]="true" (onRemove)="node.users.splice(node.users.indexOf(user), 1)"></p-chip>
|
||||
</div>
|
||||
</div>
|
||||
<div *ngIf="node.users?.length === 0">
|
||||
<span>{{ 'table.no_entries_found' | translate }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<p class="font-bold label">{{ 'user_space.invite_users' | translate }}</p>
|
||||
<div>
|
||||
<app-chip-input formControlName="emailsToInvite" type="email" placeholder="{{'common.enter_emails' | translate}}"/>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
</app-form-page>
|
||||
|
@ -8,6 +8,8 @@ import {
|
||||
UserSpaceUpdateInput,
|
||||
} from 'src/app/model/entities/user-space';
|
||||
import { UserSpacesDataService } from 'src/app/modules/admin/user-spaces/user-spaces.data.service';
|
||||
import { User } from 'src/app/model/auth/user';
|
||||
import { catchError } from 'rxjs/operators';
|
||||
|
||||
@Component({
|
||||
selector: 'app-user-space-form-page',
|
||||
@ -45,6 +47,7 @@ export class UserSpaceFormPageComponent extends FormPageBase<
|
||||
name: new FormControl<string | undefined>(undefined, [
|
||||
Validators.required,
|
||||
]),
|
||||
emailsToInvite: new FormControl<string[]>([]),
|
||||
});
|
||||
this.form.controls['id'].disable();
|
||||
}
|
||||
@ -59,6 +62,7 @@ export class UserSpaceFormPageComponent extends FormPageBase<
|
||||
getCreateInput(): UserSpaceCreateInput {
|
||||
return {
|
||||
name: this.form.controls['name'].value ?? undefined,
|
||||
users: this.node.users?.map(x => x.id) ?? [],
|
||||
};
|
||||
}
|
||||
|
||||
@ -69,11 +73,36 @@ export class UserSpaceFormPageComponent extends FormPageBase<
|
||||
|
||||
return {
|
||||
id: this.form.controls['id'].value,
|
||||
name: this.form.controls['name'].value ?? undefined,
|
||||
name: this.form.controls['name'].pristine
|
||||
? undefined
|
||||
: this.form.controls['name'].value,
|
||||
users: this.node.users?.map(x => x.id) ?? [],
|
||||
};
|
||||
}
|
||||
|
||||
protected handleEMailInvitation() {
|
||||
const emailsToInvite = this.form.controls['emailsToInvite'].value;
|
||||
if (!(emailsToInvite && emailsToInvite.length > 0)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.dataService
|
||||
.inviteUsers(emailsToInvite)
|
||||
.pipe(
|
||||
catchError(err => {
|
||||
this.spinner.hide();
|
||||
this.toast.error('action.failed', 'user_space.invite_users');
|
||||
throw err;
|
||||
})
|
||||
)
|
||||
.subscribe(() => {
|
||||
this.spinner.hide();
|
||||
this.toast.success('user_space.invited_users');
|
||||
});
|
||||
}
|
||||
|
||||
create(object: UserSpaceCreateInput): void {
|
||||
this.handleEMailInvitation();
|
||||
this.dataService.create(object).subscribe(() => {
|
||||
this.spinner.hide();
|
||||
this.toast.success('action.created');
|
||||
@ -82,6 +111,7 @@ export class UserSpaceFormPageComponent extends FormPageBase<
|
||||
}
|
||||
|
||||
update(object: UserSpaceUpdateInput): void {
|
||||
this.handleEMailInvitation();
|
||||
this.dataService.update(object).subscribe(() => {
|
||||
this.spinner.hide();
|
||||
this.toast.success('action.created');
|
||||
|
@ -85,6 +85,11 @@ export class UserSpacesDataService
|
||||
id
|
||||
name
|
||||
|
||||
users {
|
||||
id
|
||||
username
|
||||
}
|
||||
|
||||
...DB_MODEL
|
||||
}
|
||||
}
|
||||
@ -276,6 +281,29 @@ export class UserSpacesDataService
|
||||
.pipe(map(result => result.data?.userSpace.restore ?? false));
|
||||
}
|
||||
|
||||
inviteUsers(emails: string[]): Observable<boolean> {
|
||||
return this.apollo
|
||||
.mutate<{ userSpace: { inviteUsers: boolean } }>({
|
||||
mutation: gql`
|
||||
mutation inviteUsers($emails: [String!]!) {
|
||||
userSpace {
|
||||
inviteUsers(emails: $emails)
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
emails,
|
||||
},
|
||||
})
|
||||
.pipe(
|
||||
catchError(err => {
|
||||
this.spinner.hide();
|
||||
throw err;
|
||||
})
|
||||
)
|
||||
.pipe(map(result => result.data?.userSpace.inviteUsers ?? false));
|
||||
}
|
||||
|
||||
static provide(): Provider[] {
|
||||
return [
|
||||
{
|
||||
|
@ -0,0 +1,15 @@
|
||||
<div class="flex flex-wrap items-center input rounded-md p-2.5">
|
||||
<div class="flex flex-wrap items-center flex-grow gap-1">
|
||||
<p-chip *ngFor="let chip of chips; let i = index" [label]="chip" [removable]="true" (onRemove)="onChange(chips)"/>
|
||||
|
||||
<input
|
||||
pInputText
|
||||
[type]="type"
|
||||
[(ngModel)]="inputValue"
|
||||
(keydown.enter)="addChip()"
|
||||
(blur)="onTouched()"
|
||||
class="no-border flex-grow border-0 p-0"
|
||||
[placeholder]="placeholder"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ChipInputComponent } from './chip-input.component';
|
||||
|
||||
describe('ChipInputComponent', () => {
|
||||
let component: ChipInputComponent;
|
||||
let fixture: ComponentFixture<ChipInputComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ChipInputComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(ChipInputComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,55 @@
|
||||
import { Component, Input, forwardRef } from '@angular/core';
|
||||
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
|
||||
|
||||
@Component({
|
||||
selector: 'app-chip-input',
|
||||
templateUrl: './chip-input.component.html',
|
||||
styleUrls: ['./chip-input.component.scss'],
|
||||
providers: [
|
||||
{
|
||||
provide: NG_VALUE_ACCESSOR,
|
||||
useExisting: forwardRef(() => ChipInputComponent),
|
||||
multi: true,
|
||||
},
|
||||
],
|
||||
})
|
||||
export class ChipInputComponent implements ControlValueAccessor {
|
||||
@Input() type: string = 'text';
|
||||
@Input() placeholder?: string;
|
||||
chips: string[] = [];
|
||||
inputValue: string = '';
|
||||
|
||||
protected onChange: (value: string[]) => void = () => {};
|
||||
protected onTouched: () => void = () => {};
|
||||
|
||||
writeValue(value: string[]): void {
|
||||
this.chips = value || [];
|
||||
}
|
||||
|
||||
registerOnChange(fn: (value: string[]) => void): void {
|
||||
this.onChange = fn;
|
||||
}
|
||||
|
||||
registerOnTouched(fn: () => void): void {
|
||||
this.onTouched = fn;
|
||||
}
|
||||
|
||||
addChip(): void {
|
||||
if (!this.inputValue.trim()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.type === 'number' && isNaN(Number(this.inputValue.trim()))) {
|
||||
return; // Invalid number, do not add
|
||||
}
|
||||
if (
|
||||
this.type === 'email' &&
|
||||
!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(this.inputValue.trim())
|
||||
) {
|
||||
return; // Invalid email, do not add
|
||||
}
|
||||
this.chips.push(this.inputValue.trim());
|
||||
this.inputValue = '';
|
||||
this.onChange(this.chips);
|
||||
}
|
||||
}
|
@ -72,6 +72,7 @@ import { getMainDefinition } from '@apollo/client/utilities';
|
||||
import { Kind, OperationTypeNode } from 'graphql/index';
|
||||
import { HistorySidebarComponent } from 'src/app/modules/shared/components/history/history-sidebar.component';
|
||||
import { SliderModule } from 'primeng/slider';
|
||||
import { ChipInputComponent } from './components/chip-input/chip-input.component';
|
||||
|
||||
const sharedModules = [
|
||||
StepsModule,
|
||||
@ -146,9 +147,9 @@ function debounce(func: (...args: unknown[]) => void, wait: number) {
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
declarations: [...sharedComponents],
|
||||
declarations: [...sharedComponents, ChipInputComponent],
|
||||
imports: [CommonModule, ...sharedModules],
|
||||
exports: [...sharedModules, ...sharedComponents],
|
||||
exports: [...sharedModules, ...sharedComponents, ChipInputComponent],
|
||||
providers: [
|
||||
provideHttpClient(withInterceptors([tokenInterceptor])),
|
||||
provideApollo(() => {
|
||||
|
@ -2,6 +2,7 @@
|
||||
"action": {
|
||||
"created": "Erstellt",
|
||||
"deleted": "Gelöscht",
|
||||
"failed": "Fehlgeschlagen",
|
||||
"restored": "Wiederhergestellt",
|
||||
"updated": "Geändert"
|
||||
},
|
||||
@ -28,6 +29,7 @@
|
||||
"domains": "Domains",
|
||||
"download": "Herunterladen",
|
||||
"editor": "Bearbeiter",
|
||||
"enter_emails": "E-Mails eintragen",
|
||||
"error": "Fehler",
|
||||
"group": "Gruppe",
|
||||
"groups": "Gruppen",
|
||||
@ -270,5 +272,10 @@
|
||||
"keycloak_id": "Keycloak Id",
|
||||
"user": "Benutzer",
|
||||
"username": "Benutzername"
|
||||
},
|
||||
"user_space": {
|
||||
"assign_users": "Benutzer",
|
||||
"invite_users": "Benutzer einladen",
|
||||
"invited_users": "Benutzer eingeladen"
|
||||
}
|
||||
}
|
@ -2,6 +2,7 @@
|
||||
"action": {
|
||||
"created": "Created",
|
||||
"deleted": "Deleted",
|
||||
"failed": "Failed",
|
||||
"restored": "Recovered",
|
||||
"updated": "Updated"
|
||||
},
|
||||
@ -28,6 +29,7 @@
|
||||
"domains": "Domains",
|
||||
"download": "Download",
|
||||
"editor": "Editor",
|
||||
"enter_emails": "Enter E-Mails",
|
||||
"error": "Fehler",
|
||||
"group": "Group",
|
||||
"groups": "Groups",
|
||||
@ -270,5 +272,10 @@
|
||||
"keycloak_id": "Keycloak Id",
|
||||
"user": "User",
|
||||
"username": "Username"
|
||||
},
|
||||
"user_space": {
|
||||
"assign_users": "Users",
|
||||
"invite_users": "Invite users",
|
||||
"invited_users": "Invited users"
|
||||
}
|
||||
}
|
@ -68,6 +68,11 @@ body {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.no-border {
|
||||
border: 0 !important;
|
||||
outline: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
header {
|
||||
|
@ -70,7 +70,7 @@
|
||||
color: $headerColor;
|
||||
}
|
||||
|
||||
input, .p-checkbox-box, .p-dropdown {
|
||||
input, .input, .p-checkbox-box, .p-dropdown {
|
||||
border: 1px solid $accentColor;
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user