diff --git a/src/app/components/actionbar/actionbar.component.html b/src/app/components/actionbar/actionbar.component.html index 3a692b17..b4c42b8e 100644 --- a/src/app/components/actionbar/actionbar.component.html +++ b/src/app/components/actionbar/actionbar.component.html @@ -1,16 +1,13 @@ @if (element(); as element) { - @if (isAuthenticated() && isEditingAllowed() && isEntity() && isDetailPage()) { + @if (isAuthenticated() && isEditingAllowed() && isDetailPage()) {
@if (isPublished()) {
- @if (metadataDownload(); as metadataDownload) { - - - - } -
-
- - - @if (isEntity()) { - @if (isDownloadable()) { -
- -
+ } + @if (metadataDownload(); as metadataDownload) { + + + + } + @if (element(); as element) { + - + {{ 'Embed collection' | translate }} } - } + } -
+ +
+ +
+ @for (compilation of selectHistory.usedInCompilations.compilations; track compilation._id) { @@ -202,16 +203,27 @@ @if (element(); as element) { @if (element | isEntity) { + } + @if (element | isUserOfRole: ['owner', 'editor'] : user()) { + + } + @if (element | isUserOfRole: 'owner' : user()) { + + } + @if (isFinished()) { + - } - } diff --git a/src/app/components/actionbar/actionbar.component.ts b/src/app/components/actionbar/actionbar.component.ts index 913b3288..4dfabfc4 100644 --- a/src/app/components/actionbar/actionbar.component.ts +++ b/src/app/components/actionbar/actionbar.component.ts @@ -1,10 +1,18 @@ -import { combineLatest, filter, firstValueFrom, map, of, shareReplay, switchMap, tap } from 'rxjs'; +import { + filter, + firstValueFrom, + map, + of, + shareReplay, + switchMap, + tap, +} from 'rxjs'; import { Component, computed, input, signal } from '@angular/core'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { MatSelectModule } from '@angular/material/select'; import { DomSanitizer, SafeUrl } from '@angular/platform-browser'; -import { ActivatedRoute, Router, RouterLink } from '@angular/router'; +import { Router, RouterLink } from '@angular/router'; import { MatButtonModule } from '@angular/material/button'; import { MatOptionModule } from '@angular/material/core'; @@ -78,6 +86,14 @@ export class ActionbarComponent { }); isEntity = computed(() => isEntity(this.element())); isCompilation = computed(() => isCompilation(this.element())); + isFinished = computed(() => { + const element = this.element(); + if (isEntity(element)) { + return element.finished; + } else if (isCompilation(element)) { + return Object.keys(element.entities).length > 0; + } + }); showUsesInCollection = computed(() => this.isEntity() && this.isDetailPage()); @@ -131,7 +147,6 @@ export class ActionbarComponent { private detailPageHelper: DetailPageHelperService, private dialogHelper: DialogHelperService, private router: Router, - private activatedRoute: ActivatedRoute, private sanitizer: DomSanitizer, public selectHistory: SelectHistoryService, private snackbar: SnackbarService, @@ -204,9 +219,9 @@ export class ActionbarComponent { isCompilation: isCompilation(element), }); if (!element) return of(false); - return combineLatest([ - this.allowAnnotatingHelper.isUserOwner$(element), - this.allowAnnotatingHelper.userHasAccess$(element, EntityAccessRole.editor), + return this.allowAnnotatingHelper.userHasAccess$(element, [ + EntityAccessRole.editor, + EntityAccessRole.owner, ]); }), tap(isAllowed => console.log('isAnnotatingAllowed$ isAllowed', isAllowed)), @@ -215,7 +230,12 @@ export class ActionbarComponent { #isEditingAllowed$ = this.element$.pipe( filter(element => !!element), - switchMap(element => this.allowAnnotatingHelper.isUserOwner$(element)), + switchMap(element => + this.allowAnnotatingHelper.userHasAccess$(element, [ + EntityAccessRole.editor, + EntityAccessRole.owner, + ]), + ), ); isEditingAllowed = toSignal(this.#isEditingAllowed$); @@ -258,15 +278,18 @@ export class ActionbarComponent { public editMetadata() { const element = this.element(); - if (!isEntity(element)) return; - this.dialogHelper.editEntity(element); + if (isEntity(element)) { + return this.dialogHelper.editEntity(element); + } else if (isCompilation(element)) { + return this.dialogHelper.createOrEditCompilation(element); + } } public editVisibility() { const element = this.element(); - if (!isEntity(element)) return; + if (!element) return; - const ref = this.dialogHelper.editVisibilityAndAccess([element]); + const ref = this.dialogHelper.editVisibilityAndAccess([element] as IEntity[] | ICompilation[]); ref.afterClosed().subscribe(result => { if (!result) return; @@ -278,22 +301,36 @@ export class ActionbarComponent { }); } + public openTransferOwnerDialog() { + const element = this.element(); + if (!element) return; + return this.dialogHelper.openTransferOwnershipDialog(element); + } + isPublished = computed(() => { const element = this.element(); - if (!isEntity(element)) return false; - return !!element.online; + return element && 'online' in element ? !!element.online : false; }); public async togglePublished() { const element = this.element(); - if (!isEntity(element)) return; - this.backend - .pushEntity({ ...element, online: !element.online }) - .then(result => { - console.log('Toggled?:', result); - if (isEntity(result)) this.updatedElement.set(result); - }) - .catch(error => console.error(error)); + if (isEntity(element)) { + this.backend + .pushEntity({ ...element, online: !element.online }) + .then(result => { + console.log('Toggled?:', result); + if (isEntity(result)) this.updatedElement.set(result); + }) + .catch(error => console.error(error)); + } else if (isCompilation(element)) { + this.backend + .pushCompilation({ ...element, online: !element.online }) + .then(result => { + console.log('Toggled?:', result); + if (isCompilation(result)) this.updatedElement.set(result); + }) + .catch(error => console.error(error)); + } } public copyEmbed() { @@ -324,15 +361,6 @@ export class ActionbarComponent { this.detailPageHelper.copyEmbed(embedHTML); } - public copyId() { - const element = this.element(); - if (!element) return this.snackbar.showMessage('Could not find element'); - const _id = element?._id; - if (!_id) return this.snackbar.showMessage('Could not copy id'); - - this.detailPageHelper.copyID(_id.toString() ?? ''); - } - public async openDownloadDialog() { const options = await firstValueFrom(this.entityDownloadOptions$); if (!options) { @@ -351,6 +379,6 @@ export class ActionbarComponent { return; } - this.dialogHelper.openEntityDownloadDialog(element, options); + return this.dialogHelper.openEntityDownloadDialog(element, options); } } diff --git a/src/app/dialogs/confirmation-dialog/confirmation-dialog.component.ts b/src/app/dialogs/confirmation-dialog/confirmation-dialog.component.ts index e32028c6..fde8bfee 100644 --- a/src/app/dialogs/confirmation-dialog/confirmation-dialog.component.ts +++ b/src/app/dialogs/confirmation-dialog/confirmation-dialog.component.ts @@ -1,4 +1,4 @@ -import { Component, Inject } from '@angular/core'; +import { Component, inject, Inject } from '@angular/core'; import { MAT_DIALOG_DATA, MatDialogRef, @@ -8,12 +8,12 @@ import { import { TranslatePipe } from '../../pipes/translate.pipe'; import { MatButtonModule } from '@angular/material/button'; -export interface IConfirmationDialogData { - title?: string; - message: string; -} - -export type ConfirmationDialogData = string | IConfirmationDialogData; +export type ConfirmationDialogData = + | string + | { + title?: string; + message: string; + }; @Component({ selector: 'app-confirmation-dialog', @@ -22,10 +22,8 @@ export type ConfirmationDialogData = string | IConfirmationDialogData; imports: [MatDialogContent, MatButtonModule, MatDialogClose, TranslatePipe], }) export class ConfirmationDialogComponent { - constructor( - public dialogRef: MatDialogRef, - @Inject(MAT_DIALOG_DATA) public data: ConfirmationDialogData, - ) {} + dialogRef = inject(MatDialogRef); + data = inject(MAT_DIALOG_DATA) as ConfirmationDialogData; get title() { if (typeof this.data === 'string') return undefined; diff --git a/src/app/pages/profile-page/entities/entities.component.ts b/src/app/pages/profile-page/entities/entities.component.ts index a9234800..5c1be990 100644 --- a/src/app/pages/profile-page/entities/entities.component.ts +++ b/src/app/pages/profile-page/entities/entities.component.ts @@ -219,18 +219,8 @@ export class ProfileEntitiesComponent implements AfterViewInit { public openTransferOwnerDialog(entity?: IEntity) { const selection = this.selectionService().selectedElements(); const data = entity ?? (selection.length === 1 ? selection[0] : selection); - - const dialogRef = this.dialog.open(ManageOwnershipComponent, { - data: data, - disableClose: false, - }); - - dialogRef - .afterClosed() - .toPromise() - .then(result => { - this.account.updateTrigger$.next(Collection.entity); - }); + if (!isEntity(data)) return; + this.helper.openTransferOwnershipDialog(data); } public openVisibilityAndAccessDialog(entity?: IEntity) { diff --git a/src/app/pipes/is-user-of-role.pipe.ts b/src/app/pipes/is-user-of-role.pipe.ts index 7fe24131..6c78b99e 100644 --- a/src/app/pipes/is-user-of-role.pipe.ts +++ b/src/app/pipes/is-user-of-role.pipe.ts @@ -13,11 +13,16 @@ import { export class IsUserOfRolePipe { transform( item: IEntity | ICompilation, - role: string, + roles: string | string[], userData: IUserData | IUserDataWithoutData | undefined, ): boolean { if (!item.access || !userData) return false; const userAccess = item.access.find(user => user._id === userData._id); - return userAccess?.role === role; + if (!userAccess) return false; + if (typeof roles === 'string') { + return userAccess.role === roles; + } else { + return roles.includes(userAccess?.role); + } } } diff --git a/src/app/services/allow-annotating.service.ts b/src/app/services/allow-annotating.service.ts index 24042508..2363c6d1 100644 --- a/src/app/services/allow-annotating.service.ts +++ b/src/app/services/allow-annotating.service.ts @@ -36,13 +36,19 @@ export class AllowAnnotatingService { userHasAccess$( element: IEntity | ICompilation | undefined, - role: EntityAccessRole, + roles: EntityAccessRole | EntityAccessRole[], ): Observable { return of(element).pipe( switchMap(() => this.#account.user$), map(user => { if (!element) return false; - return element.access.find(u => u._id === user?._id)?.role === role; + const role = element.access.find(u => u._id === user?._id)?.role; + if (!role) return false; + if (typeof roles === 'string') { + return role === roles; + } else { + return roles.includes(role); + } }), ); } diff --git a/src/app/services/dialog-helper.service.ts b/src/app/services/dialog-helper.service.ts index 32942c7c..604b947b 100644 --- a/src/app/services/dialog-helper.service.ts +++ b/src/app/services/dialog-helper.service.ts @@ -11,7 +11,7 @@ import { VisibilityAndAccessDialogComponent, } from 'src/app/dialogs'; import { ProfilePageEditComponent } from 'src/app/dialogs/profile-page-edit/profile-page-edit.component'; -import { Collection, ICompilation, IEntity } from '@kompakkt/common'; +import { Collection, ICompilation, IEntity, isCompilation, isEntity } from '@kompakkt/common'; import { IPublicProfile } from '@kompakkt/common/interfaces'; import { AuthDialogData } from '../components/auth-dialog/auth-dialog.component'; import { EntityDownloadDialogComponent } from '../dialogs/entity-download-dialog/entity-download-dialog.component'; @@ -31,6 +31,7 @@ import { RemoveFromCompilationComponent, RemoveFromCompilationResult, } from '../dialogs/remove-from-compilation/remove-from-compilation.component'; +import { ManageOwnershipComponent } from '../dialogs/manage-ownership/manage-ownership.component'; @Injectable({ providedIn: 'root', @@ -168,6 +169,21 @@ export class DialogHelperService { return ref; } + public async openTransferOwnershipDialog(data: IEntity | ICompilation) { + const dialogRef = this.#dialog.open(ManageOwnershipComponent, { + data, + disableClose: false, + }); + + firstValueFrom(dialogRef.afterClosed()).then(() => { + if (isEntity(data)) { + this.#account.updateTrigger$.next(Collection.entity); + } else if (isCompilation(data)) { + this.#account.updateTrigger$.next(Collection.compilation); + } + }); + } + public async confirm(message: string, title?: string) { const data: ConfirmationDialogData = title ? { title, message } : message; diff --git a/src/assets/fonts/MaterialSymbols/MaterialSymbolsOutlined[FILL,GRAD,opsz,wght].ttf b/src/assets/fonts/MaterialSymbols/MaterialSymbolsOutlined[FILL,GRAD,opsz,wght].ttf index ccc55bf5..c4a5217f 100644 Binary files a/src/assets/fonts/MaterialSymbols/MaterialSymbolsOutlined[FILL,GRAD,opsz,wght].ttf and b/src/assets/fonts/MaterialSymbols/MaterialSymbolsOutlined[FILL,GRAD,opsz,wght].ttf differ diff --git a/src/assets/fonts/MaterialSymbols/MaterialSymbolsOutlined[FILL,GRAD,opsz,wght].woff2 b/src/assets/fonts/MaterialSymbols/MaterialSymbolsOutlined[FILL,GRAD,opsz,wght].woff2 index 39583bb2..18572482 100644 Binary files a/src/assets/fonts/MaterialSymbols/MaterialSymbolsOutlined[FILL,GRAD,opsz,wght].woff2 and b/src/assets/fonts/MaterialSymbols/MaterialSymbolsOutlined[FILL,GRAD,opsz,wght].woff2 differ