feat: test menu dropdown, split text/media messages, target settings, provider URL links

- Replace 3 test buttons with unified dropdown menu (basic/periodic/scheduled/memory)
- Send text message first, then assets as reply (not combined caption+media)
- Pass all target config settings to Telegram client (disable_url_preview, max_media, chunk_delay, etc.)
- Real data test notifications for periodic/scheduled/memory (fetch from Immich)
- Provider card URL is now a clickable hyperlink
- Localized test type labels (EN/RU)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-20 16:34:25 +03:00
parent 03c5c66eed
commit 5015e378fe
9 changed files with 407 additions and 78 deletions
+47 -11
View File
@@ -176,13 +176,23 @@
} catch (err: any) { error = err.message; snackError(err.message); }
confirmDelete = null;
}
let testMenuOpen = $state<string | null>(null);
let testMenuStyle = $state('');
const testTypes = [
{ key: 'basic', icon: 'mdiSend', labelKey: 'trackers.testBasic' },
{ key: 'periodic', icon: 'mdiCalendarClock', labelKey: 'trackers.testPeriodic' },
{ key: 'scheduled', icon: 'mdiImageMultiple', labelKey: 'trackers.testScheduled' },
{ key: 'memory', icon: 'mdiHistory', labelKey: 'trackers.testMemory' },
];
async function testTrackerTarget(trackerId: number, ttId: number, testType: string) {
testMenuOpen = null;
const key = `${ttId}_${testType}`;
if (ttTesting[key]) return;
ttTesting = { ...ttTesting, [key]: testType };
try {
const endpoint = testType === 'basic' ? 'test' : `test-${testType}`;
await api(`/trackers/${trackerId}/targets/${ttId}/${endpoint}?locale=${getLocale()}`, { method: 'POST' });
await api(`/trackers/${trackerId}/targets/${ttId}/test/${testType}?locale=${getLocale()}`, { method: 'POST' });
snackSuccess(t('snack.targetTestSent'));
} catch (err: any) {
snackError(err.message);
@@ -190,6 +200,13 @@
ttTesting = { ...ttTesting, [key]: '' };
}
}
function openTestMenu(ttId: number, event: MouseEvent) {
const btn = event.currentTarget as HTMLElement;
const rect = btn.getBoundingClientRect();
testMenuStyle = `position:fixed; z-index:9999; top:${rect.bottom + 4}px; right:${window.innerWidth - rect.right}px;`;
testMenuOpen = testMenuOpen === String(ttId) ? null : String(ttId);
}
function toggleCollection(collectionId: string) { form.collection_ids = form.collection_ids.includes(collectionId) ? form.collection_ids.filter(id => id !== collectionId) : [...form.collection_ids, collectionId]; }
function formatDate(dateStr: string): string {
@@ -390,15 +407,11 @@
<option value={0}>— {t('templateConfig.title')} —</option>
{#each templateConfigs as tc}<option value={tc.id}>{tc.name}</option>{/each}
</select>
<IconButton icon="mdiSend" size={14} title={t('common.test')}
onclick={() => testTrackerTarget(tracker.id, tt.id, 'basic')}
disabled={!!ttTesting[`${tt.id}_basic`]} />
<IconButton icon="mdiCalendarClock" size={14} title={t('trackingConfig.periodicSummary')}
onclick={() => testTrackerTarget(tracker.id, tt.id, 'periodic')}
disabled={!!ttTesting[`${tt.id}_periodic`]} />
<IconButton icon="mdiHistory" size={14} title={t('trackingConfig.memoryMode')}
onclick={() => testTrackerTarget(tracker.id, tt.id, 'memory')}
disabled={!!ttTesting[`${tt.id}_memory`]} />
<div class="relative">
<IconButton icon="mdiDotsVertical" size={14} title={t('common.test')}
onclick={(e: MouseEvent) => openTestMenu(tt.id, e)}
disabled={Object.keys(ttTesting).some(k => k.startsWith(`${tt.id}_`) && ttTesting[k])} />
</div>
<IconButton icon={tt.enabled ? 'mdiPause' : 'mdiPlay'} size={14}
title={tt.enabled ? t('trackers.pause') : t('trackers.resume')}
onclick={() => updateTargetLink(tracker.id, tt, 'enabled', !tt.enabled)} />
@@ -442,6 +455,29 @@
{/if}
{/if}
{#if testMenuOpen}
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div style="position:fixed; top:0; left:0; right:0; bottom:0; z-index:9998;"
onclick={() => testMenuOpen = null}
onkeydown={(e) => { if (e.key === 'Escape') testMenuOpen = null; }}>
</div>
<div style="{testMenuStyle} background:var(--color-card); border:1px solid var(--color-border); border-radius:0.5rem; box-shadow:0 10px 25px rgba(0,0,0,0.3); padding:0.25rem; min-width:10rem;">
{#each testTypes as tt}
{@const trackerId = trackers.find(t => t.tracker_targets?.some((x: any) => String(x.id) === testMenuOpen))?.id}
<button
onclick={() => trackerId && testTrackerTarget(trackerId, Number(testMenuOpen), tt.key)}
disabled={!!ttTesting[`${testMenuOpen}_${tt.key}`]}
class="flex items-center gap-2 w-full px-3 py-1.5 text-sm rounded hover:bg-[var(--color-muted)] transition-colors disabled:opacity-50 text-left">
<MdiIcon name={tt.icon} size={14} />
{t(tt.labelKey)}
{#if ttTesting[`${testMenuOpen}_${tt.key}`]}
<span class="ml-auto text-xs text-[var(--color-muted-foreground)]">...</span>
{/if}
</button>
{/each}
</div>
{/if}
{#if linkWarning}
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div style="position:fixed; top:0; left:0; right:0; bottom:0; z-index:9998; background:rgba(0,0,0,0.5);"