1
0
Fork 0
mirror of https://github.com/immich-app/immich.git synced 2025-03-01 15:11:21 +01: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:
Krisjanis Lejejs 2023-06-21 04:00:59 +03:00 committed by GitHub
parent 3c5fefde2e
commit 746ca5d5ed
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 105 additions and 14 deletions

View file

@ -19,6 +19,7 @@ Name | Type | Description | Notes
**sharedUsers** | [**List<UserResponseDto>**](UserResponseDto.md) | | [default to const []] **sharedUsers** | [**List<UserResponseDto>**](UserResponseDto.md) | | [default to const []]
**assets** | [**List<AssetResponseDto>**](AssetResponseDto.md) | | [default to const []] **assets** | [**List<AssetResponseDto>**](AssetResponseDto.md) | | [default to const []]
**owner** | [**UserResponseDto**](UserResponseDto.md) | | **owner** | [**UserResponseDto**](UserResponseDto.md) | |
**lastModifiedAssetTimestamp** | [**DateTime**](DateTime.md) | | [optional]
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)

View file

@ -24,6 +24,7 @@ class AlbumResponseDto {
this.sharedUsers = const [], this.sharedUsers = const [],
this.assets = const [], this.assets = const [],
required this.owner, required this.owner,
this.lastModifiedAssetTimestamp,
}); });
int assetCount; int assetCount;
@ -48,6 +49,14 @@ class AlbumResponseDto {
UserResponseDto owner; UserResponseDto owner;
///
/// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated
/// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note.
///
DateTime? lastModifiedAssetTimestamp;
@override @override
bool operator ==(Object other) => identical(this, other) || other is AlbumResponseDto && bool operator ==(Object other) => identical(this, other) || other is AlbumResponseDto &&
other.assetCount == assetCount && other.assetCount == assetCount &&
@ -60,7 +69,8 @@ class AlbumResponseDto {
other.shared == shared && other.shared == shared &&
other.sharedUsers == sharedUsers && other.sharedUsers == sharedUsers &&
other.assets == assets && other.assets == assets &&
other.owner == owner; other.owner == owner &&
other.lastModifiedAssetTimestamp == lastModifiedAssetTimestamp;
@override @override
int get hashCode => int get hashCode =>
@ -75,10 +85,11 @@ class AlbumResponseDto {
(shared.hashCode) + (shared.hashCode) +
(sharedUsers.hashCode) + (sharedUsers.hashCode) +
(assets.hashCode) + (assets.hashCode) +
(owner.hashCode); (owner.hashCode) +
(lastModifiedAssetTimestamp == null ? 0 : lastModifiedAssetTimestamp!.hashCode);
@override @override
String toString() => 'AlbumResponseDto[assetCount=$assetCount, id=$id, ownerId=$ownerId, albumName=$albumName, createdAt=$createdAt, updatedAt=$updatedAt, albumThumbnailAssetId=$albumThumbnailAssetId, shared=$shared, sharedUsers=$sharedUsers, assets=$assets, owner=$owner]'; String toString() => 'AlbumResponseDto[assetCount=$assetCount, id=$id, ownerId=$ownerId, albumName=$albumName, createdAt=$createdAt, updatedAt=$updatedAt, albumThumbnailAssetId=$albumThumbnailAssetId, shared=$shared, sharedUsers=$sharedUsers, assets=$assets, owner=$owner, lastModifiedAssetTimestamp=$lastModifiedAssetTimestamp]';
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final json = <String, dynamic>{}; final json = <String, dynamic>{};
@ -97,6 +108,11 @@ class AlbumResponseDto {
json[r'sharedUsers'] = this.sharedUsers; json[r'sharedUsers'] = this.sharedUsers;
json[r'assets'] = this.assets; json[r'assets'] = this.assets;
json[r'owner'] = this.owner; json[r'owner'] = this.owner;
if (this.lastModifiedAssetTimestamp != null) {
json[r'lastModifiedAssetTimestamp'] = this.lastModifiedAssetTimestamp!.toUtc().toIso8601String();
} else {
// json[r'lastModifiedAssetTimestamp'] = null;
}
return json; return json;
} }
@ -130,6 +146,7 @@ class AlbumResponseDto {
sharedUsers: UserResponseDto.listFromJson(json[r'sharedUsers']), sharedUsers: UserResponseDto.listFromJson(json[r'sharedUsers']),
assets: AssetResponseDto.listFromJson(json[r'assets']), assets: AssetResponseDto.listFromJson(json[r'assets']),
owner: UserResponseDto.fromJson(json[r'owner'])!, owner: UserResponseDto.fromJson(json[r'owner'])!,
lastModifiedAssetTimestamp: mapDateTime(json, r'lastModifiedAssetTimestamp', ''),
); );
} }
return null; return null;

View file

@ -71,6 +71,11 @@ void main() {
// TODO // TODO
}); });
// DateTime lastModifiedAssetTimestamp
test('to test the property `lastModifiedAssetTimestamp`', () async {
// TODO
});
}); });

View file

@ -4584,6 +4584,10 @@
}, },
"owner": { "owner": {
"$ref": "#/components/schemas/UserResponseDto" "$ref": "#/components/schemas/UserResponseDto"
},
"lastModifiedAssetTimestamp": {
"format": "date-time",
"type": "string"
} }
}, },
"required": [ "required": [

View file

@ -16,6 +16,7 @@ export class AlbumResponseDto {
owner!: UserResponseDto; owner!: UserResponseDto;
@ApiProperty({ type: 'integer' }) @ApiProperty({ type: 'integer' })
assetCount!: number; assetCount!: number;
lastModifiedAssetTimestamp?: Date;
} }
export function mapAlbum(entity: AlbumEntity): AlbumResponseDto { export function mapAlbum(entity: AlbumEntity): AlbumResponseDto {

View file

@ -53,15 +53,19 @@ export class AlbumService {
return obj; return obj;
}, {}); }, {});
return albums.map((album) => { return Promise.all(
return { albums.map(async (album) => {
...album, const lastModifiedAsset = await this.assetRepository.getLastUpdatedAssetForAlbumId(album.id);
assets: album?.assets?.map(mapAsset), return {
sharedLinks: undefined, // Don't return shared links ...album,
shared: album.sharedLinks?.length > 0 || album.sharedUsers?.length > 0, assets: album?.assets?.map(mapAsset),
assetCount: albumsAssetCountObj[album.id], sharedLinks: undefined, // Don't return shared links
} as AlbumResponseDto; shared: album.sharedLinks?.length > 0 || album.sharedUsers?.length > 0,
}); assetCount: albumsAssetCountObj[album.id],
lastModifiedAssetTimestamp: lastModifiedAsset?.fileModifiedAt,
} as AlbumResponseDto;
}),
);
} }
private async updateInvalidThumbnails(): Promise<number> { private async updateInvalidThumbnails(): Promise<number> {

View file

@ -47,6 +47,7 @@ export interface IAssetRepository {
getWithout(pagination: PaginationOptions, property: WithoutProperty): Paginated<AssetEntity>; getWithout(pagination: PaginationOptions, property: WithoutProperty): Paginated<AssetEntity>;
getWith(pagination: PaginationOptions, property: WithProperty): Paginated<AssetEntity>; getWith(pagination: PaginationOptions, property: WithProperty): Paginated<AssetEntity>;
getFirstAssetForAlbumId(albumId: string): Promise<AssetEntity | null>; getFirstAssetForAlbumId(albumId: string): Promise<AssetEntity | null>;
getLastUpdatedAssetForAlbumId(albumId: string): Promise<AssetEntity | null>;
deleteAll(ownerId: string): Promise<void>; deleteAll(ownerId: string): Promise<void>;
getAll(pagination: PaginationOptions, options?: AssetSearchOptions): Paginated<AssetEntity>; getAll(pagination: PaginationOptions, options?: AssetSearchOptions): Paginated<AssetEntity>;
save(asset: Partial<AssetEntity>): Promise<AssetEntity>; save(asset: Partial<AssetEntity>): Promise<AssetEntity>;

View file

@ -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[]> { async getMapMarkers(ownerId: string, options: MapMarkerSearchOptions = {}): Promise<MapMarker[]> {
const { isFavorite, fileCreatedAfter, fileCreatedBefore } = options; const { isFavorite, fileCreatedAfter, fileCreatedBefore } = options;

View file

@ -7,6 +7,7 @@ export const newAssetRepositoryMock = (): jest.Mocked<IAssetRepository> => {
getWithout: jest.fn(), getWithout: jest.fn(),
getWith: jest.fn(), getWith: jest.fn(),
getFirstAssetForAlbumId: jest.fn(), getFirstAssetForAlbumId: jest.fn(),
getLastUpdatedAssetForAlbumId: jest.fn(),
getAll: jest.fn().mockResolvedValue({ getAll: jest.fn().mockResolvedValue({
items: [], items: [],
hasNextPage: false, hasNextPage: false,

View file

@ -284,6 +284,12 @@ export interface AlbumResponseDto {
* @memberof AlbumResponseDto * @memberof AlbumResponseDto
*/ */
'owner': UserResponseDto; 'owner': UserResponseDto;
/**
*
* @type {string}
* @memberof AlbumResponseDto
*/
'lastModifiedAssetTimestamp'?: string;
} }
/** /**
* *

View file

@ -15,8 +15,17 @@
export let data: PageData; 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 { const {
albums, albums: unsortedAlbums,
isShowContextMenu, isShowContextMenu,
contextMenuPosition, contextMenuPosition,
createAlbum, createAlbum,
@ -26,6 +35,28 @@
closeAlbumContextMenu closeAlbumContextMenu
} = useAlbums({ albums: data.albums }); } = 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 handleCreateAlbum = async () => {
const newAlbum = await createAlbum(); const newAlbum = await createAlbum();
if (newAlbum) { if (newAlbum) {
@ -52,7 +83,20 @@
</script> </script>
<UserPageLayout user={data.user} title={data.meta.title}> <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}> <LinkButton on:click={handleCreateAlbum}>
<div class="flex place-items-center gap-2 text-sm"> <div class="flex place-items-center gap-2 text-sm">
<PlusBoxOutline size="18" /> <PlusBoxOutline size="18" />