mirror of
https://github.com/immich-app/immich.git
synced 2024-12-24 10:37:28 +02:00
feat(server, web): album orders (#7819)
* feat: album orders * fix: tests * pr feedback * pr feedback * pr feedback * fix: tests * add comment * pr feedback * fix: rendering issue * wording * fix: order value doesn't change --------- Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
This commit is contained in:
parent
1c4637cb43
commit
31f7e1aca3
@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
BIN
mobile/openapi/doc/AlbumResponseDto.md
generated
BIN
mobile/openapi/doc/AlbumResponseDto.md
generated
Binary file not shown.
BIN
mobile/openapi/doc/AssetApi.md
generated
BIN
mobile/openapi/doc/AssetApi.md
generated
Binary file not shown.
BIN
mobile/openapi/doc/UpdateAlbumDto.md
generated
BIN
mobile/openapi/doc/UpdateAlbumDto.md
generated
Binary file not shown.
BIN
mobile/openapi/lib/api/asset_api.dart
generated
BIN
mobile/openapi/lib/api/asset_api.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/model/album_response_dto.dart
generated
BIN
mobile/openapi/lib/model/album_response_dto.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/model/update_album_dto.dart
generated
BIN
mobile/openapi/lib/model/update_album_dto.dart
generated
Binary file not shown.
BIN
mobile/openapi/test/album_response_dto_test.dart
generated
BIN
mobile/openapi/test/album_response_dto_test.dart
generated
Binary file not shown.
BIN
mobile/openapi/test/asset_api_test.dart
generated
BIN
mobile/openapi/test/asset_api_test.dart
generated
Binary file not shown.
BIN
mobile/openapi/test/update_album_dto_test.dart
generated
BIN
mobile/openapi/test/update_album_dto_test.dart
generated
Binary file not shown.
@ -1765,6 +1765,14 @@
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "order",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/AssetOrder"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "personId",
|
||||
"required": false,
|
||||
@ -1901,6 +1909,14 @@
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "order",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/AssetOrder"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "personId",
|
||||
"required": false,
|
||||
@ -6722,6 +6738,9 @@
|
||||
"format": "date-time",
|
||||
"type": "string"
|
||||
},
|
||||
"order": {
|
||||
"$ref": "#/components/schemas/AssetOrder"
|
||||
},
|
||||
"owner": {
|
||||
"$ref": "#/components/schemas/UserResponseDto"
|
||||
},
|
||||
@ -10335,6 +10354,9 @@
|
||||
},
|
||||
"isActivityEnabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"order": {
|
||||
"$ref": "#/components/schemas/AssetOrder"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
|
@ -153,6 +153,7 @@ export type AlbumResponseDto = {
|
||||
id: string;
|
||||
isActivityEnabled: boolean;
|
||||
lastModifiedAssetTimestamp?: string;
|
||||
order?: AssetOrder;
|
||||
owner: UserResponseDto;
|
||||
ownerId: string;
|
||||
shared: boolean;
|
||||
@ -176,6 +177,7 @@ export type UpdateAlbumDto = {
|
||||
albumThumbnailAssetId?: string;
|
||||
description?: string;
|
||||
isActivityEnabled?: boolean;
|
||||
order?: AssetOrder;
|
||||
};
|
||||
export type BulkIdsDto = {
|
||||
ids: string[];
|
||||
@ -1453,12 +1455,13 @@ export function getAssetThumbnail({ format, id, key }: {
|
||||
...opts
|
||||
}));
|
||||
}
|
||||
export function getTimeBucket({ albumId, isArchived, isFavorite, isTrashed, key, personId, size, timeBucket, userId, withPartners, withStacked }: {
|
||||
export function getTimeBucket({ albumId, isArchived, isFavorite, isTrashed, key, order, personId, size, timeBucket, userId, withPartners, withStacked }: {
|
||||
albumId?: string;
|
||||
isArchived?: boolean;
|
||||
isFavorite?: boolean;
|
||||
isTrashed?: boolean;
|
||||
key?: string;
|
||||
order?: AssetOrder;
|
||||
personId?: string;
|
||||
size: TimeBucketSize;
|
||||
timeBucket: string;
|
||||
@ -1475,6 +1478,7 @@ export function getTimeBucket({ albumId, isArchived, isFavorite, isTrashed, key,
|
||||
isFavorite,
|
||||
isTrashed,
|
||||
key,
|
||||
order,
|
||||
personId,
|
||||
size,
|
||||
timeBucket,
|
||||
@ -1485,12 +1489,13 @@ export function getTimeBucket({ albumId, isArchived, isFavorite, isTrashed, key,
|
||||
...opts
|
||||
}));
|
||||
}
|
||||
export function getTimeBuckets({ albumId, isArchived, isFavorite, isTrashed, key, personId, size, userId, withPartners, withStacked }: {
|
||||
export function getTimeBuckets({ albumId, isArchived, isFavorite, isTrashed, key, order, personId, size, userId, withPartners, withStacked }: {
|
||||
albumId?: string;
|
||||
isArchived?: boolean;
|
||||
isFavorite?: boolean;
|
||||
isTrashed?: boolean;
|
||||
key?: string;
|
||||
order?: AssetOrder;
|
||||
personId?: string;
|
||||
size: TimeBucketSize;
|
||||
userId?: string;
|
||||
@ -1506,6 +1511,7 @@ export function getTimeBuckets({ albumId, isArchived, isFavorite, isTrashed, key
|
||||
isFavorite,
|
||||
isTrashed,
|
||||
key,
|
||||
order,
|
||||
personId,
|
||||
size,
|
||||
userId,
|
||||
@ -2747,6 +2753,10 @@ export enum AssetTypeEnum {
|
||||
Audio = "AUDIO",
|
||||
Other = "OTHER"
|
||||
}
|
||||
export enum AssetOrder {
|
||||
Asc = "asc",
|
||||
Desc = "desc"
|
||||
}
|
||||
export enum Error {
|
||||
Duplicate = "duplicate",
|
||||
NoPermission = "no_permission",
|
||||
@ -2774,10 +2784,6 @@ export enum TimeBucketSize {
|
||||
Day = "DAY",
|
||||
Month = "MONTH"
|
||||
}
|
||||
export enum AssetOrder {
|
||||
Asc = "asc",
|
||||
Desc = "desc"
|
||||
}
|
||||
export enum EntityType {
|
||||
Asset = "ASSET",
|
||||
Album = "ALBUM"
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { AlbumEntity } from '@app/infra/entities';
|
||||
import { AlbumEntity, AssetOrder } from '@app/infra/entities';
|
||||
import { Optional } from '@nestjs/common';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { AssetResponseDto, mapAsset } from '../asset';
|
||||
import { AuthDto } from '../auth/auth.dto';
|
||||
@ -23,6 +24,9 @@ export class AlbumResponseDto {
|
||||
startDate?: Date;
|
||||
endDate?: Date;
|
||||
isActivityEnabled!: boolean;
|
||||
@Optional()
|
||||
@ApiProperty({ enumName: 'AssetOrder', enum: AssetOrder })
|
||||
order?: AssetOrder;
|
||||
}
|
||||
|
||||
export const mapAlbum = (entity: AlbumEntity, withAssets: boolean, auth?: AuthDto): AlbumResponseDto => {
|
||||
@ -63,6 +67,7 @@ export const mapAlbum = (entity: AlbumEntity, withAssets: boolean, auth?: AuthDt
|
||||
assets: (withAssets ? assets : []).map((asset) => mapAsset(asset, { auth })),
|
||||
assetCount: entity.assets?.length || 0,
|
||||
isActivityEnabled: entity.isActivityEnabled,
|
||||
order: entity.order,
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -148,6 +148,7 @@ export class AlbumService {
|
||||
description: dto.description,
|
||||
albumThumbnailAssetId: dto.albumThumbnailAssetId,
|
||||
isActivityEnabled: dto.isActivityEnabled,
|
||||
order: dto.order,
|
||||
});
|
||||
|
||||
return mapAlbumWithoutAssets(updatedAlbum);
|
||||
|
@ -1,4 +1,6 @@
|
||||
import { IsString } from 'class-validator';
|
||||
import { AssetOrder } from '@app/infra/entities';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsEnum, IsString } from 'class-validator';
|
||||
import { Optional, ValidateBoolean, ValidateUUID } from '../../domain.util';
|
||||
|
||||
export class UpdateAlbumDto {
|
||||
@ -15,4 +17,9 @@ export class UpdateAlbumDto {
|
||||
|
||||
@ValidateBoolean({ optional: true })
|
||||
isActivityEnabled?: boolean;
|
||||
|
||||
@IsEnum(AssetOrder)
|
||||
@Optional()
|
||||
@ApiProperty({ enum: AssetOrder, enumName: 'AssetOrder' })
|
||||
order?: AssetOrder;
|
||||
}
|
||||
|
@ -18,11 +18,6 @@ export class DeviceIdDto {
|
||||
deviceId!: string;
|
||||
}
|
||||
|
||||
export enum AssetOrder {
|
||||
ASC = 'asc',
|
||||
DESC = 'desc',
|
||||
}
|
||||
|
||||
const hasGPS = (o: { latitude: undefined; longitude: undefined }) =>
|
||||
o.latitude !== undefined || o.longitude !== undefined;
|
||||
const ValidateGPS = () => ValidateIf(hasGPS);
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { AssetOrder } from '@app/infra/entities';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsEnum, IsNotEmpty, IsString } from 'class-validator';
|
||||
import { ValidateBoolean, ValidateUUID } from '../../domain.util';
|
||||
import { Optional, ValidateBoolean, ValidateUUID } from '../../domain.util';
|
||||
import { TimeBucketSize } from '../../repositories';
|
||||
|
||||
export class TimeBucketDto {
|
||||
@ -32,6 +33,11 @@ export class TimeBucketDto {
|
||||
|
||||
@ValidateBoolean({ optional: true })
|
||||
withPartners?: boolean;
|
||||
|
||||
@IsEnum(AssetOrder)
|
||||
@Optional()
|
||||
@ApiProperty({ enum: AssetOrder, enumName: 'AssetOrder' })
|
||||
order?: AssetOrder;
|
||||
}
|
||||
|
||||
export class TimeBucketAssetDto extends TimeBucketDto {
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { AssetSearchOptions, ReverseGeocodeResult, SearchExploreItem } from '@app/domain';
|
||||
import { AssetEntity, AssetJobStatusEntity, AssetType, ExifEntity } from '@app/infra/entities';
|
||||
import { AssetEntity, AssetJobStatusEntity, AssetOrder, AssetType, ExifEntity } from '@app/infra/entities';
|
||||
import { FindOptionsRelations, FindOptionsSelect } from 'typeorm';
|
||||
import { Paginated, PaginationOptions } from '../domain.util';
|
||||
|
||||
@ -66,6 +66,7 @@ export interface AssetBuilderOptions {
|
||||
|
||||
export interface TimeBucketOptions extends AssetBuilderOptions {
|
||||
size: TimeBucketSize;
|
||||
order?: AssetOrder;
|
||||
}
|
||||
|
||||
export interface TimeBucketItem {
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { AssetOrder } from '@app/domain/asset/dto/asset.dto';
|
||||
import { AssetType, GeodataPlacesEntity } from '@app/infra/entities';
|
||||
import { AssetOrder, AssetType, GeodataPlacesEntity } from '@app/infra/entities';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { Type } from 'class-transformer';
|
||||
import { IsEnum, IsInt, IsNotEmpty, IsString, Max, Min } from 'class-validator';
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { AssetEntity } from '@app/infra/entities';
|
||||
import { AssetEntity, AssetOrder } from '@app/infra/entities';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { AssetOrder, AssetResponseDto, mapAsset } from '../asset';
|
||||
import { AssetResponseDto, mapAsset } from '../asset';
|
||||
import { AuthDto } from '../auth';
|
||||
import { PersonResponseDto } from '../person';
|
||||
import {
|
||||
|
@ -14,6 +14,12 @@ import { AssetEntity } from './asset.entity';
|
||||
import { SharedLinkEntity } from './shared-link.entity';
|
||||
import { UserEntity } from './user.entity';
|
||||
|
||||
// ran into issues when importing the enum from `asset.dto.ts`
|
||||
export enum AssetOrder {
|
||||
ASC = 'asc',
|
||||
DESC = 'desc',
|
||||
}
|
||||
|
||||
@Entity('albums')
|
||||
export class AlbumEntity {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
@ -59,4 +65,7 @@ export class AlbumEntity {
|
||||
|
||||
@Column({ default: true })
|
||||
isActivityEnabled!: boolean;
|
||||
|
||||
@Column({ type: 'varchar', default: AssetOrder.DESC })
|
||||
order!: AssetOrder;
|
||||
}
|
||||
|
@ -0,0 +1,14 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class AscendingOrderAlbum1710182081326 implements MigrationInterface {
|
||||
name = 'AscendingOrderAlbum1710182081326'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "albums" ADD "order" character varying NOT NULL DEFAULT 'desc'`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "albums" DROP COLUMN "order"`);
|
||||
}
|
||||
|
||||
}
|
@ -36,7 +36,7 @@ import {
|
||||
Not,
|
||||
Repository,
|
||||
} from 'typeorm';
|
||||
import { AssetEntity, AssetJobStatusEntity, AssetType, ExifEntity, SmartInfoEntity } from '../entities';
|
||||
import { AssetEntity, AssetJobStatusEntity, AssetOrder, AssetType, ExifEntity, SmartInfoEntity } from '../entities';
|
||||
import { DummyValue, GenerateSql } from '../infra.util';
|
||||
import { Chunked, ChunkedArray, OptionalBetween, paginate, paginatedBuilder, searchAssetBuilder } from '../infra.utils';
|
||||
import { Instrumentation } from '../instrumentation';
|
||||
@ -607,7 +607,7 @@ export class AssetRepository implements IAssetRepository {
|
||||
.select(`COUNT(asset.id)::int`, 'count')
|
||||
.addSelect(truncated, 'timeBucket')
|
||||
.groupBy(truncated)
|
||||
.orderBy(truncated, 'DESC')
|
||||
.orderBy(truncated, options.order === AssetOrder.ASC ? 'ASC' : 'DESC')
|
||||
.getRawMany();
|
||||
}
|
||||
|
||||
@ -620,7 +620,7 @@ export class AssetRepository implements IAssetRepository {
|
||||
// First sort by the day in localtime (put it in the right bucket)
|
||||
.orderBy(truncated, 'DESC')
|
||||
// and then sort by the actual time
|
||||
.addOrderBy('asset.fileCreatedAt', 'DESC')
|
||||
.addOrderBy('asset.fileCreatedAt', options.order === AssetOrder.ASC ? 'ASC' : 'DESC')
|
||||
.getMany()
|
||||
);
|
||||
}
|
||||
|
@ -15,6 +15,7 @@ FROM
|
||||
"AlbumEntity"."deletedAt" AS "AlbumEntity_deletedAt",
|
||||
"AlbumEntity"."albumThumbnailAssetId" AS "AlbumEntity_albumThumbnailAssetId",
|
||||
"AlbumEntity"."isActivityEnabled" AS "AlbumEntity_isActivityEnabled",
|
||||
"AlbumEntity"."order" AS "AlbumEntity_order",
|
||||
"AlbumEntity__AlbumEntity_owner"."id" AS "AlbumEntity__AlbumEntity_owner_id",
|
||||
"AlbumEntity__AlbumEntity_owner"."name" AS "AlbumEntity__AlbumEntity_owner_name",
|
||||
"AlbumEntity__AlbumEntity_owner"."avatarColor" AS "AlbumEntity__AlbumEntity_owner_avatarColor",
|
||||
@ -91,6 +92,7 @@ SELECT
|
||||
"AlbumEntity"."deletedAt" AS "AlbumEntity_deletedAt",
|
||||
"AlbumEntity"."albumThumbnailAssetId" AS "AlbumEntity_albumThumbnailAssetId",
|
||||
"AlbumEntity"."isActivityEnabled" AS "AlbumEntity_isActivityEnabled",
|
||||
"AlbumEntity"."order" AS "AlbumEntity_order",
|
||||
"AlbumEntity__AlbumEntity_owner"."id" AS "AlbumEntity__AlbumEntity_owner_id",
|
||||
"AlbumEntity__AlbumEntity_owner"."name" AS "AlbumEntity__AlbumEntity_owner_name",
|
||||
"AlbumEntity__AlbumEntity_owner"."avatarColor" AS "AlbumEntity__AlbumEntity_owner_avatarColor",
|
||||
@ -149,6 +151,7 @@ SELECT
|
||||
"AlbumEntity"."deletedAt" AS "AlbumEntity_deletedAt",
|
||||
"AlbumEntity"."albumThumbnailAssetId" AS "AlbumEntity_albumThumbnailAssetId",
|
||||
"AlbumEntity"."isActivityEnabled" AS "AlbumEntity_isActivityEnabled",
|
||||
"AlbumEntity"."order" AS "AlbumEntity_order",
|
||||
"AlbumEntity__AlbumEntity_owner"."id" AS "AlbumEntity__AlbumEntity_owner_id",
|
||||
"AlbumEntity__AlbumEntity_owner"."name" AS "AlbumEntity__AlbumEntity_owner_name",
|
||||
"AlbumEntity__AlbumEntity_owner"."avatarColor" AS "AlbumEntity__AlbumEntity_owner_avatarColor",
|
||||
@ -279,6 +282,7 @@ SELECT
|
||||
"AlbumEntity"."deletedAt" AS "AlbumEntity_deletedAt",
|
||||
"AlbumEntity"."albumThumbnailAssetId" AS "AlbumEntity_albumThumbnailAssetId",
|
||||
"AlbumEntity"."isActivityEnabled" AS "AlbumEntity_isActivityEnabled",
|
||||
"AlbumEntity"."order" AS "AlbumEntity_order",
|
||||
"AlbumEntity__AlbumEntity_sharedUsers"."id" AS "AlbumEntity__AlbumEntity_sharedUsers_id",
|
||||
"AlbumEntity__AlbumEntity_sharedUsers"."name" AS "AlbumEntity__AlbumEntity_sharedUsers_name",
|
||||
"AlbumEntity__AlbumEntity_sharedUsers"."avatarColor" AS "AlbumEntity__AlbumEntity_sharedUsers_avatarColor",
|
||||
@ -352,6 +356,7 @@ SELECT
|
||||
"AlbumEntity"."deletedAt" AS "AlbumEntity_deletedAt",
|
||||
"AlbumEntity"."albumThumbnailAssetId" AS "AlbumEntity_albumThumbnailAssetId",
|
||||
"AlbumEntity"."isActivityEnabled" AS "AlbumEntity_isActivityEnabled",
|
||||
"AlbumEntity"."order" AS "AlbumEntity_order",
|
||||
"AlbumEntity__AlbumEntity_sharedUsers"."id" AS "AlbumEntity__AlbumEntity_sharedUsers_id",
|
||||
"AlbumEntity__AlbumEntity_sharedUsers"."name" AS "AlbumEntity__AlbumEntity_sharedUsers_name",
|
||||
"AlbumEntity__AlbumEntity_sharedUsers"."avatarColor" AS "AlbumEntity__AlbumEntity_sharedUsers_avatarColor",
|
||||
@ -462,6 +467,7 @@ SELECT
|
||||
"AlbumEntity"."deletedAt" AS "AlbumEntity_deletedAt",
|
||||
"AlbumEntity"."albumThumbnailAssetId" AS "AlbumEntity_albumThumbnailAssetId",
|
||||
"AlbumEntity"."isActivityEnabled" AS "AlbumEntity_isActivityEnabled",
|
||||
"AlbumEntity"."order" AS "AlbumEntity_order",
|
||||
"AlbumEntity__AlbumEntity_sharedUsers"."id" AS "AlbumEntity__AlbumEntity_sharedUsers_id",
|
||||
"AlbumEntity__AlbumEntity_sharedUsers"."name" AS "AlbumEntity__AlbumEntity_sharedUsers_name",
|
||||
"AlbumEntity__AlbumEntity_sharedUsers"."avatarColor" AS "AlbumEntity__AlbumEntity_sharedUsers_avatarColor",
|
||||
@ -553,6 +559,7 @@ SELECT
|
||||
"AlbumEntity"."deletedAt" AS "AlbumEntity_deletedAt",
|
||||
"AlbumEntity"."albumThumbnailAssetId" AS "AlbumEntity_albumThumbnailAssetId",
|
||||
"AlbumEntity"."isActivityEnabled" AS "AlbumEntity_isActivityEnabled",
|
||||
"AlbumEntity"."order" AS "AlbumEntity_order",
|
||||
"AlbumEntity__AlbumEntity_owner"."id" AS "AlbumEntity__AlbumEntity_owner_id",
|
||||
"AlbumEntity__AlbumEntity_owner"."name" AS "AlbumEntity__AlbumEntity_owner_name",
|
||||
"AlbumEntity__AlbumEntity_owner"."avatarColor" AS "AlbumEntity__AlbumEntity_owner_avatarColor",
|
||||
|
@ -87,6 +87,7 @@ FROM
|
||||
"SharedLinkEntity__SharedLinkEntity_album"."deletedAt" AS "SharedLinkEntity__SharedLinkEntity_album_deletedAt",
|
||||
"SharedLinkEntity__SharedLinkEntity_album"."albumThumbnailAssetId" AS "SharedLinkEntity__SharedLinkEntity_album_albumThumbnailAssetId",
|
||||
"SharedLinkEntity__SharedLinkEntity_album"."isActivityEnabled" AS "SharedLinkEntity__SharedLinkEntity_album_isActivityEnabled",
|
||||
"SharedLinkEntity__SharedLinkEntity_album"."order" AS "SharedLinkEntity__SharedLinkEntity_album_order",
|
||||
"4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."id" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_id",
|
||||
"4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."deviceAssetId" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_deviceAssetId",
|
||||
"4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."ownerId" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_ownerId",
|
||||
@ -248,6 +249,7 @@ SELECT
|
||||
"SharedLinkEntity__SharedLinkEntity_album"."deletedAt" AS "SharedLinkEntity__SharedLinkEntity_album_deletedAt",
|
||||
"SharedLinkEntity__SharedLinkEntity_album"."albumThumbnailAssetId" AS "SharedLinkEntity__SharedLinkEntity_album_albumThumbnailAssetId",
|
||||
"SharedLinkEntity__SharedLinkEntity_album"."isActivityEnabled" AS "SharedLinkEntity__SharedLinkEntity_album_isActivityEnabled",
|
||||
"SharedLinkEntity__SharedLinkEntity_album"."order" AS "SharedLinkEntity__SharedLinkEntity_album_order",
|
||||
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."id" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_id",
|
||||
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."name" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_name",
|
||||
"6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."avatarColor" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_avatarColor",
|
||||
|
12
server/test/fixtures/album.stub.ts
vendored
12
server/test/fixtures/album.stub.ts
vendored
@ -1,4 +1,4 @@
|
||||
import { AlbumEntity } from '@app/infra/entities';
|
||||
import { AlbumEntity, AssetOrder } from '@app/infra/entities';
|
||||
import { assetStub } from './asset.stub';
|
||||
import { authStub } from './auth.stub';
|
||||
import { userStub } from './user.stub';
|
||||
@ -19,6 +19,7 @@ export const albumStub = {
|
||||
sharedLinks: [],
|
||||
sharedUsers: [],
|
||||
isActivityEnabled: true,
|
||||
order: AssetOrder.DESC,
|
||||
}),
|
||||
sharedWithUser: Object.freeze<AlbumEntity>({
|
||||
id: 'album-2',
|
||||
@ -35,6 +36,7 @@ export const albumStub = {
|
||||
sharedLinks: [],
|
||||
sharedUsers: [userStub.user1],
|
||||
isActivityEnabled: true,
|
||||
order: AssetOrder.DESC,
|
||||
}),
|
||||
sharedWithMultiple: Object.freeze<AlbumEntity>({
|
||||
id: 'album-3',
|
||||
@ -51,6 +53,7 @@ export const albumStub = {
|
||||
sharedLinks: [],
|
||||
sharedUsers: [userStub.user1, userStub.user2],
|
||||
isActivityEnabled: true,
|
||||
order: AssetOrder.DESC,
|
||||
}),
|
||||
sharedWithAdmin: Object.freeze<AlbumEntity>({
|
||||
id: 'album-3',
|
||||
@ -67,6 +70,7 @@ export const albumStub = {
|
||||
sharedLinks: [],
|
||||
sharedUsers: [userStub.admin],
|
||||
isActivityEnabled: true,
|
||||
order: AssetOrder.DESC,
|
||||
}),
|
||||
oneAsset: Object.freeze<AlbumEntity>({
|
||||
id: 'album-4',
|
||||
@ -83,6 +87,7 @@ export const albumStub = {
|
||||
sharedLinks: [],
|
||||
sharedUsers: [],
|
||||
isActivityEnabled: true,
|
||||
order: AssetOrder.DESC,
|
||||
}),
|
||||
twoAssets: Object.freeze<AlbumEntity>({
|
||||
id: 'album-4a',
|
||||
@ -99,6 +104,7 @@ export const albumStub = {
|
||||
sharedLinks: [],
|
||||
sharedUsers: [],
|
||||
isActivityEnabled: true,
|
||||
order: AssetOrder.DESC,
|
||||
}),
|
||||
emptyWithInvalidThumbnail: Object.freeze<AlbumEntity>({
|
||||
id: 'album-5',
|
||||
@ -115,6 +121,7 @@ export const albumStub = {
|
||||
sharedLinks: [],
|
||||
sharedUsers: [],
|
||||
isActivityEnabled: true,
|
||||
order: AssetOrder.DESC,
|
||||
}),
|
||||
emptyWithValidThumbnail: Object.freeze<AlbumEntity>({
|
||||
id: 'album-5',
|
||||
@ -131,6 +138,7 @@ export const albumStub = {
|
||||
sharedLinks: [],
|
||||
sharedUsers: [],
|
||||
isActivityEnabled: true,
|
||||
order: AssetOrder.DESC,
|
||||
}),
|
||||
oneAssetInvalidThumbnail: Object.freeze<AlbumEntity>({
|
||||
id: 'album-6',
|
||||
@ -147,6 +155,7 @@ export const albumStub = {
|
||||
sharedLinks: [],
|
||||
sharedUsers: [],
|
||||
isActivityEnabled: true,
|
||||
order: AssetOrder.DESC,
|
||||
}),
|
||||
oneAssetValidThumbnail: Object.freeze<AlbumEntity>({
|
||||
id: 'album-6',
|
||||
@ -163,5 +172,6 @@ export const albumStub = {
|
||||
sharedLinks: [],
|
||||
sharedUsers: [],
|
||||
isActivityEnabled: true,
|
||||
order: AssetOrder.DESC,
|
||||
}),
|
||||
};
|
||||
|
4
server/test/fixtures/shared-link.stub.ts
vendored
4
server/test/fixtures/shared-link.stub.ts
vendored
@ -1,5 +1,5 @@
|
||||
import { AlbumResponseDto, AssetResponseDto, ExifResponseDto, mapUser, SharedLinkResponseDto } from '@app/domain';
|
||||
import { AssetType, SharedLinkEntity, SharedLinkType, UserEntity } from '@app/infra/entities';
|
||||
import { AssetOrder, AssetType, SharedLinkEntity, SharedLinkType, UserEntity } from '@app/infra/entities';
|
||||
import { assetStub } from './asset.stub';
|
||||
import { authStub } from './auth.stub';
|
||||
import { libraryStub } from './library.stub';
|
||||
@ -101,6 +101,7 @@ const albumResponse: AlbumResponseDto = {
|
||||
assets: [],
|
||||
assetCount: 1,
|
||||
isActivityEnabled: true,
|
||||
order: AssetOrder.DESC,
|
||||
};
|
||||
|
||||
export const sharedLinkStub = {
|
||||
@ -181,6 +182,7 @@ export const sharedLinkStub = {
|
||||
sharedUsers: [],
|
||||
sharedLinks: [],
|
||||
isActivityEnabled: true,
|
||||
order: AssetOrder.DESC,
|
||||
assets: [
|
||||
{
|
||||
id: 'id_1',
|
||||
|
@ -1,22 +1,55 @@
|
||||
<script lang="ts">
|
||||
import Icon from '$lib/components/elements/icon.svelte';
|
||||
import type { AlbumResponseDto, UserResponseDto } from '@immich/sdk';
|
||||
import { mdiClose, mdiPlus } from '@mdi/js';
|
||||
import { updateAlbumInfo, type AlbumResponseDto, type UserResponseDto, AssetOrder } from '@immich/sdk';
|
||||
import { mdiArrowDownThin, mdiArrowUpThin, mdiClose, mdiPlus } from '@mdi/js';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
|
||||
import CircleIconButton from '../elements/buttons/circle-icon-button.svelte';
|
||||
import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
|
||||
import UserAvatar from '$lib/components/shared-components/user-avatar.svelte';
|
||||
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
|
||||
import SettingDropdown from '../shared-components/settings/setting-dropdown.svelte';
|
||||
import type { RenderedOption } from '../elements/dropdown.svelte';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { findKey } from 'lodash-es';
|
||||
|
||||
export let album: AlbumResponseDto;
|
||||
export let order: AssetOrder | undefined;
|
||||
export let user: UserResponseDto;
|
||||
export let onChangeOrder: (order: AssetOrder) => void;
|
||||
|
||||
const options: Record<AssetOrder, RenderedOption> = {
|
||||
[AssetOrder.Asc]: { icon: mdiArrowUpThin, title: 'Oldest first' },
|
||||
[AssetOrder.Desc]: { icon: mdiArrowDownThin, title: 'Newest first' },
|
||||
};
|
||||
|
||||
$: selectedOption = order ? options[order] : options[AssetOrder.Desc];
|
||||
|
||||
const dispatch = createEventDispatcher<{
|
||||
close: void;
|
||||
toggleEnableActivity: void;
|
||||
showSelectSharedUser: void;
|
||||
}>();
|
||||
|
||||
const handleToggle = async (returnedOption: RenderedOption) => {
|
||||
if (selectedOption === returnedOption) {
|
||||
return;
|
||||
}
|
||||
let order = AssetOrder.Desc;
|
||||
order = findKey(options, (option) => option === returnedOption) as AssetOrder;
|
||||
|
||||
try {
|
||||
await updateAlbumInfo({
|
||||
id: album.id,
|
||||
updateAlbumDto: {
|
||||
order,
|
||||
},
|
||||
});
|
||||
onChangeOrder(order);
|
||||
} catch (error) {
|
||||
handleError(error, 'Error updating album order');
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<FullScreenModal onClose={() => dispatch('close')}>
|
||||
@ -34,8 +67,16 @@
|
||||
|
||||
<div class=" items-center justify-center p-4">
|
||||
<div class="py-2">
|
||||
<h2 class="text-gray text-sm mb-3">SHARING</h2>
|
||||
<div class="p-2">
|
||||
<h2 class="text-gray text-sm mb-2">SETTINGS</h2>
|
||||
<div class="grid p-2 gap-y-2">
|
||||
{#if order}
|
||||
<SettingDropdown
|
||||
title="Display order"
|
||||
options={Object.values(options)}
|
||||
selectedOption={options[order]}
|
||||
onToggle={handleToggle}
|
||||
/>
|
||||
{/if}
|
||||
<SettingSwitch
|
||||
title="Comments & likes"
|
||||
subtitle="Let others respond"
|
||||
|
@ -20,7 +20,7 @@
|
||||
[SlideshowNavigation.DescendingOrder]: { icon: mdiArrowDownThin, title: 'Forward' },
|
||||
};
|
||||
|
||||
export const handleToggle = (selectedOption: RenderedOption) => {
|
||||
const handleToggle = (selectedOption: RenderedOption) => {
|
||||
for (const [key, option] of Object.entries(options)) {
|
||||
if (option === selectedOption) {
|
||||
$slideshowNavigation = key as SlideshowNavigation;
|
||||
|
@ -161,7 +161,10 @@ export class AssetStore {
|
||||
this.assetToBucket = {};
|
||||
this.albumAssets = new Set();
|
||||
|
||||
const buckets = await getTimeBuckets({ ...this.options, key: getKey() });
|
||||
const buckets = await getTimeBuckets({
|
||||
...this.options,
|
||||
key: getKey(),
|
||||
});
|
||||
|
||||
this.initialized = true;
|
||||
|
||||
|
@ -58,6 +58,7 @@
|
||||
updateAlbumInfo,
|
||||
type ActivityResponseDto,
|
||||
type UserResponseDto,
|
||||
AssetOrder,
|
||||
} from '@immich/sdk';
|
||||
import {
|
||||
mdiArrowLeft,
|
||||
@ -83,6 +84,7 @@
|
||||
|
||||
$: album = data.album;
|
||||
$: albumId = album.id;
|
||||
$: albumKey = `${albumId}_${albumOrder}`;
|
||||
|
||||
$: {
|
||||
if (!album.isActivityEnabled && $numberOfComments === 0) {
|
||||
@ -112,8 +114,9 @@
|
||||
let globalWidth: number;
|
||||
let assetGridWidth: number;
|
||||
let textArea: HTMLTextAreaElement;
|
||||
let albumOrder: AssetOrder | undefined = data.album.order;
|
||||
|
||||
$: assetStore = new AssetStore({ albumId });
|
||||
$: assetStore = new AssetStore({ albumId, order: albumOrder });
|
||||
const assetInteractionStore = createAssetInteractionStore();
|
||||
const { isMultiSelectState, selectedAssets } = assetInteractionStore;
|
||||
|
||||
@ -512,7 +515,7 @@
|
||||
style={`width:${assetGridWidth}px`}
|
||||
>
|
||||
<!-- Use key because AssetGrid can't deal with changing stores -->
|
||||
{#key albumId}
|
||||
{#key albumKey}
|
||||
{#if viewMode === ViewMode.SELECT_ASSETS}
|
||||
<AssetGrid
|
||||
assetStore={timelineStore}
|
||||
@ -679,7 +682,9 @@
|
||||
{#if viewMode === ViewMode.OPTIONS && $user}
|
||||
<AlbumOptions
|
||||
{album}
|
||||
order={albumOrder}
|
||||
user={$user}
|
||||
onChangeOrder={(order) => (albumOrder = order)}
|
||||
on:close={() => (viewMode = ViewMode.VIEW)}
|
||||
on:toggleEnableActivity={handleToggleEnableActivity}
|
||||
on:showSelectSharedUser={() => (viewMode = ViewMode.SELECT_USERS)}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { faker } from '@faker-js/faker';
|
||||
import type { AlbumResponseDto } from '@immich/sdk';
|
||||
import { AssetOrder, type AlbumResponseDto } from '@immich/sdk';
|
||||
import { Sync } from 'factory.ts';
|
||||
import { userFactory } from './user-factory';
|
||||
|
||||
@ -18,4 +18,5 @@ export const albumFactory = Sync.makeFactory<AlbumResponseDto>({
|
||||
sharedUsers: [],
|
||||
hasSharedLink: false,
|
||||
isActivityEnabled: true,
|
||||
order: AssetOrder.Desc,
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user