mirror of
https://github.com/immich-app/immich
synced 2025-11-14 17:36:12 +00: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
32 changed files with 251 additions and 49 deletions
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue