refactor(web): use new open api client (#7097)

* refactor(web): use new open api client

* refactor: remove activity api

* refactor: trash, oauth, and partner apis

* refactor: job api

* refactor: face, library, system config

* refactor: user api

* refactor: album api
This commit is contained in:
Jason Rasmussen
2024-02-13 17:07:37 -05:00
committed by GitHub
parent 9b4a770b9d
commit 8fd94211c0
66 changed files with 593 additions and 850 deletions
+2 -2
View File
@@ -1,10 +1,10 @@
import { authenticate } from '$lib/utils/auth';
import { api } from '@api';
import { getAllAlbums } from '@immich/sdk';
import type { PageLoad } from './$types';
export const load = (async () => {
await authenticate();
const { data: albums } = await api.albumApi.getAllAlbums();
const albums = await getAllAlbums({});
return {
albums,
@@ -1,17 +1,21 @@
<script lang="ts">
import { afterNavigate, goto } from '$app/navigation';
import AlbumOptions from '$lib/components/album-page/album-options.svelte';
import ShareInfoModal from '$lib/components/album-page/share-info-modal.svelte';
import UserSelectionModal from '$lib/components/album-page/user-selection-modal.svelte';
import ActivityStatus from '$lib/components/asset-viewer/activity-status.svelte';
import ActivityViewer from '$lib/components/asset-viewer/activity-viewer.svelte';
import Button from '$lib/components/elements/buttons/button.svelte';
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
import Icon from '$lib/components/elements/icon.svelte';
import AddToAlbum from '$lib/components/photos-page/actions/add-to-album.svelte';
import ArchiveAction from '$lib/components/photos-page/actions/archive-action.svelte';
import ChangeDate from '$lib/components/photos-page/actions/change-date-action.svelte';
import ChangeLocation from '$lib/components/photos-page/actions/change-location-action.svelte';
import CreateSharedLink from '$lib/components/photos-page/actions/create-shared-link.svelte';
import DeleteAssets from '$lib/components/photos-page/actions/delete-assets.svelte';
import DownloadAction from '$lib/components/photos-page/actions/download-action.svelte';
import FavoriteAction from '$lib/components/photos-page/actions/favorite-action.svelte';
import ChangeDate from '$lib/components/photos-page/actions/change-date-action.svelte';
import ChangeLocation from '$lib/components/photos-page/actions/change-location-action.svelte';
import RemoveFromAlbum from '$lib/components/photos-page/actions/remove-from-album.svelte';
import SelectAllAssets from '$lib/components/photos-page/actions/select-all-assets.svelte';
import AssetGrid from '$lib/components/photos-page/asset-grid.svelte';
@@ -26,40 +30,47 @@
NotificationType,
notificationController,
} from '$lib/components/shared-components/notification/notification';
import UpdatePanel from '$lib/components/shared-components/update-panel.svelte';
import UserAvatar from '$lib/components/shared-components/user-avatar.svelte';
import { AppRoute, dateFormats } from '$lib/constants';
import { numberOfComments, setNumberOfComments, updateNumberOfComments } from '$lib/stores/activity.store';
import { createAssetInteractionStore } from '$lib/stores/asset-interaction.store';
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
import { SlideshowState, slideshowStore } from '$lib/stores/slideshow.store';
import { AssetStore } from '$lib/stores/assets.store';
import { locale } from '$lib/stores/preferences.store';
import { SlideshowState, slideshowStore } from '$lib/stores/slideshow.store';
import { user } from '$lib/stores/user.store';
import { downloadArchive } from '$lib/utils/asset-utils';
import { openFileUploadDialog } from '$lib/utils/file-uploader';
import { handleError } from '$lib/utils/handle-error';
import { type ActivityResponseDto, ReactionLevel, ReactionType, type UserResponseDto, api } from '@api';
import Icon from '$lib/components/elements/icon.svelte';
import type { PageData } from './$types';
import { autoGrowHeight } from '$lib/utils/autogrow';
import { clickOutside } from '$lib/utils/click-outside';
import { getContextMenuPosition } from '$lib/utils/context-menu';
import { openFileUploadDialog } from '$lib/utils/file-uploader';
import { handleError } from '$lib/utils/handle-error';
import { ReactionLevel, ReactionType, type ActivityResponseDto, type UserResponseDto } from '@api';
import {
addAssetsToAlbum,
addUsersToAlbum,
createActivity,
deleteActivity,
deleteAlbum,
getActivities,
getActivityStatistics,
getAlbumInfo,
updateAlbumInfo,
} from '@immich/sdk';
import {
mdiPlus,
mdiDotsVertical,
mdiArrowLeft,
mdiDeleteOutline,
mdiDotsVertical,
mdiFileImagePlusOutline,
mdiFolderDownloadOutline,
mdiLink,
mdiPlus,
mdiShareVariantOutline,
mdiDeleteOutline,
} from '@mdi/js';
import { onMount } from 'svelte';
import { fly } from 'svelte/transition';
import ActivityViewer from '$lib/components/asset-viewer/activity-viewer.svelte';
import ActivityStatus from '$lib/components/asset-viewer/activity-status.svelte';
import { numberOfComments, setNumberOfComments, updateNumberOfComments } from '$lib/stores/activity.store';
import AlbumOptions from '$lib/components/album-page/album-options.svelte';
import UpdatePanel from '$lib/components/shared-components/update-panel.svelte';
import { user } from '$lib/stores/user.store';
import { autoGrowHeight } from '$lib/utils/autogrow';
import type { PageData } from './$types';
export let data: PageData;
@@ -141,13 +152,12 @@
const handleToggleEnableActivity = async () => {
try {
const { data } = await api.albumApi.updateAlbumInfo({
album = await updateAlbumInfo({
id: album.id,
updateAlbumDto: {
isActivityEnabled: !album.isActivityEnabled,
},
});
album = data;
notificationController.show({
type: NotificationType.Info,
message: `Activity is ${album.isActivityEnabled ? 'enabled' : 'disabled'}`,
@@ -161,14 +171,13 @@
try {
if (isLiked) {
const activityId = isLiked.id;
await api.activityApi.deleteActivity({ id: activityId });
await deleteActivity({ id: activityId });
reactions = reactions.filter((reaction) => reaction.id !== activityId);
isLiked = null;
} else {
const { data } = await api.activityApi.createActivity({
isLiked = await createActivity({
activityCreateDto: { albumId: album.id, type: ReactionType.Like },
});
isLiked = data;
reactions = [...reactions, isLiked];
}
} catch (error) {
@@ -179,10 +188,10 @@
const getFavorite = async () => {
if ($user) {
try {
const { data } = await api.activityApi.getActivities({
const data = await getActivities({
userId: $user.id,
albumId: album.id,
type: ReactionType.Like,
$type: ReactionType.Like,
level: ReactionLevel.Album,
});
if (data.length > 0) {
@@ -196,8 +205,8 @@
const getNumberOfComments = async () => {
try {
const { data } = await api.activityApi.getActivityStatistics({ albumId: album.id });
setNumberOfComments(data.comments);
const { comments } = await getActivityStatistics({ albumId: album.id });
setNumberOfComments(comments);
} catch (error) {
handleError(error, "Can't get number of comments");
}
@@ -269,8 +278,7 @@
};
const refreshAlbum = async () => {
const { data } = await api.albumApi.getAlbumInfo({ id: album.id, withoutAssets: true });
album = data;
album = await getAlbumInfo({ id: album.id, withoutAssets: true });
};
const getDateRange = () => {
@@ -302,7 +310,7 @@
const assetIds = [...$timelineSelected].map((asset) => asset.id);
try {
const { data: results } = await api.albumApi.addAssetsToAlbum({
const results = await addAssetsToAlbum({
id: album.id,
bulkIdsDto: { ids: assetIds },
});
@@ -346,15 +354,13 @@
const handleAddUsers = async (users: UserResponseDto[]) => {
try {
const { data } = await api.albumApi.addUsersToAlbum({
album = await addUsersToAlbum({
id: album.id,
addUsersDto: {
sharedUserIds: [...users].map(({ id }) => id),
},
});
album = data;
viewMode = ViewMode.VIEW;
} catch (error) {
handleError(error, 'Error adding users to album');
@@ -381,7 +387,7 @@
const handleRemoveAlbum = async () => {
try {
await api.albumApi.deleteAlbum({ id: album.id });
await deleteAlbum({ id: album.id });
goto(backUrl);
} catch (error) {
handleError(error, 'Unable to delete album');
@@ -399,7 +405,7 @@
assetInteractionStore.clearMultiselect();
try {
await api.albumApi.updateAlbumInfo({
await updateAlbumInfo({
id: album.id,
updateAlbumDto: {
albumThumbnailAssetId: assetId,
@@ -418,7 +424,7 @@
}
try {
await api.albumApi.updateAlbumInfo({
await updateAlbumInfo({
id: album.id,
updateAlbumDto: {
albumName: album.albumName,
@@ -436,7 +442,7 @@
return;
}
try {
await api.albumApi.updateAlbumInfo({
await updateAlbumInfo({
id: album.id,
updateAlbumDto: {
description,
@@ -1,10 +1,10 @@
import { authenticate } from '$lib/utils/auth';
import { api } from '@api';
import { getAlbumInfo } from '@immich/sdk';
import type { PageLoad } from './$types';
export const load = (async ({ params }) => {
await authenticate();
const { data: album } = await api.albumApi.getAlbumInfo({ id: params.albumId, withoutAssets: true });
const album = await getAlbumInfo({ id: params.albumId, withoutAssets: true });
return {
album,
@@ -1,158 +0,0 @@
import { notificationController, NotificationType } from '$lib/components/shared-components/notification/notification';
import { api, type CreateAlbumDto } from '@api';
import { albumFactory } from '@test-data';
import { get } from 'svelte/store';
import { useAlbums } from '../albums.bloc';
import type { MockedObject } from 'vitest';
vi.mock('@api');
const apiMock: MockedObject<typeof api> = api as MockedObject<typeof api>;
describe('Albums BLoC', () => {
let sut: ReturnType<typeof useAlbums>;
const _albums = albumFactory.buildList(5);
beforeEach(() => {
sut = useAlbums({ albums: [..._albums] });
});
afterEach(() => {
const notifications = get(notificationController.notificationList);
for (const notification of notifications) {
notificationController.removeNotificationById(notification.id);
}
});
it('inits with provided albums', () => {
const albums = get(sut.albums);
expect(albums.length).toEqual(5);
expect(albums).toEqual(_albums);
});
it('loads albums from the server', async () => {
// TODO: this method currently deletes albums with no assets and albumName === '' which might not be the best approach
const loadedAlbums = [..._albums, albumFactory.build({ id: 'new_loaded_uuid' })];
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
// TODO: there needs to be a more robust mock of the @api to avoid mockResolvedValueOnce ts error
// this is a workaround to make ts checks not fail but the test will pass as expected
apiMock.albumApi.getAllAlbums.mockResolvedValueOnce({
data: loadedAlbums,
config: {},
headers: {},
status: 200,
statusText: '',
});
await sut.loadAlbums();
const albums = get(sut.albums);
expect(apiMock.albumApi.getAllAlbums).toHaveBeenCalledTimes(1);
expect(albums).toEqual(loadedAlbums);
});
it('shows error message when it fails loading albums', async () => {
// TODO: implement APIProblem interface in the server
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
// TODO: there needs to be a more robust mock of the @api to avoid mockResolvedValueOnce ts error
// this is a workaround to make ts checks not fail but the test will pass as expected
apiMock.albumApi.getAllAlbums.mockRejectedValueOnce({});
expect(get(notificationController.notificationList)).toHaveLength(0);
await sut.loadAlbums();
const albums = get(sut.albums);
const notifications = get(notificationController.notificationList);
expect(apiMock.albumApi.getAllAlbums).toHaveBeenCalledTimes(2);
expect(albums).toEqual(_albums);
expect(notifications).toHaveLength(1);
expect(notifications[0].type).toEqual(NotificationType.Error);
});
it('creates a new album', async () => {
const payload: CreateAlbumDto = {
albumName: '',
};
const returnedAlbum = albumFactory.build();
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
// TODO: there needs to be a more robust mock of the @api to avoid mockResolvedValueOnce ts error
// this is a workaround to make ts checks not fail but the test will pass as expected
apiMock.albumApi.createAlbum.mockResolvedValueOnce({
data: returnedAlbum,
config: {},
headers: {},
status: 200,
statusText: '',
});
const newAlbum = await sut.createAlbum();
expect(apiMock.albumApi.createAlbum).toHaveBeenCalledTimes(1);
expect(apiMock.albumApi.createAlbum).toHaveBeenCalledWith({ createAlbumDto: payload });
expect(newAlbum).toEqual(returnedAlbum);
});
it('shows error message when it fails creating an album', async () => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
// TODO: there needs to be a more robust mock of the @api to avoid mockResolvedValueOnce ts error
// this is a workaround to make ts checks not fail but the test will pass as expected
apiMock.albumApi.createAlbum.mockRejectedValueOnce({});
const newAlbum = await sut.createAlbum();
const notifications = get(notificationController.notificationList);
expect(apiMock.albumApi.createAlbum).toHaveBeenCalledTimes(2);
expect(newAlbum).not.toBeDefined();
expect(notifications).toHaveLength(1);
expect(notifications[0].type).toEqual(NotificationType.Error);
});
it('selects an album and deletes it', async () => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
// TODO: there needs to be a more robust mock of the @api to avoid mockResolvedValueOnce ts error
// this is a workaround to make ts checks not fail but the test will pass as expected
apiMock.albumApi.deleteAlbum.mockResolvedValueOnce({
data: undefined,
config: {},
headers: {},
status: 200,
statusText: '',
});
const albumToDelete = get(sut.albums)[2]; // delete third album
const albumToDeleteId = albumToDelete.id;
const contextMenuCoords = { x: 100, y: 150 };
expect(get(sut.isShowContextMenu)).toBe(false);
sut.showAlbumContextMenu(contextMenuCoords, albumToDelete);
expect(get(sut.contextMenuPosition)).toEqual(contextMenuCoords);
expect(get(sut.isShowContextMenu)).toBe(true);
expect(get(sut.contextMenuTargetAlbum)).toEqual(albumToDelete);
await sut.deleteAlbum(albumToDelete);
const updatedAlbums = get(sut.albums);
expect(apiMock.albumApi.deleteAlbum).toHaveBeenCalledTimes(1);
expect(apiMock.albumApi.deleteAlbum).toHaveBeenCalledWith({ id: albumToDeleteId });
expect(updatedAlbums).toHaveLength(4);
expect(updatedAlbums).not.toContain(albumToDelete);
});
it('closes album context menu, deselecting album', () => {
const albumToDelete = get(sut.albums)[2]; // delete third album
sut.showAlbumContextMenu({ x: 100, y: 150 }, albumToDelete);
expect(get(sut.isShowContextMenu)).toBe(true);
sut.closeAlbumContextMenu();
expect(get(sut.isShowContextMenu)).toBe(false);
});
});
+13 -25
View File
@@ -1,6 +1,7 @@
import type { OnShowContextMenuDetail } from '$lib/components/album-page/album-card';
import { notificationController, NotificationType } from '$lib/components/shared-components/notification/notification';
import { type AlbumResponseDto, api } from '@api';
import { handleError } from '$lib/utils/handle-error';
import { createAlbum, deleteAlbum, getAllAlbums, type AlbumResponseDto } from '@immich/sdk';
import { derived, get, writable } from 'svelte/store';
type AlbumsProperties = { albums: AlbumResponseDto[] };
@@ -13,14 +14,14 @@ export const useAlbums = (properties: AlbumsProperties) => {
async function loadAlbums(): Promise<void> {
try {
const { data } = await api.albumApi.getAllAlbums();
const data = await getAllAlbums({});
albums.set(data);
// Delete album that has no photos and is named ''
for (const album of data) {
if (album.albumName === '' && album.assetCount === 0) {
setTimeout(async () => {
await deleteAlbum(album);
await handleDeleteAlbum(album);
}, 500);
}
}
@@ -32,30 +33,17 @@ export const useAlbums = (properties: AlbumsProperties) => {
}
}
async function createAlbum(): Promise<AlbumResponseDto | undefined> {
async function handleCreateAlbum(): Promise<AlbumResponseDto | undefined> {
try {
const { data: newAlbum } = await api.albumApi.createAlbum({
createAlbumDto: {
albumName: '',
},
});
return newAlbum;
} catch {
notificationController.show({
message: 'Error creating album',
type: NotificationType.Error,
});
return await createAlbum({ createAlbumDto: { albumName: '' } });
} catch (error) {
handleError(error, 'Unable to create album');
}
}
async function deleteAlbum(albumToDelete: AlbumResponseDto): Promise<void> {
await api.albumApi.deleteAlbum({ id: albumToDelete.id });
albums.set(
get(albums).filter(({ id }) => {
return id !== albumToDelete.id;
}),
);
async function handleDeleteAlbum(albumToDelete: AlbumResponseDto): Promise<void> {
await deleteAlbum({ id: albumToDelete.id });
albums.set(get(albums).filter(({ id }) => id !== albumToDelete.id));
}
async function showAlbumContextMenu(
@@ -80,8 +68,8 @@ export const useAlbums = (properties: AlbumsProperties) => {
contextMenuPosition,
contextMenuTargetAlbum,
loadAlbums,
createAlbum,
deleteAlbum,
createAlbum: handleCreateAlbum,
deleteAlbum: handleDeleteAlbum,
showAlbumContextMenu,
closeAlbumContextMenu,
};
@@ -1,11 +1,11 @@
import { authenticate } from '$lib/utils/auth';
import { api } from '@api';
import { getUserById } from '@immich/sdk';
import type { PageLoad } from './$types';
export const load = (async ({ params }) => {
await authenticate();
const { data: partner } = await api.userApi.getUserById({ id: params.userId });
const partner = await getUserById({ id: params.userId });
return {
partner,
+7 -20
View File
@@ -3,38 +3,25 @@
import empty2Url from '$lib/assets/empty-2.svg';
import AlbumCard from '$lib/components/album-page/album-card.svelte';
import LinkButton from '$lib/components/elements/buttons/link-button.svelte';
import Icon from '$lib/components/elements/icon.svelte';
import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte';
import {
notificationController,
NotificationType,
} from '$lib/components/shared-components/notification/notification';
import UserAvatar from '$lib/components/shared-components/user-avatar.svelte';
import { AppRoute } from '$lib/constants';
import { api } from '@api';
import { flip } from 'svelte/animate';
import type { PageData } from './$types';
import { createAlbum } from '@immich/sdk';
import { mdiLink, mdiPlusBoxOutline } from '@mdi/js';
import Icon from '$lib/components/elements/icon.svelte';
import { flip } from 'svelte/animate';
import { handleError } from '../../../lib/utils/handle-error';
import type { PageData } from './$types';
export let data: PageData;
const createSharedAlbum = async () => {
try {
const { data: newAlbum } = await api.albumApi.createAlbum({
createAlbumDto: {
albumName: '',
},
});
const newAlbum = await createAlbum({ createAlbumDto: { albumName: '' } });
goto(`${AppRoute.ALBUMS}/${newAlbum.id}`);
} catch (error) {
notificationController.show({
message: 'Error creating album, check console for more details',
type: NotificationType.Error,
});
console.log('Error [createAlbum]', error);
handleError(error, 'Unable to create album');
}
};
</script>
+3 -3
View File
@@ -1,11 +1,11 @@
import { authenticate } from '$lib/utils/auth';
import { api } from '@api';
import { getAllAlbums, getPartners } from '@immich/sdk';
import type { PageLoad } from './$types';
export const load = (async () => {
await authenticate();
const { data: sharedAlbums } = await api.albumApi.getAllAlbums({ shared: true });
const { data: partners } = await api.partnerApi.getPartners({ direction: 'shared-with' });
const sharedAlbums = await getAllAlbums({ shared: true });
const partners = await getPartners({ direction: 'shared-with' });
return {
sharedAlbums,
+15 -15
View File
@@ -1,29 +1,29 @@
<script lang="ts">
import { goto } from '$app/navigation';
import empty3Url from '$lib/assets/empty-3.svg';
import LinkButton from '$lib/components/elements/buttons/link-button.svelte';
import Icon from '$lib/components/elements/icon.svelte';
import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
import DeleteAssets from '$lib/components/photos-page/actions/delete-assets.svelte';
import RestoreAssets from '$lib/components/photos-page/actions/restore-assets.svelte';
import SelectAllAssets from '$lib/components/photos-page/actions/select-all-assets.svelte';
import AssetGrid from '$lib/components/photos-page/asset-grid.svelte';
import AssetSelectControlBar from '$lib/components/photos-page/asset-select-control-bar.svelte';
import ConfirmDialogue from '$lib/components/shared-components/confirm-dialogue.svelte';
import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte';
import { AppRoute } from '$lib/constants';
import { createAssetInteractionStore } from '$lib/stores/asset-interaction.store';
import { handleError } from '$lib/utils/handle-error';
import {
NotificationType,
notificationController,
} from '$lib/components/shared-components/notification/notification';
import LinkButton from '$lib/components/elements/buttons/link-button.svelte';
import { AssetStore } from '$lib/stores/assets.store';
import { api } from '@api';
import Icon from '$lib/components/elements/icon.svelte';
import type { PageData } from './$types';
import { featureFlags, serverConfig } from '$lib/stores/server-config.store';
import { goto } from '$app/navigation';
import empty3Url from '$lib/assets/empty-3.svg';
import ConfirmDialogue from '$lib/components/shared-components/confirm-dialogue.svelte';
import { mdiDeleteOutline, mdiHistory } from '@mdi/js';
import UpdatePanel from '$lib/components/shared-components/update-panel.svelte';
import { AppRoute } from '$lib/constants';
import { createAssetInteractionStore } from '$lib/stores/asset-interaction.store';
import { AssetStore } from '$lib/stores/assets.store';
import { featureFlags, serverConfig } from '$lib/stores/server-config.store';
import { handleError } from '$lib/utils/handle-error';
import { emptyTrash, restoreTrash } from '@immich/sdk';
import { mdiDeleteOutline, mdiHistory } from '@mdi/js';
import type { PageData } from './$types';
export let data: PageData;
@@ -37,7 +37,7 @@
const handleEmptyTrash = async () => {
isShowEmptyConfirmation = false;
try {
await api.trashApi.emptyTrash();
await emptyTrash();
notificationController.show({
message: `Empty trash initiated. Refresh the page to see the changes`,
@@ -50,7 +50,7 @@
const handleRestoreTrash = async () => {
try {
await api.trashApi.restoreTrash();
await restoreTrash();
notificationController.show({
message: `Restore trash initiated. Refresh the page to see the changes`,
+3 -3
View File
@@ -1,12 +1,12 @@
import { authenticate } from '$lib/utils/auth';
import { api } from '@api';
import { getApiKeys, getAuthDevices } from '@immich/sdk';
import type { PageLoad } from './$types';
export const load = (async () => {
await authenticate();
const { data: keys } = await api.keyApi.getApiKeys();
const { data: devices } = await api.authenticationApi.getAuthDevices();
const keys = await getApiKeys();
const devices = await getAuthDevices();
return {
keys,