feat(calendar): add domain field and test-connection button to Exchange sources
- Prisma: domain String? added to CalendarSource model (db push applied) - DTOs: domain in CreateCalendarSourceDto, UpdateCalendarSourceDto, new TestCalendarSourceConfigDto - Service: domain in SOURCE_SAFE_SELECT, addSource, updateSource; new testConnectionFromConfig method - Controller: POST /calendar/sources/test-config (before :id routes to avoid collision) - ExchangeProvider: domain in all source interfaces; passed as 3rd arg to EWS WebCredentials - Frontend: domain in CalendarSource/CreateSourcePayload/UpdateSourcePayload; testSourceConfig API fn - Form: domain field (Exchange-only), "Test connection" button with idle/loading/success/error states - i18n: de+en keys for formFieldDomain, formFieldDomainHint, formTestConnection, formTesting, formTestSuccess, formTestFailed Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,23 @@
|
|||||||
|
---
|
||||||
|
slug: calendar-domain-test-button
|
||||||
|
created: "2026-07-01"
|
||||||
|
status: in-progress
|
||||||
|
---
|
||||||
|
|
||||||
|
# Calendar: Domain-Feld + Test-Button
|
||||||
|
|
||||||
|
Füge Domain-Feld (Exchange) und "Verbindung testen"-Button zur Kalenderquellen-Form hinzu.
|
||||||
|
|
||||||
|
## Tasks
|
||||||
|
|
||||||
|
- [ ] T1: Prisma schema — `domain String?` zu CalendarSource
|
||||||
|
- [ ] T2: Migration ausführen
|
||||||
|
- [ ] T3: Create + Update DTOs — `domain?: string` hinzufügen
|
||||||
|
- [ ] T4: TestCalendarSourceConfigDto erstellen
|
||||||
|
- [ ] T5: CalendarService — domain in SOURCE_SAFE_SELECT, addSource, updateSource; `testConnectionFromConfig` Methode
|
||||||
|
- [ ] T6: CalendarController — `POST /calendar/sources/test-config` Endpoint
|
||||||
|
- [ ] T7: ExchangeProvider — domain in Interface + EWS WebCredentials
|
||||||
|
- [ ] T8: calendar-api.ts (Frontend) — domain in Types + `testSourceConfig` Funktion
|
||||||
|
- [ ] T9: calendar-source-form.tsx — Domain-Feld + Test-Button mit Statusanzeige
|
||||||
|
- [ ] T10: i18n de.json + en.json — neue Translations
|
||||||
|
- [ ] T11: Commit
|
||||||
@@ -40,6 +40,7 @@ model User {
|
|||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
lastLoginAt DateTime?
|
lastLoginAt DateTime?
|
||||||
avatarPath String?
|
avatarPath String?
|
||||||
|
accentColor String?
|
||||||
passwordResetTokens PasswordResetToken[]
|
passwordResetTokens PasswordResetToken[]
|
||||||
|
|
||||||
@@index([tenantId])
|
@@index([tenantId])
|
||||||
@@ -156,6 +157,7 @@ model CalendarSource {
|
|||||||
name String
|
name String
|
||||||
type String // 'caldav' | 'ics' | 'exchange'
|
type String // 'caldav' | 'ics' | 'exchange'
|
||||||
exchangeMode String? // 'ews' | 'graph' — only for exchange type
|
exchangeMode String? // 'ews' | 'graph' — only for exchange type
|
||||||
|
domain String? // Exchange EWS only: Windows domain (e.g. COMPANY)
|
||||||
url String
|
url String
|
||||||
username String?
|
username String?
|
||||||
encryptedPassword String? // AES-256-GCM ciphertext (iv:authTag:ciphertext hex)
|
encryptedPassword String? // AES-256-GCM ciphertext (iv:authTag:ciphertext hex)
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { Request } from 'express';
|
|||||||
import { CalendarService } from './calendar.service';
|
import { CalendarService } from './calendar.service';
|
||||||
import { CreateCalendarSourceDto } from './dto/create-calendar-source.dto';
|
import { CreateCalendarSourceDto } from './dto/create-calendar-source.dto';
|
||||||
import { UpdateCalendarSourceDto } from './dto/update-calendar-source.dto';
|
import { UpdateCalendarSourceDto } from './dto/update-calendar-source.dto';
|
||||||
|
import { TestCalendarSourceConfigDto } from './dto/test-calendar-source-config.dto';
|
||||||
import { CalendarEventsQueryDto } from './dto/calendar-events-query.dto';
|
import { CalendarEventsQueryDto } from './dto/calendar-events-query.dto';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -103,6 +104,18 @@ export class CalendarController {
|
|||||||
return this.calendarService.deleteSource(id, userId);
|
return this.calendarService.deleteSource(id, userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /calendar/sources/test-config
|
||||||
|
* Tests a calendar configuration without saving. Used by the form "Test connection" button.
|
||||||
|
* Must be declared BEFORE sources/:id routes to avoid route collision.
|
||||||
|
*/
|
||||||
|
@Post('sources/test-config')
|
||||||
|
async testSourceConfig(
|
||||||
|
@Body() dto: TestCalendarSourceConfigDto,
|
||||||
|
) {
|
||||||
|
return this.calendarService.testConnectionFromConfig(dto);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* POST /calendar/sources/:id/test
|
* POST /calendar/sources/:id/test
|
||||||
* Tests connection to a calendar source. Implemented in Task 3.
|
* Tests connection to a calendar source. Implemented in Task 3.
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { PrismaService } from '../prisma/prisma.service';
|
|||||||
import { CalendarCryptoService } from './crypto.service';
|
import { CalendarCryptoService } from './crypto.service';
|
||||||
import { CreateCalendarSourceDto } from './dto/create-calendar-source.dto';
|
import { CreateCalendarSourceDto } from './dto/create-calendar-source.dto';
|
||||||
import { UpdateCalendarSourceDto } from './dto/update-calendar-source.dto';
|
import { UpdateCalendarSourceDto } from './dto/update-calendar-source.dto';
|
||||||
|
import { TestCalendarSourceConfigDto } from './dto/test-calendar-source-config.dto';
|
||||||
import { ICSProvider } from './providers/ics.provider';
|
import { ICSProvider } from './providers/ics.provider';
|
||||||
import { CalDAVProvider } from './providers/caldav.provider';
|
import { CalDAVProvider } from './providers/caldav.provider';
|
||||||
import { ExchangeProvider } from './providers/exchange.provider';
|
import { ExchangeProvider } from './providers/exchange.provider';
|
||||||
@@ -33,13 +34,13 @@ export interface CalendarEvent {
|
|||||||
*/
|
*/
|
||||||
export interface CalendarProvider {
|
export interface CalendarProvider {
|
||||||
fetchEvents(
|
fetchEvents(
|
||||||
source: { url: string; username?: string; password?: string; exchangeMode?: string | null; id: string; color?: string | null },
|
source: { url: string; username?: string; password?: string; exchangeMode?: string | null; domain?: string; id: string; color?: string | null },
|
||||||
from: Date,
|
from: Date,
|
||||||
to: Date,
|
to: Date,
|
||||||
): Promise<CalendarEvent[]>;
|
): Promise<CalendarEvent[]>;
|
||||||
|
|
||||||
testConnection(
|
testConnection(
|
||||||
source: { url: string; username?: string; password?: string; exchangeMode?: string | null; id: string },
|
source: { url: string; username?: string; password?: string; exchangeMode?: string | null; domain?: string; id: string },
|
||||||
): Promise<boolean>;
|
): Promise<boolean>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,6 +55,7 @@ const SOURCE_SAFE_SELECT = {
|
|||||||
name: true,
|
name: true,
|
||||||
type: true,
|
type: true,
|
||||||
exchangeMode: true,
|
exchangeMode: true,
|
||||||
|
domain: true,
|
||||||
url: true,
|
url: true,
|
||||||
username: true,
|
username: true,
|
||||||
// encryptedPassword: NEVER included — T-05-09
|
// encryptedPassword: NEVER included — T-05-09
|
||||||
@@ -149,6 +151,7 @@ export class CalendarService {
|
|||||||
url: dto.url,
|
url: dto.url,
|
||||||
username: dto.username ?? null,
|
username: dto.username ?? null,
|
||||||
exchangeMode: dto.exchangeMode ?? null,
|
exchangeMode: dto.exchangeMode ?? null,
|
||||||
|
domain: dto.domain ?? null,
|
||||||
color: dto.color ?? '#3B82F6',
|
color: dto.color ?? '#3B82F6',
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -191,6 +194,7 @@ export class CalendarService {
|
|||||||
if (dto.url !== undefined) data.url = dto.url;
|
if (dto.url !== undefined) data.url = dto.url;
|
||||||
if (dto.username !== undefined) data.username = dto.username;
|
if (dto.username !== undefined) data.username = dto.username;
|
||||||
if (dto.exchangeMode !== undefined) data.exchangeMode = dto.exchangeMode;
|
if (dto.exchangeMode !== undefined) data.exchangeMode = dto.exchangeMode;
|
||||||
|
if (dto.domain !== undefined) data.domain = dto.domain;
|
||||||
if (dto.color !== undefined) data.color = dto.color;
|
if (dto.color !== undefined) data.color = dto.color;
|
||||||
if (dto.isVisible !== undefined) data.isVisible = dto.isVisible;
|
if (dto.isVisible !== undefined) data.isVisible = dto.isVisible;
|
||||||
|
|
||||||
@@ -250,6 +254,7 @@ export class CalendarService {
|
|||||||
? this.crypto.decrypt(source.encryptedPassword)
|
? this.crypto.decrypt(source.encryptedPassword)
|
||||||
: undefined,
|
: undefined,
|
||||||
exchangeMode: source.exchangeMode,
|
exchangeMode: source.exchangeMode,
|
||||||
|
domain: source.domain ?? undefined,
|
||||||
id: source.id,
|
id: source.id,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -275,6 +280,33 @@ export class CalendarService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests a calendar source configuration without saving it to the database.
|
||||||
|
* Used by the "Test connection" button in the form before the source is created.
|
||||||
|
*/
|
||||||
|
async testConnectionFromConfig(
|
||||||
|
dto: TestCalendarSourceConfigDto,
|
||||||
|
): Promise<{ success: boolean; error?: string }> {
|
||||||
|
await this.validateUrlNotPrivate(dto.url);
|
||||||
|
|
||||||
|
const provider = this.getProvider(dto.type);
|
||||||
|
const tempSource = {
|
||||||
|
url: dto.url,
|
||||||
|
username: dto.username,
|
||||||
|
password: dto.password,
|
||||||
|
exchangeMode: dto.exchangeMode ?? null,
|
||||||
|
domain: dto.domain,
|
||||||
|
id: 'test-config',
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const success = await provider.testConnection(tempSource);
|
||||||
|
return { success };
|
||||||
|
} catch {
|
||||||
|
return { success: false, error: 'Connection failed' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Aggregate events from all visible sources for a user (CAL-02/CAL-03).
|
* Aggregate events from all visible sources for a user (CAL-02/CAL-03).
|
||||||
*
|
*
|
||||||
@@ -335,6 +367,7 @@ export class CalendarService {
|
|||||||
? this.crypto.decrypt(source.encryptedPassword)
|
? this.crypto.decrypt(source.encryptedPassword)
|
||||||
: undefined,
|
: undefined,
|
||||||
exchangeMode: source.exchangeMode,
|
exchangeMode: source.exchangeMode,
|
||||||
|
domain: source.domain ?? undefined,
|
||||||
id: source.id,
|
id: source.id,
|
||||||
color: source.color,
|
color: source.color,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -41,6 +41,10 @@ export class CreateCalendarSourceDto {
|
|||||||
@IsIn(['ews', 'graph'])
|
@IsIn(['ews', 'graph'])
|
||||||
exchangeMode?: string;
|
exchangeMode?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
domain?: string;
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsHexColor()
|
@IsHexColor()
|
||||||
color?: string;
|
color?: string;
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import {
|
||||||
|
IsIn,
|
||||||
|
IsOptional,
|
||||||
|
IsString,
|
||||||
|
IsUrl,
|
||||||
|
} from 'class-validator';
|
||||||
|
|
||||||
|
export class TestCalendarSourceConfigDto {
|
||||||
|
@IsIn(['caldav', 'ics', 'exchange'])
|
||||||
|
type!: string;
|
||||||
|
|
||||||
|
@IsUrl(
|
||||||
|
{ protocols: ['https'], require_protocol: true },
|
||||||
|
{ message: 'URL must use HTTPS protocol' },
|
||||||
|
)
|
||||||
|
url!: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
username?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
password?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsIn(['ews', 'graph'])
|
||||||
|
exchangeMode?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
domain?: string;
|
||||||
|
}
|
||||||
@@ -45,6 +45,10 @@ export class UpdateCalendarSourceDto {
|
|||||||
@IsIn(['ews', 'graph'])
|
@IsIn(['ews', 'graph'])
|
||||||
exchangeMode?: string;
|
exchangeMode?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
domain?: string;
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsHexColor()
|
@IsHexColor()
|
||||||
color?: string;
|
color?: string;
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ export class ExchangeProvider implements CalendarProvider {
|
|||||||
username?: string;
|
username?: string;
|
||||||
password?: string;
|
password?: string;
|
||||||
exchangeMode?: string | null;
|
exchangeMode?: string | null;
|
||||||
|
domain?: string;
|
||||||
id: string;
|
id: string;
|
||||||
color?: string | null;
|
color?: string | null;
|
||||||
},
|
},
|
||||||
@@ -56,6 +57,7 @@ export class ExchangeProvider implements CalendarProvider {
|
|||||||
username?: string;
|
username?: string;
|
||||||
password?: string;
|
password?: string;
|
||||||
exchangeMode?: string | null;
|
exchangeMode?: string | null;
|
||||||
|
domain?: string;
|
||||||
id: string;
|
id: string;
|
||||||
},
|
},
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
@@ -144,6 +146,7 @@ export class ExchangeProvider implements CalendarProvider {
|
|||||||
url: string;
|
url: string;
|
||||||
username?: string;
|
username?: string;
|
||||||
password?: string;
|
password?: string;
|
||||||
|
domain?: string;
|
||||||
id: string;
|
id: string;
|
||||||
color?: string | null;
|
color?: string | null;
|
||||||
},
|
},
|
||||||
@@ -158,6 +161,7 @@ export class ExchangeProvider implements CalendarProvider {
|
|||||||
service.Credentials = new ews.WebCredentials(
|
service.Credentials = new ews.WebCredentials(
|
||||||
source.username || '',
|
source.username || '',
|
||||||
source.password || '',
|
source.password || '',
|
||||||
|
source.domain || undefined,
|
||||||
);
|
);
|
||||||
|
|
||||||
// CalendarView constructor accepts JS Dates in ews-javascript-api
|
// CalendarView constructor accepts JS Dates in ews-javascript-api
|
||||||
@@ -210,7 +214,7 @@ export class ExchangeProvider implements CalendarProvider {
|
|||||||
* Tests EWS connection by binding to the calendar folder.
|
* Tests EWS connection by binding to the calendar folder.
|
||||||
*/
|
*/
|
||||||
private async testEwsConnection(
|
private async testEwsConnection(
|
||||||
source: { url: string; username?: string; password?: string },
|
source: { url: string; username?: string; password?: string; domain?: string },
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
const ews: any = await import('ews-javascript-api');
|
const ews: any = await import('ews-javascript-api');
|
||||||
|
|
||||||
@@ -219,6 +223,7 @@ export class ExchangeProvider implements CalendarProvider {
|
|||||||
service.Credentials = new ews.WebCredentials(
|
service.Credentials = new ews.WebCredentials(
|
||||||
source.username || '',
|
source.username || '',
|
||||||
source.password || '',
|
source.password || '',
|
||||||
|
source.domain || undefined,
|
||||||
);
|
);
|
||||||
|
|
||||||
await ews.Folder.Bind(service, ews.WellKnownFolderName.Calendar);
|
await ews.Folder.Bind(service, ews.WellKnownFolderName.Calendar);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import { useCallback, useState } from 'react';
|
import { useCallback, useState } from 'react';
|
||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
import type { CreateSourcePayload } from '@/lib/calendar-api';
|
import type { CreateSourcePayload } from '@/lib/calendar-api';
|
||||||
|
import { testSourceConfig } from '@/lib/calendar-api';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 8-color palette for calendar sources (UI-SPEC).
|
* 8-color palette for calendar sources (UI-SPEC).
|
||||||
@@ -69,6 +70,8 @@ export function CalendarSourceForm({
|
|||||||
const [exchangeMode, setExchangeMode] = useState(
|
const [exchangeMode, setExchangeMode] = useState(
|
||||||
initialValues?.exchangeMode ?? 'graph',
|
initialValues?.exchangeMode ?? 'graph',
|
||||||
);
|
);
|
||||||
|
const [domain, setDomain] = useState(initialValues?.domain ?? '');
|
||||||
|
const [testStatus, setTestStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle');
|
||||||
const [color, setColor] = useState(
|
const [color, setColor] = useState(
|
||||||
initialValues?.color ?? SOURCE_COLOR_PALETTE[0].value,
|
initialValues?.color ?? SOURCE_COLOR_PALETTE[0].value,
|
||||||
);
|
);
|
||||||
@@ -103,6 +106,24 @@ export function CalendarSourceForm({
|
|||||||
url.trim() !== '' &&
|
url.trim() !== '' &&
|
||||||
!urlError;
|
!urlError;
|
||||||
|
|
||||||
|
const handleTest = useCallback(async () => {
|
||||||
|
if (!validateUrl(url)) return;
|
||||||
|
setTestStatus('loading');
|
||||||
|
try {
|
||||||
|
const result = await testSourceConfig({
|
||||||
|
type,
|
||||||
|
url: url.trim(),
|
||||||
|
username: username.trim() || undefined,
|
||||||
|
password: password || undefined,
|
||||||
|
exchangeMode: isExchange ? exchangeMode : undefined,
|
||||||
|
domain: isExchange && domain.trim() ? domain.trim() : undefined,
|
||||||
|
});
|
||||||
|
setTestStatus(result.success ? 'success' : 'error');
|
||||||
|
} catch {
|
||||||
|
setTestStatus('error');
|
||||||
|
}
|
||||||
|
}, [type, url, username, password, exchangeMode, domain, isExchange, validateUrl]);
|
||||||
|
|
||||||
const handleSubmit = useCallback(
|
const handleSubmit = useCallback(
|
||||||
async (e: React.FormEvent) => {
|
async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -122,14 +143,15 @@ export function CalendarSourceForm({
|
|||||||
if (password) payload.password = password;
|
if (password) payload.password = password;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Include exchangeMode for Exchange type
|
// Include exchangeMode and domain for Exchange type
|
||||||
if (isExchange) {
|
if (isExchange) {
|
||||||
payload.exchangeMode = exchangeMode;
|
payload.exchangeMode = exchangeMode;
|
||||||
|
if (domain.trim()) payload.domain = domain.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
await onSave(payload);
|
await onSave(payload);
|
||||||
},
|
},
|
||||||
[name, type, url, username, password, exchangeMode, color, isICS, isExchange, onSave, validateUrl],
|
[name, type, url, username, password, exchangeMode, domain, color, isICS, isExchange, onSave, validateUrl],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -194,6 +216,23 @@ export function CalendarSourceForm({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Domain (Exchange EWS only) */}
|
||||||
|
{isExchange && (
|
||||||
|
<div>
|
||||||
|
<label htmlFor="exchange-domain" className="mb-1 block text-sm text-foreground">
|
||||||
|
{t('calendar.formFieldDomain')}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="exchange-domain"
|
||||||
|
type="text"
|
||||||
|
placeholder={t('calendar.formFieldDomainHint')}
|
||||||
|
className="h-9 w-full max-w-md rounded border border-border bg-background px-3 text-sm text-foreground"
|
||||||
|
value={domain}
|
||||||
|
onChange={(e) => setDomain(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* URL */}
|
{/* URL */}
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="source-url" className="mb-1 block text-sm text-foreground">
|
<label htmlFor="source-url" className="mb-1 block text-sm text-foreground">
|
||||||
@@ -282,7 +321,7 @@ export function CalendarSourceForm({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Actions */}
|
{/* Actions */}
|
||||||
<div className="flex gap-3 pt-2">
|
<div className="flex flex-wrap gap-3 pt-2">
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
data-testid="source-form-submit"
|
data-testid="source-form-submit"
|
||||||
@@ -291,6 +330,15 @@ export function CalendarSourceForm({
|
|||||||
>
|
>
|
||||||
{isSaving ? t('calendar.formSaving') : t('calendar.formSave')}
|
{isSaving ? t('calendar.formSaving') : t('calendar.formSave')}
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
data-testid="source-form-test"
|
||||||
|
disabled={!isFormValid || testStatus === 'loading'}
|
||||||
|
onClick={handleTest}
|
||||||
|
className="rounded border border-border px-4 py-2 text-sm text-foreground transition-colors hover:bg-muted disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
{testStatus === 'loading' ? t('calendar.formTesting') : t('calendar.formTestConnection')}
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onCancel}
|
onClick={onCancel}
|
||||||
@@ -299,6 +347,12 @@ export function CalendarSourceForm({
|
|||||||
{t('calendar.formCancel')}
|
{t('calendar.formCancel')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
{testStatus === 'success' && (
|
||||||
|
<p className="text-sm text-green-600 dark:text-green-400">{t('calendar.formTestSuccess')}</p>
|
||||||
|
)}
|
||||||
|
{testStatus === 'error' && (
|
||||||
|
<p className="text-sm text-destructive">{t('calendar.formTestFailed')}</p>
|
||||||
|
)}
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ export interface CalendarSource {
|
|||||||
name: string;
|
name: string;
|
||||||
type: 'caldav' | 'ics' | 'exchange';
|
type: 'caldav' | 'ics' | 'exchange';
|
||||||
exchangeMode?: string | null;
|
exchangeMode?: string | null;
|
||||||
|
domain?: string | null;
|
||||||
url: string;
|
url: string;
|
||||||
username?: string | null;
|
username?: string | null;
|
||||||
color?: string | null;
|
color?: string | null;
|
||||||
@@ -59,6 +60,7 @@ export interface CreateSourcePayload {
|
|||||||
username?: string;
|
username?: string;
|
||||||
password?: string;
|
password?: string;
|
||||||
exchangeMode?: string;
|
exchangeMode?: string;
|
||||||
|
domain?: string;
|
||||||
color?: string;
|
color?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,6 +75,7 @@ export interface UpdateSourcePayload {
|
|||||||
username?: string;
|
username?: string;
|
||||||
password?: string;
|
password?: string;
|
||||||
exchangeMode?: string;
|
exchangeMode?: string;
|
||||||
|
domain?: string;
|
||||||
color?: string;
|
color?: string;
|
||||||
isVisible?: boolean;
|
isVisible?: boolean;
|
||||||
}
|
}
|
||||||
@@ -138,6 +141,28 @@ export async function deleteSource(id: string): Promise<void> {
|
|||||||
if (!res.ok) throw new Error('Failed to delete calendar source');
|
if (!res.ok) throw new Error('Failed to delete calendar source');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test a calendar configuration without saving it.
|
||||||
|
* POST /calendar/sources/test-config
|
||||||
|
*/
|
||||||
|
export async function testSourceConfig(config: {
|
||||||
|
type: string;
|
||||||
|
url: string;
|
||||||
|
username?: string;
|
||||||
|
password?: string;
|
||||||
|
exchangeMode?: string;
|
||||||
|
domain?: string;
|
||||||
|
}): Promise<{ success: boolean; error?: string }> {
|
||||||
|
const res = await fetch(`${API_URL}/calendar/sources/test-config`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
credentials: 'include',
|
||||||
|
body: JSON.stringify(config),
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error('Test request failed');
|
||||||
|
return res.json();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test connection to a calendar source.
|
* Test connection to a calendar source.
|
||||||
* POST /calendar/sources/:id/test
|
* POST /calendar/sources/:id/test
|
||||||
|
|||||||
@@ -202,6 +202,12 @@
|
|||||||
"formFieldPassword": "Passwort",
|
"formFieldPassword": "Passwort",
|
||||||
"formFieldColor": "Farbe",
|
"formFieldColor": "Farbe",
|
||||||
"formFieldExchangeMode": "Exchange-Modus",
|
"formFieldExchangeMode": "Exchange-Modus",
|
||||||
|
"formFieldDomain": "Domain",
|
||||||
|
"formFieldDomainHint": "Windows-Domain (z.B. FIRMA)",
|
||||||
|
"formTestConnection": "Verbindung testen",
|
||||||
|
"formTesting": "Teste...",
|
||||||
|
"formTestSuccess": "Verbindung erfolgreich",
|
||||||
|
"formTestFailed": "Verbindung fehlgeschlagen",
|
||||||
"formSave": "Speichern",
|
"formSave": "Speichern",
|
||||||
"formSaving": "Wird gespeichert...",
|
"formSaving": "Wird gespeichert...",
|
||||||
"formCancel": "Abbrechen",
|
"formCancel": "Abbrechen",
|
||||||
|
|||||||
@@ -202,6 +202,12 @@
|
|||||||
"formFieldPassword": "Password",
|
"formFieldPassword": "Password",
|
||||||
"formFieldColor": "Color",
|
"formFieldColor": "Color",
|
||||||
"formFieldExchangeMode": "Exchange Mode",
|
"formFieldExchangeMode": "Exchange Mode",
|
||||||
|
"formFieldDomain": "Domain",
|
||||||
|
"formFieldDomainHint": "Windows domain (e.g. COMPANY)",
|
||||||
|
"formTestConnection": "Test connection",
|
||||||
|
"formTesting": "Testing...",
|
||||||
|
"formTestSuccess": "Connection successful",
|
||||||
|
"formTestFailed": "Connection failed",
|
||||||
"formSave": "Save",
|
"formSave": "Save",
|
||||||
"formSaving": "Saving...",
|
"formSaving": "Saving...",
|
||||||
"formCancel": "Cancel",
|
"formCancel": "Cancel",
|
||||||
|
|||||||
Reference in New Issue
Block a user