Skip to content

Commit 63e38f3

Browse files
committed
pr feedback
1 parent 6222c4e commit 63e38f3

File tree

5 files changed

+174
-57
lines changed

5 files changed

+174
-57
lines changed
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import type { Attachment } from 'svelte/attachments';
2+
3+
export interface DragAndDropOptions {
4+
index: number;
5+
onDragStart?: (index: number) => void;
6+
onDragEnter?: (index: number) => void;
7+
onDrop?: (e: DragEvent, index: number) => void;
8+
onDragEnd?: () => void;
9+
isDragging?: boolean;
10+
isDragOver?: boolean;
11+
}
12+
13+
export function dragAndDrop(options: DragAndDropOptions): Attachment {
14+
return (node: Element) => {
15+
const element = node as HTMLElement;
16+
const { index, onDragStart, onDragEnter, onDrop, onDragEnd, isDragging, isDragOver } = options;
17+
18+
const isFormElement = (el: HTMLElement) => {
19+
return el.tagName === 'INPUT' || el.tagName === 'TEXTAREA' || el.tagName === 'SELECT';
20+
};
21+
22+
const handleDragStart = (e: DragEvent) => {
23+
// Prevent drag if it originated from an input, textarea, or select element
24+
const target = e.target as HTMLElement;
25+
if (isFormElement(target)) {
26+
e.preventDefault();
27+
return;
28+
}
29+
onDragStart?.(index);
30+
};
31+
32+
const handleDragEnter = () => {
33+
onDragEnter?.(index);
34+
};
35+
36+
const handleDragOver = (e: DragEvent) => {
37+
e.preventDefault();
38+
};
39+
40+
const handleDrop = (e: DragEvent) => {
41+
onDrop?.(e, index);
42+
};
43+
44+
const handleDragEnd = () => {
45+
onDragEnd?.();
46+
};
47+
48+
// Disable draggable when focusing on form elements (fixes Firefox input interaction)
49+
const handleFocusIn = (e: FocusEvent) => {
50+
const target = e.target as HTMLElement;
51+
if (isFormElement(target)) {
52+
element.setAttribute('draggable', 'false');
53+
}
54+
};
55+
56+
const handleFocusOut = (e: FocusEvent) => {
57+
const target = e.target as HTMLElement;
58+
if (isFormElement(target)) {
59+
element.setAttribute('draggable', 'true');
60+
}
61+
};
62+
63+
// Update classes based on drag state
64+
const updateClasses = (dragging: boolean, dragOver: boolean) => {
65+
// Remove all drag-related classes first
66+
element.classList.remove('opacity-50', 'border-light-500', 'border-solid');
67+
68+
// Add back only the active ones
69+
if (dragging) {
70+
element.classList.add('opacity-50');
71+
}
72+
73+
if (dragOver) {
74+
element.classList.add('border-light-500', 'border-solid');
75+
element.classList.remove('border-transparent');
76+
} else {
77+
element.classList.add('border-transparent');
78+
}
79+
};
80+
81+
element.setAttribute('draggable', 'true');
82+
element.setAttribute('role', 'button');
83+
element.setAttribute('tabindex', '0');
84+
85+
element.addEventListener('dragstart', handleDragStart);
86+
element.addEventListener('dragenter', handleDragEnter);
87+
element.addEventListener('dragover', handleDragOver);
88+
element.addEventListener('drop', handleDrop);
89+
element.addEventListener('dragend', handleDragEnd);
90+
element.addEventListener('focusin', handleFocusIn);
91+
element.addEventListener('focusout', handleFocusOut);
92+
93+
updateClasses(isDragging || false, isDragOver || false);
94+
95+
return () => {
96+
element.removeEventListener('dragstart', handleDragStart);
97+
element.removeEventListener('dragenter', handleDragEnter);
98+
element.removeEventListener('dragover', handleDragOver);
99+
element.removeEventListener('drop', handleDrop);
100+
element.removeEventListener('dragend', handleDragEnd);
101+
element.removeEventListener('focusin', handleFocusIn);
102+
element.removeEventListener('focusout', handleFocusOut);
103+
};
104+
};
105+
}

web/src/lib/components/workflows/WorkflowPickerField.svelte

