your-ai-workflow-firebase-os 1.2.6 → 1.2.8
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/your-ai-workflow-firebase-os.cjs.js +13 -13
- package/dist/your-ai-workflow-firebase-os.es.js +13385 -13554
- package/package.json +4 -3
- package/src/pages/PagesAdmin.tsx +21 -124
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "your-ai-workflow-firebase-os",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.8",
|
|
4
4
|
"description": "A complete Firebase-powered admin OS — one React component.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/your-ai-workflow-firebase-os.cjs.js",
|
|
@@ -32,7 +32,8 @@
|
|
|
32
32
|
"build:lib": "vite build",
|
|
33
33
|
"lint": "eslint .",
|
|
34
34
|
"preview": "vite preview",
|
|
35
|
-
"postinstall": "node scripts/postinstall.js"
|
|
35
|
+
"postinstall": "node scripts/postinstall.js",
|
|
36
|
+
"release": "npm version patch && npm run build:lib && npm pack"
|
|
36
37
|
},
|
|
37
38
|
"dependencies": {
|
|
38
39
|
"@eslint/js": "^9.39.4",
|
|
@@ -78,4 +79,4 @@
|
|
|
78
79
|
"react-dom": ">=18",
|
|
79
80
|
"react-router-dom": ">=6"
|
|
80
81
|
}
|
|
81
|
-
}
|
|
82
|
+
}
|
package/src/pages/PagesAdmin.tsx
CHANGED
|
@@ -9,8 +9,6 @@ import { doc, onSnapshot, setDoc, collection, getDocs, getDoc } from 'firebase/f
|
|
|
9
9
|
import { ConfirmModal } from '../components/ConfirmModal';
|
|
10
10
|
import { JsonNode } from './ThemeAdmin';
|
|
11
11
|
import { defaultHomeConfig } from '../configs/pages/home.config';
|
|
12
|
-
import { defaultContactConfig } from '../configs/pages/contact.config';
|
|
13
|
-
import { defaultSupportConfig } from '../configs/pages/support.config';
|
|
14
12
|
import { defaultPublicConfig } from '../configs/pages/pub.config';
|
|
15
13
|
import { userPageConfig as defaultPrivateConfig } from '../configs/pages/user.config';
|
|
16
14
|
import { adminPageConfig as defaultAdminConfig } from '../configs/pages/admin.config';
|
|
@@ -31,10 +29,6 @@ interface PageItem {
|
|
|
31
29
|
const initialPages: PageItem[] = [
|
|
32
30
|
// Public
|
|
33
31
|
{ id: 'home', title: 'Home Page', path: '/', type: 'public', isSystem: true, enabled: true },
|
|
34
|
-
{ id: 'contact', title: 'Contact', path: '/contact', type: 'public', isSystem: true, enabled: true },
|
|
35
|
-
|
|
36
|
-
// Private
|
|
37
|
-
{ id: 'support', title: 'Request Support', path: '/support', type: 'private', isSystem: true, enabled: true },
|
|
38
32
|
];
|
|
39
33
|
|
|
40
34
|
const Toggle = ({ enabled, onChange, disabled, className = '' }: { enabled: boolean, onChange: (val: boolean) => void, disabled?: boolean, className?: string }) => (
|
|
@@ -76,14 +70,6 @@ export function PagesAdmin() {
|
|
|
76
70
|
const [homeConfig, setHomeConfig] = useState<any>(null);
|
|
77
71
|
const [savingHome, setSavingHome] = useState(false);
|
|
78
72
|
const [resettingHome, setResettingHome] = useState(false);
|
|
79
|
-
|
|
80
|
-
const [contactConfig, setContactConfig] = useState<any>(null);
|
|
81
|
-
const [savingContact, setSavingContact] = useState(false);
|
|
82
|
-
const [resettingContact, setResettingContact] = useState(false);
|
|
83
|
-
|
|
84
|
-
const [supportConfig, setSupportConfig] = useState<any>(null);
|
|
85
|
-
const [savingSupport, setSavingSupport] = useState(false);
|
|
86
|
-
const [resettingSupport, setResettingSupport] = useState(false);
|
|
87
73
|
// ── Order-based visibility (single source of truth) ─────────────────────
|
|
88
74
|
const [pageHiddenIds, setPageHiddenIds] = useState<Set<string>>(new Set());
|
|
89
75
|
const [tabHiddenIds, setTabHiddenIds] = useState<Set<string>>(new Set());
|
|
@@ -126,13 +112,7 @@ export function PagesAdmin() {
|
|
|
126
112
|
const unsubHome = onSnapshot(doc(db, 'sys_pages', 'home'), (docSnap) => {
|
|
127
113
|
setHomeConfig(docSnap.exists() ? mergeConfig(defaultHomeConfig, docSnap.data()) : defaultHomeConfig);
|
|
128
114
|
});
|
|
129
|
-
|
|
130
|
-
setContactConfig(docSnap.exists() ? mergeConfig(defaultContactConfig, docSnap.data()) : defaultContactConfig);
|
|
131
|
-
});
|
|
132
|
-
const unsubSupport = onSnapshot(doc(db, 'sys_tabs', 'support'), (docSnap) => {
|
|
133
|
-
setSupportConfig(docSnap.exists() ? mergeConfig(defaultSupportConfig, docSnap.data()) : defaultSupportConfig);
|
|
134
|
-
});
|
|
135
|
-
return () => { unsubHome(); unsubContact(); unsubSupport(); };
|
|
115
|
+
return () => { unsubHome(); };
|
|
136
116
|
}, []);
|
|
137
117
|
|
|
138
118
|
useEffect(() => {
|
|
@@ -199,12 +179,21 @@ export function PagesAdmin() {
|
|
|
199
179
|
const [copyStateId, setCopyStateId] = useState<string | null>(null);
|
|
200
180
|
|
|
201
181
|
const handleCopyBtn = (id: string, val: any) => {
|
|
202
|
-
let str
|
|
182
|
+
let str: string;
|
|
183
|
+
if (id === 'home') {
|
|
184
|
+
// Output a complete TypeScript file so it can be pasted directly into home.config.ts
|
|
185
|
+
const json = JSON.stringify(val, null, 2);
|
|
186
|
+
str = `export const defaultHomeConfig = ${json};\n`;
|
|
187
|
+
} else {
|
|
188
|
+
str = JSON.stringify(val, null, 2);
|
|
189
|
+
}
|
|
203
190
|
navigator.clipboard.writeText(str);
|
|
204
191
|
setCopyStateId(id);
|
|
205
192
|
setTimeout(() => setCopyStateId(null), 2000);
|
|
206
193
|
};
|
|
207
194
|
|
|
195
|
+
|
|
196
|
+
|
|
208
197
|
const handleSaveConfig = async (id: string, val: any) => {
|
|
209
198
|
if (val.route && !val.route.startsWith('/')) {
|
|
210
199
|
val.route = '/' + val.route;
|
|
@@ -218,14 +207,12 @@ export function PagesAdmin() {
|
|
|
218
207
|
val.pageType = pageType;
|
|
219
208
|
}
|
|
220
209
|
|
|
221
|
-
const isSystemPage = ['home'
|
|
210
|
+
const isSystemPage = ['home'].includes(id);
|
|
222
211
|
const targetId = isSystemPage
|
|
223
212
|
? id
|
|
224
213
|
: (val.route ? val.route.replace(/^\/+/, '').replace(/[^a-zA-Z0-9_\-]/g, '_').toLowerCase() : id);
|
|
225
214
|
|
|
226
215
|
if (id === 'home') setSavingHome(true);
|
|
227
|
-
else if (id === 'contact') setSavingContact(true);
|
|
228
|
-
else if (id === 'support') setSavingSupport(true);
|
|
229
216
|
else setSavingConfigMap(p => ({ ...p, [id]: true, [targetId]: true }));
|
|
230
217
|
|
|
231
218
|
try {
|
|
@@ -267,8 +254,6 @@ export function PagesAdmin() {
|
|
|
267
254
|
} else {
|
|
268
255
|
await setDoc(doc(db, collectionName, id), val);
|
|
269
256
|
if (id === 'home') setHomeConfig(val);
|
|
270
|
-
else if (id === 'contact') setContactConfig(val);
|
|
271
|
-
else if (id === 'support') setSupportConfig(val);
|
|
272
257
|
else {
|
|
273
258
|
setDraftConfigs(p => ({ ...p, [id]: val }));
|
|
274
259
|
setPages(prev => prev.map(p => p.id === id ? { ...p, path: val.route || p.path, title: val.pageName || val.tabName || val.title || p.title } : p));
|
|
@@ -279,8 +264,6 @@ export function PagesAdmin() {
|
|
|
279
264
|
} finally {
|
|
280
265
|
setTimeout(() => {
|
|
281
266
|
if (id === 'home') setSavingHome(false);
|
|
282
|
-
else if (id === 'contact') setSavingContact(false);
|
|
283
|
-
else if (id === 'support') setSavingSupport(false);
|
|
284
267
|
else setSavingConfigMap(p => ({ ...p, [id]: false, [targetId]: false }));
|
|
285
268
|
}, 500);
|
|
286
269
|
}
|
|
@@ -292,17 +275,6 @@ export function PagesAdmin() {
|
|
|
292
275
|
setTimeout(() => setResettingHome(false), 500);
|
|
293
276
|
};
|
|
294
277
|
|
|
295
|
-
const handleResetContact = async (e: React.MouseEvent) => {
|
|
296
|
-
e.stopPropagation(); setResettingContact(true);
|
|
297
|
-
await handleSaveConfig('contact', defaultContactConfig);
|
|
298
|
-
setTimeout(() => setResettingContact(false), 500);
|
|
299
|
-
};
|
|
300
|
-
|
|
301
|
-
const handleResetSupport = async (e: React.MouseEvent) => {
|
|
302
|
-
e.stopPropagation(); setResettingSupport(true);
|
|
303
|
-
await handleSaveConfig('support', defaultSupportConfig);
|
|
304
|
-
setTimeout(() => setResettingSupport(false), 500);
|
|
305
|
-
};
|
|
306
278
|
|
|
307
279
|
const handleResetPage = async (e: React.MouseEvent, pageId: string, defConfig: any) => {
|
|
308
280
|
e.stopPropagation(); setResettingMap(p => ({ ...p, [pageId]: true }));
|
|
@@ -428,12 +400,11 @@ export function PagesAdmin() {
|
|
|
428
400
|
setPages(prev => [...prev, newPage]);
|
|
429
401
|
|
|
430
402
|
let sourceConfig = draftConfigs[originalPage.id];
|
|
431
|
-
if (!sourceConfig) {
|
|
432
|
-
|
|
433
|
-
if (originalPage.id === 'support') sourceConfig = supportConfig || defaultSupportConfig;
|
|
403
|
+
if (!sourceConfig && originalPage.id === 'home') {
|
|
404
|
+
sourceConfig = homeConfig;
|
|
434
405
|
}
|
|
435
406
|
|
|
436
|
-
const newConfig = { ...sourceConfig, route: newPage.path, pageName: newPage.title };
|
|
407
|
+
const newConfig = { ...(sourceConfig || {}), route: newPage.path, pageName: newPage.title };
|
|
437
408
|
if (newPage.type !== 'public') {
|
|
438
409
|
newConfig.tabName = newPage.title;
|
|
439
410
|
newConfig.pageType = newPage.type;
|
|
@@ -483,13 +454,9 @@ export function PagesAdmin() {
|
|
|
483
454
|
try {
|
|
484
455
|
const { deleteDoc } = await import('firebase/firestore');
|
|
485
456
|
await Promise.all([
|
|
486
|
-
deleteDoc(doc(db, 'sys_pages', 'home'))
|
|
487
|
-
deleteDoc(doc(db, 'sys_pages', 'contact')),
|
|
488
|
-
deleteDoc(doc(db, 'sys_tabs', 'support'))
|
|
457
|
+
deleteDoc(doc(db, 'sys_pages', 'home'))
|
|
489
458
|
]);
|
|
490
459
|
setHomeConfig(defaultHomeConfig);
|
|
491
|
-
setContactConfig(defaultContactConfig);
|
|
492
|
-
setSupportConfig(defaultSupportConfig);
|
|
493
460
|
} catch (e) {
|
|
494
461
|
console.error("Error resetting defaults:", e);
|
|
495
462
|
}
|
|
@@ -699,10 +666,8 @@ export function PagesAdmin() {
|
|
|
699
666
|
|
|
700
667
|
const renderPage = (page: PageItem, pIdx: number, isHidden: boolean) => {
|
|
701
668
|
const isHome = page.id === 'home';
|
|
702
|
-
const isContact = page.id === 'contact';
|
|
703
|
-
const isSupport = page.id === 'support';
|
|
704
669
|
const isExpanded = expandedPageId === page.id;
|
|
705
|
-
const isConfigurable = isHome ||
|
|
670
|
+
const isConfigurable = isHome || !page.isSystem;
|
|
706
671
|
|
|
707
672
|
return (
|
|
708
673
|
<div
|
|
@@ -721,7 +686,7 @@ export function PagesAdmin() {
|
|
|
721
686
|
<div className="flex items-center gap-1 w-full flex-1">
|
|
722
687
|
<div className="flex items-center gap-2">
|
|
723
688
|
<h3 className="text-[14px] font-bold text-foreground truncate">{page.title}</h3>
|
|
724
|
-
{
|
|
689
|
+
{isHome && savingHome && <Loader2 className="w-3 h-3 animate-spin text-accent ml-2" />}
|
|
725
690
|
</div>
|
|
726
691
|
<div className={`text-[11px] font-medium font-mono px-2 py-0.5 rounded w-fit ml-6 border ${isHidden ? 'text-foreground/50 border-foreground/10 bg-foreground/5' : 'text-accent/70 bg-accent/5 border-accent/10'}`}>
|
|
727
692
|
{window.location.host}{page.path}
|
|
@@ -780,6 +745,7 @@ export function PagesAdmin() {
|
|
|
780
745
|
<button onClick={(e) => { e.stopPropagation(); handleCopyBtn('home', homeConfig); }} className="w-8 h-8 p-0 border-transparent bg-transparent hover:bg-foreground/10 rounded-lg flex items-center justify-center shrink-0 transition-colors" title="Copy JSON">
|
|
781
746
|
{copyStateId === 'home' ? <CheckCircle2 className="w-3.5 h-3.5 text-emerald-500" /> : <Copy className="w-3.5 h-3.5 text-foreground/40 hover:text-foreground transition-colors" />}
|
|
782
747
|
</button>
|
|
748
|
+
|
|
783
749
|
</div>
|
|
784
750
|
<JsonNode
|
|
785
751
|
value={homeConfig}
|
|
@@ -795,77 +761,7 @@ export function PagesAdmin() {
|
|
|
795
761
|
</div>
|
|
796
762
|
</motion.div>
|
|
797
763
|
)}
|
|
798
|
-
{isExpanded && isContact && contactConfig && (
|
|
799
|
-
<motion.div
|
|
800
|
-
initial={{ height: 0, opacity: 0 }}
|
|
801
|
-
animate={{ height: "auto", opacity: 1 }}
|
|
802
|
-
exit={{ height: 0, opacity: 0 }}
|
|
803
|
-
className="overflow-hidden mt-4 pt-4 border-t border-[var(--panel-border)]/50 pb-4"
|
|
804
|
-
onClick={e => e.stopPropagation()}
|
|
805
|
-
>
|
|
806
|
-
<div className="flex flex-col font-mono text-[13px] leading-relaxed relative bg-background/50 p-4 py-6 rounded-xl border border-[var(--panel-border)] shadow-inner">
|
|
807
764
|
|
|
808
|
-
<div className="absolute top-4 right-4 flex items-center gap-1 z-20">
|
|
809
|
-
<button onClick={handleResetContact} className="w-8 h-8 p-0 border-transparent bg-transparent hover:bg-red-500/10 rounded-lg flex items-center justify-center shrink-0 transition-colors group" title="Reset to Defaults">
|
|
810
|
-
{resettingContact ? <Loader2 className="w-3.5 h-3.5 text-red-500 animate-spin" /> : <RotateCcw className="w-3.5 h-3.5 text-foreground/40 group-hover:text-red-500 transition-colors" />}
|
|
811
|
-
</button>
|
|
812
|
-
<button onClick={(e) => { e.stopPropagation(); handleCopyBtn('contact', contactConfig); }} className="w-8 h-8 p-0 border-transparent bg-transparent hover:bg-foreground/10 rounded-lg flex items-center justify-center shrink-0 transition-colors" title="Copy JSON">
|
|
813
|
-
{copyStateId === 'contact' ? <CheckCircle2 className="w-3.5 h-3.5 text-emerald-500" /> : <Copy className="w-3.5 h-3.5 text-foreground/40 hover:text-foreground transition-colors" />}
|
|
814
|
-
</button>
|
|
815
|
-
</div>
|
|
816
|
-
<JsonNode
|
|
817
|
-
value={contactConfig}
|
|
818
|
-
keysToRender={contactConfig.showButton !== false ? ['pageName', 'route', 'template', 'showButton', 'form'] : ['pageName', 'route', 'template', 'showButton']}
|
|
819
|
-
disabledKeys={['route']}
|
|
820
|
-
onChange={(val: any) => handleSaveConfig('contact', val)}
|
|
821
|
-
isLast={true}
|
|
822
|
-
isGlobalSaving={savingContact}
|
|
823
|
-
customEnums={{ form: publicForms, template: ['popup_form', 'inline_form'] }}
|
|
824
|
-
customExplanations={{
|
|
825
|
-
pageName: "This defines the text that actually appears globally in the Navigation menus.",
|
|
826
|
-
template: "This just simply tells our app when a user clicks on the contact page item what type of form do we load? is it a page or popup? this doesn't define form content! this setting just defines page layout.",
|
|
827
|
-
form: "This is the form that will be used. Go to /forms to edit it."
|
|
828
|
-
}}
|
|
829
|
-
/>
|
|
830
|
-
</div>
|
|
831
|
-
</motion.div>
|
|
832
|
-
)}
|
|
833
|
-
{isExpanded && isSupport && supportConfig && (
|
|
834
|
-
<motion.div
|
|
835
|
-
initial={{ height: 0, opacity: 0 }}
|
|
836
|
-
animate={{ height: "auto", opacity: 1 }}
|
|
837
|
-
exit={{ height: 0, opacity: 0 }}
|
|
838
|
-
className="overflow-hidden mt-4 pt-4 border-t border-[var(--panel-border)]/50 pb-4"
|
|
839
|
-
onClick={e => e.stopPropagation()}
|
|
840
|
-
>
|
|
841
|
-
<div className="flex flex-col font-mono text-[13px] leading-relaxed relative bg-background/50 p-4 py-6 rounded-xl border border-[var(--panel-border)] shadow-inner">
|
|
842
|
-
|
|
843
|
-
<div className="absolute top-4 right-4 flex items-center gap-1 z-20">
|
|
844
|
-
<button onClick={handleResetSupport} className="w-8 h-8 p-0 border-transparent bg-transparent hover:bg-red-500/10 rounded-lg flex items-center justify-center shrink-0 transition-colors group" title="Reset to Defaults">
|
|
845
|
-
{resettingSupport ? <Loader2 className="w-3.5 h-3.5 text-red-500 animate-spin" /> : <RotateCcw className="w-3.5 h-3.5 text-foreground/40 group-hover:text-red-500 transition-colors" />}
|
|
846
|
-
</button>
|
|
847
|
-
<button onClick={(e) => { e.stopPropagation(); handleCopyBtn('support', supportConfig); }} className="w-8 h-8 p-0 border-transparent bg-transparent hover:bg-foreground/10 rounded-lg flex items-center justify-center shrink-0 transition-colors" title="Copy JSON">
|
|
848
|
-
{copyStateId === 'support' ? <CheckCircle2 className="w-3.5 h-3.5 text-emerald-500" /> : <Copy className="w-3.5 h-3.5 text-foreground/40 hover:text-foreground transition-colors" />}
|
|
849
|
-
</button>
|
|
850
|
-
</div>
|
|
851
|
-
<JsonNode
|
|
852
|
-
value={supportConfig}
|
|
853
|
-
keysToRender={supportConfig.showButton !== false ? ['pageName', 'iconName', 'route', 'template', 'showButton', 'form'] : ['pageName', 'iconName', 'route', 'template', 'showButton']}
|
|
854
|
-
disabledKeys={['route']}
|
|
855
|
-
onChange={(val: any) => handleSaveConfig('support', val)}
|
|
856
|
-
isLast={true}
|
|
857
|
-
isGlobalSaving={savingSupport}
|
|
858
|
-
customEnums={{ form: privateForms, template: ['popup_form', 'inline_form'] }}
|
|
859
|
-
customExplanations={{
|
|
860
|
-
pageName: "Defines the textual title label rendering in internal sidebars.",
|
|
861
|
-
iconName: "To modify this icon, visit https://iconoir.com/, find an icon, and copy its exact name (e.g. 'learning', 'server'). Then paste it here.",
|
|
862
|
-
template: "This just simply tells our app when a user clicks on the support page item what type of form do we load? is it a page or popup? this doesn't define form content! this setting just defines page layout.",
|
|
863
|
-
form: "This is the form that will be used. Go to /forms to edit it."
|
|
864
|
-
}}
|
|
865
|
-
/>
|
|
866
|
-
</div>
|
|
867
|
-
</motion.div>
|
|
868
|
-
)}
|
|
869
765
|
{isExpanded && !page.isSystem && draftConfigs[page.id] && (
|
|
870
766
|
<motion.div
|
|
871
767
|
initial={{ height: 0, opacity: 0 }}
|
|
@@ -881,8 +777,9 @@ export function PagesAdmin() {
|
|
|
881
777
|
{resettingMap[page.id] ? <Loader2 className="w-3.5 h-3.5 text-red-500 animate-spin" /> : <RotateCcw className="w-3.5 h-3.5 text-foreground/40 group-hover:text-red-500 transition-colors" />}
|
|
882
778
|
</button>
|
|
883
779
|
<button onClick={(e) => { e.stopPropagation(); handleCopyBtn(page.id, draftConfigs[page.id]); }} className="w-8 h-8 p-0 border-transparent bg-transparent hover:bg-foreground/10 rounded-lg flex items-center justify-center shrink-0 transition-colors" title="Copy JSON">
|
|
884
|
-
{copyStateId === page.id ? <CheckCircle2 className="w-
|
|
780
|
+
{copyStateId === page.id ? <CheckCircle2 className="w-3.5 h-3.5 text-emerald-500" /> : <Copy className="w-3.5 h-3.5 text-foreground/40 hover:text-foreground transition-colors" />}
|
|
885
781
|
</button>
|
|
782
|
+
|
|
886
783
|
</div>
|
|
887
784
|
<JsonNode
|
|
888
785
|
value={draftConfigs[page.id]}
|