pr feedback

This commit is contained in:
Alex Tran
2025-12-05 16:07:37 +00:00
parent 6222c4e97f
commit 63e38f347e
5 changed files with 174 additions and 57 deletions

View File

@@ -0,0 +1,105 @@
import type { Attachment } from 'svelte/attachments';
export interface DragAndDropOptions {
index: number;
onDragStart?: (index: number) => void;
onDragEnter?: (index: number) => void;
onDrop?: (e: DragEvent, index: number) => void;
onDragEnd?: () => void;
isDragging?: boolean;
isDragOver?: boolean;
}
export function dragAndDrop(options: DragAndDropOptions): Attachment {
return (node: Element) => {
const element = node as HTMLElement;
const { index, onDragStart, onDragEnter, onDrop, onDragEnd, isDragging, isDragOver } = options;
const isFormElement = (el: HTMLElement) => {
return el.tagName === 'INPUT' || el.tagName === 'TEXTAREA' || el.tagName === 'SELECT';
};
const handleDragStart = (e: DragEvent) => {
// Prevent drag if it originated from an input, textarea, or select element
const target = e.target as HTMLElement;
if (isFormElement(target)) {
e.preventDefault();
return;
}
onDragStart?.(index);
};
const handleDragEnter = () => {
onDragEnter?.(index);
};
const handleDragOver = (e: DragEvent) => {
e.preventDefault();
};
const handleDrop = (e: DragEvent) => {
onDrop?.(e, index);
};
const handleDragEnd = () => {
onDragEnd?.();
};
// Disable draggable when focusing on form elements (fixes Firefox input interaction)
const handleFocusIn = (e: FocusEvent) => {
const target = e.target as HTMLElement;
if (isFormElement(target)) {
element.setAttribute('draggable', 'false');
}
};
const handleFocusOut = (e: FocusEvent) => {
const target = e.target as HTMLElement;
if (isFormElement(target)) {
element.setAttribute('draggable', 'true');
}
};
// Update classes based on drag state
const updateClasses = (dragging: boolean, dragOver: boolean) => {
// Remove all drag-related classes first
element.classList.remove('opacity-50', 'border-light-500', 'border-solid');
// Add back only the active ones
if (dragging) {
element.classList.add('opacity-50');
}
if (dragOver) {
element.classList.add('border-light-500', 'border-solid');
element.classList.remove('border-transparent');
} else {
element.classList.add('border-transparent');
}
};
element.setAttribute('draggable', 'true');
element.setAttribute('role', 'button');
element.setAttribute('tabindex', '0');
element.addEventListener('dragstart', handleDragStart);
element.addEventListener('dragenter', handleDragEnter);
element.addEventListener('dragover', handleDragOver);
element.addEventListener('drop', handleDrop);
element.addEventListener('dragend', handleDragEnd);
element.addEventListener('focusin', handleFocusIn);
element.addEventListener('focusout', handleFocusOut);
updateClasses(isDragging || false, isDragOver || false);
return () => {
element.removeEventListener('dragstart', handleDragStart);
element.removeEventListener('dragenter', handleDragEnter);
element.removeEventListener('dragover', handleDragOver);
element.removeEventListener('drop', handleDrop);
element.removeEventListener('dragend', handleDragEnd);
element.removeEventListener('focusin', handleFocusIn);
element.removeEventListener('focusout', handleFocusOut);
};
};
}

View File

