feat: standalone buttons for all plugin pages + window system for modals

Standalone buttons:
- Add ExternalLink button to Dms, Calendar, Mail, ContactsList toolbars
- Create standalone pages for each (no sidebar, full screen)
- Add standalone routes outside ProtectedRoute

Window system for modals:
- AppointmentModal -> AppointmentEditForm + openWindow()
- ComposeModal -> MailComposeForm + openWindow()
- FilePreviewModal -> FilePreviewContent + openWindow()
- Calendar, Mail, Dms use openWindow() instead of modal state
This commit is contained in:
Agent Zero
2026-07-25 00:21:37 +02:00
parent 2f6c3175a3
commit 868bb274ef
12 changed files with 1220 additions and 124 deletions
+79 -50
View File
@@ -20,12 +20,13 @@ import { DayView } from '@/components/calendar/DayView';
import { RangeView } from '@/components/calendar/RangeView';
import { formatDateInput } from '@/utils/date';
import { CalendarDetail } from '@/components/calendar/CalendarDetail';
import { AppointmentModal } from '@/components/calendar/AppointmentModal';
import { AppointmentEditForm } from '@/components/calendar/AppointmentEditForm';
import { useWindowStore } from '@/store/windowStore';
import { IcsControls } from '@/components/calendar/IcsControls';
import { SharingSettings } from '@/components/calendar/SharingSettings';
import { useCalendarStore, type CalendarViewMode } from '@/store/calendarStore';
import { usePluginToolbarStore } from '@/store/pluginToolbarStore';
import { ChevronLeft, ChevronRight, Info, Plus } from 'lucide-react';
import { ChevronLeft, ChevronRight, ExternalLink, Info, Plus } from 'lucide-react';
import {
fetchCalendars,
createCalendar,
@@ -72,9 +73,7 @@ export function CalendarPage() {
const [loadingCalendars, setLoadingCalendars] = useState(true);
const [error, setError] = useState<string | null>(null);
const [modalOpen, setModalOpen] = useState(false);
const [modalEntry, setModalEntry] = useState<CalendarEntry | null>(null);
const [prefillDate, setPrefillDate] = useState<Date | null>(null);
const openWindow = useWindowStore((s) => s.openWindow);
const [showSharing, setShowSharing] = useState(false);
const [showIcs, setShowIcs] = useState(false);
@@ -176,11 +175,44 @@ export function CalendarPage() {
// ??? Handlers ???????????????????????????????????????????????????????????
const handleSaved = useCallback(
(saved: CalendarEntry) => {
setEntries((cur) => {
const exists = cur.some((e) => e.id === saved.id);
return exists ? cur.map((e) => (e.id === saved.id ? saved : e)) : [...cur, saved];
});
if (selectedEntry?.id === saved.id) {
setSelectedEntry(saved);
}
},
[selectedEntry, setSelectedEntry],
);
const handleDeleted = useCallback(
(entryId: string) => {
setEntries((cur) => cur.filter((e) => e.id !== entryId));
if (selectedEntry?.id === entryId) {
setSelectedEntry(null);
}
},
[selectedEntry, setSelectedEntry],
);
const handleCreateAt = useCallback((date: Date) => {
setModalEntry(null);
setPrefillDate(date);
setModalOpen(true);
}, []);
openWindow({
title: t('calendar.newAppointment'),
type: 'appointment-create',
component: AppointmentEditForm,
componentProps: {
entry: null,
prefillDate: date,
calendars,
defaultCalendarId: activeCalendarId,
onSaved: handleSaved,
onDeleted: handleDeleted,
},
});
}, [openWindow, t, calendars, activeCalendarId, handleSaved, handleDeleted]);
const handleEditEntry = useCallback(
(entry: CalendarEntry) => {
@@ -194,10 +226,20 @@ export function CalendarPage() {
const handleOpenEditModal = useCallback(() => {
if (!selectedEntry) return;
setModalEntry(selectedEntry);
setPrefillDate(null);
setModalOpen(true);
}, [selectedEntry]);
openWindow({
title: t('calendar.editAppointment'),
type: 'appointment-edit',
component: AppointmentEditForm,
componentProps: {
entry: selectedEntry,
prefillDate: null,
calendars,
defaultCalendarId: activeCalendarId,
onSaved: handleSaved,
onDeleted: handleDeleted,
},
});
}, [selectedEntry, openWindow, t, calendars, activeCalendarId, handleSaved, handleDeleted]);
const handleMoveEntry = useCallback(
async (entry: CalendarEntry, newStart: Date) => {
@@ -223,29 +265,6 @@ export function CalendarPage() {
[t, loadEntries, selectedEntry, setSelectedEntry],
);
const handleSaved = useCallback(
(saved: CalendarEntry) => {
setEntries((cur) => {
const exists = cur.some((e) => e.id === saved.id);
return exists ? cur.map((e) => (e.id === saved.id ? saved : e)) : [...cur, saved];
});
if (selectedEntry?.id === saved.id) {
setSelectedEntry(saved);
}
},
[selectedEntry, setSelectedEntry],
);
const handleDeleted = useCallback(
(entryId: string) => {
setEntries((cur) => cur.filter((e) => e.id !== entryId));
if (selectedEntry?.id === entryId) {
setSelectedEntry(null);
}
},
[selectedEntry, setSelectedEntry],
);
const handleConfirmDelete = useCallback(async () => {
if (!deleteTarget) return;
try {
@@ -367,9 +386,19 @@ export function CalendarPage() {
label: t('calendar.newAppointment'),
group: 'actions',
onClick: () => {
setModalEntry(null);
setPrefillDate(null);
setModalOpen(true);
openWindow({
title: t('calendar.newAppointment'),
type: 'appointment-create',
component: AppointmentEditForm,
componentProps: {
entry: null,
prefillDate: null,
calendars,
defaultCalendarId: activeCalendarId,
onSaved: handleSaved,
onDeleted: handleDeleted,
},
});
},
icon: (
<Plus className="w-3.5 h-3.5" strokeWidth={2} />
@@ -401,6 +430,17 @@ export function CalendarPage() {
<Info className="w-3.5 h-3.5" strokeWidth={2} />
),
},
{
id: 'open-standalone',
plugin: 'calendar',
label: 'In neuem Fenster',
type: 'button' as const,
group: 'actions',
icon: (
<ExternalLink className="w-3.5 h-3.5" strokeWidth={2} />
),
onClick: () => window.open('/calendar-standalone', '_blank', 'width=1200,height=800'),
},
];
registerItems('calendar', items);
@@ -685,17 +725,6 @@ export function CalendarPage() {
onCancel={() => setDeleteTarget(null)}
/>
{/* Appointment modal */}
<AppointmentModal
open={modalOpen}
onClose={() => setModalOpen(false)}
entry={modalEntry}
prefillDate={prefillDate}
calendars={calendars}
defaultCalendarId={activeCalendarId}
onSaved={handleSaved}
onDeleted={handleDeleted}
/>
</div>
);
}
+10
View File
@@ -0,0 +1,10 @@
import React from 'react';
import { CalendarPage } from './Calendar';
export function CalendarStandalonePage() {
return (
<div className="h-screen w-screen overflow-hidden bg-white">
<CalendarPage />
</div>
);
}
+12 -1
View File
@@ -18,7 +18,7 @@ import { ContactDetail } from '@/components/contacts/ContactDetail';
import { ContactEditForm } from '@/components/contacts/ContactEditForm';
import { useWindowStore } from '@/store/windowStore';
import { SavedFilters } from '@/components/SavedFilters';
import { ArrowDownAZ, ArrowUpZA, ChevronLeft, LayoutGrid, List, Plus } from 'lucide-react';
import { ArrowDownAZ, ArrowUpZA, ChevronLeft, ExternalLink, LayoutGrid, List, Plus } from 'lucide-react';
import {
useUnifiedContacts,
useUnifiedContact,
@@ -201,6 +201,17 @@ export function ContactsListPage() {
),
onClick: handleCreate,
},
{
id: 'open-standalone',
plugin: 'contacts',
label: 'In neuem Fenster',
type: 'button' as const,
group: 'actions',
icon: (
<ExternalLink className="w-3.5 h-3.5" strokeWidth={2} />
),
onClick: () => window.open('/contacts-standalone', '_blank', 'width=1200,height=800'),
},
];
registerItems('contacts', items);
return () => unregisterPlugin('contacts');
+10
View File
@@ -0,0 +1,10 @@
import React from 'react';
import { ContactsListPage } from './ContactsList';
export function ContactsStandalonePage() {
return (
<div className="h-screen w-screen overflow-hidden bg-white">
<ContactsListPage />
</div>
);
}
+33 -14
View File
@@ -20,11 +20,12 @@ import { SourceTree } from '@/components/dms/SourceTree';
import { FileExplorer, type ViewMode, type SortBy, type SortOrder } from '@/components/dms/FileExplorer';
import { FileDetails } from '@/components/dms/FileDetails';
import { UploadDropzone } from '@/components/dms/UploadDropzone';
import { FilePreviewModal } from '@/components/dms/FilePreviewModal';
import { FilePreviewContent } from '@/components/dms/FilePreviewContent';
import { useWindowStore } from '@/store/windowStore';
import { ShareDialog } from '@/components/dms/ShareDialog';
import { BulkActions } from '@/components/dms/BulkActions';
import { usePluginToolbarStore } from '@/store/pluginToolbarStore';
import { ArrowRight, ChevronLeft, ChevronRight, Info, Plus, Trash2, Upload } from 'lucide-react';
import { ArrowRight, ChevronLeft, ChevronRight, ExternalLink, Info, Plus, Trash2, Upload } from 'lucide-react';
import {
fetchFolders,
createFolder,
@@ -72,7 +73,7 @@ export function DmsPage() {
// Modal state
const [showUpload, setShowUpload] = useState(false);
const [showNewFolder, setShowNewFolder] = useState(false);
const [previewFile, setPreviewFile] = useState<DmsFile | null>(null);
const openWindow = useWindowStore((s) => s.openWindow);
const [shareFile, setShareFile] = useState<DmsFile | null>(null);
const [deleteTarget, setDeleteTarget] = useState<DmsFile | null>(null);
const [submittingFolder, setSubmittingFolder] = useState(false);
@@ -195,8 +196,15 @@ export function DmsPage() {
// Handle file double click (open preview)
const handleFileDoubleClick = useCallback((file: DmsFile) => {
setPreviewFile(file);
}, []);
openWindow({
title: file.name,
type: 'file-preview',
component: FilePreviewContent,
componentProps: {
file,
},
});
}, [openWindow]);
// Handle file share
const handleFileShare = useCallback((file: DmsFile) => {
@@ -210,8 +218,15 @@ export function DmsPage() {
// Handle file preview
const handleFilePreview = useCallback((file: DmsFile) => {
setPreviewFile(file);
}, []);
openWindow({
title: file.name,
type: 'file-preview',
component: FilePreviewContent,
componentProps: {
file,
},
});
}, [openWindow]);
// Handle toggle select
const handleToggleSelect = useCallback((fileId: string) => {
@@ -439,6 +454,17 @@ export function DmsPage() {
),
onClick: () => setShowDetails((v) => !v),
},
{
id: 'open-standalone',
plugin: 'dms',
label: 'In neuem Fenster',
type: 'button' as const,
group: 'actions',
icon: (
<ExternalLink className="w-3.5 h-3.5" strokeWidth={2} />
),
onClick: () => window.open('/dms-standalone', '_blank', 'width=1200,height=800'),
},
];
// Add bulk actions when files are selected
@@ -706,13 +732,6 @@ export function DmsPage() {
onCancel={() => setDeleteTarget(null)}
/>
{/* File preview modal */}
<FilePreviewModal
open={!!previewFile}
file={previewFile}
onClose={() => setPreviewFile(null)}
/>
{/* Share dialog */}
<ShareDialog
open={!!shareFile}
+10
View File
@@ -0,0 +1,10 @@
import React from 'react';
import { DmsPage } from './Dms';
export function DmsStandalonePage() {
return (
<div className="h-screen w-screen overflow-hidden bg-white">
<DmsPage />
</div>
);
}
+117 -59
View File
@@ -15,9 +15,10 @@ import { ResizablePanel } from '@/components/ui/ResizablePanel';
import { MailFolderTree } from '@/components/mail/MailFolderTree';
import { MailList } from '@/components/mail/MailList';
import { MailDetail } from '@/components/mail/MailDetail';
import { ComposeModal, type ComposeMode } from '@/components/mail/ComposeModal';
import { MailComposeForm, type ComposeMode } from '@/components/mail/MailComposeForm';
import { useWindowStore } from '@/store/windowStore';
import { usePluginToolbarStore } from '@/store/pluginToolbarStore';
import { ArrowRight, Check, ChevronLeft, Loader2, Plus, Redo2, Trash2, TrendingUp, Undo2 } from 'lucide-react';
import { ArrowRight, Check, ChevronLeft, ExternalLink, Loader2, Plus, Redo2, Trash2, TrendingUp, Undo2 } from 'lucide-react';
import {
fetchAccounts,
fetchFolders,
@@ -69,11 +70,11 @@ export function MailPage() {
const [loadingMails, setLoadingMails] = useState(false);
const [error, setError] = useState<string | null>(null);
const [signatures, setSignatures] = useState<MailSignature[]>([]);
const [composeOpen, setComposeOpen] = useState(false);
const [composeMode, setComposeMode] = useState<ComposeMode>('new');
const [replyToMail, setReplyToMail] = useState<Mail | null>(null);
const [forwardMailState, setForwardMailState] = useState<Mail | null>(null);
const [draftMailState, setDraftMailState] = useState<Mail | null>(null);
const openWindow = useWindowStore((s) => s.openWindow);
const [downloadingAttachmentId, setDownloadingAttachmentId] = useState<string | null>(null);
const [searchQuery, setSearchQuery] = useState('');
const [activeView, setActiveView] = useState<'folders' | 'list' | 'detail'>('folders');
@@ -233,14 +234,66 @@ export function MailPage() {
[toast],
);
// Handle send (compose)
const handleSend = useCallback(
async (payload: SendMailPayload | ReplyPayload | ForwardPayload, mode: ComposeMode) => {
try {
if (mode === 'reply' && replyToMail) {
await replyMail(replyToMail.id, payload as ReplyPayload);
} else if (mode === 'forward' && forwardMailState) {
await forwardMail(forwardMailState.id, payload as ForwardPayload);
} else {
await sendMail(payload as SendMailPayload);
}
toast.success(t('mail.sent'));
} catch (err) {
toast.error(err instanceof Error ? err.message : String(err));
throw err;
}
},
[replyToMail, forwardMailState, toast, t],
);
// Handle save draft (new or existing)
const handleSaveDraft = useCallback(
async (payload: MailDraftPayload) => {
try {
if (composeMode === 'draft' && draftMailState) {
await updateDraft(draftMailState.id, payload);
} else {
await saveDraft(payload);
}
toast.success(t('mail.draftSaved'));
} catch (err) {
toast.error(err instanceof Error ? err.message : String(err));
throw err;
}
},
[composeMode, draftMailState, toast, t],
);
// Handle compose
const handleCompose = useCallback(() => {
setComposeMode('new');
setReplyToMail(null);
setForwardMailState(null);
setDraftMailState(null);
setComposeOpen(true);
}, []);
openWindow({
title: t('mail.compose'),
type: 'mail-compose',
component: MailComposeForm,
componentProps: {
mode: 'new',
accountId: selectedAccountId,
replyToMail: null,
forwardMail: null,
draftMail: null,
signatures,
onSend: handleSend,
onSaveDraft: handleSaveDraft,
},
});
}, [openWindow, t, selectedAccountId, signatures, handleSend, handleSaveDraft]);
// Handle reply
const handleReply = useCallback((mail: Mail) => {
@@ -248,8 +301,22 @@ export function MailPage() {
setReplyToMail(mail);
setForwardMailState(null);
setDraftMailState(null);
setComposeOpen(true);
}, []);
openWindow({
title: t('mail.reply'),
type: 'mail-compose',
component: MailComposeForm,
componentProps: {
mode: 'reply',
accountId: selectedAccountId,
replyToMail: mail,
forwardMail: null,
draftMail: null,
signatures,
onSend: handleSend,
onSaveDraft: handleSaveDraft,
},
});
}, [openWindow, t, selectedAccountId, signatures, handleSend, handleSaveDraft]);
// Handle forward
const handleForward = useCallback((mail: Mail) => {
@@ -257,8 +324,22 @@ export function MailPage() {
setForwardMailState(mail);
setReplyToMail(null);
setDraftMailState(null);
setComposeOpen(true);
}, []);
openWindow({
title: t('mail.forward'),
type: 'mail-compose',
component: MailComposeForm,
componentProps: {
mode: 'forward',
accountId: selectedAccountId,
replyToMail: null,
forwardMail: mail,
draftMail: null,
signatures,
onSend: handleSend,
onSaveDraft: handleSaveDraft,
},
});
}, [openWindow, t, selectedAccountId, signatures, handleSend, handleSaveDraft]);
// Handle delete single mail
const handleDeleteMail = useCallback(
@@ -324,23 +405,6 @@ export function MailPage() {
[selectedMailIds, toast, t],
);
// Handle save draft (new or existing)
const handleSaveDraft = useCallback(
async (payload: MailDraftPayload) => {
try {
if (composeMode === 'draft' && draftMailState) {
await updateDraft(draftMailState.id, payload);
} else {
await saveDraft(payload);
}
toast.success(t('mail.draftSaved'));
} catch (err) {
toast.error(err instanceof Error ? err.message : String(err));
throw err;
}
},
[composeMode, draftMailState, toast, t],
);
// Handle edit draft
const handleEditDraft = useCallback((mail: Mail) => {
@@ -348,8 +412,22 @@ export function MailPage() {
setDraftMailState(mail);
setReplyToMail(null);
setForwardMailState(null);
setComposeOpen(true);
}, []);
openWindow({
title: t('mail.editDraft'),
type: 'mail-compose',
component: MailComposeForm,
componentProps: {
mode: 'draft',
accountId: selectedAccountId,
replyToMail: null,
forwardMail: null,
draftMail: mail,
signatures,
onSend: handleSend,
onSaveDraft: handleSaveDraft,
},
});
}, [openWindow, t, selectedAccountId, signatures, handleSend, handleSaveDraft]);
// Handle toggle flag
const handleToggleFlag = useCallback(
@@ -486,25 +564,6 @@ export function MailPage() {
[toast],
);
// Handle send (compose)
const handleSend = useCallback(
async (payload: SendMailPayload | ReplyPayload | ForwardPayload, mode: ComposeMode) => {
try {
if (mode === 'reply' && replyToMail) {
await replyMail(replyToMail.id, payload as ReplyPayload);
} else if (mode === 'forward' && forwardMailState) {
await forwardMail(forwardMailState.id, payload as ForwardPayload);
} else {
await sendMail(payload as SendMailPayload);
}
toast.success(t('mail.sent'));
} catch (err) {
toast.error(err instanceof Error ? err.message : String(err));
throw err;
}
},
[replyToMail, forwardMailState, toast, t],
);
// Handle search
const handleSearch = useCallback((query: string) => {
@@ -632,6 +691,17 @@ export function MailPage() {
}
},
},
{
id: 'open-standalone',
plugin: 'mail',
label: 'In neuem Fenster',
type: 'button' as const,
group: 'tools',
icon: (
<ExternalLink className="w-3.5 h-3.5" strokeWidth={2} />
),
onClick: () => window.open('/mail-standalone', '_blank', 'width=1200,height=800'),
},
];
if (hasBulkSelection) {
items.push(
@@ -935,18 +1005,6 @@ export function MailPage() {
</div>
)}
<ComposeModal
open={composeOpen}
mode={composeMode}
accountId={selectedAccountId}
replyToMail={replyToMail}
forwardMail={forwardMailState}
draftMail={draftMailState}
signatures={signatures}
onSend={handleSend}
onSaveDraft={handleSaveDraft}
onClose={() => setComposeOpen(false)}
/>
</div>
);
}
+10
View File
@@ -0,0 +1,10 @@
import React from 'react';
import { MailPage } from './Mail';
export function MailStandalonePage() {
return (
<div className="h-screen w-screen overflow-hidden bg-white">
<MailPage />
</div>
);
}