Skip to content

Commit

Permalink
chore: clean up
Browse files Browse the repository at this point in the history
  • Loading branch information
jrasm91 committed Aug 29, 2024
1 parent 8bba334 commit 03480e8
Show file tree
Hide file tree
Showing 13 changed files with 169 additions and 74 deletions.
91 changes: 76 additions & 15 deletions e2e/src/api/specs/tag.e2e-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ describe('/tags', () => {
});

beforeEach(async () => {
// tagging assets eventually triggers metadata extraction which can impact other tests
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
await utils.resetDatabase(['tags']);
});

Expand All @@ -44,7 +46,7 @@ describe('/tags', () => {
expect(body).toEqual(errorDto.unauthorized);
});

it('should not work without permission', async () => {
it('should require authorization (api key)', async () => {
const { secret } = await utils.createApiKey(user.accessToken, [Permission.AssetRead]);
const { status, body } = await request(app).post('/tags').set('x-api-key', secret).send({ name: 'TagA' });
expect(status).toBe(403);
Expand Down Expand Up @@ -104,6 +106,13 @@ describe('/tags', () => {
expect(body).toEqual(errorDto.unauthorized);
});

it('should require authorization (api key)', async () => {
const { secret } = await utils.createApiKey(user.accessToken, [Permission.AssetRead]);
const { status, body } = await request(app).get('/tags').set('x-api-key', secret);
expect(status).toBe(403);
expect(body).toEqual(errorDto.missingPermission('tag.read'));
});

it('should start off empty', async () => {
const { status, body } = await request(app).get('/tags').set('Authorization', `Bearer ${admin.accessToken}`);
expect(body).toEqual([]);
Expand Down Expand Up @@ -143,7 +152,7 @@ describe('/tags', () => {
expect(body).toEqual(errorDto.unauthorized);
});

it('should not work without permission', async () => {
it('should require authorization (api key)', async () => {
const { secret } = await utils.createApiKey(user.accessToken, [Permission.AssetRead]);
const { status, body } = await request(app).put('/tags').set('x-api-key', secret).send({ name: 'TagA' });
expect(status).toBe(403);
Expand All @@ -167,7 +176,7 @@ describe('/tags', () => {
expect(body).toEqual(errorDto.unauthorized);
});

it('should not work without permission', async () => {
it('should require authorization (api key)', async () => {
const { secret } = await utils.createApiKey(user.accessToken, [Permission.AssetRead]);
const { status, body } = await request(app)
.put('/tags/assets')
Expand Down Expand Up @@ -242,6 +251,16 @@ describe('/tags', () => {
expect(body).toEqual(errorDto.noPermission);
});

it('should require authorization (api key)', async () => {
const { secret } = await utils.createApiKey(user.accessToken, [Permission.AssetRead]);
const { status, body } = await request(app)
.get(`/tags/${uuidDto.notFound}`)
.set('x-api-key', secret)
.send({ assetIds: [], tagIds: [] });
expect(status).toBe(403);
expect(body).toEqual(errorDto.missingPermission('tag.read'));
});

it('should require a valid uuid', async () => {
const { status, body } = await request(app)
.get(`/tags/${uuidDto.invalid}`)
Expand Down Expand Up @@ -293,7 +312,17 @@ describe('/tags', () => {
expect(body).toEqual(errorDto.unauthorized);
});

it('should not work without permission', async () => {
it('should require authorization', async () => {
const tag = await create(admin.accessToken, { name: 'tagA' });
const { status, body } = await request(app)
.put(`/tags/${tag.id}`)
.send({ color: '#000000' })
.set('Authorization', `Bearer ${user.accessToken}`);
expect(status).toBe(400);
expect(body).toEqual(errorDto.noPermission);
});

it('should require authorization (api key)', async () => {
const tag = await create(user.accessToken, { name: 'TagA' });
const { secret } = await utils.createApiKey(user.accessToken, [Permission.AssetRead]);
const { status, body } = await request(app)
Expand All @@ -304,16 +333,6 @@ describe('/tags', () => {
expect(body).toEqual(errorDto.missingPermission('tag.update'));
});

it('should require tag access', async () => {
const tag = await create(admin.accessToken, { name: 'tagA' });
const { status, body } = await request(app)
.put(`/tags/${tag.id}`)
.send({ color: '#000000' })
.set('Authorization', `Bearer ${user.accessToken}`);
expect(status).toBe(400);
expect(body).toEqual(errorDto.noPermission);
});

it('should update a tag', async () => {
const tag = await create(user.accessToken, { name: 'tagA' });
const { status, body } = await request(app)
Expand Down Expand Up @@ -351,6 +370,14 @@ describe('/tags', () => {
expect(body).toEqual(errorDto.noPermission);
});

it('should require authorization (api key)', async () => {
const tag = await create(user.accessToken, { name: 'TagA' });
const { secret } = await utils.createApiKey(user.accessToken, [Permission.AssetRead]);
const { status, body } = await request(app).delete(`/tags/${tag.id}`).set('x-api-key', secret);
expect(status).toBe(403);
expect(body).toEqual(errorDto.missingPermission('tag.delete'));
});

it('should require a valid uuid', async () => {
const { status, body } = await request(app)
.delete(`/tags/${uuidDto.invalid}`)
Expand Down Expand Up @@ -394,11 +421,34 @@ describe('/tags', () => {
describe('PUT /tags/:id/assets', () => {
it('should require authentication', async () => {
const tagA = await create(user.accessToken, { name: 'TagA' });
const { status, body } = await request(app).put(`/tags/${tagA.id}/assets`);
const { status, body } = await request(app)
.put(`/tags/${tagA.id}/assets`)
.send({ ids: [userAsset.id] });
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});

it('should require authorization', async () => {
const tag = await create(user.accessToken, { name: 'TagA' });
const { status, body } = await request(app)
.put(`/tags/${tag.id}/assets`)
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ ids: [userAsset.id] });
expect(status).toBe(400);
expect(body).toEqual(errorDto.noPermission);
});

it('should require authorization (api key)', async () => {
const tag = await create(user.accessToken, { name: 'TagA' });
const { secret } = await utils.createApiKey(user.accessToken, [Permission.AssetRead]);
const { status, body } = await request(app)
.put(`/tags/${tag.id}/assets`)
.set('x-api-key', secret)
.send({ ids: [userAsset.id] });
expect(status).toBe(403);
expect(body).toEqual(errorDto.missingPermission('tag.asset'));
});

it('should be able to tag own asset', async () => {
const tagA = await create(user.accessToken, { name: 'TagA' });
const { status, body } = await request(app)
Expand Down Expand Up @@ -462,6 +512,17 @@ describe('/tags', () => {
expect(body).toEqual(errorDto.noPermission);
});

it('should require authorization (api key)', async () => {
const tag = await create(user.accessToken, { name: 'TagA' });
const { secret } = await utils.createApiKey(user.accessToken, [Permission.AssetRead]);
const { status, body } = await request(app)
.delete(`/tags/${tag.id}/assets`)
.set('x-api-key', secret)
.send({ ids: [userAsset.id] });
expect(status).toBe(403);
expect(body).toEqual(errorDto.missingPermission('tag.asset'));
});

it('should be able to remove own asset from own tag', async () => {
const tagA = await create(user.accessToken, { name: 'TagA' });
await tagAssets(
Expand Down
1 change: 1 addition & 0 deletions server/src/migrations/1724790460210-NestedTagTable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export class NestedTagTable1724790460210 implements MigrationInterface {
name = 'NestedTagTable1724790460210'

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query('TRUNCATE TABLE "tags" CASCADE');
await queryRunner.query(`ALTER TABLE "tags" DROP CONSTRAINT "FK_92e67dc508c705dd66c94615576"`);
await queryRunner.query(`ALTER TABLE "tags" DROP CONSTRAINT "UQ_tag_name_userId"`);
await queryRunner.query(`CREATE TABLE "tags_closure" ("id_ancestor" uuid NOT NULL, "id_descendant" uuid NOT NULL, CONSTRAINT "PK_eab38eb12a3ec6df8376c95477c" PRIMARY KEY ("id_ancestor", "id_descendant"))`);
Expand Down
6 changes: 2 additions & 4 deletions web/src/lib/components/asset-viewer/detail-panel-tags.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import TagAssetForm from '$lib/components/forms/tag-asset-form.svelte';
import { AppRoute } from '$lib/constants';
import { isSharedLink } from '$lib/utils';
import { remoteTag, tagAssets } from '$lib/utils/asset-utils';
import { removeTag, tagAssets } from '$lib/utils/asset-utils';
import { getAssetInfo, type AssetResponseDto } from '@immich/sdk';
import { mdiClose, mdiPlus } from '@mdi/js';
import { t } from 'svelte-i18n';
Expand All @@ -21,7 +21,6 @@
const handleTag = async (tagIds: string[]) => {
const ids = await tagAssets({ tagIds, assetIds: [asset.id], showNotification: false });
if (ids) {
isOpen = false;
}
Expand All @@ -30,8 +29,7 @@
};
const handleRemove = async (tagId: string) => {
const ids = await remoteTag({ tagIds: [tagId], assetIds: [asset.id] });
const ids = await removeTag({ tagIds: [tagId], assetIds: [asset.id], showNotification: false });
if (ids) {
asset = await getAssetInfo({ id: asset.id });
}
Expand Down
2 changes: 1 addition & 1 deletion web/src/lib/components/assets/thumbnail/thumbnail.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@
return;
}
if (dateGroup && assetStore) {
assetStore.taskManager.seperatedThumbnail(componentId, dateGroup, asset, () => (intersecting = false));
assetStore.taskManager.separatedThumbnail(componentId, dateGroup, asset, () => (intersecting = false));
} else {
intersecting = false;
}
Expand Down
56 changes: 43 additions & 13 deletions web/src/lib/components/forms/tag-asset-form.svelte
Original file line number Diff line number Diff line change
@@ -1,36 +1,39 @@
<script lang="ts">
import { mdiTag } from '@mdi/js';
import { mdiClose, mdiTag } from '@mdi/js';
import { t } from 'svelte-i18n';
import Button from '../elements/buttons/button.svelte';
import Combobox, { type ComboBoxOption } from '../shared-components/combobox.svelte';
import FullScreenModal from '../shared-components/full-screen-modal.svelte';
import { onMount } from 'svelte';
import { getAllTags, type TagResponseDto } from '@immich/sdk';
import Icon from '$lib/components/elements/icon.svelte';
export let onTag: (tagIds: string[]) => void;
export let onCancel: () => void;
let tags: TagResponseDto[] = [];
let selectedTags = new Set<string>();
$: disabled = selectedTags.size === 0;
let allTags: TagResponseDto[] = [];
$: tagMap = Object.fromEntries(allTags.map((tag) => [tag.id, tag]));
let selectedIds = new Set<string>();
$: disabled = selectedIds.size === 0;
onMount(async () => {
tags = await getAllTags();
allTags = await getAllTags();
});
const handleSubmit = () => {
onTag([...selectedTags]);
selectedTags.clear();
};
const handleSubmit = () => onTag([...selectedIds]);
const handleSelect = (option?: ComboBoxOption) => {
if (!option) {
selectedTags = new Set();
return;
}
selectedTags.add(option.value);
selectedTags = new Set(selectedTags);
selectedIds.add(option.value);
selectedIds = selectedIds;
};
const handleRemove = (tag: string) => {
selectedIds.delete(tag);
selectedIds = selectedIds;
};
</script>

Expand All @@ -40,11 +43,38 @@
<Combobox
on:select={({ detail: option }) => handleSelect(option)}
label={$t('tag')}
options={tags.map((tag) => ({ id: tag.id, label: tag.value, value: tag.id }))}
options={allTags.map((tag) => ({ id: tag.id, label: tag.value, value: tag.id }))}
placeholder={$t('search_tags')}
/>
</div>
</form>

<section class="flex flex-wrap pt-2 gap-1">
{#each selectedIds as tagId (tagId)}
{@const tag = tagMap[tagId]}
{#if tag}
<div class="flex group transition-all">
<span
class="inline-block h-min whitespace-nowrap pl-3 pr-1 group-hover:pl-3 py-1 text-center align-baseline leading-none text-gray-100 dark:text-immich-dark-gray bg-immich-primary dark:bg-immich-dark-primary rounded-tl-full rounded-bl-full hover:bg-immich-primary/80 dark:hover:bg-immich-dark-primary/80 transition-all"
>
<p class="text-sm">
{tag.value}
</p>
</span>

<button
type="button"
class="text-gray-100 dark:text-immich-dark-gray bg-immich-primary/95 dark:bg-immich-dark-primary/95 rounded-tr-full rounded-br-full place-items-center place-content-center pr-2 pl-1 py-1 hover:bg-immich-primary/80 dark:hover:bg-immich-dark-primary/80 transition-all"
title="Remove tag"
on:click={() => handleRemove(tagId)}
>
<Icon path={mdiClose} />
</button>
</div>
{/if}
{/each}
</section>

<svelte:fragment slot="sticky-bottom">
<Button color="gray" fullwidth on:click={onCancel}>{$t('cancel')}</Button>
<Button type="submit" fullwidth form="create-tag-form" {disabled}>{$t('tag_assets')}</Button>
Expand Down
2 changes: 1 addition & 1 deletion web/src/lib/components/photos-page/asset-date-group.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@
);
},
onSeparate: () => {
$assetStore.taskManager.seperatedDateGroup(componentId, dateGroup, () =>
$assetStore.taskManager.separatedDateGroup(componentId, dateGroup, () =>
assetStore.updateBucketDateGroup(bucket, dateGroup, { intersecting: false }),
);
},
Expand Down
16 changes: 8 additions & 8 deletions web/src/lib/components/photos-page/asset-grid.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down Expand Up @@ -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,
Expand Down
3 changes: 3 additions & 0 deletions web/src/lib/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1008,6 +1008,7 @@
"remove_from_favorites": "Remove from favorites",
"remove_from_shared_link": "Remove from shared link",
"remove_offline_files": "Remove Offline Files",
"remove_tagged_assets": "Removed tag from {count, plural, one {# asset} other {# assets}}",
"remove_user": "Remove user",
"removed_api_key": "Removed API Key: {name}",
"removed_from_archive": "Removed from archive",
Expand Down Expand Up @@ -1169,6 +1170,7 @@
"tag_assets": "Tag assets",
"tag_created": "Created tag: {tag}",
"tag_updated": "Updated tag: {tag}",
"tagged_assets": "Tagged {count, plural, one {# asset} other {# assets}}",
"tags": "Tags",
"template": "Template",
"theme": "Theme",
Expand All @@ -1181,6 +1183,7 @@
"to_change_password": "Change password",
"to_favorite": "Favorite",
"to_login": "Login",
"to_root": "To root",
"to_trash": "Trash",
"toggle_settings": "Toggle settings",
"toggle_theme": "Toggle dark theme",
Expand Down
Loading

0 comments on commit 03480e8

Please sign in to comment.