mirror of
https://github.com/immich-app/immich.git
synced 2026-05-22 15:42:32 -04:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 54c1fbebde |
@@ -1,16 +0,0 @@
|
||||
import { Kysely, sql } from 'kysely';
|
||||
|
||||
export async function up(db: Kysely<any>): Promise<void> {
|
||||
// Delete unauthorized cross-owner asset faces
|
||||
await sql`
|
||||
DELETE FROM asset_face
|
||||
USING person, asset
|
||||
WHERE asset_face."personId" = person.id
|
||||
AND asset_face."assetId" = asset.id
|
||||
AND person."ownerId" <> asset."ownerId"
|
||||
`.execute(db);
|
||||
}
|
||||
|
||||
export async function down(): Promise<void> {
|
||||
// Not implemented: the deleted rows were unauthorized cross-owner entries
|
||||
}
|
||||
@@ -454,30 +454,6 @@ describe(PersonService.name, () => {
|
||||
expect(mocks.person.update).not.toHaveBeenCalled();
|
||||
expect(mocks.job.queueAll).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should reject creating a face on an asset the user does not own', async () => {
|
||||
const auth = AuthFactory.create();
|
||||
const asset = AssetFactory.create();
|
||||
const person = PersonFactory.create({ faceAssetId: null });
|
||||
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set());
|
||||
mocks.access.person.checkOwnerAccess.mockResolvedValue(new Set([person.id]));
|
||||
|
||||
await expect(
|
||||
sut.createFace(auth, {
|
||||
assetId: asset.id,
|
||||
personId: person.id,
|
||||
imageHeight: 500,
|
||||
imageWidth: 400,
|
||||
x: 10,
|
||||
y: 20,
|
||||
width: 100,
|
||||
height: 110,
|
||||
}),
|
||||
).rejects.toBeInstanceOf(BadRequestException);
|
||||
|
||||
expect(mocks.person.createAssetFace).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('createNewFeaturePhoto', () => {
|
||||
|
||||
@@ -627,7 +627,7 @@ export class PersonService extends BaseService {
|
||||
// TODO return a asset face response
|
||||
async createFace(auth: AuthDto, dto: AssetFaceCreateDto): Promise<void> {
|
||||
await Promise.all([
|
||||
this.requireAccess({ auth, permission: Permission.AssetUpdate, ids: [dto.assetId] }),
|
||||
this.requireAccess({ auth, permission: Permission.AssetRead, ids: [dto.assetId] }),
|
||||
this.requireAccess({ auth, permission: Permission.PersonRead, ids: [dto.personId] }),
|
||||
]);
|
||||
|
||||
|
||||
@@ -3,15 +3,18 @@
|
||||
import type { AlbumResponseDto } from '@immich/sdk';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
interface Props {
|
||||
type Props = {
|
||||
album: AlbumResponseDto;
|
||||
}
|
||||
};
|
||||
|
||||
let { album }: Props = $props();
|
||||
const { album }: Props = $props();
|
||||
const startDate = album.startDate;
|
||||
</script>
|
||||
|
||||
<span class="my-2 flex gap-2 text-sm font-medium text-gray-500" data-testid="album-details">
|
||||
<span>{getAlbumDateRange(album)}</span>
|
||||
<span>•</span>
|
||||
{#if startDate}
|
||||
<span>{getAlbumDateRange(startDate, album.endDate ?? startDate)}</span>
|
||||
<span>•</span>
|
||||
{/if}
|
||||
<span>{$t('items_count', { values: { count: album.assetCount } })}</span>
|
||||
</span>
|
||||
|
||||
@@ -34,6 +34,12 @@ export const dateFormats = {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
timeZone: 'UTC',
|
||||
} satisfies Intl.DateTimeFormatOptions,
|
||||
albumShort: {
|
||||
month: 'short',
|
||||
year: 'numeric',
|
||||
timeZone: 'UTC',
|
||||
} satisfies Intl.DateTimeFormatOptions,
|
||||
settings: {
|
||||
month: 'short',
|
||||
|
||||
@@ -1,66 +1,93 @@
|
||||
import { writable } from 'svelte/store';
|
||||
import { locale } from '$lib/stores/preferences.store';
|
||||
import { getAlbumDateRange, getShortDateRange } from './date-time';
|
||||
|
||||
vitest.mock('$lib/stores/preferences.store', () => ({
|
||||
locale: writable('en'),
|
||||
}));
|
||||
|
||||
describe('getShortDateRange', () => {
|
||||
beforeEach(() => {
|
||||
vi.stubEnv('TZ', 'UTC');
|
||||
locale.set('en');
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
vi.unstubAllEnvs();
|
||||
locale.set('en');
|
||||
});
|
||||
|
||||
it('should correctly return long month if start and end date are within the same month', () => {
|
||||
expect(getShortDateRange('2022-01-01T00:00:00.000Z', '2022-01-31T00:00:00.000Z')).toEqual('January 2022');
|
||||
});
|
||||
|
||||
it('should correctly return month range if start and end date are in separate months within the same year', () => {
|
||||
expect(getShortDateRange('2022-01-01T00:00:00.000Z', '2022-02-01T00:00:00.000Z')).toEqual('Jan – Feb 2022');
|
||||
});
|
||||
|
||||
it('should correctly return range if start and end date are in separate months and years', () => {
|
||||
expect(getShortDateRange('2021-12-01T00:00:00.000Z', '2022-01-01T00:00:00.000Z')).toEqual('Dec 2021 – Jan 2022');
|
||||
});
|
||||
|
||||
it('should correctly return long month if start and end date are within the same month, ignoring local time zone', () => {
|
||||
vi.stubEnv('TZ', 'UTC+6');
|
||||
expect(getShortDateRange('2022-01-01T00:00:00.000Z', '2022-01-31T00:00:00.000Z')).toEqual('January 2022');
|
||||
});
|
||||
|
||||
it('should correctly return month range if start and end date are in separate months within the same year, ignoring local time zone', () => {
|
||||
vi.stubEnv('TZ', 'UTC+6');
|
||||
expect(getShortDateRange('2022-01-01T00:00:00.000Z', '2022-02-01T00:00:00.000Z')).toEqual('Jan – Feb 2022');
|
||||
});
|
||||
|
||||
it('should correctly return range if start and end date are in separate months and years, ignoring local time zone', () => {
|
||||
vi.stubEnv('TZ', 'UTC+6');
|
||||
expect(getShortDateRange('2021-12-01T00:00:00.000Z', '2022-01-01T00:00:00.000Z')).toEqual('Dec 2021 – Jan 2022');
|
||||
});
|
||||
|
||||
it('should correctly return range if start and end date are in separate months and years, ignoring local time zone', () => {
|
||||
vi.stubEnv('TZ', 'UTC-6');
|
||||
expect(getShortDateRange('2021-12-01T00:00:00.000Z', '2022-01-01T00:00:00.000Z')).toEqual('Dec 2021 – Jan 2022');
|
||||
});
|
||||
|
||||
it('should use the correct locale to return month range', () => {
|
||||
locale.set('fr');
|
||||
expect(getShortDateRange('2022-01-01T00:00:00.000Z', '2022-02-01T00:00:00.000Z')).toEqual('janv.–févr. 2022');
|
||||
});
|
||||
|
||||
it('should use the correct locale to return month-year range', () => {
|
||||
locale.set('fr');
|
||||
expect(getShortDateRange('2021-12-01T00:00:00.000Z', '2022-01-01T00:00:00.000Z')).toEqual('déc. 2021 – janv. 2022');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAlbumDateRange', () => {
|
||||
beforeAll(() => {
|
||||
vi.stubEnv('TZ', 'UTC');
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
vi.unstubAllEnvs();
|
||||
});
|
||||
|
||||
it('should correctly return month if start and end date are within the same month', () => {
|
||||
expect(getShortDateRange('2022-01-01T00:00:00.000Z', '2022-01-31T00:00:00.000Z')).toEqual('Jan 2022');
|
||||
it('should work', () => {
|
||||
expect(getAlbumDateRange('2021-01-01T00:00:00Z', '2021-01-05T00:00:00Z')).toEqual('Jan 1 – 5, 2021');
|
||||
});
|
||||
|
||||
it('should correctly return month range if start and end date are in separate months within the same year', () => {
|
||||
expect(getShortDateRange('2022-01-01T00:00:00.000Z', '2022-02-01T00:00:00.000Z')).toEqual('Jan - Feb 2022');
|
||||
it('should work with a single day range', () => {
|
||||
expect(getAlbumDateRange('2021-01-01T09:00:00Z', '2021-01-01T10:00:00Z')).toEqual('Jan 1, 2021');
|
||||
});
|
||||
|
||||
it('should correctly return range if start and end date are in separate months and years', () => {
|
||||
expect(getShortDateRange('2021-12-01T00:00:00.000Z', '2022-01-01T00:00:00.000Z')).toEqual('Dec 2021 - Jan 2022');
|
||||
it('should work with positive time zone present', () => {
|
||||
expect(getAlbumDateRange('2021-01-01T00:00:00+05:00', '2021-01-01T00:00:00+05:00')).toEqual('Jan 1, 2021');
|
||||
});
|
||||
|
||||
it('should correctly return month if start and end date are within the same month, ignoring local time zone', () => {
|
||||
vi.stubEnv('TZ', 'UTC+6');
|
||||
expect(getShortDateRange('2022-01-01T00:00:00.000Z', '2022-01-31T00:00:00.000Z')).toEqual('Jan 2022');
|
||||
it('should work with negative time zone present', () => {
|
||||
expect(getAlbumDateRange('2021-01-01T00:00:00-05:00', '2021-01-01T00:00:00-05:00')).toEqual('Jan 1, 2021');
|
||||
});
|
||||
|
||||
it('should correctly return month range if start and end date are in separate months within the same year, ignoring local time zone', () => {
|
||||
vi.stubEnv('TZ', 'UTC+6');
|
||||
expect(getShortDateRange('2022-01-01T00:00:00.000Z', '2022-02-01T00:00:00.000Z')).toEqual('Jan - Feb 2022');
|
||||
});
|
||||
|
||||
it('should correctly return range if start and end date are in separate months and years, ignoring local time zone', () => {
|
||||
vi.stubEnv('TZ', 'UTC+6');
|
||||
expect(getShortDateRange('2021-12-01T00:00:00.000Z', '2022-01-01T00:00:00.000Z')).toEqual('Dec 2021 - Jan 2022');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAlbumDate', () => {
|
||||
beforeAll(() => {
|
||||
process.env.TZ = 'UTC';
|
||||
|
||||
vitest.mock('$lib/stores/preferences.store', () => ({
|
||||
locale: writable('en'),
|
||||
}));
|
||||
});
|
||||
|
||||
it('should work with only a start date', () => {
|
||||
expect(getAlbumDateRange({ startDate: '2021-01-01T00:00:00Z' })).toEqual('Jan 1, 2021');
|
||||
});
|
||||
|
||||
it('should work with a start and end date', () => {
|
||||
expect(
|
||||
getAlbumDateRange({
|
||||
startDate: '2021-01-01T00:00:00Z',
|
||||
endDate: '2021-01-05T00:00:00Z',
|
||||
}),
|
||||
).toEqual('Jan 1, 2021 - Jan 5, 2021');
|
||||
});
|
||||
|
||||
it('should work with the new date format', () => {
|
||||
expect(getAlbumDateRange({ startDate: '2021-01-01T00:00:00+05:00' })).toEqual('Jan 1, 2021');
|
||||
it('should use the proper locale', () => {
|
||||
locale.set('fr');
|
||||
expect(getAlbumDateRange('2020-03-26T12:00:00Z', '2021-12-01T00:00:00Z')).toEqual('26 mars 2020 – 1 déc. 2021');
|
||||
locale.set('en');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,69 +7,28 @@ export function parseUtcDate(date: string) {
|
||||
return DateTime.fromISO(date, { zone: 'UTC' }).toUTC();
|
||||
}
|
||||
|
||||
export const getShortDateRange = (startTimestamp: string, endTimestamp: string) => {
|
||||
const getDateRange = (startTimestamp: string, endTimestamp: string, format: 'short' | 'long') => {
|
||||
const userLocale = get(locale);
|
||||
let startDate = DateTime.fromISO(startTimestamp).setZone('UTC');
|
||||
let endDate = DateTime.fromISO(endTimestamp).setZone('UTC');
|
||||
const startDate = DateTime.fromISO(startTimestamp).setZone('UTC');
|
||||
const endDate = DateTime.fromISO(endTimestamp).setZone('UTC').setLocale(userLocale);
|
||||
|
||||
if (userLocale) {
|
||||
startDate = startDate.setLocale(userLocale);
|
||||
endDate = endDate.setLocale(userLocale);
|
||||
if (startDate.year === endDate.year && startDate.month === endDate.month && format === 'short') {
|
||||
return endDate.toLocaleString({ month: 'long', year: 'numeric' });
|
||||
}
|
||||
|
||||
const endDateLocalized = endDate.toLocaleString({
|
||||
month: 'short',
|
||||
year: 'numeric',
|
||||
});
|
||||
|
||||
if (startDate.year === endDate.year) {
|
||||
if (startDate.month === endDate.month) {
|
||||
// Same year and month.
|
||||
// e.g.: aug. 2024
|
||||
return endDateLocalized;
|
||||
} else {
|
||||
// Same year but different month.
|
||||
// e.g.: jul. - sept. 2024
|
||||
const startMonthLocalized = startDate.toLocaleString({
|
||||
month: 'short',
|
||||
});
|
||||
return `${startMonthLocalized} - ${endDateLocalized}`;
|
||||
}
|
||||
} else {
|
||||
// Different year.
|
||||
// e.g.: feb. 2021 - sept. 2024
|
||||
const startDateLocalized = startDate.toLocaleString({
|
||||
month: 'short',
|
||||
year: 'numeric',
|
||||
});
|
||||
return `${startDateLocalized} - ${endDateLocalized}`;
|
||||
}
|
||||
const formatter = new Intl.DateTimeFormat(
|
||||
userLocale,
|
||||
format === 'short' ? dateFormats.albumShort : dateFormats.album,
|
||||
);
|
||||
return formatter.formatRange(startDate.toJSDate(), endDate.toJSDate());
|
||||
};
|
||||
|
||||
const formatDate = (date?: string) => {
|
||||
if (!date) {
|
||||
return;
|
||||
}
|
||||
/**
|
||||
* Get localized date range in short format like 'Oct – Nov 2026', with full month if start and end are the same: 'October 2026'
|
||||
*/
|
||||
export const getShortDateRange = (start: string, end: string) => getDateRange(start, end, 'short');
|
||||
|
||||
// without timezone
|
||||
const localDate = date.replace(/Z$/, '').replace(/\+.+$/, '');
|
||||
return localDate ? new Date(localDate).toLocaleDateString(get(locale), dateFormats.album) : undefined;
|
||||
};
|
||||
|
||||
export const getAlbumDateRange = (album: { startDate?: string; endDate?: string }) => {
|
||||
const start = formatDate(album.startDate);
|
||||
const end = formatDate(album.endDate);
|
||||
|
||||
if (start && end && start !== end) {
|
||||
return `${start} - ${end}`;
|
||||
}
|
||||
|
||||
if (start) {
|
||||
return start;
|
||||
}
|
||||
|
||||
return '';
|
||||
};
|
||||
export const getAlbumDateRange = (start: string, end: string) => getDateRange(start, end, 'long');
|
||||
|
||||
/**
|
||||
* Use this to convert from "5pm EST" to "5pm UTC"
|
||||
|
||||
Reference in New Issue
Block a user