mirror of
https://github.com/immich-app/immich
synced 2026-08-15 13:03:57 +00:00
refactor(server): allow -1 rating again (#28886)
This commit is contained in:
parent
1ce961fbb3
commit
5c38373808
11 changed files with 52 additions and 45 deletions
|
|
@ -492,6 +492,20 @@ describe('/asset', () => {
|
|||
expect(status).toEqual(200);
|
||||
});
|
||||
|
||||
it('should set the negative rating', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.put(`/assets/${user1Assets[0].id}`)
|
||||
.set('Authorization', `Bearer ${user1.accessToken}`)
|
||||
.send({ rating: -1 });
|
||||
expect(body).toMatchObject({
|
||||
id: user1Assets[0].id,
|
||||
exifInfo: expect.objectContaining({
|
||||
rating: -1,
|
||||
}),
|
||||
});
|
||||
expect(status).toEqual(200);
|
||||
});
|
||||
|
||||
it('should return tagged people', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.put(`/assets/${user1Assets[0].id}`)
|
||||
|
|
|
|||
|
|
@ -95,9 +95,9 @@ class AssetBulkUpdateDto {
|
|||
///
|
||||
Optional<num?> longitude;
|
||||
|
||||
/// Rating in range [1-5], or null for unrated
|
||||
/// Rating in range [1-5] (starred), -1 (rejected), or null (unrated)
|
||||
///
|
||||
/// Minimum value: 1
|
||||
/// Minimum value: -1
|
||||
/// Maximum value: 5
|
||||
Optional<int?> rating;
|
||||
|
||||
|
|
|
|||
4
mobile/openapi/lib/model/update_asset_dto.dart
generated
4
mobile/openapi/lib/model/update_asset_dto.dart
generated
|
|
@ -77,9 +77,9 @@ class UpdateAssetDto {
|
|||
///
|
||||
Optional<num?> longitude;
|
||||
|
||||
/// Rating in range [1-5], or null for unrated
|
||||
/// Rating in range [1-5] (starred), -1 (rejected), or null (unrated)
|
||||
///
|
||||
/// Minimum value: 1
|
||||
/// Minimum value: -1
|
||||
/// Maximum value: 5
|
||||
Optional<int?> rating;
|
||||
|
||||
|
|
|
|||
|
|
@ -16602,9 +16602,9 @@
|
|||
"type": "number"
|
||||
},
|
||||
"rating": {
|
||||
"description": "Rating in range [1-5], or null for unrated",
|
||||
"description": "Rating in range [1-5] (starred), -1 (rejected), or null (unrated)",
|
||||
"maximum": 5,
|
||||
"minimum": 1,
|
||||
"minimum": -1,
|
||||
"nullable": true,
|
||||
"type": "integer",
|
||||
"x-immich-history": [
|
||||
|
|
@ -16616,15 +16616,10 @@
|
|||
"version": "v2",
|
||||
"state": "Stable"
|
||||
},
|
||||
{
|
||||
"version": "v2.6.0",
|
||||
"state": "Updated",
|
||||
"description": "Using -1 as a rating is deprecated and will be removed in the next major version."
|
||||
},
|
||||
{
|
||||
"version": "v3",
|
||||
"state": "Updated",
|
||||
"description": "Using -1 as a rating is no longer valid."
|
||||
"description": "Using 0 as a rating is no longer valid."
|
||||
}
|
||||
],
|
||||
"x-immich-state": "Stable"
|
||||
|
|
@ -26430,9 +26425,9 @@
|
|||
"type": "number"
|
||||
},
|
||||
"rating": {
|
||||
"description": "Rating in range [1-5], or null for unrated",
|
||||
"description": "Rating in range [1-5] (starred), -1 (rejected), or null (unrated)",
|
||||
"maximum": 5,
|
||||
"minimum": 1,
|
||||
"minimum": -1,
|
||||
"nullable": true,
|
||||
"type": "integer",
|
||||
"x-immich-history": [
|
||||
|
|
@ -26444,15 +26439,10 @@
|
|||
"version": "v2",
|
||||
"state": "Stable"
|
||||
},
|
||||
{
|
||||
"version": "v2.6.0",
|
||||
"state": "Updated",
|
||||
"description": "Using -1 as a rating is deprecated and will be removed in the next major version."
|
||||
},
|
||||
{
|
||||
"version": "v3",
|
||||
"state": "Updated",
|
||||
"description": "Using -1 as a rating is no longer valid."
|
||||
"description": "Using 0 as a rating is no longer valid."
|
||||
}
|
||||
],
|
||||
"x-immich-state": "Stable"
|
||||
|
|
|
|||
|
|
@ -672,7 +672,7 @@ export type AssetBulkUpdateDto = {
|
|||
latitude?: number;
|
||||
/** Longitude coordinate */
|
||||
longitude?: number;
|
||||
/** Rating in range [1-5], or null for unrated */
|
||||
/** Rating in range [1-5] (starred), -1 (rejected), or null (unrated) */
|
||||
rating?: number | null;
|
||||
/** Time zone (IANA timezone) */
|
||||
timeZone?: string;
|
||||
|
|
@ -919,7 +919,7 @@ export type UpdateAssetDto = {
|
|||
livePhotoVideoId?: string | null;
|
||||
/** Longitude coordinate */
|
||||
longitude?: number;
|
||||
/** Rating in range [1-5], or null for unrated */
|
||||
/** Rating in range [1-5] (starred), -1 (rejected), or null (unrated) */
|
||||
rating?: number | null;
|
||||
visibility?: AssetVisibility;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -240,7 +240,16 @@ describe(AssetController.name, () => {
|
|||
for (const [test, errors] of [
|
||||
[{ rating: 7 }, [{ path: ['rating'], message: 'Too big: expected number to be <=5' }]],
|
||||
[{ rating: 3.5 }, [{ path: ['rating'], message: 'Invalid input: expected int, received number' }]],
|
||||
[{ rating: -2 }, [{ path: ['rating'], message: 'Too small: expected number to be >=1' }]],
|
||||
[{ rating: -2 }, [{ path: ['rating'], message: 'Too small: expected number to be >=-1' }]],
|
||||
[
|
||||
{ rating: 0 },
|
||||
[
|
||||
{
|
||||
path: ['rating'],
|
||||
message: 'Rating must be -1 (rejected), 1–5 (starred), or null (unrated); 0 is not valid',
|
||||
},
|
||||
],
|
||||
],
|
||||
] as const) {
|
||||
const { status, body } = await request(ctx.getHttpServer()).put(`/assets/${factory.uuid()}`).send(test);
|
||||
expect(status).toBe(400);
|
||||
|
|
|
|||
|
|
@ -15,16 +15,18 @@ const UpdateAssetBaseSchema = z
|
|||
longitude: longitudeSchema.optional().describe('Longitude coordinate'),
|
||||
rating: z
|
||||
.int()
|
||||
.min(1)
|
||||
.min(-1)
|
||||
.max(5)
|
||||
.nullish()
|
||||
.describe('Rating in range [1-5], or null for unrated')
|
||||
.refine((v) => v !== 0, {
|
||||
error: 'Rating must be -1 (rejected), 1–5 (starred), or null (unrated); 0 is not valid',
|
||||
})
|
||||
.describe('Rating in range [1-5] (starred), -1 (rejected), or null (unrated)')
|
||||
.meta({
|
||||
...new HistoryBuilder()
|
||||
.added('v1')
|
||||
.stable('v2')
|
||||
.updated('v2.6.0', 'Using -1 as a rating is deprecated and will be removed in the next major version.')
|
||||
.updated('v3', 'Using -1 as a rating is no longer valid.')
|
||||
.updated('v3', 'Using 0 as a rating is no longer valid.')
|
||||
.getExtensions(),
|
||||
}),
|
||||
description: z.string().optional().describe('Asset description'),
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
import { Kysely, sql } from 'kysely';
|
||||
|
||||
export async function up(db: Kysely<any>): Promise<void> {
|
||||
await sql`UPDATE "asset_exif" SET "rating" = NULL WHERE "rating" = -1;`.execute(db);
|
||||
export async function up(): Promise<void> {
|
||||
// await sql`UPDATE "asset_exif" SET "rating" = NULL WHERE "rating" = -1;`.execute(db);
|
||||
}
|
||||
|
||||
export async function down(): Promise<void> {
|
||||
|
|
|
|||
|
|
@ -13,9 +13,9 @@
|
|||
|
||||
let { asset, isOwner }: Props = $props();
|
||||
|
||||
let rating = $derived(asset.exifInfo?.rating || null) as Rating;
|
||||
let rating = $derived(asset.exifInfo?.rating ?? null) as Rating;
|
||||
|
||||
const handleChangeRating = async (rating: number | null) => {
|
||||
const handleChangeRating = async (rating: Rating) => {
|
||||
try {
|
||||
await updateAsset({ id: asset.id, updateAssetDto: { rating } });
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
import { mdiStar, mdiStarOutline } from '@mdi/js';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
export type Rating = 1 | 2 | 3 | 4 | 5 | null;
|
||||
export type Rating = -1 | 1 | 2 | 3 | 4 | 5 | null;
|
||||
|
||||
interface Props {
|
||||
count?: number;
|
||||
|
|
@ -33,6 +33,7 @@
|
|||
return;
|
||||
}
|
||||
|
||||
ratingSelection = newRating;
|
||||
onRating(newRating);
|
||||
};
|
||||
|
||||
|
|
@ -70,7 +71,7 @@
|
|||
<div class="flex flex-row" data-testid="star-container">
|
||||
{#each { length: count } as _, index (index)}
|
||||
{@const value = index + 1}
|
||||
{@const filled = hoverRating === null ? (ratingSelection || 0) >= value : hoverRating >= value}
|
||||
{@const filled = hoverRating === null ? (ratingSelection ?? 0) >= value : hoverRating >= value}
|
||||
{@const starId = `${id}-${value}`}
|
||||
<!-- svelte-ignore a11y_mouse_events_have_key_events -->
|
||||
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
||||
|
|
@ -102,14 +103,7 @@
|
|||
</div>
|
||||
</fieldset>
|
||||
{#if ratingSelection !== null && !readOnly}
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => {
|
||||
ratingSelection = null;
|
||||
handleSelect(ratingSelection);
|
||||
}}
|
||||
class="cursor-pointer text-xs text-primary"
|
||||
>
|
||||
<button type="button" onclick={() => handleSelect(null)} class="cursor-pointer text-xs text-primary">
|
||||
{$t('rating_clear')}
|
||||
</button>
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import {
|
|||
fromTimelinePlainDate,
|
||||
fromTimelinePlainDateTime,
|
||||
fromTimelinePlainYearMonth,
|
||||
fromISODateTimeUTC,
|
||||
fromISODateTimeUTCToObject,
|
||||
getTimes,
|
||||
setDifference,
|
||||
type TimelineDateTime,
|
||||
|
|
@ -190,7 +190,7 @@ export class TimelineMonth {
|
|||
isVideo: !bucketAssets.isImage[i],
|
||||
livePhotoVideoId: bucketAssets.livePhotoVideoId[i],
|
||||
localDateTime,
|
||||
createdAt: fromISODateTimeUTC(bucketAssets.createdAt[i]).setZone('local'),
|
||||
createdAt: fromISODateTimeUTCToObject(bucketAssets.createdAt[i]),
|
||||
fileCreatedAt,
|
||||
ownerId: bucketAssets.ownerId[i],
|
||||
projectionType: bucketAssets.projectionType[i],
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue