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:
parent
3c5fefde2e
commit
746ca5d5ed
11 changed files with 105 additions and 14 deletions
1
mobile/openapi/doc/AlbumResponseDto.md
generated
1
mobile/openapi/doc/AlbumResponseDto.md
generated
|
@ -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)
|
||||||
|
|
||||||
|
|
23
mobile/openapi/lib/model/album_response_dto.dart
generated
23
mobile/openapi/lib/model/album_response_dto.dart
generated
|
@ -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;
|
||||||
|
|
5
mobile/openapi/test/album_response_dto_test.dart
generated
5
mobile/openapi/test/album_response_dto_test.dart
generated
|
@ -71,6 +71,11 @@ void main() {
|
||||||
// TODO
|
// TODO
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// DateTime lastModifiedAssetTimestamp
|
||||||
|
test('to test the property `lastModifiedAssetTimestamp`', () async {
|
||||||
|
// TODO
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -4584,6 +4584,10 @@
|
||||||
},
|
},
|
||||||
"owner": {
|
"owner": {
|
||||||
"$ref": "#/components/schemas/UserResponseDto"
|
"$ref": "#/components/schemas/UserResponseDto"
|
||||||
|
},
|
||||||
|
"lastModifiedAssetTimestamp": {
|
||||||
|
"format": "date-time",
|
||||||
|
"type": "string"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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> {
|
||||||
|
|
|
@ -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>;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
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
|
* @memberof AlbumResponseDto
|
||||||
*/
|
*/
|
||||||
'owner': UserResponseDto;
|
'owner': UserResponseDto;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {string}
|
||||||
|
* @memberof AlbumResponseDto
|
||||||
|
*/
|
||||||
|
'lastModifiedAssetTimestamp'?: string;
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
|
|
|
@ -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" />
|
||||||
|
|
Loading…
Add table
Reference in a new issue