feat: tags (#11980)

* feat: tags

* fix: folder tree icons

* navigate to tag from detail panel

* delete tag

* Tag position and add tag button

* Tag asset in detail panel

* refactor form

* feat: navigate to tag page from clicking on a tag

* feat: delete tags from the tag page

* refactor: moving tag section in detail panel and add + tag button

* feat: tag asset action in detail panel

* refactor add tag form

* fdisable add tag button when there is no selection

* feat: tag bulk endpoint

* feat: tag colors

* chore: clean up

* chore: unit tests

* feat: write tags to sidecar

* Remove tag and auto focus on tag creation form opened

* chore: regenerate migration

* chore: linting

* add color picker to tag edit form

* fix: force render tags timeline on navigating back from asset viewer

* feat: read tags from keywords

* chore: clean up

---------

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
This commit is contained in:
Jason Rasmussen
2024-08-29 12:14:03 -04:00
committed by GitHub
parent 682adaa334
commit d08a20bd57
68 changed files with 3032 additions and 814 deletions

View File

@@ -16,16 +16,63 @@ class TagsApi {
final ApiClient apiClient;
/// Performs an HTTP 'PUT /tags/assets' operation and returns the [Response].
/// Parameters:
///
/// * [TagBulkAssetsDto] tagBulkAssetsDto (required):
Future<Response> bulkTagAssetsWithHttpInfo(TagBulkAssetsDto tagBulkAssetsDto,) async {
// ignore: prefer_const_declarations
final path = r'/tags/assets';
// ignore: prefer_final_locals
Object? postBody = tagBulkAssetsDto;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>['application/json'];
return apiClient.invokeAPI(
path,
'PUT',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// Parameters:
///
/// * [TagBulkAssetsDto] tagBulkAssetsDto (required):
Future<TagBulkAssetsResponseDto?> bulkTagAssets(TagBulkAssetsDto tagBulkAssetsDto,) async {
final response = await bulkTagAssetsWithHttpInfo(tagBulkAssetsDto,);
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
// When a remote server returns no body with a status of 204, we shall not decode it.
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
// FormatException when trying to decode an empty string.
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'TagBulkAssetsResponseDto',) as TagBulkAssetsResponseDto;
}
return null;
}
/// Performs an HTTP 'POST /tags' operation and returns the [Response].
/// Parameters:
///
/// * [CreateTagDto] createTagDto (required):
Future<Response> createTagWithHttpInfo(CreateTagDto createTagDto,) async {
/// * [TagCreateDto] tagCreateDto (required):
Future<Response> createTagWithHttpInfo(TagCreateDto tagCreateDto,) async {
// ignore: prefer_const_declarations
final path = r'/tags';
// ignore: prefer_final_locals
Object? postBody = createTagDto;
Object? postBody = tagCreateDto;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
@@ -47,9 +94,9 @@ class TagsApi {
/// Parameters:
///
/// * [CreateTagDto] createTagDto (required):
Future<TagResponseDto?> createTag(CreateTagDto createTagDto,) async {
final response = await createTagWithHttpInfo(createTagDto,);
/// * [TagCreateDto] tagCreateDto (required):
Future<TagResponseDto?> createTag(TagCreateDto tagCreateDto,) async {
final response = await createTagWithHttpInfo(tagCreateDto,);
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
@@ -147,57 +194,6 @@ class TagsApi {
return null;
}
/// Performs an HTTP 'GET /tags/{id}/assets' operation and returns the [Response].
/// Parameters:
///
/// * [String] id (required):
Future<Response> getTagAssetsWithHttpInfo(String id,) async {
// ignore: prefer_const_declarations
final path = r'/tags/{id}/assets'
.replaceAll('{id}', id);
// ignore: prefer_final_locals
Object? postBody;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>[];
return apiClient.invokeAPI(
path,
'GET',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// Parameters:
///
/// * [String] id (required):
Future<List<AssetResponseDto>?> getTagAssets(String id,) async {
final response = await getTagAssetsWithHttpInfo(id,);
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
// When a remote server returns no body with a status of 204, we shall not decode it.
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
// FormatException when trying to decode an empty string.
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
final responseBody = await _decodeBodyBytes(response);
return (await apiClient.deserializeAsync(responseBody, 'List<AssetResponseDto>') as List)
.cast<AssetResponseDto>()
.toList(growable: false);
}
return null;
}
/// Performs an HTTP 'GET /tags/{id}' operation and returns the [Response].
/// Parameters:
///
@@ -251,14 +247,14 @@ class TagsApi {
///
/// * [String] id (required):
///
/// * [AssetIdsDto] assetIdsDto (required):
Future<Response> tagAssetsWithHttpInfo(String id, AssetIdsDto assetIdsDto,) async {
/// * [BulkIdsDto] bulkIdsDto (required):
Future<Response> tagAssetsWithHttpInfo(String id, BulkIdsDto bulkIdsDto,) async {
// ignore: prefer_const_declarations
final path = r'/tags/{id}/assets'
.replaceAll('{id}', id);
// ignore: prefer_final_locals
Object? postBody = assetIdsDto;
Object? postBody = bulkIdsDto;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
@@ -282,9 +278,9 @@ class TagsApi {
///
/// * [String] id (required):
///
/// * [AssetIdsDto] assetIdsDto (required):
Future<List<AssetIdsResponseDto>?> tagAssets(String id, AssetIdsDto assetIdsDto,) async {
final response = await tagAssetsWithHttpInfo(id, assetIdsDto,);
/// * [BulkIdsDto] bulkIdsDto (required):
Future<List<BulkIdResponseDto>?> tagAssets(String id, BulkIdsDto bulkIdsDto,) async {
final response = await tagAssetsWithHttpInfo(id, bulkIdsDto,);
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
@@ -293,8 +289,8 @@ class TagsApi {
// FormatException when trying to decode an empty string.
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
final responseBody = await _decodeBodyBytes(response);
return (await apiClient.deserializeAsync(responseBody, 'List<AssetIdsResponseDto>') as List)
.cast<AssetIdsResponseDto>()
return (await apiClient.deserializeAsync(responseBody, 'List<BulkIdResponseDto>') as List)
.cast<BulkIdResponseDto>()
.toList(growable: false);
}
@@ -306,14 +302,14 @@ class TagsApi {
///
/// * [String] id (required):
///
/// * [AssetIdsDto] assetIdsDto (required):
Future<Response> untagAssetsWithHttpInfo(String id, AssetIdsDto assetIdsDto,) async {
/// * [BulkIdsDto] bulkIdsDto (required):
Future<Response> untagAssetsWithHttpInfo(String id, BulkIdsDto bulkIdsDto,) async {
// ignore: prefer_const_declarations
final path = r'/tags/{id}/assets'
.replaceAll('{id}', id);
// ignore: prefer_final_locals
Object? postBody = assetIdsDto;
Object? postBody = bulkIdsDto;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
@@ -337,9 +333,9 @@ class TagsApi {
///
/// * [String] id (required):
///
/// * [AssetIdsDto] assetIdsDto (required):
Future<List<AssetIdsResponseDto>?> untagAssets(String id, AssetIdsDto assetIdsDto,) async {
final response = await untagAssetsWithHttpInfo(id, assetIdsDto,);
/// * [BulkIdsDto] bulkIdsDto (required):
Future<List<BulkIdResponseDto>?> untagAssets(String id, BulkIdsDto bulkIdsDto,) async {
final response = await untagAssetsWithHttpInfo(id, bulkIdsDto,);
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
@@ -348,27 +344,27 @@ class TagsApi {
// FormatException when trying to decode an empty string.
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
final responseBody = await _decodeBodyBytes(response);
return (await apiClient.deserializeAsync(responseBody, 'List<AssetIdsResponseDto>') as List)
.cast<AssetIdsResponseDto>()
return (await apiClient.deserializeAsync(responseBody, 'List<BulkIdResponseDto>') as List)
.cast<BulkIdResponseDto>()
.toList(growable: false);
}
return null;
}
/// Performs an HTTP 'PATCH /tags/{id}' operation and returns the [Response].
/// Performs an HTTP 'PUT /tags/{id}' operation and returns the [Response].
/// Parameters:
///
/// * [String] id (required):
///
/// * [UpdateTagDto] updateTagDto (required):
Future<Response> updateTagWithHttpInfo(String id, UpdateTagDto updateTagDto,) async {
/// * [TagUpdateDto] tagUpdateDto (required):
Future<Response> updateTagWithHttpInfo(String id, TagUpdateDto tagUpdateDto,) async {
// ignore: prefer_const_declarations
final path = r'/tags/{id}'
.replaceAll('{id}', id);
// ignore: prefer_final_locals
Object? postBody = updateTagDto;
Object? postBody = tagUpdateDto;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
@@ -379,7 +375,7 @@ class TagsApi {
return apiClient.invokeAPI(
path,
'PATCH',
'PUT',
queryParams,
postBody,
headerParams,
@@ -392,9 +388,9 @@ class TagsApi {
///
/// * [String] id (required):
///
/// * [UpdateTagDto] updateTagDto (required):
Future<TagResponseDto?> updateTag(String id, UpdateTagDto updateTagDto,) async {
final response = await updateTagWithHttpInfo(id, updateTagDto,);
/// * [TagUpdateDto] tagUpdateDto (required):
Future<TagResponseDto?> updateTag(String id, TagUpdateDto tagUpdateDto,) async {
final response = await updateTagWithHttpInfo(id, tagUpdateDto,);
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
@@ -407,4 +403,54 @@ class TagsApi {
}
return null;
}
/// Performs an HTTP 'PUT /tags' operation and returns the [Response].
/// Parameters:
///
/// * [TagUpsertDto] tagUpsertDto (required):
Future<Response> upsertTagsWithHttpInfo(TagUpsertDto tagUpsertDto,) async {
// ignore: prefer_const_declarations
final path = r'/tags';
// ignore: prefer_final_locals
Object? postBody = tagUpsertDto;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>['application/json'];
return apiClient.invokeAPI(
path,
'PUT',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// Parameters:
///
/// * [TagUpsertDto] tagUpsertDto (required):
Future<List<TagResponseDto>?> upsertTags(TagUpsertDto tagUpsertDto,) async {
final response = await upsertTagsWithHttpInfo(tagUpsertDto,);
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
// When a remote server returns no body with a status of 204, we shall not decode it.
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
// FormatException when trying to decode an empty string.
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
final responseBody = await _decodeBodyBytes(response);
return (await apiClient.deserializeAsync(responseBody, 'List<TagResponseDto>') as List)
.cast<TagResponseDto>()
.toList(growable: false);
}
return null;
}
}

View File

@@ -37,12 +37,14 @@ class TimelineApi {
///
/// * [String] personId:
///
/// * [String] tagId:
///
/// * [String] userId:
///
/// * [bool] withPartners:
///
/// * [bool] withStacked:
Future<Response> getTimeBucketWithHttpInfo(TimeBucketSize size, String timeBucket, { String? albumId, bool? isArchived, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? userId, bool? withPartners, bool? withStacked, }) async {
Future<Response> getTimeBucketWithHttpInfo(TimeBucketSize size, String timeBucket, { String? albumId, bool? isArchived, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? tagId, String? userId, bool? withPartners, bool? withStacked, }) async {
// ignore: prefer_const_declarations
final path = r'/timeline/bucket';
@@ -75,6 +77,9 @@ class TimelineApi {
queryParams.addAll(_queryParams('', 'personId', personId));
}
queryParams.addAll(_queryParams('', 'size', size));
if (tagId != null) {
queryParams.addAll(_queryParams('', 'tagId', tagId));
}
queryParams.addAll(_queryParams('', 'timeBucket', timeBucket));
if (userId != null) {
queryParams.addAll(_queryParams('', 'userId', userId));
@@ -120,13 +125,15 @@ class TimelineApi {
///
/// * [String] personId:
///
/// * [String] tagId:
///
/// * [String] userId:
///
/// * [bool] withPartners:
///
/// * [bool] withStacked:
Future<List<AssetResponseDto>?> getTimeBucket(TimeBucketSize size, String timeBucket, { String? albumId, bool? isArchived, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? userId, bool? withPartners, bool? withStacked, }) async {
final response = await getTimeBucketWithHttpInfo(size, timeBucket, albumId: albumId, isArchived: isArchived, isFavorite: isFavorite, isTrashed: isTrashed, key: key, order: order, personId: personId, userId: userId, withPartners: withPartners, withStacked: withStacked, );
Future<List<AssetResponseDto>?> getTimeBucket(TimeBucketSize size, String timeBucket, { String? albumId, bool? isArchived, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? tagId, String? userId, bool? withPartners, bool? withStacked, }) async {
final response = await getTimeBucketWithHttpInfo(size, timeBucket, albumId: albumId, isArchived: isArchived, isFavorite: isFavorite, isTrashed: isTrashed, key: key, order: order, personId: personId, tagId: tagId, userId: userId, withPartners: withPartners, withStacked: withStacked, );
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
@@ -162,12 +169,14 @@ class TimelineApi {
///
/// * [String] personId:
///
/// * [String] tagId:
///
/// * [String] userId:
///
/// * [bool] withPartners:
///
/// * [bool] withStacked:
Future<Response> getTimeBucketsWithHttpInfo(TimeBucketSize size, { String? albumId, bool? isArchived, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? userId, bool? withPartners, bool? withStacked, }) async {
Future<Response> getTimeBucketsWithHttpInfo(TimeBucketSize size, { String? albumId, bool? isArchived, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? tagId, String? userId, bool? withPartners, bool? withStacked, }) async {
// ignore: prefer_const_declarations
final path = r'/timeline/buckets';
@@ -200,6 +209,9 @@ class TimelineApi {
queryParams.addAll(_queryParams('', 'personId', personId));
}
queryParams.addAll(_queryParams('', 'size', size));
if (tagId != null) {
queryParams.addAll(_queryParams('', 'tagId', tagId));
}
if (userId != null) {
queryParams.addAll(_queryParams('', 'userId', userId));
}
@@ -242,13 +254,15 @@ class TimelineApi {
///
/// * [String] personId:
///
/// * [String] tagId:
///
/// * [String] userId:
///
/// * [bool] withPartners:
///
/// * [bool] withStacked:
Future<List<TimeBucketResponseDto>?> getTimeBuckets(TimeBucketSize size, { String? albumId, bool? isArchived, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? userId, bool? withPartners, bool? withStacked, }) async {
final response = await getTimeBucketsWithHttpInfo(size, albumId: albumId, isArchived: isArchived, isFavorite: isFavorite, isTrashed: isTrashed, key: key, order: order, personId: personId, userId: userId, withPartners: withPartners, withStacked: withStacked, );
Future<List<TimeBucketResponseDto>?> getTimeBuckets(TimeBucketSize size, { String? albumId, bool? isArchived, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? tagId, String? userId, bool? withPartners, bool? withStacked, }) async {
final response = await getTimeBucketsWithHttpInfo(size, albumId: albumId, isArchived: isArchived, isFavorite: isFavorite, isTrashed: isTrashed, key: key, order: order, personId: personId, tagId: tagId, userId: userId, withPartners: withPartners, withStacked: withStacked, );
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}