mirror of
https://github.com/immich-app/immich.git
synced 2026-05-18 03:10:24 +03:00
feat: sharing permissions
This commit is contained in:
@@ -26,12 +26,13 @@
|
||||
import { Route } from '$lib/route';
|
||||
import { getGlobalActions } from '$lib/services/app.service';
|
||||
import { getAssetActions } from '$lib/services/asset.service';
|
||||
import { getSharedLink, withoutIcons } from '$lib/utils';
|
||||
import { getSharedLink, hasPermissions, withoutIcons } from '$lib/utils';
|
||||
import type { OnUndoDelete } from '$lib/utils/actions';
|
||||
import { toTimelineAsset } from '$lib/utils/timeline-util';
|
||||
import {
|
||||
AssetTypeEnum,
|
||||
AssetVisibility,
|
||||
SharingPermission,
|
||||
type AlbumResponseDto,
|
||||
type AssetResponseDto,
|
||||
type PersonResponseDto,
|
||||
@@ -141,7 +142,7 @@
|
||||
|
||||
<ActionButton action={Actions.Edit} />
|
||||
|
||||
{#if isOwner}
|
||||
{#if hasPermissions(asset, SharingPermission.AssetDelete)}
|
||||
<DeleteAction {asset} {onAction} {preAction} {onUndoDelete} />
|
||||
{/if}
|
||||
|
||||
@@ -159,7 +160,7 @@
|
||||
{/if}
|
||||
|
||||
<ActionMenuItem action={Actions.AddToAlbum} />
|
||||
{#if album && (isOwner || isAlbumOwner)}
|
||||
{#if album && (hasPermissions(asset, SharingPermission.AssetShare) || isAlbumOwner)}
|
||||
<RemoveFromAlbumAction {album} onRemove={onRemoveFromAlbum} assetIds={[asset.id]} menuItem />
|
||||
{/if}
|
||||
|
||||
@@ -187,7 +188,7 @@
|
||||
{/if}
|
||||
|
||||
{#if !isLocked}
|
||||
{#if isOwner}
|
||||
{#if hasPermissions(asset, SharingPermission.AssetUpdate)}
|
||||
<ArchiveAction {asset} {onAction} {preAction} />
|
||||
{#if !asset.isArchived && !asset.isTrashed}
|
||||
<MenuOption
|
||||
@@ -217,7 +218,7 @@
|
||||
text={playOriginalVideo ? $t('play_transcoded_video') : $t('play_original_video')}
|
||||
/>
|
||||
{/if}
|
||||
{#if isOwner}
|
||||
{#if hasPermissions(asset, SharingPermission.AssetUpdate)}
|
||||
<hr />
|
||||
<ActionMenuItem action={Actions.RefreshFacesJob} />
|
||||
<ActionMenuItem action={Actions.RefreshMetadataJob} />
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import DetailPanelDate from '$lib/components/asset-viewer/DetailPanelDate.svelte';
|
||||
import DetailPanelDescription from '$lib/components/asset-viewer/DetailPanelDescription.svelte';
|
||||
import DetailPanelLocation from '$lib/components/asset-viewer/DetailPanelLocation.svelte';
|
||||
import DetailPanelPeople from '$lib/components/asset-viewer/DetailPanelPeople.svelte';
|
||||
import DetailPanelRating from '$lib/components/asset-viewer/DetailPanelStarRating.svelte';
|
||||
import DetailPanelTags from '$lib/components/asset-viewer/DetailPanelTags.svelte';
|
||||
import { timeToLoadTheMap } from '$lib/constants';
|
||||
@@ -11,7 +12,7 @@
|
||||
import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
|
||||
import { Route } from '$lib/route';
|
||||
import { locale } from '$lib/stores/preferences.store';
|
||||
import { getAssetMediaUrl } from '$lib/utils';
|
||||
import { getAssetMediaUrl, hasPermissions } from '$lib/utils';
|
||||
import { delay, getDimensions } from '$lib/utils/asset-utils';
|
||||
import { getByteUnitString } from '$lib/utils/byte-units';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
@@ -20,6 +21,7 @@
|
||||
AssetMediaSize,
|
||||
getAllAlbums,
|
||||
getAssetInfo,
|
||||
SharingPermission,
|
||||
type AlbumResponseDto,
|
||||
type AssetResponseDto,
|
||||
} from '@immich/sdk';
|
||||
@@ -32,7 +34,6 @@
|
||||
import OnEvents from '../OnEvents.svelte';
|
||||
import UserAvatar from '../shared-components/UserAvatar.svelte';
|
||||
import AlbumListItemDetails from './AlbumListItemDetails.svelte';
|
||||
import DetailPanelPeople from '$lib/components/asset-viewer/DetailPanelPeople.svelte';
|
||||
|
||||
interface Props {
|
||||
asset: AssetResponseDto;
|
||||
@@ -42,6 +43,7 @@
|
||||
let { asset, currentAlbum = null }: Props = $props();
|
||||
|
||||
let isOwner = $derived(authManager.authenticated && authManager.user.id === asset.ownerId);
|
||||
const allowExifUpdate = $derived(hasPermissions(asset, SharingPermission.AssetUpdate, SharingPermission.ExifRead));
|
||||
let latlng = $derived(
|
||||
(() => {
|
||||
const lat = asset.exifInfo?.latitude;
|
||||
@@ -147,9 +149,9 @@
|
||||
</section>
|
||||
{/if}
|
||||
|
||||
<DetailPanelDescription {asset} {isOwner} />
|
||||
<DetailPanelRating {asset} {isOwner} />
|
||||
<DetailPanelPeople {asset} {isOwner} {previousRoute} />
|
||||
<DetailPanelDescription {asset} {allowExifUpdate} />
|
||||
<DetailPanelRating {asset} {allowExifUpdate} />
|
||||
<DetailPanelPeople {asset} {previousRoute} />
|
||||
|
||||
<div class="p-4">
|
||||
{#if asset.exifInfo}
|
||||
@@ -160,7 +162,7 @@
|
||||
<Text size="small" color="muted">{$t('no_exif_info_available')}</Text>
|
||||
{/if}
|
||||
|
||||
<DetailPanelDate {asset} />
|
||||
<DetailPanelDate {asset} {allowExifUpdate} />
|
||||
|
||||
<div class="flex gap-4 py-4">
|
||||
<div><Icon icon={mdiImageOutline} size="24" /></div>
|
||||
@@ -168,7 +170,7 @@
|
||||
<div>
|
||||
<p class="flex place-items-center gap-2 break-all whitespace-pre-wrap">
|
||||
{asset.originalFileName}
|
||||
{#if isOwner}
|
||||
{#if allowExifUpdate}
|
||||
<IconButton
|
||||
icon={mdiInformationOutline}
|
||||
aria-label={$t('show_file_location')}
|
||||
@@ -271,7 +273,7 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<DetailPanelLocation {isOwner} {asset} />
|
||||
<DetailPanelLocation {allowExifUpdate} {asset} />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
@@ -10,9 +10,10 @@
|
||||
|
||||
type Props = {
|
||||
asset: AssetResponseDto;
|
||||
allowExifUpdate: boolean;
|
||||
};
|
||||
|
||||
const { asset }: Props = $props();
|
||||
const { asset, allowExifUpdate }: Props = $props();
|
||||
|
||||
const timeZone = $derived(asset.exifInfo?.timeZone ?? undefined);
|
||||
const dateTime = $derived(
|
||||
@@ -20,13 +21,8 @@
|
||||
? fromISODateTime(asset.exifInfo.dateTimeOriginal, timeZone)
|
||||
: fromISODateTimeUTC(asset.localDateTime),
|
||||
);
|
||||
const isOwner = $derived(authManager.authenticated && asset.ownerId === authManager.user.id);
|
||||
|
||||
const handleChangeDate = async () => {
|
||||
if (!isOwner) {
|
||||
return;
|
||||
}
|
||||
|
||||
await modalManager.show(AssetChangeDateModal, {
|
||||
asset: toTimelineAsset(asset),
|
||||
initialDate: dateTime,
|
||||
@@ -40,8 +36,8 @@
|
||||
type="button"
|
||||
class="flex w-full place-items-start justify-between gap-4 py-4 text-start"
|
||||
onclick={handleChangeDate}
|
||||
title={isOwner ? $t('edit_date') : ''}
|
||||
class:hover:text-primary={isOwner}
|
||||
title={allowExifUpdate ? $t('edit_date') : ''}
|
||||
class:hover:text-primary={allowExifUpdate}
|
||||
data-testid="detail-panel-edit-date-button"
|
||||
>
|
||||
<div class="flex gap-4">
|
||||
@@ -68,13 +64,13 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if isOwner}
|
||||
{#if allowExifUpdate}
|
||||
<div class="p-1">
|
||||
<Icon icon={mdiPencil} size="20" />
|
||||
</div>
|
||||
{/if}
|
||||
</button>
|
||||
{:else if !dateTime && isOwner}
|
||||
{:else if !dateTime && allowExifUpdate}
|
||||
<div class="flex place-items-start justify-between gap-4 py-4">
|
||||
<div class="flex gap-4">
|
||||
<Icon icon={mdiCalendar} size="24" />
|
||||
|
||||
@@ -8,10 +8,10 @@
|
||||
|
||||
interface Props {
|
||||
asset: AssetResponseDto;
|
||||
isOwner: boolean;
|
||||
allowExifUpdate: boolean;
|
||||
}
|
||||
|
||||
let { asset, isOwner }: Props = $props();
|
||||
let { asset, allowExifUpdate }: Props = $props();
|
||||
|
||||
let description = $derived(asset.exifInfo?.description ?? '');
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
};
|
||||
</script>
|
||||
|
||||
{#if isOwner}
|
||||
{#if allowExifUpdate}
|
||||
<section class="mt-10 px-4">
|
||||
<Textarea
|
||||
bind:value={description}
|
||||
|
||||
@@ -7,11 +7,11 @@
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
type Props = {
|
||||
isOwner: boolean;
|
||||
allowExifUpdate: boolean;
|
||||
asset: AssetResponseDto;
|
||||
};
|
||||
|
||||
let { isOwner, asset = $bindable() }: Props = $props();
|
||||
let { allowExifUpdate, asset = $bindable() }: Props = $props();
|
||||
|
||||
const onAction = async () => {
|
||||
const point = await modalManager.show(GeolocationPointPickerModal, { asset });
|
||||
@@ -34,9 +34,9 @@
|
||||
<button
|
||||
type="button"
|
||||
class="flex w-full place-items-start justify-between gap-4 py-4 text-start"
|
||||
onclick={isOwner ? onAction : undefined}
|
||||
title={isOwner ? $t('edit_location') : ''}
|
||||
class:hover:text-primary={isOwner}
|
||||
onclick={allowExifUpdate ? onAction : undefined}
|
||||
title={allowExifUpdate ? $t('edit_location') : ''}
|
||||
class:hover:text-primary={allowExifUpdate}
|
||||
>
|
||||
<div class="flex gap-4">
|
||||
<div><Icon icon={mdiMapMarkerOutline} size="24" /></div>
|
||||
@@ -58,13 +58,13 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if isOwner}
|
||||
{#if allowExifUpdate}
|
||||
<div>
|
||||
<Icon icon={mdiPencil} size="20" />
|
||||
</div>
|
||||
{/if}
|
||||
</button>
|
||||
{:else if !asset.exifInfo?.city && isOwner}
|
||||
{:else if !asset.exifInfo?.city && allowExifUpdate}
|
||||
<button
|
||||
type="button"
|
||||
class="flex w-full place-items-start justify-between gap-4 rounded-lg py-4 text-start hover:text-primary"
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
import { Route } from '$lib/route';
|
||||
import { faceManager } from '$lib/stores/face.svelte';
|
||||
import { locale } from '$lib/stores/preferences.store';
|
||||
import { getPeopleThumbnailUrl } from '$lib/utils';
|
||||
import { type AssetResponseDto } from '@immich/sdk';
|
||||
import { getPeopleThumbnailUrl, hasPermissions } from '$lib/utils';
|
||||
import { SharingPermission, type AssetResponseDto } from '@immich/sdk';
|
||||
import { IconButton, Text } from '@immich/ui';
|
||||
import { mdiEye, mdiEyeOff, mdiPencil, mdiPlus } from '@mdi/js';
|
||||
import { DateTime } from 'luxon';
|
||||
@@ -14,11 +14,10 @@
|
||||
|
||||
type Props = {
|
||||
asset: AssetResponseDto;
|
||||
isOwner: boolean;
|
||||
previousRoute: string;
|
||||
};
|
||||
|
||||
const { asset, isOwner, previousRoute }: Props = $props();
|
||||
const { asset, previousRoute }: Props = $props();
|
||||
|
||||
const people = $derived(Array.from(faceManager.people));
|
||||
const visiblePeople = $derived(
|
||||
@@ -56,7 +55,7 @@
|
||||
);
|
||||
</script>
|
||||
|
||||
{#if !authManager.isSharedLink && isOwner}
|
||||
{#if !authManager.isSharedLink && hasPermissions(asset, SharingPermission.PersonRead)}
|
||||
<section class="px-4 pt-4 text-sm">
|
||||
<div class="flex h-10 w-full items-center justify-between">
|
||||
<Text size="small" color="muted">{$t('people')}</Text>
|
||||
|
||||
@@ -8,10 +8,10 @@
|
||||
|
||||
interface Props {
|
||||
asset: AssetResponseDto;
|
||||
isOwner: boolean;
|
||||
allowExifUpdate: boolean;
|
||||
}
|
||||
|
||||
let { asset, isOwner }: Props = $props();
|
||||
let { asset, allowExifUpdate }: Props = $props();
|
||||
|
||||
let rating = $derived(asset.exifInfo?.rating || null) as Rating;
|
||||
|
||||
@@ -26,6 +26,10 @@
|
||||
|
||||
{#if !authManager.isSharedLink && authManager.authenticated && authManager.preferences.ratings.enabled}
|
||||
<section class="px-4 pt-4">
|
||||
<StarRating {rating} readOnly={!isOwner} onRating={(rating) => handlePromiseError(handleChangeRating(rating))} />
|
||||
<StarRating
|
||||
{rating}
|
||||
readOnly={!allowExifUpdate}
|
||||
onRating={(rating) => handlePromiseError(handleChangeRating(rating))}
|
||||
/>
|
||||
</section>
|
||||
{/if}
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
{ title: $t('admin.memory_cleanup_job'), value: ManualJobName.MemoryCleanup },
|
||||
{ title: $t('admin.memory_generate_job'), value: ManualJobName.MemoryCreate },
|
||||
{ title: $t('admin.backup_database'), value: ManualJobName.BackupDatabase },
|
||||
{ title: 'Person grouping', value: ManualJobName.PersonGroupMerge },
|
||||
].map(({ value, title }) => ({ id: value, label: title, value }));
|
||||
|
||||
let selectedJob: ComboBoxOption | undefined = $state(undefined);
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
try {
|
||||
await mergePerson({
|
||||
id: personToBeMergedInto.id,
|
||||
mergePersonDto: { ids: [personToMerge.id] },
|
||||
mergeFaceClusterDto: { ids: [personToMerge.id] },
|
||||
});
|
||||
toastManager.primary($t('merge_people_successfully'));
|
||||
onClose([personToMerge, personToBeMergedInto]);
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
<script lang="ts">
|
||||
import { getOwnAlbumUser, SharingPermission, updateOwnAlbumUser } from '@immich/sdk';
|
||||
import { Checkbox, Field, FormModal, Heading, Stack, Switch, toastManager } from '@immich/ui';
|
||||
import { onMount } from 'svelte';
|
||||
import { init } from 'svelte-i18n';
|
||||
|
||||
type Props = {
|
||||
onClose: () => void;
|
||||
albumId?: string;
|
||||
partnerId?: string;
|
||||
};
|
||||
|
||||
const { onClose, ...rest }: Props = $props();
|
||||
|
||||
let checkedPermissions = $state<SharingPermission[]>([]);
|
||||
let viewInTimeline = $state<boolean>(false);
|
||||
|
||||
const onCheckedChange = (permission: SharingPermission, checked: boolean) => {
|
||||
if (checked) {
|
||||
checkedPermissions.push(permission);
|
||||
} else {
|
||||
checkedPermissions = checkedPermissions.filter((perm) => perm !== permission);
|
||||
}
|
||||
};
|
||||
|
||||
const onSubmit = async () => {
|
||||
const permissions =
|
||||
checkedPermissions.length === Object.values(SharingPermission).length - 1
|
||||
? [SharingPermission.All]
|
||||
: checkedPermissions;
|
||||
if (rest.albumId) {
|
||||
await updateOwnAlbumUser({
|
||||
id: rest.albumId,
|
||||
updateSharingOptionsDto: { permissions, inTimeline: viewInTimeline },
|
||||
});
|
||||
toastManager.success();
|
||||
}
|
||||
onClose();
|
||||
};
|
||||
|
||||
onMount(async () => {
|
||||
if (rest.albumId) {
|
||||
const { permissions, inTimeline } = await getOwnAlbumUser({ id: rest.albumId });
|
||||
checkedPermissions = permissions;
|
||||
viewInTimeline = inTimeline;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<FormModal title="Sharing options" {onClose} {onSubmit}>
|
||||
<Stack>
|
||||
<Field label="View in timeline">
|
||||
<Switch bind:checked={viewInTimeline} />
|
||||
</Field>
|
||||
<Heading>Permissions</Heading>
|
||||
<Field label={SharingPermission.All}>
|
||||
<Checkbox
|
||||
id="permission-{SharingPermission.All}"
|
||||
checked={checkedPermissions.length === Object.values(SharingPermission).length - 1}
|
||||
onCheckedChange={(checked) =>
|
||||
checked
|
||||
? (checkedPermissions = Object.values(SharingPermission).filter(
|
||||
(permission) => permission !== SharingPermission.All,
|
||||
))
|
||||
: (checkedPermissions = [])}
|
||||
/>
|
||||
</Field>
|
||||
{#each Object.values(SharingPermission).filter((permission) => permission !== SharingPermission.All) as permission (permission)}
|
||||
<Field label={permission}>
|
||||
<Checkbox
|
||||
id="permission-{permission}"
|
||||
checked={checkedPermissions.includes(permission)}
|
||||
onCheckedChange={(checked) => onCheckedChange(permission, checked)}
|
||||
/>
|
||||
</Field>
|
||||
{/each}
|
||||
</Stack>
|
||||
</FormModal>
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
AssetVisibility,
|
||||
getAssetInfo,
|
||||
runAssetJobs,
|
||||
SharingPermission,
|
||||
updateAsset,
|
||||
type AssetJobsDto,
|
||||
type AssetResponseDto,
|
||||
@@ -41,7 +42,7 @@ import { eventManager } from '$lib/managers/event-manager.svelte';
|
||||
import AssetAddToAlbumModal from '$lib/modals/AssetAddToAlbumModal.svelte';
|
||||
import AssetTagModal from '$lib/modals/AssetTagModal.svelte';
|
||||
import SharedLinkCreateModal from '$lib/modals/SharedLinkCreateModal.svelte';
|
||||
import { getAssetMediaUrl, getSharedLink, sleep } from '$lib/utils';
|
||||
import { getAssetMediaUrl, getSharedLink, hasPermissions, sleep } from '$lib/utils';
|
||||
import { downloadUrl } from '$lib/utils/asset-utils';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { getFormatter } from '$lib/utils/i18n';
|
||||
@@ -98,7 +99,12 @@ export const getAssetActions = ($t: MessageFormatter, asset: AssetResponseDto) =
|
||||
const Share: ActionItem = {
|
||||
title: $t('share'),
|
||||
icon: mdiShareVariantOutline,
|
||||
$if: () => !!(authUser && !asset.isTrashed && asset.visibility !== AssetVisibility.Locked),
|
||||
$if: () =>
|
||||
!!(
|
||||
hasPermissions(asset, SharingPermission.AssetShare) &&
|
||||
!asset.isTrashed &&
|
||||
asset.visibility !== AssetVisibility.Locked
|
||||
),
|
||||
onAction: () => modalManager.show(SharedLinkCreateModal, { assetIds: [asset.id] }),
|
||||
};
|
||||
|
||||
@@ -119,7 +125,7 @@ export const getAssetActions = ($t: MessageFormatter, asset: AssetResponseDto) =
|
||||
|
||||
const SharedLinkDownload: ActionItem = {
|
||||
...Download,
|
||||
$if: () => isOwner || !!sharedLink?.allowDownload,
|
||||
$if: () => hasPermissions(asset, SharingPermission.AssetShare) || !!sharedLink?.allowDownload,
|
||||
};
|
||||
|
||||
const PlayMotionPhoto: ActionItem = {
|
||||
@@ -222,7 +228,7 @@ export const getAssetActions = ($t: MessageFormatter, asset: AssetResponseDto) =
|
||||
icon: mdiTune,
|
||||
$if: () =>
|
||||
!sharedLink &&
|
||||
isOwner &&
|
||||
hasPermissions(asset, SharingPermission.AssetEdit) &&
|
||||
asset.type === AssetTypeEnum.Image &&
|
||||
!asset.livePhotoVideoId &&
|
||||
asset.exifInfo?.projectionType !== ProjectionType.EQUIRECTANGULAR &&
|
||||
|
||||
@@ -2,6 +2,7 @@ import {
|
||||
AssetMediaSize,
|
||||
AssetTypeEnum,
|
||||
MemoryType,
|
||||
SharingPermission,
|
||||
finishOAuth,
|
||||
getAssetOriginalPath,
|
||||
getAssetPlaybackPath,
|
||||
@@ -413,3 +414,17 @@ export const transformToTitleCase = (text: string) => {
|
||||
}
|
||||
return result.trim();
|
||||
};
|
||||
|
||||
export const hasPermissions = (asset: AssetResponseDto, ...permissions: SharingPermission[]) => {
|
||||
if (asset.permissions.includes(SharingPermission.All)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (const permission of permissions) {
|
||||
if (!asset.permissions.includes(permission)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
+12
-3
@@ -73,11 +73,13 @@
|
||||
mdiLink,
|
||||
mdiPlus,
|
||||
mdiPresentationPlay,
|
||||
mdiShareVariant,
|
||||
} from '@mdi/js';
|
||||
import { onDestroy } from 'svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
import { fly } from 'svelte/transition';
|
||||
import type { PageData } from './$types';
|
||||
import SharingOptionsModal from '$lib/modals/SharingOptionsModal.svelte';
|
||||
|
||||
interface Props {
|
||||
data: PageData;
|
||||
@@ -405,9 +407,16 @@
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
{#if isOwned}
|
||||
<ActionButton action={Share} />
|
||||
{/if}
|
||||
<IconButton
|
||||
shape="round"
|
||||
aria-label="Sharing permissions"
|
||||
color="secondary"
|
||||
size="medium"
|
||||
icon={mdiShareVariant}
|
||||
onclick={() => modalManager.show(SharingOptionsModal, { albumId: album.id })}
|
||||
/>
|
||||
|
||||
<ActionButton action={Share} />
|
||||
</div>
|
||||
{/if}
|
||||
<AlbumDescription
|
||||
|
||||
@@ -231,7 +231,7 @@
|
||||
}
|
||||
|
||||
const personWithSimilarName = await findPeopleWithSimilarName(name, targetPerson.id);
|
||||
if (personWithSimilarName) {
|
||||
if (personWithSimilarName && personWithSimilarName.faceClusterId !== targetPerson.faceClusterId) {
|
||||
personMerge1 = targetPerson;
|
||||
personMerge2 = personWithSimilarName;
|
||||
potentialMergePeople = people
|
||||
|
||||
+1
-1
@@ -68,7 +68,7 @@
|
||||
try {
|
||||
let results = await mergePerson({
|
||||
id: person.id,
|
||||
mergePersonDto: { ids: selectedPeople.map(({ id }) => id) },
|
||||
mergeFaceClusterDto: { ids: selectedPeople.map(({ id }) => id) },
|
||||
});
|
||||
const mergedPerson = await getPerson({ id: person.id });
|
||||
const count = results.filter(({ success }) => success).length;
|
||||
|
||||
Reference in New Issue
Block a user