You've already forked immich
							
							
				mirror of
				https://github.com/immich-app/immich.git
				synced 2025-10-31 00:18:28 +02:00 
			
		
		
		
	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 <alex.tran1502@gmail.com>
This commit is contained in:
		| @@ -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, | ||||
|       }); | ||||
|     }); | ||||
|   }); | ||||
|   | ||||
							
								
								
									
										1
									
								
								mobile/openapi/doc/AlbumResponseDto.md
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1
									
								
								mobile/openapi/doc/AlbumResponseDto.md
									
									
									
										generated
									
									
									
								
							| @@ -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** |  |  | ||||
|   | ||||
							
								
								
									
										12
									
								
								mobile/openapi/doc/AssetApi.md
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										12
									
								
								mobile/openapi/doc/AssetApi.md
									
									
									
										generated
									
									
									
								
							| @@ -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<AssetResponseDto> getTimeBucket(size, timeBucket, albumId, isArchived, isFavorite, isTrashed, key, personId, userId, withPartners, withStacked) | ||||
| > List<AssetResponseDto> 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<TimeBucketResponseDto> getTimeBuckets(size, albumId, isArchived, isFavorite, isTrashed, key, personId, userId, withPartners, withStacked) | ||||
| > List<TimeBucketResponseDto> 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]  | ||||
|   | ||||
							
								
								
									
										1
									
								
								mobile/openapi/doc/UpdateAlbumDto.md
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1
									
								
								mobile/openapi/doc/UpdateAlbumDto.md
									
									
									
										generated
									
									
									
								
							| @@ -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) | ||||
| 
 | ||||
|   | ||||
							
								
								
									
										26
									
								
								mobile/openapi/lib/api/asset_api.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										26
									
								
								mobile/openapi/lib/api/asset_api.dart
									
									
									
										generated
									
									
									
								
							| @@ -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<Response> 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<Response> 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<List<AssetResponseDto>?> 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<List<AssetResponseDto>?> 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<Response> getTimeBucketsWithHttpInfo(TimeBucketSize size, { String? albumId, bool? isArchived, bool? isFavorite, bool? isTrashed, String? key, String? personId, String? userId, bool? withPartners, bool? withStacked, }) async { | ||||
|   Future<Response> 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<List<TimeBucketResponseDto>?> 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<List<TimeBucketResponseDto>?> 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)); | ||||
|     } | ||||
|   | ||||
							
								
								
									
										19
									
								
								mobile/openapi/lib/model/album_response_dto.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										19
									
								
								mobile/openapi/lib/model/album_response_dto.dart
									
									
									
										generated
									
									
									
								
							| @@ -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<String, dynamic> toJson() { | ||||
|     final json = <String, dynamic>{}; | ||||
| @@ -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<String>(json, r'id')!, | ||||
|         isActivityEnabled: mapValueOfType<bool>(json, r'isActivityEnabled')!, | ||||
|         lastModifiedAssetTimestamp: mapDateTime(json, r'lastModifiedAssetTimestamp', r''), | ||||
|         order: AssetOrder.fromJson(json[r'order']), | ||||
|         owner: UserResponseDto.fromJson(json[r'owner'])!, | ||||
|         ownerId: mapValueOfType<String>(json, r'ownerId')!, | ||||
|         shared: mapValueOfType<bool>(json, r'shared')!, | ||||
|   | ||||
							
								
								
									
										23
									
								
								mobile/openapi/lib/model/update_album_dto.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										23
									
								
								mobile/openapi/lib/model/update_album_dto.dart
									
									
									
										generated
									
									
									
								
							| @@ -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<String, dynamic> toJson() { | ||||
|     final json = <String, dynamic>{}; | ||||
| @@ -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<String>(json, r'albumThumbnailAssetId'), | ||||
|         description: mapValueOfType<String>(json, r'description'), | ||||
|         isActivityEnabled: mapValueOfType<bool>(json, r'isActivityEnabled'), | ||||
|         order: AssetOrder.fromJson(json[r'order']), | ||||
|       ); | ||||
|     } | ||||
|     return null; | ||||
|   | ||||
							
								
								
									
										5
									
								
								mobile/openapi/test/album_response_dto_test.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										5
									
								
								mobile/openapi/test/album_response_dto_test.dart
									
									
									
										generated
									
									
									
								
							| @@ -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 | ||||
|   | ||||
							
								
								
									
										4
									
								
								mobile/openapi/test/asset_api_test.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										4
									
								
								mobile/openapi/test/asset_api_test.dart
									
									
									
										generated
									
									
									
								
							| @@ -95,12 +95,12 @@ void main() { | ||||
|       // TODO | ||||
|     }); | ||||
| 
 | ||||
|     //Future<List<AssetResponseDto>> 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<List<AssetResponseDto>> 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<List<TimeBucketResponseDto>> getTimeBuckets(TimeBucketSize size, { String albumId, bool isArchived, bool isFavorite, bool isTrashed, String key, String personId, String userId, bool withPartners, bool withStacked }) async | ||||
|     //Future<List<TimeBucketResponseDto>> 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 | ||||
|     }); | ||||
|   | ||||
							
								
								
									
										5
									
								
								mobile/openapi/test/update_album_dto_test.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										5
									
								
								mobile/openapi/test/update_album_dto_test.dart
									
									
									
										generated
									
									
									
								
							| @@ -36,6 +36,11 @@ void main() { | ||||
|       // TODO | ||||
|     }); | ||||
| 
 | ||||
|     // AssetOrder order | ||||
|     test('to test the property `order`', () async { | ||||
|       // TODO | ||||
|     }); | ||||
| 
 | ||||
| 
 | ||||
|   }); | ||||
| 
 | ||||
|   | ||||
| @@ -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" | ||||
|   | ||||
| @@ -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" | ||||
|   | ||||
| @@ -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, | ||||
|   }; | ||||
| }; | ||||
|  | ||||
|   | ||||
| @@ -148,6 +148,7 @@ export class AlbumService { | ||||
|       description: dto.description, | ||||
|       albumThumbnailAssetId: dto.albumThumbnailAssetId, | ||||
|       isActivityEnabled: dto.isActivityEnabled, | ||||
|       order: dto.order, | ||||
|     }); | ||||
|  | ||||
|     return mapAlbumWithoutAssets(updatedAlbum); | ||||
|   | ||||
| @@ -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; | ||||
| } | ||||
|   | ||||
| @@ -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); | ||||
|   | ||||
| @@ -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 { | ||||
|   | ||||
| @@ -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 { | ||||
|   | ||||
| @@ -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'; | ||||
|   | ||||
| @@ -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 { | ||||
|   | ||||
| @@ -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; | ||||
| } | ||||
|   | ||||
| @@ -0,0 +1,14 @@ | ||||
| import { MigrationInterface, QueryRunner } from "typeorm"; | ||||
|  | ||||
| export class AscendingOrderAlbum1710182081326 implements MigrationInterface { | ||||
|     name = 'AscendingOrderAlbum1710182081326' | ||||
|  | ||||
|     public async up(queryRunner: QueryRunner): Promise<void> { | ||||
|         await queryRunner.query(`ALTER TABLE "albums" ADD "order" character varying NOT NULL DEFAULT 'desc'`); | ||||
|     } | ||||
|  | ||||
|     public async down(queryRunner: QueryRunner): Promise<void> { | ||||
|         await queryRunner.query(`ALTER TABLE "albums" DROP COLUMN "order"`); | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -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() | ||||
|     ); | ||||
|   } | ||||
|   | ||||
| @@ -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", | ||||
|   | ||||
| @@ -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", | ||||
|   | ||||
							
								
								
									
										12
									
								
								server/test/fixtures/album.stub.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										12
									
								
								server/test/fixtures/album.stub.ts
									
									
									
									
										vendored
									
									
								
							| @@ -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<AlbumEntity>({ | ||||
|     id: 'album-2', | ||||
| @@ -35,6 +36,7 @@ export const albumStub = { | ||||
|     sharedLinks: [], | ||||
|     sharedUsers: [userStub.user1], | ||||
|     isActivityEnabled: true, | ||||
|     order: AssetOrder.DESC, | ||||
|   }), | ||||
|   sharedWithMultiple: Object.freeze<AlbumEntity>({ | ||||
|     id: 'album-3', | ||||
| @@ -51,6 +53,7 @@ export const albumStub = { | ||||
|     sharedLinks: [], | ||||
|     sharedUsers: [userStub.user1, userStub.user2], | ||||
|     isActivityEnabled: true, | ||||
|     order: AssetOrder.DESC, | ||||
|   }), | ||||
|   sharedWithAdmin: Object.freeze<AlbumEntity>({ | ||||
|     id: 'album-3', | ||||
| @@ -67,6 +70,7 @@ export const albumStub = { | ||||
|     sharedLinks: [], | ||||
|     sharedUsers: [userStub.admin], | ||||
|     isActivityEnabled: true, | ||||
|     order: AssetOrder.DESC, | ||||
|   }), | ||||
|   oneAsset: Object.freeze<AlbumEntity>({ | ||||
|     id: 'album-4', | ||||
| @@ -83,6 +87,7 @@ export const albumStub = { | ||||
|     sharedLinks: [], | ||||
|     sharedUsers: [], | ||||
|     isActivityEnabled: true, | ||||
|     order: AssetOrder.DESC, | ||||
|   }), | ||||
|   twoAssets: Object.freeze<AlbumEntity>({ | ||||
|     id: 'album-4a', | ||||
| @@ -99,6 +104,7 @@ export const albumStub = { | ||||
|     sharedLinks: [], | ||||
|     sharedUsers: [], | ||||
|     isActivityEnabled: true, | ||||
|     order: AssetOrder.DESC, | ||||
|   }), | ||||
|   emptyWithInvalidThumbnail: Object.freeze<AlbumEntity>({ | ||||
|     id: 'album-5', | ||||
| @@ -115,6 +121,7 @@ export const albumStub = { | ||||
|     sharedLinks: [], | ||||
|     sharedUsers: [], | ||||
|     isActivityEnabled: true, | ||||
|     order: AssetOrder.DESC, | ||||
|   }), | ||||
|   emptyWithValidThumbnail: Object.freeze<AlbumEntity>({ | ||||
|     id: 'album-5', | ||||
| @@ -131,6 +138,7 @@ export const albumStub = { | ||||
|     sharedLinks: [], | ||||
|     sharedUsers: [], | ||||
|     isActivityEnabled: true, | ||||
|     order: AssetOrder.DESC, | ||||
|   }), | ||||
|   oneAssetInvalidThumbnail: Object.freeze<AlbumEntity>({ | ||||
|     id: 'album-6', | ||||
| @@ -147,6 +155,7 @@ export const albumStub = { | ||||
|     sharedLinks: [], | ||||
|     sharedUsers: [], | ||||
|     isActivityEnabled: true, | ||||
|     order: AssetOrder.DESC, | ||||
|   }), | ||||
|   oneAssetValidThumbnail: Object.freeze<AlbumEntity>({ | ||||
|     id: 'album-6', | ||||
| @@ -163,5 +172,6 @@ export const albumStub = { | ||||
|     sharedLinks: [], | ||||
|     sharedUsers: [], | ||||
|     isActivityEnabled: true, | ||||
|     order: AssetOrder.DESC, | ||||
|   }), | ||||
| }; | ||||
|   | ||||
							
								
								
									
										4
									
								
								server/test/fixtures/shared-link.stub.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								server/test/fixtures/shared-link.stub.ts
									
									
									
									
										vendored
									
									
								
							| @@ -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', | ||||
|   | ||||
| @@ -1,22 +1,55 @@ | ||||
| <script lang="ts"> | ||||
|   import Icon from '$lib/components/elements/icon.svelte'; | ||||
|   import type { AlbumResponseDto, UserResponseDto } from '@immich/sdk'; | ||||
|   import { mdiClose, mdiPlus } from '@mdi/js'; | ||||
|   import { updateAlbumInfo, type AlbumResponseDto, type UserResponseDto, AssetOrder } from '@immich/sdk'; | ||||
|   import { mdiArrowDownThin, mdiArrowUpThin, mdiClose, mdiPlus } from '@mdi/js'; | ||||
|   import { createEventDispatcher } from 'svelte'; | ||||
|  | ||||
|   import CircleIconButton from '../elements/buttons/circle-icon-button.svelte'; | ||||
|   import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte'; | ||||
|   import UserAvatar from '$lib/components/shared-components/user-avatar.svelte'; | ||||
|   import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte'; | ||||
|   import SettingDropdown from '../shared-components/settings/setting-dropdown.svelte'; | ||||
|   import type { RenderedOption } from '../elements/dropdown.svelte'; | ||||
|   import { handleError } from '$lib/utils/handle-error'; | ||||
|   import { findKey } from 'lodash-es'; | ||||
|  | ||||
|   export let album: AlbumResponseDto; | ||||
|   export let order: AssetOrder | undefined; | ||||
|   export let user: UserResponseDto; | ||||
|   export let onChangeOrder: (order: AssetOrder) => void; | ||||
|  | ||||
|   const options: Record<AssetOrder, RenderedOption> = { | ||||
|     [AssetOrder.Asc]: { icon: mdiArrowUpThin, title: 'Oldest first' }, | ||||
|     [AssetOrder.Desc]: { icon: mdiArrowDownThin, title: 'Newest first' }, | ||||
|   }; | ||||
|  | ||||
|   $: selectedOption = order ? options[order] : options[AssetOrder.Desc]; | ||||
|  | ||||
|   const dispatch = createEventDispatcher<{ | ||||
|     close: void; | ||||
|     toggleEnableActivity: void; | ||||
|     showSelectSharedUser: void; | ||||
|   }>(); | ||||
|  | ||||
|   const handleToggle = async (returnedOption: RenderedOption) => { | ||||
|     if (selectedOption === returnedOption) { | ||||
|       return; | ||||
|     } | ||||
|     let order = AssetOrder.Desc; | ||||
|     order = findKey(options, (option) => option === returnedOption) as AssetOrder; | ||||
|  | ||||
|     try { | ||||
|       await updateAlbumInfo({ | ||||
|         id: album.id, | ||||
|         updateAlbumDto: { | ||||
|           order, | ||||
|         }, | ||||
|       }); | ||||
|       onChangeOrder(order); | ||||
|     } catch (error) { | ||||
|       handleError(error, 'Error updating album order'); | ||||
|     } | ||||
|   }; | ||||
| </script> | ||||
|  | ||||
| <FullScreenModal onClose={() => dispatch('close')}> | ||||
| @@ -34,8 +67,16 @@ | ||||
|  | ||||
|         <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"> | ||||
|             <h2 class="text-gray text-sm mb-2">SETTINGS</h2> | ||||
|             <div class="grid p-2 gap-y-2"> | ||||
|               {#if order} | ||||
|                 <SettingDropdown | ||||
|                   title="Display order" | ||||
|                   options={Object.values(options)} | ||||
|                   selectedOption={options[order]} | ||||
|                   onToggle={handleToggle} | ||||
|                 /> | ||||
|               {/if} | ||||
|               <SettingSwitch | ||||
|                 title="Comments & likes" | ||||
|                 subtitle="Let others respond" | ||||
|   | ||||
| @@ -20,7 +20,7 @@ | ||||
|     [SlideshowNavigation.DescendingOrder]: { icon: mdiArrowDownThin, title: 'Forward' }, | ||||
|   }; | ||||
|  | ||||
|   export const handleToggle = (selectedOption: RenderedOption) => { | ||||
|   const handleToggle = (selectedOption: RenderedOption) => { | ||||
|     for (const [key, option] of Object.entries(options)) { | ||||
|       if (option === selectedOption) { | ||||
|         $slideshowNavigation = key as SlideshowNavigation; | ||||
|   | ||||
| @@ -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; | ||||
|  | ||||
|   | ||||
| @@ -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`} | ||||
|     > | ||||
|       <!-- Use key because AssetGrid can't deal with changing stores --> | ||||
|       {#key albumId} | ||||
|       {#key albumKey} | ||||
|         {#if viewMode === ViewMode.SELECT_ASSETS} | ||||
|           <AssetGrid | ||||
|             assetStore={timelineStore} | ||||
| @@ -679,7 +682,9 @@ | ||||
| {#if viewMode === ViewMode.OPTIONS && $user} | ||||
|   <AlbumOptions | ||||
|     {album} | ||||
|     order={albumOrder} | ||||
|     user={$user} | ||||
|     onChangeOrder={(order) => (albumOrder = order)} | ||||
|     on:close={() => (viewMode = ViewMode.VIEW)} | ||||
|     on:toggleEnableActivity={handleToggleEnableActivity} | ||||
|     on:showSelectSharedUser={() => (viewMode = ViewMode.SELECT_USERS)} | ||||
|   | ||||
| @@ -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<AlbumResponseDto>({ | ||||
|   sharedUsers: [], | ||||
|   hasSharedLink: false, | ||||
|   isActivityEnabled: true, | ||||
|   order: AssetOrder.Desc, | ||||
| }); | ||||
|   | ||||
		Reference in New Issue
	
	Block a user