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:
@@ -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;
|
||||
};
|
||||
Reference in New Issue
Block a user