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:
parent
5a50d32748
commit
ec92608024
12 changed files with 83 additions and 53 deletions
|
@ -144,7 +144,7 @@
|
|||
<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"
|
||||
>
|
||||
<AssetGrid {album} {user} {assetStore} {assetInteractionStore}>
|
||||
<AssetGrid {album} {assetStore} {assetInteractionStore}>
|
||||
<section class="pt-24">
|
||||
<!-- ALBUM TITLE -->
|
||||
<p
|
||||
|
|
|
@ -9,7 +9,6 @@
|
|||
AssetTypeEnum,
|
||||
ReactionType,
|
||||
SharedLinkResponseDto,
|
||||
UserResponseDto,
|
||||
} from '@api';
|
||||
import { createEventDispatcher, onDestroy, onMount } from 'svelte';
|
||||
import { fly } from 'svelte/transition';
|
||||
|
@ -42,6 +41,7 @@
|
|||
import { updateNumberOfComments } from '$lib/stores/activity.store';
|
||||
import { SlideshowState, slideshowStore } from '$lib/stores/slideshow.store';
|
||||
import SlideshowBar from './slideshow-bar.svelte';
|
||||
import { user } from '$lib/stores/user.store';
|
||||
|
||||
export let assetStore: AssetStore | null = null;
|
||||
export let asset: AssetResponseDto;
|
||||
|
@ -51,7 +51,6 @@
|
|||
export let force = false;
|
||||
export let withStacked = false;
|
||||
export let isShared = false;
|
||||
export let user: UserResponseDto | null = null;
|
||||
export let album: AlbumResponseDto | null = null;
|
||||
|
||||
let reactions: ActivityResponseDto[] = [];
|
||||
|
@ -143,10 +142,10 @@
|
|||
};
|
||||
|
||||
const getFavorite = async () => {
|
||||
if (album && user) {
|
||||
if (album && $user) {
|
||||
try {
|
||||
const { data } = await api.activityApi.getActivities({
|
||||
userId: user.id,
|
||||
userId: $user.id,
|
||||
assetId: asset.id,
|
||||
albumId: album.id,
|
||||
type: ReactionType.Like,
|
||||
|
@ -743,7 +742,7 @@
|
|||
</div>
|
||||
{/if}
|
||||
|
||||
{#if isShared && album && isShowActivity && user}
|
||||
{#if isShared && album && isShowActivity && $user}
|
||||
<div
|
||||
transition:fly={{ duration: 150 }}
|
||||
id="activity-panel"
|
||||
|
@ -751,7 +750,7 @@
|
|||
translate="yes"
|
||||
>
|
||||
<ActivityViewer
|
||||
{user}
|
||||
user={$user}
|
||||
disabled={!album.isActivityEnabled}
|
||||
assetType={asset.type}
|
||||
albumOwnerId={album.ownerId}
|
||||
|
|
|
@ -26,6 +26,7 @@
|
|||
import { AppRoute } from '$lib/constants';
|
||||
import ChangeLocation from '../shared-components/change-location.svelte';
|
||||
import { handleError } from '../../utils/handle-error';
|
||||
import { user } from '$lib/stores/user.store';
|
||||
|
||||
export let asset: AssetResponseDto;
|
||||
export let albums: AlbumResponseDto[] = [];
|
||||
|
@ -238,12 +239,14 @@
|
|||
zone: asset.exifInfo.timeZone ?? undefined,
|
||||
})}
|
||||
<div
|
||||
class="flex justify-between place-items-start gap-4 py-4 hover:dark:text-immich-dark-primary hover:text-immich-primary cursor-pointer"
|
||||
on:click={() => (isShowChangeDate = true)}
|
||||
on:keydown={(event) => event.key === 'Enter' && (isShowChangeDate = true)}
|
||||
class="flex justify-between place-items-start gap-4 py-4"
|
||||
tabindex="0"
|
||||
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>
|
||||
|
@ -276,11 +279,14 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button class="focus:outline-none">
|
||||
<Icon path={mdiPencil} size="20" />
|
||||
</button>
|
||||
|
||||
{#if isOwner}
|
||||
<button class="focus:outline-none">
|
||||
<Icon path={mdiPencil} size="20" />
|
||||
</button>
|
||||
{/if}
|
||||
</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 gap-4">
|
||||
<div>
|
||||
|
@ -410,12 +416,14 @@
|
|||
|
||||
{#if asset.exifInfo?.city && !asset.isReadOnly}
|
||||
<div
|
||||
class="flex justify-between place-items-start gap-4 py-4 hover:dark:text-immich-dark-primary hover:text-immich-primary cursor-pointer"
|
||||
on:click={() => (isShowChangeLocation = true)}
|
||||
on:keydown={(event) => event.key === 'Enter' && (isShowChangeLocation = true)}
|
||||
class="flex justify-between place-items-start gap-4 py-4"
|
||||
on:click={() => (isOwner ? (isShowChangeLocation = true) : null)}
|
||||
on:keydown={(event) => (isOwner ? event.key === 'Enter' && (isShowChangeLocation = true) : null)}
|
||||
tabindex="0"
|
||||
title={isOwner ? 'Edit location' : ''}
|
||||
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><Icon path={mdiMapMarkerOutline} size="24" /></div>
|
||||
|
@ -435,11 +443,13 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Icon path={mdiPencil} size="20" />
|
||||
</div>
|
||||
{#if isOwner}
|
||||
<div>
|
||||
<Icon path={mdiPencil} size="20" />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if !asset.exifInfo?.city && !asset.isReadOnly}
|
||||
{:else if !asset.exifInfo?.city && !asset.isReadOnly && $user && asset.ownerId === $user.id}
|
||||
<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"
|
||||
on:click={() => (isShowChangeLocation = true)}
|
||||
|
|
|
@ -5,6 +5,8 @@
|
|||
import { DateTime } from 'luxon';
|
||||
import MenuOption from '../../shared-components/context-menu/menu-option.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;
|
||||
const { clearSelect, getOwnedAssets } = getAssetControlContext();
|
||||
|
||||
|
@ -12,9 +14,7 @@
|
|||
|
||||
const handleConfirm = async (dateTimeOriginal: string) => {
|
||||
isShowChangeDate = false;
|
||||
const ids = Array.from(getOwnedAssets())
|
||||
.filter((a) => !a.isExternal)
|
||||
.map((a) => a.id);
|
||||
const ids = getSelectedAssets(getOwnedAssets(), $user);
|
||||
|
||||
try {
|
||||
await api.assetApi.updateAssets({
|
||||
|
|
|
@ -4,6 +4,8 @@
|
|||
import { getAssetControlContext } from '../asset-select-control-bar.svelte';
|
||||
import ChangeLocation from '$lib/components/shared-components/change-location.svelte';
|
||||
import { handleError } from '../../../utils/handle-error';
|
||||
import { user } from '$lib/stores/user.store';
|
||||
import { getSelectedAssets } from '$lib/utils/asset-utils';
|
||||
|
||||
export let menuItem = false;
|
||||
const { clearSelect, getOwnedAssets } = getAssetControlContext();
|
||||
|
@ -12,9 +14,7 @@
|
|||
|
||||
async function handleConfirm(point: { lng: number; lat: number }) {
|
||||
isShowChangeLocation = false;
|
||||
const ids = Array.from(getOwnedAssets())
|
||||
.filter((a) => !a.isExternal)
|
||||
.map((a) => a.id);
|
||||
const ids = getSelectedAssets(getOwnedAssets(), $user);
|
||||
|
||||
try {
|
||||
await api.assetApi.updateAssets({
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
import { locale } from '$lib/stores/preferences.store';
|
||||
import { isSearchEnabled } from '$lib/stores/search.store';
|
||||
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 { createEventDispatcher, onDestroy, onMount } from 'svelte';
|
||||
import AssetViewer from '../asset-viewer/asset-viewer.svelte';
|
||||
|
@ -27,7 +27,6 @@
|
|||
export let removeAction: AssetAction | null = null;
|
||||
export let withStacked = false;
|
||||
export let isShared = false;
|
||||
export let user: UserResponseDto | null = null;
|
||||
export let album: AlbumResponseDto | null = null;
|
||||
|
||||
$: isTrashEnabled = $featureFlags.loaded && $featureFlags.trash;
|
||||
|
@ -394,7 +393,6 @@
|
|||
<Portal target="body">
|
||||
{#if $showAssetViewer}
|
||||
<AssetViewer
|
||||
{user}
|
||||
{withStacked}
|
||||
{assetStore}
|
||||
asset={$viewingAsset}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
import { goto } from '$app/navigation';
|
||||
import { fileUploadHandler, openFileUploadDialog } from '$lib/utils/file-uploader';
|
||||
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 CircleIconButton from '../elements/buttons/circle-icon-button.svelte';
|
||||
import DownloadAction from '../photos-page/actions/download-action.svelte';
|
||||
|
@ -17,7 +17,6 @@
|
|||
|
||||
export let sharedLink: SharedLinkResponseDto;
|
||||
export let isOwned: boolean;
|
||||
export let user: UserResponseDto | undefined = undefined;
|
||||
|
||||
let selectedAssets: Set<AssetResponseDto> = new Set();
|
||||
|
||||
|
@ -103,6 +102,6 @@
|
|||
</ControlAppBar>
|
||||
{/if}
|
||||
<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>
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
import { page } from '$app/stores';
|
||||
import Thumbnail from '$lib/components/assets/thumbnail/thumbnail.svelte';
|
||||
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 { flip } from 'svelte/animate';
|
||||
import { getThumbnailSize } from '$lib/utils/thumbnail-util';
|
||||
|
@ -13,7 +13,6 @@
|
|||
export let selectedAssets: Set<AssetResponseDto> = new Set();
|
||||
export let disableAssetSelect = false;
|
||||
export let showArchiveIcon = false;
|
||||
export let user: UserResponseDto | undefined = undefined;
|
||||
|
||||
let { isViewing: showAssetViewer } = assetViewingStore;
|
||||
|
||||
|
@ -109,7 +108,6 @@
|
|||
<!-- Overlay Asset Viewer -->
|
||||
{#if $showAssetViewer}
|
||||
<AssetViewer
|
||||
{user}
|
||||
asset={selectedAsset}
|
||||
on:previous={navigateAssetBackward}
|
||||
on:next={navigateAssetForward}
|
||||
|
|
4
web/src/lib/stores/user.store.ts
Normal file
4
web/src/lib/stores/user.store.ts
Normal file
|
@ -0,0 +1,4 @@
|
|||
import { writable } from 'svelte/store';
|
||||
import type { UserResponseDto } from '@api';
|
||||
|
||||
export const user = writable<UserResponseDto | null>(null);
|
|
@ -1,6 +1,14 @@
|
|||
import { notificationController, NotificationType } from '$lib/components/shared-components/notification/notification';
|
||||
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';
|
||||
|
||||
export const addAssetsToAlbum = async (albumId: string, assetIds: Array<string>): Promise<BulkIdResponseDto[]> =>
|
||||
|
@ -203,3 +211,17 @@ export const getAssetType = (type: AssetTypeEnum) => {
|
|||
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;
|
||||
};
|
||||
|
|
|
@ -59,6 +59,7 @@
|
|||
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';
|
||||
|
||||
export let data: PageData;
|
||||
|
||||
|
@ -66,6 +67,9 @@
|
|||
let { slideshowState, slideshowShuffle } = slideshowStore;
|
||||
|
||||
let album = data.album;
|
||||
|
||||
$user = data.user;
|
||||
|
||||
$: album = data.album;
|
||||
|
||||
$: {
|
||||
|
@ -96,7 +100,6 @@
|
|||
let isShowActivity = false;
|
||||
let isLiked: ActivityResponseDto | null = null;
|
||||
let reactions: ActivityResponseDto[] = [];
|
||||
let user = data.user;
|
||||
let globalWidth: number;
|
||||
let assetGridWidth: number;
|
||||
|
||||
|
@ -179,10 +182,10 @@
|
|||
};
|
||||
|
||||
const getFavorite = async () => {
|
||||
if (user) {
|
||||
if ($user) {
|
||||
try {
|
||||
const { data } = await api.activityApi.getActivities({
|
||||
userId: user.id,
|
||||
userId: $user.id,
|
||||
albumId: album.id,
|
||||
type: ReactionType.Like,
|
||||
level: ReactionLevel.Album,
|
||||
|
@ -549,16 +552,10 @@
|
|||
style={`width:${assetGridWidth}px`}
|
||||
>
|
||||
{#if viewMode === ViewMode.SELECT_ASSETS}
|
||||
<AssetGrid
|
||||
user={data.user}
|
||||
assetStore={timelineStore}
|
||||
assetInteractionStore={timelineInteractionStore}
|
||||
isSelectionMode={true}
|
||||
/>
|
||||
<AssetGrid assetStore={timelineStore} assetInteractionStore={timelineInteractionStore} isSelectionMode={true} />
|
||||
{:else}
|
||||
<AssetGrid
|
||||
{album}
|
||||
user={data.user}
|
||||
{assetStore}
|
||||
{assetInteractionStore}
|
||||
isShared={album.sharedUsers.length > 0}
|
||||
|
@ -679,7 +676,7 @@
|
|||
{/if}
|
||||
</main>
|
||||
</div>
|
||||
{#if album.sharedUsers.length > 0 && album && isShowActivity && user && !$showAssetViewer}
|
||||
{#if album.sharedUsers.length > 0 && album && isShowActivity && $user && !$showAssetViewer}
|
||||
<div class="flex">
|
||||
<div
|
||||
transition:fly={{ duration: 150 }}
|
||||
|
@ -688,7 +685,7 @@
|
|||
translate="yes"
|
||||
>
|
||||
<ActivityViewer
|
||||
{user}
|
||||
user={$user}
|
||||
disabled={!album.isActivityEnabled}
|
||||
albumOwnerId={album.ownerId}
|
||||
albumId={album.id}
|
||||
|
@ -738,10 +735,10 @@
|
|||
</ConfirmDialogue>
|
||||
{/if}
|
||||
|
||||
{#if viewMode === ViewMode.OPTIONS}
|
||||
{#if viewMode === ViewMode.OPTIONS && $user}
|
||||
<AlbumOptions
|
||||
{album}
|
||||
{user}
|
||||
user={$user}
|
||||
on:close={() => (viewMode = ViewMode.VIEW)}
|
||||
on:toggleEnableActivity={handleToggleEnableActivity}
|
||||
on:showSelectSharedUser={() => (viewMode = ViewMode.SELECT_USERS)}
|
||||
|
|
|
@ -24,6 +24,7 @@
|
|||
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
||||
import { mdiDotsVertical, mdiPlus } from '@mdi/js';
|
||||
import UpdatePanel from '$lib/components/shared-components/update-panel.svelte';
|
||||
import { user } from '$lib/stores/user.store';
|
||||
|
||||
export let data: PageData;
|
||||
|
||||
|
@ -33,6 +34,8 @@
|
|||
const assetInteractionStore = createAssetInteractionStore();
|
||||
const { isMultiSelectState, selectedAssets } = assetInteractionStore;
|
||||
|
||||
$user = data.user;
|
||||
|
||||
$: isAllFavorite = Array.from($selectedAssets).every((asset) => asset.isFavorite);
|
||||
|
||||
const handleEscape = () => {
|
||||
|
|
Loading…
Reference in a new issue