From 31f7e1aca31d53b7c855733ecd3e095fb5105334 Mon Sep 17 00:00:00 2001 From: martin <74269598+martabal@users.noreply.github.com> Date: Thu, 14 Mar 2024 17:45:03 +0100 Subject: [PATCH] feat(server, web): album orders (#7819) * feat: album orders * fix: tests * pr feedback * pr feedback * pr feedback * fix: tests * add comment * pr feedback * fix: rendering issue * wording * fix: order value doesn't change --------- Co-authored-by: Alex Tran --- e2e/src/api/specs/album.e2e-spec.ts | 2 + mobile/openapi/doc/AlbumResponseDto.md | Bin 1226 -> 1287 bytes mobile/openapi/doc/AssetApi.md | Bin 57798 -> 57994 bytes mobile/openapi/doc/UpdateAlbumDto.md | Bin 576 -> 637 bytes mobile/openapi/lib/api/asset_api.dart | Bin 59681 -> 60103 bytes .../openapi/lib/model/album_response_dto.dart | Bin 8948 -> 9588 bytes .../openapi/lib/model/update_album_dto.dart | Bin 5614 -> 6254 bytes .../openapi/test/album_response_dto_test.dart | Bin 2365 -> 2464 bytes mobile/openapi/test/asset_api_test.dart | Bin 5755 -> 5791 bytes .../openapi/test/update_album_dto_test.dart | Bin 919 -> 1018 bytes open-api/immich-openapi-specs.json | 22 ++++++++ open-api/typescript-sdk/src/fetch-client.ts | 18 ++++--- server/src/domain/album/album-response.dto.ts | 7 ++- server/src/domain/album/album.service.ts | 1 + .../src/domain/album/dto/album-update.dto.ts | 9 +++- server/src/domain/asset/dto/asset.dto.ts | 5 -- .../src/domain/asset/dto/time-bucket.dto.ts | 8 ++- .../domain/repositories/asset.repository.ts | 3 +- server/src/domain/search/dto/search.dto.ts | 3 +- server/src/domain/search/search.service.ts | 4 +- server/src/infra/entities/album.entity.ts | 9 ++++ .../1710182081326-AscendingOrderAlbum.ts | 14 +++++ .../infra/repositories/asset.repository.ts | 6 +-- server/src/infra/sql/album.repository.sql | 7 +++ .../src/infra/sql/shared.link.repository.sql | 2 + server/test/fixtures/album.stub.ts | 12 ++++- server/test/fixtures/shared-link.stub.ts | 4 +- .../album-page/album-options.svelte | 49 ++++++++++++++++-- .../lib/components/slideshow-settings.svelte | 2 +- web/src/lib/stores/assets.store.ts | 5 +- .../(user)/albums/[albumId]/+page.svelte | 9 +++- web/src/test-data/factories/album-factory.ts | 3 +- 32 files changed, 171 insertions(+), 33 deletions(-) create mode 100644 server/src/infra/migrations/1710182081326-AscendingOrderAlbum.ts diff --git a/e2e/src/api/specs/album.e2e-spec.ts b/e2e/src/api/specs/album.e2e-spec.ts index 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 bc00d30af1a6237d217a1f16c28ff97cb0a3c96f..dd4a94e88310e62ec682d8d6b3cf6791e744b189 100644 GIT binary patch delta 53 wcmX@b+0M1$DU(W3N@|gomO_m}w3e1*adB#iKSU%}1Hqeomsw)-6Q*a30QhkeXaE2J delta 12 TcmZqYI>ou+Dbr>p=BJDRAgu(t diff --git a/mobile/openapi/doc/AssetApi.md b/mobile/openapi/doc/AssetApi.md index c65e6a605f7bd3ce2e2c71a41e6dafc6ed48967e..1aaf195f3a97c1b6846bfca64196f04cd7c34d12 100644 GIT binary patch delta 117 zcmX?hn7QjH^9G)3*8HNB)S}II+e%nA*VS_{PHtVSxVf#tnGqr}`EHxuqF^ gGJwj8SSIi4zOs3Jj|C%0-Dau2yNsK)CL{y_0Az?GNdN!< diff --git a/mobile/openapi/doc/UpdateAlbumDto.md b/mobile/openapi/doc/UpdateAlbumDto.md index 4ded87d1bfbc5a9bb6e1bf575925ea36006c730a..89edf1c6efb5e1c02ffa18aedd18e93ca837f93d 100644 GIT binary patch delta 55 ycmX@W@|R`9Y({M@t^A^t)FLe{g&KuuEiK35;?xpqHF<7n`Q*1^ypvmVr%XODReG{h?X1na^L8*z4qg~Nd1}L~ z&Ade&%pjGU=axz{LX`we7ARlFlV6mQT4bdFrZ#)EZDE?6vuojI&T3^w6q9$k{m+X}PH5)^>S)^hseKdEX6x!Bj7VnnZ+5DSX5RdxaUEFi<_oQS Nj7S<=fSRr*0RTY2L|Om< diff --git a/mobile/openapi/lib/model/album_response_dto.dart b/mobile/openapi/lib/model/album_response_dto.dart index 43e24f87beb95686beb55cecf7961dce9612639b..d764028558b4b575cc745a8c8b46d2a43a79bf34 100644 GIT binary patch delta 231 zcmez3`o(L*WhSnYjLc%a{Gyc9qREWRBAXvFWwC5N${Nck=vZ8wTH+6quvY-fZ+2sM zVc`V{DA?LUg{rylGfBdP@=9}Z6zmlY6s#b!ij(8`#5c$B2{Cbll-R03jL8+qW8uq8 zQ_w)Nx>it91T3o_t6-~uB+(}##|oC1{8vzJ^AnLXtQsJ*tP~K=(@QJL&-E(K&(p{P Q646EKP=huvkqY1d0Ell;_W%F@ delta 50 zcmV-20L}mOO7umr(*lzh0}iwC0%`=a&ImsQvndVG0<$F$76P+L5^DsL>k|pHFc-`P IvoIY*2=UPofdBvi diff --git a/mobile/openapi/lib/model/update_album_dto.dart b/mobile/openapi/lib/model/update_album_dto.dart index dfe245aaf8b08fe5fd2d3db043401effab449c97..d9408cedfbb784deaa53afb2ff64921f677c0baa 100644 GIT binary patch delta 245 zcmaE-{mx*+BSsztg_4ZSV!iyLl+>cl_ZVNZZVq6JWfXKQE>11+2MO9MK;(|FD=^9^ zsHt%&06~69Mrsj6lY*@+RK*hxSH{U}IXNW38bIQCr8zkY_6i0HRuCnV<+u*>=|I@F zDiCuYa&2Ye%S=yKq9(G9cs>I5rJE*0C~$(>Hq)$ delta 41 zcmV+^0M`HRFzzd`;sLYr0p|s?ObHtSvx5pn0h9C#xU;$pwF0x^4~GJ?of6RniW?E; diff --git a/mobile/openapi/test/album_response_dto_test.dart b/mobile/openapi/test/album_response_dto_test.dart index 933f77c19635bb527b772c760e6e6df5f7da713c..5c79e5d2fc5d680bf376088edc7fa308a90f0fe1 100644 GIT binary patch delta 42 scmdlhv_N>nDmDSf;^Nd2|Du%CB87YqIaz^Si3QByW76Hcifu9z07#1tfB*mh delta 12 TcmZ1=yjN($Dz?qM?30-QADaX} diff --git a/mobile/openapi/test/asset_api_test.dart b/mobile/openapi/test/asset_api_test.dart index 846a5998cc88e229dee793d3bc8f4df4ee2622bd..d210d0e4d94f798486ac4f19be69f07a9d8237f2 100644 GIT binary patch delta 28 icmeyZGhcVZcAm-e1SBWF;!T>YzjpXABhT)?N#0RU*a2vq<8 diff --git a/mobile/openapi/test/update_album_dto_test.dart b/mobile/openapi/test/update_album_dto_test.dart index 67ec80010ddb7cbe5cd95d3d01af518c948bf4e0..7f1591a52ca8d5517213463ce9796cc08a4260cf 100644 GIT binary patch delta 46 vcmbQv{)>G>FY{zQCUybG;^Nd2|Du%CB87YqIXRF?krl$?U{agh%ghA;bd?VZ delta 11 ScmeyxKAn9-FZ1LN%v=B+?*woF 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, });