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:
martin 2024-03-14 17:45:03 +01:00 committed by GitHub
parent 1c4637cb43
commit 31f7e1aca3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
32 changed files with 251 additions and 49 deletions

View File

@ -1,6 +1,7 @@
import { import {
AlbumResponseDto, AlbumResponseDto,
AssetFileUploadResponseDto, AssetFileUploadResponseDto,
AssetOrder,
LoginResponseDto, LoginResponseDto,
SharedLinkType, SharedLinkType,
deleteUser, deleteUser,
@ -353,6 +354,7 @@ describe('/album', () => {
assetCount: 0, assetCount: 0,
owner: expect.objectContaining({ email: user1.userEmail }), owner: expect.objectContaining({ email: user1.userEmail }),
isActivityEnabled: true, isActivityEnabled: true,
order: AssetOrder.Desc,
}); });
}); });
}); });

View File

@ -19,6 +19,7 @@ Name | Type | Description | Notes
**id** | **String** | | **id** | **String** | |
**isActivityEnabled** | **bool** | | **isActivityEnabled** | **bool** | |
**lastModifiedAssetTimestamp** | [**DateTime**](DateTime.md) | | [optional] **lastModifiedAssetTimestamp** | [**DateTime**](DateTime.md) | | [optional]
**order** | [**AssetOrder**](AssetOrder.md) | | [optional]
**owner** | [**UserResponseDto**](UserResponseDto.md) | | **owner** | [**UserResponseDto**](UserResponseDto.md) | |
**ownerId** | **String** | | **ownerId** | **String** | |
**shared** | **bool** | | **shared** | **bool** | |

View File

@ -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) [[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** # **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 isFavorite = true; // bool |
final isTrashed = true; // bool | final isTrashed = true; // bool |
final key = key_example; // String | final key = key_example; // String |
final order = ; // AssetOrder |
final personId = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String | final personId = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String |
final userId = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String | final userId = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String |
final withPartners = true; // bool | final withPartners = true; // bool |
final withStacked = true; // bool | final withStacked = true; // bool |
try { 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); print(result);
} catch (e) { } catch (e) {
print('Exception when calling AssetApi->getTimeBucket: $e\n'); print('Exception when calling AssetApi->getTimeBucket: $e\n');
@ -888,6 +889,7 @@ Name | Type | Description | Notes
**isFavorite** | **bool**| | [optional] **isFavorite** | **bool**| | [optional]
**isTrashed** | **bool**| | [optional] **isTrashed** | **bool**| | [optional]
**key** | **String**| | [optional] **key** | **String**| | [optional]
**order** | [**AssetOrder**](.md)| | [optional]
**personId** | **String**| | [optional] **personId** | **String**| | [optional]
**userId** | **String**| | [optional] **userId** | **String**| | [optional]
**withPartners** | **bool**| | [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) [[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** # **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 isFavorite = true; // bool |
final isTrashed = true; // bool | final isTrashed = true; // bool |
final key = key_example; // String | final key = key_example; // String |
final order = ; // AssetOrder |
final personId = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String | final personId = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String |
final userId = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String | final userId = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String |
final withPartners = true; // bool | final withPartners = true; // bool |
final withStacked = true; // bool | final withStacked = true; // bool |
try { 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); print(result);
} catch (e) { } catch (e) {
print('Exception when calling AssetApi->getTimeBuckets: $e\n'); print('Exception when calling AssetApi->getTimeBuckets: $e\n');
@ -961,6 +964,7 @@ Name | Type | Description | Notes
**isFavorite** | **bool**| | [optional] **isFavorite** | **bool**| | [optional]
**isTrashed** | **bool**| | [optional] **isTrashed** | **bool**| | [optional]
**key** | **String**| | [optional] **key** | **String**| | [optional]
**order** | [**AssetOrder**](.md)| | [optional]
**personId** | **String**| | [optional] **personId** | **String**| | [optional]
**userId** | **String**| | [optional] **userId** | **String**| | [optional]
**withPartners** | **bool**| | [optional] **withPartners** | **bool**| | [optional]

View File

@ -12,6 +12,7 @@ Name | Type | Description | Notes
**albumThumbnailAssetId** | **String** | | [optional] **albumThumbnailAssetId** | **String** | | [optional]
**description** | **String** | | [optional] **description** | **String** | | [optional]
**isActivityEnabled** | **bool** | | [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) [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)

View File

@ -852,6 +852,8 @@ class AssetApi {
/// ///
/// * [String] key: /// * [String] key:
/// ///
/// * [AssetOrder] order:
///
/// * [String] personId: /// * [String] personId:
/// ///
/// * [String] userId: /// * [String] userId:
@ -859,7 +861,7 @@ class AssetApi {
/// * [bool] withPartners: /// * [bool] withPartners:
/// ///
/// * [bool] withStacked: /// * [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 // ignore: prefer_const_declarations
final path = r'/asset/time-bucket'; final path = r'/asset/time-bucket';
@ -885,6 +887,9 @@ class AssetApi {
if (key != null) { if (key != null) {
queryParams.addAll(_queryParams('', 'key', key)); queryParams.addAll(_queryParams('', 'key', key));
} }
if (order != null) {
queryParams.addAll(_queryParams('', 'order', order));
}
if (personId != null) { if (personId != null) {
queryParams.addAll(_queryParams('', 'personId', personId)); queryParams.addAll(_queryParams('', 'personId', personId));
} }
@ -930,6 +935,8 @@ class AssetApi {
/// ///
/// * [String] key: /// * [String] key:
/// ///
/// * [AssetOrder] order:
///
/// * [String] personId: /// * [String] personId:
/// ///
/// * [String] userId: /// * [String] userId:
@ -937,8 +944,8 @@ class AssetApi {
/// * [bool] withPartners: /// * [bool] withPartners:
/// ///
/// * [bool] withStacked: /// * [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 { 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, personId: personId, userId: userId, withPartners: withPartners, withStacked: withStacked, ); 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) { if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response)); throw ApiException(response.statusCode, await _decodeBodyBytes(response));
} }
@ -970,6 +977,8 @@ class AssetApi {
/// ///
/// * [String] key: /// * [String] key:
/// ///
/// * [AssetOrder] order:
///
/// * [String] personId: /// * [String] personId:
/// ///
/// * [String] userId: /// * [String] userId:
@ -977,7 +986,7 @@ class AssetApi {
/// * [bool] withPartners: /// * [bool] withPartners:
/// ///
/// * [bool] withStacked: /// * [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 // ignore: prefer_const_declarations
final path = r'/asset/time-buckets'; final path = r'/asset/time-buckets';
@ -1003,6 +1012,9 @@ class AssetApi {
if (key != null) { if (key != null) {
queryParams.addAll(_queryParams('', 'key', key)); queryParams.addAll(_queryParams('', 'key', key));
} }
if (order != null) {
queryParams.addAll(_queryParams('', 'order', order));
}
if (personId != null) { if (personId != null) {
queryParams.addAll(_queryParams('', 'personId', personId)); queryParams.addAll(_queryParams('', 'personId', personId));
} }
@ -1045,6 +1057,8 @@ class AssetApi {
/// ///
/// * [String] key: /// * [String] key:
/// ///
/// * [AssetOrder] order:
///
/// * [String] personId: /// * [String] personId:
/// ///
/// * [String] userId: /// * [String] userId:
@ -1052,8 +1066,8 @@ class AssetApi {
/// * [bool] withPartners: /// * [bool] withPartners:
/// ///
/// * [bool] withStacked: /// * [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 { 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, personId: personId, userId: userId, withPartners: withPartners, withStacked: withStacked, ); 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) { if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response)); throw ApiException(response.statusCode, await _decodeBodyBytes(response));
} }

View File

@ -24,6 +24,7 @@ class AlbumResponseDto {
required this.id, required this.id,
required this.isActivityEnabled, required this.isActivityEnabled,
this.lastModifiedAssetTimestamp, this.lastModifiedAssetTimestamp,
this.order,
required this.owner, required this.owner,
required this.ownerId, required this.ownerId,
required this.shared, required this.shared,
@ -66,6 +67,14 @@ class AlbumResponseDto {
/// ///
DateTime? lastModifiedAssetTimestamp; 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; UserResponseDto owner;
String ownerId; String ownerId;
@ -97,6 +106,7 @@ class AlbumResponseDto {
other.id == id && other.id == id &&
other.isActivityEnabled == isActivityEnabled && other.isActivityEnabled == isActivityEnabled &&
other.lastModifiedAssetTimestamp == lastModifiedAssetTimestamp && other.lastModifiedAssetTimestamp == lastModifiedAssetTimestamp &&
other.order == order &&
other.owner == owner && other.owner == owner &&
other.ownerId == ownerId && other.ownerId == ownerId &&
other.shared == shared && other.shared == shared &&
@ -118,6 +128,7 @@ class AlbumResponseDto {
(id.hashCode) + (id.hashCode) +
(isActivityEnabled.hashCode) + (isActivityEnabled.hashCode) +
(lastModifiedAssetTimestamp == null ? 0 : lastModifiedAssetTimestamp!.hashCode) + (lastModifiedAssetTimestamp == null ? 0 : lastModifiedAssetTimestamp!.hashCode) +
(order == null ? 0 : order!.hashCode) +
(owner.hashCode) + (owner.hashCode) +
(ownerId.hashCode) + (ownerId.hashCode) +
(shared.hashCode) + (shared.hashCode) +
@ -126,7 +137,7 @@ class AlbumResponseDto {
(updatedAt.hashCode); (updatedAt.hashCode);
@override @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() { Map<String, dynamic> toJson() {
final json = <String, dynamic>{}; final json = <String, dynamic>{};
@ -152,6 +163,11 @@ class AlbumResponseDto {
json[r'lastModifiedAssetTimestamp'] = this.lastModifiedAssetTimestamp!.toUtc().toIso8601String(); json[r'lastModifiedAssetTimestamp'] = this.lastModifiedAssetTimestamp!.toUtc().toIso8601String();
} else { } else {
// json[r'lastModifiedAssetTimestamp'] = null; // 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'owner'] = this.owner;
json[r'ownerId'] = this.ownerId; json[r'ownerId'] = this.ownerId;
@ -185,6 +201,7 @@ class AlbumResponseDto {
id: mapValueOfType<String>(json, r'id')!, id: mapValueOfType<String>(json, r'id')!,
isActivityEnabled: mapValueOfType<bool>(json, r'isActivityEnabled')!, isActivityEnabled: mapValueOfType<bool>(json, r'isActivityEnabled')!,
lastModifiedAssetTimestamp: mapDateTime(json, r'lastModifiedAssetTimestamp', r''), lastModifiedAssetTimestamp: mapDateTime(json, r'lastModifiedAssetTimestamp', r''),
order: AssetOrder.fromJson(json[r'order']),
owner: UserResponseDto.fromJson(json[r'owner'])!, owner: UserResponseDto.fromJson(json[r'owner'])!,
ownerId: mapValueOfType<String>(json, r'ownerId')!, ownerId: mapValueOfType<String>(json, r'ownerId')!,
shared: mapValueOfType<bool>(json, r'shared')!, shared: mapValueOfType<bool>(json, r'shared')!,

View File

@ -17,6 +17,7 @@ class UpdateAlbumDto {
this.albumThumbnailAssetId, this.albumThumbnailAssetId,
this.description, this.description,
this.isActivityEnabled, this.isActivityEnabled,
this.order,
}); });
/// ///
@ -51,12 +52,21 @@ class UpdateAlbumDto {
/// ///
bool? isActivityEnabled; 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 @override
bool operator ==(Object other) => identical(this, other) || other is UpdateAlbumDto && bool operator ==(Object other) => identical(this, other) || other is UpdateAlbumDto &&
other.albumName == albumName && other.albumName == albumName &&
other.albumThumbnailAssetId == albumThumbnailAssetId && other.albumThumbnailAssetId == albumThumbnailAssetId &&
other.description == description && other.description == description &&
other.isActivityEnabled == isActivityEnabled; other.isActivityEnabled == isActivityEnabled &&
other.order == order;
@override @override
int get hashCode => int get hashCode =>
@ -64,10 +74,11 @@ class UpdateAlbumDto {
(albumName == null ? 0 : albumName!.hashCode) + (albumName == null ? 0 : albumName!.hashCode) +
(albumThumbnailAssetId == null ? 0 : albumThumbnailAssetId!.hashCode) + (albumThumbnailAssetId == null ? 0 : albumThumbnailAssetId!.hashCode) +
(description == null ? 0 : description!.hashCode) + (description == null ? 0 : description!.hashCode) +
(isActivityEnabled == null ? 0 : isActivityEnabled!.hashCode); (isActivityEnabled == null ? 0 : isActivityEnabled!.hashCode) +
(order == null ? 0 : order!.hashCode);
@override @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() { Map<String, dynamic> toJson() {
final json = <String, dynamic>{}; final json = <String, dynamic>{};
@ -91,6 +102,11 @@ class UpdateAlbumDto {
} else { } else {
// json[r'isActivityEnabled'] = null; // json[r'isActivityEnabled'] = null;
} }
if (this.order != null) {
json[r'order'] = this.order;
} else {
// json[r'order'] = null;
}
return json; return json;
} }
@ -106,6 +122,7 @@ class UpdateAlbumDto {
albumThumbnailAssetId: mapValueOfType<String>(json, r'albumThumbnailAssetId'), albumThumbnailAssetId: mapValueOfType<String>(json, r'albumThumbnailAssetId'),
description: mapValueOfType<String>(json, r'description'), description: mapValueOfType<String>(json, r'description'),
isActivityEnabled: mapValueOfType<bool>(json, r'isActivityEnabled'), isActivityEnabled: mapValueOfType<bool>(json, r'isActivityEnabled'),
order: AssetOrder.fromJson(json[r'order']),
); );
} }
return null; return null;

View File

@ -71,6 +71,11 @@ void main() {
// TODO // TODO
}); });
// AssetOrder order
test('to test the property `order`', () async {
// TODO
});
// UserResponseDto owner // UserResponseDto owner
test('to test the property `owner`', () async { test('to test the property `owner`', () async {
// TODO // TODO

View File

@ -95,12 +95,12 @@ void main() {
// TODO // 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 { test('test getTimeBucket', () async {
// TODO // 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 { test('test getTimeBuckets', () async {
// TODO // TODO
}); });

View File

@ -36,6 +36,11 @@ void main() {
// TODO // TODO
}); });
// AssetOrder order
test('to test the property `order`', () async {
// TODO
});
}); });

View File

@ -1765,6 +1765,14 @@
"type": "string" "type": "string"
} }
}, },
{
"name": "order",
"required": false,
"in": "query",
"schema": {
"$ref": "#/components/schemas/AssetOrder"
}
},
{ {
"name": "personId", "name": "personId",
"required": false, "required": false,
@ -1901,6 +1909,14 @@
"type": "string" "type": "string"
} }
}, },
{
"name": "order",
"required": false,
"in": "query",
"schema": {
"$ref": "#/components/schemas/AssetOrder"
}
},
{ {
"name": "personId", "name": "personId",
"required": false, "required": false,
@ -6722,6 +6738,9 @@
"format": "date-time", "format": "date-time",
"type": "string" "type": "string"
}, },
"order": {
"$ref": "#/components/schemas/AssetOrder"
},
"owner": { "owner": {
"$ref": "#/components/schemas/UserResponseDto" "$ref": "#/components/schemas/UserResponseDto"
}, },
@ -10335,6 +10354,9 @@
}, },
"isActivityEnabled": { "isActivityEnabled": {
"type": "boolean" "type": "boolean"
},
"order": {
"$ref": "#/components/schemas/AssetOrder"
} }
}, },
"type": "object" "type": "object"

