1
0
Fork 0
mirror of https://github.com/immich-app/immich.git synced 2025-04-10 10:06:25 +02:00

feat(server, web): album orders ()

* 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 {
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,
});
});
});

View file

@ -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** | |

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)
# **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]

View file

@ -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)

View file

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

View file

@ -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')!,

View file

@ -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;

View file

@ -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

View file

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

View file

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

View file

@ -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"

View file

@ -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"

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 { 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,
};
};

View file

@ -148,6 +148,7 @@ export class AlbumService {
description: dto.description,
albumThumbnailAssetId: dto.albumThumbnailAssetId,
isActivityEnabled: dto.isActivityEnabled,
order: dto.order,
});
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';
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;
}

View file

@ -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);

View file

@ -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 {

View file

@ -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 {

View file

@ -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';

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 { AssetOrder, AssetResponseDto, mapAsset } from '../asset';
import { AssetResponseDto, mapAsset } from '../asset';
import { AuthDto } from '../auth';
import { PersonResponseDto } from '../person';
import {

View file

@ -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;
}

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,
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()
);
}

View file

@ -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",

View file

@ -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",

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 { 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,
}),
};

View file

@ -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',

View file

@ -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"

View file

@ -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;

View file

@ -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;

View file

@ -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)}

View file

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