your-ai-workflow-firebase-os 1.2.20 → 1.2.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{ContactPopup-B666UTn5.cjs → ContactPopup-5mXFBqTs.cjs} +1 -1
- package/dist/{ContactPopup-QbqZ8VJs.js → ContactPopup-D0Fb80uS.js} +1 -1
- package/dist/renderer.cjs.js +1 -1
- package/dist/renderer.es.js +1 -1
- package/dist/your-ai-workflow-firebase-os.cjs.js +35 -35
- package/dist/your-ai-workflow-firebase-os.css +1 -1
- package/dist/your-ai-workflow-firebase-os.es.js +4029 -2588
- package/package.json +1 -1
- package/src/pages/PagesAdmin.tsx +143 -25
- package/src/pages/StorageAdmin.tsx +8 -8
- package/src/pages/ThemeAdmin.tsx +15 -0
- package/src/templates/AdminPageTemplate.tsx +500 -111
- package/src/templates/PrivatePageTemplate.tsx +440 -59
- package/src/templates/SharedPageTemplate.tsx +432 -38
- package/src/templates/TemplateBoard.tsx +17 -8
- package/src/templates/TemplateCalendar.tsx +9 -4
- package/src/templates/TemplateTable.tsx +10 -1
package/package.json
CHANGED
package/src/pages/PagesAdmin.tsx
CHANGED
|
@@ -4,7 +4,7 @@ import { DashboardNav } from '../components/DashboardNav';
|
|
|
4
4
|
import { useAuth } from '../lib/AuthContext';
|
|
5
5
|
import { Shield, Eye, EyeOff, Lock, Globe, Plus, Trash2, Power, ChevronUp, ChevronDown, Layers, Loader2, Save, Copy, RotateCcw, CheckCircle2 } from 'lucide-react';
|
|
6
6
|
import { Button } from '../components/Button';
|
|
7
|
-
import { db } from '../lib/firebase';
|
|
7
|
+
import { db, storage } from '../lib/firebase';
|
|
8
8
|
import { doc, onSnapshot, setDoc, collection, getDocs, getDoc } from 'firebase/firestore';
|
|
9
9
|
import { ConfirmModal } from '../components/ConfirmModal';
|
|
10
10
|
import { JsonNode } from './ThemeAdmin';
|
|
@@ -50,7 +50,10 @@ export function PagesAdmin() {
|
|
|
50
50
|
const location = useLocation();
|
|
51
51
|
const config = useConfig();
|
|
52
52
|
const [pages, setPages] = useState<PageItem[]>(initialPages);
|
|
53
|
-
const [expandedPageId, setExpandedPageId] = useState<string | null>(
|
|
53
|
+
const [expandedPageId, setExpandedPageId] = useState<string | null>(() => {
|
|
54
|
+
try { return sessionStorage.getItem('fbos_pages_expandedId') || null; } catch { return null; }
|
|
55
|
+
});
|
|
56
|
+
const expandedPageIdRef = useRef(expandedPageId);
|
|
54
57
|
const [isAddingPage, setIsAddingPage] = useState(false);
|
|
55
58
|
const [draftConfigs, setDraftConfigs] = useState<Record<string, any>>({});
|
|
56
59
|
const [savingConfigMap, setSavingConfigMap] = useState<Record<string, boolean>>({});
|
|
@@ -102,6 +105,32 @@ export function PagesAdmin() {
|
|
|
102
105
|
}
|
|
103
106
|
}, [location.pathname, pages]);
|
|
104
107
|
|
|
108
|
+
// Persist expanded page ID to sessionStorage and keep ref in sync
|
|
109
|
+
useEffect(() => {
|
|
110
|
+
expandedPageIdRef.current = expandedPageId;
|
|
111
|
+
try {
|
|
112
|
+
if (expandedPageId) sessionStorage.setItem('fbos_pages_expandedId', expandedPageId);
|
|
113
|
+
else sessionStorage.removeItem('fbos_pages_expandedId');
|
|
114
|
+
} catch {}
|
|
115
|
+
}, [expandedPageId]);
|
|
116
|
+
|
|
117
|
+
// Persist scroll position
|
|
118
|
+
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
|
119
|
+
useEffect(() => {
|
|
120
|
+
const el = scrollContainerRef.current;
|
|
121
|
+
if (!el) return;
|
|
122
|
+
// Restore scroll position on mount
|
|
123
|
+
try {
|
|
124
|
+
const saved = sessionStorage.getItem('fbos_pages_scrollY');
|
|
125
|
+
if (saved) el.scrollTop = parseInt(saved, 10);
|
|
126
|
+
} catch {}
|
|
127
|
+
const handleScroll = () => {
|
|
128
|
+
try { sessionStorage.setItem('fbos_pages_scrollY', String(el.scrollTop)); } catch {}
|
|
129
|
+
};
|
|
130
|
+
el.addEventListener('scroll', handleScroll, { passive: true });
|
|
131
|
+
return () => el.removeEventListener('scroll', handleScroll);
|
|
132
|
+
}, []);
|
|
133
|
+
|
|
105
134
|
|
|
106
135
|
|
|
107
136
|
useEffect(() => {
|
|
@@ -234,7 +263,13 @@ export function PagesAdmin() {
|
|
|
234
263
|
} catch (e) { }
|
|
235
264
|
await setDoc(doc(db, collectionName, targetId), val);
|
|
236
265
|
|
|
266
|
+
// Batch pages + expandedPageId update together to prevent collapse
|
|
237
267
|
setPages(prev => prev.map(p => p.id === id ? { ...p, id: targetId, _uiKey: p._uiKey || id, path: val.route, title: val.pageName || val.tabName || val.title || p.title } : p));
|
|
268
|
+
// Use ref to read the LATEST expandedPageId (closure value is stale after async await)
|
|
269
|
+
if (expandedPageIdRef.current === id) {
|
|
270
|
+
setExpandedPageId(targetId);
|
|
271
|
+
window.history.replaceState(null, '', `/pages/${targetId}`);
|
|
272
|
+
}
|
|
238
273
|
setDraftConfigs(prev => {
|
|
239
274
|
const next = { ...prev };
|
|
240
275
|
next[targetId] = val;
|
|
@@ -242,11 +277,6 @@ export function PagesAdmin() {
|
|
|
242
277
|
return next;
|
|
243
278
|
});
|
|
244
279
|
|
|
245
|
-
if (expandedPageId === id) {
|
|
246
|
-
setExpandedPageId(targetId);
|
|
247
|
-
window.history.replaceState(null, '', `/pages/${targetId}`);
|
|
248
|
-
}
|
|
249
|
-
|
|
250
280
|
// Append new tab to the TOP of the order list
|
|
251
281
|
const isPublicPage = pageType === 'public';
|
|
252
282
|
const orderDocId = isPublicPage ? 'page_order' : 'tab_order';
|
|
@@ -306,19 +336,27 @@ export function PagesAdmin() {
|
|
|
306
336
|
const newId = 'draft_' + Math.floor(Math.random() * 10000);
|
|
307
337
|
setPages([...pages, { id: newId, title: 'New Tab', path: '/draft', type, isSystem: false, enabled: true }]);
|
|
308
338
|
|
|
309
|
-
|
|
339
|
+
// Count existing pages of this type to generate a unique numbered route
|
|
340
|
+
const existingCount = pages.filter(p => p.type === type && !p.id.startsWith('draft_')).length;
|
|
341
|
+
const routeNum = existingCount + 1;
|
|
342
|
+
|
|
343
|
+
let defaultPayload: any = {};
|
|
310
344
|
if (type === 'public') {
|
|
311
|
-
defaultPayload = { ...defaultPublicConfig };
|
|
345
|
+
defaultPayload = { ...defaultPublicConfig, route: `/page-${routeNum}` };
|
|
312
346
|
} else if (type === 'private') {
|
|
313
|
-
defaultPayload = { ...defaultPrivateConfig };
|
|
347
|
+
defaultPayload = { ...defaultPrivateConfig, route: `/private-tab-${routeNum}` };
|
|
314
348
|
} else if (type === 'admin') {
|
|
315
|
-
defaultPayload = { ...defaultAdminConfig };
|
|
349
|
+
defaultPayload = { ...defaultAdminConfig, route: `/admin-tab-${routeNum}` };
|
|
316
350
|
} else if (type === 'shared') {
|
|
317
|
-
defaultPayload = { ...defaultSharedConfig };
|
|
351
|
+
defaultPayload = { ...defaultSharedConfig, route: `/shared-tab-${routeNum}` };
|
|
318
352
|
}
|
|
319
353
|
setDraftConfigs(p => ({ ...p, [newId]: defaultPayload }));
|
|
320
354
|
setExpandedPageId(newId);
|
|
321
355
|
setIsAddingPage(false);
|
|
356
|
+
|
|
357
|
+
// Immediately save the draft to Firestore so the route is resolvable
|
|
358
|
+
handleSaveConfig(newId, { ...defaultPayload, pageType: type !== 'public' ? type : undefined });
|
|
359
|
+
|
|
322
360
|
setTimeout(() => {
|
|
323
361
|
document.getElementById(`page-${newId}`)?.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
324
362
|
}, 100);
|
|
@@ -367,30 +405,110 @@ export function PagesAdmin() {
|
|
|
367
405
|
if (!deleteConfirmId) return;
|
|
368
406
|
setIsDeleting(true);
|
|
369
407
|
try {
|
|
370
|
-
const { deleteDoc, collection, getDocs, doc } = await import('firebase/firestore');
|
|
408
|
+
const { deleteDoc, collection, getDocs, doc, getDoc: getDocFn, query, where } = await import('firebase/firestore');
|
|
409
|
+
const { ref, deleteObject } = await import('firebase/storage');
|
|
371
410
|
const page = pages.find(p => p.id === deleteConfirmId);
|
|
372
411
|
const collectionName = page?.type === 'public' ? 'sys_pages' : 'sys_tabs';
|
|
373
|
-
await deleteDoc(doc(db, collectionName, deleteConfirmId));
|
|
374
412
|
|
|
375
413
|
if (collectionName === 'sys_tabs' && page) {
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
414
|
+
// Read the tab config to derive the correct collection names
|
|
415
|
+
// (templates use config.tabName || config.pageId, lowercased + sanitized)
|
|
416
|
+
let tabConfig = draftConfigs[deleteConfirmId];
|
|
417
|
+
if (!tabConfig) {
|
|
418
|
+
try {
|
|
419
|
+
const tabDoc = await getDocFn(doc(db, 'sys_tabs', deleteConfirmId));
|
|
420
|
+
if (tabDoc.exists()) tabConfig = tabDoc.data();
|
|
421
|
+
} catch {}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const parsedTabName = ((tabConfig?.tabName || tabConfig?.pageId || deleteConfirmId) as string)
|
|
425
|
+
.toLowerCase().replace(/[^a-z0-9]+/g, '_');
|
|
426
|
+
|
|
427
|
+
const collectionsClean = [
|
|
428
|
+
`admin_${parsedTabName}_records`,
|
|
429
|
+
`mem_${parsedTabName}_records`,
|
|
430
|
+
`user_${parsedTabName}_records`
|
|
431
|
+
];
|
|
383
432
|
|
|
384
|
-
|
|
433
|
+
// Storage folder prefixes and their Drive collections
|
|
434
|
+
const storageMap: Record<string, { prefix: string; driveCollection: string }> = {
|
|
435
|
+
[`admin_${parsedTabName}_records`]: { prefix: 'admin_files', driveCollection: 'admin_files' },
|
|
436
|
+
[`mem_${parsedTabName}_records`]: { prefix: 'mem_files', driveCollection: 'mem_files' },
|
|
437
|
+
[`user_${parsedTabName}_records`]: { prefix: 'user_files', driveCollection: 'user_files' },
|
|
438
|
+
};
|
|
439
|
+
|
|
440
|
+
for (const col of collectionsClean) {
|
|
441
|
+
try {
|
|
385
442
|
const recordsSnap = await getDocs(collection(db, col));
|
|
443
|
+
const mapping = storageMap[col];
|
|
444
|
+
|
|
445
|
+
// Delete files from Storage and Drive for file-type records
|
|
446
|
+
for (const d of recordsSnap.docs) {
|
|
447
|
+
const data = d.data();
|
|
448
|
+
if (data.recordType === 'file' && data.downloadURL && mapping) {
|
|
449
|
+
try {
|
|
450
|
+
const encodedPrefix = `${mapping.prefix}%2F`;
|
|
451
|
+
const fileName = data.downloadURL.split(encodedPrefix)[1]?.split('?')[0];
|
|
452
|
+
if (fileName) {
|
|
453
|
+
const fileRef = ref(storage, `${mapping.prefix}/${decodeURIComponent(fileName)}`);
|
|
454
|
+
await deleteObject(fileRef).catch(() => {});
|
|
455
|
+
}
|
|
456
|
+
// Remove matching Drive metadata doc by downloadURL
|
|
457
|
+
const driveSnap = await getDocs(query(
|
|
458
|
+
collection(db, mapping.driveCollection),
|
|
459
|
+
where('downloadURL', '==', data.downloadURL)
|
|
460
|
+
));
|
|
461
|
+
for (const driveDoc of driveSnap.docs) {
|
|
462
|
+
await deleteDoc(driveDoc.ref);
|
|
463
|
+
}
|
|
464
|
+
} catch (fileErr) {
|
|
465
|
+
console.error('Error deleting storage file:', fileErr);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// Delete all record documents in this collection
|
|
386
471
|
const deletePromises = recordsSnap.docs.map(d => deleteDoc(d.ref));
|
|
387
472
|
await Promise.all(deletePromises);
|
|
473
|
+
|
|
474
|
+
// Clear localStorage cache for this collection
|
|
475
|
+
try { localStorage.removeItem(`fbos_records_${col}`); } catch {}
|
|
476
|
+
} catch (colErr) {
|
|
477
|
+
console.error(`Error cleaning up collection ${col}:`, colErr);
|
|
388
478
|
}
|
|
389
|
-
}
|
|
390
|
-
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// Also clean Drive collections by sourceTab (catches any orphaned file docs)
|
|
482
|
+
const driveCollections = ['admin_files', 'mem_files', 'user_files'];
|
|
483
|
+
for (const driveColl of driveCollections) {
|
|
484
|
+
try {
|
|
485
|
+
const orphanedSnap = await getDocs(query(
|
|
486
|
+
collection(db, driveColl),
|
|
487
|
+
where('sourceTab', '==', parsedTabName)
|
|
488
|
+
));
|
|
489
|
+
for (const orphanDoc of orphanedSnap.docs) {
|
|
490
|
+
const data = orphanDoc.data();
|
|
491
|
+
// Also delete the actual file from Storage if we haven't already
|
|
492
|
+
if (data.downloadURL) {
|
|
493
|
+
try {
|
|
494
|
+
const prefix = driveColl.replace('_files', '_files');
|
|
495
|
+
const encodedPrefix = `${prefix}%2F`;
|
|
496
|
+
const fName = data.downloadURL.split(encodedPrefix)[1]?.split('?')[0];
|
|
497
|
+
if (fName) {
|
|
498
|
+
const fRef = ref(storage, `${prefix}/${decodeURIComponent(fName)}`);
|
|
499
|
+
await deleteObject(fRef).catch(() => {});
|
|
500
|
+
}
|
|
501
|
+
} catch {}
|
|
502
|
+
}
|
|
503
|
+
await deleteDoc(orphanDoc.ref);
|
|
504
|
+
}
|
|
505
|
+
} catch {}
|
|
391
506
|
}
|
|
392
507
|
}
|
|
393
508
|
|
|
509
|
+
// Delete the tab/page document itself
|
|
510
|
+
await deleteDoc(doc(db, collectionName, deleteConfirmId));
|
|
511
|
+
|
|
394
512
|
const orderDocId = collectionName === 'sys_pages' ? 'page_order' : 'tab_order';
|
|
395
513
|
const currentOrderItems = collectionName === 'sys_pages' ? pageOrderItems : tabOrderItems;
|
|
396
514
|
const { setDoc } = await import('firebase/firestore');
|
|
@@ -545,7 +663,7 @@ export function PagesAdmin() {
|
|
|
545
663
|
</div>
|
|
546
664
|
</div>
|
|
547
665
|
</div>
|
|
548
|
-
<div className="px-6 md:px-10 pb-6 md:pb-10 flex flex-col pt-0 gap-10 overflow-y-auto mt-2 relative min-h-[400px]">
|
|
666
|
+
<div ref={scrollContainerRef} className="px-6 md:px-10 pb-6 md:pb-10 flex flex-col pt-0 gap-10 overflow-y-auto mt-2 relative min-h-[400px]">
|
|
549
667
|
<AnimatePresence>
|
|
550
668
|
{isResettingAll && (
|
|
551
669
|
<motion.div
|
|
@@ -185,7 +185,7 @@ export function StorageAdmin() {
|
|
|
185
185
|
|
|
186
186
|
return onSnapshot(q, (snap) => {
|
|
187
187
|
const newData = (snap.docs || []).map(d => ({ _id: d.id, accessPrefix: pfx as any, ...d.data() }) as StorageRecord);
|
|
188
|
-
|
|
188
|
+
|
|
189
189
|
setRecords(prev => {
|
|
190
190
|
// Filter out existing records for this prefix and add new ones
|
|
191
191
|
const otherPrefixes = prev.filter(r => r.accessPrefix !== pfx);
|
|
@@ -381,7 +381,7 @@ export function StorageAdmin() {
|
|
|
381
381
|
</div>
|
|
382
382
|
</motion.div>
|
|
383
383
|
|
|
384
|
-
<div
|
|
384
|
+
<div
|
|
385
385
|
className="flex flex-col gap-6 flex-1 pb-16"
|
|
386
386
|
onDragOver={handleDragOver}
|
|
387
387
|
onDragLeave={handleDragLeave}
|
|
@@ -515,7 +515,7 @@ export function StorageAdmin() {
|
|
|
515
515
|
onBlur={() => executeRename()}
|
|
516
516
|
onKeyDown={e => { if (e.key === 'Enter') executeRename(); if (e.key === 'Escape') setRenameFile(null); }}
|
|
517
517
|
onClick={e => e.stopPropagation()}
|
|
518
|
-
className="text-[12.5px] font-bold text-foreground bg-transparent border-b border-accent/30 outline-none pb-0.5 w-full"
|
|
518
|
+
className="no-glow text-[12.5px] font-bold text-foreground bg-transparent border-b border-accent/30 outline-none pb-0.5 w-full"
|
|
519
519
|
/>
|
|
520
520
|
) : (
|
|
521
521
|
<span className="text-[12.5px] font-bold text-foreground truncate select-none group-hover:text-accent transition-colors" title={rec.fileName}>{rec.fileName}</span>
|
|
@@ -624,7 +624,7 @@ export function StorageAdmin() {
|
|
|
624
624
|
value={stagedName}
|
|
625
625
|
onChange={e => setStagedName(e.target.value)}
|
|
626
626
|
disabled={isUploading}
|
|
627
|
-
className="w-full bg-foreground/[0.02] border border-[var(--panel-border)] rounded-xl px-4 py-3.5 text-[14px] font-bold text-foreground outline-none focus:border-accent transition-colors shadow-sm"
|
|
627
|
+
className="no-glow w-full bg-foreground/[0.02] border border-[var(--panel-border)] rounded-xl px-4 py-3.5 text-[14px] font-bold text-foreground outline-none focus:border-accent transition-colors shadow-sm"
|
|
628
628
|
/>
|
|
629
629
|
</div>
|
|
630
630
|
|
|
@@ -731,7 +731,7 @@ export function StorageAdmin() {
|
|
|
731
731
|
<ConfirmModal
|
|
732
732
|
isOpen={!!confirmDelete}
|
|
733
733
|
title="Confirm Deletion"
|
|
734
|
-
message={<>Are you
|
|
734
|
+
message={<>Are you sure you want to delete <strong className="text-foreground block my-0.5 text-left overflow-hidden text-ellipsis whitespace-nowrap max-w-full">{confirmDelete?.name}</strong> This action is permanent.</>}
|
|
735
735
|
onConfirm={executeDelete}
|
|
736
736
|
onCancel={() => setConfirmDelete(null)}
|
|
737
737
|
isProcessing={isDeleting}
|
|
@@ -740,13 +740,13 @@ export function StorageAdmin() {
|
|
|
740
740
|
|
|
741
741
|
<AnimatePresence>
|
|
742
742
|
{isDragging && (
|
|
743
|
-
<motion.div
|
|
743
|
+
<motion.div
|
|
744
744
|
initial={{ opacity: 0 }}
|
|
745
745
|
animate={{ opacity: 1 }}
|
|
746
746
|
exit={{ opacity: 0 }}
|
|
747
747
|
className="fixed inset-0 z-[1000] bg-background/60 backdrop-blur-md pointer-events-none flex items-center justify-center p-6"
|
|
748
748
|
>
|
|
749
|
-
<motion.div
|
|
749
|
+
<motion.div
|
|
750
750
|
initial={{ scale: 0.9, opacity: 0 }}
|
|
751
751
|
animate={{ scale: 1, opacity: 1 }}
|
|
752
752
|
exit={{ scale: 0.9, opacity: 0 }}
|
|
@@ -754,7 +754,7 @@ export function StorageAdmin() {
|
|
|
754
754
|
>
|
|
755
755
|
<div className="absolute inset-0 bg-accent/5 rounded-[38px] animate-pulse" />
|
|
756
756
|
<div className="w-20 h-20 rounded-full bg-accent/10 flex items-center justify-center text-accent shadow-[0_0_40px_rgba(var(--accent-rgb),0.2)]">
|
|
757
|
-
|
|
757
|
+
<UploadCloud className="w-10 h-10 animate-bounce" />
|
|
758
758
|
</div>
|
|
759
759
|
<div className="flex flex-col items-center text-center gap-2 relative z-10">
|
|
760
760
|
<span className="text-[24px] font-extrabold text-foreground tracking-tight">Drop files to upload</span>
|
package/src/pages/ThemeAdmin.tsx
CHANGED
|
@@ -607,6 +607,21 @@ export function ThemeAdmin() {
|
|
|
607
607
|
const [copiedStatus, setCopiedStatus] = useState<boolean>(false);
|
|
608
608
|
const [showResetConfirm, setShowResetConfirm] = useState<boolean>(false);
|
|
609
609
|
|
|
610
|
+
// Persist scroll position
|
|
611
|
+
useEffect(() => {
|
|
612
|
+
try {
|
|
613
|
+
const saved = sessionStorage.getItem('fbos_theme_scrollY');
|
|
614
|
+
if (saved) {
|
|
615
|
+
requestAnimationFrame(() => window.scrollTo(0, parseInt(saved, 10)));
|
|
616
|
+
}
|
|
617
|
+
} catch {}
|
|
618
|
+
const handleScroll = () => {
|
|
619
|
+
try { sessionStorage.setItem('fbos_theme_scrollY', String(window.scrollY)); } catch {}
|
|
620
|
+
};
|
|
621
|
+
window.addEventListener('scroll', handleScroll, { passive: true });
|
|
622
|
+
return () => window.removeEventListener('scroll', handleScroll);
|
|
623
|
+
}, []);
|
|
624
|
+
|
|
610
625
|
// Auto-Save: only fires when localConfig actually changes compared to
|
|
611
626
|
// what we last saved. Does NOT depend on activeConfig to avoid the
|
|
612
627
|
// Firestore snapshot echo loop.
|