mirror of
				https://github.com/immich-app/immich.git
				synced 2025-11-04 03:27:09 -05:00 
			
		
		
		
	fix(server): album perf query (#5232)
* Revert "fix: album performances (#5224)" This reverts commit c438e179543fe4f079dfa4ec15a227ad89b359ae. * Revert "fix: album sorting options (#5127)" This reverts commit 725f30c49448689f781d6d25374e6d08d1874c4b.
This commit is contained in:
		
							parent
							
								
									a13052e24c
								
							
						
					
					
						commit
						f094ff2aa1
					
				@ -37,6 +37,15 @@ export const mapAlbum = (entity: AlbumEntity, withAssets: boolean): AlbumRespons
 | 
				
			|||||||
  const hasSharedLink = entity.sharedLinks?.length > 0;
 | 
					  const hasSharedLink = entity.sharedLinks?.length > 0;
 | 
				
			||||||
  const hasSharedUser = sharedUsers.length > 0;
 | 
					  const hasSharedUser = sharedUsers.length > 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  let startDate = assets.at(0)?.fileCreatedAt || undefined;
 | 
				
			||||||
 | 
					  let endDate = assets.at(-1)?.fileCreatedAt || undefined;
 | 
				
			||||||
 | 
					  // Swap dates if start date is greater than end date.
 | 
				
			||||||
 | 
					  if (startDate && endDate && startDate > endDate) {
 | 
				
			||||||
 | 
					    const temp = startDate;
 | 
				
			||||||
 | 
					    startDate = endDate;
 | 
				
			||||||
 | 
					    endDate = temp;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return {
 | 
					  return {
 | 
				
			||||||
    albumName: entity.albumName,
 | 
					    albumName: entity.albumName,
 | 
				
			||||||
    description: entity.description,
 | 
					    description: entity.description,
 | 
				
			||||||
@ -49,10 +58,10 @@ export const mapAlbum = (entity: AlbumEntity, withAssets: boolean): AlbumRespons
 | 
				
			|||||||
    sharedUsers,
 | 
					    sharedUsers,
 | 
				
			||||||
    shared: hasSharedUser || hasSharedLink,
 | 
					    shared: hasSharedUser || hasSharedLink,
 | 
				
			||||||
    hasSharedLink,
 | 
					    hasSharedLink,
 | 
				
			||||||
    startDate: entity.startDate ? entity.startDate : undefined,
 | 
					    startDate,
 | 
				
			||||||
    endDate: entity.endDate ? entity.endDate : undefined,
 | 
					    endDate,
 | 
				
			||||||
    assets: (withAssets ? assets : []).map((asset) => mapAsset(asset)),
 | 
					    assets: (withAssets ? assets : []).map((asset) => mapAsset(asset)),
 | 
				
			||||||
    assetCount: entity.assetCount,
 | 
					    assetCount: entity.assets?.length || 0,
 | 
				
			||||||
    isActivityEnabled: entity.isActivityEnabled,
 | 
					    isActivityEnabled: entity.isActivityEnabled,
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
				
			|||||||
@ -58,6 +58,10 @@ describe(AlbumService.name, () => {
 | 
				
			|||||||
  describe('getAll', () => {
 | 
					  describe('getAll', () => {
 | 
				
			||||||
    it('gets list of albums for auth user', async () => {
 | 
					    it('gets list of albums for auth user', async () => {
 | 
				
			||||||
      albumMock.getOwned.mockResolvedValue([albumStub.empty, albumStub.sharedWithUser]);
 | 
					      albumMock.getOwned.mockResolvedValue([albumStub.empty, albumStub.sharedWithUser]);
 | 
				
			||||||
 | 
					      albumMock.getAssetCountForIds.mockResolvedValue([
 | 
				
			||||||
 | 
					        { albumId: albumStub.empty.id, assetCount: 0 },
 | 
				
			||||||
 | 
					        { albumId: albumStub.sharedWithUser.id, assetCount: 0 },
 | 
				
			||||||
 | 
					      ]);
 | 
				
			||||||
      albumMock.getInvalidThumbnail.mockResolvedValue([]);
 | 
					      albumMock.getInvalidThumbnail.mockResolvedValue([]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      const result = await sut.getAll(authStub.admin, {});
 | 
					      const result = await sut.getAll(authStub.admin, {});
 | 
				
			||||||
@ -68,6 +72,7 @@ describe(AlbumService.name, () => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    it('gets list of albums that have a specific asset', async () => {
 | 
					    it('gets list of albums that have a specific asset', async () => {
 | 
				
			||||||
      albumMock.getByAssetId.mockResolvedValue([albumStub.oneAsset]);
 | 
					      albumMock.getByAssetId.mockResolvedValue([albumStub.oneAsset]);
 | 
				
			||||||
 | 
					      albumMock.getAssetCountForIds.mockResolvedValue([{ albumId: albumStub.oneAsset.id, assetCount: 1 }]);
 | 
				
			||||||
      albumMock.getInvalidThumbnail.mockResolvedValue([]);
 | 
					      albumMock.getInvalidThumbnail.mockResolvedValue([]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      const result = await sut.getAll(authStub.admin, { assetId: albumStub.oneAsset.id });
 | 
					      const result = await sut.getAll(authStub.admin, { assetId: albumStub.oneAsset.id });
 | 
				
			||||||
@ -78,6 +83,7 @@ describe(AlbumService.name, () => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    it('gets list of albums that are shared', async () => {
 | 
					    it('gets list of albums that are shared', async () => {
 | 
				
			||||||
      albumMock.getShared.mockResolvedValue([albumStub.sharedWithUser]);
 | 
					      albumMock.getShared.mockResolvedValue([albumStub.sharedWithUser]);
 | 
				
			||||||
 | 
					      albumMock.getAssetCountForIds.mockResolvedValue([{ albumId: albumStub.sharedWithUser.id, assetCount: 0 }]);
 | 
				
			||||||
      albumMock.getInvalidThumbnail.mockResolvedValue([]);
 | 
					      albumMock.getInvalidThumbnail.mockResolvedValue([]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      const result = await sut.getAll(authStub.admin, { shared: true });
 | 
					      const result = await sut.getAll(authStub.admin, { shared: true });
 | 
				
			||||||
@ -88,6 +94,7 @@ describe(AlbumService.name, () => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    it('gets list of albums that are NOT shared', async () => {
 | 
					    it('gets list of albums that are NOT shared', async () => {
 | 
				
			||||||
      albumMock.getNotShared.mockResolvedValue([albumStub.empty]);
 | 
					      albumMock.getNotShared.mockResolvedValue([albumStub.empty]);
 | 
				
			||||||
 | 
					      albumMock.getAssetCountForIds.mockResolvedValue([{ albumId: albumStub.empty.id, assetCount: 0 }]);
 | 
				
			||||||
      albumMock.getInvalidThumbnail.mockResolvedValue([]);
 | 
					      albumMock.getInvalidThumbnail.mockResolvedValue([]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      const result = await sut.getAll(authStub.admin, { shared: false });
 | 
					      const result = await sut.getAll(authStub.admin, { shared: false });
 | 
				
			||||||
@ -99,6 +106,7 @@ describe(AlbumService.name, () => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  it('counts assets correctly', async () => {
 | 
					  it('counts assets correctly', async () => {
 | 
				
			||||||
    albumMock.getOwned.mockResolvedValue([albumStub.oneAsset]);
 | 
					    albumMock.getOwned.mockResolvedValue([albumStub.oneAsset]);
 | 
				
			||||||
 | 
					    albumMock.getAssetCountForIds.mockResolvedValue([{ albumId: albumStub.oneAsset.id, assetCount: 1 }]);
 | 
				
			||||||
    albumMock.getInvalidThumbnail.mockResolvedValue([]);
 | 
					    albumMock.getInvalidThumbnail.mockResolvedValue([]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const result = await sut.getAll(authStub.admin, {});
 | 
					    const result = await sut.getAll(authStub.admin, {});
 | 
				
			||||||
@ -110,6 +118,9 @@ describe(AlbumService.name, () => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  it('updates the album thumbnail by listing all albums', async () => {
 | 
					  it('updates the album thumbnail by listing all albums', async () => {
 | 
				
			||||||
    albumMock.getOwned.mockResolvedValue([albumStub.oneAssetInvalidThumbnail]);
 | 
					    albumMock.getOwned.mockResolvedValue([albumStub.oneAssetInvalidThumbnail]);
 | 
				
			||||||
 | 
					    albumMock.getAssetCountForIds.mockResolvedValue([
 | 
				
			||||||
 | 
					      { albumId: albumStub.oneAssetInvalidThumbnail.id, assetCount: 1 },
 | 
				
			||||||
 | 
					    ]);
 | 
				
			||||||
    albumMock.getInvalidThumbnail.mockResolvedValue([albumStub.oneAssetInvalidThumbnail.id]);
 | 
					    albumMock.getInvalidThumbnail.mockResolvedValue([albumStub.oneAssetInvalidThumbnail.id]);
 | 
				
			||||||
    albumMock.update.mockResolvedValue(albumStub.oneAssetValidThumbnail);
 | 
					    albumMock.update.mockResolvedValue(albumStub.oneAssetValidThumbnail);
 | 
				
			||||||
    assetMock.getFirstAssetForAlbumId.mockResolvedValue(albumStub.oneAssetInvalidThumbnail.assets[0]);
 | 
					    assetMock.getFirstAssetForAlbumId.mockResolvedValue(albumStub.oneAssetInvalidThumbnail.assets[0]);
 | 
				
			||||||
@ -123,6 +134,9 @@ describe(AlbumService.name, () => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  it('removes the thumbnail for an empty album', async () => {
 | 
					  it('removes the thumbnail for an empty album', async () => {
 | 
				
			||||||
    albumMock.getOwned.mockResolvedValue([albumStub.emptyWithInvalidThumbnail]);
 | 
					    albumMock.getOwned.mockResolvedValue([albumStub.emptyWithInvalidThumbnail]);
 | 
				
			||||||
 | 
					    albumMock.getAssetCountForIds.mockResolvedValue([
 | 
				
			||||||
 | 
					      { albumId: albumStub.emptyWithInvalidThumbnail.id, assetCount: 1 },
 | 
				
			||||||
 | 
					    ]);
 | 
				
			||||||
    albumMock.getInvalidThumbnail.mockResolvedValue([albumStub.emptyWithInvalidThumbnail.id]);
 | 
					    albumMock.getInvalidThumbnail.mockResolvedValue([albumStub.emptyWithInvalidThumbnail.id]);
 | 
				
			||||||
    albumMock.update.mockResolvedValue(albumStub.emptyWithValidThumbnail);
 | 
					    albumMock.update.mockResolvedValue(albumStub.emptyWithValidThumbnail);
 | 
				
			||||||
    assetMock.getFirstAssetForAlbumId.mockResolvedValue(null);
 | 
					    assetMock.getFirstAssetForAlbumId.mockResolvedValue(null);
 | 
				
			||||||
 | 
				
			|||||||
@ -66,12 +66,21 @@ export class AlbumService {
 | 
				
			|||||||
      albums = await this.albumRepository.getOwned(ownerId);
 | 
					      albums = await this.albumRepository.getOwned(ownerId);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Get asset count for each album. Then map the result to an object:
 | 
				
			||||||
 | 
					    // { [albumId]: assetCount }
 | 
				
			||||||
 | 
					    const albumsAssetCount = await this.albumRepository.getAssetCountForIds(albums.map((album) => album.id));
 | 
				
			||||||
 | 
					    const albumsAssetCountObj = albumsAssetCount.reduce((obj: Record<string, number>, { albumId, assetCount }) => {
 | 
				
			||||||
 | 
					      obj[albumId] = assetCount;
 | 
				
			||||||
 | 
					      return obj;
 | 
				
			||||||
 | 
					    }, {});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return Promise.all(
 | 
					    return Promise.all(
 | 
				
			||||||
      albums.map(async (album) => {
 | 
					      albums.map(async (album) => {
 | 
				
			||||||
        const lastModifiedAsset = await this.assetRepository.getLastUpdatedAssetForAlbumId(album.id);
 | 
					        const lastModifiedAsset = await this.assetRepository.getLastUpdatedAssetForAlbumId(album.id);
 | 
				
			||||||
        return {
 | 
					        return {
 | 
				
			||||||
          ...mapAlbumWithoutAssets(album),
 | 
					          ...mapAlbumWithoutAssets(album),
 | 
				
			||||||
          sharedLinks: undefined,
 | 
					          sharedLinks: undefined,
 | 
				
			||||||
 | 
					          assetCount: albumsAssetCountObj[album.id],
 | 
				
			||||||
          lastModifiedAssetTimestamp: lastModifiedAsset?.fileModifiedAt,
 | 
					          lastModifiedAssetTimestamp: lastModifiedAsset?.fileModifiedAt,
 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
      }),
 | 
					      }),
 | 
				
			||||||
@ -81,8 +90,7 @@ export class AlbumService {
 | 
				
			|||||||
  async get(authUser: AuthUserDto, id: string, dto: AlbumInfoDto): Promise<AlbumResponseDto> {
 | 
					  async get(authUser: AuthUserDto, id: string, dto: AlbumInfoDto): Promise<AlbumResponseDto> {
 | 
				
			||||||
    await this.access.requirePermission(authUser, Permission.ALBUM_READ, id);
 | 
					    await this.access.requirePermission(authUser, Permission.ALBUM_READ, id);
 | 
				
			||||||
    await this.albumRepository.updateThumbnails();
 | 
					    await this.albumRepository.updateThumbnails();
 | 
				
			||||||
    const withAssets = dto.withoutAssets === undefined ? true : !dto.withoutAssets;
 | 
					    return mapAlbum(await this.findOrFail(id, { withAssets: true }), !dto.withoutAssets);
 | 
				
			||||||
    return mapAlbum(await this.findOrFail(id, { withAssets }), !dto.withoutAssets);
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async create(authUser: AuthUserDto, dto: CreateAlbumDto): Promise<AlbumResponseDto> {
 | 
					  async create(authUser: AuthUserDto, dto: CreateAlbumDto): Promise<AlbumResponseDto> {
 | 
				
			||||||
 | 
				
			|||||||
@ -30,6 +30,7 @@ export interface IAlbumRepository {
 | 
				
			|||||||
  hasAsset(asset: AlbumAsset): Promise<boolean>;
 | 
					  hasAsset(asset: AlbumAsset): Promise<boolean>;
 | 
				
			||||||
  removeAsset(assetId: string): Promise<void>;
 | 
					  removeAsset(assetId: string): Promise<void>;
 | 
				
			||||||
  removeAssets(assets: AlbumAssets): Promise<void>;
 | 
					  removeAssets(assets: AlbumAssets): Promise<void>;
 | 
				
			||||||
 | 
					  getAssetCountForIds(ids: string[]): Promise<AlbumAssetCount[]>;
 | 
				
			||||||
  getInvalidThumbnail(): Promise<string[]>;
 | 
					  getInvalidThumbnail(): Promise<string[]>;
 | 
				
			||||||
  getOwned(ownerId: string): Promise<AlbumEntity[]>;
 | 
					  getOwned(ownerId: string): Promise<AlbumEntity[]>;
 | 
				
			||||||
  getShared(ownerId: string): Promise<AlbumEntity[]>;
 | 
					  getShared(ownerId: string): Promise<AlbumEntity[]>;
 | 
				
			||||||
 | 
				
			|||||||
@ -40,7 +40,7 @@ export function mapSharedLink(sharedLink: SharedLinkEntity): SharedLinkResponseD
 | 
				
			|||||||
    createdAt: sharedLink.createdAt,
 | 
					    createdAt: sharedLink.createdAt,
 | 
				
			||||||
    expiresAt: sharedLink.expiresAt,
 | 
					    expiresAt: sharedLink.expiresAt,
 | 
				
			||||||
    assets: assets.map((asset) => mapAsset(asset)),
 | 
					    assets: assets.map((asset) => mapAsset(asset)),
 | 
				
			||||||
    album: sharedLink.album?.id ? mapAlbumWithoutAssets(sharedLink.album) : undefined,
 | 
					    album: sharedLink.album ? mapAlbumWithoutAssets(sharedLink.album) : undefined,
 | 
				
			||||||
    allowUpload: sharedLink.allowUpload,
 | 
					    allowUpload: sharedLink.allowUpload,
 | 
				
			||||||
    allowDownload: sharedLink.allowDownload,
 | 
					    allowDownload: sharedLink.allowDownload,
 | 
				
			||||||
    showMetadata: sharedLink.showExif,
 | 
					    showMetadata: sharedLink.showExif,
 | 
				
			||||||
 | 
				
			|||||||
@ -9,7 +9,6 @@ import {
 | 
				
			|||||||
  OneToMany,
 | 
					  OneToMany,
 | 
				
			||||||
  PrimaryGeneratedColumn,
 | 
					  PrimaryGeneratedColumn,
 | 
				
			||||||
  UpdateDateColumn,
 | 
					  UpdateDateColumn,
 | 
				
			||||||
  VirtualColumn,
 | 
					 | 
				
			||||||
} from 'typeorm';
 | 
					} from 'typeorm';
 | 
				
			||||||
import { AssetEntity } from './asset.entity';
 | 
					import { AssetEntity } from './asset.entity';
 | 
				
			||||||
import { SharedLinkEntity } from './shared-link.entity';
 | 
					import { SharedLinkEntity } from './shared-link.entity';
 | 
				
			||||||
@ -60,34 +59,4 @@ export class AlbumEntity {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  @Column({ default: true })
 | 
					  @Column({ default: true })
 | 
				
			||||||
  isActivityEnabled!: boolean;
 | 
					  isActivityEnabled!: boolean;
 | 
				
			||||||
 | 
					 | 
				
			||||||
  @VirtualColumn({
 | 
					 | 
				
			||||||
    query: (alias) => `
 | 
					 | 
				
			||||||
    SELECT MIN(assets."fileCreatedAt") 
 | 
					 | 
				
			||||||
    FROM "assets" assets
 | 
					 | 
				
			||||||
    JOIN "albums_assets_assets" aa ON aa."assetsId" = assets.id
 | 
					 | 
				
			||||||
    WHERE aa."albumsId" = ${alias}.id
 | 
					 | 
				
			||||||
    `,
 | 
					 | 
				
			||||||
  })
 | 
					 | 
				
			||||||
  startDate!: Date | null;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @VirtualColumn({
 | 
					 | 
				
			||||||
    query: (alias) => `
 | 
					 | 
				
			||||||
    SELECT MAX(assets."fileCreatedAt") 
 | 
					 | 
				
			||||||
    FROM "assets" assets
 | 
					 | 
				
			||||||
    JOIN "albums_assets_assets" aa ON aa."assetsId" = assets.id
 | 
					 | 
				
			||||||
    WHERE aa."albumsId" = ${alias}.id
 | 
					 | 
				
			||||||
    `,
 | 
					 | 
				
			||||||
  })
 | 
					 | 
				
			||||||
  endDate!: Date | null;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @VirtualColumn({
 | 
					 | 
				
			||||||
    query: (alias) => `
 | 
					 | 
				
			||||||
    SELECT COUNT(assets."id") 
 | 
					 | 
				
			||||||
    FROM "assets" assets
 | 
					 | 
				
			||||||
    JOIN "albums_assets_assets" aa ON aa."assetsId" = assets.id
 | 
					 | 
				
			||||||
    WHERE aa."albumsId" = ${alias}.id
 | 
					 | 
				
			||||||
    `,
 | 
					 | 
				
			||||||
  })
 | 
					 | 
				
			||||||
  assetCount!: number;
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -1,4 +1,4 @@
 | 
				
			|||||||
import { AlbumAsset, AlbumAssets, AlbumInfoOptions, IAlbumRepository } from '@app/domain';
 | 
					import { AlbumAsset, AlbumAssetCount, AlbumAssets, AlbumInfoOptions, IAlbumRepository } from '@app/domain';
 | 
				
			||||||
import { Injectable } from '@nestjs/common';
 | 
					import { Injectable } from '@nestjs/common';
 | 
				
			||||||
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
 | 
					import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
 | 
				
			||||||
import { DataSource, FindOptionsOrder, FindOptionsRelations, In, IsNull, Not, Repository } from 'typeorm';
 | 
					import { DataSource, FindOptionsOrder, FindOptionsRelations, In, IsNull, Not, Repository } from 'typeorm';
 | 
				
			||||||
@ -56,10 +56,31 @@ export class AlbumRepository implements IAlbumRepository {
 | 
				
			|||||||
      ],
 | 
					      ],
 | 
				
			||||||
      relations: { owner: true, sharedUsers: true },
 | 
					      relations: { owner: true, sharedUsers: true },
 | 
				
			||||||
      order: { createdAt: 'DESC' },
 | 
					      order: { createdAt: 'DESC' },
 | 
				
			||||||
      relationLoadStrategy: 'query',
 | 
					 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  async getAssetCountForIds(ids: string[]): Promise<AlbumAssetCount[]> {
 | 
				
			||||||
 | 
					    // Guard against running invalid query when ids list is empty.
 | 
				
			||||||
 | 
					    if (!ids.length) {
 | 
				
			||||||
 | 
					      return [];
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Only possible with query builder because of GROUP BY.
 | 
				
			||||||
 | 
					    const countByAlbums = await this.repository
 | 
				
			||||||
 | 
					      .createQueryBuilder('album')
 | 
				
			||||||
 | 
					      .select('album.id')
 | 
				
			||||||
 | 
					      .addSelect('COUNT(albums_assets.assetsId)', 'asset_count')
 | 
				
			||||||
 | 
					      .leftJoin('albums_assets_assets', 'albums_assets', 'albums_assets.albumsId = album.id')
 | 
				
			||||||
 | 
					      .where('album.id IN (:...ids)', { ids })
 | 
				
			||||||
 | 
					      .groupBy('album.id')
 | 
				
			||||||
 | 
					      .getRawMany();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return countByAlbums.map<AlbumAssetCount>((albumCount) => ({
 | 
				
			||||||
 | 
					      albumId: albumCount['album_id'],
 | 
				
			||||||
 | 
					      assetCount: Number(albumCount['asset_count']),
 | 
				
			||||||
 | 
					    }));
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					  /**
 | 
				
			||||||
   * Returns the album IDs that have an invalid thumbnail, when:
 | 
					   * Returns the album IDs that have an invalid thumbnail, when:
 | 
				
			||||||
   *  - Thumbnail references an asset outside the album
 | 
					   *  - Thumbnail references an asset outside the album
 | 
				
			||||||
@ -92,7 +113,6 @@ export class AlbumRepository implements IAlbumRepository {
 | 
				
			|||||||
      relations: { sharedUsers: true, sharedLinks: true, owner: true },
 | 
					      relations: { sharedUsers: true, sharedLinks: true, owner: true },
 | 
				
			||||||
      where: { ownerId },
 | 
					      where: { ownerId },
 | 
				
			||||||
      order: { createdAt: 'DESC' },
 | 
					      order: { createdAt: 'DESC' },
 | 
				
			||||||
      relationLoadStrategy: 'query',
 | 
					 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -108,7 +128,6 @@ export class AlbumRepository implements IAlbumRepository {
 | 
				
			|||||||
        { ownerId, sharedUsers: { id: Not(IsNull()) } },
 | 
					        { ownerId, sharedUsers: { id: Not(IsNull()) } },
 | 
				
			||||||
      ],
 | 
					      ],
 | 
				
			||||||
      order: { createdAt: 'DESC' },
 | 
					      order: { createdAt: 'DESC' },
 | 
				
			||||||
      relationLoadStrategy: 'query',
 | 
					 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -120,7 +139,6 @@ export class AlbumRepository implements IAlbumRepository {
 | 
				
			|||||||
      relations: { sharedUsers: true, sharedLinks: true, owner: true },
 | 
					      relations: { sharedUsers: true, sharedLinks: true, owner: true },
 | 
				
			||||||
      where: { ownerId, sharedUsers: { id: IsNull() }, sharedLinks: { id: IsNull() } },
 | 
					      where: { ownerId, sharedUsers: { id: IsNull() }, sharedLinks: { id: IsNull() } },
 | 
				
			||||||
      order: { createdAt: 'DESC' },
 | 
					      order: { createdAt: 'DESC' },
 | 
				
			||||||
      relationLoadStrategy: 'query',
 | 
					 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										30
									
								
								server/test/fixtures/album.stub.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										30
									
								
								server/test/fixtures/album.stub.ts
									
									
									
									
										vendored
									
									
								
							@ -19,9 +19,6 @@ export const albumStub = {
 | 
				
			|||||||
    sharedLinks: [],
 | 
					    sharedLinks: [],
 | 
				
			||||||
    sharedUsers: [],
 | 
					    sharedUsers: [],
 | 
				
			||||||
    isActivityEnabled: true,
 | 
					    isActivityEnabled: true,
 | 
				
			||||||
    startDate: null,
 | 
					 | 
				
			||||||
    endDate: null,
 | 
					 | 
				
			||||||
    assetCount: 0,
 | 
					 | 
				
			||||||
  }),
 | 
					  }),
 | 
				
			||||||
  sharedWithUser: Object.freeze<AlbumEntity>({
 | 
					  sharedWithUser: Object.freeze<AlbumEntity>({
 | 
				
			||||||
    id: 'album-2',
 | 
					    id: 'album-2',
 | 
				
			||||||
@ -38,9 +35,6 @@ export const albumStub = {
 | 
				
			|||||||
    sharedLinks: [],
 | 
					    sharedLinks: [],
 | 
				
			||||||
    sharedUsers: [userStub.user1],
 | 
					    sharedUsers: [userStub.user1],
 | 
				
			||||||
    isActivityEnabled: true,
 | 
					    isActivityEnabled: true,
 | 
				
			||||||
    startDate: null,
 | 
					 | 
				
			||||||
    endDate: null,
 | 
					 | 
				
			||||||
    assetCount: 0,
 | 
					 | 
				
			||||||
  }),
 | 
					  }),
 | 
				
			||||||
  sharedWithMultiple: Object.freeze<AlbumEntity>({
 | 
					  sharedWithMultiple: Object.freeze<AlbumEntity>({
 | 
				
			||||||
    id: 'album-3',
 | 
					    id: 'album-3',
 | 
				
			||||||
@ -57,9 +51,6 @@ export const albumStub = {
 | 
				
			|||||||
    sharedLinks: [],
 | 
					    sharedLinks: [],
 | 
				
			||||||
    sharedUsers: [userStub.user1, userStub.user2],
 | 
					    sharedUsers: [userStub.user1, userStub.user2],
 | 
				
			||||||
    isActivityEnabled: true,
 | 
					    isActivityEnabled: true,
 | 
				
			||||||
    startDate: null,
 | 
					 | 
				
			||||||
    endDate: null,
 | 
					 | 
				
			||||||
    assetCount: 0,
 | 
					 | 
				
			||||||
  }),
 | 
					  }),
 | 
				
			||||||
  sharedWithAdmin: Object.freeze<AlbumEntity>({
 | 
					  sharedWithAdmin: Object.freeze<AlbumEntity>({
 | 
				
			||||||
    id: 'album-3',
 | 
					    id: 'album-3',
 | 
				
			||||||
@ -76,9 +67,6 @@ export const albumStub = {
 | 
				
			|||||||
    sharedLinks: [],
 | 
					    sharedLinks: [],
 | 
				
			||||||
    sharedUsers: [userStub.admin],
 | 
					    sharedUsers: [userStub.admin],
 | 
				
			||||||
    isActivityEnabled: true,
 | 
					    isActivityEnabled: true,
 | 
				
			||||||
    startDate: null,
 | 
					 | 
				
			||||||
    endDate: null,
 | 
					 | 
				
			||||||
    assetCount: 0,
 | 
					 | 
				
			||||||
  }),
 | 
					  }),
 | 
				
			||||||
  oneAsset: Object.freeze<AlbumEntity>({
 | 
					  oneAsset: Object.freeze<AlbumEntity>({
 | 
				
			||||||
    id: 'album-4',
 | 
					    id: 'album-4',
 | 
				
			||||||
@ -95,9 +83,6 @@ export const albumStub = {
 | 
				
			|||||||
    sharedLinks: [],
 | 
					    sharedLinks: [],
 | 
				
			||||||
    sharedUsers: [],
 | 
					    sharedUsers: [],
 | 
				
			||||||
    isActivityEnabled: true,
 | 
					    isActivityEnabled: true,
 | 
				
			||||||
    startDate: assetStub.image.fileCreatedAt,
 | 
					 | 
				
			||||||
    endDate: assetStub.image.fileCreatedAt,
 | 
					 | 
				
			||||||
    assetCount: 1,
 | 
					 | 
				
			||||||
  }),
 | 
					  }),
 | 
				
			||||||
  twoAssets: Object.freeze<AlbumEntity>({
 | 
					  twoAssets: Object.freeze<AlbumEntity>({
 | 
				
			||||||
    id: 'album-4a',
 | 
					    id: 'album-4a',
 | 
				
			||||||
@ -114,9 +99,6 @@ export const albumStub = {
 | 
				
			|||||||
    sharedLinks: [],
 | 
					    sharedLinks: [],
 | 
				
			||||||
    sharedUsers: [],
 | 
					    sharedUsers: [],
 | 
				
			||||||
    isActivityEnabled: true,
 | 
					    isActivityEnabled: true,
 | 
				
			||||||
    startDate: assetStub.withLocation.fileCreatedAt,
 | 
					 | 
				
			||||||
    endDate: assetStub.image.fileCreatedAt,
 | 
					 | 
				
			||||||
    assetCount: 2,
 | 
					 | 
				
			||||||
  }),
 | 
					  }),
 | 
				
			||||||
  emptyWithInvalidThumbnail: Object.freeze<AlbumEntity>({
 | 
					  emptyWithInvalidThumbnail: Object.freeze<AlbumEntity>({
 | 
				
			||||||
    id: 'album-5',
 | 
					    id: 'album-5',
 | 
				
			||||||
@ -133,9 +115,6 @@ export const albumStub = {
 | 
				
			|||||||
    sharedLinks: [],
 | 
					    sharedLinks: [],
 | 
				
			||||||
    sharedUsers: [],
 | 
					    sharedUsers: [],
 | 
				
			||||||
    isActivityEnabled: true,
 | 
					    isActivityEnabled: true,
 | 
				
			||||||
    startDate: null,
 | 
					 | 
				
			||||||
    endDate: null,
 | 
					 | 
				
			||||||
    assetCount: 0,
 | 
					 | 
				
			||||||
  }),
 | 
					  }),
 | 
				
			||||||
  emptyWithValidThumbnail: Object.freeze<AlbumEntity>({
 | 
					  emptyWithValidThumbnail: Object.freeze<AlbumEntity>({
 | 
				
			||||||
    id: 'album-5',
 | 
					    id: 'album-5',
 | 
				
			||||||
@ -152,9 +131,6 @@ export const albumStub = {
 | 
				
			|||||||
    sharedLinks: [],
 | 
					    sharedLinks: [],
 | 
				
			||||||
    sharedUsers: [],
 | 
					    sharedUsers: [],
 | 
				
			||||||
    isActivityEnabled: true,
 | 
					    isActivityEnabled: true,
 | 
				
			||||||
    startDate: null,
 | 
					 | 
				
			||||||
    endDate: null,
 | 
					 | 
				
			||||||
    assetCount: 0,
 | 
					 | 
				
			||||||
  }),
 | 
					  }),
 | 
				
			||||||
  oneAssetInvalidThumbnail: Object.freeze<AlbumEntity>({
 | 
					  oneAssetInvalidThumbnail: Object.freeze<AlbumEntity>({
 | 
				
			||||||
    id: 'album-6',
 | 
					    id: 'album-6',
 | 
				
			||||||
@ -171,9 +147,6 @@ export const albumStub = {
 | 
				
			|||||||
    sharedLinks: [],
 | 
					    sharedLinks: [],
 | 
				
			||||||
    sharedUsers: [],
 | 
					    sharedUsers: [],
 | 
				
			||||||
    isActivityEnabled: true,
 | 
					    isActivityEnabled: true,
 | 
				
			||||||
    startDate: assetStub.image.fileCreatedAt,
 | 
					 | 
				
			||||||
    endDate: assetStub.image.fileCreatedAt,
 | 
					 | 
				
			||||||
    assetCount: 1,
 | 
					 | 
				
			||||||
  }),
 | 
					  }),
 | 
				
			||||||
  oneAssetValidThumbnail: Object.freeze<AlbumEntity>({
 | 
					  oneAssetValidThumbnail: Object.freeze<AlbumEntity>({
 | 
				
			||||||
    id: 'album-6',
 | 
					    id: 'album-6',
 | 
				
			||||||
@ -190,8 +163,5 @@ export const albumStub = {
 | 
				
			|||||||
    sharedLinks: [],
 | 
					    sharedLinks: [],
 | 
				
			||||||
    sharedUsers: [],
 | 
					    sharedUsers: [],
 | 
				
			||||||
    isActivityEnabled: true,
 | 
					    isActivityEnabled: true,
 | 
				
			||||||
    startDate: assetStub.image.fileCreatedAt,
 | 
					 | 
				
			||||||
    endDate: assetStub.image.fileCreatedAt,
 | 
					 | 
				
			||||||
    assetCount: 1,
 | 
					 | 
				
			||||||
  }),
 | 
					  }),
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										3
									
								
								server/test/fixtures/shared-link.stub.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								server/test/fixtures/shared-link.stub.ts
									
									
									
									
										vendored
									
									
								
							@ -181,9 +181,6 @@ export const sharedLinkStub = {
 | 
				
			|||||||
      sharedUsers: [],
 | 
					      sharedUsers: [],
 | 
				
			||||||
      sharedLinks: [],
 | 
					      sharedLinks: [],
 | 
				
			||||||
      isActivityEnabled: true,
 | 
					      isActivityEnabled: true,
 | 
				
			||||||
      startDate: today,
 | 
					 | 
				
			||||||
      endDate: today,
 | 
					 | 
				
			||||||
      assetCount: 1,
 | 
					 | 
				
			||||||
      assets: [
 | 
					      assets: [
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
          id: 'id_1',
 | 
					          id: 'id_1',
 | 
				
			||||||
 | 
				
			|||||||
@ -5,6 +5,7 @@ export const newAlbumRepositoryMock = (): jest.Mocked<IAlbumRepository> => {
 | 
				
			|||||||
    getById: jest.fn(),
 | 
					    getById: jest.fn(),
 | 
				
			||||||
    getByIds: jest.fn(),
 | 
					    getByIds: jest.fn(),
 | 
				
			||||||
    getByAssetId: jest.fn(),
 | 
					    getByAssetId: jest.fn(),
 | 
				
			||||||
 | 
					    getAssetCountForIds: jest.fn(),
 | 
				
			||||||
    getInvalidThumbnail: jest.fn(),
 | 
					    getInvalidThumbnail: jest.fn(),
 | 
				
			||||||
    getOwned: jest.fn(),
 | 
					    getOwned: jest.fn(),
 | 
				
			||||||
    getShared: jest.fn(),
 | 
					    getShared: jest.fn(),
 | 
				
			||||||
 | 
				
			|||||||
@ -5,10 +5,10 @@
 | 
				
			|||||||
  export let option: Sort;
 | 
					  export let option: Sort;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const handleSort = () => {
 | 
					  const handleSort = () => {
 | 
				
			||||||
    if (albumViewSettings === option.title) {
 | 
					    if (albumViewSettings === option.sortTitle) {
 | 
				
			||||||
      option.sortDesc = !option.sortDesc;
 | 
					      option.sortDesc = !option.sortDesc;
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
      albumViewSettings = option.title;
 | 
					      albumViewSettings = option.sortTitle;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
@ -18,12 +18,12 @@
 | 
				
			|||||||
    class="rounded-lg p-2 hover:bg-immich-dark-primary hover:dark:bg-immich-dark-primary/50"
 | 
					    class="rounded-lg p-2 hover:bg-immich-dark-primary hover:dark:bg-immich-dark-primary/50"
 | 
				
			||||||
    on:click={() => handleSort()}
 | 
					    on:click={() => handleSort()}
 | 
				
			||||||
  >
 | 
					  >
 | 
				
			||||||
    {#if albumViewSettings === option.title}
 | 
					    {#if albumViewSettings === option.sortTitle}
 | 
				
			||||||
      {#if option.sortDesc}
 | 
					      {#if option.sortDesc}
 | 
				
			||||||
        ↓
 | 
					        ↓
 | 
				
			||||||
      {:else}
 | 
					      {:else}
 | 
				
			||||||
        ↑
 | 
					        ↑
 | 
				
			||||||
      {/if}
 | 
					      {/if}
 | 
				
			||||||
    {/if}{option.title}</button
 | 
					    {/if}{option.table}</button
 | 
				
			||||||
  ></th
 | 
					  ></th
 | 
				
			||||||
>
 | 
					>
 | 
				
			||||||
 | 
				
			|||||||
@ -7,7 +7,6 @@
 | 
				
			|||||||
  import { createEventDispatcher } from 'svelte';
 | 
					  import { createEventDispatcher } from 'svelte';
 | 
				
			||||||
  import { goto } from '$app/navigation';
 | 
					  import { goto } from '$app/navigation';
 | 
				
			||||||
  import { mdiCircleEditOutline, mdiContentCopy, mdiDelete, mdiOpenInNew } from '@mdi/js';
 | 
					  import { mdiCircleEditOutline, mdiContentCopy, mdiDelete, mdiOpenInNew } from '@mdi/js';
 | 
				
			||||||
  import noThumbnailUrl from '$lib/assets/no-thumbnail.png';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  export let link: SharedLinkResponseDto;
 | 
					  export let link: SharedLinkResponseDto;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -61,7 +60,6 @@
 | 
				
			|||||||
  class="flex w-full gap-4 border-b border-gray-200 py-4 transition-all hover:border-immich-primary dark:border-gray-600 dark:text-immich-gray dark:hover:border-immich-dark-primary"
 | 
					  class="flex w-full gap-4 border-b border-gray-200 py-4 transition-all hover:border-immich-primary dark:border-gray-600 dark:text-immich-gray dark:hover:border-immich-dark-primary"
 | 
				
			||||||
>
 | 
					>
 | 
				
			||||||
  <div>
 | 
					  <div>
 | 
				
			||||||
    {#if (link?.album?.assetCount && link?.album?.assetCount > 0) || link.assets.length > 0}
 | 
					 | 
				
			||||||
    {#await getAssetInfo()}
 | 
					    {#await getAssetInfo()}
 | 
				
			||||||
      <LoadingSpinner />
 | 
					      <LoadingSpinner />
 | 
				
			||||||
    {:then asset}
 | 
					    {:then asset}
 | 
				
			||||||
@ -74,15 +72,6 @@
 | 
				
			|||||||
        draggable="false"
 | 
					        draggable="false"
 | 
				
			||||||
      />
 | 
					      />
 | 
				
			||||||
    {/await}
 | 
					    {/await}
 | 
				
			||||||
    {:else}
 | 
					 | 
				
			||||||
      <img
 | 
					 | 
				
			||||||
        src={noThumbnailUrl}
 | 
					 | 
				
			||||||
        alt={'Album without assets'}
 | 
					 | 
				
			||||||
        class="h-[100px] w-[100px] rounded-lg object-cover"
 | 
					 | 
				
			||||||
        loading="lazy"
 | 
					 | 
				
			||||||
        draggable="false"
 | 
					 | 
				
			||||||
      />
 | 
					 | 
				
			||||||
    {/if}
 | 
					 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  <div class="flex flex-col justify-between">
 | 
					  <div class="flex flex-col justify-between">
 | 
				
			||||||
 | 
				
			|||||||
@ -1,6 +1,9 @@
 | 
				
			|||||||
<script lang="ts" context="module">
 | 
					<script lang="ts" context="module">
 | 
				
			||||||
 | 
					  // table is the text printed in the table and sortTitle is the text printed in the dropDow menu
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  export interface Sort {
 | 
					  export interface Sort {
 | 
				
			||||||
    title: string;
 | 
					    table: string;
 | 
				
			||||||
 | 
					    sortTitle: string;
 | 
				
			||||||
    sortDesc: boolean;
 | 
					    sortDesc: boolean;
 | 
				
			||||||
    widthClass: string;
 | 
					    widthClass: string;
 | 
				
			||||||
    sortFn: (reverse: boolean, albums: AlbumResponseDto[]) => AlbumResponseDto[];
 | 
					    sortFn: (reverse: boolean, albums: AlbumResponseDto[]) => AlbumResponseDto[];
 | 
				
			||||||
@ -51,75 +54,46 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  let sortByOptions: Record<string, Sort> = {
 | 
					  let sortByOptions: Record<string, Sort> = {
 | 
				
			||||||
    albumTitle: {
 | 
					    albumTitle: {
 | 
				
			||||||
      title: 'Album title',
 | 
					      table: 'Album title',
 | 
				
			||||||
 | 
					      sortTitle: 'Album title',
 | 
				
			||||||
      sortDesc: $albumViewSettings.sortDesc, // Load Sort Direction
 | 
					      sortDesc: $albumViewSettings.sortDesc, // Load Sort Direction
 | 
				
			||||||
      widthClass: 'text-left w-8/12 sm:w-4/12 md:w-4/12 md:w-4/12 xl:w-[30%] 2xl:w-[40%]',
 | 
					      widthClass: 'w-8/12 text-left sm:w-4/12 md:w-4/12 md:w-4/12 2xl:w-6/12',
 | 
				
			||||||
      sortFn: (reverse, albums) => {
 | 
					      sortFn: (reverse, albums) => {
 | 
				
			||||||
        return orderBy(albums, 'albumName', [reverse ? 'desc' : 'asc']);
 | 
					        return orderBy(albums, 'albumName', [reverse ? 'desc' : 'asc']);
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    numberOfAssets: {
 | 
					    numberOfAssets: {
 | 
				
			||||||
      title: 'Number of assets',
 | 
					      table: 'Assets',
 | 
				
			||||||
 | 
					      sortTitle: 'Number of assets',
 | 
				
			||||||
      sortDesc: $albumViewSettings.sortDesc,
 | 
					      sortDesc: $albumViewSettings.sortDesc,
 | 
				
			||||||
      widthClass: 'text-center w-4/12 m:w-2/12 md:w-2/12 xl:w-[15%] 2xl:w-[12%]',
 | 
					      widthClass: 'w-4/12 text-center sm:w-2/12 2xl:w-1/12',
 | 
				
			||||||
      sortFn: (reverse, albums) => {
 | 
					      sortFn: (reverse, albums) => {
 | 
				
			||||||
        return orderBy(albums, 'assetCount', [reverse ? 'desc' : 'asc']);
 | 
					        return orderBy(albums, 'assetCount', [reverse ? 'desc' : 'asc']);
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    lastModified: {
 | 
					    lastModified: {
 | 
				
			||||||
      title: 'Last modified',
 | 
					      table: 'Updated date',
 | 
				
			||||||
 | 
					      sortTitle: 'Last modified',
 | 
				
			||||||
      sortDesc: $albumViewSettings.sortDesc,
 | 
					      sortDesc: $albumViewSettings.sortDesc,
 | 
				
			||||||
      widthClass: 'text-center hidden sm:block w-3/12 xl:w-[15%] 2xl:w-[12%]',
 | 
					      widthClass: 'text-center hidden sm:block w-3/12 lg:w-2/12',
 | 
				
			||||||
      sortFn: (reverse, albums) => {
 | 
					      sortFn: (reverse, albums) => {
 | 
				
			||||||
        return orderBy(albums, [(album) => new Date(album.updatedAt)], [reverse ? 'desc' : 'asc']);
 | 
					        return orderBy(albums, [(album) => new Date(album.updatedAt)], [reverse ? 'desc' : 'asc']);
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    created: {
 | 
					 | 
				
			||||||
      title: 'Created date',
 | 
					 | 
				
			||||||
      sortDesc: $albumViewSettings.sortDesc,
 | 
					 | 
				
			||||||
      widthClass: 'text-center hidden sm:block w-3/12 xl:w-[15%] 2xl:w-[12%]',
 | 
					 | 
				
			||||||
      sortFn: (reverse, albums) => {
 | 
					 | 
				
			||||||
        return orderBy(albums, [(album) => new Date(album.createdAt)], [reverse ? 'desc' : 'asc']);
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    mostRecent: {
 | 
					    mostRecent: {
 | 
				
			||||||
      title: 'Most recent photo',
 | 
					      table: 'Created date',
 | 
				
			||||||
 | 
					      sortTitle: 'Most recent photo',
 | 
				
			||||||
      sortDesc: $albumViewSettings.sortDesc,
 | 
					      sortDesc: $albumViewSettings.sortDesc,
 | 
				
			||||||
      widthClass: 'text-center hidden xl:block xl:w-[15%] 2xl:w-[12%]',
 | 
					      widthClass: 'text-center hidden sm:block w-3/12 lg:w-2/12',
 | 
				
			||||||
      sortFn: (reverse, albums) => {
 | 
					      sortFn: (reverse, albums) => {
 | 
				
			||||||
        return orderBy(
 | 
					        return orderBy(
 | 
				
			||||||
          albums,
 | 
					          albums,
 | 
				
			||||||
          [(album) => (album.endDate ? new Date(album.endDate) : '')],
 | 
					          [
 | 
				
			||||||
 | 
					            (album) =>
 | 
				
			||||||
 | 
					              album.lastModifiedAssetTimestamp ? new Date(album.lastModifiedAssetTimestamp) : new Date(album.updatedAt),
 | 
				
			||||||
 | 
					          ],
 | 
				
			||||||
          [reverse ? 'desc' : 'asc'],
 | 
					          [reverse ? 'desc' : 'asc'],
 | 
				
			||||||
        ).sort((a, b) => {
 | 
					        );
 | 
				
			||||||
          if (a.endDate === undefined) {
 | 
					 | 
				
			||||||
            return 1;
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
          if (b.endDate === undefined) {
 | 
					 | 
				
			||||||
            return -1;
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
          return 0;
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    mostOld: {
 | 
					 | 
				
			||||||
      title: 'Oldest photo',
 | 
					 | 
				
			||||||
      sortDesc: $albumViewSettings.sortDesc,
 | 
					 | 
				
			||||||
      widthClass: 'text-center hidden xl:block xl:w-[15%] 2xl:w-[12%]',
 | 
					 | 
				
			||||||
      sortFn: (reverse, albums) => {
 | 
					 | 
				
			||||||
        return orderBy(
 | 
					 | 
				
			||||||
          albums,
 | 
					 | 
				
			||||||
          [(album) => (album.startDate ? new Date(album.startDate) : null)],
 | 
					 | 
				
			||||||
          [reverse ? 'desc' : 'asc'],
 | 
					 | 
				
			||||||
        ).sort((a, b) => {
 | 
					 | 
				
			||||||
          if (a.startDate === undefined) {
 | 
					 | 
				
			||||||
            return 1;
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
          if (b.startDate === undefined) {
 | 
					 | 
				
			||||||
            return -1;
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
          return 0;
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
@ -170,25 +144,16 @@
 | 
				
			|||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  $: {
 | 
					  $: {
 | 
				
			||||||
 | 
					    const { sortBy } = $albumViewSettings;
 | 
				
			||||||
    for (const key in sortByOptions) {
 | 
					    for (const key in sortByOptions) {
 | 
				
			||||||
      if (sortByOptions[key].title === $albumViewSettings.sortBy) {
 | 
					      if (sortByOptions[key].sortTitle === sortBy) {
 | 
				
			||||||
        $albums = sortByOptions[key].sortFn(sortByOptions[key].sortDesc, $unsortedAlbums);
 | 
					        $albums = sortByOptions[key].sortFn(sortByOptions[key].sortDesc, $unsortedAlbums);
 | 
				
			||||||
        $albumViewSettings.sortDesc = sortByOptions[key].sortDesc; // "Save" sortDesc
 | 
					        $albumViewSettings.sortDesc = sortByOptions[key].sortDesc; // "Save" sortDesc
 | 
				
			||||||
        $albumViewSettings.sortBy = sortByOptions[key].title;
 | 
					 | 
				
			||||||
        break;
 | 
					        break;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const test = (searched: string): Sort => {
 | 
					 | 
				
			||||||
    for (const key in sortByOptions) {
 | 
					 | 
				
			||||||
      if (sortByOptions[key].title === searched) {
 | 
					 | 
				
			||||||
        return sortByOptions[key];
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    return sortByOptions[0];
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const handleCreateAlbum = async () => {
 | 
					  const handleCreateAlbum = async () => {
 | 
				
			||||||
    const newAlbum = await createAlbum();
 | 
					    const newAlbum = await createAlbum();
 | 
				
			||||||
    if (newAlbum) {
 | 
					    if (newAlbum) {
 | 
				
			||||||
@ -255,20 +220,19 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    <Dropdown
 | 
					    <Dropdown
 | 
				
			||||||
      options={Object.values(sortByOptions)}
 | 
					      options={Object.values(sortByOptions)}
 | 
				
			||||||
      selectedOption={test($albumViewSettings.sortBy)}
 | 
					 | 
				
			||||||
      render={(option) => {
 | 
					      render={(option) => {
 | 
				
			||||||
        return {
 | 
					        return {
 | 
				
			||||||
          title: option.title,
 | 
					          title: option.sortTitle,
 | 
				
			||||||
          icon: option.sortDesc ? mdiArrowDownThin : mdiArrowUpThin,
 | 
					          icon: option.sortDesc ? mdiArrowDownThin : mdiArrowUpThin,
 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
      }}
 | 
					      }}
 | 
				
			||||||
      on:select={(event) => {
 | 
					      on:select={(event) => {
 | 
				
			||||||
        for (const key in sortByOptions) {
 | 
					        for (const key in sortByOptions) {
 | 
				
			||||||
          if (sortByOptions[key].title === event.detail.title) {
 | 
					          if (sortByOptions[key].sortTitle === event.detail.sortTitle) {
 | 
				
			||||||
            sortByOptions[key].sortDesc = !sortByOptions[key].sortDesc;
 | 
					            sortByOptions[key].sortDesc = !sortByOptions[key].sortDesc;
 | 
				
			||||||
            $albumViewSettings.sortBy = sortByOptions[key].title;
 | 
					 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					        $albumViewSettings.sortBy = event.detail.sortTitle;
 | 
				
			||||||
      }}
 | 
					      }}
 | 
				
			||||||
    />
 | 
					    />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -307,7 +271,7 @@
 | 
				
			|||||||
            {#each Object.keys(sortByOptions) as key (key)}
 | 
					            {#each Object.keys(sortByOptions) as key (key)}
 | 
				
			||||||
              <TableHeader bind:albumViewSettings={$albumViewSettings.sortBy} bind:option={sortByOptions[key]} />
 | 
					              <TableHeader bind:albumViewSettings={$albumViewSettings.sortBy} bind:option={sortByOptions[key]} />
 | 
				
			||||||
            {/each}
 | 
					            {/each}
 | 
				
			||||||
            <th class="hidden text-center text-sm font-medium 2xl:block 2xl:w-[12%]">Action</th>
 | 
					            <th class="hidden w-2/12 text-center text-sm font-medium lg:block 2xl:w-1/12">Action</th>
 | 
				
			||||||
          </tr>
 | 
					          </tr>
 | 
				
			||||||
        </thead>
 | 
					        </thead>
 | 
				
			||||||
        <tbody
 | 
					        <tbody
 | 
				
			||||||
@ -320,34 +284,18 @@
 | 
				
			|||||||
              on:keydown={(event) => event.key === 'Enter' && goto(`albums/${album.id}`)}
 | 
					              on:keydown={(event) => event.key === 'Enter' && goto(`albums/${album.id}`)}
 | 
				
			||||||
              tabindex="0"
 | 
					              tabindex="0"
 | 
				
			||||||
            >
 | 
					            >
 | 
				
			||||||
              <td class="text-md text-ellipsis text-left w-8/12 sm:w-4/12 md:w-4/12 xl:w-[30%] 2xl:w-[40%]"
 | 
					              <td class="text-md w-8/12 text-ellipsis text-left sm:w-4/12 md:w-4/12 2xl:w-6/12">{album.albumName}</td>
 | 
				
			||||||
                >{album.albumName}</td
 | 
					              <td class="text-md w-4/12 text-ellipsis text-center sm:w-2/12 md:w-2/12 2xl:w-1/12">
 | 
				
			||||||
              >
 | 
					 | 
				
			||||||
              <td class="text-md text-ellipsis text-center sm:w-2/12 md:w-2/12 xl:w-[15%] 2xl:w-[12%]">
 | 
					 | 
				
			||||||
                {album.assetCount}
 | 
					                {album.assetCount}
 | 
				
			||||||
                {album.assetCount > 1 ? `items` : `item`}
 | 
					                {album.assetCount == 1 ? `item` : `items`}
 | 
				
			||||||
              </td>
 | 
					              </td>
 | 
				
			||||||
              <td class="text-md hidden text-ellipsis text-center sm:block w-3/12 xl:w-[15%] 2xl:w-[12%]"
 | 
					              <td class="text-md hidden w-3/12 text-ellipsis text-center sm:block lg:w-2/12"
 | 
				
			||||||
                >{dateLocaleString(album.updatedAt)}
 | 
					                >{dateLocaleString(album.updatedAt)}</td
 | 
				
			||||||
              </td>
 | 
					              >
 | 
				
			||||||
              <td class="text-md hidden text-ellipsis text-center sm:block w-3/12 xl:w-[15%] 2xl:w-[12%]"
 | 
					              <td class="text-md hidden w-3/12 text-ellipsis text-center sm:block lg:w-2/12"
 | 
				
			||||||
                >{dateLocaleString(album.createdAt)}</td
 | 
					                >{dateLocaleString(album.createdAt)}</td
 | 
				
			||||||
              >
 | 
					              >
 | 
				
			||||||
              <td class="text-md text-ellipsis text-center hidden xl:block xl:w-[15%] 2xl:w-[12%]">
 | 
					              <td class="text-md hidden w-2/12 text-ellipsis text-center lg:block 2xl:w-1/12">
 | 
				
			||||||
                {#if album.endDate}
 | 
					 | 
				
			||||||
                  {dateLocaleString(album.endDate)}
 | 
					 | 
				
			||||||
                {:else}
 | 
					 | 
				
			||||||
                  ❌
 | 
					 | 
				
			||||||
                {/if}</td
 | 
					 | 
				
			||||||
              >
 | 
					 | 
				
			||||||
              <td class="text-md text-ellipsis text-center hidden xl:block xl:w-[15%] 2xl:w-[12%]"
 | 
					 | 
				
			||||||
                >{#if album.startDate}
 | 
					 | 
				
			||||||
                  {dateLocaleString(album.startDate)}
 | 
					 | 
				
			||||||
                {:else}
 | 
					 | 
				
			||||||
                  ❌
 | 
					 | 
				
			||||||
                {/if}</td
 | 
					 | 
				
			||||||
              >
 | 
					 | 
				
			||||||
              <td class="text-md hidden text-ellipsis text-center 2xl:block xl:w-[15%] 2xl:w-[12%]">
 | 
					 | 
				
			||||||
                <button
 | 
					                <button
 | 
				
			||||||
                  on:click|stopPropagation={() => handleEdit(album)}
 | 
					                  on:click|stopPropagation={() => handleEdit(album)}
 | 
				
			||||||
                  class="rounded-full bg-immich-primary p-3 text-gray-100 transition-all duration-150 hover:bg-immich-primary/75 dark:bg-immich-dark-primary dark:text-gray-700"
 | 
					                  class="rounded-full bg-immich-primary p-3 text-gray-100 transition-all duration-150 hover:bg-immich-primary/75 dark:bg-immich-dark-primary dark:text-gray-700"
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user