feat: readonly album sharing (#8720)

* rename albums_shared_users_users to album_permissions and add readonly column

* disable synchronize on the original join table

* remove unnecessary FK names

* set readonly=true as default for new album shares

* separate and implement album READ and WRITE permission

* expose albumPermissions on the API, deprecate sharedUsers

* generate openapi

* create readonly view on frontend

* ??? move slideshow button out from ellipsis menu so that non-owners can have access too

* correct sharedUsers joins

* add album permission repository

* remove a log

* fix assetCount getting reset when adding users

* fix lint

* add set permission endpoint and UI

* sort users

* remove log

* Revert "??? move slideshow button out from ellipsis menu so that non-owners can have access too"

This reverts commit 1343bfa311.

* rename stuff

* fix db schema annotations

* sql generate

* change readonly default to follow migration

* fix deprecation notice

* change readonly boolean to role enum

* fix joincolumn as primary key

* rename albumUserRepository in album service

* clean up userId and albumId

* add write access to shared link

* fix existing tests

* switch to vitest

* format and fix tests on web

* add new test

* fix one e2e test

* rename new API field to albumUsers

* capitalize serverside enum

* remove unused ReadWrite type

* missed rename from previous commit

* rename to albumUsers in album entity as well

* remove outdated Equals calls

* unnecessary relation

* rename to updateUser in album service

* minor renamery

* move sorting to backend

* rename and separate ALBUM_WRITE as ADD_ASSET and REMOVE_ASSET

* fix tests

* fix "should migrate single moving picture" test failing on European system timezone

* generated changes after merge

* lint fix

* fix correct page to open after removing user from album

* fix e2e tests and some bugs

* rename updateAlbumUser rest endpoint

* add new e2e tests for updateAlbumUser endpoint

* small optimizations

* refactor album e2e test, add new album shared with viewer

* add new test to check if viewer can see the album

* add new e2e tests for readonly share

* failing test: User delete doesn't cascade to UserAlbum entity

* fix: handle deleted users

* use lodash for sort

* add role to addUsersToAlbum endpoint

* add UI for adding editors

* lint fixes

* change role back to editor as DB default

* fix server tests

* redesign user selection modal editor selector

* style tweaks

* fix type error

* Revert "style tweaks"

This reverts commit ab604f4c8f.

* Revert "redesign user selection modal editor selector"

This reverts commit e6f344856c.

* chore: cleanup and improve add user modal

* chore: open api

* small styling

---------

Co-authored-by: mgabor <>
Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
This commit is contained in:
mgabor
2024-04-25 06:19:49 +02:00
committed by GitHub
parent 0b3373c552
commit 2943f93098
56 changed files with 1778 additions and 370 deletions
@@ -1,6 +1,9 @@
<script lang="ts">
import { afterNavigate, goto } from '$app/navigation';
import AlbumDescription from '$lib/components/album-page/album-description.svelte';
import AlbumOptions from '$lib/components/album-page/album-options.svelte';
import AlbumSummary from '$lib/components/album-page/album-summary.svelte';
import AlbumTitle from '$lib/components/album-page/album-title.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';
@@ -39,12 +42,16 @@
import { locale } from '$lib/stores/preferences.store';
import { SlideshowNavigation, SlideshowState, slideshowStore } from '$lib/stores/slideshow.store';
import { user } from '$lib/stores/user.store';
import { handlePromiseError } from '$lib/utils';
import { downloadAlbum } from '$lib/utils/asset-utils';
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 { isAlbumsRoute, isPeopleRoute, isSearchRoute } from '$lib/utils/navigation';
import {
AlbumUserRole,
AssetOrder,
ReactionLevel,
ReactionType,
addAssetsToAlbum,
@@ -57,29 +64,23 @@
getAlbumInfo,
updateAlbumInfo,
type ActivityResponseDto,
type UserResponseDto,
AssetOrder,
type AlbumUserAddDto,
} from '@immich/sdk';
import {
mdiArrowLeft,
mdiCogOutline,
mdiDeleteOutline,
mdiDotsVertical,
mdiFolderDownloadOutline,
mdiLink,
mdiPlus,
mdiShareVariantOutline,
mdiPresentationPlay,
mdiCogOutline,
mdiImageOutline,
mdiImagePlusOutline,
mdiLink,
mdiPlus,
mdiPresentationPlay,
mdiShareVariantOutline,
} from '@mdi/js';
import { fly } from 'svelte/transition';
import type { PageData } from './$types';
import AlbumTitle from '$lib/components/album-page/album-title.svelte';
import AlbumDescription from '$lib/components/album-page/album-description.svelte';
import { handlePromiseError } from '$lib/utils';
import AlbumSummary from '$lib/components/album-page/album-summary.svelte';
import { isAlbumsRoute, isPeopleRoute, isSearchRoute } from '$lib/utils/navigation';
export let data: PageData;
@@ -137,6 +138,9 @@
$: showActivityStatus =
album.sharedUsers.length > 0 && !$showAssetViewer && (album.isActivityEnabled || $numberOfComments > 0);
$: isEditor = album.albumUsers.find(({ user: { id } }) => id === $user.id)?.role === AlbumUserRole.Editor;
$: albumHasViewers = album.albumUsers.some(({ role }) => role === AlbumUserRole.Viewer);
afterNavigate(({ from }) => {
let url: string | undefined = from?.url?.pathname;
@@ -312,12 +316,12 @@
viewMode = ViewMode.VIEW;
};
const handleAddUsers = async (users: UserResponseDto[]) => {
const handleAddUsers = async (albumUsers: AlbumUserAddDto[]) => {
try {
album = await addUsersToAlbum({
id: album.id,
addUsersDto: {
sharedUserIds: [...users].map(({ id }) => id),
albumUsers,
},
});
@@ -335,9 +339,9 @@
try {
await refreshAlbum();
viewMode = album.sharedUsers.length > 1 ? ViewMode.SELECT_USERS : ViewMode.VIEW;
viewMode = album.sharedUsers.length > 0 ? ViewMode.VIEW_USERS : ViewMode.VIEW;
} catch (error) {
handleError(error, 'Error deleting share users');
handleError(error, 'Error deleting shared user');
}
};
@@ -433,11 +437,13 @@
{#if viewMode === ViewMode.VIEW || viewMode === ViewMode.ALBUM_OPTIONS}
<ControlAppBar showBackButton backIcon={mdiArrowLeft} on:close={() => goto(backUrl)}>
<svelte:fragment slot="trailing">
<CircleIconButton
title="Add photos"
on:click={() => (viewMode = ViewMode.SELECT_ASSETS)}
icon={mdiImagePlusOutline}
/>
{#if isEditor}
<CircleIconButton
title="Add photos"
on:click={() => (viewMode = ViewMode.SELECT_ASSETS)}
icon={mdiImagePlusOutline}
/>
{/if}
{#if isOwned}
<CircleIconButton
@@ -578,13 +584,25 @@
<UserAvatar user={album.owner} size="md" />
</button>
<!-- users -->
{#each album.sharedUsers as user (user.id)}
<!-- users with write access (collaborators) -->
{#each album.albumUsers.filter(({ role }) => role === AlbumUserRole.Editor) as { user } (user.id)}
<button on:click={() => (viewMode = ViewMode.VIEW_USERS)}>
<UserAvatar {user} size="md" />
</button>
{/each}
<!-- display ellipsis if there are readonly users too -->
{#if albumHasViewers}
<CircleIconButton
title="View all users"
backgroundColor="#d3d3d3"
forceDark
size="20"
icon={mdiDotsVertical}
on:click={() => (viewMode = ViewMode.VIEW_USERS)}
/>
{/if}
{#if isOwned}
<CircleIconButton
backgroundColor="#d3d3d3"
@@ -678,6 +696,7 @@
onClose={() => (viewMode = ViewMode.VIEW)}
{album}
on:remove={({ detail: userId }) => handleRemoveUser(userId)}
on:refreshAlbum={refreshAlbum}
/>
{/if}