Lines changed: 5 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
<script lang="ts">
2+
import WorkflowPickerItemCard from '$lib/components/workflows/WorkflowPickerItemCard.svelte';
23
import AlbumPickerModal from '$lib/modals/AlbumPickerModal.svelte';
34
import PeoplePickerModal from '$lib/modals/PeoplePickerModal.svelte';
4-
import { getAssetThumbnailUrl, getPeopleThumbnailUrl } from '$lib/utils';
55
import type { ComponentConfig } from '$lib/utils/workflow';
66
import { getAlbumInfo, getPerson, type AlbumResponseDto, type PersonResponseDto } from '@immich/sdk';
7-
import { Button, Card, CardBody, Field, IconButton, modalManager, Text } from '@immich/ui';
8-
import { mdiClose, mdiPlus } from '@mdi/js';
7+
import { Button, Field, modalManager } from '@immich/ui';
8+
import { mdiPlus } from '@mdi/js';
99
import { t } from 'svelte-i18n';
1010
1111
type Props = {
@@ -99,58 +99,14 @@
9999
};
100100
</script>
101101

102-
{#snippet pickerItemCard(item: AlbumResponseDto | PersonResponseDto, onRemove: () => void)}
103-
<Card color="secondary">
104-
<CardBody class="flex items-center gap-3">
105-
<div class="shrink-0">
106-
{#if isAlbum && 'albumThumbnailAssetId' in item}
107-
{#if item.albumThumbnailAssetId}
108-
<img
109-
src={getAssetThumbnailUrl(item.albumThumbnailAssetId)}
110-
alt={item.albumName}
111-
class="h-12 w-12 rounded-lg object-cover"
112-
/>
113-
{:else}
114-
<div class="h-12 w-12 rounded-lg"></div>
115-
{/if}
116-
{:else if !isAlbum && 'name' in item}
117-
<img src={getPeopleThumbnailUrl(item)} alt={item.name} class="h-12 w-12 rounded-full object-cover" />
118-
{/if}
119-
</div>
120-
<div class="min-w-0 flex-1">
121-
<Text class="font-semibold truncate">
122-
{isAlbum && 'albumName' in item ? item.albumName : 'name' in item ? item.name : ''}
123-
</Text>
124-
{#if isAlbum && 'assetCount' in item}
125-
<Text size="small" color="muted">
126-
{$t('items_count', { values: { count: item.assetCount } })}
127-
</Text>
128-
{/if}
129-
</div>
130-
131-
<IconButton
132-
type="button"
133-
onclick={onRemove}
134-
class="shrink-0"
135-
shape="round"
136-
aria-label={$t('remove')}
137-
icon={mdiClose}
138-
size="small"
139-
variant="ghost"
140-
color="secondary"
141-
/>
142-
</CardBody>
143-
</Card>
144-
{/snippet}
145-
146102
<Field {label} required={component.required} description={component.description} requiredIndicator={component.required}>
147103
<div class="flex flex-col gap-3">
148104
{#if pickerMetadata && !Array.isArray(pickerMetadata)}
149-
{@render pickerItemCard(pickerMetadata, removeSelection)}
105+
<WorkflowPickerItemCard item={pickerMetadata} {isAlbum} onRemove={removeSelection} />
150106
{:else if pickerMetadata && Array.isArray(pickerMetadata) && pickerMetadata.length > 0}
151107
<div class="flex flex-col gap-2">
152108
{#each pickerMetadata as item (item.id)}
153-
{@render pickerItemCard(item, () => removeItemFromSelection(item.id))}
109+
<WorkflowPickerItemCard {item} {isAlbum} onRemove={() => removeItemFromSelection(item.id)} />
154110
{/each}
155111
</div>
156112
{/if}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
<script lang="ts">
2+
import { getAssetThumbnailUrl, getPeopleThumbnailUrl } from '$lib/utils';
3+
import type { AlbumResponseDto, PersonResponseDto } from '@immich/sdk';
4+
import { Card, CardBody, IconButton, Text } from '@immich/ui';
5+
import { mdiClose } from '@mdi/js';
6+
import { t } from 'svelte-i18n';
7+
8+
type Props = {
9+
item: AlbumResponseDto | PersonResponseDto;
10+
isAlbum: boolean;
11+
onRemove: () => void;
12+
};
13+
14+
let { item, isAlbum, onRemove }: Props = $props();
15+
</script>
16+
17+
<Card color="secondary">
18+
<CardBody class="flex items-center gap-3">
19+
<div class="shrink-0">
20+
{#if isAlbum && 'albumThumbnailAssetId' in item}
21+
{#if item.albumThumbnailAssetId}
22+
<img
23+
src={getAssetThumbnailUrl(item.albumThumbnailAssetId)}
24+
alt={item.albumName}
25+
class="h-12 w-12 rounded-lg object-cover"
26+
/>
27+
{:else}
28+
<div class="h-12 w-12 rounded-lg"></div>
29+
{/if}
30+
{:else if !isAlbum && 'name' in item}
31+
<img src={getPeopleThumbnailUrl(item)} alt={item.name} class="h-12 w-12 rounded-full object-cover" />
32+
{/if}
33+
</div>
34+
<div class="min-w-0 flex-1">
35+
<Text class="font-semibold truncate">
36+
{isAlbum && 'albumName' in item ? item.albumName : 'name' in item ? item.name : ''}
37+
</Text>
38+
{#if isAlbum && 'assetCount' in item}
39+
<Text size="small" color="muted">
40+
{$t('items_count', { values: { count: item.assetCount } })}
41+
</Text>
42+
{/if}
43+
</div>
44+
45+
<IconButton
46+
type="button"
47+
onclick={onRemove}
48+
class="shrink-0"
49+
shape="round"
50+
aria-label={$t('remove')}
51+
icon={mdiClose}
52+
size="small"
53+
variant="ghost"
54+
color="secondary"
55+
/>
56+
</CardBody>
57+
</Card>

web/src/routes/(user)/utilities/workflows/+page.svelte

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,6 @@
248248
icon: mdiPencil,
249249
onAction: () => void handleEditWorkflow(workflow),
250250
},
251-
252251
{
253252
title: expandedWorkflows.has(workflow.id) ? $t('hide_schema') : $t('show_schema'),
254253
icon: mdiCodeJson,

web/src/routes/(user)/utilities/workflows/[workflowId]/+page.svelte

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<script lang="ts">
22
import { beforeNavigate, goto } from '$app/navigation';
3-
import { dragAndDrop } from '$lib/actions/drag-and-drop';
3+
import { dragAndDrop } from '$lib/attachments/drag-and-drop.svelte';
44
import ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte';
55
import SchemaFormFields from '$lib/components/workflows/SchemaFormFields.svelte';
66
import WorkflowCardConnector from '$lib/components/workflows/WorkflowCardConnector.svelte';
@@ -321,7 +321,7 @@
321321
</script>
322322

323323
{#snippet cardOrder(index: number)}
324-
<div class="h-8 w-8 rounded-lg flex place-items-center place-content-center shrink-0 border">
324+
<div class="h-8 w-8 rounded-lg flex place-items-center place-content-center shrink-0 border bg-light-50">
325325
<Text size="small" class="font-mono font-bold">
326326
{index + 1}
327327
</Text>
@@ -455,15 +455,15 @@
455455
{@render stepSeparator()}
456456
{/if}
457457
<div
458-
use:dragAndDrop={{
458+
{@attach dragAndDrop({
459459
index,
460460
onDragStart: handleFilterDragStart,
461461
onDragEnter: handleFilterDragEnter,
462462
onDrop: handleFilterDrop,
463463
onDragEnd: handleFilterDragEnd,
464464
isDragging: draggedFilterIndex === index,
465465
isDragOver: dragOverFilterIndex === index,
466-
}}
466+
})}
467467
class="mb-4 cursor-move rounded-2xl border-2 p-4 transition-all bg-light-50 border-dashed hover:border-light-300"
468468
>
469469
<div class="flex items-start gap-4">
@@ -524,16 +524,16 @@
524524
{@render stepSeparator()}
525525
{/if}
526526
<div
527-
use:dragAndDrop={{
527+
{@attach dragAndDrop({
528528
index,
529529
onDragStart: handleActionDragStart,
530530
onDragEnter: handleActionDragEnter,
531531
onDrop: handleActionDrop,
532532
onDragEnd: handleActionDragEnd,
533533
isDragging: draggedActionIndex === index,
534534
isDragOver: dragOverActionIndex === index,
535-
}}
536-
class="mb-4 cursor-move rounded-2xl border-2 p-4 transition-all bg-light-50 border-dashed hover:border-light-300"
535+
})}
536+
class="mb-4 cursor-move rounded-2xl border-2 p-4 transition-all bg-light-100 border-dashed hover:border-light-300"
537537
>
538538
<div class="flex items-start gap-4">
539539
{@render cardOrder(index)}

0 commit comments

Comments
 (0)