feat(server, web): Album's options (#4870)
* feat: disable activity * fix: disable reactions * fix: tests * fix: tests * fix: tests * pr feedback * pr feedback * chore: styling & wording * refactor component --------- Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
This commit is contained in:
parent
ace0a5911c
commit
9d01885b58
29 changed files with 293 additions and 24 deletions
12
cli/src/api/open-api/api.ts
generated
12
cli/src/api/open-api/api.ts
generated
|
@ -331,6 +331,12 @@ export interface AlbumResponseDto {
|
|||
* @memberof AlbumResponseDto
|
||||
*/
|
||||
'id': string;
|
||||
/**
|
||||
*
|
||||
* @type {boolean}
|
||||
* @memberof AlbumResponseDto
|
||||
*/
|
||||
'isActivityEnabled': boolean;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
|
@ -4160,6 +4166,12 @@ export interface UpdateAlbumDto {
|
|||
* @memberof UpdateAlbumDto
|
||||
*/
|
||||
'description'?: string;
|
||||
/**
|
||||
*
|
||||
* @type {boolean}
|
||||
* @memberof UpdateAlbumDto
|
||||
*/
|
||||
'isActivityEnabled'?: boolean;
|
||||
}
|
||||
/**
|
||||
*
|
||||
|
|
1
mobile/openapi/doc/AlbumResponseDto.md
generated
1
mobile/openapi/doc/AlbumResponseDto.md
generated
|
@ -17,6 +17,7 @@ Name | Type | Description | Notes
|
|||
**endDate** | [**DateTime**](DateTime.md) | | [optional]
|
||||
**hasSharedLink** | **bool** | |
|
||||
**id** | **String** | |
|
||||
**isActivityEnabled** | **bool** | |
|
||||
**lastModifiedAssetTimestamp** | [**DateTime**](DateTime.md) | | [optional]
|
||||
**owner** | [**UserResponseDto**](UserResponseDto.md) | |
|
||||
**ownerId** | **String** | |
|
||||
|
|
1
mobile/openapi/doc/UpdateAlbumDto.md
generated
1
mobile/openapi/doc/UpdateAlbumDto.md
generated
|
@ -11,6 +11,7 @@ Name | Type | Description | Notes
|
|||
**albumName** | **String** | | [optional]
|
||||
**albumThumbnailAssetId** | **String** | | [optional]
|
||||
**description** | **String** | | [optional]
|
||||
**isActivityEnabled** | **bool** | | [optional]
|
||||
|
||||
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
|
||||
|
||||
|
|
10
mobile/openapi/lib/model/album_response_dto.dart
generated
10
mobile/openapi/lib/model/album_response_dto.dart
generated
|
@ -22,6 +22,7 @@ class AlbumResponseDto {
|
|||
this.endDate,
|
||||
required this.hasSharedLink,
|
||||
required this.id,
|
||||
required this.isActivityEnabled,
|
||||
this.lastModifiedAssetTimestamp,
|
||||
required this.owner,
|
||||
required this.ownerId,
|
||||
|
@ -55,6 +56,8 @@ class AlbumResponseDto {
|
|||
|
||||
String id;
|
||||
|
||||
bool isActivityEnabled;
|
||||
|
||||
///
|
||||
/// Please note: This property should have been non-nullable! Since the specification file
|
||||
/// does not include a default value (using the "default:" property), however, the generated
|
||||
|
@ -92,6 +95,7 @@ class AlbumResponseDto {
|
|||
other.endDate == endDate &&
|
||||
other.hasSharedLink == hasSharedLink &&
|
||||
other.id == id &&
|
||||
other.isActivityEnabled == isActivityEnabled &&
|
||||
other.lastModifiedAssetTimestamp == lastModifiedAssetTimestamp &&
|
||||
other.owner == owner &&
|
||||
other.ownerId == ownerId &&
|
||||
|
@ -112,6 +116,7 @@ class AlbumResponseDto {
|
|||
(endDate == null ? 0 : endDate!.hashCode) +
|
||||
(hasSharedLink.hashCode) +
|
||||
(id.hashCode) +
|
||||
(isActivityEnabled.hashCode) +
|
||||
(lastModifiedAssetTimestamp == null ? 0 : lastModifiedAssetTimestamp!.hashCode) +
|
||||
(owner.hashCode) +
|
||||
(ownerId.hashCode) +
|
||||
|
@ -121,7 +126,7 @@ class AlbumResponseDto {
|
|||
(updatedAt.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'AlbumResponseDto[albumName=$albumName, albumThumbnailAssetId=$albumThumbnailAssetId, assetCount=$assetCount, assets=$assets, createdAt=$createdAt, description=$description, endDate=$endDate, hasSharedLink=$hasSharedLink, id=$id, lastModifiedAssetTimestamp=$lastModifiedAssetTimestamp, owner=$owner, ownerId=$ownerId, shared=$shared, sharedUsers=$sharedUsers, startDate=$startDate, updatedAt=$updatedAt]';
|
||||
String toString() => 'AlbumResponseDto[albumName=$albumName, albumThumbnailAssetId=$albumThumbnailAssetId, assetCount=$assetCount, assets=$assets, createdAt=$createdAt, description=$description, endDate=$endDate, hasSharedLink=$hasSharedLink, id=$id, isActivityEnabled=$isActivityEnabled, lastModifiedAssetTimestamp=$lastModifiedAssetTimestamp, owner=$owner, ownerId=$ownerId, shared=$shared, sharedUsers=$sharedUsers, startDate=$startDate, updatedAt=$updatedAt]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
|
@ -142,6 +147,7 @@ class AlbumResponseDto {
|
|||
}
|
||||
json[r'hasSharedLink'] = this.hasSharedLink;
|
||||
json[r'id'] = this.id;
|
||||
json[r'isActivityEnabled'] = this.isActivityEnabled;
|
||||
if (this.lastModifiedAssetTimestamp != null) {
|
||||
json[r'lastModifiedAssetTimestamp'] = this.lastModifiedAssetTimestamp!.toUtc().toIso8601String();
|
||||
} else {
|
||||
|
@ -177,6 +183,7 @@ class AlbumResponseDto {
|
|||
endDate: mapDateTime(json, r'endDate', ''),
|
||||
hasSharedLink: mapValueOfType<bool>(json, r'hasSharedLink')!,
|
||||
id: mapValueOfType<String>(json, r'id')!,
|
||||
isActivityEnabled: mapValueOfType<bool>(json, r'isActivityEnabled')!,
|
||||
lastModifiedAssetTimestamp: mapDateTime(json, r'lastModifiedAssetTimestamp', ''),
|
||||
owner: UserResponseDto.fromJson(json[r'owner'])!,
|
||||
ownerId: mapValueOfType<String>(json, r'ownerId')!,
|
||||
|
@ -239,6 +246,7 @@ class AlbumResponseDto {
|
|||
'description',
|
||||
'hasSharedLink',
|
||||
'id',
|
||||
'isActivityEnabled',
|
||||
'owner',
|
||||
'ownerId',
|
||||
'shared',
|
||||
|
|
23
mobile/openapi/lib/model/update_album_dto.dart
generated
23
mobile/openapi/lib/model/update_album_dto.dart
generated
|
@ -16,6 +16,7 @@ class UpdateAlbumDto {
|
|||
this.albumName,
|
||||
this.albumThumbnailAssetId,
|
||||
this.description,
|
||||
this.isActivityEnabled,
|
||||
});
|
||||
|
||||
///
|
||||
|
@ -42,21 +43,31 @@ class UpdateAlbumDto {
|
|||
///
|
||||
String? description;
|
||||
|
||||
///
|
||||
/// Please note: This property should have been non-nullable! Since the specification file
|
||||
/// does not include a default value (using the "default:" property), however, the generated
|
||||
/// source code must fall back to having a nullable type.
|
||||
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||
///
|
||||
bool? isActivityEnabled;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is UpdateAlbumDto &&
|
||||
other.albumName == albumName &&
|
||||
other.albumThumbnailAssetId == albumThumbnailAssetId &&
|
||||
other.description == description;
|
||||
other.description == description &&
|
||||
other.isActivityEnabled == isActivityEnabled;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(albumName == null ? 0 : albumName!.hashCode) +
|
||||
(albumThumbnailAssetId == null ? 0 : albumThumbnailAssetId!.hashCode) +
|
||||
(description == null ? 0 : description!.hashCode);
|
||||
(description == null ? 0 : description!.hashCode) +
|
||||
(isActivityEnabled == null ? 0 : isActivityEnabled!.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'UpdateAlbumDto[albumName=$albumName, albumThumbnailAssetId=$albumThumbnailAssetId, description=$description]';
|
||||
String toString() => 'UpdateAlbumDto[albumName=$albumName, albumThumbnailAssetId=$albumThumbnailAssetId, description=$description, isActivityEnabled=$isActivityEnabled]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
|
@ -75,6 +86,11 @@ class UpdateAlbumDto {
|
|||
} else {
|
||||
// json[r'description'] = null;
|
||||
}
|
||||
if (this.isActivityEnabled != null) {
|
||||
json[r'isActivityEnabled'] = this.isActivityEnabled;
|
||||
} else {
|
||||
// json[r'isActivityEnabled'] = null;
|
||||
}
|
||||
return json;
|
||||
}
|
||||
|
||||
|
@ -89,6 +105,7 @@ class UpdateAlbumDto {
|
|||
albumName: mapValueOfType<String>(json, r'albumName'),
|
||||
albumThumbnailAssetId: mapValueOfType<String>(json, r'albumThumbnailAssetId'),
|
||||
description: mapValueOfType<String>(json, r'description'),
|
||||
isActivityEnabled: mapValueOfType<bool>(json, r'isActivityEnabled'),
|
||||
);
|
||||
}
|
||||
return null;
|
||||
|
|
5
mobile/openapi/test/album_response_dto_test.dart
generated
5
mobile/openapi/test/album_response_dto_test.dart
generated
|
@ -61,6 +61,11 @@ void main() {
|
|||
// TODO
|
||||
});
|
||||
|
||||
// bool isActivityEnabled
|
||||
test('to test the property `isActivityEnabled`', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
// DateTime lastModifiedAssetTimestamp
|
||||
test('to test the property `lastModifiedAssetTimestamp`', () async {
|
||||
// TODO
|
||||
|
|
5
mobile/openapi/test/update_album_dto_test.dart
generated
5
mobile/openapi/test/update_album_dto_test.dart
generated
|
@ -31,6 +31,11 @@ void main() {
|
|||
// TODO
|
||||
});
|
||||
|
||||
// bool isActivityEnabled
|
||||
test('to test the property `isActivityEnabled`', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
|
|
|
@ -5894,6 +5894,9 @@
|
|||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"isActivityEnabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"lastModifiedAssetTimestamp": {
|
||||
"format": "date-time",
|
||||
"type": "string"
|
||||
|
@ -5935,7 +5938,8 @@
|
|||
"sharedUsers",
|
||||
"hasSharedLink",
|
||||
"assets",
|
||||
"owner"
|
||||
"owner",
|
||||
"isActivityEnabled"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
|
@ -8910,6 +8914,9 @@
|
|||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"isActivityEnabled": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
|
|
|
@ -138,10 +138,7 @@ export class AccessCore {
|
|||
switch (permission) {
|
||||
// uses album id
|
||||
case Permission.ACTIVITY_CREATE:
|
||||
return (
|
||||
(await this.repository.album.hasOwnerAccess(authUser.id, id)) ||
|
||||
(await this.repository.album.hasSharedAlbumAccess(authUser.id, id))
|
||||
);
|
||||
return await this.repository.activity.hasCreateAccess(authUser.id, id);
|
||||
|
||||
// uses activity id
|
||||
case Permission.ACTIVITY_DELETE:
|
||||
|
|
|
@ -94,7 +94,7 @@ describe(ActivityService.name, () => {
|
|||
});
|
||||
|
||||
it('should create a comment', async () => {
|
||||
accessMock.album.hasOwnerAccess.mockResolvedValue(true);
|
||||
accessMock.activity.hasCreateAccess.mockResolvedValue(true);
|
||||
activityMock.create.mockResolvedValue(activityStub.oneComment);
|
||||
|
||||
await sut.create(authStub.admin, {
|
||||
|
@ -113,8 +113,23 @@ describe(ActivityService.name, () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('should create a like', async () => {
|
||||
it('should fail because activity is disabled for the album', async () => {
|
||||
accessMock.album.hasOwnerAccess.mockResolvedValue(true);
|
||||
accessMock.activity.hasCreateAccess.mockResolvedValue(false);
|
||||
activityMock.create.mockResolvedValue(activityStub.oneComment);
|
||||
|
||||
await expect(
|
||||
sut.create(authStub.admin, {
|
||||
albumId: 'album-id',
|
||||
assetId: 'asset-id',
|
||||
type: ReactionType.COMMENT,
|
||||
comment: 'comment',
|
||||
}),
|
||||
).rejects.toBeInstanceOf(BadRequestException);
|
||||
});
|
||||
|
||||
it('should create a like', async () => {
|
||||
accessMock.activity.hasCreateAccess.mockResolvedValue(true);
|
||||
activityMock.create.mockResolvedValue(activityStub.liked);
|
||||
activityMock.search.mockResolvedValue([]);
|
||||
|
||||
|
@ -134,6 +149,7 @@ describe(ActivityService.name, () => {
|
|||
|
||||
it('should skip if like exists', async () => {
|
||||
accessMock.album.hasOwnerAccess.mockResolvedValue(true);
|
||||
accessMock.activity.hasCreateAccess.mockResolvedValue(true);
|
||||
activityMock.search.mockResolvedValue([activityStub.liked]);
|
||||
|
||||
await sut.create(authStub.admin, {
|
||||
|
|
|
@ -21,6 +21,7 @@ export class AlbumResponseDto {
|
|||
lastModifiedAssetTimestamp?: Date;
|
||||
startDate?: Date;
|
||||
endDate?: Date;
|
||||
isActivityEnabled!: boolean;
|
||||
}
|
||||
|
||||
export const mapAlbum = (entity: AlbumEntity, withAssets: boolean): AlbumResponseDto => {
|
||||
|
@ -61,6 +62,7 @@ export const mapAlbum = (entity: AlbumEntity, withAssets: boolean): AlbumRespons
|
|||
endDate,
|
||||
assets: (withAssets ? assets : []).map((asset) => mapAsset(asset)),
|
||||
assetCount: entity.assets?.length || 0,
|
||||
isActivityEnabled: entity.isActivityEnabled,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -125,12 +125,12 @@ export class AlbumService {
|
|||
throw new BadRequestException('Invalid album thumbnail');
|
||||
}
|
||||
}
|
||||
|
||||
const updatedAlbum = await this.albumRepository.update({
|
||||
id: album.id,
|
||||
albumName: dto.albumName,
|
||||
description: dto.description,
|
||||
albumThumbnailAssetId: dto.albumThumbnailAssetId,
|
||||
isActivityEnabled: dto.isActivityEnabled,
|
||||
});
|
||||
|
||||
await this.jobRepository.queue({ name: JobName.SEARCH_INDEX_ALBUM, data: { ids: [updatedAlbum.id] } });
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { IsString } from 'class-validator';
|
||||
import { IsBoolean, IsString } from 'class-validator';
|
||||
import { Optional, ValidateUUID } from '../../domain.util';
|
||||
|
||||
export class UpdateAlbumDto {
|
||||
|
@ -12,4 +12,8 @@ export class UpdateAlbumDto {
|
|||
|
||||
@ValidateUUID({ optional: true })
|
||||
albumThumbnailAssetId?: string;
|
||||
|
||||
@Optional()
|
||||
@IsBoolean()
|
||||
isActivityEnabled?: boolean;
|
||||
}
|
||||
|
|
|
@ -2,8 +2,9 @@ export const IAccessRepository = 'IAccessRepository';
|
|||
|
||||
export interface IAccessRepository {
|
||||
activity: {
|
||||
hasOwnerAccess(userId: string, albumId: string): Promise<boolean>;
|
||||
hasAlbumOwnerAccess(userId: string, albumId: string): Promise<boolean>;
|
||||
hasOwnerAccess(userId: string, activityId: string): Promise<boolean>;
|
||||
hasAlbumOwnerAccess(userId: string, activityId: string): Promise<boolean>;
|
||||
hasCreateAccess(userId: string, albumId: string): Promise<boolean>;
|
||||
};
|
||||
asset: {
|
||||
hasOwnerAccess(userId: string, assetId: string): Promise<boolean>;
|
||||
|
|
|
@ -56,4 +56,7 @@ export class AlbumEntity {
|
|||
|
||||
@OneToMany(() => SharedLinkEntity, (link) => link.album)
|
||||
sharedLinks!: SharedLinkEntity[];
|
||||
|
||||
@Column({ default: true })
|
||||
isActivityEnabled!: boolean;
|
||||
}
|
||||
|
|
14
server/src/infra/migrations/1699268680508-DisableActivity.ts
Normal file
14
server/src/infra/migrations/1699268680508-DisableActivity.ts
Normal file
|
@ -0,0 +1,14 @@
|
|||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class DisableActivity1699268680508 implements MigrationInterface {
|
||||
name = 'DisableActivity1699268680508'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "albums" ADD "isActivityEnabled" boolean NOT NULL DEFAULT true`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "albums" DROP COLUMN "isActivityEnabled"`);
|
||||
}
|
||||
|
||||
}
|
|
@ -43,6 +43,24 @@ export class AccessRepository implements IAccessRepository {
|
|||
},
|
||||
});
|
||||
},
|
||||
hasCreateAccess: (userId: string, albumId: string): Promise<boolean> => {
|
||||
return this.albumRepository.exist({
|
||||
where: [
|
||||
{
|
||||
id: albumId,
|
||||
isActivityEnabled: true,
|
||||
sharedUsers: {
|
||||
id: userId,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: albumId,
|
||||
isActivityEnabled: true,
|
||||
ownerId: userId,
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
};
|
||||
library = {
|
||||
hasOwnerAccess: (userId: string, libraryId: string): Promise<boolean> => {
|
||||
|
|
|
@ -226,6 +226,7 @@ describe(`${AlbumController.name} (e2e)`, () => {
|
|||
assets: [],
|
||||
assetCount: 0,
|
||||
owner: expect.objectContaining({ email: user1.userEmail }),
|
||||
isActivityEnabled: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
10
server/test/fixtures/album.stub.ts
vendored
10
server/test/fixtures/album.stub.ts
vendored
|
@ -18,6 +18,7 @@ export const albumStub = {
|
|||
deletedAt: null,
|
||||
sharedLinks: [],
|
||||
sharedUsers: [],
|
||||
isActivityEnabled: true,
|
||||
}),
|
||||
sharedWithUser: Object.freeze<AlbumEntity>({
|
||||
id: 'album-2',
|
||||
|
@ -33,6 +34,7 @@ export const albumStub = {
|
|||
deletedAt: null,
|
||||
sharedLinks: [],
|
||||
sharedUsers: [userStub.user1],
|
||||
isActivityEnabled: true,
|
||||
}),
|
||||
sharedWithMultiple: Object.freeze<AlbumEntity>({
|
||||
id: 'album-3',
|
||||
|
@ -48,6 +50,7 @@ export const albumStub = {
|
|||
deletedAt: null,
|
||||
sharedLinks: [],
|
||||
sharedUsers: [userStub.user1, userStub.user2],
|
||||
isActivityEnabled: true,
|
||||
}),
|
||||
sharedWithAdmin: Object.freeze<AlbumEntity>({
|
||||
id: 'album-3',
|
||||
|
@ -63,6 +66,7 @@ export const albumStub = {
|
|||
deletedAt: null,
|
||||
sharedLinks: [],
|
||||
sharedUsers: [userStub.admin],
|
||||
isActivityEnabled: true,
|
||||
}),
|
||||
oneAsset: Object.freeze<AlbumEntity>({
|
||||
id: 'album-4',
|
||||
|
@ -78,6 +82,7 @@ export const albumStub = {
|
|||
deletedAt: null,
|
||||
sharedLinks: [],
|
||||
sharedUsers: [],
|
||||
isActivityEnabled: true,
|
||||
}),
|
||||
twoAssets: Object.freeze<AlbumEntity>({
|
||||
id: 'album-4a',
|
||||
|
@ -93,6 +98,7 @@ export const albumStub = {
|
|||
deletedAt: null,
|
||||
sharedLinks: [],
|
||||
sharedUsers: [],
|
||||
isActivityEnabled: true,
|
||||
}),
|
||||
emptyWithInvalidThumbnail: Object.freeze<AlbumEntity>({
|
||||
id: 'album-5',
|
||||
|
@ -108,6 +114,7 @@ export const albumStub = {
|
|||
deletedAt: null,
|
||||
sharedLinks: [],
|
||||
sharedUsers: [],
|
||||
isActivityEnabled: true,
|
||||
}),
|
||||
emptyWithValidThumbnail: Object.freeze<AlbumEntity>({
|
||||
id: 'album-5',
|
||||
|
@ -123,6 +130,7 @@ export const albumStub = {
|
|||
deletedAt: null,
|
||||
sharedLinks: [],
|
||||
sharedUsers: [],
|
||||
isActivityEnabled: true,
|
||||
}),
|
||||
oneAssetInvalidThumbnail: Object.freeze<AlbumEntity>({
|
||||
id: 'album-6',
|
||||
|
@ -138,6 +146,7 @@ export const albumStub = {
|
|||
deletedAt: null,
|
||||
sharedLinks: [],
|
||||
sharedUsers: [],
|
||||
isActivityEnabled: true,
|
||||
}),
|
||||
oneAssetValidThumbnail: Object.freeze<AlbumEntity>({
|
||||
id: 'album-6',
|
||||
|
@ -153,5 +162,6 @@ export const albumStub = {
|
|||
deletedAt: null,
|
||||
sharedLinks: [],
|
||||
sharedUsers: [],
|
||||
isActivityEnabled: true,
|
||||
}),
|
||||
};
|
||||
|
|
2
server/test/fixtures/shared-link.stub.ts
vendored
2
server/test/fixtures/shared-link.stub.ts
vendored
|
@ -100,6 +100,7 @@ const albumResponse: AlbumResponseDto = {
|
|||
hasSharedLink: false,
|
||||
assets: [],
|
||||
assetCount: 1,
|
||||
isActivityEnabled: true,
|
||||
};
|
||||
|
||||
export const sharedLinkStub = {
|
||||
|
@ -179,6 +180,7 @@ export const sharedLinkStub = {
|
|||
albumThumbnailAssetId: null,
|
||||
sharedUsers: [],
|
||||
sharedLinks: [],
|
||||
isActivityEnabled: true,
|
||||
assets: [
|
||||
{
|
||||
id: 'id_1',
|
||||
|
|
|
@ -19,6 +19,7 @@ export const newAccessRepositoryMock = (reset = true): IAccessRepositoryMock =>
|
|||
activity: {
|
||||
hasOwnerAccess: jest.fn(),
|
||||
hasAlbumOwnerAccess: jest.fn(),
|
||||
hasCreateAccess: jest.fn(),
|
||||
},
|
||||
asset: {
|
||||
hasOwnerAccess: jest.fn(),
|
||||
|
|
12
web/src/api/open-api/api.ts
generated
12
web/src/api/open-api/api.ts
generated
|
@ -331,6 +331,12 @@ export interface AlbumResponseDto {
|
|||
* @memberof AlbumResponseDto
|
||||
*/
|
||||
'id': string;
|
||||
/**
|
||||
*
|
||||
* @type {boolean}
|
||||
* @memberof AlbumResponseDto
|
||||
*/
|
||||
'isActivityEnabled': boolean;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
|
@ -4160,6 +4166,12 @@ export interface UpdateAlbumDto {
|
|||
* @memberof UpdateAlbumDto
|
||||
*/
|
||||
'description'?: string;
|
||||
/**
|
||||
*
|
||||
* @type {boolean}
|
||||
* @memberof UpdateAlbumDto
|
||||
*/
|
||||
'isActivityEnabled'?: boolean;
|
||||
}
|
||||
/**
|
||||
*
|
||||
|
|
76
web/src/lib/components/album-page/album-options.svelte
Normal file
76
web/src/lib/components/album-page/album-options.svelte
Normal file
|
@ -0,0 +1,76 @@
|
|||
<script lang="ts">
|
||||
import { mdiClose, mdiPlus } from '@mdi/js';
|
||||
import SettingSwitch from '../admin-page/settings/setting-switch.svelte';
|
||||
import CircleIconButton from '../elements/buttons/circle-icon-button.svelte';
|
||||
import FullScreenModal from '../shared-components/full-screen-modal.svelte';
|
||||
import UserAvatar from '../shared-components/user-avatar.svelte';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import type { AlbumResponseDto, UserResponseDto } from '../../../api/open-api';
|
||||
import Icon from '$lib/components/elements/icon.svelte';
|
||||
|
||||
export let album: AlbumResponseDto;
|
||||
export let user: UserResponseDto;
|
||||
|
||||
const dispatch = createEventDispatcher<{
|
||||
close: void;
|
||||
toggleEnableActivity: void;
|
||||
showSelectSharedUser: void;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<FullScreenModal on:clickOutside={() => dispatch('close')}>
|
||||
<div class="flex h-full w-full place-content-center place-items-center overflow-hidden p-2 md:p-0">
|
||||
<div
|
||||
class="w-[550px] rounded-3xl border bg-immich-bg shadow-sm dark:border-immich-dark-gray dark:bg-immich-dark-gray dark:text-immich-dark-fg"
|
||||
>
|
||||
<div class="px-2 pt-2">
|
||||
<div class="flex items-center">
|
||||
<h1 class="px-4 w-full self-center font-medium text-immich-primary dark:text-immich-dark-primary">Options</h1>
|
||||
<div>
|
||||
<CircleIconButton icon={mdiClose} on:click={() => dispatch('close')} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class=" items-center justify-center p-4">
|
||||
<div class="py-2">
|
||||
<h2 class="text-gray text-sm mb-3">SHARING</h2>
|
||||
<div class="p-2">
|
||||
<SettingSwitch
|
||||
title="Comments & likes"
|
||||
subtitle="Let others respond"
|
||||
checked={album.isActivityEnabled}
|
||||
on:toggle={() => dispatch('toggleEnableActivity')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="py-2">
|
||||
<div class="text-gray text-sm mb-3">PEOPLE</div>
|
||||
<div class="p-2">
|
||||
<button class="flex items-center gap-2" on:click={() => dispatch('showSelectSharedUser')}>
|
||||
<div class="rounded-full w-10 h-10 border border-gray-500 flex items-center justify-center">
|
||||
<div><Icon path={mdiPlus} size="25" /></div>
|
||||
</div>
|
||||
<div>Invite People</div>
|
||||
</button>
|
||||
<div class="flex items-center gap-2 py-2 mt-2">
|
||||
<div>
|
||||
<UserAvatar {user} size="md" />
|
||||
</div>
|
||||
<div class="w-full">{`${user.firstName} ${user.lastName}`}</div>
|
||||
<div>Owner</div>
|
||||
</div>
|
||||
{#each album.sharedUsers as user (user.id)}
|
||||
<div class="flex items-center gap-2 py-2">
|
||||
<div>
|
||||
<UserAvatar {user} size="md" />
|
||||
</div>
|
||||
<div class="w-full">{`${user.firstName} ${user.lastName}`}</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</FullScreenModal>
|
|
@ -7,6 +7,7 @@
|
|||
export let isLiked: ActivityResponseDto | null;
|
||||
export let numberOfComments: number | undefined;
|
||||
export let isShowActivity: boolean | undefined;
|
||||
export let disabled: boolean;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
</script>
|
||||
|
@ -14,7 +15,7 @@
|
|||
<div
|
||||
class="w-full h-14 flex p-4 text-white items-center justify-center rounded-full gap-4 bg-immich-dark-bg bg-opacity-60"
|
||||
>
|
||||
<button on:click={() => dispatch('favorite')}>
|
||||
<button class={disabled ? 'cursor-not-allowed' : ''} on:click={() => dispatch('favorite')} {disabled}>
|
||||
<!-- svelte-ignore missing-declaration -->
|
||||
<div class="items-center justify-center">
|
||||
<Icon path={isLiked ? mdiHeart : mdiHeartOutline} size={24} />
|
||||
|
|
|
@ -38,6 +38,7 @@
|
|||
export let albumId: string;
|
||||
export let assetType: AssetTypeEnum | undefined = undefined;
|
||||
export let albumOwnerId: string;
|
||||
export let disabled: boolean;
|
||||
|
||||
let textArea: HTMLTextAreaElement;
|
||||
let innerHeight: number;
|
||||
|
@ -280,12 +281,15 @@
|
|||
<form class="flex w-full max-h-56 gap-1" on:submit|preventDefault={() => handleSendComment()}>
|
||||
<div class="flex w-full items-center gap-4">
|
||||
<textarea
|
||||
{disabled}
|
||||
bind:this={textArea}
|
||||
bind:value={message}
|
||||
placeholder="Say something"
|
||||
placeholder={disabled ? 'Comments are disabled' : 'Say something'}
|
||||
on:input={autoGrow}
|
||||
on:keypress={handleEnter}
|
||||
class="h-[18px] w-full max-h-56 pr-2 items-center overflow-y-auto leading-4 outline-none resize-none bg-gray-200"
|
||||
class="h-[18px] {disabled
|
||||
? 'cursor-not-allowed'
|
||||
: ''} w-full max-h-56 pr-2 items-center overflow-y-auto leading-4 outline-none resize-none bg-gray-200"
|
||||
/>
|
||||
</div>
|
||||
{#if isSendingMessage}
|
||||
|
|
|
@ -104,6 +104,12 @@
|
|||
}
|
||||
}
|
||||
|
||||
$: {
|
||||
if (album && !album.isActivityEnabled && numberOfComments === 0) {
|
||||
isShowActivity = false;
|
||||
}
|
||||
}
|
||||
|
||||
const handleAddComment = () => {
|
||||
numberOfComments++;
|
||||
updateNumberOfComments(1);
|
||||
|
@ -115,7 +121,7 @@
|
|||
};
|
||||
|
||||
const handleFavorite = async () => {
|
||||
if (album) {
|
||||
if (album && album.isActivityEnabled) {
|
||||
try {
|
||||
if (isLiked) {
|
||||
const activityId = isLiked.id;
|
||||
|
@ -661,9 +667,10 @@
|
|||
on:onVideoStarted={handleVideoStarted}
|
||||
/>
|
||||
{/if}
|
||||
{#if $slideshowState === SlideshowState.None && isShared}
|
||||
{#if $slideshowState === SlideshowState.None && isShared && ((album && album.isActivityEnabled) || numberOfComments > 0)}
|
||||
<div class="z-[9999] absolute bottom-0 right-0 mb-6 mr-6 justify-self-end">
|
||||
<ActivityStatus
|
||||
disabled={!album?.isActivityEnabled}
|
||||
{isLiked}
|
||||
{numberOfComments}
|
||||
{isShowActivity}
|
||||
|
@ -744,6 +751,7 @@
|
|||
>
|
||||
<ActivityViewer
|
||||
{user}
|
||||
disabled={!album.isActivityEnabled}
|
||||
assetType={asset.type}
|
||||
albumOwnerId={album.ownerId}
|
||||
albumId={album.id}
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import { writable } from 'svelte/store';
|
||||
|
||||
export const numberOfComments = writable<number | undefined>(undefined);
|
||||
export const numberOfComments = writable<number>(0);
|
||||
|
||||
export const setNumberOfComments = (number: number) => {
|
||||
numberOfComments.set(number);
|
||||
};
|
||||
|
||||
export const updateNumberOfComments = (addOrRemove: 1 | -1) => {
|
||||
numberOfComments.update((n) => (n ? n + addOrRemove : undefined));
|
||||
numberOfComments.update((n) => n + addOrRemove);
|
||||
};
|
||||
|
|
|
@ -55,6 +55,7 @@
|
|||
import ActivityViewer from '$lib/components/asset-viewer/activity-viewer.svelte';
|
||||
import ActivityStatus from '$lib/components/asset-viewer/activity-status.svelte';
|
||||
import { numberOfComments, setNumberOfComments, updateNumberOfComments } from '$lib/stores/activity.store';
|
||||
import AlbumOptions from '$lib/components/album-page/album-options.svelte';
|
||||
|
||||
export let data: PageData;
|
||||
|
||||
|
@ -64,6 +65,12 @@
|
|||
let album = data.album;
|
||||
$: album = data.album;
|
||||
|
||||
$: {
|
||||
if (!album.isActivityEnabled && $numberOfComments === 0) {
|
||||
isShowActivity = false;
|
||||
}
|
||||
}
|
||||
|
||||
enum ViewMode {
|
||||
CONFIRM_DELETE = 'confirm-delete',
|
||||
LINK_SHARING = 'link-sharing',
|
||||
|
@ -73,6 +80,7 @@
|
|||
ALBUM_OPTIONS = 'album-options',
|
||||
VIEW_USERS = 'view-users',
|
||||
VIEW = 'view',
|
||||
OPTIONS = 'options',
|
||||
}
|
||||
|
||||
let backUrl: string = AppRoute.ALBUMS;
|
||||
|
@ -107,6 +115,8 @@
|
|||
assetGridWidth = globalWidth;
|
||||
}
|
||||
}
|
||||
$: showActivityStatus =
|
||||
album.sharedUsers.length > 0 && !$showAssetViewer && (album.isActivityEnabled || $numberOfComments > 0);
|
||||
|
||||
afterNavigate(({ from }) => {
|
||||
assetViewingStore.showAssetViewer(false);
|
||||
|
@ -128,6 +138,24 @@
|
|||
}
|
||||
});
|
||||
|
||||
const handleToggleEnableActivity = async () => {
|
||||
try {
|
||||
const { data } = await api.albumApi.updateAlbumInfo({
|
||||
id: album.id,
|
||||
updateAlbumDto: {
|
||||
isActivityEnabled: !album.isActivityEnabled,
|
||||
},
|
||||
});
|
||||
album = data;
|
||||
notificationController.show({
|
||||
type: NotificationType.Info,
|
||||
message: `Activity is ${album.isActivityEnabled ? 'enabled' : 'disabled'}`,
|
||||
});
|
||||
} catch (error) {
|
||||
handleError(error, `Can't ${!album.isActivityEnabled ? 'enable' : 'disable'} activity`);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFavorite = async () => {
|
||||
try {
|
||||
if (isLiked) {
|
||||
|
@ -374,6 +402,7 @@
|
|||
},
|
||||
});
|
||||
currentAlbumName = album.albumName;
|
||||
notificationController.show({ type: NotificationType.Info, message: 'New album name has been saved' });
|
||||
} catch (error) {
|
||||
handleError(error, 'Unable to update album name');
|
||||
}
|
||||
|
@ -455,6 +484,7 @@
|
|||
<MenuOption on:click={handleStartSlideshow} text="Slideshow" />
|
||||
{/if}
|
||||
<MenuOption on:click={() => (viewMode = ViewMode.SELECT_THUMBNAIL)} text="Set album cover" />
|
||||
<MenuOption on:click={() => (viewMode = ViewMode.OPTIONS)} text="Options" />
|
||||
</ContextMenu>
|
||||
{/if}
|
||||
</CircleIconButton>
|
||||
|
@ -630,9 +660,10 @@
|
|||
</AssetGrid>
|
||||
{/if}
|
||||
|
||||
{#if album.sharedUsers.length > 0 && !$showAssetViewer}
|
||||
{#if showActivityStatus}
|
||||
<div class="absolute z-[2] bottom-0 right-0 mb-6 mr-6 justify-self-end">
|
||||
<ActivityStatus
|
||||
disabled={!album.isActivityEnabled}
|
||||
{isLiked}
|
||||
numberOfComments={$numberOfComments}
|
||||
{isShowActivity}
|
||||
|
@ -648,11 +679,12 @@
|
|||
<div
|
||||
transition:fly={{ duration: 150 }}
|
||||
id="activity-panel"
|
||||
class="z-[1002] w-[360px] md:w-[460px] overflow-y-auto bg-immich-bg transition-all dark:border-l dark:border-l-immich-dark-gray dark:bg-immich-dark-bg pl-4"
|
||||
class="z-[2] w-[360px] md:w-[460px] overflow-y-auto bg-immich-bg transition-all dark:border-l dark:border-l-immich-dark-gray dark:bg-immich-dark-bg pl-4"
|
||||
translate="yes"
|
||||
>
|
||||
<ActivityViewer
|
||||
{user}
|
||||
disabled={!album.isActivityEnabled}
|
||||
albumOwnerId={album.ownerId}
|
||||
albumId={album.id}
|
||||
bind:reactions
|
||||
|
@ -700,6 +732,16 @@
|
|||
</ConfirmDialogue>
|
||||
{/if}
|
||||
|
||||
{#if viewMode === ViewMode.OPTIONS}
|
||||
<AlbumOptions
|
||||
{album}
|
||||
{user}
|
||||
on:close={() => (viewMode = ViewMode.VIEW)}
|
||||
on:toggleEnableActivity={handleToggleEnableActivity}
|
||||
on:showSelectSharedUser={() => (viewMode = ViewMode.SELECT_USERS)}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if isEditingDescription}
|
||||
<EditDescriptionModal
|
||||
{album}
|
||||
|
|
|
@ -17,4 +17,5 @@ export const albumFactory = Sync.makeFactory<AlbumResponseDto>({
|
|||
shared: false,
|
||||
sharedUsers: [],
|
||||
hasSharedLink: false,
|
||||
isActivityEnabled: true,
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue