mirror of
https://github.com/immich-app/immich.git
synced 2024-12-29 15:11:58 +00:00
feat(web): Add album sorting to albums view (#2861)
* Add album sorting to web albums view * generate api --------- Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
This commit is contained in:
parent
3c5fefde2e
commit
746ca5d5ed
11 changed files with 79 additions and 11 deletions
BIN
mobile/openapi/doc/AlbumResponseDto.md
generated
BIN
mobile/openapi/doc/AlbumResponseDto.md
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/test/album_response_dto_test.dart
generated
BIN
mobile/openapi/test/album_response_dto_test.dart
generated
Binary file not shown.
|
@ -4584,6 +4584,10 @@
|
|||
},
|
||||
"owner": {
|
||||
"$ref": "#/components/schemas/UserResponseDto"
|
||||
},
|
||||
"lastModifiedAssetTimestamp": {
|
||||
"format": "date-time",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
|
|
|
@ -16,6 +16,7 @@ export class AlbumResponseDto {
|
|||
owner!: UserResponseDto;
|
||||
@ApiProperty({ type: 'integer' })
|
||||
assetCount!: number;
|
||||
lastModifiedAssetTimestamp?: Date;
|
||||
}
|
||||
|
||||
export function mapAlbum(entity: AlbumEntity): AlbumResponseDto {
|
||||
|
|
|
@ -53,15 +53,19 @@ export class AlbumService {
|
|||
return obj;
|
||||
}, {});
|
||||
|
||||
return albums.map((album) => {
|
||||
return {
|
||||
...album,
|
||||
assets: album?.assets?.map(mapAsset),
|
||||
sharedLinks: undefined, // Don't return shared links
|
||||
shared: album.sharedLinks?.length > 0 || album.sharedUsers?.length > 0,
|
||||
assetCount: albumsAssetCountObj[album.id],
|
||||
} as AlbumResponseDto;
|
||||
});
|
||||
return Promise.all(
|
||||
albums.map(async (album) => {
|
||||
const lastModifiedAsset = await this.assetRepository.getLastUpdatedAssetForAlbumId(album.id);
|
||||
return {
|
||||
...album,
|
||||
assets: album?.assets?.map(mapAsset),
|
||||
sharedLinks: undefined, // Don't return shared links
|
||||
shared: album.sharedLinks?.length > 0 || album.sharedUsers?.length > 0,
|
||||
assetCount: albumsAssetCountObj[album.id],
|
||||
lastModifiedAssetTimestamp: lastModifiedAsset?.fileModifiedAt,
|
||||
} as AlbumResponseDto;
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
private async updateInvalidThumbnails(): Promise<number> {
|
||||
|
|
|
@ -47,6 +47,7 @@ export interface IAssetRepository {
|
|||
getWithout(pagination: PaginationOptions, property: WithoutProperty): Paginated<AssetEntity>;
|
||||
getWith(pagination: PaginationOptions, property: WithProperty): Paginated<AssetEntity>;
|
||||
getFirstAssetForAlbumId(albumId: string): Promise<AssetEntity | null>;
|
||||
getLastUpdatedAssetForAlbumId(albumId: string): Promise<AssetEntity | null>;
|
||||
deleteAll(ownerId: string): Promise<void>;
|
||||
getAll(pagination: PaginationOptions, options?: AssetSearchOptions): Paginated<AssetEntity>;
|
||||
save(asset: Partial<AssetEntity>): Promise<AssetEntity>;
|
||||
|
|
|
@ -248,6 +248,13 @@ export class AssetRepository implements IAssetRepository {
|
|||
});
|
||||
}
|
||||
|
||||
getLastUpdatedAssetForAlbumId(albumId: string): Promise<AssetEntity | null> {
|
||||
return this.repository.findOne({
|
||||
where: { albums: { id: albumId } },
|
||||
order: { updatedAt: 'DESC' },
|
||||
});
|
||||
}
|
||||
|
||||
async getMapMarkers(ownerId: string, options: MapMarkerSearchOptions = {}): Promise<MapMarker[]> {
|
||||
const { isFavorite, fileCreatedAfter, fileCreatedBefore } = options;
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@ export const newAssetRepositoryMock = (): jest.Mocked<IAssetRepository> => {
|
|||
getWithout: jest.fn(),
|
||||
getWith: jest.fn(),
|
||||
getFirstAssetForAlbumId: jest.fn(),
|
||||
getLastUpdatedAssetForAlbumId: jest.fn(),
|
||||
getAll: jest.fn().mockResolvedValue({
|
||||
items: [],
|
||||
hasNextPage: false,
|
||||
|
|
6
web/src/api/open-api/api.ts
generated
6
web/src/api/open-api/api.ts
generated
|
@ -284,6 +284,12 @@ export interface AlbumResponseDto {
|
|||
* @memberof AlbumResponseDto
|
||||
*/
|
||||
'owner': UserResponseDto;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof AlbumResponseDto
|
||||
*/
|
||||
'lastModifiedAssetTimestamp'?: string;
|
||||
}
|
||||
/**
|
||||
*
|
||||
|
|
|
@ -15,8 +15,17 @@
|
|||
|
||||
export let data: PageData;
|
||||
|
||||
const sortByOptions = ['Most recent photo', 'Last modified', 'Album title'];
|
||||
|
||||
let selectedSortBy = sortByOptions[0];
|
||||
|
||||
const handleChangeSortBy = (e: Event) => {
|
||||
const target = e.target as HTMLSelectElement;
|
||||
selectedSortBy = target.value;
|
||||
};
|
||||
|
||||
const {
|
||||
albums,
|
||||
albums: unsortedAlbums,
|
||||
isShowContextMenu,
|
||||
contextMenuPosition,
|
||||
createAlbum,
|
||||
|
@ -26,6 +35,28 @@
|
|||
closeAlbumContextMenu
|
||||
} = useAlbums({ albums: data.albums });
|
||||
|
||||
let albums = unsortedAlbums;
|
||||
|
||||
const sortByDate = (a: string, b: string) => {
|
||||
const aDate = new Date(a);
|
||||
const bDate = new Date(b);
|
||||
return bDate.getTime() - aDate.getTime();
|
||||
};
|
||||
|
||||
$: {
|
||||
if (selectedSortBy === 'Most recent photo') {
|
||||
$albums = $unsortedAlbums.sort((a, b) =>
|
||||
a.lastModifiedAssetTimestamp && b.lastModifiedAssetTimestamp
|
||||
? sortByDate(a.lastModifiedAssetTimestamp, b.lastModifiedAssetTimestamp)
|
||||
: sortByDate(a.updatedAt, b.updatedAt)
|
||||
);
|
||||
} else if (selectedSortBy === 'Last modified') {
|
||||
$albums = $unsortedAlbums.sort((a, b) => sortByDate(a.updatedAt, b.updatedAt));
|
||||
} else if (selectedSortBy === 'Album title') {
|
||||
$albums = $unsortedAlbums.sort((a, b) => a.albumName.localeCompare(b.albumName));
|
||||
}
|
||||
}
|
||||
|
||||
const handleCreateAlbum = async () => {
|
||||
const newAlbum = await createAlbum();
|
||||
if (newAlbum) {
|
||||
|
@ -52,7 +83,20 @@
|
|||
</script>
|
||||
|
||||
<UserPageLayout user={data.user} title={data.meta.title}>
|
||||
<div slot="buttons">
|
||||
<div class="flex place-items-center gap-2" slot="buttons">
|
||||
<label class="text-xs" for="sortBy">Sort by:</label>
|
||||
<select
|
||||
class="text-sm bg-slate-200 p-2 rounded-lg dark:bg-gray-600 hover:cursor-pointer"
|
||||
name="sortBy"
|
||||
id="sortBy-select"
|
||||
bind:value={selectedSortBy}
|
||||
on:change={handleChangeSortBy}
|
||||
>
|
||||
{#each sortByOptions as option}
|
||||
<option value={option}>{option}</option>
|
||||
{/each}
|
||||
</select>
|
||||
|
||||
<LinkButton on:click={handleCreateAlbum}>
|
||||
<div class="flex place-items-center gap-2 text-sm">
|
||||
<PlusBoxOutline size="18" />
|
||||
|
|
Loading…
Reference in a new issue