From 559565d6a7c31d39e044b33bf17c06de61b9f479 Mon Sep 17 00:00:00 2001 From: aviv926 <51673860+aviv926@users.noreply.github.com> Date: Thu, 14 Mar 2024 17:16:20 +0200 Subject: [PATCH 1/5] chore(web): Sharing -> Partner Sharing (#7952) --- .../lib/components/user-settings-page/user-settings-list.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/lib/components/user-settings-page/user-settings-list.svelte b/web/src/lib/components/user-settings-page/user-settings-list.svelte index a0eaa8cb4..7a3be6fb5 100644 --- a/web/src/lib/components/user-settings-page/user-settings-list.svelte +++ b/web/src/lib/components/user-settings-page/user-settings-list.svelte @@ -55,7 +55,7 @@ - + From 1c4637cb4371df6c3a5090ab2232ed968c72f47c Mon Sep 17 00:00:00 2001 From: bo0tzz Date: Thu, 14 Mar 2024 16:16:33 +0100 Subject: [PATCH 2/5] chore(ci): Clean up docker buildx workaround (#7949) Co-authored-by: Alex --- .github/workflows/docker.yml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index fd7904415..c1df0a6ed 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -67,12 +67,6 @@ jobs: - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3.2.0 - # Workaround to fix error: - # failed to push: failed to copy: io: read/write on closed pipe - # See https://github.com/docker/build-push-action/issues/761 - with: - driver-opts: | - image=moby/buildkit:v0.10.6 - name: Login to Docker Hub # Only push to Docker Hub when making a release From 31f7e1aca31d53b7c855733ecd3e095fb5105334 Mon Sep 17 00:00:00 2001 From: martin <74269598+martabal@users.noreply.github.com> Date: Thu, 14 Mar 2024 17:45:03 +0100 Subject: [PATCH 3/5] feat(server, web): album orders (#7819) * feat: album orders * fix: tests * pr feedback * pr feedback * pr feedback * fix: tests * add comment * pr feedback * fix: rendering issue * wording * fix: order value doesn't change --------- Co-authored-by: Alex Tran --- e2e/src/api/specs/album.e2e-spec.ts | 2 + mobile/openapi/doc/AlbumResponseDto.md | 1 + mobile/openapi/doc/AssetApi.md | 12 +++-- mobile/openapi/doc/UpdateAlbumDto.md | 1 + mobile/openapi/lib/api/asset_api.dart | 26 +++++++--- .../openapi/lib/model/album_response_dto.dart | 19 ++++++- .../openapi/lib/model/update_album_dto.dart | 23 +++++++-- .../openapi/test/album_response_dto_test.dart | 5 ++ mobile/openapi/test/asset_api_test.dart | 4 +- .../openapi/test/update_album_dto_test.dart | 5 ++ open-api/immich-openapi-specs.json | 22 +++++++++ open-api/typescript-sdk/src/fetch-client.ts | 18 ++++--- server/src/domain/album/album-response.dto.ts | 7 ++- server/src/domain/album/album.service.ts | 1 + .../src/domain/album/dto/album-update.dto.ts | 9 +++- server/src/domain/asset/dto/asset.dto.ts | 5 -- .../src/domain/asset/dto/time-bucket.dto.ts | 8 ++- .../domain/repositories/asset.repository.ts | 3 +- server/src/domain/search/dto/search.dto.ts | 3 +- server/src/domain/search/search.service.ts | 4 +- server/src/infra/entities/album.entity.ts | 9 ++++ .../1710182081326-AscendingOrderAlbum.ts | 14 ++++++ .../infra/repositories/asset.repository.ts | 6 +-- server/src/infra/sql/album.repository.sql | 7 +++ .../src/infra/sql/shared.link.repository.sql | 2 + server/test/fixtures/album.stub.ts | 12 ++++- server/test/fixtures/shared-link.stub.ts | 4 +- .../album-page/album-options.svelte | 49 +++++++++++++++++-- .../lib/components/slideshow-settings.svelte | 2 +- web/src/lib/stores/assets.store.ts | 5 +- .../(user)/albums/[albumId]/+page.svelte | 9 +++- web/src/test-data/factories/album-factory.ts | 3 +- 32 files changed, 251 insertions(+), 49 deletions(-) create mode 100644 server/src/infra/migrations/1710182081326-AscendingOrderAlbum.ts diff --git a/e2e/src/api/specs/album.e2e-spec.ts b/e2e/src/api/specs/album.e2e-spec.ts index 2310b4718..de320ee95 100644 --- a/e2e/src/api/specs/album.e2e-spec.ts +++ b/e2e/src/api/specs/album.e2e-spec.ts @@ -1,6 +1,7 @@ import { AlbumResponseDto, AssetFileUploadResponseDto, + AssetOrder, LoginResponseDto, SharedLinkType, deleteUser, @@ -353,6 +354,7 @@ describe('/album', () => { assetCount: 0, owner: expect.objectContaining({ email: user1.userEmail }), isActivityEnabled: true, + order: AssetOrder.Desc, }); }); }); diff --git a/mobile/openapi/doc/AlbumResponseDto.md b/mobile/openapi/doc/AlbumResponseDto.md index bc00d30af..dd4a94e88 100644 --- a/mobile/openapi/doc/AlbumResponseDto.md +++ b/mobile/openapi/doc/AlbumResponseDto.md @@ -19,6 +19,7 @@ Name | Type | Description | Notes **id** | **String** | | **isActivityEnabled** | **bool** | | **lastModifiedAssetTimestamp** | [**DateTime**](DateTime.md) | | [optional] +**order** | [**AssetOrder**](AssetOrder.md) | | [optional] **owner** | [**UserResponseDto**](UserResponseDto.md) | | **ownerId** | **String** | | **shared** | **bool** | | diff --git a/mobile/openapi/doc/AssetApi.md b/mobile/openapi/doc/AssetApi.md index c65e6a605..1aaf195f3 100644 --- a/mobile/openapi/doc/AssetApi.md +++ b/mobile/openapi/doc/AssetApi.md @@ -834,7 +834,7 @@ Name | Type | Description | Notes [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) # **getTimeBucket** -> List getTimeBucket(size, timeBucket, albumId, isArchived, isFavorite, isTrashed, key, personId, userId, withPartners, withStacked) +> List getTimeBucket(size, timeBucket, albumId, isArchived, isFavorite, isTrashed, key, order, personId, userId, withPartners, withStacked) @@ -864,13 +864,14 @@ final isArchived = true; // bool | final isFavorite = true; // bool | final isTrashed = true; // bool | final key = key_example; // String | +final order = ; // AssetOrder | final personId = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String | final userId = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String | final withPartners = true; // bool | final withStacked = true; // bool | try { - final result = api_instance.getTimeBucket(size, timeBucket, albumId, isArchived, isFavorite, isTrashed, key, personId, userId, withPartners, withStacked); + final result = api_instance.getTimeBucket(size, timeBucket, albumId, isArchived, isFavorite, isTrashed, key, order, personId, userId, withPartners, withStacked); print(result); } catch (e) { print('Exception when calling AssetApi->getTimeBucket: $e\n'); @@ -888,6 +889,7 @@ Name | Type | Description | Notes **isFavorite** | **bool**| | [optional] **isTrashed** | **bool**| | [optional] **key** | **String**| | [optional] + **order** | [**AssetOrder**](.md)| | [optional] **personId** | **String**| | [optional] **userId** | **String**| | [optional] **withPartners** | **bool**| | [optional] @@ -909,7 +911,7 @@ Name | Type | Description | Notes [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) # **getTimeBuckets** -> List getTimeBuckets(size, albumId, isArchived, isFavorite, isTrashed, key, personId, userId, withPartners, withStacked) +> List getTimeBuckets(size, albumId, isArchived, isFavorite, isTrashed, key, order, personId, userId, withPartners, withStacked) @@ -938,13 +940,14 @@ final isArchived = true; // bool | final isFavorite = true; // bool | final isTrashed = true; // bool | final key = key_example; // String | +final order = ; // AssetOrder | final personId = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String | final userId = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String | final withPartners = true; // bool | final withStacked = true; // bool | try { - final result = api_instance.getTimeBuckets(size, albumId, isArchived, isFavorite, isTrashed, key, personId, userId, withPartners, withStacked); + final result = api_instance.getTimeBuckets(size, albumId, isArchived, isFavorite, isTrashed, key, order, personId, userId, withPartners, withStacked); print(result); } catch (e) { print('Exception when calling AssetApi->getTimeBuckets: $e\n'); @@ -961,6 +964,7 @@ Name | Type | Description | Notes **isFavorite** | **bool**| | [optional] **isTrashed** | **bool**| | [optional] **key** | **String**| | [optional] + **order** | [**AssetOrder**](.md)| | [optional] **personId** | **String**| | [optional] **userId** | **String**| | [optional] **withPartners** | **bool**| | [optional] diff --git a/mobile/openapi/doc/UpdateAlbumDto.md b/mobile/openapi/doc/UpdateAlbumDto.md index 4ded87d1b..89edf1c6e 100644 --- a/mobile/openapi/doc/UpdateAlbumDto.md +++ b/mobile/openapi/doc/UpdateAlbumDto.md @@ -12,6 +12,7 @@ Name | Type | Description | Notes **albumThumbnailAssetId** | **String** | | [optional] **description** | **String** | | [optional] **isActivityEnabled** | **bool** | | [optional] +**order** | [**AssetOrder**](AssetOrder.md) | | [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) diff --git a/mobile/openapi/lib/api/asset_api.dart b/mobile/openapi/lib/api/asset_api.dart index 786129b45..b0395bfcb 100644 --- a/mobile/openapi/lib/api/asset_api.dart +++ b/mobile/openapi/lib/api/asset_api.dart @@ -852,6 +852,8 @@ class AssetApi { /// /// * [String] key: /// + /// * [AssetOrder] order: + /// /// * [String] personId: /// /// * [String] userId: @@ -859,7 +861,7 @@ class AssetApi { /// * [bool] withPartners: /// /// * [bool] withStacked: - Future getTimeBucketWithHttpInfo(TimeBucketSize size, String timeBucket, { String? albumId, bool? isArchived, bool? isFavorite, bool? isTrashed, String? key, String? personId, String? userId, bool? withPartners, bool? withStacked, }) async { + Future getTimeBucketWithHttpInfo(TimeBucketSize size, String timeBucket, { String? albumId, bool? isArchived, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? userId, bool? withPartners, bool? withStacked, }) async { // ignore: prefer_const_declarations final path = r'/asset/time-bucket'; @@ -885,6 +887,9 @@ class AssetApi { if (key != null) { queryParams.addAll(_queryParams('', 'key', key)); } + if (order != null) { + queryParams.addAll(_queryParams('', 'order', order)); + } if (personId != null) { queryParams.addAll(_queryParams('', 'personId', personId)); } @@ -930,6 +935,8 @@ class AssetApi { /// /// * [String] key: /// + /// * [AssetOrder] order: + /// /// * [String] personId: /// /// * [String] userId: @@ -937,8 +944,8 @@ class AssetApi { /// * [bool] withPartners: /// /// * [bool] withStacked: - Future?> getTimeBucket(TimeBucketSize size, String timeBucket, { String? albumId, bool? isArchived, bool? isFavorite, bool? isTrashed, String? key, String? personId, String? userId, bool? withPartners, bool? withStacked, }) async { - final response = await getTimeBucketWithHttpInfo(size, timeBucket, albumId: albumId, isArchived: isArchived, isFavorite: isFavorite, isTrashed: isTrashed, key: key, personId: personId, userId: userId, withPartners: withPartners, withStacked: withStacked, ); + Future?> getTimeBucket(TimeBucketSize size, String timeBucket, { String? albumId, bool? isArchived, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? userId, bool? withPartners, bool? withStacked, }) async { + final response = await getTimeBucketWithHttpInfo(size, timeBucket, albumId: albumId, isArchived: isArchived, isFavorite: isFavorite, isTrashed: isTrashed, key: key, order: order, personId: personId, userId: userId, withPartners: withPartners, withStacked: withStacked, ); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -970,6 +977,8 @@ class AssetApi { /// /// * [String] key: /// + /// * [AssetOrder] order: + /// /// * [String] personId: /// /// * [String] userId: @@ -977,7 +986,7 @@ class AssetApi { /// * [bool] withPartners: /// /// * [bool] withStacked: - Future getTimeBucketsWithHttpInfo(TimeBucketSize size, { String? albumId, bool? isArchived, bool? isFavorite, bool? isTrashed, String? key, String? personId, String? userId, bool? withPartners, bool? withStacked, }) async { + Future getTimeBucketsWithHttpInfo(TimeBucketSize size, { String? albumId, bool? isArchived, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? userId, bool? withPartners, bool? withStacked, }) async { // ignore: prefer_const_declarations final path = r'/asset/time-buckets'; @@ -1003,6 +1012,9 @@ class AssetApi { if (key != null) { queryParams.addAll(_queryParams('', 'key', key)); } + if (order != null) { + queryParams.addAll(_queryParams('', 'order', order)); + } if (personId != null) { queryParams.addAll(_queryParams('', 'personId', personId)); } @@ -1045,6 +1057,8 @@ class AssetApi { /// /// * [String] key: /// + /// * [AssetOrder] order: + /// /// * [String] personId: /// /// * [String] userId: @@ -1052,8 +1066,8 @@ class AssetApi { /// * [bool] withPartners: /// /// * [bool] withStacked: - Future?> getTimeBuckets(TimeBucketSize size, { String? albumId, bool? isArchived, bool? isFavorite, bool? isTrashed, String? key, String? personId, String? userId, bool? withPartners, bool? withStacked, }) async { - final response = await getTimeBucketsWithHttpInfo(size, albumId: albumId, isArchived: isArchived, isFavorite: isFavorite, isTrashed: isTrashed, key: key, personId: personId, userId: userId, withPartners: withPartners, withStacked: withStacked, ); + Future?> getTimeBuckets(TimeBucketSize size, { String? albumId, bool? isArchived, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? userId, bool? withPartners, bool? withStacked, }) async { + final response = await getTimeBucketsWithHttpInfo(size, albumId: albumId, isArchived: isArchived, isFavorite: isFavorite, isTrashed: isTrashed, key: key, order: order, personId: personId, userId: userId, withPartners: withPartners, withStacked: withStacked, ); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/mobile/openapi/lib/model/album_response_dto.dart b/mobile/openapi/lib/model/album_response_dto.dart index 43e24f87b..d76402855 100644 --- a/mobile/openapi/lib/model/album_response_dto.dart +++ b/mobile/openapi/lib/model/album_response_dto.dart @@ -24,6 +24,7 @@ class AlbumResponseDto { required this.id, required this.isActivityEnabled, this.lastModifiedAssetTimestamp, + this.order, required this.owner, required this.ownerId, required this.shared, @@ -66,6 +67,14 @@ class AlbumResponseDto { /// DateTime? lastModifiedAssetTimestamp; + /// + /// 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. + /// + AssetOrder? order; + UserResponseDto owner; String ownerId; @@ -97,6 +106,7 @@ class AlbumResponseDto { other.id == id && other.isActivityEnabled == isActivityEnabled && other.lastModifiedAssetTimestamp == lastModifiedAssetTimestamp && + other.order == order && other.owner == owner && other.ownerId == ownerId && other.shared == shared && @@ -118,6 +128,7 @@ class AlbumResponseDto { (id.hashCode) + (isActivityEnabled.hashCode) + (lastModifiedAssetTimestamp == null ? 0 : lastModifiedAssetTimestamp!.hashCode) + + (order == null ? 0 : order!.hashCode) + (owner.hashCode) + (ownerId.hashCode) + (shared.hashCode) + @@ -126,7 +137,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, isActivityEnabled=$isActivityEnabled, 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, order=$order, owner=$owner, ownerId=$ownerId, shared=$shared, sharedUsers=$sharedUsers, startDate=$startDate, updatedAt=$updatedAt]'; Map toJson() { final json = {}; @@ -152,6 +163,11 @@ class AlbumResponseDto { json[r'lastModifiedAssetTimestamp'] = this.lastModifiedAssetTimestamp!.toUtc().toIso8601String(); } else { // json[r'lastModifiedAssetTimestamp'] = null; + } + if (this.order != null) { + json[r'order'] = this.order; + } else { + // json[r'order'] = null; } json[r'owner'] = this.owner; json[r'ownerId'] = this.ownerId; @@ -185,6 +201,7 @@ class AlbumResponseDto { id: mapValueOfType(json, r'id')!, isActivityEnabled: mapValueOfType(json, r'isActivityEnabled')!, lastModifiedAssetTimestamp: mapDateTime(json, r'lastModifiedAssetTimestamp', r''), + order: AssetOrder.fromJson(json[r'order']), owner: UserResponseDto.fromJson(json[r'owner'])!, ownerId: mapValueOfType(json, r'ownerId')!, shared: mapValueOfType(json, r'shared')!, diff --git a/mobile/openapi/lib/model/update_album_dto.dart b/mobile/openapi/lib/model/update_album_dto.dart index dfe245aaf..d9408cedf 100644 --- a/mobile/openapi/lib/model/update_album_dto.dart +++ b/mobile/openapi/lib/model/update_album_dto.dart @@ -17,6 +17,7 @@ class UpdateAlbumDto { this.albumThumbnailAssetId, this.description, this.isActivityEnabled, + this.order, }); /// @@ -51,12 +52,21 @@ class UpdateAlbumDto { /// 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 + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + AssetOrder? order; + @override bool operator ==(Object other) => identical(this, other) || other is UpdateAlbumDto && other.albumName == albumName && other.albumThumbnailAssetId == albumThumbnailAssetId && other.description == description && - other.isActivityEnabled == isActivityEnabled; + other.isActivityEnabled == isActivityEnabled && + other.order == order; @override int get hashCode => @@ -64,10 +74,11 @@ class UpdateAlbumDto { (albumName == null ? 0 : albumName!.hashCode) + (albumThumbnailAssetId == null ? 0 : albumThumbnailAssetId!.hashCode) + (description == null ? 0 : description!.hashCode) + - (isActivityEnabled == null ? 0 : isActivityEnabled!.hashCode); + (isActivityEnabled == null ? 0 : isActivityEnabled!.hashCode) + + (order == null ? 0 : order!.hashCode); @override - String toString() => 'UpdateAlbumDto[albumName=$albumName, albumThumbnailAssetId=$albumThumbnailAssetId, description=$description, isActivityEnabled=$isActivityEnabled]'; + String toString() => 'UpdateAlbumDto[albumName=$albumName, albumThumbnailAssetId=$albumThumbnailAssetId, description=$description, isActivityEnabled=$isActivityEnabled, order=$order]'; Map toJson() { final json = {}; @@ -91,6 +102,11 @@ class UpdateAlbumDto { } else { // json[r'isActivityEnabled'] = null; } + if (this.order != null) { + json[r'order'] = this.order; + } else { + // json[r'order'] = null; + } return json; } @@ -106,6 +122,7 @@ class UpdateAlbumDto { albumThumbnailAssetId: mapValueOfType(json, r'albumThumbnailAssetId'), description: mapValueOfType(json, r'description'), isActivityEnabled: mapValueOfType(json, r'isActivityEnabled'), + order: AssetOrder.fromJson(json[r'order']), ); } return null; diff --git a/mobile/openapi/test/album_response_dto_test.dart b/mobile/openapi/test/album_response_dto_test.dart index 933f77c19..5c79e5d2f 100644 --- a/mobile/openapi/test/album_response_dto_test.dart +++ b/mobile/openapi/test/album_response_dto_test.dart @@ -71,6 +71,11 @@ void main() { // TODO }); + // AssetOrder order + test('to test the property `order`', () async { + // TODO + }); + // UserResponseDto owner test('to test the property `owner`', () async { // TODO diff --git a/mobile/openapi/test/asset_api_test.dart b/mobile/openapi/test/asset_api_test.dart index 846a5998c..d210d0e4d 100644 --- a/mobile/openapi/test/asset_api_test.dart +++ b/mobile/openapi/test/asset_api_test.dart @@ -95,12 +95,12 @@ void main() { // TODO }); - //Future> getTimeBucket(TimeBucketSize size, String timeBucket, { String albumId, bool isArchived, bool isFavorite, bool isTrashed, String key, String personId, String userId, bool withPartners, bool withStacked }) async + //Future> getTimeBucket(TimeBucketSize size, String timeBucket, { String albumId, bool isArchived, bool isFavorite, bool isTrashed, String key, AssetOrder order, String personId, String userId, bool withPartners, bool withStacked }) async test('test getTimeBucket', () async { // TODO }); - //Future> getTimeBuckets(TimeBucketSize size, { String albumId, bool isArchived, bool isFavorite, bool isTrashed, String key, String personId, String userId, bool withPartners, bool withStacked }) async + //Future> getTimeBuckets(TimeBucketSize size, { String albumId, bool isArchived, bool isFavorite, bool isTrashed, String key, AssetOrder order, String personId, String userId, bool withPartners, bool withStacked }) async test('test getTimeBuckets', () async { // TODO }); diff --git a/mobile/openapi/test/update_album_dto_test.dart b/mobile/openapi/test/update_album_dto_test.dart index 67ec80010..7f1591a52 100644 --- a/mobile/openapi/test/update_album_dto_test.dart +++ b/mobile/openapi/test/update_album_dto_test.dart @@ -36,6 +36,11 @@ void main() { // TODO }); + // AssetOrder order + test('to test the property `order`', () async { + // TODO + }); + }); diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 2540baf77..15ada078c 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -1765,6 +1765,14 @@ "type": "string" } }, + { + "name": "order", + "required": false, + "in": "query", + "schema": { + "$ref": "#/components/schemas/AssetOrder" + } + }, { "name": "personId", "required": false, @@ -1901,6 +1909,14 @@ "type": "string" } }, + { + "name": "order", + "required": false, + "in": "query", + "schema": { + "$ref": "#/components/schemas/AssetOrder" + } + }, { "name": "personId", "required": false, @@ -6722,6 +6738,9 @@ "format": "date-time", "type": "string" }, + "order": { + "$ref": "#/components/schemas/AssetOrder" + }, "owner": { "$ref": "#/components/schemas/UserResponseDto" }, @@ -10335,6 +10354,9 @@ }, "isActivityEnabled": { "type": "boolean" + }, + "order": { + "$ref": "#/components/schemas/AssetOrder" } }, "type": "object" diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index acf540aff..6a660f4e1 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -153,6 +153,7 @@ export type AlbumResponseDto = { id: string; isActivityEnabled: boolean; lastModifiedAssetTimestamp?: string; + order?: AssetOrder; owner: UserResponseDto; ownerId: string; shared: boolean; @@ -176,6 +177,7 @@ export type UpdateAlbumDto = { albumThumbnailAssetId?: string; description?: string; isActivityEnabled?: boolean; + order?: AssetOrder; }; export type BulkIdsDto = { ids: string[]; @@ -1453,12 +1455,13 @@ export function getAssetThumbnail({ format, id, key }: { ...opts })); } -export function getTimeBucket({ albumId, isArchived, isFavorite, isTrashed, key, personId, size, timeBucket, userId, withPartners, withStacked }: { +export function getTimeBucket({ albumId, isArchived, isFavorite, isTrashed, key, order, personId, size, timeBucket, userId, withPartners, withStacked }: { albumId?: string; isArchived?: boolean; isFavorite?: boolean; isTrashed?: boolean; key?: string; + order?: AssetOrder; personId?: string; size: TimeBucketSize; timeBucket: string; @@ -1475,6 +1478,7 @@ export function getTimeBucket({ albumId, isArchived, isFavorite, isTrashed, key, isFavorite, isTrashed, key, + order, personId, size, timeBucket, @@ -1485,12 +1489,13 @@ export function getTimeBucket({ albumId, isArchived, isFavorite, isTrashed, key, ...opts })); } -export function getTimeBuckets({ albumId, isArchived, isFavorite, isTrashed, key, personId, size, userId, withPartners, withStacked }: { +export function getTimeBuckets({ albumId, isArchived, isFavorite, isTrashed, key, order, personId, size, userId, withPartners, withStacked }: { albumId?: string; isArchived?: boolean; isFavorite?: boolean; isTrashed?: boolean; key?: string; + order?: AssetOrder; personId?: string; size: TimeBucketSize; userId?: string; @@ -1506,6 +1511,7 @@ export function getTimeBuckets({ albumId, isArchived, isFavorite, isTrashed, key isFavorite, isTrashed, key, + order, personId, size, userId, @@ -2747,6 +2753,10 @@ export enum AssetTypeEnum { Audio = "AUDIO", Other = "OTHER" } +export enum AssetOrder { + Asc = "asc", + Desc = "desc" +} export enum Error { Duplicate = "duplicate", NoPermission = "no_permission", @@ -2774,10 +2784,6 @@ export enum TimeBucketSize { Day = "DAY", Month = "MONTH" } -export enum AssetOrder { - Asc = "asc", - Desc = "desc" -} export enum EntityType { Asset = "ASSET", Album = "ALBUM" diff --git a/server/src/domain/album/album-response.dto.ts b/server/src/domain/album/album-response.dto.ts index 168b38592..bcca1cd31 100644 --- a/server/src/domain/album/album-response.dto.ts +++ b/server/src/domain/album/album-response.dto.ts @@ -1,4 +1,5 @@ -import { AlbumEntity } from '@app/infra/entities'; +import { AlbumEntity, AssetOrder } from '@app/infra/entities'; +import { Optional } from '@nestjs/common'; import { ApiProperty } from '@nestjs/swagger'; import { AssetResponseDto, mapAsset } from '../asset'; import { AuthDto } from '../auth/auth.dto'; @@ -23,6 +24,9 @@ export class AlbumResponseDto { startDate?: Date; endDate?: Date; isActivityEnabled!: boolean; + @Optional() + @ApiProperty({ enumName: 'AssetOrder', enum: AssetOrder }) + order?: AssetOrder; } export const mapAlbum = (entity: AlbumEntity, withAssets: boolean, auth?: AuthDto): AlbumResponseDto => { @@ -63,6 +67,7 @@ export const mapAlbum = (entity: AlbumEntity, withAssets: boolean, auth?: AuthDt assets: (withAssets ? assets : []).map((asset) => mapAsset(asset, { auth })), assetCount: entity.assets?.length || 0, isActivityEnabled: entity.isActivityEnabled, + order: entity.order, }; }; diff --git a/server/src/domain/album/album.service.ts b/server/src/domain/album/album.service.ts index 9a7b940f7..dc3d510d4 100644 --- a/server/src/domain/album/album.service.ts +++ b/server/src/domain/album/album.service.ts @@ -148,6 +148,7 @@ export class AlbumService { description: dto.description, albumThumbnailAssetId: dto.albumThumbnailAssetId, isActivityEnabled: dto.isActivityEnabled, + order: dto.order, }); return mapAlbumWithoutAssets(updatedAlbum); diff --git a/server/src/domain/album/dto/album-update.dto.ts b/server/src/domain/album/dto/album-update.dto.ts index 1b6c754f0..4f88cefbb 100644 --- a/server/src/domain/album/dto/album-update.dto.ts +++ b/server/src/domain/album/dto/album-update.dto.ts @@ -1,4 +1,6 @@ -import { IsString } from 'class-validator'; +import { AssetOrder } from '@app/infra/entities'; +import { ApiProperty } from '@nestjs/swagger'; +import { IsEnum, IsString } from 'class-validator'; import { Optional, ValidateBoolean, ValidateUUID } from '../../domain.util'; export class UpdateAlbumDto { @@ -15,4 +17,9 @@ export class UpdateAlbumDto { @ValidateBoolean({ optional: true }) isActivityEnabled?: boolean; + + @IsEnum(AssetOrder) + @Optional() + @ApiProperty({ enum: AssetOrder, enumName: 'AssetOrder' }) + order?: AssetOrder; } diff --git a/server/src/domain/asset/dto/asset.dto.ts b/server/src/domain/asset/dto/asset.dto.ts index 8b5c675d8..2abe31d0a 100644 --- a/server/src/domain/asset/dto/asset.dto.ts +++ b/server/src/domain/asset/dto/asset.dto.ts @@ -18,11 +18,6 @@ export class DeviceIdDto { deviceId!: string; } -export enum AssetOrder { - ASC = 'asc', - DESC = 'desc', -} - const hasGPS = (o: { latitude: undefined; longitude: undefined }) => o.latitude !== undefined || o.longitude !== undefined; const ValidateGPS = () => ValidateIf(hasGPS); diff --git a/server/src/domain/asset/dto/time-bucket.dto.ts b/server/src/domain/asset/dto/time-bucket.dto.ts index 597a5de35..7c5b9c212 100644 --- a/server/src/domain/asset/dto/time-bucket.dto.ts +++ b/server/src/domain/asset/dto/time-bucket.dto.ts @@ -1,6 +1,7 @@ +import { AssetOrder } from '@app/infra/entities'; import { ApiProperty } from '@nestjs/swagger'; import { IsEnum, IsNotEmpty, IsString } from 'class-validator'; -import { ValidateBoolean, ValidateUUID } from '../../domain.util'; +import { Optional, ValidateBoolean, ValidateUUID } from '../../domain.util'; import { TimeBucketSize } from '../../repositories'; export class TimeBucketDto { @@ -32,6 +33,11 @@ export class TimeBucketDto { @ValidateBoolean({ optional: true }) withPartners?: boolean; + + @IsEnum(AssetOrder) + @Optional() + @ApiProperty({ enum: AssetOrder, enumName: 'AssetOrder' }) + order?: AssetOrder; } export class TimeBucketAssetDto extends TimeBucketDto { diff --git a/server/src/domain/repositories/asset.repository.ts b/server/src/domain/repositories/asset.repository.ts index 362700442..8b14ce597 100644 --- a/server/src/domain/repositories/asset.repository.ts +++ b/server/src/domain/repositories/asset.repository.ts @@ -1,5 +1,5 @@ import { AssetSearchOptions, ReverseGeocodeResult, SearchExploreItem } from '@app/domain'; -import { AssetEntity, AssetJobStatusEntity, AssetType, ExifEntity } from '@app/infra/entities'; +import { AssetEntity, AssetJobStatusEntity, AssetOrder, AssetType, ExifEntity } from '@app/infra/entities'; import { FindOptionsRelations, FindOptionsSelect } from 'typeorm'; import { Paginated, PaginationOptions } from '../domain.util'; @@ -66,6 +66,7 @@ export interface AssetBuilderOptions { export interface TimeBucketOptions extends AssetBuilderOptions { size: TimeBucketSize; + order?: AssetOrder; } export interface TimeBucketItem { diff --git a/server/src/domain/search/dto/search.dto.ts b/server/src/domain/search/dto/search.dto.ts index 9fa7d8e8b..1bc67266a 100644 --- a/server/src/domain/search/dto/search.dto.ts +++ b/server/src/domain/search/dto/search.dto.ts @@ -1,5 +1,4 @@ -import { AssetOrder } from '@app/domain/asset/dto/asset.dto'; -import { AssetType, GeodataPlacesEntity } from '@app/infra/entities'; +import { AssetOrder, AssetType, GeodataPlacesEntity } from '@app/infra/entities'; import { ApiProperty } from '@nestjs/swagger'; import { Type } from 'class-transformer'; import { IsEnum, IsInt, IsNotEmpty, IsString, Max, Min } from 'class-validator'; diff --git a/server/src/domain/search/search.service.ts b/server/src/domain/search/search.service.ts index 4cb0665e0..56c4498bc 100644 --- a/server/src/domain/search/search.service.ts +++ b/server/src/domain/search/search.service.ts @@ -1,6 +1,6 @@ -import { AssetEntity } from '@app/infra/entities'; +import { AssetEntity, AssetOrder } from '@app/infra/entities'; import { Inject, Injectable } from '@nestjs/common'; -import { AssetOrder, AssetResponseDto, mapAsset } from '../asset'; +import { AssetResponseDto, mapAsset } from '../asset'; import { AuthDto } from '../auth'; import { PersonResponseDto } from '../person'; import { diff --git a/server/src/infra/entities/album.entity.ts b/server/src/infra/entities/album.entity.ts index fbc125351..daa8fcbc3 100644 --- a/server/src/infra/entities/album.entity.ts +++ b/server/src/infra/entities/album.entity.ts @@ -14,6 +14,12 @@ import { AssetEntity } from './asset.entity'; import { SharedLinkEntity } from './shared-link.entity'; import { UserEntity } from './user.entity'; +// ran into issues when importing the enum from `asset.dto.ts` +export enum AssetOrder { + ASC = 'asc', + DESC = 'desc', +} + @Entity('albums') export class AlbumEntity { @PrimaryGeneratedColumn('uuid') @@ -59,4 +65,7 @@ export class AlbumEntity { @Column({ default: true }) isActivityEnabled!: boolean; + + @Column({ type: 'varchar', default: AssetOrder.DESC }) + order!: AssetOrder; } diff --git a/server/src/infra/migrations/1710182081326-AscendingOrderAlbum.ts b/server/src/infra/migrations/1710182081326-AscendingOrderAlbum.ts new file mode 100644 index 000000000..b672ff2b2 --- /dev/null +++ b/server/src/infra/migrations/1710182081326-AscendingOrderAlbum.ts @@ -0,0 +1,14 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class AscendingOrderAlbum1710182081326 implements MigrationInterface { + name = 'AscendingOrderAlbum1710182081326' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "albums" ADD "order" character varying NOT NULL DEFAULT 'desc'`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "albums" DROP COLUMN "order"`); + } + +} diff --git a/server/src/infra/repositories/asset.repository.ts b/server/src/infra/repositories/asset.repository.ts index 5d571d11e..871a44460 100644 --- a/server/src/infra/repositories/asset.repository.ts +++ b/server/src/infra/repositories/asset.repository.ts @@ -36,7 +36,7 @@ import { Not, Repository, } from 'typeorm'; -import { AssetEntity, AssetJobStatusEntity, AssetType, ExifEntity, SmartInfoEntity } from '../entities'; +import { AssetEntity, AssetJobStatusEntity, AssetOrder, AssetType, ExifEntity, SmartInfoEntity } from '../entities'; import { DummyValue, GenerateSql } from '../infra.util'; import { Chunked, ChunkedArray, OptionalBetween, paginate, paginatedBuilder, searchAssetBuilder } from '../infra.utils'; import { Instrumentation } from '../instrumentation'; @@ -607,7 +607,7 @@ export class AssetRepository implements IAssetRepository { .select(`COUNT(asset.id)::int`, 'count') .addSelect(truncated, 'timeBucket') .groupBy(truncated) - .orderBy(truncated, 'DESC') + .orderBy(truncated, options.order === AssetOrder.ASC ? 'ASC' : 'DESC') .getRawMany(); } @@ -620,7 +620,7 @@ export class AssetRepository implements IAssetRepository { // First sort by the day in localtime (put it in the right bucket) .orderBy(truncated, 'DESC') // and then sort by the actual time - .addOrderBy('asset.fileCreatedAt', 'DESC') + .addOrderBy('asset.fileCreatedAt', options.order === AssetOrder.ASC ? 'ASC' : 'DESC') .getMany() ); } diff --git a/server/src/infra/sql/album.repository.sql b/server/src/infra/sql/album.repository.sql index d9b2e896e..ddedc0095 100644 --- a/server/src/infra/sql/album.repository.sql +++ b/server/src/infra/sql/album.repository.sql @@ -15,6 +15,7 @@ FROM "AlbumEntity"."deletedAt" AS "AlbumEntity_deletedAt", "AlbumEntity"."albumThumbnailAssetId" AS "AlbumEntity_albumThumbnailAssetId", "AlbumEntity"."isActivityEnabled" AS "AlbumEntity_isActivityEnabled", + "AlbumEntity"."order" AS "AlbumEntity_order", "AlbumEntity__AlbumEntity_owner"."id" AS "AlbumEntity__AlbumEntity_owner_id", "AlbumEntity__AlbumEntity_owner"."name" AS "AlbumEntity__AlbumEntity_owner_name", "AlbumEntity__AlbumEntity_owner"."avatarColor" AS "AlbumEntity__AlbumEntity_owner_avatarColor", @@ -91,6 +92,7 @@ SELECT "AlbumEntity"."deletedAt" AS "AlbumEntity_deletedAt", "AlbumEntity"."albumThumbnailAssetId" AS "AlbumEntity_albumThumbnailAssetId", "AlbumEntity"."isActivityEnabled" AS "AlbumEntity_isActivityEnabled", + "AlbumEntity"."order" AS "AlbumEntity_order", "AlbumEntity__AlbumEntity_owner"."id" AS "AlbumEntity__AlbumEntity_owner_id", "AlbumEntity__AlbumEntity_owner"."name" AS "AlbumEntity__AlbumEntity_owner_name", "AlbumEntity__AlbumEntity_owner"."avatarColor" AS "AlbumEntity__AlbumEntity_owner_avatarColor", @@ -149,6 +151,7 @@ SELECT "AlbumEntity"."deletedAt" AS "AlbumEntity_deletedAt", "AlbumEntity"."albumThumbnailAssetId" AS "AlbumEntity_albumThumbnailAssetId", "AlbumEntity"."isActivityEnabled" AS "AlbumEntity_isActivityEnabled", + "AlbumEntity"."order" AS "AlbumEntity_order", "AlbumEntity__AlbumEntity_owner"."id" AS "AlbumEntity__AlbumEntity_owner_id", "AlbumEntity__AlbumEntity_owner"."name" AS "AlbumEntity__AlbumEntity_owner_name", "AlbumEntity__AlbumEntity_owner"."avatarColor" AS "AlbumEntity__AlbumEntity_owner_avatarColor", @@ -279,6 +282,7 @@ SELECT "AlbumEntity"."deletedAt" AS "AlbumEntity_deletedAt", "AlbumEntity"."albumThumbnailAssetId" AS "AlbumEntity_albumThumbnailAssetId", "AlbumEntity"."isActivityEnabled" AS "AlbumEntity_isActivityEnabled", + "AlbumEntity"."order" AS "AlbumEntity_order", "AlbumEntity__AlbumEntity_sharedUsers"."id" AS "AlbumEntity__AlbumEntity_sharedUsers_id", "AlbumEntity__AlbumEntity_sharedUsers"."name" AS "AlbumEntity__AlbumEntity_sharedUsers_name", "AlbumEntity__AlbumEntity_sharedUsers"."avatarColor" AS "AlbumEntity__AlbumEntity_sharedUsers_avatarColor", @@ -352,6 +356,7 @@ SELECT "AlbumEntity"."deletedAt" AS "AlbumEntity_deletedAt", "AlbumEntity"."albumThumbnailAssetId" AS "AlbumEntity_albumThumbnailAssetId", "AlbumEntity"."isActivityEnabled" AS "AlbumEntity_isActivityEnabled", + "AlbumEntity"."order" AS "AlbumEntity_order", "AlbumEntity__AlbumEntity_sharedUsers"."id" AS "AlbumEntity__AlbumEntity_sharedUsers_id", "AlbumEntity__AlbumEntity_sharedUsers"."name" AS "AlbumEntity__AlbumEntity_sharedUsers_name", "AlbumEntity__AlbumEntity_sharedUsers"."avatarColor" AS "AlbumEntity__AlbumEntity_sharedUsers_avatarColor", @@ -462,6 +467,7 @@ SELECT "AlbumEntity"."deletedAt" AS "AlbumEntity_deletedAt", "AlbumEntity"."albumThumbnailAssetId" AS "AlbumEntity_albumThumbnailAssetId", "AlbumEntity"."isActivityEnabled" AS "AlbumEntity_isActivityEnabled", + "AlbumEntity"."order" AS "AlbumEntity_order", "AlbumEntity__AlbumEntity_sharedUsers"."id" AS "AlbumEntity__AlbumEntity_sharedUsers_id", "AlbumEntity__AlbumEntity_sharedUsers"."name" AS "AlbumEntity__AlbumEntity_sharedUsers_name", "AlbumEntity__AlbumEntity_sharedUsers"."avatarColor" AS "AlbumEntity__AlbumEntity_sharedUsers_avatarColor", @@ -553,6 +559,7 @@ SELECT "AlbumEntity"."deletedAt" AS "AlbumEntity_deletedAt", "AlbumEntity"."albumThumbnailAssetId" AS "AlbumEntity_albumThumbnailAssetId", "AlbumEntity"."isActivityEnabled" AS "AlbumEntity_isActivityEnabled", + "AlbumEntity"."order" AS "AlbumEntity_order", "AlbumEntity__AlbumEntity_owner"."id" AS "AlbumEntity__AlbumEntity_owner_id", "AlbumEntity__AlbumEntity_owner"."name" AS "AlbumEntity__AlbumEntity_owner_name", "AlbumEntity__AlbumEntity_owner"."avatarColor" AS "AlbumEntity__AlbumEntity_owner_avatarColor", diff --git a/server/src/infra/sql/shared.link.repository.sql b/server/src/infra/sql/shared.link.repository.sql index b5e689413..27531cfc9 100644 --- a/server/src/infra/sql/shared.link.repository.sql +++ b/server/src/infra/sql/shared.link.repository.sql @@ -87,6 +87,7 @@ FROM "SharedLinkEntity__SharedLinkEntity_album"."deletedAt" AS "SharedLinkEntity__SharedLinkEntity_album_deletedAt", "SharedLinkEntity__SharedLinkEntity_album"."albumThumbnailAssetId" AS "SharedLinkEntity__SharedLinkEntity_album_albumThumbnailAssetId", "SharedLinkEntity__SharedLinkEntity_album"."isActivityEnabled" AS "SharedLinkEntity__SharedLinkEntity_album_isActivityEnabled", + "SharedLinkEntity__SharedLinkEntity_album"."order" AS "SharedLinkEntity__SharedLinkEntity_album_order", "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."id" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_id", "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."deviceAssetId" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_deviceAssetId", "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."ownerId" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_ownerId", @@ -248,6 +249,7 @@ SELECT "SharedLinkEntity__SharedLinkEntity_album"."deletedAt" AS "SharedLinkEntity__SharedLinkEntity_album_deletedAt", "SharedLinkEntity__SharedLinkEntity_album"."albumThumbnailAssetId" AS "SharedLinkEntity__SharedLinkEntity_album_albumThumbnailAssetId", "SharedLinkEntity__SharedLinkEntity_album"."isActivityEnabled" AS "SharedLinkEntity__SharedLinkEntity_album_isActivityEnabled", + "SharedLinkEntity__SharedLinkEntity_album"."order" AS "SharedLinkEntity__SharedLinkEntity_album_order", "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."id" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_id", "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."name" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_name", "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."avatarColor" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_avatarColor", diff --git a/server/test/fixtures/album.stub.ts b/server/test/fixtures/album.stub.ts index 2fdc5b5dd..bfb6acb6d 100644 --- a/server/test/fixtures/album.stub.ts +++ b/server/test/fixtures/album.stub.ts @@ -1,4 +1,4 @@ -import { AlbumEntity } from '@app/infra/entities'; +import { AlbumEntity, AssetOrder } from '@app/infra/entities'; import { assetStub } from './asset.stub'; import { authStub } from './auth.stub'; import { userStub } from './user.stub'; @@ -19,6 +19,7 @@ export const albumStub = { sharedLinks: [], sharedUsers: [], isActivityEnabled: true, + order: AssetOrder.DESC, }), sharedWithUser: Object.freeze({ id: 'album-2', @@ -35,6 +36,7 @@ export const albumStub = { sharedLinks: [], sharedUsers: [userStub.user1], isActivityEnabled: true, + order: AssetOrder.DESC, }), sharedWithMultiple: Object.freeze({ id: 'album-3', @@ -51,6 +53,7 @@ export const albumStub = { sharedLinks: [], sharedUsers: [userStub.user1, userStub.user2], isActivityEnabled: true, + order: AssetOrder.DESC, }), sharedWithAdmin: Object.freeze({ id: 'album-3', @@ -67,6 +70,7 @@ export const albumStub = { sharedLinks: [], sharedUsers: [userStub.admin], isActivityEnabled: true, + order: AssetOrder.DESC, }), oneAsset: Object.freeze({ id: 'album-4', @@ -83,6 +87,7 @@ export const albumStub = { sharedLinks: [], sharedUsers: [], isActivityEnabled: true, + order: AssetOrder.DESC, }), twoAssets: Object.freeze({ id: 'album-4a', @@ -99,6 +104,7 @@ export const albumStub = { sharedLinks: [], sharedUsers: [], isActivityEnabled: true, + order: AssetOrder.DESC, }), emptyWithInvalidThumbnail: Object.freeze({ id: 'album-5', @@ -115,6 +121,7 @@ export const albumStub = { sharedLinks: [], sharedUsers: [], isActivityEnabled: true, + order: AssetOrder.DESC, }), emptyWithValidThumbnail: Object.freeze({ id: 'album-5', @@ -131,6 +138,7 @@ export const albumStub = { sharedLinks: [], sharedUsers: [], isActivityEnabled: true, + order: AssetOrder.DESC, }), oneAssetInvalidThumbnail: Object.freeze({ id: 'album-6', @@ -147,6 +155,7 @@ export const albumStub = { sharedLinks: [], sharedUsers: [], isActivityEnabled: true, + order: AssetOrder.DESC, }), oneAssetValidThumbnail: Object.freeze({ id: 'album-6', @@ -163,5 +172,6 @@ export const albumStub = { sharedLinks: [], sharedUsers: [], isActivityEnabled: true, + order: AssetOrder.DESC, }), }; diff --git a/server/test/fixtures/shared-link.stub.ts b/server/test/fixtures/shared-link.stub.ts index 61b44a544..109f05190 100644 --- a/server/test/fixtures/shared-link.stub.ts +++ b/server/test/fixtures/shared-link.stub.ts @@ -1,5 +1,5 @@ import { AlbumResponseDto, AssetResponseDto, ExifResponseDto, mapUser, SharedLinkResponseDto } from '@app/domain'; -import { AssetType, SharedLinkEntity, SharedLinkType, UserEntity } from '@app/infra/entities'; +import { AssetOrder, AssetType, SharedLinkEntity, SharedLinkType, UserEntity } from '@app/infra/entities'; import { assetStub } from './asset.stub'; import { authStub } from './auth.stub'; import { libraryStub } from './library.stub'; @@ -101,6 +101,7 @@ const albumResponse: AlbumResponseDto = { assets: [], assetCount: 1, isActivityEnabled: true, + order: AssetOrder.DESC, }; export const sharedLinkStub = { @@ -181,6 +182,7 @@ export const sharedLinkStub = { sharedUsers: [], sharedLinks: [], isActivityEnabled: true, + order: AssetOrder.DESC, assets: [ { id: 'id_1', diff --git a/web/src/lib/components/album-page/album-options.svelte b/web/src/lib/components/album-page/album-options.svelte index 6cbce418b..d5f816047 100644 --- a/web/src/lib/components/album-page/album-options.svelte +++ b/web/src/lib/components/album-page/album-options.svelte @@ -1,22 +1,55 @@ dispatch('close')}> @@ -34,8 +67,16 @@
-

SHARING

-
+

SETTINGS

+
+ {#if order} + + {/if} { + const handleToggle = (selectedOption: RenderedOption) => { for (const [key, option] of Object.entries(options)) { if (option === selectedOption) { $slideshowNavigation = key as SlideshowNavigation; diff --git a/web/src/lib/stores/assets.store.ts b/web/src/lib/stores/assets.store.ts index 15519b4b2..a61f9ed35 100644 --- a/web/src/lib/stores/assets.store.ts +++ b/web/src/lib/stores/assets.store.ts @@ -161,7 +161,10 @@ export class AssetStore { this.assetToBucket = {}; this.albumAssets = new Set(); - const buckets = await getTimeBuckets({ ...this.options, key: getKey() }); + const buckets = await getTimeBuckets({ + ...this.options, + key: getKey(), + }); this.initialized = true; diff --git a/web/src/routes/(user)/albums/[albumId]/+page.svelte b/web/src/routes/(user)/albums/[albumId]/+page.svelte index 05d94bf3e..9e272ce3f 100644 --- a/web/src/routes/(user)/albums/[albumId]/+page.svelte +++ b/web/src/routes/(user)/albums/[albumId]/+page.svelte @@ -58,6 +58,7 @@ updateAlbumInfo, type ActivityResponseDto, type UserResponseDto, + AssetOrder, } from '@immich/sdk'; import { mdiArrowLeft, @@ -83,6 +84,7 @@ $: album = data.album; $: albumId = album.id; + $: albumKey = `${albumId}_${albumOrder}`; $: { if (!album.isActivityEnabled && $numberOfComments === 0) { @@ -112,8 +114,9 @@ let globalWidth: number; let assetGridWidth: number; let textArea: HTMLTextAreaElement; + let albumOrder: AssetOrder | undefined = data.album.order; - $: assetStore = new AssetStore({ albumId }); + $: assetStore = new AssetStore({ albumId, order: albumOrder }); const assetInteractionStore = createAssetInteractionStore(); const { isMultiSelectState, selectedAssets } = assetInteractionStore; @@ -512,7 +515,7 @@ style={`width:${assetGridWidth}px`} > - {#key albumId} + {#key albumKey} {#if viewMode === ViewMode.SELECT_ASSETS} (albumOrder = order)} on:close={() => (viewMode = ViewMode.VIEW)} on:toggleEnableActivity={handleToggleEnableActivity} on:showSelectSharedUser={() => (viewMode = ViewMode.SELECT_USERS)} diff --git a/web/src/test-data/factories/album-factory.ts b/web/src/test-data/factories/album-factory.ts index fd941f51f..3d761fcf3 100644 --- a/web/src/test-data/factories/album-factory.ts +++ b/web/src/test-data/factories/album-factory.ts @@ -1,5 +1,5 @@ import { faker } from '@faker-js/faker'; -import type { AlbumResponseDto } from '@immich/sdk'; +import { AssetOrder, type AlbumResponseDto } from '@immich/sdk'; import { Sync } from 'factory.ts'; import { userFactory } from './user-factory'; @@ -18,4 +18,5 @@ export const albumFactory = Sync.makeFactory({ sharedUsers: [], hasSharedLink: false, isActivityEnabled: true, + order: AssetOrder.Desc, }); From 2080aeee4d5eda7339c68f756298badecb716342 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Thu, 14 Mar 2024 13:09:27 -0400 Subject: [PATCH 4/5] chore(cli): clean up files (#7955) --- .github/workflows/test.yml | 8 ++------ cli/.npmignore | 10 +++++++--- cli/tsconfig.json | 12 ------------ 3 files changed, 9 insertions(+), 21 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d704aa629..1d57b3a84 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -91,17 +91,13 @@ jobs: with: node-version: 20 - - name: Run setup typescript-sdk + - name: Setup typescript-sdk run: npm ci && npm run build working-directory: ./open-api/typescript-sdk - - name: Run npm install (cli) + - name: Install deps run: npm ci - - name: Run npm install (server) - run: npm ci - working-directory: ./server - - name: Run linter run: npm run lint if: ${{ !cancelled() }} diff --git a/cli/.npmignore b/cli/.npmignore index 42809f8e8..fab798db6 100644 --- a/cli/.npmignore +++ b/cli/.npmignore @@ -1,11 +1,15 @@ **/*.spec.js +coverage/** +src/** upload/** .editorconfig .eslintignore -.eslintrc.js +.eslintrc.cjs +.gitignore .prettierignore .prettierrc +Dockerfile package-lock.json -testSetup.js tsconfig.json -tsconfig.build.json +vite.config.ts +vitest.config.ts diff --git a/cli/tsconfig.json b/cli/tsconfig.json index 3742f4c19..fcd01e01c 100644 --- a/cli/tsconfig.json +++ b/cli/tsconfig.json @@ -15,19 +15,7 @@ "incremental": true, "skipLibCheck": true, "esModuleInterop": true, - "rootDirs": ["src", "../server/src"], "baseUrl": "./", - "paths": { - "@test": ["../server/test"], - "@test/*": ["../server/test/*"], - "@test-utils": ["../server/src/test-utils/utils"], - "@app/immich": ["../server/src/immich"], - "@app/immich/*": ["../server/src/immich/*"], - "@app/infra": ["../server/src/infra"], - "@app/infra/*": ["../server/src/infra/*"], - "@app/domain": ["../server/src/domain"], - "@app/domain/*": ["../server/src/domain/*"] - }, "types": ["vitest/globals"] }, "exclude": ["dist", "node_modules"] From c04dfdf38b9d352694fdcf3a7eb817a150b8533c Mon Sep 17 00:00:00 2001 From: martin <74269598+martabal@users.noreply.github.com> Date: Thu, 14 Mar 2024 20:05:57 +0100 Subject: [PATCH 5/5] refactor(web): albums list (1) (#7660) * refactor: albums list * fix: rename filename * chore: fix merge * pr feedback * chore: fix merge * pr feedback --------- Co-authored-by: Alex Tran --- .../album-page/albums-controls.svelte | 68 +++ .../components/album-page/albums-list.svelte | 282 +++++++++++++ .../albums-table-header.svelte} | 11 +- .../components/album-page/albums-table.svelte | 87 ++++ web/src/routes/(user)/albums/+page.svelte | 397 +----------------- web/src/routes/(user)/albums/albums.bloc.ts | 73 ---- 6 files changed, 447 insertions(+), 471 deletions(-) create mode 100644 web/src/lib/components/album-page/albums-controls.svelte create mode 100644 web/src/lib/components/album-page/albums-list.svelte rename web/src/lib/components/{elements/table-header.svelte => album-page/albums-table-header.svelte} (57%) create mode 100644 web/src/lib/components/album-page/albums-table.svelte delete mode 100644 web/src/routes/(user)/albums/albums.bloc.ts diff --git a/web/src/lib/components/album-page/albums-controls.svelte b/web/src/lib/components/album-page/albums-controls.svelte new file mode 100644 index 000000000..a58dbc0e3 --- /dev/null +++ b/web/src/lib/components/album-page/albums-controls.svelte @@ -0,0 +1,68 @@ + + + + +
+ + Create album +
+
+ + { + return { + title: option.title, + icon: option.sortDesc ? mdiArrowDownThin : mdiArrowUpThin, + }; + }} + on:select={(event) => { + for (const key of sortByOptions) { + if (key.title === event.detail.title) { + key.sortDesc = !key.sortDesc; + $albumViewSettings.sortBy = key.title; + break; + } + } + }} +/> + + handleChangeListMode()}> +
+ {#if $albumViewSettings.view === AlbumViewMode.List} + + + {:else} + + + {/if} +
+
diff --git a/web/src/lib/components/album-page/albums-list.svelte b/web/src/lib/components/album-page/albums-list.svelte new file mode 100644 index 000000000..1a681a884 --- /dev/null +++ b/web/src/lib/components/album-page/albums-list.svelte @@ -0,0 +1,282 @@ + + + + +{#if shouldShowEditAlbumForm} + (shouldShowEditAlbumForm = false)}> + successModifyAlbum()} + on:cancel={() => (shouldShowEditAlbumForm = false)} + /> + +{/if} + +{#if albums.length > 0} + + {#if $albumViewSettings.view === AlbumViewMode.Cover} +
+ {#each albumsFiltered as album, index (album.id)} + + showAlbumContextMenu(detail, album)} + /> + + {/each} +
+ {:else if $albumViewSettings.view === AlbumViewMode.List} + chooseAlbumToDelete(album)} + onAlbumToEdit={(album) => handleEdit(album)} + /> + {/if} + + +{:else} + +{/if} + + +{#if isShowContextMenu} +
+ + setAlbumToDelete()}> + + +

Delete album

+
+
+
+
+{/if} + +{#if albumToDelete} + (albumToDelete = null)} + > + +

Are you sure you want to delete the album {albumToDelete.albumName}?

+

If this album is shared, other users will not be able to access it anymore.

+
+
+{/if} diff --git a/web/src/lib/components/elements/table-header.svelte b/web/src/lib/components/album-page/albums-table-header.svelte similarity index 57% rename from web/src/lib/components/elements/table-header.svelte rename to web/src/lib/components/album-page/albums-table-header.svelte index 0b68dd0e5..b10e34e11 100644 --- a/web/src/lib/components/elements/table-header.svelte +++ b/web/src/lib/components/album-page/albums-table-header.svelte @@ -1,14 +1,15 @@ @@ -18,7 +19,7 @@ class="rounded-lg p-2 hover:bg-immich-dark-primary hover:dark:bg-immich-dark-primary/50" on:click={() => handleSort()} > - {#if albumViewSettings === option.title} + {#if $albumViewSettings.sortBy === option.title} {#if option.sortDesc} ↓ {:else} diff --git a/web/src/lib/components/album-page/albums-table.svelte b/web/src/lib/components/album-page/albums-table.svelte new file mode 100644 index 000000000..467d8d2c5 --- /dev/null +++ b/web/src/lib/components/album-page/albums-table.svelte @@ -0,0 +1,87 @@ + + + + + + {#each sortByOptions as option, index (index)} + + {/each} + + + + + {#each albumsFiltered as album (album.id)} + goto(`${AppRoute.ALBUMS}/${album.id}`)} + on:keydown={(event) => event.key === 'Enter' && goto(`${AppRoute.ALBUMS}/${album.id}`)} + tabindex="0" + > + + + + + + + + + + + {/each} + +
{album.albumName} + {album.assetCount} + {album.assetCount > 1 ? `items` : `item`} +
diff --git a/web/src/routes/(user)/albums/+page.svelte b/web/src/routes/(user)/albums/+page.svelte index c8d591f91..0e98ae6f1 100644 --- a/web/src/routes/(user)/albums/+page.svelte +++ b/web/src/routes/(user)/albums/+page.svelte @@ -1,406 +1,17 @@ - - -{#if shouldShowEditUserForm} - (shouldShowEditUserForm = false)}> - successModifyAlbum()} - on:cancel={() => (shouldShowEditUserForm = false)} - /> - -{/if} -
- - -
- - Create album -
-
- - { - return { - title: option.title, - icon: option.sortDesc ? mdiArrowDownThin : mdiArrowUpThin, - }; - }} - on:select={(event) => { - for (const key in sortByOptions) { - if (sortByOptions[key].title === event.detail.title) { - sortByOptions[key].sortDesc = !sortByOptions[key].sortDesc; - $albumViewSettings.sortBy = sortByOptions[key].title; - } - } - }} - /> - - handleChangeListMode()}> -
- {#if $albumViewSettings.view === AlbumViewMode.List} - - - {:else} - - - {/if} -
-
+
- {#if $albums.length > 0} - - {#if $albumViewSettings.view === AlbumViewMode.Cover} -
- {#each albumsFiltered as album, index (album.id)} - - showAlbumContextMenu(e.detail, album)} - /> - - {/each} -
- {:else if $albumViewSettings.view === AlbumViewMode.List} - - - - {#each Object.keys(sortByOptions) as key (key)} - - {/each} - - - - - {#each albumsFiltered as album (album.id)} - goto(`${AppRoute.ALBUMS}/${album.id}`)} - on:keydown={(event) => event.key === 'Enter' && goto(`${AppRoute.ALBUMS}/${album.id}`)} - tabindex="0" - > - - - - - - - - - - - {/each} - -
{album.albumName} - {album.assetCount} - {album.assetCount > 1 ? `items` : `item`} -
- {/if} - - - {:else} - - {/if} +
- - -{#if $isShowContextMenu} - - setAlbumToDelete()}> - - -

Delete album

-
-
-
-{/if} - -{#if albumToDelete} - (albumToDelete = null)} - > - -

Are you sure you want to delete the album {albumToDelete.albumName}?

-

If this album is shared, other users will not be able to access it anymore.

-
-
-{/if} diff --git a/web/src/routes/(user)/albums/albums.bloc.ts b/web/src/routes/(user)/albums/albums.bloc.ts deleted file mode 100644 index 89a933e0f..000000000 --- a/web/src/routes/(user)/albums/albums.bloc.ts +++ /dev/null @@ -1,73 +0,0 @@ -import type { OnShowContextMenuDetail } from '$lib/components/album-page/album-card'; -import { notificationController, NotificationType } from '$lib/components/shared-components/notification/notification'; -import { asyncTimeout } from '$lib/utils'; -import { handleError } from '$lib/utils/handle-error'; -import { createAlbum, deleteAlbum, getAllAlbums, type AlbumResponseDto } from '@immich/sdk'; -import { derived, get, writable } from 'svelte/store'; - -type AlbumsProperties = { albums: AlbumResponseDto[] }; - -export const useAlbums = (properties: AlbumsProperties) => { - const albums = writable([...properties.albums]); - const contextMenuPosition = writable({ x: 0, y: 0 }); - const contextMenuTargetAlbum = writable(); - const isShowContextMenu = derived(contextMenuTargetAlbum, ($selectedAlbum) => !!$selectedAlbum); - - async function loadAlbums(): Promise { - try { - const data = await getAllAlbums({}); - albums.set(data); - - // Delete album that has no photos and is named '' - for (const album of data) { - if (album.albumName === '' && album.assetCount === 0) { - await asyncTimeout(500); - await handleDeleteAlbum(album); - } - } - } catch { - notificationController.show({ - message: 'Error loading albums', - type: NotificationType.Error, - }); - } - } - - async function handleCreateAlbum(): Promise { - try { - return await createAlbum({ createAlbumDto: { albumName: '' } }); - } catch (error) { - handleError(error, 'Unable to create album'); - } - } - - async function handleDeleteAlbum(albumToDelete: AlbumResponseDto): Promise { - await deleteAlbum({ id: albumToDelete.id }); - albums.set(get(albums).filter(({ id }) => id !== albumToDelete.id)); - } - - function showAlbumContextMenu(contextMenuDetail: OnShowContextMenuDetail, album: AlbumResponseDto): void { - contextMenuTargetAlbum.set(album); - - contextMenuPosition.set({ - x: contextMenuDetail.x, - y: contextMenuDetail.y, - }); - } - - function closeAlbumContextMenu() { - contextMenuTargetAlbum.set(undefined); - } - - return { - albums, - isShowContextMenu, - contextMenuPosition, - contextMenuTargetAlbum, - loadAlbums, - createAlbum: handleCreateAlbum, - deleteAlbum: handleDeleteAlbum, - showAlbumContextMenu, - closeAlbumContextMenu, - }; -};