Add batch upload configure modal and settings mapping utilities.

Users configure share mode, expiry, and max views before upload; batch summary confirms choices above the queue.
This commit is contained in:
2026-10-22 03:14:02 +02:00
parent 3bb3258686
commit 805665745b
23 changed files with 637 additions and 7 deletions
@@ -0,0 +1,74 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import type { UploadBatchSettingsForm } from '@/types/upload/UploadBatchSettingsForm';
import { buildZkUploadPrepareOptionsFromBatchSettings } from '@/utils/upload/buildZkUploadPrepareOptionsFromBatchSettings';
const baseSettings: UploadBatchSettingsForm = {
mode: 'zk',
expiryPreset: 'never',
maxViewsPreset: 'unlimited',
passwordEnabled: false,
password: ''
};
describe('buildZkUploadPrepareOptionsFromBatchSettings', () => {
beforeEach(() => {
vi.useFakeTimers();
vi.setSystemTime(new Date('2026-06-01T00:00:00.000Z'));
});
afterEach(() => {
vi.useRealTimers();
});
it('builds non-password options', () => {
const settings = {
...baseSettings,
expiryPreset: '24h' as const,
maxViewsPreset: '10' as const
};
const result = buildZkUploadPrepareOptionsFromBatchSettings(settings);
expect(result).toEqual({
ok: true,
options: {
expiresAt: '2026-06-02T00:00:00.000Z',
maxViews: 10,
passwordProtected: false
}
});
});
it('builds password-protected options', () => {
const settings = {
...baseSettings,
passwordEnabled: true,
password: 'batch-secret'
};
const result = buildZkUploadPrepareOptionsFromBatchSettings(settings);
expect(result).toEqual({
ok: true,
options: {
expiresAt: null,
maxViews: null,
passwordProtected: true,
password: 'batch-secret'
}
});
});
it('returns validation errors', () => {
const settings = {
...baseSettings,
passwordEnabled: true,
password: ''
};
expect(buildZkUploadPrepareOptionsFromBatchSettings(settings)).toEqual({
ok: false,
error: 'Enter a password or turn off password protection.'
});
});
});
@@ -0,0 +1,39 @@
import type { UploadBatchSettingsForm } from '@/types/upload/UploadBatchSettingsForm';
import type { ZkUploadPrepareOptionsResult } from '@/types/upload/ZkUploadPrepareOptionsResult';
import { resolveUploadBatchExpiresAt } from '@/utils/upload/resolveUploadBatchExpiresAt';
import { resolveUploadBatchMaxViews } from '@/utils/upload/resolveUploadBatchMaxViews';
import { validateUploadBatchSettings } from '@/utils/upload/validateUploadBatchSettings';
export const buildZkUploadPrepareOptionsFromBatchSettings = (
settings: UploadBatchSettingsForm
): ZkUploadPrepareOptionsResult => {
const validationError = validateUploadBatchSettings(settings);
if (validationError) {
return { ok: false, error: validationError };
}
const uploadOptions = {
expiresAt: resolveUploadBatchExpiresAt(settings.expiryPreset),
maxViews: resolveUploadBatchMaxViews(settings.maxViewsPreset)
};
if (settings.passwordEnabled) {
return {
ok: true,
options: {
...uploadOptions,
passwordProtected: true,
password: settings.password
}
};
}
return {
ok: true,
options: {
...uploadOptions,
passwordProtected: false
}
};
};
@@ -0,0 +1,42 @@
import { describe, expect, it } from 'vitest';
import type { UploadBatchSettingsForm } from '@/types/upload/UploadBatchSettingsForm';
import { formatUploadBatchSettingsSummary } from '@/utils/upload/formatUploadBatchSettingsSummary';
const baseSettings: UploadBatchSettingsForm = {
mode: 'zk',
expiryPreset: 'never',
maxViewsPreset: 'unlimited',
passwordEnabled: false,
password: ''
};
describe('formatUploadBatchSettingsSummary', () => {
it('formats default zk settings', () => {
expect(formatUploadBatchSettingsSummary(baseSettings)).toBe(
'Zero knowledge · Never expires · Unlimited views'
);
});
it('includes password when enabled', () => {
const settings = {
...baseSettings,
passwordEnabled: true,
password: 'secret'
};
expect(formatUploadBatchSettingsSummary(settings)).toBe(
'Zero knowledge · Never expires · Unlimited views · Password protected'
);
});
it('formats hotlink mode', () => {
const settings = {
...baseSettings,
mode: 'hotlink' as const,
expiryPreset: '24h' as const,
maxViewsPreset: '10' as const
};
expect(formatUploadBatchSettingsSummary(settings)).toBe('Hotlink · 24 hours · 10 views');
});
});
@@ -0,0 +1,29 @@
import type { UploadBatchSettingsForm } from '@/types/upload/UploadBatchSettingsForm';
const EXPIRY_LABELS: Record<UploadBatchSettingsForm['expiryPreset'], string> = {
never: 'Never expires',
'1h': '1 hour',
'24h': '24 hours',
'7d': '7 days',
'30d': '30 days'
};
const MAX_VIEWS_LABELS: Record<UploadBatchSettingsForm['maxViewsPreset'], string> = {
unlimited: 'Unlimited views',
'1': 'Burn after read',
'10': '10 views',
'100': '100 views',
'1000': '1,000 views',
'10000': '10,000 views'
};
export const formatUploadBatchSettingsSummary = (settings: UploadBatchSettingsForm): string => {
const modeLabel = settings.mode === 'zk' ? 'Zero knowledge' : 'Hotlink';
const parts = [modeLabel, EXPIRY_LABELS[settings.expiryPreset], MAX_VIEWS_LABELS[settings.maxViewsPreset]];
if (settings.mode === 'zk' && settings.passwordEnabled) {
parts.push('Password protected');
}
return parts.join(' · ');
};
@@ -0,0 +1,24 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { resolveUploadBatchExpiresAt } from '@/utils/upload/resolveUploadBatchExpiresAt';
describe('resolveUploadBatchExpiresAt', () => {
beforeEach(() => {
vi.useFakeTimers();
vi.setSystemTime(new Date('2026-01-01T12:00:00.000Z'));
});
afterEach(() => {
vi.useRealTimers();
});
it('returns null for never', () => {
expect(resolveUploadBatchExpiresAt('never')).toBeNull();
});
it('adds preset offsets from the reference time', () => {
expect(resolveUploadBatchExpiresAt('1h')).toBe('2026-01-01T13:00:00.000Z');
expect(resolveUploadBatchExpiresAt('24h')).toBe('2026-01-02T12:00:00.000Z');
expect(resolveUploadBatchExpiresAt('7d')).toBe('2026-01-08T12:00:00.000Z');
expect(resolveUploadBatchExpiresAt('30d')).toBe('2026-01-31T12:00:00.000Z');
});
});
@@ -0,0 +1,20 @@
import dayjs from 'dayjs';
import type { UploadExpiryPreset } from '@/types/upload/UploadExpiryPreset';
import type { UploadExpiryOffset } from '@/types/upload/UploadExpiryOffset';
const EXPIRY_OFFSET: Record<Exclude<UploadExpiryPreset, 'never'>, UploadExpiryOffset> = {
'1h': { amount: 1, unit: 'hour' },
'24h': { amount: 24, unit: 'hour' },
'7d': { amount: 7, unit: 'day' },
'30d': { amount: 30, unit: 'day' }
};
export const resolveUploadBatchExpiresAt = (preset: UploadExpiryPreset): string | null => {
if (preset === 'never') {
return null;
}
const { amount, unit } = EXPIRY_OFFSET[preset];
return dayjs().add(amount, unit).toISOString();
};
@@ -0,0 +1,13 @@
import { describe, expect, it } from 'vitest';
import { resolveUploadBatchMaxViews } from '@/utils/upload/resolveUploadBatchMaxViews';
describe('resolveUploadBatchMaxViews', () => {
it('maps presets to API values', () => {
expect(resolveUploadBatchMaxViews('unlimited')).toBeNull();
expect(resolveUploadBatchMaxViews('1')).toBe(1);
expect(resolveUploadBatchMaxViews('10')).toBe(10);
expect(resolveUploadBatchMaxViews('100')).toBe(100);
expect(resolveUploadBatchMaxViews('1000')).toBe(1000);
expect(resolveUploadBatchMaxViews('10000')).toBe(10000);
});
});
@@ -0,0 +1,9 @@
import type { UploadMaxViewsPreset } from '@/types/upload/UploadMaxViewsPreset';
export const resolveUploadBatchMaxViews = (preset: UploadMaxViewsPreset): number | null => {
if (preset === 'unlimited') {
return null;
}
return parseInt(preset, 10);
};
@@ -0,0 +1,44 @@
import { describe, expect, it } from 'vitest';
import type { UploadBatchSettingsForm } from '@/types/upload/UploadBatchSettingsForm';
import { validateUploadBatchSettings } from '@/utils/upload/validateUploadBatchSettings';
const baseSettings: UploadBatchSettingsForm = {
mode: 'zk',
expiryPreset: 'never',
maxViewsPreset: 'unlimited',
passwordEnabled: false,
password: ''
};
describe('validateUploadBatchSettings', () => {
it('allows hotlink mode without password', () => {
const settings = {
...baseSettings,
mode: 'hotlink' as const,
passwordEnabled: true,
password: ''
};
expect(validateUploadBatchSettings(settings)).toBeNull();
});
it('requires password when zk protection is enabled', () => {
const settings = {
...baseSettings,
passwordEnabled: true,
password: ''
};
expect(validateUploadBatchSettings(settings)).toBe('Enter a password or turn off password protection.');
});
it('allows zk upload when password is set', () => {
const settings = {
...baseSettings,
passwordEnabled: true,
password: 'secret'
};
expect(validateUploadBatchSettings(settings)).toBeNull();
});
});
@@ -0,0 +1,13 @@
import type { UploadBatchSettingsForm } from '@/types/upload/UploadBatchSettingsForm';
export const validateUploadBatchSettings = (settings: UploadBatchSettingsForm): string | null => {
if (settings.mode !== 'zk' || !settings.passwordEnabled) {
return null;
}
if (settings.password.length === 0) {
return 'Enter a password or turn off password protection.';
}
return null;
};