@@ -1,11 +1,11 @@
<script lang="ts"> <script lang="ts">
import WorkflowPickerItemCard from '$lib/components/workflows/WorkflowPickerItemCard.svelte';
import AlbumPickerModal from '$lib/modals/AlbumPickerModal.svelte'; import AlbumPickerModal from '$lib/modals/AlbumPickerModal.svelte';
import PeoplePickerModal from '$lib/modals/PeoplePickerModal.svelte'; import PeoplePickerModal from '$lib/modals/PeoplePickerModal.svelte';
import { getAssetThumbnailUrl, getPeopleThumbnailUrl } from '$lib/utils';
import type { ComponentConfig } from '$lib/utils/workflow'; import type { ComponentConfig } from '$lib/utils/workflow';
import { getAlbumInfo, getPerson, type AlbumResponseDto, type PersonResponseDto } from '@immich/sdk'; import { getAlbumInfo, getPerson, type AlbumResponseDto, type PersonResponseDto } from '@immich/sdk';
import { Button, Card, CardBody, Field, IconButton, modalManager, Text } from '@immich/ui'; import { Button, Field, modalManager } from '@immich/ui';
import { mdiClose, mdiPlus } from '@mdi/js'; import { mdiPlus } from '@mdi/js';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
type Props = { type Props = {
@@ -99,58 +99,14 @@
}; };
</script> </script>
{#snippet pickerItemCard(item: AlbumResponseDto | PersonResponseDto, onRemove: () => void)}
<Card color="secondary">
<CardBody class="flex items-center gap-3">
<div class="shrink-0">
{#if isAlbum && 'albumThumbnailAssetId' in item}
{#if item.albumThumbnailAssetId}
<img
src={getAssetThumbnailUrl(item.albumThumbnailAssetId)}
alt={item.albumName}
class="h-12 w-12 rounded-lg object-cover"
/>
{:else}
<div class="h-12 w-12 rounded-lg"></div>
{/if}
{:else if !isAlbum && 'name' in item}
<img src={getPeopleThumbnailUrl(item)} alt={item.name} class="h-12 w-12 rounded-full object-cover" />
{/if}
</div>
<div class="min-w-0 flex-1">
<Text class="font-semibold truncate">
{isAlbum && 'albumName' in item ? item.albumName : 'name' in item ? item.name : ''}
</Text>
{#if isAlbum && 'assetCount' in item}
<Text size="small" color="muted">
{$t('items_count', { values: { count: item.assetCount } })}
</Text>
{/if}
</div>
<IconButton
type="button"
onclick={onRemove}
class="shrink-0"
shape="round"
aria-label={$t('remove')}
icon={mdiClose}
size="small"
variant="ghost"
color="secondary"
/>
</CardBody>
</Card>
{/snippet}
<Field {label} required={component.required} description={component.description} requiredIndicator={component.required}> <Field {label} required={component.required} description={component.description} requiredIndicator={component.required}>
<div class="flex flex-col gap-3"> <div class="flex flex-col gap-3">
{#if pickerMetadata && !Array.isArray(pickerMetadata)} {#if pickerMetadata && !Array.isArray(pickerMetadata)}
{@render pickerItemCard(pickerMetadata, removeSelection)} <WorkflowPickerItemCard item={pickerMetadata} {isAlbum} onRemove={removeSelection} />
{:else if pickerMetadata && Array.isArray(pickerMetadata) && pickerMetadata.length > 0} {:else if pickerMetadata && Array.isArray(pickerMetadata) && pickerMetadata.length > 0}
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
{#each pickerMetadata as item (item.id)} {#each pickerMetadata as item (item.id)}
{@render pickerItemCard(item, () => removeItemFromSelection(item.id))} <WorkflowPickerItemCard {item} {isAlbum} onRemove={() => removeItemFromSelection(item.id)} />
{/each} {/each}
</div> </div>
{/if} {/if}

View File

@@ -0,0 +1,57 @@
<script lang="ts">
import { getAssetThumbnailUrl, getPeopleThumbnailUrl } from '$lib/utils';
import type { AlbumResponseDto, PersonResponseDto } from '@immich/sdk';
import { Card, CardBody, IconButton, Text } from '@immich/ui';
import { mdiClose } from '@mdi/js';
import { t } from 'svelte-i18n';
type Props = {
item: AlbumResponseDto | PersonResponseDto;
isAlbum: boolean;
onRemove: () => void;
};
let { item, isAlbum, onRemove }: Props = $props();
</script>
<Card color="secondary">
<CardBody class="flex items-center gap-3">
<div class="shrink-0">
{#if isAlbum && 'albumThumbnailAssetId' in item}
{#if item.albumThumbnailAssetId}
<img
src={getAssetThumbnailUrl(item.albumThumbnailAssetId)}
alt={item.albumName}
class="h-12 w-12 rounded-lg object-cover"
/>
{:else}
<div class="h-12 w-12 rounded-lg"></div>
{/if}
{:else if !isAlbum && 'name' in item}
<img src={getPeopleThumbnailUrl(item)} alt={item.name} class="h-12 w-12 rounded-full object-cover" />
{/if}
</div>
<div class="min-w-0 flex-1">
<Text class="font-semibold truncate">
{isAlbum && 'albumName' in item ? item.albumName : 'name' in item ? item.name : ''}
</Text>
{#if isAlbum && 'assetCount' in item}
<Text size="small" color="muted">
{$t('items_count', { values: { count: item.assetCount } })}
</Text>
{/if}
</div>
<IconButton
type="button"
onclick={onRemove}
class="shrink-0"
shape="round"
aria-label={$t('remove')}
icon={mdiClose}
size="small"
variant="ghost"
color="secondary"
/>
</CardBody>
</Card>

View File

@@ -248,7 +248,6 @@
icon: mdiPencil, icon: mdiPencil,
onAction: () => void handleEditWorkflow(workflow), onAction: () => void handleEditWorkflow(workflow),
}, },
{ {
title: expandedWorkflows.has(workflow.id) ? $t('hide_schema') : $t('show_schema'), title: expandedWorkflows.has(workflow.id) ? $t('hide_schema') : $t('show_schema'),
icon: mdiCodeJson, icon: mdiCodeJson,

View File

@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { beforeNavigate, goto } from '$app/navigation'; import { beforeNavigate, goto } from '$app/navigation';
import { dragAndDrop } from '$lib/actions/drag-and-drop'; import { dragAndDrop } from '$lib/attachments/drag-and-drop.svelte';
import ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte'; import ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte';
import SchemaFormFields from '$lib/components/workflows/SchemaFormFields.svelte'; import SchemaFormFields from '$lib/components/workflows/SchemaFormFields.svelte';
import WorkflowCardConnector from '$lib/components/workflows/WorkflowCardConnector.svelte'; import WorkflowCardConnector from '$lib/components/workflows/WorkflowCardConnector.svelte';
@@ -321,7 +321,7 @@
</script> </script>
{#snippet cardOrder(index: number)} {#snippet cardOrder(index: number)}
<div class="h-8 w-8 rounded-lg flex place-items-center place-content-center shrink-0 border"> <div class="h-8 w-8 rounded-lg flex place-items-center place-content-center shrink-0 border bg-light-50">
<Text size="small" class="font-mono font-bold"> <Text size="small" class="font-mono font-bold">
{index + 1} {index + 1}
</Text> </Text>
@@ -455,7 +455,7 @@
{@render stepSeparator()} {@render stepSeparator()}
{/if} {/if}
<div <div
use:dragAndDrop={{ {@attach dragAndDrop({
index, index,
onDragStart: handleFilterDragStart, onDragStart: handleFilterDragStart,
onDragEnter: handleFilterDragEnter, onDragEnter: handleFilterDragEnter,
@@ -463,7 +463,7 @@
onDragEnd: handleFilterDragEnd, onDragEnd: handleFilterDragEnd,
isDragging: draggedFilterIndex === index, isDragging: draggedFilterIndex === index,
isDragOver: dragOverFilterIndex === index, isDragOver: dragOverFilterIndex === index,
}} })}
class="mb-4 cursor-move rounded-2xl border-2 p-4 transition-all bg-light-50 border-dashed hover:border-light-300" class="mb-4 cursor-move rounded-2xl border-2 p-4 transition-all bg-light-50 border-dashed hover:border-light-300"
> >
<div class="flex items-start gap-4"> <div class="flex items-start gap-4">
@@ -524,7 +524,7 @@
{@render stepSeparator()} {@render stepSeparator()}
{/if} {/if}
<div <div
use:dragAndDrop={{ {@attach dragAndDrop({
index, index,
onDragStart: handleActionDragStart, onDragStart: handleActionDragStart,
onDragEnter: handleActionDragEnter, onDragEnter: handleActionDragEnter,
@@ -532,8 +532,8 @@
onDragEnd: handleActionDragEnd, onDragEnd: handleActionDragEnd,
isDragging: draggedActionIndex === index, isDragging: draggedActionIndex === index,
isDragOver: dragOverActionIndex === index, isDragOver: dragOverActionIndex === index,
}} })}
class="mb-4 cursor-move rounded-2xl border-2 p-4 transition-all bg-light-50 border-dashed hover:border-light-300" class="mb-4 cursor-move rounded-2xl border-2 p-4 transition-all bg-light-100 border-dashed hover:border-light-300"
> >
<div class="flex items-start gap-4"> <div class="flex items-start gap-4">
{@render cardOrder(index)} {@render cardOrder(index)}