change readonly boolean to role enum

This commit is contained in:
mgabor
2024-04-17 09:31:56 +02:00
parent 9126bf2520
commit 87bc244b68
30 changed files with 297 additions and 141 deletions
+3
View File
@@ -17,6 +17,7 @@ doc/AlbumApi.md
doc/AlbumCountResponseDto.md doc/AlbumCountResponseDto.md
doc/AlbumResponseDto.md doc/AlbumResponseDto.md
doc/AlbumUserResponseDto.md doc/AlbumUserResponseDto.md
doc/AlbumUserRole.md
doc/AllJobStatusResponseDto.md doc/AllJobStatusResponseDto.md
doc/AssetApi.md doc/AssetApi.md
doc/AssetBulkDeleteDto.md doc/AssetBulkDeleteDto.md
@@ -240,6 +241,7 @@ lib/model/add_users_dto.dart
lib/model/album_count_response_dto.dart lib/model/album_count_response_dto.dart
lib/model/album_response_dto.dart lib/model/album_response_dto.dart
lib/model/album_user_response_dto.dart lib/model/album_user_response_dto.dart
lib/model/album_user_role.dart
lib/model/all_job_status_response_dto.dart lib/model/all_job_status_response_dto.dart
lib/model/api_key_create_dto.dart lib/model/api_key_create_dto.dart
lib/model/api_key_create_response_dto.dart lib/model/api_key_create_response_dto.dart
@@ -419,6 +421,7 @@ test/album_api_test.dart
test/album_count_response_dto_test.dart test/album_count_response_dto_test.dart
test/album_response_dto_test.dart test/album_response_dto_test.dart
test/album_user_response_dto_test.dart test/album_user_response_dto_test.dart
test/album_user_role_test.dart
test/all_job_status_response_dto_test.dart test/all_job_status_response_dto_test.dart
test/api_key_api_test.dart test/api_key_api_test.dart
test/api_key_create_dto_test.dart test/api_key_create_dto_test.dart
+1
View File
@@ -235,6 +235,7 @@ Class | Method | HTTP request | Description
- [AlbumCountResponseDto](doc//AlbumCountResponseDto.md) - [AlbumCountResponseDto](doc//AlbumCountResponseDto.md)
- [AlbumResponseDto](doc//AlbumResponseDto.md) - [AlbumResponseDto](doc//AlbumResponseDto.md)
- [AlbumUserResponseDto](doc//AlbumUserResponseDto.md) - [AlbumUserResponseDto](doc//AlbumUserResponseDto.md)
- [AlbumUserRole](doc//AlbumUserRole.md)
- [AllJobStatusResponseDto](doc//AllJobStatusResponseDto.md) - [AllJobStatusResponseDto](doc//AllJobStatusResponseDto.md)
- [AssetBulkDeleteDto](doc//AssetBulkDeleteDto.md) - [AssetBulkDeleteDto](doc//AssetBulkDeleteDto.md)
- [AssetBulkUpdateDto](doc//AssetBulkUpdateDto.md) - [AssetBulkUpdateDto](doc//AssetBulkUpdateDto.md)
+1 -1
View File
@@ -23,7 +23,7 @@ Name | Type | Description | Notes
**owner** | [**UserResponseDto**](UserResponseDto.md) | | **owner** | [**UserResponseDto**](UserResponseDto.md) | |
**ownerId** | **String** | | **ownerId** | **String** | |
**shared** | **bool** | | **shared** | **bool** | |
**sharedUsers** | [**List<UserResponseDto>**](UserResponseDto.md) | Deprecated in favor of users | [default to const []] **sharedUsers** | [**List<UserResponseDto>**](UserResponseDto.md) | Deprecated in favor of sharedUsersV2 | [default to const []]
**sharedUsersV2** | [**List<AlbumUserResponseDto>**](AlbumUserResponseDto.md) | | [default to const []] **sharedUsersV2** | [**List<AlbumUserResponseDto>**](AlbumUserResponseDto.md) | | [default to const []]
**startDate** | [**DateTime**](DateTime.md) | | [optional] **startDate** | [**DateTime**](DateTime.md) | | [optional]
**updatedAt** | [**DateTime**](DateTime.md) | | **updatedAt** | [**DateTime**](DateTime.md) | |
+1 -1
View File
@@ -8,7 +8,7 @@ import 'package:openapi/api.dart';
## Properties ## Properties
Name | Type | Description | Notes Name | Type | Description | Notes
------------ | ------------- | ------------- | ------------- ------------ | ------------- | ------------- | -------------
**readonly** | **bool** | | **role** | [**AlbumUserRole**](AlbumUserRole.md) | |
**user** | [**UserResponseDto**](UserResponseDto.md) | | **user** | [**UserResponseDto**](UserResponseDto.md) | |
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
+14
View File
@@ -0,0 +1,14 @@
# openapi.model.AlbumUserRole
## Load the model package
```dart
import 'package:openapi/api.dart';
```
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
+1 -1
View File
@@ -8,7 +8,7 @@ import 'package:openapi/api.dart';
## Properties ## Properties
Name | Type | Description | Notes Name | Type | Description | Notes
------------ | ------------- | ------------- | ------------- ------------ | ------------- | ------------- | -------------
**readonly** | **bool** | | **role** | [**AlbumUserRole**](AlbumUserRole.md) | |
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
+1
View File
@@ -63,6 +63,7 @@ part 'model/add_users_dto.dart';
part 'model/album_count_response_dto.dart'; part 'model/album_count_response_dto.dart';
part 'model/album_response_dto.dart'; part 'model/album_response_dto.dart';
part 'model/album_user_response_dto.dart'; part 'model/album_user_response_dto.dart';
part 'model/album_user_role.dart';
part 'model/all_job_status_response_dto.dart'; part 'model/all_job_status_response_dto.dart';
part 'model/asset_bulk_delete_dto.dart'; part 'model/asset_bulk_delete_dto.dart';
part 'model/asset_bulk_update_dto.dart'; part 'model/asset_bulk_update_dto.dart';
+2
View File
@@ -204,6 +204,8 @@ class ApiClient {
return AlbumResponseDto.fromJson(value); return AlbumResponseDto.fromJson(value);
case 'AlbumUserResponseDto': case 'AlbumUserResponseDto':
return AlbumUserResponseDto.fromJson(value); return AlbumUserResponseDto.fromJson(value);
case 'AlbumUserRole':
return AlbumUserRoleTypeTransformer().decode(value);
case 'AllJobStatusResponseDto': case 'AllJobStatusResponseDto':
return AllJobStatusResponseDto.fromJson(value); return AllJobStatusResponseDto.fromJson(value);
case 'AssetBulkDeleteDto': case 'AssetBulkDeleteDto':
+3
View File
@@ -55,6 +55,9 @@ String parameterToString(dynamic value) {
if (value is DateTime) { if (value is DateTime) {
return value.toUtc().toIso8601String(); return value.toUtc().toIso8601String();
} }
if (value is AlbumUserRole) {
return AlbumUserRoleTypeTransformer().encode(value).toString();
}
if (value is AssetJobName) { if (value is AssetJobName) {
return AssetJobNameTypeTransformer().encode(value).toString(); return AssetJobNameTypeTransformer().encode(value).toString();
} }
+1 -1
View File
@@ -82,7 +82,7 @@ class AlbumResponseDto {
bool shared; bool shared;
/// Deprecated in favor of users /// Deprecated in favor of sharedUsersV2
List<UserResponseDto> sharedUsers; List<UserResponseDto> sharedUsers;
List<AlbumUserResponseDto> sharedUsersV2; List<AlbumUserResponseDto> sharedUsersV2;
+8 -8
View File
@@ -13,31 +13,31 @@ part of openapi.api;
class AlbumUserResponseDto { class AlbumUserResponseDto {
/// Returns a new [AlbumUserResponseDto] instance. /// Returns a new [AlbumUserResponseDto] instance.
AlbumUserResponseDto({ AlbumUserResponseDto({
required this.readonly, required this.role,
required this.user, required this.user,
}); });
bool readonly; AlbumUserRole role;
UserResponseDto user; UserResponseDto user;
@override @override
bool operator ==(Object other) => identical(this, other) || other is AlbumUserResponseDto && bool operator ==(Object other) => identical(this, other) || other is AlbumUserResponseDto &&
other.readonly == readonly && other.role == role &&
other.user == user; other.user == user;
@override @override
int get hashCode => int get hashCode =>
// ignore: unnecessary_parenthesis // ignore: unnecessary_parenthesis
(readonly.hashCode) + (role.hashCode) +
(user.hashCode); (user.hashCode);
@override @override
String toString() => 'AlbumUserResponseDto[readonly=$readonly, user=$user]'; String toString() => 'AlbumUserResponseDto[role=$role, user=$user]';
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final json = <String, dynamic>{}; final json = <String, dynamic>{};
json[r'readonly'] = this.readonly; json[r'role'] = this.role;
json[r'user'] = this.user; json[r'user'] = this.user;
return json; return json;
} }
@@ -50,7 +50,7 @@ class AlbumUserResponseDto {
final json = value.cast<String, dynamic>(); final json = value.cast<String, dynamic>();
return AlbumUserResponseDto( return AlbumUserResponseDto(
readonly: mapValueOfType<bool>(json, r'readonly')!, role: AlbumUserRole.fromJson(json[r'role'])!,
user: UserResponseDto.fromJson(json[r'user'])!, user: UserResponseDto.fromJson(json[r'user'])!,
); );
} }
@@ -99,7 +99,7 @@ class AlbumUserResponseDto {
/// The list of required keys that must be present in a JSON. /// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{ static const requiredKeys = <String>{
'readonly', 'role',
'user', 'user',
}; };
} }
+85
View File
@@ -0,0 +1,85 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.12
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
part of openapi.api;
class AlbumUserRole {
/// Instantiate a new enum with the provided [value].
const AlbumUserRole._(this.value);
/// The underlying value of this enum member.
final String value;
@override
String toString() => value;
String toJson() => value;
static const editor = AlbumUserRole._(r'editor');
static const viewer = AlbumUserRole._(r'viewer');
/// List of all possible values in this [enum][AlbumUserRole].
static const values = <AlbumUserRole>[
editor,
viewer,
];
static AlbumUserRole? fromJson(dynamic value) => AlbumUserRoleTypeTransformer().decode(value);
static List<AlbumUserRole> listFromJson(dynamic json, {bool growable = false,}) {
final result = <AlbumUserRole>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = AlbumUserRole.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
}
/// Transformation class that can [encode] an instance of [AlbumUserRole] to String,
/// and [decode] dynamic data back to [AlbumUserRole].
class AlbumUserRoleTypeTransformer {
factory AlbumUserRoleTypeTransformer() => _instance ??= const AlbumUserRoleTypeTransformer._();
const AlbumUserRoleTypeTransformer._();
String encode(AlbumUserRole data) => data.value;
/// Decodes a [dynamic value][data] to a AlbumUserRole.
///
/// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully,
/// then null is returned. However, if [allowNull] is false and the [dynamic value][data]
/// cannot be decoded successfully, then an [UnimplementedError] is thrown.
///
/// The [allowNull] is very handy when an API changes and a new enum value is added or removed,
/// and users are still using an old app with the old code.
AlbumUserRole? decode(dynamic data, {bool allowNull = true}) {
if (data != null) {
switch (data) {
case r'editor': return AlbumUserRole.editor;
case r'viewer': return AlbumUserRole.viewer;
default:
if (!allowNull) {
throw ArgumentError('Unknown enum value to decode: $data');
}
}
}
return null;
}
/// Singleton [AlbumUserRoleTypeTransformer] instance.
static AlbumUserRoleTypeTransformer? _instance;
}
+8 -8
View File
@@ -13,26 +13,26 @@ part of openapi.api;
class UpdateAlbumUserDto { class UpdateAlbumUserDto {
/// Returns a new [UpdateAlbumUserDto] instance. /// Returns a new [UpdateAlbumUserDto] instance.
UpdateAlbumUserDto({ UpdateAlbumUserDto({
required this.readonly, required this.role,
}); });
bool readonly; AlbumUserRole role;
@override @override
bool operator ==(Object other) => identical(this, other) || other is UpdateAlbumUserDto && bool operator ==(Object other) => identical(this, other) || other is UpdateAlbumUserDto &&
other.readonly == readonly; other.role == role;
@override @override
int get hashCode => int get hashCode =>
// ignore: unnecessary_parenthesis // ignore: unnecessary_parenthesis
(readonly.hashCode); (role.hashCode);
@override @override
String toString() => 'UpdateAlbumUserDto[readonly=$readonly]'; String toString() => 'UpdateAlbumUserDto[role=$role]';
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final json = <String, dynamic>{}; final json = <String, dynamic>{};
json[r'readonly'] = this.readonly; json[r'role'] = this.role;
return json; return json;
} }
@@ -44,7 +44,7 @@ class UpdateAlbumUserDto {
final json = value.cast<String, dynamic>(); final json = value.cast<String, dynamic>();
return UpdateAlbumUserDto( return UpdateAlbumUserDto(
readonly: mapValueOfType<bool>(json, r'readonly')!, role: AlbumUserRole.fromJson(json[r'role'])!,
); );
} }
return null; return null;
@@ -92,7 +92,7 @@ class UpdateAlbumUserDto {
/// The list of required keys that must be present in a JSON. /// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{ static const requiredKeys = <String>{
'readonly', 'role',
}; };
} }
+1 -1
View File
@@ -91,7 +91,7 @@ void main() {
// TODO // TODO
}); });
// Deprecated in favor of users // Deprecated in favor of sharedUsersV2
// List<UserResponseDto> sharedUsers (default value: const []) // List<UserResponseDto> sharedUsers (default value: const [])
test('to test the property `sharedUsers`', () async { test('to test the property `sharedUsers`', () async {
// TODO // TODO
+2 -2
View File
@@ -16,8 +16,8 @@ void main() {
// final instance = AlbumUserResponseDto(); // final instance = AlbumUserResponseDto();
group('test AlbumUserResponseDto', () { group('test AlbumUserResponseDto', () {
// bool readonly // AlbumUserRole role
test('to test the property `readonly`', () async { test('to test the property `role`', () async {
// TODO // TODO
}); });
+21
View File
@@ -0,0 +1,21 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.12
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
import 'package:openapi/api.dart';
import 'package:test/test.dart';
// tests for AlbumUserRole
void main() {
group('test AlbumUserRole', () {
});
}
+2 -2
View File
@@ -16,8 +16,8 @@ void main() {
// final instance = UpdateAlbumUserDto(); // final instance = UpdateAlbumUserDto();
group('test UpdateAlbumUserDto', () { group('test UpdateAlbumUserDto', () {
// bool readonly // AlbumUserRole role
test('to test the property `readonly`', () async { test('to test the property `role`', () async {
// TODO // TODO
}); });
+14 -7
View File
@@ -7174,7 +7174,7 @@
}, },
"sharedUsers": { "sharedUsers": {
"deprecated": true, "deprecated": true,
"description": "Deprecated in favor of users", "description": "Deprecated in favor of sharedUsersV2",
"items": { "items": {
"$ref": "#/components/schemas/UserResponseDto" "$ref": "#/components/schemas/UserResponseDto"
}, },
@@ -7216,19 +7216,26 @@
}, },
"AlbumUserResponseDto": { "AlbumUserResponseDto": {
"properties": { "properties": {
"readonly": { "role": {
"type": "boolean" "$ref": "#/components/schemas/AlbumUserRole"
}, },
"user": { "user": {
"$ref": "#/components/schemas/UserResponseDto" "$ref": "#/components/schemas/UserResponseDto"
} }
}, },
"required": [ "required": [
"readonly", "role",
"user" "user"
], ],
"type": "object" "type": "object"
}, },
"AlbumUserRole": {
"enum": [
"editor",
"viewer"
],
"type": "string"
},
"AllJobStatusResponseDto": { "AllJobStatusResponseDto": {
"properties": { "properties": {
"backgroundTask": { "backgroundTask": {
@@ -10962,12 +10969,12 @@
}, },
"UpdateAlbumUserDto": { "UpdateAlbumUserDto": {
"properties": { "properties": {
"readonly": { "role": {
"type": "boolean" "$ref": "#/components/schemas/AlbumUserRole"
} }
}, },
"required": [ "required": [
"readonly" "role"
], ],
"type": "object" "type": "object"
}, },
+7 -3
View File
@@ -142,7 +142,7 @@ export type AssetResponseDto = {
updatedAt: string; updatedAt: string;
}; };
export type AlbumUserResponseDto = { export type AlbumUserResponseDto = {
"readonly": boolean; role: AlbumUserRole;
user: UserResponseDto; user: UserResponseDto;
}; };
export type AlbumResponseDto = { export type AlbumResponseDto = {
@@ -161,7 +161,7 @@ export type AlbumResponseDto = {
owner: UserResponseDto; owner: UserResponseDto;
ownerId: string; ownerId: string;
shared: boolean; shared: boolean;
/** Deprecated in favor of users */ /** Deprecated in favor of sharedUsersV2 */
sharedUsers: UserResponseDto[]; sharedUsers: UserResponseDto[];
sharedUsersV2: AlbumUserResponseDto[]; sharedUsersV2: AlbumUserResponseDto[];
startDate?: string; startDate?: string;
@@ -194,7 +194,7 @@ export type BulkIdResponseDto = {
success: boolean; success: boolean;
}; };
export type UpdateAlbumUserDto = { export type UpdateAlbumUserDto = {
"readonly": boolean; role: AlbumUserRole;
}; };
export type AddUsersDto = { export type AddUsersDto = {
sharedUserIds: string[]; sharedUserIds: string[];
@@ -2901,6 +2901,10 @@ export enum AssetOrder {
Asc = "asc", Asc = "asc",
Desc = "desc" Desc = "desc"
} }
export enum AlbumUserRole {
Editor = "editor",
Viewer = "viewer"
}
export enum Error { export enum Error {
Duplicate = "duplicate", Duplicate = "duplicate",
NoPermission = "no_permission", NoPermission = "no_permission",
+4 -3
View File
@@ -1,5 +1,6 @@
import { BadRequestException, UnauthorizedException } from '@nestjs/common'; import { BadRequestException, UnauthorizedException } from '@nestjs/common';
import { AuthDto } from 'src/dtos/auth.dto'; import { AuthDto } from 'src/dtos/auth.dto';
import { AlbumUserRole } from 'src/entities/album-user.entity';
import { SharedLinkEntity } from 'src/entities/shared-link.entity'; import { SharedLinkEntity } from 'src/entities/shared-link.entity';
import { IAccessRepository } from 'src/interfaces/access.interface'; import { IAccessRepository } from 'src/interfaces/access.interface';
import { setDifference, setIsEqual, setUnion } from 'src/utils/set'; import { setDifference, setIsEqual, setUnion } from 'src/utils/set';
@@ -219,7 +220,7 @@ export class AccessCore {
const isShared = await this.repository.album.checkSharedAlbumAccess( const isShared = await this.repository.album.checkSharedAlbumAccess(
auth.user.id, auth.user.id,
setDifference(ids, isOwner), setDifference(ids, isOwner),
'read', AlbumUserRole.Viewer,
); );
return setUnion(isOwner, isShared); return setUnion(isOwner, isShared);
} }
@@ -229,7 +230,7 @@ export class AccessCore {
const isShared = await this.repository.album.checkSharedAlbumAccess( const isShared = await this.repository.album.checkSharedAlbumAccess(
auth.user.id, auth.user.id,
setDifference(ids, isOwner), setDifference(ids, isOwner),
'write', AlbumUserRole.Editor,
); );
return setUnion(isOwner, isShared); return setUnion(isOwner, isShared);
} }
@@ -251,7 +252,7 @@ export class AccessCore {
const isShared = await this.repository.album.checkSharedAlbumAccess( const isShared = await this.repository.album.checkSharedAlbumAccess(
auth.user.id, auth.user.id,
setDifference(ids, isOwner), setDifference(ids, isOwner),
'read', AlbumUserRole.Viewer,
); );
return setUnion(isOwner, isShared); return setUnion(isOwner, isShared);
} }
+7 -4
View File
@@ -3,6 +3,7 @@ import { ArrayNotEmpty, IsEnum, IsString } from 'class-validator';
import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto'; import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto';
import { AuthDto } from 'src/dtos/auth.dto'; import { AuthDto } from 'src/dtos/auth.dto';
import { UserResponseDto, mapUser } from 'src/dtos/user.dto'; import { UserResponseDto, mapUser } from 'src/dtos/user.dto';
import { AlbumUserRole } from 'src/entities/album-user.entity';
import { AlbumEntity, AssetOrder } from 'src/entities/album.entity'; import { AlbumEntity, AssetOrder } from 'src/entities/album.entity';
import { Optional, ValidateBoolean, ValidateUUID } from 'src/validation'; import { Optional, ValidateBoolean, ValidateUUID } from 'src/validation';
@@ -84,13 +85,15 @@ export class AlbumCountResponseDto {
} }
export class UpdateAlbumUserDto { export class UpdateAlbumUserDto {
@ValidateBoolean() @IsEnum(AlbumUserRole)
readonly!: boolean; @ApiProperty({ enum: AlbumUserRole, enumName: 'AlbumUserRole' })
role!: AlbumUserRole;
} }
export class AlbumUserResponseDto { export class AlbumUserResponseDto {
user!: UserResponseDto; user!: UserResponseDto;
readonly!: boolean; @ApiProperty({ enum: AlbumUserRole, enumName: 'AlbumUserRole' })
role!: AlbumUserRole;
} }
export class AlbumResponseDto { export class AlbumResponseDto {
@@ -128,7 +131,7 @@ export const mapAlbum = (entity: AlbumEntity, withAssets: boolean, auth?: AuthDt
sharedUsers.push(mapUser(permission.user)); sharedUsers.push(mapUser(permission.user));
sharedUsersV2.push({ sharedUsersV2.push({
user: mapUser(permission.user), user: mapUser(permission.user),
readonly: permission.readonly, role: permission.role,
}); });
} }
} }
+7 -2
View File
@@ -2,6 +2,11 @@ import { AlbumEntity } from 'src/entities/album.entity';
import { UserEntity } from 'src/entities/user.entity'; import { UserEntity } from 'src/entities/user.entity';
import { Column, Entity, Index, JoinColumn, ManyToOne, PrimaryColumn } from 'typeorm'; import { Column, Entity, Index, JoinColumn, ManyToOne, PrimaryColumn } from 'typeorm';
export enum AlbumUserRole {
Editor = 'editor',
Viewer = 'viewer',
}
@Entity('albums_shared_users_users') @Entity('albums_shared_users_users')
// Pre-existing indices from original album <--> user ManyToMany mapping // Pre-existing indices from original album <--> user ManyToMany mapping
@Index('IDX_427c350ad49bd3935a50baab73', ['album']) @Index('IDX_427c350ad49bd3935a50baab73', ['album'])
@@ -17,6 +22,6 @@ export class AlbumUserEntity {
@ManyToOne(() => UserEntity, { onDelete: 'CASCADE', onUpdate: 'CASCADE', nullable: false }) @ManyToOne(() => UserEntity, { onDelete: 'CASCADE', onUpdate: 'CASCADE', nullable: false })
user!: UserEntity; user!: UserEntity;
@Column({ default: true }) @Column({ type: 'varchar', default: 'viewer' })
readonly!: boolean; role!: AlbumUserRole;
} }
+3 -1
View File
@@ -1,3 +1,5 @@
import { AlbumUserRole } from 'src/entities/album-user.entity';
export const IAccessRepository = 'IAccessRepository'; export const IAccessRepository = 'IAccessRepository';
export type ReadWrite = 'read' | 'write'; export type ReadWrite = 'read' | 'write';
@@ -22,7 +24,7 @@ export interface IAccessRepository {
album: { album: {
checkOwnerAccess(userId: string, albumIds: Set<string>): Promise<Set<string>>; checkOwnerAccess(userId: string, albumIds: Set<string>): Promise<Set<string>>;
checkSharedAlbumAccess(userId: string, albumIds: Set<string>, readWrite: ReadWrite): Promise<Set<string>>; checkSharedAlbumAccess(userId: string, albumIds: Set<string>, access: AlbumUserRole): Promise<Set<string>>;
checkSharedLinkAccess(sharedLinkId: string, albumIds: Set<string>): Promise<Set<string>>; checkSharedLinkAccess(sharedLinkId: string, albumIds: Set<string>): Promise<Set<string>>;
}; };
@@ -1,15 +1,15 @@
import { MigrationInterface, QueryRunner } from "typeorm"; import { MigrationInterface, QueryRunner } from "typeorm";
export class AddAlbumUserReadonly1713298646379 implements MigrationInterface { export class AddAlbumUserRole1713337511945 implements MigrationInterface {
name = 'AddAlbumUserReadonly1713298646379' name = 'AddAlbumUserRole1713337511945'
public async up(queryRunner: QueryRunner): Promise<void> { public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "albums_shared_users_users" ADD "readonly" boolean NOT NULL DEFAULT false`); await queryRunner.query(`ALTER TABLE "albums_shared_users_users" ADD "role" character varying NOT NULL DEFAULT 'editor'`);
await queryRunner.query(`ALTER TABLE "albums_shared_users_users" ALTER COLUMN "readonly" SET DEFAULT true`); await queryRunner.query(`ALTER TABLE "albums_shared_users_users" ALTER COLUMN "role" SET DEFAULT 'viewer'`);
} }
public async down(queryRunner: QueryRunner): Promise<void> { public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "albums_shared_users_users" DROP COLUMN "readonly"`); await queryRunner.query(`ALTER TABLE "albums_shared_users_users" DROP COLUMN "role"`);
} }
} }
+4 -1
View File
@@ -70,7 +70,7 @@ SELECT
"AlbumEntity"."id" AS "AlbumEntity_id", "AlbumEntity"."id" AS "AlbumEntity_id",
"AlbumEntity__AlbumEntity_sharedUsers"."albumsId" AS "AlbumEntity__AlbumEntity_sharedUsers_albumsId", "AlbumEntity__AlbumEntity_sharedUsers"."albumsId" AS "AlbumEntity__AlbumEntity_sharedUsers_albumsId",
"AlbumEntity__AlbumEntity_sharedUsers"."usersId" AS "AlbumEntity__AlbumEntity_sharedUsers_usersId", "AlbumEntity__AlbumEntity_sharedUsers"."usersId" AS "AlbumEntity__AlbumEntity_sharedUsers_usersId",
"AlbumEntity__AlbumEntity_sharedUsers"."readonly" AS "AlbumEntity__AlbumEntity_sharedUsers_readonly" "AlbumEntity__AlbumEntity_sharedUsers"."role" AS "AlbumEntity__AlbumEntity_sharedUsers_role"
FROM FROM
"albums" "AlbumEntity" "albums" "AlbumEntity"
LEFT JOIN "albums_shared_users_users" "AlbumEntity__AlbumEntity_sharedUsers" ON "AlbumEntity__AlbumEntity_sharedUsers"."albumsId" = "AlbumEntity"."id" LEFT JOIN "albums_shared_users_users" "AlbumEntity__AlbumEntity_sharedUsers" ON "AlbumEntity__AlbumEntity_sharedUsers"."albumsId" = "AlbumEntity"."id"
@@ -83,6 +83,9 @@ WHERE
( (
"AlbumEntity__AlbumEntity_sharedUsers"."usersId" = $2 "AlbumEntity__AlbumEntity_sharedUsers"."usersId" = $2
) )
AND (
"AlbumEntity__AlbumEntity_sharedUsers"."role" IN ($3, $4)
)
) )
) )
) )
+6 -6
View File
@@ -34,7 +34,7 @@ FROM
"AlbumEntity__AlbumEntity_owner"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaUsageInBytes", "AlbumEntity__AlbumEntity_owner"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaUsageInBytes",
"AlbumEntity__AlbumEntity_sharedUsers"."albumsId" AS "AlbumEntity__AlbumEntity_sharedUsers_albumsId", "AlbumEntity__AlbumEntity_sharedUsers"."albumsId" AS "AlbumEntity__AlbumEntity_sharedUsers_albumsId",
"AlbumEntity__AlbumEntity_sharedUsers"."usersId" AS "AlbumEntity__AlbumEntity_sharedUsers_usersId", "AlbumEntity__AlbumEntity_sharedUsers"."usersId" AS "AlbumEntity__AlbumEntity_sharedUsers_usersId",
"AlbumEntity__AlbumEntity_sharedUsers"."readonly" AS "AlbumEntity__AlbumEntity_sharedUsers_readonly", "AlbumEntity__AlbumEntity_sharedUsers"."role" AS "AlbumEntity__AlbumEntity_sharedUsers_role",
"c20102de0f4f51a0efbaca481ef9bb2f99dd7c0a"."id" AS "c20102de0f4f51a0efbaca481ef9bb2f99dd7c0a_id", "c20102de0f4f51a0efbaca481ef9bb2f99dd7c0a"."id" AS "c20102de0f4f51a0efbaca481ef9bb2f99dd7c0a_id",
"c20102de0f4f51a0efbaca481ef9bb2f99dd7c0a"."name" AS "c20102de0f4f51a0efbaca481ef9bb2f99dd7c0a_name", "c20102de0f4f51a0efbaca481ef9bb2f99dd7c0a"."name" AS "c20102de0f4f51a0efbaca481ef9bb2f99dd7c0a_name",
"c20102de0f4f51a0efbaca481ef9bb2f99dd7c0a"."avatarColor" AS "c20102de0f4f51a0efbaca481ef9bb2f99dd7c0a_avatarColor", "c20102de0f4f51a0efbaca481ef9bb2f99dd7c0a"."avatarColor" AS "c20102de0f4f51a0efbaca481ef9bb2f99dd7c0a_avatarColor",
@@ -114,7 +114,7 @@ SELECT
"AlbumEntity__AlbumEntity_owner"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaUsageInBytes", "AlbumEntity__AlbumEntity_owner"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaUsageInBytes",
"AlbumEntity__AlbumEntity_sharedUsers"."albumsId" AS "AlbumEntity__AlbumEntity_sharedUsers_albumsId", "AlbumEntity__AlbumEntity_sharedUsers"."albumsId" AS "AlbumEntity__AlbumEntity_sharedUsers_albumsId",
"AlbumEntity__AlbumEntity_sharedUsers"."usersId" AS "AlbumEntity__AlbumEntity_sharedUsers_usersId", "AlbumEntity__AlbumEntity_sharedUsers"."usersId" AS "AlbumEntity__AlbumEntity_sharedUsers_usersId",
"AlbumEntity__AlbumEntity_sharedUsers"."readonly" AS "AlbumEntity__AlbumEntity_sharedUsers_readonly", "AlbumEntity__AlbumEntity_sharedUsers"."role" AS "AlbumEntity__AlbumEntity_sharedUsers_role",
"c20102de0f4f51a0efbaca481ef9bb2f99dd7c0a"."id" AS "c20102de0f4f51a0efbaca481ef9bb2f99dd7c0a_id", "c20102de0f4f51a0efbaca481ef9bb2f99dd7c0a"."id" AS "c20102de0f4f51a0efbaca481ef9bb2f99dd7c0a_id",
"c20102de0f4f51a0efbaca481ef9bb2f99dd7c0a"."name" AS "c20102de0f4f51a0efbaca481ef9bb2f99dd7c0a_name", "c20102de0f4f51a0efbaca481ef9bb2f99dd7c0a"."name" AS "c20102de0f4f51a0efbaca481ef9bb2f99dd7c0a_name",
"c20102de0f4f51a0efbaca481ef9bb2f99dd7c0a"."avatarColor" AS "c20102de0f4f51a0efbaca481ef9bb2f99dd7c0a_avatarColor", "c20102de0f4f51a0efbaca481ef9bb2f99dd7c0a"."avatarColor" AS "c20102de0f4f51a0efbaca481ef9bb2f99dd7c0a_avatarColor",
@@ -176,7 +176,7 @@ SELECT
"AlbumEntity__AlbumEntity_owner"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaUsageInBytes", "AlbumEntity__AlbumEntity_owner"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaUsageInBytes",
"AlbumEntity__AlbumEntity_sharedUsers"."albumsId" AS "AlbumEntity__AlbumEntity_sharedUsers_albumsId", "AlbumEntity__AlbumEntity_sharedUsers"."albumsId" AS "AlbumEntity__AlbumEntity_sharedUsers_albumsId",
"AlbumEntity__AlbumEntity_sharedUsers"."usersId" AS "AlbumEntity__AlbumEntity_sharedUsers_usersId", "AlbumEntity__AlbumEntity_sharedUsers"."usersId" AS "AlbumEntity__AlbumEntity_sharedUsers_usersId",
"AlbumEntity__AlbumEntity_sharedUsers"."readonly" AS "AlbumEntity__AlbumEntity_sharedUsers_readonly", "AlbumEntity__AlbumEntity_sharedUsers"."role" AS "AlbumEntity__AlbumEntity_sharedUsers_role",
"c20102de0f4f51a0efbaca481ef9bb2f99dd7c0a"."id" AS "c20102de0f4f51a0efbaca481ef9bb2f99dd7c0a_id", "c20102de0f4f51a0efbaca481ef9bb2f99dd7c0a"."id" AS "c20102de0f4f51a0efbaca481ef9bb2f99dd7c0a_id",
"c20102de0f4f51a0efbaca481ef9bb2f99dd7c0a"."name" AS "c20102de0f4f51a0efbaca481ef9bb2f99dd7c0a_name", "c20102de0f4f51a0efbaca481ef9bb2f99dd7c0a"."name" AS "c20102de0f4f51a0efbaca481ef9bb2f99dd7c0a_name",
"c20102de0f4f51a0efbaca481ef9bb2f99dd7c0a"."avatarColor" AS "c20102de0f4f51a0efbaca481ef9bb2f99dd7c0a_avatarColor", "c20102de0f4f51a0efbaca481ef9bb2f99dd7c0a"."avatarColor" AS "c20102de0f4f51a0efbaca481ef9bb2f99dd7c0a_avatarColor",
@@ -296,7 +296,7 @@ SELECT
"AlbumEntity"."order" AS "AlbumEntity_order", "AlbumEntity"."order" AS "AlbumEntity_order",
"AlbumEntity__AlbumEntity_sharedUsers"."albumsId" AS "AlbumEntity__AlbumEntity_sharedUsers_albumsId", "AlbumEntity__AlbumEntity_sharedUsers"."albumsId" AS "AlbumEntity__AlbumEntity_sharedUsers_albumsId",
"AlbumEntity__AlbumEntity_sharedUsers"."usersId" AS "AlbumEntity__AlbumEntity_sharedUsers_usersId", "AlbumEntity__AlbumEntity_sharedUsers"."usersId" AS "AlbumEntity__AlbumEntity_sharedUsers_usersId",
"AlbumEntity__AlbumEntity_sharedUsers"."readonly" AS "AlbumEntity__AlbumEntity_sharedUsers_readonly", "AlbumEntity__AlbumEntity_sharedUsers"."role" AS "AlbumEntity__AlbumEntity_sharedUsers_role",
"c20102de0f4f51a0efbaca481ef9bb2f99dd7c0a"."id" AS "c20102de0f4f51a0efbaca481ef9bb2f99dd7c0a_id", "c20102de0f4f51a0efbaca481ef9bb2f99dd7c0a"."id" AS "c20102de0f4f51a0efbaca481ef9bb2f99dd7c0a_id",
"c20102de0f4f51a0efbaca481ef9bb2f99dd7c0a"."name" AS "c20102de0f4f51a0efbaca481ef9bb2f99dd7c0a_name", "c20102de0f4f51a0efbaca481ef9bb2f99dd7c0a"."name" AS "c20102de0f4f51a0efbaca481ef9bb2f99dd7c0a_name",
"c20102de0f4f51a0efbaca481ef9bb2f99dd7c0a"."avatarColor" AS "c20102de0f4f51a0efbaca481ef9bb2f99dd7c0a_avatarColor", "c20102de0f4f51a0efbaca481ef9bb2f99dd7c0a"."avatarColor" AS "c20102de0f4f51a0efbaca481ef9bb2f99dd7c0a_avatarColor",
@@ -373,7 +373,7 @@ SELECT
"AlbumEntity"."order" AS "AlbumEntity_order", "AlbumEntity"."order" AS "AlbumEntity_order",
"AlbumEntity__AlbumEntity_sharedUsers"."albumsId" AS "AlbumEntity__AlbumEntity_sharedUsers_albumsId", "AlbumEntity__AlbumEntity_sharedUsers"."albumsId" AS "AlbumEntity__AlbumEntity_sharedUsers_albumsId",
"AlbumEntity__AlbumEntity_sharedUsers"."usersId" AS "AlbumEntity__AlbumEntity_sharedUsers_usersId", "AlbumEntity__AlbumEntity_sharedUsers"."usersId" AS "AlbumEntity__AlbumEntity_sharedUsers_usersId",
"AlbumEntity__AlbumEntity_sharedUsers"."readonly" AS "AlbumEntity__AlbumEntity_sharedUsers_readonly", "AlbumEntity__AlbumEntity_sharedUsers"."role" AS "AlbumEntity__AlbumEntity_sharedUsers_role",
"c20102de0f4f51a0efbaca481ef9bb2f99dd7c0a"."id" AS "c20102de0f4f51a0efbaca481ef9bb2f99dd7c0a_id", "c20102de0f4f51a0efbaca481ef9bb2f99dd7c0a"."id" AS "c20102de0f4f51a0efbaca481ef9bb2f99dd7c0a_id",
"c20102de0f4f51a0efbaca481ef9bb2f99dd7c0a"."name" AS "c20102de0f4f51a0efbaca481ef9bb2f99dd7c0a_name", "c20102de0f4f51a0efbaca481ef9bb2f99dd7c0a"."name" AS "c20102de0f4f51a0efbaca481ef9bb2f99dd7c0a_name",
"c20102de0f4f51a0efbaca481ef9bb2f99dd7c0a"."avatarColor" AS "c20102de0f4f51a0efbaca481ef9bb2f99dd7c0a_avatarColor", "c20102de0f4f51a0efbaca481ef9bb2f99dd7c0a"."avatarColor" AS "c20102de0f4f51a0efbaca481ef9bb2f99dd7c0a_avatarColor",
@@ -489,7 +489,7 @@ SELECT
"AlbumEntity"."order" AS "AlbumEntity_order", "AlbumEntity"."order" AS "AlbumEntity_order",
"AlbumEntity__AlbumEntity_sharedUsers"."albumsId" AS "AlbumEntity__AlbumEntity_sharedUsers_albumsId", "AlbumEntity__AlbumEntity_sharedUsers"."albumsId" AS "AlbumEntity__AlbumEntity_sharedUsers_albumsId",
"AlbumEntity__AlbumEntity_sharedUsers"."usersId" AS "AlbumEntity__AlbumEntity_sharedUsers_usersId", "AlbumEntity__AlbumEntity_sharedUsers"."usersId" AS "AlbumEntity__AlbumEntity_sharedUsers_usersId",
"AlbumEntity__AlbumEntity_sharedUsers"."readonly" AS "AlbumEntity__AlbumEntity_sharedUsers_readonly", "AlbumEntity__AlbumEntity_sharedUsers"."role" AS "AlbumEntity__AlbumEntity_sharedUsers_role",
"AlbumEntity__AlbumEntity_sharedLinks"."id" AS "AlbumEntity__AlbumEntity_sharedLinks_id", "AlbumEntity__AlbumEntity_sharedLinks"."id" AS "AlbumEntity__AlbumEntity_sharedLinks_id",
"AlbumEntity__AlbumEntity_sharedLinks"."description" AS "AlbumEntity__AlbumEntity_sharedLinks_description", "AlbumEntity__AlbumEntity_sharedLinks"."description" AS "AlbumEntity__AlbumEntity_sharedLinks_description",
"AlbumEntity__AlbumEntity_sharedLinks"."password" AS "AlbumEntity__AlbumEntity_sharedLinks_password", "AlbumEntity__AlbumEntity_sharedLinks"."password" AS "AlbumEntity__AlbumEntity_sharedLinks_password",
+6 -4
View File
@@ -1,6 +1,7 @@
import { InjectRepository } from '@nestjs/typeorm'; import { InjectRepository } from '@nestjs/typeorm';
import { ChunkedSet, DummyValue, GenerateSql } from 'src/decorators'; import { ChunkedSet, DummyValue, GenerateSql } from 'src/decorators';
import { ActivityEntity } from 'src/entities/activity.entity'; import { ActivityEntity } from 'src/entities/activity.entity';
import { AlbumUserRole } from 'src/entities/album-user.entity';
import { AlbumEntity } from 'src/entities/album.entity'; import { AlbumEntity } from 'src/entities/album.entity';
import { AssetFaceEntity } from 'src/entities/asset-face.entity'; import { AssetFaceEntity } from 'src/entities/asset-face.entity';
import { AssetEntity } from 'src/entities/asset.entity'; import { AssetEntity } from 'src/entities/asset.entity';
@@ -10,7 +11,7 @@ import { PartnerEntity } from 'src/entities/partner.entity';
import { PersonEntity } from 'src/entities/person.entity'; import { PersonEntity } from 'src/entities/person.entity';
import { SharedLinkEntity } from 'src/entities/shared-link.entity'; import { SharedLinkEntity } from 'src/entities/shared-link.entity';
import { UserTokenEntity } from 'src/entities/user-token.entity'; import { UserTokenEntity } from 'src/entities/user-token.entity';
import { IAccessRepository, ReadWrite } from 'src/interfaces/access.interface'; import { IAccessRepository } from 'src/interfaces/access.interface';
import { Instrumentation } from 'src/utils/instrumentation'; import { Instrumentation } from 'src/utils/instrumentation';
import { Brackets, In, Repository } from 'typeorm'; import { Brackets, In, Repository } from 'typeorm';
@@ -119,7 +120,7 @@ class AlbumAccess implements IAlbumAccess {
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] }) @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] })
@ChunkedSet({ paramIndex: 1 }) @ChunkedSet({ paramIndex: 1 })
async checkSharedAlbumAccess(userId: string, albumIds: Set<string>, readWrite: ReadWrite): Promise<Set<string>> { async checkSharedAlbumAccess(userId: string, albumIds: Set<string>, access: AlbumUserRole): Promise<Set<string>> {
if (albumIds.size === 0) { if (albumIds.size === 0) {
return new Set(); return new Set();
} }
@@ -132,8 +133,9 @@ class AlbumAccess implements IAlbumAccess {
id: In([...albumIds]), id: In([...albumIds]),
sharedUsers: { sharedUsers: {
user: { id: userId }, user: { id: userId },
// If write is needed we check for it, otherwise both are accepted // If editor access is needed we check for it, otherwise both are accepted
readonly: readWrite === 'write' ? false : undefined, role:
access === AlbumUserRole.Editor ? AlbumUserRole.Editor : In([AlbumUserRole.Editor, AlbumUserRole.Viewer]),
}, },
}, },
}) })
+1 -1
View File
@@ -273,7 +273,7 @@ export class AlbumService {
throw new BadRequestException('Album not shared with user'); throw new BadRequestException('Album not shared with user');
} }
await this.albumPermissionRepository.update({ albumId: id, userId }, { readonly: dto.readonly }); await this.albumPermissionRepository.update({ albumId: id, userId }, { role: dto.role });
} }
private async findOrFail(id: string, options: AlbumInfoOptions) { private async findOrFail(id: string, options: AlbumInfoOptions) {
@@ -4,8 +4,8 @@
removeUserFromAlbum, removeUserFromAlbum,
type AlbumResponseDto, type AlbumResponseDto,
type UserResponseDto, type UserResponseDto,
updateAlbumUser, updateAlbumUser, AlbumUserRole,
} from '@immich/sdk'; } from '@immich/sdk'
import { mdiDotsVertical } from '@mdi/js'; import { mdiDotsVertical } from '@mdi/js';
import { createEventDispatcher, onMount } from 'svelte'; import { createEventDispatcher, onMount } from 'svelte';
import { getContextMenuPosition } from '../../utils/context-menu'; import { getContextMenuPosition } from '../../utils/context-menu';
@@ -71,10 +71,10 @@
} }
}; };
const handleSetReadonly = async (user: UserResponseDto, readonly: boolean) => { const handleSetReadonly = async (user: UserResponseDto, role: AlbumUserRole) => {
try { try {
await updateAlbumUser({ id: album.id, userId: user.id, updateAlbumUserDto: { readonly } }); await updateAlbumUser({ id: album.id, userId: user.id, updateAlbumUserDto: { role } });
const message = readonly ? `Set ${user.name} as viewer` : `Set ${user.name} as editor`; const message = `Set ${user.name} as ${role}`;
dispatch('refreshAlbum'); dispatch('refreshAlbum');
notificationController.show({ type: NotificationType.Info, message }); notificationController.show({ type: NotificationType.Info, message });
} catch (error) { } catch (error) {
@@ -99,14 +99,14 @@
</div> </div>
</div> </div>
{#each album.sharedUsersV2.toSorted((a, b) => { {#each album.sharedUsersV2.toSorted((a, b) => {
if (a.readonly && !b.readonly) { if (a.role === AlbumUserRole.Viewer && b.role === AlbumUserRole.Editor) {
return 1; return 1;
} }
if (!a.readonly && b.readonly) { if (a.role === AlbumUserRole.Editor && b.role === AlbumUserRole.Viewer) {
return -1; return -1;
} }
return a.user.name.localeCompare(b.user.name); return a.user.name.localeCompare(b.user.name);
}) as { user, readonly }} }) as { user, role }}
<div <div
class="flex w-full place-items-center justify-between gap-4 p-5 transition-colors hover:bg-gray-50 dark:hover:bg-gray-700" class="flex w-full place-items-center justify-between gap-4 p-5 transition-colors hover:bg-gray-50 dark:hover:bg-gray-700"
> >
@@ -117,7 +117,7 @@
<div id="icon-{user.id}" class="flex place-items-center gap-2"> <div id="icon-{user.id}" class="flex place-items-center gap-2">
<div> <div>
{#if readonly} {#if role === AlbumUserRole.Viewer}
Viewer Viewer
{:else} {:else}
Editor Editor
@@ -136,10 +136,10 @@
{#if selectedMenuUser === user} {#if selectedMenuUser === user}
<ContextMenu {...position} on:outclick={() => (selectedMenuUser = null)}> <ContextMenu {...position} on:outclick={() => (selectedMenuUser = null)}>
{#if readonly} {#if role === AlbumUserRole.Viewer}
<MenuOption on:click={() => handleSetReadonly(user, false)} text="Allow edits" /> <MenuOption on:click={() => handleSetReadonly(user, AlbumUserRole.Editor)} text="Allow edits" />
{:else} {:else}
<MenuOption on:click={() => handleSetReadonly(user, true)} text="Disallow edits" /> <MenuOption on:click={() => handleSetReadonly(user, AlbumUserRole.Viewer)} text="Disallow edits" />
{/if} {/if}
<MenuOption on:click={handleMenuRemove} text="Remove" /> <MenuOption on:click={handleMenuRemove} text="Remove" />
</ContextMenu> </ContextMenu>
@@ -1,84 +1,83 @@
<script lang="ts"> <script lang="ts">
import { afterNavigate, goto } from '$app/navigation'; import {afterNavigate, goto} from '$app/navigation'
import AlbumOptions from '$lib/components/album-page/album-options.svelte'; import AlbumDescription from '$lib/components/album-page/album-description.svelte'
import ShareInfoModal from '$lib/components/album-page/share-info-modal.svelte'; import AlbumOptions from '$lib/components/album-page/album-options.svelte'
import UserSelectionModal from '$lib/components/album-page/user-selection-modal.svelte'; import AlbumSummary from '$lib/components/album-page/album-summary.svelte'
import ActivityStatus from '$lib/components/asset-viewer/activity-status.svelte'; import AlbumTitle from '$lib/components/album-page/album-title.svelte'
import ActivityViewer from '$lib/components/asset-viewer/activity-viewer.svelte'; import ShareInfoModal from '$lib/components/album-page/share-info-modal.svelte'
import Button from '$lib/components/elements/buttons/button.svelte'; import UserSelectionModal from '$lib/components/album-page/user-selection-modal.svelte'
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte'; import ActivityStatus from '$lib/components/asset-viewer/activity-status.svelte'
import Icon from '$lib/components/elements/icon.svelte'; import ActivityViewer from '$lib/components/asset-viewer/activity-viewer.svelte'
import AddToAlbum from '$lib/components/photos-page/actions/add-to-album.svelte'; import Button from '$lib/components/elements/buttons/button.svelte'
import ArchiveAction from '$lib/components/photos-page/actions/archive-action.svelte'; import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte'
import ChangeDate from '$lib/components/photos-page/actions/change-date-action.svelte'; import Icon from '$lib/components/elements/icon.svelte'
import ChangeLocation from '$lib/components/photos-page/actions/change-location-action.svelte'; import AddToAlbum from '$lib/components/photos-page/actions/add-to-album.svelte'
import CreateSharedLink from '$lib/components/photos-page/actions/create-shared-link.svelte'; import ArchiveAction from '$lib/components/photos-page/actions/archive-action.svelte'
import DeleteAssets from '$lib/components/photos-page/actions/delete-assets.svelte'; import ChangeDate from '$lib/components/photos-page/actions/change-date-action.svelte'
import DownloadAction from '$lib/components/photos-page/actions/download-action.svelte'; import ChangeLocation from '$lib/components/photos-page/actions/change-location-action.svelte'
import FavoriteAction from '$lib/components/photos-page/actions/favorite-action.svelte'; import CreateSharedLink from '$lib/components/photos-page/actions/create-shared-link.svelte'
import RemoveFromAlbum from '$lib/components/photos-page/actions/remove-from-album.svelte'; import DeleteAssets from '$lib/components/photos-page/actions/delete-assets.svelte'
import SelectAllAssets from '$lib/components/photos-page/actions/select-all-assets.svelte'; import DownloadAction from '$lib/components/photos-page/actions/download-action.svelte'
import AssetGrid from '$lib/components/photos-page/asset-grid.svelte'; import FavoriteAction from '$lib/components/photos-page/actions/favorite-action.svelte'
import AssetSelectContextMenu from '$lib/components/photos-page/asset-select-context-menu.svelte'; import RemoveFromAlbum from '$lib/components/photos-page/actions/remove-from-album.svelte'
import AssetSelectControlBar from '$lib/components/photos-page/asset-select-control-bar.svelte'; import SelectAllAssets from '$lib/components/photos-page/actions/select-all-assets.svelte'
import ConfirmDialogue from '$lib/components/shared-components/confirm-dialogue.svelte'; import AssetGrid from '$lib/components/photos-page/asset-grid.svelte'
import ContextMenu from '$lib/components/shared-components/context-menu/context-menu.svelte'; import AssetSelectContextMenu from '$lib/components/photos-page/asset-select-context-menu.svelte'
import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte'; import AssetSelectControlBar from '$lib/components/photos-page/asset-select-control-bar.svelte'
import ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte'; import ConfirmDialogue from '$lib/components/shared-components/confirm-dialogue.svelte'
import CreateSharedLinkModal from '$lib/components/shared-components/create-share-link-modal/create-shared-link-modal.svelte'; import ContextMenu from '$lib/components/shared-components/context-menu/context-menu.svelte'
import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte'
import ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte'
import CreateSharedLinkModal
from '$lib/components/shared-components/create-share-link-modal/create-shared-link-modal.svelte'
import {notificationController, NotificationType,} from '$lib/components/shared-components/notification/notification'
import UserAvatar from '$lib/components/shared-components/user-avatar.svelte'
import {AppRoute} from '$lib/constants'
import {numberOfComments, setNumberOfComments, updateNumberOfComments} from '$lib/stores/activity.store'
import {createAssetInteractionStore} from '$lib/stores/asset-interaction.store'
import {assetViewingStore} from '$lib/stores/asset-viewing.store'
import {AssetStore} from '$lib/stores/assets.store'
import {locale} from '$lib/stores/preferences.store'
import {SlideshowNavigation, SlideshowState, slideshowStore} from '$lib/stores/slideshow.store'
import {user} from '$lib/stores/user.store'
import {handlePromiseError} from '$lib/utils'
import {downloadAlbum} from '$lib/utils/asset-utils'
import {clickOutside} from '$lib/utils/click-outside'
import {getContextMenuPosition} from '$lib/utils/context-menu'
import {openFileUploadDialog} from '$lib/utils/file-uploader'
import {handleError} from '$lib/utils/handle-error'
import { import {
NotificationType, type ActivityResponseDto,
notificationController,
} from '$lib/components/shared-components/notification/notification';
import UserAvatar from '$lib/components/shared-components/user-avatar.svelte';
import { AppRoute } from '$lib/constants';
import { numberOfComments, setNumberOfComments, updateNumberOfComments } from '$lib/stores/activity.store';
import { createAssetInteractionStore } from '$lib/stores/asset-interaction.store';
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
import { AssetStore } from '$lib/stores/assets.store';
import { locale } from '$lib/stores/preferences.store';
import { SlideshowNavigation, SlideshowState, slideshowStore } from '$lib/stores/slideshow.store';
import { user } from '$lib/stores/user.store';
import { downloadAlbum } from '$lib/utils/asset-utils';
import { clickOutside } from '$lib/utils/click-outside';
import { getContextMenuPosition } from '$lib/utils/context-menu';
import { openFileUploadDialog } from '$lib/utils/file-uploader';
import { handleError } from '$lib/utils/handle-error';
import {
ReactionLevel,
ReactionType,
addAssetsToAlbum, addAssetsToAlbum,
addUsersToAlbum, addUsersToAlbum,
AlbumUserRole,
AssetOrder,
createActivity, createActivity,
deleteActivity, deleteActivity,
deleteAlbum, deleteAlbum,
getActivities, getActivities,
getActivityStatistics, getActivityStatistics,
getAlbumInfo, getAlbumInfo,
ReactionLevel,
ReactionType,
updateAlbumInfo, updateAlbumInfo,
type ActivityResponseDto,
type UserResponseDto, type UserResponseDto,
AssetOrder, } from '@immich/sdk'
} from '@immich/sdk';
import { import {
mdiArrowLeft, mdiArrowLeft,
mdiCogOutline,
mdiDeleteOutline, mdiDeleteOutline,
mdiDotsVertical, mdiDotsVertical,
mdiFolderDownloadOutline, mdiFolderDownloadOutline,
mdiLink,
mdiPlus,
mdiShareVariantOutline,
mdiPresentationPlay,
mdiCogOutline,
mdiImageOutline, mdiImageOutline,
mdiImagePlusOutline, mdiImagePlusOutline,
} from '@mdi/js'; mdiLink,
import { fly } from 'svelte/transition'; mdiPlus,
import type { PageData } from './$types'; mdiPresentationPlay,
import AlbumTitle from '$lib/components/album-page/album-title.svelte'; mdiShareVariantOutline,
import AlbumDescription from '$lib/components/album-page/album-description.svelte'; } from '@mdi/js'
import { handlePromiseError } from '$lib/utils'; import {fly} from 'svelte/transition'
import AlbumSummary from '$lib/components/album-page/album-summary.svelte'; import type {PageData} from './$types'
export let data: PageData; export let data: PageData;
@@ -136,8 +135,8 @@
$: showActivityStatus = $: showActivityStatus =
album.sharedUsers.length > 0 && !$showAssetViewer && (album.isActivityEnabled || $numberOfComments > 0); album.sharedUsers.length > 0 && !$showAssetViewer && (album.isActivityEnabled || $numberOfComments > 0);
$: userHasWriteAccess = !album.sharedUsersV2.find(({ user: { id } }) => id === $user.id)?.readonly; $: isEditor = album.sharedUsersV2.find(({ user: { id } }) => id === $user.id)?.role === AlbumUserRole.Editor;
$: albumHasReadonlyUsers = album.sharedUsersV2.some(({ readonly }) => readonly); $: albumHasViewers = album.sharedUsersV2.some(({ role }) => role === AlbumUserRole.Viewer);
afterNavigate(({ from }) => { afterNavigate(({ from }) => {
assetViewingStore.showAssetViewer(false); assetViewingStore.showAssetViewer(false);
@@ -436,7 +435,7 @@
{#if viewMode === ViewMode.VIEW || viewMode === ViewMode.ALBUM_OPTIONS} {#if viewMode === ViewMode.VIEW || viewMode === ViewMode.ALBUM_OPTIONS}
<ControlAppBar showBackButton backIcon={mdiArrowLeft} on:close={() => goto(backUrl)}> <ControlAppBar showBackButton backIcon={mdiArrowLeft} on:close={() => goto(backUrl)}>
<svelte:fragment slot="trailing"> <svelte:fragment slot="trailing">
{#if userHasWriteAccess} {#if isEditor}
<CircleIconButton <CircleIconButton
title="Add photos" title="Add photos"
on:click={() => (viewMode = ViewMode.SELECT_ASSETS)} on:click={() => (viewMode = ViewMode.SELECT_ASSETS)}
@@ -584,14 +583,14 @@
</button> </button>
<!-- users with write access (collaborators) --> <!-- users with write access (collaborators) -->
{#each album.sharedUsersV2.filter(({ readonly }) => !readonly) as { user } (user.id)} {#each album.sharedUsersV2.filter(({ role }) => role === AlbumUserRole.Editor) as { user } (user.id)}
<button on:click={() => (viewMode = ViewMode.VIEW_USERS)}> <button on:click={() => (viewMode = ViewMode.VIEW_USERS)}>
<UserAvatar {user} size="md" /> <UserAvatar {user} size="md" />
</button> </button>
{/each} {/each}
<!-- display ellipsis if there are readonly users too --> <!-- display ellipsis if there are readonly users too -->
{#if albumHasReadonlyUsers} {#if albumHasViewers}
<CircleIconButton <CircleIconButton
title="View all users" title="View all users"
backgroundColor="#d3d3d3" backgroundColor="#d3d3d3"