fix(web): disable metadata edit if user is not owner (#5415)

* fix(web): disable metadata edit if user is not owner

* pr feedback

* pr feedback

* get data from page data

* fix: better representation

* feat: warn user if there's issues with the selected assets

---------

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
This commit is contained in:
martin 2023-12-01 21:58:24 +01:00 committed by GitHub
parent 5a50d32748
commit ec92608024
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 83 additions and 53 deletions

View file

@ -144,7 +144,7 @@
<main <main
class="relative h-screen overflow-hidden bg-immich-bg px-6 pt-[var(--navbar-height)] dark:bg-immich-dark-bg sm:px-12 md:px-24 lg:px-40" class="relative h-screen overflow-hidden bg-immich-bg px-6 pt-[var(--navbar-height)] dark:bg-immich-dark-bg sm:px-12 md:px-24 lg:px-40"
> >
<AssetGrid {album} {user} {assetStore} {assetInteractionStore}> <AssetGrid {album} {assetStore} {assetInteractionStore}>
<section class="pt-24"> <section class="pt-24">
<!-- ALBUM TITLE --> <!-- ALBUM TITLE -->
<p <p

View file

@ -9,7 +9,6 @@
AssetTypeEnum, AssetTypeEnum,
ReactionType, ReactionType,
SharedLinkResponseDto, SharedLinkResponseDto,
UserResponseDto,
} from '@api'; } from '@api';
import { createEventDispatcher, onDestroy, onMount } from 'svelte'; import { createEventDispatcher, onDestroy, onMount } from 'svelte';
import { fly } from 'svelte/transition'; import { fly } from 'svelte/transition';
@ -42,6 +41,7 @@
import { updateNumberOfComments } from '$lib/stores/activity.store'; import { updateNumberOfComments } from '$lib/stores/activity.store';
import { SlideshowState, slideshowStore } from '$lib/stores/slideshow.store'; import { SlideshowState, slideshowStore } from '$lib/stores/slideshow.store';
import SlideshowBar from './slideshow-bar.svelte'; import SlideshowBar from './slideshow-bar.svelte';
import { user } from '$lib/stores/user.store';
export let assetStore: AssetStore | null = null; export let assetStore: AssetStore | null = null;
export let asset: AssetResponseDto; export let asset: AssetResponseDto;
@ -51,7 +51,6 @@
export let force = false; export let force = false;
export let withStacked = false; export let withStacked = false;
export let isShared = false; export let isShared = false;
export let user: UserResponseDto | null = null;
export let album: AlbumResponseDto | null = null; export let album: AlbumResponseDto | null = null;
let reactions: ActivityResponseDto[] = []; let reactions: ActivityResponseDto[] = [];
@ -143,10 +142,10 @@
}; };
const getFavorite = async () => { const getFavorite = async () => {
if (album && user) { if (album && $user) {
try { try {
const { data } = await api.activityApi.getActivities({ const { data } = await api.activityApi.getActivities({
userId: user.id, userId: $user.id,
assetId: asset.id, assetId: asset.id,
albumId: album.id, albumId: album.id,
type: ReactionType.Like, type: ReactionType.Like,
@ -743,7 +742,7 @@
</div> </div>
{/if} {/if}
{#if isShared && album && isShowActivity && user} {#if isShared && album && isShowActivity && $user}
<div <div
transition:fly={{ duration: 150 }} transition:fly={{ duration: 150 }}
id="activity-panel" id="activity-panel"
@ -751,7 +750,7 @@
translate="yes" translate="yes"
> >
<ActivityViewer <ActivityViewer
{user} user={$user}
disabled={!album.isActivityEnabled} disabled={!album.isActivityEnabled}
assetType={asset.type} assetType={asset.type}
albumOwnerId={album.ownerId} albumOwnerId={album.ownerId}

View file

@ -26,6 +26,7 @@
import { AppRoute } from '$lib/constants'; import { AppRoute } from '$lib/constants';
import ChangeLocation from '../shared-components/change-location.svelte'; import ChangeLocation from '../shared-components/change-location.svelte';
import { handleError } from '../../utils/handle-error'; import { handleError } from '../../utils/handle-error';
import { user } from '$lib/stores/user.store';
export let asset: AssetResponseDto; export let asset: AssetResponseDto;
export let albums: AlbumResponseDto[] = []; export let albums: AlbumResponseDto[] = [];
@ -238,12 +239,14 @@
zone: asset.exifInfo.timeZone ?? undefined, zone: asset.exifInfo.timeZone ?? undefined,
})} })}
<div <div
class="flex justify-between place-items-start gap-4 py-4 hover:dark:text-immich-dark-primary hover:text-immich-primary cursor-pointer" class="flex justify-between place-items-start gap-4 py-4"
on:click={() => (isShowChangeDate = true)}
on:keydown={(event) => event.key === 'Enter' && (isShowChangeDate = true)}
tabindex="0" tabindex="0"
role="button" role="button"
title="Edit date" on:click={() => (isOwner ? (isShowChangeDate = true) : null)}
on:keydown={(event) => (isOwner ? event.key === 'Enter' && (isShowChangeDate = true) : null)}
title={isOwner ? 'Edit date' : ''}
class:hover:dark:text-immich-dark-primary={isOwner}
class:hover:text-immich-primary={isOwner}
> >
<div class="flex gap-4"> <div class="flex gap-4">
<div> <div>
@ -276,11 +279,14 @@
</div> </div>
</div> </div>
</div> </div>
<button class="focus:outline-none">
<Icon path={mdiPencil} size="20" /> {#if isOwner}
</button> <button class="focus:outline-none">
<Icon path={mdiPencil} size="20" />
</button>
{/if}
</div> </div>
{:else if !asset.exifInfo?.dateTimeOriginal && !asset.isReadOnly} {:else if !asset.exifInfo?.dateTimeOriginal && !asset.isReadOnly && $user && asset.ownerId === $user.id}
<div class="flex justify-between place-items-start gap-4 py-4"> <div class="flex justify-between place-items-start gap-4 py-4">
<div class="flex gap-4"> <div class="flex gap-4">
<div> <div>
@ -410,12 +416,14 @@
{#if asset.exifInfo?.city && !asset.isReadOnly} {#if asset.exifInfo?.city && !asset.isReadOnly}
<div <div
class="flex justify-between place-items-start gap-4 py-4 hover:dark:text-immich-dark-primary hover:text-immich-primary cursor-pointer" class="flex justify-between place-items-start gap-4 py-4"
on:click={() => (isShowChangeLocation = true)} on:click={() => (isOwner ? (isShowChangeLocation = true) : null)}
on:keydown={(event) => event.key === 'Enter' && (isShowChangeLocation = true)} on:keydown={(event) => (isOwner ? event.key === 'Enter' && (isShowChangeLocation = true) : null)}
tabindex="0" tabindex="0"
title={isOwner ? 'Edit location' : ''}
role="button" role="button"
title="Edit location" class:hover:dark:text-immich-dark-primary={isOwner}
class:hover:text-immich-primary={isOwner}
> >
<div class="flex gap-4"> <div class="flex gap-4">
<div><Icon path={mdiMapMarkerOutline} size="24" /></div> <div><Icon path={mdiMapMarkerOutline} size="24" /></div>
@ -435,11 +443,13 @@
</div> </div>
</div> </div>
<div> {#if isOwner}
<Icon path={mdiPencil} size="20" /> <div>
</div> <Icon path={mdiPencil} size="20" />
</div>
{/if}
</div> </div>
{:else if !asset.exifInfo?.city && !asset.isReadOnly} {:else if !asset.exifInfo?.city && !asset.isReadOnly && $user && asset.ownerId === $user.id}
<div <div
class="flex justify-between place-items-start gap-4 py-4 rounded-lg pr-2 hover:dark:text-immich-dark-primary hover:text-immich-primary" class="flex justify-between place-items-start gap-4 py-4 rounded-lg pr-2 hover:dark:text-immich-dark-primary hover:text-immich-primary"
on:click={() => (isShowChangeLocation = true)} on:click={() => (isShowChangeLocation = true)}

View file

@ -5,6 +5,8 @@
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import MenuOption from '../../shared-components/context-menu/menu-option.svelte'; import MenuOption from '../../shared-components/context-menu/menu-option.svelte';
import { getAssetControlContext } from '../asset-select-control-bar.svelte'; import { getAssetControlContext } from '../asset-select-control-bar.svelte';
import { user } from '$lib/stores/user.store';
import { getSelectedAssets } from '$lib/utils/asset-utils';
export let menuItem = false; export let menuItem = false;
const { clearSelect, getOwnedAssets } = getAssetControlContext(); const { clearSelect, getOwnedAssets } = getAssetControlContext();
@ -12,9 +14,7 @@
const handleConfirm = async (dateTimeOriginal: string) => { const handleConfirm = async (dateTimeOriginal: string) => {
isShowChangeDate = false; isShowChangeDate = false;
const ids = Array.from(getOwnedAssets()) const ids = getSelectedAssets(getOwnedAssets(), $user);
.filter((a) => !a.isExternal)
.map((a) => a.id);
try { try {
await api.assetApi.updateAssets({ await api.assetApi.updateAssets({

View file

@ -4,6 +4,8 @@
import { getAssetControlContext } from '../asset-select-control-bar.svelte'; import { getAssetControlContext } from '../asset-select-control-bar.svelte';
import ChangeLocation from '$lib/components/shared-components/change-location.svelte'; import ChangeLocation from '$lib/components/shared-components/change-location.svelte';
import { handleError } from '../../../utils/handle-error'; import { handleError } from '../../../utils/handle-error';
import { user } from '$lib/stores/user.store';
import { getSelectedAssets } from '$lib/utils/asset-utils';
export let menuItem = false; export let menuItem = false;
const { clearSelect, getOwnedAssets } = getAssetControlContext(); const { clearSelect, getOwnedAssets } = getAssetControlContext();
@ -12,9 +14,7 @@
async function handleConfirm(point: { lng: number; lat: number }) { async function handleConfirm(point: { lng: number; lat: number }) {
isShowChangeLocation = false; isShowChangeLocation = false;
const ids = Array.from(getOwnedAssets()) const ids = getSelectedAssets(getOwnedAssets(), $user);
.filter((a) => !a.isExternal)
.map((a) => a.id);
try { try {
await api.assetApi.updateAssets({ await api.assetApi.updateAssets({

View file

@ -8,7 +8,7 @@
import { locale } from '$lib/stores/preferences.store'; import { locale } from '$lib/stores/preferences.store';
import { isSearchEnabled } from '$lib/stores/search.store'; import { isSearchEnabled } from '$lib/stores/search.store';
import { formatGroupTitle, splitBucketIntoDateGroups } from '$lib/utils/timeline-util'; import { formatGroupTitle, splitBucketIntoDateGroups } from '$lib/utils/timeline-util';
import type { AlbumResponseDto, AssetResponseDto, UserResponseDto } from '@api'; import type { AlbumResponseDto, AssetResponseDto } from '@api';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import { createEventDispatcher, onDestroy, onMount } from 'svelte'; import { createEventDispatcher, onDestroy, onMount } from 'svelte';
import AssetViewer from '../asset-viewer/asset-viewer.svelte'; import AssetViewer from '../asset-viewer/asset-viewer.svelte';
@ -27,7 +27,6 @@
export let removeAction: AssetAction | null = null; export let removeAction: AssetAction | null = null;
export let withStacked = false; export let withStacked = false;
export let isShared = false; export let isShared = false;
export let user: UserResponseDto | null = null;
export let album: AlbumResponseDto | null = null; export let album: AlbumResponseDto | null = null;
$: isTrashEnabled = $featureFlags.loaded && $featureFlags.trash; $: isTrashEnabled = $featureFlags.loaded && $featureFlags.trash;
@ -394,7 +393,6 @@
<Portal target="body"> <Portal target="body">
{#if $showAssetViewer} {#if $showAssetViewer}
<AssetViewer <AssetViewer
{user}
{withStacked} {withStacked}
{assetStore} {assetStore}
asset={$viewingAsset} asset={$viewingAsset}

View file

@ -2,7 +2,7 @@
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { fileUploadHandler, openFileUploadDialog } from '$lib/utils/file-uploader'; import { fileUploadHandler, openFileUploadDialog } from '$lib/utils/file-uploader';
import { downloadArchive } from '$lib/utils/asset-utils'; import { downloadArchive } from '$lib/utils/asset-utils';
import { api, AssetResponseDto, SharedLinkResponseDto, UserResponseDto } from '@api'; import { api, AssetResponseDto, SharedLinkResponseDto } from '@api';
import { dragAndDropFilesStore } from '$lib/stores/drag-and-drop-files.store'; import { dragAndDropFilesStore } from '$lib/stores/drag-and-drop-files.store';
import CircleIconButton from '../elements/buttons/circle-icon-button.svelte'; import CircleIconButton from '../elements/buttons/circle-icon-button.svelte';
import DownloadAction from '../photos-page/actions/download-action.svelte'; import DownloadAction from '../photos-page/actions/download-action.svelte';
@ -17,7 +17,6 @@
export let sharedLink: SharedLinkResponseDto; export let sharedLink: SharedLinkResponseDto;
export let isOwned: boolean; export let isOwned: boolean;
export let user: UserResponseDto | undefined = undefined;
let selectedAssets: Set<AssetResponseDto> = new Set(); let selectedAssets: Set<AssetResponseDto> = new Set();
@ -103,6 +102,6 @@
</ControlAppBar> </ControlAppBar>
{/if} {/if}
<section class="my-[160px] flex flex-col px-6 sm:px-12 md:px-24 lg:px-40"> <section class="my-[160px] flex flex-col px-6 sm:px-12 md:px-24 lg:px-40">
<GalleryViewer {user} {assets} bind:selectedAssets /> <GalleryViewer {assets} bind:selectedAssets />
</section> </section>
</section> </section>

View file

@ -2,7 +2,7 @@
import { page } from '$app/stores'; import { page } from '$app/stores';
import Thumbnail from '$lib/components/assets/thumbnail/thumbnail.svelte'; import Thumbnail from '$lib/components/assets/thumbnail/thumbnail.svelte';
import { handleError } from '$lib/utils/handle-error'; import { handleError } from '$lib/utils/handle-error';
import { AssetResponseDto, ThumbnailFormat, UserResponseDto } from '@api'; import { AssetResponseDto, ThumbnailFormat } from '@api';
import AssetViewer from '../../asset-viewer/asset-viewer.svelte'; import AssetViewer from '../../asset-viewer/asset-viewer.svelte';
import { flip } from 'svelte/animate'; import { flip } from 'svelte/animate';
import { getThumbnailSize } from '$lib/utils/thumbnail-util'; import { getThumbnailSize } from '$lib/utils/thumbnail-util';
@ -13,7 +13,6 @@
export let selectedAssets: Set<AssetResponseDto> = new Set(); export let selectedAssets: Set<AssetResponseDto> = new Set();
export let disableAssetSelect = false; export let disableAssetSelect = false;
export let showArchiveIcon = false; export let showArchiveIcon = false;
export let user: UserResponseDto | undefined = undefined;
let { isViewing: showAssetViewer } = assetViewingStore; let { isViewing: showAssetViewer } = assetViewingStore;
@ -109,7 +108,6 @@
<!-- Overlay Asset Viewer --> <!-- Overlay Asset Viewer -->
{#if $showAssetViewer} {#if $showAssetViewer}
<AssetViewer <AssetViewer
{user}
asset={selectedAsset} asset={selectedAsset}
on:previous={navigateAssetBackward} on:previous={navigateAssetBackward}
on:next={navigateAssetForward} on:next={navigateAssetForward}

View file

@ -0,0 +1,4 @@
import { writable } from 'svelte/store';
import type { UserResponseDto } from '@api';
export const user = writable<UserResponseDto | null>(null);

View file

@ -1,6 +1,14 @@
import { notificationController, NotificationType } from '$lib/components/shared-components/notification/notification'; import { notificationController, NotificationType } from '$lib/components/shared-components/notification/notification';
import { downloadManager } from '$lib/stores/download'; import { downloadManager } from '$lib/stores/download';
import { api, BulkIdResponseDto, AssetResponseDto, DownloadResponseDto, DownloadInfoDto, AssetTypeEnum } from '@api'; import {
api,
BulkIdResponseDto,
AssetResponseDto,
DownloadResponseDto,
DownloadInfoDto,
AssetTypeEnum,
UserResponseDto,
} from '@api';
import { handleError } from './handle-error'; import { handleError } from './handle-error';
export const addAssetsToAlbum = async (albumId: string, assetIds: Array<string>): Promise<BulkIdResponseDto[]> => export const addAssetsToAlbum = async (albumId: string, assetIds: Array<string>): Promise<BulkIdResponseDto[]> =>
@ -203,3 +211,17 @@ export const getAssetType = (type: AssetTypeEnum) => {
return 'Asset'; return 'Asset';
} }
}; };
export const getSelectedAssets = (assets: Set<AssetResponseDto>, user: UserResponseDto | null): string[] => {
const ids = Array.from(assets)
.filter((a) => !a.isExternal && user && a.ownerId !== user.id)
.map((a) => a.id);
const numberOfIssues = Array.from(assets).filter((a) => a.isExternal || (user && a.ownerId === user.id)).length;
if (numberOfIssues > 0) {
notificationController.show({
message: `Can't change metadata of ${numberOfIssues} asset${numberOfIssues > 1 ? 's' : ''}`,
type: NotificationType.Warning,
});
}
return ids;
};

View file

@ -59,6 +59,7 @@
import { numberOfComments, setNumberOfComments, updateNumberOfComments } from '$lib/stores/activity.store'; import { numberOfComments, setNumberOfComments, updateNumberOfComments } from '$lib/stores/activity.store';
import AlbumOptions from '$lib/components/album-page/album-options.svelte'; import AlbumOptions from '$lib/components/album-page/album-options.svelte';
import UpdatePanel from '$lib/components/shared-components/update-panel.svelte'; import UpdatePanel from '$lib/components/shared-components/update-panel.svelte';
import { user } from '$lib/stores/user.store';
export let data: PageData; export let data: PageData;
@ -66,6 +67,9 @@
let { slideshowState, slideshowShuffle } = slideshowStore; let { slideshowState, slideshowShuffle } = slideshowStore;
let album = data.album; let album = data.album;
$user = data.user;
$: album = data.album; $: album = data.album;
$: { $: {
@ -96,7 +100,6 @@
let isShowActivity = false; let isShowActivity = false;
let isLiked: ActivityResponseDto | null = null; let isLiked: ActivityResponseDto | null = null;
let reactions: ActivityResponseDto[] = []; let reactions: ActivityResponseDto[] = [];
let user = data.user;
let globalWidth: number; let globalWidth: number;
let assetGridWidth: number; let assetGridWidth: number;
@ -179,10 +182,10 @@
}; };
const getFavorite = async () => { const getFavorite = async () => {
if (user) { if ($user) {
try { try {
const { data } = await api.activityApi.getActivities({ const { data } = await api.activityApi.getActivities({
userId: user.id, userId: $user.id,
albumId: album.id, albumId: album.id,
type: ReactionType.Like, type: ReactionType.Like,
level: ReactionLevel.Album, level: ReactionLevel.Album,
@ -549,16 +552,10 @@
style={`width:${assetGridWidth}px`} style={`width:${assetGridWidth}px`}
> >
{#if viewMode === ViewMode.SELECT_ASSETS} {#if viewMode === ViewMode.SELECT_ASSETS}
<AssetGrid <AssetGrid assetStore={timelineStore} assetInteractionStore={timelineInteractionStore} isSelectionMode={true} />
user={data.user}
assetStore={timelineStore}
assetInteractionStore={timelineInteractionStore}
isSelectionMode={true}
/>
{:else} {:else}
<AssetGrid <AssetGrid
{album} {album}
user={data.user}
{assetStore} {assetStore}
{assetInteractionStore} {assetInteractionStore}
isShared={album.sharedUsers.length > 0} isShared={album.sharedUsers.length > 0}
@ -679,7 +676,7 @@
{/if} {/if}
</main> </main>
</div> </div>
{#if album.sharedUsers.length > 0 && album && isShowActivity && user && !$showAssetViewer} {#if album.sharedUsers.length > 0 && album && isShowActivity && $user && !$showAssetViewer}
<div class="flex"> <div class="flex">
<div <div
transition:fly={{ duration: 150 }} transition:fly={{ duration: 150 }}
@ -688,7 +685,7 @@
translate="yes" translate="yes"
> >
<ActivityViewer <ActivityViewer
{user} user={$user}
disabled={!album.isActivityEnabled} disabled={!album.isActivityEnabled}
albumOwnerId={album.ownerId} albumOwnerId={album.ownerId}
albumId={album.id} albumId={album.id}
@ -738,10 +735,10 @@
</ConfirmDialogue> </ConfirmDialogue>
{/if} {/if}
{#if viewMode === ViewMode.OPTIONS} {#if viewMode === ViewMode.OPTIONS && $user}
<AlbumOptions <AlbumOptions
{album} {album}
{user} user={$user}
on:close={() => (viewMode = ViewMode.VIEW)} on:close={() => (viewMode = ViewMode.VIEW)}
on:toggleEnableActivity={handleToggleEnableActivity} on:toggleEnableActivity={handleToggleEnableActivity}
on:showSelectSharedUser={() => (viewMode = ViewMode.SELECT_USERS)} on:showSelectSharedUser={() => (viewMode = ViewMode.SELECT_USERS)}

View file

@ -24,6 +24,7 @@
import { assetViewingStore } from '$lib/stores/asset-viewing.store'; import { assetViewingStore } from '$lib/stores/asset-viewing.store';
import { mdiDotsVertical, mdiPlus } from '@mdi/js'; import { mdiDotsVertical, mdiPlus } from '@mdi/js';
import UpdatePanel from '$lib/components/shared-components/update-panel.svelte'; import UpdatePanel from '$lib/components/shared-components/update-panel.svelte';
import { user } from '$lib/stores/user.store';
export let data: PageData; export let data: PageData;
@ -33,6 +34,8 @@
const assetInteractionStore = createAssetInteractionStore(); const assetInteractionStore = createAssetInteractionStore();
const { isMultiSelectState, selectedAssets } = assetInteractionStore; const { isMultiSelectState, selectedAssets } = assetInteractionStore;
$user = data.user;
$: isAllFavorite = Array.from($selectedAssets).every((asset) => asset.isFavorite); $: isAllFavorite = Array.from($selectedAssets).every((asset) => asset.isFavorite);
const handleEscape = () => { const handleEscape = () => {