View File

@ -153,6 +153,7 @@ export type AlbumResponseDto = {
id: string; id: string;
isActivityEnabled: boolean; isActivityEnabled: boolean;
lastModifiedAssetTimestamp?: string; lastModifiedAssetTimestamp?: string;
order?: AssetOrder;
owner: UserResponseDto; owner: UserResponseDto;
ownerId: string; ownerId: string;
shared: boolean; shared: boolean;
@ -176,6 +177,7 @@ export type UpdateAlbumDto = {
albumThumbnailAssetId?: string; albumThumbnailAssetId?: string;
description?: string; description?: string;
isActivityEnabled?: boolean; isActivityEnabled?: boolean;
order?: AssetOrder;
}; };
export type BulkIdsDto = { export type BulkIdsDto = {
ids: string[]; ids: string[];
@ -1453,12 +1455,13 @@ export function getAssetThumbnail({ format, id, key }: {
...opts ...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; albumId?: string;
isArchived?: boolean; isArchived?: boolean;
isFavorite?: boolean; isFavorite?: boolean;
isTrashed?: boolean; isTrashed?: boolean;
key?: string; key?: string;
order?: AssetOrder;
personId?: string; personId?: string;
size: TimeBucketSize; size: TimeBucketSize;
timeBucket: string; timeBucket: string;
@ -1475,6 +1478,7 @@ export function getTimeBucket({ albumId, isArchived, isFavorite, isTrashed, key,
isFavorite, isFavorite,
isTrashed, isTrashed,
key, key,
order,
personId, personId,
size, size,
timeBucket, timeBucket,
@ -1485,12 +1489,13 @@ export function getTimeBucket({ albumId, isArchived, isFavorite, isTrashed, key,
...opts ...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; albumId?: string;
isArchived?: boolean; isArchived?: boolean;
isFavorite?: boolean; isFavorite?: boolean;
isTrashed?: boolean; isTrashed?: boolean;
key?: string; key?: string;
order?: AssetOrder;
personId?: string; personId?: string;
size: TimeBucketSize; size: TimeBucketSize;
userId?: string; userId?: string;
@ -1506,6 +1511,7 @@ export function getTimeBuckets({ albumId, isArchived, isFavorite, isTrashed, key
isFavorite, isFavorite,
isTrashed, isTrashed,
key, key,
order,
personId, personId,
size, size,
userId, userId,
@ -2747,6 +2753,10 @@ export enum AssetTypeEnum {
Audio = "AUDIO", Audio = "AUDIO",
Other = "OTHER" Other = "OTHER"
} }
export enum AssetOrder {
Asc = "asc",
Desc = "desc"
}
export enum Error { export enum Error {
Duplicate = "duplicate", Duplicate = "duplicate",
NoPermission = "no_permission", NoPermission = "no_permission",
@ -2774,10 +2784,6 @@ export enum TimeBucketSize {
Day = "DAY", Day = "DAY",
Month = "MONTH" Month = "MONTH"
} }
export enum AssetOrder {
Asc = "asc",
Desc = "desc"
}
export enum EntityType { export enum EntityType {
Asset = "ASSET", Asset = "ASSET",
Album = "ALBUM" Album = "ALBUM"

View File

@ -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 { ApiProperty } from '@nestjs/swagger';
import { AssetResponseDto, mapAsset } from '../asset'; import { AssetResponseDto, mapAsset } from '../asset';
import { AuthDto } from '../auth/auth.dto'; import { AuthDto } from '../auth/auth.dto';
@ -23,6 +24,9 @@ export class AlbumResponseDto {
startDate?: Date; startDate?: Date;
endDate?: Date; endDate?: Date;
isActivityEnabled!: boolean; isActivityEnabled!: boolean;
@Optional()
@ApiProperty({ enumName: 'AssetOrder', enum: AssetOrder })
order?: AssetOrder;
} }
export const mapAlbum = (entity: AlbumEntity, withAssets: boolean, auth?: AuthDto): AlbumResponseDto => { 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 })), assets: (withAssets ? assets : []).map((asset) => mapAsset(asset, { auth })),
assetCount: entity.assets?.length || 0, assetCount: entity.assets?.length || 0,
isActivityEnabled: entity.isActivityEnabled, isActivityEnabled: entity.isActivityEnabled,
order: entity.order,
}; };
}; };

View File

@ -148,6 +148,7 @@ export class AlbumService {
description: dto.description, description: dto.description,
albumThumbnailAssetId: dto.albumThumbnailAssetId, albumThumbnailAssetId: dto.albumThumbnailAssetId,
isActivityEnabled: dto.isActivityEnabled, isActivityEnabled: dto.isActivityEnabled,
order: dto.order,
}); });
return mapAlbumWithoutAssets(updatedAlbum); return mapAlbumWithoutAssets(updatedAlbum);

View File

@ -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'; import { Optional, ValidateBoolean, ValidateUUID } from '../../domain.util';
export class UpdateAlbumDto { export class UpdateAlbumDto {
@ -15,4 +17,9 @@ export class UpdateAlbumDto {
@ValidateBoolean({ optional: true }) @ValidateBoolean({ optional: true })
isActivityEnabled?: boolean; isActivityEnabled?: boolean;
@IsEnum(AssetOrder)
@Optional()
@ApiProperty({ enum: AssetOrder, enumName: 'AssetOrder' })
order?: AssetOrder;
} }

View File

@ -18,11 +18,6 @@ export class DeviceIdDto {
deviceId!: string; deviceId!: string;
} }
export enum AssetOrder {
ASC = 'asc',
DESC = 'desc',
}
const hasGPS = (o: { latitude: undefined; longitude: undefined }) => const hasGPS = (o: { latitude: undefined; longitude: undefined }) =>
o.latitude !== undefined || o.longitude !== undefined; o.latitude !== undefined || o.longitude !== undefined;
const ValidateGPS = () => ValidateIf(hasGPS); const ValidateGPS = () => ValidateIf(hasGPS);

View File

@ -1,6 +1,7 @@
import { AssetOrder } from '@app/infra/entities';
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
import { IsEnum, IsNotEmpty, IsString } from 'class-validator'; import { IsEnum, IsNotEmpty, IsString } from 'class-validator';
import { ValidateBoolean, ValidateUUID } from '../../domain.util'; import { Optional, ValidateBoolean, ValidateUUID } from '../../domain.util';
import { TimeBucketSize } from '../../repositories'; import { TimeBucketSize } from '../../repositories';
export class TimeBucketDto { export class TimeBucketDto {
@ -32,6 +33,11 @@ export class TimeBucketDto {
@ValidateBoolean({ optional: true }) @ValidateBoolean({ optional: true })
withPartners?: boolean; withPartners?: boolean;
@IsEnum(AssetOrder)
@Optional()
@ApiProperty({ enum: AssetOrder, enumName: 'AssetOrder' })
order?: AssetOrder;
} }
export class TimeBucketAssetDto extends TimeBucketDto { export class TimeBucketAssetDto extends TimeBucketDto {

View File

@ -1,5 +1,5 @@
import { AssetSearchOptions, ReverseGeocodeResult, SearchExploreItem } from '@app/domain'; 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 { FindOptionsRelations, FindOptionsSelect } from 'typeorm';
import { Paginated, PaginationOptions } from '../domain.util'; import { Paginated, PaginationOptions } from '../domain.util';
@ -66,6 +66,7 @@ export interface AssetBuilderOptions {
export interface TimeBucketOptions extends AssetBuilderOptions { export interface TimeBucketOptions extends AssetBuilderOptions {
size: TimeBucketSize; size: TimeBucketSize;
order?: AssetOrder;
} }
export interface TimeBucketItem { export interface TimeBucketItem {

View File

@ -1,5 +1,4 @@
import { AssetOrder } from '@app/domain/asset/dto/asset.dto'; import { AssetOrder, AssetType, GeodataPlacesEntity } from '@app/infra/entities';
import { AssetType, GeodataPlacesEntity } from '@app/infra/entities';
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer'; import { Type } from 'class-transformer';
import { IsEnum, IsInt, IsNotEmpty, IsString, Max, Min } from 'class-validator'; import { IsEnum, IsInt, IsNotEmpty, IsString, Max, Min } from 'class-validator';

View File

@ -1,6 +1,6 @@
import { AssetEntity } from '@app/infra/entities'; import { AssetEntity, AssetOrder } from '@app/infra/entities';
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { AssetOrder, AssetResponseDto, mapAsset } from '../asset'; import { AssetResponseDto, mapAsset } from '../asset';
import { AuthDto } from '../auth'; import { AuthDto } from '../auth';
import { PersonResponseDto } from '../person'; import { PersonResponseDto } from '../person';
import { import {

View File

@ -14,6 +14,12 @@ import { AssetEntity } from './asset.entity';
import { SharedLinkEntity } from './shared-link.entity'; import { SharedLinkEntity } from './shared-link.entity';
import { UserEntity } from './user.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') @Entity('albums')
export class AlbumEntity { export class AlbumEntity {
@PrimaryGeneratedColumn('uuid') @PrimaryGeneratedColumn('uuid')
@ -59,4 +65,7 @@ export class AlbumEntity {
@Column({ default: true }) @Column({ default: true })
isActivityEnabled!: boolean; isActivityEnabled!: boolean;
@Column({ type: 'varchar', default: AssetOrder.DESC })
order!: AssetOrder;
} }

View File

@ -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"`);
}
}

View File

@ -36,7 +36,7 @@ import {
Not, Not,
Repository, Repository,
} from 'typeorm'; } 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 { DummyValue, GenerateSql } from '../infra.util';
import { Chunked, ChunkedArray, OptionalBetween, paginate, paginatedBuilder, searchAssetBuilder } from '../infra.utils'; import { Chunked, ChunkedArray, OptionalBetween, paginate, paginatedBuilder, searchAssetBuilder } from '../infra.utils';
import { Instrumentation } from '../instrumentation'; import { Instrumentation } from '../instrumentation';
@ -607,7 +607,7 @@ export class AssetRepository implements IAssetRepository {
.select(`COUNT(asset.id)::int`, 'count') .select(`COUNT(asset.id)::int`, 'count')
.addSelect(truncated, 'timeBucket') .addSelect(truncated, 'timeBucket')
.groupBy(truncated) .groupBy(truncated)
.orderBy(truncated, 'DESC') .orderBy(truncated, options.order === AssetOrder.ASC ? 'ASC' : 'DESC')
.getRawMany(); .getRawMany();
} }
@ -620,7 +620,7 @@ export class AssetRepository implements IAssetRepository {
// First sort by the day in localtime (put it in the right bucket) // First sort by the day in localtime (put it in the right bucket)
.orderBy(truncated, 'DESC') .orderBy(truncated, 'DESC')
// and then sort by the actual time // and then sort by the actual time
.addOrderBy('asset.fileCreatedAt', 'DESC') .addOrderBy('asset.fileCreatedAt', options.order === AssetOrder.ASC ? 'ASC' : 'DESC')
.getMany() .getMany()
); );
} }

View File

@ -15,6 +15,7 @@ FROM
"AlbumEntity"."deletedAt" AS "AlbumEntity_deletedAt", "AlbumEntity"."deletedAt" AS "AlbumEntity_deletedAt",
"AlbumEntity"."albumThumbnailAssetId" AS "AlbumEntity_albumThumbnailAssetId", "AlbumEntity"."albumThumbnailAssetId" AS "AlbumEntity_albumThumbnailAssetId",
"AlbumEntity"."isActivityEnabled" AS "AlbumEntity_isActivityEnabled", "AlbumEntity"."isActivityEnabled" AS "AlbumEntity_isActivityEnabled",
"AlbumEntity"."order" AS "AlbumEntity_order",
"AlbumEntity__AlbumEntity_owner"."id" AS "AlbumEntity__AlbumEntity_owner_id", "AlbumEntity__AlbumEntity_owner"."id" AS "AlbumEntity__AlbumEntity_owner_id",
"AlbumEntity__AlbumEntity_owner"."name" AS "AlbumEntity__AlbumEntity_owner_name", "AlbumEntity__AlbumEntity_owner"."name" AS "AlbumEntity__AlbumEntity_owner_name",
"AlbumEntity__AlbumEntity_owner"."avatarColor" AS "AlbumEntity__AlbumEntity_owner_avatarColor", "AlbumEntity__AlbumEntity_owner"."avatarColor" AS "AlbumEntity__AlbumEntity_owner_avatarColor",
@ -91,6 +92,7 @@ SELECT
"AlbumEntity"."deletedAt" AS "AlbumEntity_deletedAt", "AlbumEntity"."deletedAt" AS "AlbumEntity_deletedAt",
"AlbumEntity"."albumThumbnailAssetId" AS "AlbumEntity_albumThumbnailAssetId", "AlbumEntity"."albumThumbnailAssetId" AS "AlbumEntity_albumThumbnailAssetId",
"AlbumEntity"."isActivityEnabled" AS "AlbumEntity_isActivityEnabled", "AlbumEntity"."isActivityEnabled" AS "AlbumEntity_isActivityEnabled",
"AlbumEntity"."order" AS "AlbumEntity_order",
"AlbumEntity__AlbumEntity_owner"."id" AS "AlbumEntity__AlbumEntity_owner_id", "AlbumEntity__AlbumEntity_owner"."id" AS "AlbumEntity__AlbumEntity_owner_id",
"AlbumEntity__AlbumEntity_owner"."name" AS "AlbumEntity__AlbumEntity_owner_name", "AlbumEntity__AlbumEntity_owner"."name" AS "AlbumEntity__AlbumEntity_owner_name",
"AlbumEntity__AlbumEntity_owner"."avatarColor" AS "AlbumEntity__AlbumEntity_owner_avatarColor", "AlbumEntity__AlbumEntity_owner"."avatarColor" AS "AlbumEntity__AlbumEntity_owner_avatarColor",
@ -149,6 +151,7 @@ SELECT
"AlbumEntity"."deletedAt" AS "AlbumEntity_deletedAt", "AlbumEntity"."deletedAt" AS "AlbumEntity_deletedAt",
"AlbumEntity"."albumThumbnailAssetId" AS "AlbumEntity_albumThumbnailAssetId", "AlbumEntity"."albumThumbnailAssetId" AS "AlbumEntity_albumThumbnailAssetId",
"AlbumEntity"."isActivityEnabled" AS "AlbumEntity_isActivityEnabled", "AlbumEntity"."isActivityEnabled" AS "AlbumEntity_isActivityEnabled",
"AlbumEntity"."order" AS "AlbumEntity_order",
"AlbumEntity__AlbumEntity_owner"."id" AS "AlbumEntity__AlbumEntity_owner_id", "AlbumEntity__AlbumEntity_owner"."id" AS "AlbumEntity__AlbumEntity_owner_id",
"AlbumEntity__AlbumEntity_owner"."name" AS "AlbumEntity__AlbumEntity_owner_name", "AlbumEntity__AlbumEntity_owner"."name" AS "AlbumEntity__AlbumEntity_owner_name",
"AlbumEntity__AlbumEntity_owner"."avatarColor" AS "AlbumEntity__AlbumEntity_owner_avatarColor", "AlbumEntity__AlbumEntity_owner"."avatarColor" AS "AlbumEntity__AlbumEntity_owner_avatarColor",
@ -279,6 +282,7 @@ SELECT
"AlbumEntity"."deletedAt" AS "AlbumEntity_deletedAt", "AlbumEntity"."deletedAt" AS "AlbumEntity_deletedAt",
"AlbumEntity"."albumThumbnailAssetId" AS "AlbumEntity_albumThumbnailAssetId", "AlbumEntity"."albumThumbnailAssetId" AS "AlbumEntity_albumThumbnailAssetId",
"AlbumEntity"."isActivityEnabled" AS "AlbumEntity_isActivityEnabled", "AlbumEntity"."isActivityEnabled" AS "AlbumEntity_isActivityEnabled",
"AlbumEntity"."order" AS "AlbumEntity_order",
"AlbumEntity__AlbumEntity_sharedUsers"."id" AS "AlbumEntity__AlbumEntity_sharedUsers_id", "AlbumEntity__AlbumEntity_sharedUsers"."id" AS "AlbumEntity__AlbumEntity_sharedUsers_id",
"AlbumEntity__AlbumEntity_sharedUsers"."name" AS "AlbumEntity__AlbumEntity_sharedUsers_name", "AlbumEntity__AlbumEntity_sharedUsers"."name" AS "AlbumEntity__AlbumEntity_sharedUsers_name",
"AlbumEntity__AlbumEntity_sharedUsers"."avatarColor" AS "AlbumEntity__AlbumEntity_sharedUsers_avatarColor", "AlbumEntity__AlbumEntity_sharedUsers"."avatarColor" AS "AlbumEntity__AlbumEntity_sharedUsers_avatarColor",
@ -352,6 +356,7 @@ SELECT
"AlbumEntity"."deletedAt" AS "AlbumEntity_deletedAt", "AlbumEntity"."deletedAt" AS "AlbumEntity_deletedAt",
"AlbumEntity"."albumThumbnailAssetId" AS "AlbumEntity_albumThumbnailAssetId", "AlbumEntity"."albumThumbnailAssetId" AS "AlbumEntity_albumThumbnailAssetId",
"AlbumEntity"."isActivityEnabled" AS "AlbumEntity_isActivityEnabled", "AlbumEntity"."isActivityEnabled" AS "AlbumEntity_isActivityEnabled",
"AlbumEntity"."order" AS "AlbumEntity_order",
"AlbumEntity__AlbumEntity_sharedUsers"."id" AS "AlbumEntity__AlbumEntity_sharedUsers_id", "AlbumEntity__AlbumEntity_sharedUsers"."id" AS "AlbumEntity__AlbumEntity_sharedUsers_id",
"AlbumEntity__AlbumEntity_sharedUsers"."name" AS "AlbumEntity__AlbumEntity_sharedUsers_name", "AlbumEntity__AlbumEntity_sharedUsers"."name" AS "AlbumEntity__AlbumEntity_sharedUsers_name",
"AlbumEntity__AlbumEntity_sharedUsers"."avatarColor" AS "AlbumEntity__AlbumEntity_sharedUsers_avatarColor", "AlbumEntity__AlbumEntity_sharedUsers"."avatarColor" AS "AlbumEntity__AlbumEntity_sharedUsers_avatarColor",
@ -462,6 +467,7 @@ SELECT
"AlbumEntity"."deletedAt" AS "AlbumEntity_deletedAt", "AlbumEntity"."deletedAt" AS "AlbumEntity_deletedAt",
"AlbumEntity"."albumThumbnailAssetId" AS "AlbumEntity_albumThumbnailAssetId", "AlbumEntity"."albumThumbnailAssetId" AS "AlbumEntity_albumThumbnailAssetId",
"AlbumEntity"."isActivityEnabled" AS "AlbumEntity_isActivityEnabled", "AlbumEntity"."isActivityEnabled" AS "AlbumEntity_isActivityEnabled",
"AlbumEntity"."order" AS "AlbumEntity_order",
"AlbumEntity__AlbumEntity_sharedUsers"."id" AS "AlbumEntity__AlbumEntity_sharedUsers_id", "AlbumEntity__AlbumEntity_sharedUsers"."id" AS "AlbumEntity__AlbumEntity_sharedUsers_id",
"AlbumEntity__AlbumEntity_sharedUsers"."name" AS "AlbumEntity__AlbumEntity_sharedUsers_name", "AlbumEntity__AlbumEntity_sharedUsers"."name" AS "AlbumEntity__AlbumEntity_sharedUsers_name",
"AlbumEntity__AlbumEntity_sharedUsers"."avatarColor" AS "AlbumEntity__AlbumEntity_sharedUsers_avatarColor", "AlbumEntity__AlbumEntity_sharedUsers"."avatarColor" AS "AlbumEntity__AlbumEntity_sharedUsers_avatarColor",
@ -553,6 +559,7 @@ SELECT
"AlbumEntity"."deletedAt" AS "AlbumEntity_deletedAt", "AlbumEntity"."deletedAt" AS "AlbumEntity_deletedAt",
"AlbumEntity"."albumThumbnailAssetId" AS "AlbumEntity_albumThumbnailAssetId", "AlbumEntity"."albumThumbnailAssetId" AS "AlbumEntity_albumThumbnailAssetId",
"AlbumEntity"."isActivityEnabled" AS "AlbumEntity_isActivityEnabled", "AlbumEntity"."isActivityEnabled" AS "AlbumEntity_isActivityEnabled",
"AlbumEntity"."order" AS "AlbumEntity_order",
"AlbumEntity__AlbumEntity_owner"."id" AS "AlbumEntity__AlbumEntity_owner_id", "AlbumEntity__AlbumEntity_owner"."id" AS "AlbumEntity__AlbumEntity_owner_id",
"AlbumEntity__AlbumEntity_owner"."name" AS "AlbumEntity__AlbumEntity_owner_name", "AlbumEntity__AlbumEntity_owner"."name" AS "AlbumEntity__AlbumEntity_owner_name",
"AlbumEntity__AlbumEntity_owner"."avatarColor" AS "AlbumEntity__AlbumEntity_owner_avatarColor", "AlbumEntity__AlbumEntity_owner"."avatarColor" AS "AlbumEntity__AlbumEntity_owner_avatarColor",

View File

@ -87,6 +87,7 @@ FROM
"SharedLinkEntity__SharedLinkEntity_album"."deletedAt" AS "SharedLinkEntity__SharedLinkEntity_album_deletedAt", "SharedLinkEntity__SharedLinkEntity_album"."deletedAt" AS "SharedLinkEntity__SharedLinkEntity_album_deletedAt",
"SharedLinkEntity__SharedLinkEntity_album"."albumThumbnailAssetId" AS "SharedLinkEntity__SharedLinkEntity_album_albumThumbnailAssetId", "SharedLinkEntity__SharedLinkEntity_album"."albumThumbnailAssetId" AS "SharedLinkEntity__SharedLinkEntity_album_albumThumbnailAssetId",
"SharedLinkEntity__SharedLinkEntity_album"."isActivityEnabled" AS "SharedLinkEntity__SharedLinkEntity_album_isActivityEnabled", "SharedLinkEntity__SharedLinkEntity_album"."isActivityEnabled" AS "SharedLinkEntity__SharedLinkEntity_album_isActivityEnabled",
"SharedLinkEntity__SharedLinkEntity_album"."order" AS "SharedLinkEntity__SharedLinkEntity_album_order",
"4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."id" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_id", "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."id" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_id",
"4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."deviceAssetId" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_deviceAssetId", "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."deviceAssetId" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_deviceAssetId",
"4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."ownerId" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_ownerId", "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."ownerId" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_ownerId",
@ -248,6 +249,7 @@ SELECT
"SharedLinkEntity__SharedLinkEntity_album"."deletedAt" AS "SharedLinkEntity__SharedLinkEntity_album_deletedAt", "SharedLinkEntity__SharedLinkEntity_album"."deletedAt" AS "SharedLinkEntity__SharedLinkEntity_album_deletedAt",
"SharedLinkEntity__SharedLinkEntity_album"."albumThumbnailAssetId" AS "SharedLinkEntity__SharedLinkEntity_album_albumThumbnailAssetId", "SharedLinkEntity__SharedLinkEntity_album"."albumThumbnailAssetId" AS "SharedLinkEntity__SharedLinkEntity_album_albumThumbnailAssetId",
"SharedLinkEntity__SharedLinkEntity_album"."isActivityEnabled" AS "SharedLinkEntity__SharedLinkEntity_album_isActivityEnabled", "SharedLinkEntity__SharedLinkEntity_album"."isActivityEnabled" AS "SharedLinkEntity__SharedLinkEntity_album_isActivityEnabled",
"SharedLinkEntity__SharedLinkEntity_album"."order" AS "SharedLinkEntity__SharedLinkEntity_album_order",
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."id" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_id", "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."id" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_id",
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."name" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_name", "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."name" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_name",
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."avatarColor" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_avatarColor", "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."avatarColor" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_avatarColor",

View File

@ -1,4 +1,4 @@
import { AlbumEntity } from '@app/infra/entities'; import { AlbumEntity, AssetOrder } from '@app/infra/entities';
import { assetStub } from './asset.stub'; import { assetStub } from './asset.stub';
import { authStub } from './auth.stub'; import { authStub } from './auth.stub';
import { userStub } from './user.stub'; import { userStub } from './user.stub';
@ -19,6 +19,7 @@ export const albumStub = {
sharedLinks: [], sharedLinks: [],
sharedUsers: [], sharedUsers: [],
isActivityEnabled: true, isActivityEnabled: true,
order: AssetOrder.DESC,
}), }),
sharedWithUser: Object.freeze<AlbumEntity>({ sharedWithUser: Object.freeze<AlbumEntity>({
id: 'album-2', id: 'album-2',
@ -35,6 +36,7 @@ export const albumStub = {
sharedLinks: [], sharedLinks: [],
sharedUsers: [userStub.user1], sharedUsers: [userStub.user1],
isActivityEnabled: true, isActivityEnabled: true,
order: AssetOrder.DESC,
}), }),
sharedWithMultiple: Object.freeze<AlbumEntity>({ sharedWithMultiple: Object.freeze<AlbumEntity>({
id: 'album-3', id: 'album-3',
@ -51,6 +53,7 @@ export const albumStub = {
sharedLinks: [], sharedLinks: [],
sharedUsers: [userStub.user1, userStub.user2], sharedUsers: [userStub.user1, userStub.user2],
isActivityEnabled: true, isActivityEnabled: true,
order: AssetOrder.DESC,
}), }),
sharedWithAdmin: Object.freeze<AlbumEntity>({ sharedWithAdmin: Object.freeze<AlbumEntity>({
id: 'album-3', id: 'album-3',
@ -67,6 +70,7 @@ export const albumStub = {
sharedLinks: [], sharedLinks: [],
sharedUsers: [userStub.admin], sharedUsers: [userStub.admin],
isActivityEnabled: true, isActivityEnabled: true,
order: AssetOrder.DESC,
}), }),
oneAsset: Object.freeze<AlbumEntity>({ oneAsset: Object.freeze<AlbumEntity>({
id: 'album-4', id: 'album-4',
@ -83,6 +87,7 @@ export const albumStub = {
sharedLinks: [], sharedLinks: [],
sharedUsers: [], sharedUsers: [],
isActivityEnabled: true, isActivityEnabled: true,
order: AssetOrder.DESC,
}), }),
twoAssets: Object.freeze<AlbumEntity>({ twoAssets: Object.freeze<AlbumEntity>({
id: 'album-4a', id: 'album-4a',
@ -99,6 +104,7 @@ export const albumStub = {
sharedLinks: [], sharedLinks: [],
sharedUsers: [], sharedUsers: [],
isActivityEnabled: true, isActivityEnabled: true,
order: AssetOrder.DESC,
}), }),
emptyWithInvalidThumbnail: Object.freeze<AlbumEntity>({ emptyWithInvalidThumbnail: Object.freeze<AlbumEntity>({
id: 'album-5', id: 'album-5',
@ -115,6 +121,7 @@ export const albumStub = {
sharedLinks: [], sharedLinks: [],
sharedUsers: [], sharedUsers: [],
isActivityEnabled: true, isActivityEnabled: true,
order: AssetOrder.DESC,
}), }),
emptyWithValidThumbnail: Object.freeze<AlbumEntity>({ emptyWithValidThumbnail: Object.freeze<AlbumEntity>({
id: 'album-5', id: 'album-5',
@ -131,6 +138,7 @@ export const albumStub = {
sharedLinks: [], sharedLinks: [],
sharedUsers: [], sharedUsers: [],
isActivityEnabled: true, isActivityEnabled: true,
order: AssetOrder.DESC,
}), }),
oneAssetInvalidThumbnail: Object.freeze<AlbumEntity>({ oneAssetInvalidThumbnail: Object.freeze<AlbumEntity>({
id: 'album-6', id: 'album-6',
@ -147,6 +155,7 @@ export const albumStub = {
sharedLinks: [], sharedLinks: [],
sharedUsers: [], sharedUsers: [],
isActivityEnabled: true, isActivityEnabled: true,
order: AssetOrder.DESC,
}), }),
oneAssetValidThumbnail: Object.freeze<AlbumEntity>({ oneAssetValidThumbnail: Object.freeze<AlbumEntity>({
id: 'album-6', id: 'album-6',
@ -163,5 +172,6 @@ export const albumStub = {
sharedLinks: [], sharedLinks: [],
sharedUsers: [], sharedUsers: [],
isActivityEnabled: true, isActivityEnabled: true,
order: AssetOrder.DESC,
}), }),
}; };

View File

@ -1,5 +1,5 @@
import { AlbumResponseDto, AssetResponseDto, ExifResponseDto, mapUser, SharedLinkResponseDto } from '@app/domain'; 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 { assetStub } from './asset.stub';
import { authStub } from './auth.stub'; import { authStub } from './auth.stub';
import { libraryStub } from './library.stub'; import { libraryStub } from './library.stub';
@ -101,6 +101,7 @@ const albumResponse: AlbumResponseDto = {
assets: [], assets: [],
assetCount: 1, assetCount: 1,
isActivityEnabled: true, isActivityEnabled: true,
order: AssetOrder.DESC,
}; };
export const sharedLinkStub = { export const sharedLinkStub = {
@ -181,6 +182,7 @@ export const sharedLinkStub = {
sharedUsers: [], sharedUsers: [],
sharedLinks: [], sharedLinks: [],
isActivityEnabled: true, isActivityEnabled: true,
order: AssetOrder.DESC,
assets: [ assets: [
{ {
id: 'id_1', id: 'id_1',

View File

@ -1,22 +1,55 @@
<script lang="ts"> <script lang="ts">
import Icon from '$lib/components/elements/icon.svelte'; import Icon from '$lib/components/elements/icon.svelte';
import type { AlbumResponseDto, UserResponseDto } from '@immich/sdk'; import { updateAlbumInfo, type AlbumResponseDto, type UserResponseDto, AssetOrder } from '@immich/sdk';
import { mdiClose, mdiPlus } from '@mdi/js'; import { mdiArrowDownThin, mdiArrowUpThin, mdiClose, mdiPlus } from '@mdi/js';
import { createEventDispatcher } from 'svelte'; import { createEventDispatcher } from 'svelte';
import CircleIconButton from '../elements/buttons/circle-icon-button.svelte'; import CircleIconButton from '../elements/buttons/circle-icon-button.svelte';
import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte'; import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
import UserAvatar from '$lib/components/shared-components/user-avatar.svelte'; import UserAvatar from '$lib/components/shared-components/user-avatar.svelte';
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.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 album: AlbumResponseDto;
export let order: AssetOrder | undefined;
export let user: UserResponseDto; 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<{ const dispatch = createEventDispatcher<{
close: void; close: void;
toggleEnableActivity: void; toggleEnableActivity: void;
showSelectSharedUser: 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> </script>
<FullScreenModal onClose={() => dispatch('close')}> <FullScreenModal onClose={() => dispatch('close')}>
@ -34,8 +67,16 @@
<div class=" items-center justify-center p-4"> <div class=" items-center justify-center p-4">
<div class="py-2"> <div class="py-2">
<h2 class="text-gray text-sm mb-3">SHARING</h2> <h2 class="text-gray text-sm mb-2">SETTINGS</h2>
<div class="p-2"> <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 <SettingSwitch
title="Comments & likes" title="Comments & likes"
subtitle="Let others respond" subtitle="Let others respond"

View File

@ -20,7 +20,7 @@
[SlideshowNavigation.DescendingOrder]: { icon: mdiArrowDownThin, title: 'Forward' }, [SlideshowNavigation.DescendingOrder]: { icon: mdiArrowDownThin, title: 'Forward' },
}; };
export const handleToggle = (selectedOption: RenderedOption) => { const handleToggle = (selectedOption: RenderedOption) => {
for (const [key, option] of Object.entries(options)) { for (const [key, option] of Object.entries(options)) {
if (option === selectedOption) { if (option === selectedOption) {
$slideshowNavigation = key as SlideshowNavigation; $slideshowNavigation = key as SlideshowNavigation;

View File

@ -161,7 +161,10 @@ export class AssetStore {
this.assetToBucket = {}; this.assetToBucket = {};
this.albumAssets = new Set(); this.albumAssets = new Set();
const buckets = await getTimeBuckets({ ...this.options, key: getKey() }); const buckets = await getTimeBuckets({
...this.options,
key: getKey(),
});
this.initialized = true; this.initialized = true;

View File

@ -58,6 +58,7 @@
updateAlbumInfo, updateAlbumInfo,
type ActivityResponseDto, type ActivityResponseDto,
type UserResponseDto, type UserResponseDto,
AssetOrder,
} from '@immich/sdk'; } from '@immich/sdk';
import { import {
mdiArrowLeft, mdiArrowLeft,
@ -83,6 +84,7 @@
$: album = data.album; $: album = data.album;
$: albumId = album.id; $: albumId = album.id;
$: albumKey = `${albumId}_${albumOrder}`;
$: { $: {
if (!album.isActivityEnabled && $numberOfComments === 0) { if (!album.isActivityEnabled && $numberOfComments === 0) {
@ -112,8 +114,9 @@
let globalWidth: number; let globalWidth: number;
let assetGridWidth: number; let assetGridWidth: number;
let textArea: HTMLTextAreaElement; let textArea: HTMLTextAreaElement;
let albumOrder: AssetOrder | undefined = data.album.order;
$: assetStore = new AssetStore({ albumId }); $: assetStore = new AssetStore({ albumId, order: albumOrder });
const assetInteractionStore = createAssetInteractionStore(); const assetInteractionStore = createAssetInteractionStore();
const { isMultiSelectState, selectedAssets } = assetInteractionStore; const { isMultiSelectState, selectedAssets } = assetInteractionStore;
@ -512,7 +515,7 @@
style={`width:${assetGridWidth}px`} style={`width:${assetGridWidth}px`}
> >
<!-- Use key because AssetGrid can't deal with changing stores --> <!-- Use key because AssetGrid can't deal with changing stores -->
{#key albumId} {#key albumKey}
{#if viewMode === ViewMode.SELECT_ASSETS} {#if viewMode === ViewMode.SELECT_ASSETS}
<AssetGrid <AssetGrid
assetStore={timelineStore} assetStore={timelineStore}
@ -679,7 +682,9 @@
{#if viewMode === ViewMode.OPTIONS && $user} {#if viewMode === ViewMode.OPTIONS && $user}
<AlbumOptions <AlbumOptions
{album} {album}
order={albumOrder}
user={$user} user={$user}
onChangeOrder={(order) => (albumOrder = order)}
on:close={() => (viewMode = ViewMode.VIEW)} on:close={() => (viewMode = ViewMode.VIEW)}
on:toggleEnableActivity={handleToggleEnableActivity} on:toggleEnableActivity={handleToggleEnableActivity}
on:showSelectSharedUser={() => (viewMode = ViewMode.SELECT_USERS)} on:showSelectSharedUser={() => (viewMode = ViewMode.SELECT_USERS)}

View File

@ -1,5 +1,5 @@
import { faker } from '@faker-js/faker'; 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 { Sync } from 'factory.ts';
import { userFactory } from './user-factory'; import { userFactory } from './user-factory';
@ -18,4 +18,5 @@ export const albumFactory = Sync.makeFactory<AlbumResponseDto>({
sharedUsers: [], sharedUsers: [],
hasSharedLink: false, hasSharedLink: false,
isActivityEnabled: true, isActivityEnabled: true,
order: AssetOrder.Desc,
}); });