diff --git a/e2e/src/api/specs/album.e2e-spec.ts b/e2e/src/api/specs/album.e2e-spec.ts index 2310b4718c..de320ee95f 100644 --- a/e2e/src/api/specs/album.e2e-spec.ts +++ b/e2e/src/api/specs/album.e2e-spec.ts @@ -1,6 +1,7 @@ import { AlbumResponseDto, AssetFileUploadResponseDto, + AssetOrder, LoginResponseDto, SharedLinkType, deleteUser, @@ -353,6 +354,7 @@ describe('/album', () => { assetCount: 0, owner: expect.objectContaining({ email: user1.userEmail }), isActivityEnabled: true, + order: AssetOrder.Desc, }); }); }); diff --git a/mobile/openapi/doc/AlbumResponseDto.md b/mobile/openapi/doc/AlbumResponseDto.md index bc00d30af1..dd4a94e883 100644 Binary files a/mobile/openapi/doc/AlbumResponseDto.md and b/mobile/openapi/doc/AlbumResponseDto.md differ diff --git a/mobile/openapi/doc/AssetApi.md b/mobile/openapi/doc/AssetApi.md index c65e6a605f..1aaf195f3a 100644 Binary files a/mobile/openapi/doc/AssetApi.md and b/mobile/openapi/doc/AssetApi.md differ diff --git a/mobile/openapi/doc/UpdateAlbumDto.md b/mobile/openapi/doc/UpdateAlbumDto.md index 4ded87d1bf..89edf1c6ef 100644 Binary files a/mobile/openapi/doc/UpdateAlbumDto.md and b/mobile/openapi/doc/UpdateAlbumDto.md differ diff --git a/mobile/openapi/lib/api/asset_api.dart b/mobile/openapi/lib/api/asset_api.dart index 786129b457..b0395bfcbe 100644 Binary files a/mobile/openapi/lib/api/asset_api.dart and b/mobile/openapi/lib/api/asset_api.dart differ diff --git a/mobile/openapi/lib/model/album_response_dto.dart b/mobile/openapi/lib/model/album_response_dto.dart index 43e24f87be..d764028558 100644 Binary files a/mobile/openapi/lib/model/album_response_dto.dart and b/mobile/openapi/lib/model/album_response_dto.dart differ diff --git a/mobile/openapi/lib/model/update_album_dto.dart b/mobile/openapi/lib/model/update_album_dto.dart index dfe245aaf8..d9408cedfb 100644 Binary files a/mobile/openapi/lib/model/update_album_dto.dart and b/mobile/openapi/lib/model/update_album_dto.dart differ diff --git a/mobile/openapi/test/album_response_dto_test.dart b/mobile/openapi/test/album_response_dto_test.dart index 933f77c196..5c79e5d2fc 100644 Binary files a/mobile/openapi/test/album_response_dto_test.dart and b/mobile/openapi/test/album_response_dto_test.dart differ diff --git a/mobile/openapi/test/asset_api_test.dart b/mobile/openapi/test/asset_api_test.dart index 846a5998cc..d210d0e4d9 100644 Binary files a/mobile/openapi/test/asset_api_test.dart and b/mobile/openapi/test/asset_api_test.dart differ diff --git a/mobile/openapi/test/update_album_dto_test.dart b/mobile/openapi/test/update_album_dto_test.dart index 67ec80010d..7f1591a52c 100644 Binary files a/mobile/openapi/test/update_album_dto_test.dart and b/mobile/openapi/test/update_album_dto_test.dart differ diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 2540baf775..15ada078cb 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -1765,6 +1765,14 @@ "type": "string" } }, + { + "name": "order", + "required": false, + "in": "query", + "schema": { + "$ref": "#/components/schemas/AssetOrder" + } + }, { "name": "personId", "required": false, @@ -1901,6 +1909,14 @@ "type": "string" } }, + { + "name": "order", + "required": false, + "in": "query", + "schema": { + "$ref": "#/components/schemas/AssetOrder" + } + }, { "name": "personId", "required": false, @@ -6722,6 +6738,9 @@ "format": "date-time", "type": "string" }, + "order": { + "$ref": "#/components/schemas/AssetOrder" + }, "owner": { "$ref": "#/components/schemas/UserResponseDto" }, @@ -10335,6 +10354,9 @@ }, "isActivityEnabled": { "type": "boolean" + }, + "order": { + "$ref": "#/components/schemas/AssetOrder" } }, "type": "object" diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index acf540aff1..6a660f4e1b 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -153,6 +153,7 @@ export type AlbumResponseDto = { id: string; isActivityEnabled: boolean; lastModifiedAssetTimestamp?: string; + order?: AssetOrder; owner: UserResponseDto; ownerId: string; shared: boolean; @@ -176,6 +177,7 @@ export type UpdateAlbumDto = { albumThumbnailAssetId?: string; description?: string; isActivityEnabled?: boolean; + order?: AssetOrder; }; export type BulkIdsDto = { ids: string[]; @@ -1453,12 +1455,13 @@ export function getAssetThumbnail({ format, id, key }: { ...opts })); } -export function getTimeBucket({ albumId, isArchived, isFavorite, isTrashed, key, personId, size, timeBucket, userId, withPartners, withStacked }: { +export function getTimeBucket({ albumId, isArchived, isFavorite, isTrashed, key, order, personId, size, timeBucket, userId, withPartners, withStacked }: { albumId?: string; isArchived?: boolean; isFavorite?: boolean; isTrashed?: boolean; key?: string; + order?: AssetOrder; personId?: string; size: TimeBucketSize; timeBucket: string; @@ -1475,6 +1478,7 @@ export function getTimeBucket({ albumId, isArchived, isFavorite, isTrashed, key, isFavorite, isTrashed, key, + order, personId, size, timeBucket, @@ -1485,12 +1489,13 @@ export function getTimeBucket({ albumId, isArchived, isFavorite, isTrashed, key, ...opts })); } -export function getTimeBuckets({ albumId, isArchived, isFavorite, isTrashed, key, personId, size, userId, withPartners, withStacked }: { +export function getTimeBuckets({ albumId, isArchived, isFavorite, isTrashed, key, order, personId, size, userId, withPartners, withStacked }: { albumId?: string; isArchived?: boolean; isFavorite?: boolean; isTrashed?: boolean; key?: string; + order?: AssetOrder; personId?: string; size: TimeBucketSize; userId?: string; @@ -1506,6 +1511,7 @@ export function getTimeBuckets({ albumId, isArchived, isFavorite, isTrashed, key isFavorite, isTrashed, key, + order, personId, size, userId, @@ -2747,6 +2753,10 @@ export enum AssetTypeEnum { Audio = "AUDIO", Other = "OTHER" } +export enum AssetOrder { + Asc = "asc", + Desc = "desc" +} export enum Error { Duplicate = "duplicate", NoPermission = "no_permission", @@ -2774,10 +2784,6 @@ export enum TimeBucketSize { Day = "DAY", Month = "MONTH" } -export enum AssetOrder { - Asc = "asc", - Desc = "desc" -} export enum EntityType { Asset = "ASSET", Album = "ALBUM" diff --git a/server/src/domain/album/album-response.dto.ts b/server/src/domain/album/album-response.dto.ts index 168b385928..bcca1cd315 100644 --- a/server/src/domain/album/album-response.dto.ts +++ b/server/src/domain/album/album-response.dto.ts @@ -1,4 +1,5 @@ -import { AlbumEntity } from '@app/infra/entities'; +import { AlbumEntity, AssetOrder } from '@app/infra/entities'; +import { Optional } from '@nestjs/common'; import { ApiProperty } from '@nestjs/swagger'; import { AssetResponseDto, mapAsset } from '../asset'; import { AuthDto } from '../auth/auth.dto'; @@ -23,6 +24,9 @@ export class AlbumResponseDto { startDate?: Date; endDate?: Date; isActivityEnabled!: boolean; + @Optional() + @ApiProperty({ enumName: 'AssetOrder', enum: AssetOrder }) + order?: AssetOrder; } export const mapAlbum = (entity: AlbumEntity, withAssets: boolean, auth?: AuthDto): AlbumResponseDto => { @@ -63,6 +67,7 @@ export const mapAlbum = (entity: AlbumEntity, withAssets: boolean, auth?: AuthDt assets: (withAssets ? assets : []).map((asset) => mapAsset(asset, { auth })), assetCount: entity.assets?.length || 0, isActivityEnabled: entity.isActivityEnabled, + order: entity.order, }; }; diff --git a/server/src/domain/album/album.service.ts b/server/src/domain/album/album.service.ts index 9a7b940f77..dc3d510d4b 100644 --- a/server/src/domain/album/album.service.ts +++ b/server/src/domain/album/album.service.ts @@ -148,6 +148,7 @@ export class AlbumService { description: dto.description, albumThumbnailAssetId: dto.albumThumbnailAssetId, isActivityEnabled: dto.isActivityEnabled, + order: dto.order, }); return mapAlbumWithoutAssets(updatedAlbum); diff --git a/server/src/domain/album/dto/album-update.dto.ts b/server/src/domain/album/dto/album-update.dto.ts index 1b6c754f02..4f88cefbbd 100644 --- a/server/src/domain/album/dto/album-update.dto.ts +++ b/server/src/domain/album/dto/album-update.dto.ts @@ -1,4 +1,6 @@ -import { IsString } from 'class-validator'; +import { AssetOrder } from '@app/infra/entities'; +import { ApiProperty } from '@nestjs/swagger'; +import { IsEnum, IsString } from 'class-validator'; import { Optional, ValidateBoolean, ValidateUUID } from '../../domain.util'; export class UpdateAlbumDto { @@ -15,4 +17,9 @@ export class UpdateAlbumDto { @ValidateBoolean({ optional: true }) isActivityEnabled?: boolean; + + @IsEnum(AssetOrder) + @Optional() + @ApiProperty({ enum: AssetOrder, enumName: 'AssetOrder' }) + order?: AssetOrder; } diff --git a/server/src/domain/asset/dto/asset.dto.ts b/server/src/domain/asset/dto/asset.dto.ts index 8b5c675d89..2abe31d0ad 100644 --- a/server/src/domain/asset/dto/asset.dto.ts +++ b/server/src/domain/asset/dto/asset.dto.ts @@ -18,11 +18,6 @@ export class DeviceIdDto { deviceId!: string; } -export enum AssetOrder { - ASC = 'asc', - DESC = 'desc', -} - const hasGPS = (o: { latitude: undefined; longitude: undefined }) => o.latitude !== undefined || o.longitude !== undefined; const ValidateGPS = () => ValidateIf(hasGPS); diff --git a/server/src/domain/asset/dto/time-bucket.dto.ts b/server/src/domain/asset/dto/time-bucket.dto.ts index 597a5de356..7c5b9c212b 100644 --- a/server/src/domain/asset/dto/time-bucket.dto.ts +++ b/server/src/domain/asset/dto/time-bucket.dto.ts @@ -1,6 +1,7 @@ +import { AssetOrder } from '@app/infra/entities'; import { ApiProperty } from '@nestjs/swagger'; import { IsEnum, IsNotEmpty, IsString } from 'class-validator'; -import { ValidateBoolean, ValidateUUID } from '../../domain.util'; +import { Optional, ValidateBoolean, ValidateUUID } from '../../domain.util'; import { TimeBucketSize } from '../../repositories'; export class TimeBucketDto { @@ -32,6 +33,11 @@ export class TimeBucketDto { @ValidateBoolean({ optional: true }) withPartners?: boolean; + + @IsEnum(AssetOrder) + @Optional() + @ApiProperty({ enum: AssetOrder, enumName: 'AssetOrder' }) + order?: AssetOrder; } export class TimeBucketAssetDto extends TimeBucketDto { diff --git a/server/src/domain/repositories/asset.repository.ts b/server/src/domain/repositories/asset.repository.ts index 3627004421..8b14ce597e 100644 --- a/server/src/domain/repositories/asset.repository.ts +++ b/server/src/domain/repositories/asset.repository.ts @@ -1,5 +1,5 @@ import { AssetSearchOptions, ReverseGeocodeResult, SearchExploreItem } from '@app/domain'; -import { AssetEntity, AssetJobStatusEntity, AssetType, ExifEntity } from '@app/infra/entities'; +import { AssetEntity, AssetJobStatusEntity, AssetOrder, AssetType, ExifEntity } from '@app/infra/entities'; import { FindOptionsRelations, FindOptionsSelect } from 'typeorm'; import { Paginated, PaginationOptions } from '../domain.util'; @@ -66,6 +66,7 @@ export interface AssetBuilderOptions { export interface TimeBucketOptions extends AssetBuilderOptions { size: TimeBucketSize; + order?: AssetOrder; } export interface TimeBucketItem { diff --git a/server/src/domain/search/dto/search.dto.ts b/server/src/domain/search/dto/search.dto.ts index 9fa7d8e8ba..1bc67266a3 100644 --- a/server/src/domain/search/dto/search.dto.ts +++ b/server/src/domain/search/dto/search.dto.ts @@ -1,5 +1,4 @@ -import { AssetOrder } from '@app/domain/asset/dto/asset.dto'; -import { AssetType, GeodataPlacesEntity } from '@app/infra/entities'; +import { AssetOrder, AssetType, GeodataPlacesEntity } from '@app/infra/entities'; import { ApiProperty } from '@nestjs/swagger'; import { Type } from 'class-transformer'; import { IsEnum, IsInt, IsNotEmpty, IsString, Max, Min } from 'class-validator'; diff --git a/server/src/domain/search/search.service.ts b/server/src/domain/search/search.service.ts index 4cb0665e08..56c4498bce 100644 --- a/server/src/domain/search/search.service.ts +++ b/server/src/domain/search/search.service.ts @@ -1,6 +1,6 @@ -import { AssetEntity } from '@app/infra/entities'; +import { AssetEntity, AssetOrder } from '@app/infra/entities'; import { Inject, Injectable } from '@nestjs/common'; -import { AssetOrder, AssetResponseDto, mapAsset } from '../asset'; +import { AssetResponseDto, mapAsset } from '../asset'; import { AuthDto } from '../auth'; import { PersonResponseDto } from '../person'; import { diff --git a/server/src/infra/entities/album.entity.ts b/server/src/infra/entities/album.entity.ts index fbc125351a..daa8fcbc36 100644 --- a/server/src/infra/entities/album.entity.ts +++ b/server/src/infra/entities/album.entity.ts @@ -14,6 +14,12 @@ import { AssetEntity } from './asset.entity'; import { SharedLinkEntity } from './shared-link.entity'; import { UserEntity } from './user.entity'; +// ran into issues when importing the enum from `asset.dto.ts` +export enum AssetOrder { + ASC = 'asc', + DESC = 'desc', +} + @Entity('albums') export class AlbumEntity { @PrimaryGeneratedColumn('uuid') @@ -59,4 +65,7 @@ export class AlbumEntity { @Column({ default: true }) isActivityEnabled!: boolean; + + @Column({ type: 'varchar', default: AssetOrder.DESC }) + order!: AssetOrder; } diff --git a/server/src/infra/migrations/1710182081326-AscendingOrderAlbum.ts b/server/src/infra/migrations/1710182081326-AscendingOrderAlbum.ts new file mode 100644 index 0000000000..b672ff2b20 --- /dev/null +++ b/server/src/infra/migrations/1710182081326-AscendingOrderAlbum.ts @@ -0,0 +1,14 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class AscendingOrderAlbum1710182081326 implements MigrationInterface { + name = 'AscendingOrderAlbum1710182081326' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "albums" ADD "order" character varying NOT NULL DEFAULT 'desc'`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "albums" DROP COLUMN "order"`); + } + +} diff --git a/server/src/infra/repositories/asset.repository.ts b/server/src/infra/repositories/asset.repository.ts index 5d571d11eb..871a44460b 100644 --- a/server/src/infra/repositories/asset.repository.ts +++ b/server/src/infra/repositories/asset.repository.ts @@ -36,7 +36,7 @@ import { Not, Repository, } from 'typeorm'; -import { AssetEntity, AssetJobStatusEntity, AssetType, ExifEntity, SmartInfoEntity } from '../entities'; +import { AssetEntity, AssetJobStatusEntity, AssetOrder, AssetType, ExifEntity, SmartInfoEntity } from '../entities'; import { DummyValue, GenerateSql } from '../infra.util'; import { Chunked, ChunkedArray, OptionalBetween, paginate, paginatedBuilder, searchAssetBuilder } from '../infra.utils'; import { Instrumentation } from '../instrumentation'; @@ -607,7 +607,7 @@ export class AssetRepository implements IAssetRepository { .select(`COUNT(asset.id)::int`, 'count') .addSelect(truncated, 'timeBucket') .groupBy(truncated) - .orderBy(truncated, 'DESC') + .orderBy(truncated, options.order === AssetOrder.ASC ? 'ASC' : 'DESC') .getRawMany(); } @@ -620,7 +620,7 @@ export class AssetRepository implements IAssetRepository { // First sort by the day in localtime (put it in the right bucket) .orderBy(truncated, 'DESC') // and then sort by the actual time - .addOrderBy('asset.fileCreatedAt', 'DESC') + .addOrderBy('asset.fileCreatedAt', options.order === AssetOrder.ASC ? 'ASC' : 'DESC') .getMany() ); } diff --git a/server/src/infra/sql/album.repository.sql b/server/src/infra/sql/album.repository.sql index d9b2e896e9..ddedc00959 100644 --- a/server/src/infra/sql/album.repository.sql +++ b/server/src/infra/sql/album.repository.sql @@ -15,6 +15,7 @@ FROM "AlbumEntity"."deletedAt" AS "AlbumEntity_deletedAt", "AlbumEntity"."albumThumbnailAssetId" AS "AlbumEntity_albumThumbnailAssetId", "AlbumEntity"."isActivityEnabled" AS "AlbumEntity_isActivityEnabled", + "AlbumEntity"."order" AS "AlbumEntity_order", "AlbumEntity__AlbumEntity_owner"."id" AS "AlbumEntity__AlbumEntity_owner_id", "AlbumEntity__AlbumEntity_owner"."name" AS "AlbumEntity__AlbumEntity_owner_name", "AlbumEntity__AlbumEntity_owner"."avatarColor" AS "AlbumEntity__AlbumEntity_owner_avatarColor", @@ -91,6 +92,7 @@ SELECT "AlbumEntity"."deletedAt" AS "AlbumEntity_deletedAt", "AlbumEntity"."albumThumbnailAssetId" AS "AlbumEntity_albumThumbnailAssetId", "AlbumEntity"."isActivityEnabled" AS "AlbumEntity_isActivityEnabled", + "AlbumEntity"."order" AS "AlbumEntity_order", "AlbumEntity__AlbumEntity_owner"."id" AS "AlbumEntity__AlbumEntity_owner_id", "AlbumEntity__AlbumEntity_owner"."name" AS "AlbumEntity__AlbumEntity_owner_name", "AlbumEntity__AlbumEntity_owner"."avatarColor" AS "AlbumEntity__AlbumEntity_owner_avatarColor", @@ -149,6 +151,7 @@ SELECT "AlbumEntity"."deletedAt" AS "AlbumEntity_deletedAt", "AlbumEntity"."albumThumbnailAssetId" AS "AlbumEntity_albumThumbnailAssetId", "AlbumEntity"."isActivityEnabled" AS "AlbumEntity_isActivityEnabled", + "AlbumEntity"."order" AS "AlbumEntity_order", "AlbumEntity__AlbumEntity_owner"."id" AS "AlbumEntity__AlbumEntity_owner_id", "AlbumEntity__AlbumEntity_owner"."name" AS "AlbumEntity__AlbumEntity_owner_name", "AlbumEntity__AlbumEntity_owner"."avatarColor" AS "AlbumEntity__AlbumEntity_owner_avatarColor", @@ -279,6 +282,7 @@ SELECT "AlbumEntity"."deletedAt" AS "AlbumEntity_deletedAt", "AlbumEntity"."albumThumbnailAssetId" AS "AlbumEntity_albumThumbnailAssetId", "AlbumEntity"."isActivityEnabled" AS "AlbumEntity_isActivityEnabled", + "AlbumEntity"."order" AS "AlbumEntity_order", "AlbumEntity__AlbumEntity_sharedUsers"."id" AS "AlbumEntity__AlbumEntity_sharedUsers_id", "AlbumEntity__AlbumEntity_sharedUsers"."name" AS "AlbumEntity__AlbumEntity_sharedUsers_name", "AlbumEntity__AlbumEntity_sharedUsers"."avatarColor" AS "AlbumEntity__AlbumEntity_sharedUsers_avatarColor", @@ -352,6 +356,7 @@ SELECT "AlbumEntity"."deletedAt" AS "AlbumEntity_deletedAt", "AlbumEntity"."albumThumbnailAssetId" AS "AlbumEntity_albumThumbnailAssetId", "AlbumEntity"."isActivityEnabled" AS "AlbumEntity_isActivityEnabled", + "AlbumEntity"."order" AS "AlbumEntity_order", "AlbumEntity__AlbumEntity_sharedUsers"."id" AS "AlbumEntity__AlbumEntity_sharedUsers_id", "AlbumEntity__AlbumEntity_sharedUsers"."name" AS "AlbumEntity__AlbumEntity_sharedUsers_name", "AlbumEntity__AlbumEntity_sharedUsers"."avatarColor" AS "AlbumEntity__AlbumEntity_sharedUsers_avatarColor", @@ -462,6 +467,7 @@ SELECT "AlbumEntity"."deletedAt" AS "AlbumEntity_deletedAt", "AlbumEntity"."albumThumbnailAssetId" AS "AlbumEntity_albumThumbnailAssetId", "AlbumEntity"."isActivityEnabled" AS "AlbumEntity_isActivityEnabled", + "AlbumEntity"."order" AS "AlbumEntity_order", "AlbumEntity__AlbumEntity_sharedUsers"."id" AS "AlbumEntity__AlbumEntity_sharedUsers_id", "AlbumEntity__AlbumEntity_sharedUsers"."name" AS "AlbumEntity__AlbumEntity_sharedUsers_name", "AlbumEntity__AlbumEntity_sharedUsers"."avatarColor" AS "AlbumEntity__AlbumEntity_sharedUsers_avatarColor", @@ -553,6 +559,7 @@ SELECT "AlbumEntity"."deletedAt" AS "AlbumEntity_deletedAt", "AlbumEntity"."albumThumbnailAssetId" AS "AlbumEntity_albumThumbnailAssetId", "AlbumEntity"."isActivityEnabled" AS "AlbumEntity_isActivityEnabled", + "AlbumEntity"."order" AS "AlbumEntity_order", "AlbumEntity__AlbumEntity_owner"."id" AS "AlbumEntity__AlbumEntity_owner_id", "AlbumEntity__AlbumEntity_owner"."name" AS "AlbumEntity__AlbumEntity_owner_name", "AlbumEntity__AlbumEntity_owner"."avatarColor" AS "AlbumEntity__AlbumEntity_owner_avatarColor", diff --git a/server/src/infra/sql/shared.link.repository.sql b/server/src/infra/sql/shared.link.repository.sql index b5e6894130..27531cfc9e 100644 --- a/server/src/infra/sql/shared.link.repository.sql +++ b/server/src/infra/sql/shared.link.repository.sql @@ -87,6 +87,7 @@ FROM "SharedLinkEntity__SharedLinkEntity_album"."deletedAt" AS "SharedLinkEntity__SharedLinkEntity_album_deletedAt", "SharedLinkEntity__SharedLinkEntity_album"."albumThumbnailAssetId" AS "SharedLinkEntity__SharedLinkEntity_album_albumThumbnailAssetId", "SharedLinkEntity__SharedLinkEntity_album"."isActivityEnabled" AS "SharedLinkEntity__SharedLinkEntity_album_isActivityEnabled", + "SharedLinkEntity__SharedLinkEntity_album"."order" AS "SharedLinkEntity__SharedLinkEntity_album_order", "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."id" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_id", "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."deviceAssetId" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_deviceAssetId", "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."ownerId" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_ownerId", @@ -248,6 +249,7 @@ SELECT "SharedLinkEntity__SharedLinkEntity_album"."deletedAt" AS "SharedLinkEntity__SharedLinkEntity_album_deletedAt", "SharedLinkEntity__SharedLinkEntity_album"."albumThumbnailAssetId" AS "SharedLinkEntity__SharedLinkEntity_album_albumThumbnailAssetId", "SharedLinkEntity__SharedLinkEntity_album"."isActivityEnabled" AS "SharedLinkEntity__SharedLinkEntity_album_isActivityEnabled", + "SharedLinkEntity__SharedLinkEntity_album"."order" AS "SharedLinkEntity__SharedLinkEntity_album_order", "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."id" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_id", "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."name" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_name", "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."avatarColor" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_avatarColor", diff --git a/server/test/fixtures/album.stub.ts b/server/test/fixtures/album.stub.ts index 2fdc5b5dd4..bfb6acb6d1 100644 --- a/server/test/fixtures/album.stub.ts +++ b/server/test/fixtures/album.stub.ts @@ -1,4 +1,4 @@ -import { AlbumEntity } from '@app/infra/entities'; +import { AlbumEntity, AssetOrder } from '@app/infra/entities'; import { assetStub } from './asset.stub'; import { authStub } from './auth.stub'; import { userStub } from './user.stub'; @@ -19,6 +19,7 @@ export const albumStub = { sharedLinks: [], sharedUsers: [], isActivityEnabled: true, + order: AssetOrder.DESC, }), sharedWithUser: Object.freeze({ id: 'album-2', @@ -35,6 +36,7 @@ export const albumStub = { sharedLinks: [], sharedUsers: [userStub.user1], isActivityEnabled: true, + order: AssetOrder.DESC, }), sharedWithMultiple: Object.freeze({ id: 'album-3', @@ -51,6 +53,7 @@ export const albumStub = { sharedLinks: [], sharedUsers: [userStub.user1, userStub.user2], isActivityEnabled: true, + order: AssetOrder.DESC, }), sharedWithAdmin: Object.freeze({ id: 'album-3', @@ -67,6 +70,7 @@ export const albumStub = { sharedLinks: [], sharedUsers: [userStub.admin], isActivityEnabled: true, + order: AssetOrder.DESC, }), oneAsset: Object.freeze({ id: 'album-4', @@ -83,6 +87,7 @@ export const albumStub = { sharedLinks: [], sharedUsers: [], isActivityEnabled: true, + order: AssetOrder.DESC, }), twoAssets: Object.freeze({ id: 'album-4a', @@ -99,6 +104,7 @@ export const albumStub = { sharedLinks: [], sharedUsers: [], isActivityEnabled: true, + order: AssetOrder.DESC, }), emptyWithInvalidThumbnail: Object.freeze({ id: 'album-5', @@ -115,6 +121,7 @@ export const albumStub = { sharedLinks: [], sharedUsers: [], isActivityEnabled: true, + order: AssetOrder.DESC, }), emptyWithValidThumbnail: Object.freeze({ id: 'album-5', @@ -131,6 +138,7 @@ export const albumStub = { sharedLinks: [], sharedUsers: [], isActivityEnabled: true, + order: AssetOrder.DESC, }), oneAssetInvalidThumbnail: Object.freeze({ id: 'album-6', @@ -147,6 +155,7 @@ export const albumStub = { sharedLinks: [], sharedUsers: [], isActivityEnabled: true, + order: AssetOrder.DESC, }), oneAssetValidThumbnail: Object.freeze({ id: 'album-6', @@ -163,5 +172,6 @@ export const albumStub = { sharedLinks: [], sharedUsers: [], isActivityEnabled: true, + order: AssetOrder.DESC, }), }; diff --git a/server/test/fixtures/shared-link.stub.ts b/server/test/fixtures/shared-link.stub.ts index 61b44a544a..109f051907 100644 --- a/server/test/fixtures/shared-link.stub.ts +++ b/server/test/fixtures/shared-link.stub.ts @@ -1,5 +1,5 @@ import { AlbumResponseDto, AssetResponseDto, ExifResponseDto, mapUser, SharedLinkResponseDto } from '@app/domain'; -import { AssetType, SharedLinkEntity, SharedLinkType, UserEntity } from '@app/infra/entities'; +import { AssetOrder, AssetType, SharedLinkEntity, SharedLinkType, UserEntity } from '@app/infra/entities'; import { assetStub } from './asset.stub'; import { authStub } from './auth.stub'; import { libraryStub } from './library.stub'; @@ -101,6 +101,7 @@ const albumResponse: AlbumResponseDto = { assets: [], assetCount: 1, isActivityEnabled: true, + order: AssetOrder.DESC, }; export const sharedLinkStub = { @@ -181,6 +182,7 @@ export const sharedLinkStub = { sharedUsers: [], sharedLinks: [], isActivityEnabled: true, + order: AssetOrder.DESC, assets: [ { id: 'id_1', diff --git a/web/src/lib/components/album-page/album-options.svelte b/web/src/lib/components/album-page/album-options.svelte index 6cbce418ba..d5f816047f 100644 --- a/web/src/lib/components/album-page/album-options.svelte +++ b/web/src/lib/components/album-page/album-options.svelte @@ -1,22 +1,55 @@ dispatch('close')}> @@ -34,8 +67,16 @@
-

SHARING

-
+

SETTINGS

+
+ {#if order} + + {/if} { + const handleToggle = (selectedOption: RenderedOption) => { for (const [key, option] of Object.entries(options)) { if (option === selectedOption) { $slideshowNavigation = key as SlideshowNavigation; diff --git a/web/src/lib/stores/assets.store.ts b/web/src/lib/stores/assets.store.ts index 15519b4b26..a61f9ed35a 100644 --- a/web/src/lib/stores/assets.store.ts +++ b/web/src/lib/stores/assets.store.ts @@ -161,7 +161,10 @@ export class AssetStore { this.assetToBucket = {}; this.albumAssets = new Set(); - const buckets = await getTimeBuckets({ ...this.options, key: getKey() }); + const buckets = await getTimeBuckets({ + ...this.options, + key: getKey(), + }); this.initialized = true; diff --git a/web/src/routes/(user)/albums/[albumId]/+page.svelte b/web/src/routes/(user)/albums/[albumId]/+page.svelte index 05d94bf3e1..9e272ce3f6 100644 --- a/web/src/routes/(user)/albums/[albumId]/+page.svelte +++ b/web/src/routes/(user)/albums/[albumId]/+page.svelte @@ -58,6 +58,7 @@ updateAlbumInfo, type ActivityResponseDto, type UserResponseDto, + AssetOrder, } from '@immich/sdk'; import { mdiArrowLeft, @@ -83,6 +84,7 @@ $: album = data.album; $: albumId = album.id; + $: albumKey = `${albumId}_${albumOrder}`; $: { if (!album.isActivityEnabled && $numberOfComments === 0) { @@ -112,8 +114,9 @@ let globalWidth: number; let assetGridWidth: number; let textArea: HTMLTextAreaElement; + let albumOrder: AssetOrder | undefined = data.album.order; - $: assetStore = new AssetStore({ albumId }); + $: assetStore = new AssetStore({ albumId, order: albumOrder }); const assetInteractionStore = createAssetInteractionStore(); const { isMultiSelectState, selectedAssets } = assetInteractionStore; @@ -512,7 +515,7 @@ style={`width:${assetGridWidth}px`} > - {#key albumId} + {#key albumKey} {#if viewMode === ViewMode.SELECT_ASSETS} (albumOrder = order)} on:close={() => (viewMode = ViewMode.VIEW)} on:toggleEnableActivity={handleToggleEnableActivity} on:showSelectSharedUser={() => (viewMode = ViewMode.SELECT_USERS)} diff --git a/web/src/test-data/factories/album-factory.ts b/web/src/test-data/factories/album-factory.ts index fd941f51f7..3d761fcf35 100644 --- a/web/src/test-data/factories/album-factory.ts +++ b/web/src/test-data/factories/album-factory.ts @@ -1,5 +1,5 @@ import { faker } from '@faker-js/faker'; -import type { AlbumResponseDto } from '@immich/sdk'; +import { AssetOrder, type AlbumResponseDto } from '@immich/sdk'; import { Sync } from 'factory.ts'; import { userFactory } from './user-factory'; @@ -18,4 +18,5 @@ export const albumFactory = Sync.makeFactory({ sharedUsers: [], hasSharedLink: false, isActivityEnabled: true, + order: AssetOrder.Desc, });