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

@@ -0,0 +1,47 @@
<script lang="ts">
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
import { tagAssets } from '$lib/utils/asset-utils';
import { mdiTagMultipleOutline, mdiTimerSand } from '@mdi/js';
import { t } from 'svelte-i18n';
import MenuOption from '../../shared-components/context-menu/menu-option.svelte';
import { getAssetControlContext } from '../asset-select-control-bar.svelte';
import TagAssetForm from '$lib/components/forms/tag-asset-form.svelte';
export let menuItem = false;
const text = $t('tag');
const icon = mdiTagMultipleOutline;
let loading = false;
let isOpen = false;
const { clearSelect, getOwnedAssets } = getAssetControlContext();
const handleOpen = () => (isOpen = true);
const handleCancel = () => (isOpen = false);
const handleTag = async (tagIds: string[]) => {
const assets = [...getOwnedAssets()];
loading = true;
const ids = await tagAssets({ tagIds, assetIds: assets.map((asset) => asset.id) });
if (ids) {
clearSelect();
}
loading = false;
};
</script>
{#if menuItem}
<MenuOption {text} {icon} onClick={handleOpen} />
{/if}
{#if !menuItem}
{#if loading}
<CircleIconButton title={$t('loading')} icon={mdiTimerSand} />
{:else}
<CircleIconButton title={text} {icon} on:click={handleOpen} />
{/if}
{/if}
{#if isOpen}
<TagAssetForm onTag={(tagIds) => handleTag(tagIds)} onCancel={handleCancel} />
{/if}

View File

@@ -109,7 +109,7 @@
);
},
onSeparate: () => {
$assetStore.taskManager.seperatedDateGroup(componentId, dateGroup, () =>
$assetStore.taskManager.separatedDateGroup(componentId, dateGroup, () =>
assetStore.updateBucketDateGroup(bucket, dateGroup, { intersecting: false }),
);
},
@@ -186,9 +186,9 @@
<div
use:intersectionObserver={{
onIntersect: () => onAssetInGrid?.(asset),
top: `-${TITLE_HEIGHT}px`,
bottom: `-${viewport.height - TITLE_HEIGHT - 1}px`,
right: `-${viewport.width - 1}px`,
top: `${-TITLE_HEIGHT}px`,
bottom: `${-(viewport.height - TITLE_HEIGHT - 1)}px`,
right: `${-(viewport.width - 1)}px`,
root: assetGridElement,
}}
data-asset-id={asset.id}

View File

@@ -498,21 +498,21 @@
}
};
function intersectedHandler(bucket: AssetBucket) {
function handleIntersect(bucket: AssetBucket) {
updateLastIntersectedBucketDate();
const intersectedTask = () => {
const task = () => {
$assetStore.updateBucket(bucket.bucketDate, { intersecting: true });
void $assetStore.loadBucket(bucket.bucketDate);
};
$assetStore.taskManager.intersectedBucket(componentId, bucket, intersectedTask);
$assetStore.taskManager.intersectedBucket(componentId, bucket, task);
}
function seperatedHandler(bucket: AssetBucket) {
const seperatedTask = () => {
function handleSeparate(bucket: AssetBucket) {
const task = () => {
$assetStore.updateBucket(bucket.bucketDate, { intersecting: false });
bucket.cancel();
};
$assetStore.taskManager.seperatedBucket(componentId, bucket, seperatedTask);
$assetStore.taskManager.separatedBucket(componentId, bucket, task);
}
const handlePrevious = async () => {
@@ -809,8 +809,8 @@
<div
id="bucket"
use:intersectionObserver={{
onIntersect: () => intersectedHandler(bucket),
onSeparate: () => seperatedHandler(bucket),
onIntersect: () => handleIntersect(bucket),
onSeparate: () => handleSeparate(bucket),
top: BUCKET_INTERSECTION_ROOT_TOP,
bottom: BUCKET_INTERSECTION_ROOT_BOTTOM,
root: element,