mirror of
https://github.com/johndoe6345789/metabuilder.git
synced 2026-04-25 06:14:59 +00:00
Generated by Spark: Import / export packages as zips, various images and other asserts can go in same zip
This commit is contained in:
264
PACKAGE_IMPORT_EXPORT.md
Normal file
264
PACKAGE_IMPORT_EXPORT.md
Normal file
@@ -0,0 +1,264 @@
|
||||
# Package Import/Export Guide
|
||||
|
||||
## Overview
|
||||
|
||||
The MetaBuilder Package Import/Export system allows you to package and share complete applications, features, or database snapshots as ZIP files. This enables modular development and easy distribution of pre-built functionality.
|
||||
|
||||
## Features
|
||||
|
||||
### Export Capabilities
|
||||
|
||||
1. **Custom Package Export**
|
||||
- Create reusable packages with selected components
|
||||
- Include or exclude specific data types
|
||||
- Add metadata (name, version, author, description, tags)
|
||||
- Automatic README generation
|
||||
|
||||
2. **Database Snapshot Export**
|
||||
- Complete backup of entire database
|
||||
- One-click export with timestamp
|
||||
- Includes all schemas, pages, workflows, scripts, and configurations
|
||||
|
||||
3. **Selective Export Options**
|
||||
- ✅ Data schemas
|
||||
- ✅ Page configurations
|
||||
- ✅ Workflows
|
||||
- ✅ Lua scripts
|
||||
- ✅ Component hierarchies
|
||||
- ✅ Component configurations
|
||||
- ✅ CSS classes
|
||||
- ✅ Dropdown configurations
|
||||
- ✅ Seed data
|
||||
- ✅ Assets (images, videos, audio, documents)
|
||||
|
||||
### Import Capabilities
|
||||
|
||||
1. **Package Installation**
|
||||
- Import packages from ZIP files
|
||||
- Automatic validation of package structure
|
||||
- Merge with existing data
|
||||
- Asset restoration
|
||||
|
||||
2. **Safety Features**
|
||||
- Package validation before import
|
||||
- Non-destructive merging (adds to existing data)
|
||||
- Import warnings and confirmations
|
||||
|
||||
## ZIP Package Structure
|
||||
|
||||
```
|
||||
package-name-1.0.0.zip
|
||||
├── manifest.json # Package metadata
|
||||
├── content.json # Database content
|
||||
├── README.md # Auto-generated documentation
|
||||
└── assets/ # Asset files
|
||||
├── asset-manifest.json
|
||||
├── images/
|
||||
│ └── *.png, *.jpg, *.svg
|
||||
├── videos/
|
||||
│ └── *.mp4, *.webm
|
||||
├── audios/
|
||||
│ └── *.mp3, *.wav
|
||||
└── documents/
|
||||
└── *.pdf, *.txt
|
||||
```
|
||||
|
||||
### manifest.json
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "pkg_1234567890",
|
||||
"name": "My Package",
|
||||
"version": "1.0.0",
|
||||
"description": "Package description",
|
||||
"author": "Your Name",
|
||||
"category": "social",
|
||||
"icon": "📦",
|
||||
"screenshots": [],
|
||||
"tags": ["tag1", "tag2"],
|
||||
"dependencies": [],
|
||||
"createdAt": 1234567890,
|
||||
"updatedAt": 1234567890,
|
||||
"downloadCount": 0,
|
||||
"rating": 0,
|
||||
"installed": false
|
||||
}
|
||||
```
|
||||
|
||||
### content.json
|
||||
|
||||
```json
|
||||
{
|
||||
"schemas": [...],
|
||||
"pages": [...],
|
||||
"workflows": [...],
|
||||
"luaScripts": [...],
|
||||
"componentHierarchy": {...},
|
||||
"componentConfigs": {...},
|
||||
"cssClasses": [...],
|
||||
"dropdownConfigs": [...],
|
||||
"seedData": {...}
|
||||
}
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Exporting a Package
|
||||
|
||||
1. Navigate to **Level 4 (God Panel)** or **Level 5 (Super God Panel)**
|
||||
2. Open **Package Manager**
|
||||
3. Click **Export** button
|
||||
4. Choose export type:
|
||||
- **Custom Package**: Configure metadata and select what to include
|
||||
- **Full Snapshot**: Export everything instantly
|
||||
5. For custom packages:
|
||||
- Fill in package name (required)
|
||||
- Add version, author, description
|
||||
- Add tags for searchability
|
||||
- Select export options (checkboxes)
|
||||
6. Click **Export Package**
|
||||
7. ZIP file will download automatically
|
||||
|
||||
### Importing a Package
|
||||
|
||||
1. Navigate to **Level 4 (God Panel)** or **Level 5 (Super God Panel)**
|
||||
2. Open **Package Manager**
|
||||
3. Click **Import** button
|
||||
4. Click the upload area or drag a ZIP file
|
||||
5. Package will be validated and imported
|
||||
6. Success message shows what was imported
|
||||
7. Refresh the page if needed to see new content
|
||||
|
||||
## Pre-Built Packages
|
||||
|
||||
The system comes with several pre-built packages in the Package Catalog:
|
||||
|
||||
### 1. **Classic Forum** 💬
|
||||
- Discussion threads and categories
|
||||
- User profiles and moderation
|
||||
- Schema: ForumCategory, ForumThread, ForumPost
|
||||
|
||||
### 2. **Retro Guestbook** 📖
|
||||
- 90s-style visitor messages
|
||||
- Custom backgrounds and GIFs
|
||||
- Schema: GuestbookEntry
|
||||
|
||||
### 3. **Video Platform** 🎥
|
||||
- Video upload and streaming
|
||||
- Comments, likes, subscriptions
|
||||
- Schema: Video, VideoComment, Subscription, Playlist
|
||||
|
||||
### 4. **Music Streaming Platform** 🎵
|
||||
- Artists, albums, tracks
|
||||
- Playlists and playback
|
||||
- Schema: Artist, Album, Track, MusicPlaylist
|
||||
|
||||
### 5. **Retro Games Arcade** 🕹️
|
||||
- Game collection with high scores
|
||||
- Leaderboards and achievements
|
||||
- Schema: Game, HighScore, Achievement, UserAchievement
|
||||
|
||||
### 6. **E-Commerce Store** 🛒
|
||||
- Product catalog and inventory
|
||||
- Shopping cart and orders
|
||||
- Schema: Product, Cart, Order
|
||||
|
||||
## Best Practices
|
||||
|
||||
### For Package Authors
|
||||
|
||||
1. **Descriptive Naming**: Use clear, descriptive package names
|
||||
2. **Versioning**: Follow semantic versioning (major.minor.patch)
|
||||
3. **Documentation**: Add comprehensive descriptions and tags
|
||||
4. **Dependencies**: List any required packages
|
||||
5. **Testing**: Test your package before distribution
|
||||
6. **Assets**: Include all necessary assets in the package
|
||||
|
||||
### For Package Users
|
||||
|
||||
1. **Backup First**: Export a database snapshot before importing new packages
|
||||
2. **Review Contents**: Check package contents in Package Manager before installing
|
||||
3. **Test in Development**: Test new packages in a development environment first
|
||||
4. **Check Conflicts**: Be aware of potential schema or page ID conflicts
|
||||
5. **Documentation**: Read the package README for setup instructions
|
||||
|
||||
## API Reference
|
||||
|
||||
### Export Functions
|
||||
|
||||
```typescript
|
||||
import { exportPackageAsZip, downloadZip } from '@/lib/package-export'
|
||||
|
||||
// Export a custom package
|
||||
const blob = await exportPackageAsZip(manifest, content, assets, options)
|
||||
downloadZip(blob, 'package-name.zip')
|
||||
|
||||
// Export database snapshot
|
||||
const blob = await exportDatabaseSnapshot(
|
||||
schemas,
|
||||
pages,
|
||||
workflows,
|
||||
luaScripts,
|
||||
componentHierarchy,
|
||||
componentConfigs,
|
||||
cssClasses,
|
||||
dropdownConfigs,
|
||||
assets
|
||||
)
|
||||
```
|
||||
|
||||
### Import Functions
|
||||
|
||||
```typescript
|
||||
import { importPackageFromZip } from '@/lib/package-export'
|
||||
|
||||
// Import from ZIP file
|
||||
const { manifest, content, assets } = await importPackageFromZip(zipFile)
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Import Fails
|
||||
|
||||
- **Invalid ZIP**: Ensure the ZIP file has the correct structure
|
||||
- **Missing manifest.json**: Package must include a manifest file
|
||||
- **Missing content.json**: Package must include content data
|
||||
- **Corrupted File**: Try re-downloading or re-exporting the package
|
||||
|
||||
### Export Fails
|
||||
|
||||
- **No Package Name**: Package name is required for custom exports
|
||||
- **No Data**: Ensure your database has data to export
|
||||
- **Browser Memory**: Large exports may fail on low-memory devices
|
||||
|
||||
### Assets Not Working
|
||||
|
||||
- **Path Issues**: Asset paths are preserved from the original location
|
||||
- **Missing Files**: Ensure all assets were included during export
|
||||
- **Format Support**: Only specific formats are supported (see structure above)
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
Planned features for future versions:
|
||||
|
||||
- 🔄 Package versioning and updates
|
||||
- 🔍 Package marketplace/registry
|
||||
- 🔐 Package signing and verification
|
||||
- 📦 Dependency resolution
|
||||
- 🎨 Custom package icons
|
||||
- 📸 Package screenshots
|
||||
- 💬 Package reviews and ratings
|
||||
- 🔗 Remote package installation via URL
|
||||
- 📊 Package analytics
|
||||
|
||||
## Support
|
||||
|
||||
For issues or questions:
|
||||
- Check the console for error messages
|
||||
- Verify ZIP file structure
|
||||
- Ensure you have the latest version of MetaBuilder
|
||||
- Review this documentation for proper usage
|
||||
|
||||
---
|
||||
|
||||
**Note**: The import/export system is designed to be non-destructive. Imported data is merged with existing data rather than replacing it. Always backup your database before major imports.
|
||||
26
PRD.md
26
PRD.md
@@ -104,7 +104,25 @@ Elevate MetaBuilder to support multi-tenant architecture with a Super God level
|
||||
- Search and filter by category, rating, downloads
|
||||
- Seed data automatically loaded with packages
|
||||
|
||||
### 2. CSS Class Builder
|
||||
### 8. Package Import/Export System
|
||||
**Functionality:** Export database configurations and packages as ZIP files, import packages from ZIP files, including support for assets (images, videos, audio, documents)
|
||||
**Purpose:** Enable sharing of complete application packages, backing up database configurations, and distributing reusable modules across MetaBuilder instances
|
||||
**Trigger:** User clicks Import/Export buttons in Package Manager
|
||||
**Progression:**
|
||||
- **Export**: Click Export → Choose Custom Package or Full Snapshot → Enter metadata (name, version, author, description, tags) → Select export options → Click Export Package → ZIP downloads
|
||||
- **Import**: Click Import → Select/drag ZIP file → Package validated → Data merged with existing database → Assets restored → Success notification
|
||||
**Success Criteria:**
|
||||
- Export packages as ZIP files with manifest.json, content.json, README.md, and assets folder
|
||||
- Import packages from ZIP files with validation
|
||||
- Selective export options (schemas, pages, workflows, Lua scripts, components, CSS, dropdowns, seed data, assets)
|
||||
- Full database snapshot export for backup
|
||||
- Non-destructive import (merges with existing data)
|
||||
- Asset support for images, videos, audio, and documents
|
||||
- Auto-generated README in packages
|
||||
- Import/Export accessible from Package Manager
|
||||
- Visual feedback during import/export operations
|
||||
|
||||
### 9. CSS Class Builder
|
||||
**Functionality:** Visual selector for Tailwind CSS classes organized into logical categories
|
||||
**Purpose:** Eliminate the need to memorize or type CSS class names, reducing errors and speeding up styling
|
||||
**Trigger:** User clicks palette icon next to any className field in PropertyInspector
|
||||
@@ -115,7 +133,7 @@ Elevate MetaBuilder to support multi-tenant architecture with a Super God level
|
||||
- 200+ predefined classes organized into 10 categories
|
||||
- Custom class input available for edge cases
|
||||
|
||||
### 3. Dynamic Dropdown Configuration
|
||||
### 10. Dynamic Dropdown Configuration
|
||||
**Functionality:** Centralized management of dropdown option sets usable across multiple components
|
||||
**Purpose:** Prevent duplication and ensure consistency when the same options appear in multiple places
|
||||
**Trigger:** User navigates to "Dropdowns" tab in god-tier panel or components reference dropdown by name
|
||||
@@ -126,7 +144,7 @@ Elevate MetaBuilder to support multi-tenant architecture with a Super God level
|
||||
- Visual GUI for managing options (no JSON required)
|
||||
- Pre-loaded with common examples (status, priority, category)
|
||||
|
||||
### 4. CSS Class Library Manager
|
||||
### 11. CSS Class Library Manager
|
||||
**Functionality:** Manage the catalog of CSS classes available in the builder
|
||||
**Purpose:** Allow customization of available classes and organization for project-specific needs
|
||||
**Trigger:** User navigates to "CSS Classes" tab in god-tier panel
|
||||
@@ -137,7 +155,7 @@ Elevate MetaBuilder to support multi-tenant architecture with a Super God level
|
||||
- Changes immediately reflected in CSS Class Builder
|
||||
- System initializes with comprehensive Tailwind utilities
|
||||
|
||||
### 5. Monaco Code Editor Integration
|
||||
### 12. Monaco Code Editor Integration
|
||||
**Functionality:** Professional-grade code editor for JSON and Lua with syntax highlighting and validation
|
||||
**Purpose:** When code editing is necessary, provide best-in-class tooling comparable to VS Code
|
||||
**Trigger:** User opens SchemaEditor, LuaEditor, or JsonEditor components
|
||||
|
||||
110
package-lock.json
generated
110
package-lock.json
generated
@@ -44,6 +44,7 @@
|
||||
"@tailwindcss/container-queries": "^0.1.1",
|
||||
"@tailwindcss/vite": "^4.1.11",
|
||||
"@tanstack/react-query": "^5.83.1",
|
||||
"@types/jszip": "^3.4.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
@@ -54,6 +55,7 @@
|
||||
"fengari-web": "^0.1.4",
|
||||
"framer-motion": "^12.6.2",
|
||||
"input-otp": "^1.4.2",
|
||||
"jszip": "^3.10.1",
|
||||
"lucide-react": "^0.484.0",
|
||||
"marked": "^15.0.7",
|
||||
"next-themes": "^0.4.6",
|
||||
@@ -4358,6 +4360,15 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/jszip": {
|
||||
"version": "3.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/jszip/-/jszip-3.4.0.tgz",
|
||||
"integrity": "sha512-GFHqtQQP3R4NNuvZH3hNCYD0NbyBZ42bkN7kO3NDrU/SnvIZWMS8Bp38XCsRKBT5BXvgm0y1zqpZWp/ZkRzBzg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"jszip": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "22.19.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.3.tgz",
|
||||
@@ -5382,6 +5393,12 @@
|
||||
"node": ">=6.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/core-util-is": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
|
||||
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
@@ -7270,6 +7287,12 @@
|
||||
"node": ">= 4"
|
||||
}
|
||||
},
|
||||
"node_modules/immediate": {
|
||||
"version": "3.0.6",
|
||||
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
|
||||
"integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/import-fresh": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
|
||||
@@ -7446,6 +7469,12 @@
|
||||
"@types/estree": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/isarray": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
||||
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/isexe": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||
@@ -7543,6 +7572,18 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/jszip": {
|
||||
"version": "3.10.1",
|
||||
"resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz",
|
||||
"integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==",
|
||||
"license": "(MIT OR GPL-3.0-or-later)",
|
||||
"dependencies": {
|
||||
"lie": "~3.3.0",
|
||||
"pako": "~1.0.2",
|
||||
"readable-stream": "~2.3.6",
|
||||
"setimmediate": "^1.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/keyv": {
|
||||
"version": "4.5.4",
|
||||
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
||||
@@ -7567,6 +7608,15 @@
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/lie": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
|
||||
"integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"immediate": "~3.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss": {
|
||||
"version": "1.30.2",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz",
|
||||
@@ -8259,6 +8309,12 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/pako": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
|
||||
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
|
||||
"license": "(MIT AND Zlib)"
|
||||
},
|
||||
"node_modules/parent-module": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
|
||||
@@ -8468,6 +8524,12 @@
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/process-nextick-args": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
||||
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/prop-types": {
|
||||
"version": "15.8.1",
|
||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||
@@ -8774,6 +8836,27 @@
|
||||
"react-dom": ">=16.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/readable-stream": {
|
||||
"version": "2.3.8",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
|
||||
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"core-util-is": "~1.0.0",
|
||||
"inherits": "~2.0.3",
|
||||
"isarray": "~1.0.0",
|
||||
"process-nextick-args": "~2.0.0",
|
||||
"safe-buffer": "~5.1.1",
|
||||
"string_decoder": "~1.1.1",
|
||||
"util-deprecate": "~1.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/readable-stream/node_modules/safe-buffer": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/readline-sync": {
|
||||
"version": "1.4.10",
|
||||
"resolved": "https://registry.npmjs.org/readline-sync/-/readline-sync-1.4.10.tgz",
|
||||
@@ -9139,6 +9222,12 @@
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/setimmediate": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
|
||||
"integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/setprototypeof": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
|
||||
@@ -9342,6 +9431,21 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/string_decoder": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
||||
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safe-buffer": "~5.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/string_decoder/node_modules/safe-buffer": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/strip-indent": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz",
|
||||
@@ -9863,6 +9967,12 @@
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/util-deprecate": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/uuid": {
|
||||
"version": "11.1.0",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz",
|
||||
|
||||
@@ -48,6 +48,7 @@
|
||||
"@tailwindcss/container-queries": "^0.1.1",
|
||||
"@tailwindcss/vite": "^4.1.11",
|
||||
"@tanstack/react-query": "^5.83.1",
|
||||
"@types/jszip": "^3.4.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
@@ -58,6 +59,7 @@
|
||||
"fengari-web": "^0.1.4",
|
||||
"framer-motion": "^12.6.2",
|
||||
"input-otp": "^1.4.2",
|
||||
"jszip": "^3.10.1",
|
||||
"lucide-react": "^0.484.0",
|
||||
"marked": "^15.0.7",
|
||||
"next-themes": "^0.4.6",
|
||||
|
||||
638
src/components/PackageImportExport.tsx
Normal file
638
src/components/PackageImportExport.tsx
Normal file
@@ -0,0 +1,638 @@
|
||||
import { useState, useRef } from 'react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import { Checkbox } from '@/components/ui/checkbox'
|
||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
import { toast } from 'sonner'
|
||||
import { Database } from '@/lib/database'
|
||||
import { exportPackageAsZip, importPackageFromZip, downloadZip, exportDatabaseSnapshot } from '@/lib/package-export'
|
||||
import type { PackageManifest, PackageContent } from '@/lib/package-types'
|
||||
import type { ExportPackageOptions } from '@/lib/package-export'
|
||||
import {
|
||||
Export,
|
||||
ArrowSquareIn,
|
||||
FileArchive,
|
||||
FileArrowDown,
|
||||
FileArrowUp,
|
||||
Package,
|
||||
CloudArrowDown,
|
||||
Database as DatabaseIcon,
|
||||
CheckCircle,
|
||||
Warning,
|
||||
Image as ImageIcon,
|
||||
FilmStrip,
|
||||
MusicNote,
|
||||
FileText
|
||||
} from '@phosphor-icons/react'
|
||||
|
||||
interface PackageImportExportProps {
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
mode: 'export' | 'import'
|
||||
}
|
||||
|
||||
export function PackageImportExport({ open, onOpenChange, mode }: PackageImportExportProps) {
|
||||
const [exporting, setExporting] = useState(false)
|
||||
const [importing, setImporting] = useState(false)
|
||||
const [exportOptions, setExportOptions] = useState<ExportPackageOptions>({
|
||||
includeAssets: true,
|
||||
includeSchemas: true,
|
||||
includePages: true,
|
||||
includeWorkflows: true,
|
||||
includeLuaScripts: true,
|
||||
includeComponentHierarchy: true,
|
||||
includeComponentConfigs: true,
|
||||
includeCssClasses: true,
|
||||
includeDropdownConfigs: true,
|
||||
includeSeedData: true,
|
||||
})
|
||||
const [manifest, setManifest] = useState<Partial<PackageManifest>>({
|
||||
name: '',
|
||||
version: '1.0.0',
|
||||
description: '',
|
||||
author: '',
|
||||
category: 'other',
|
||||
tags: [],
|
||||
})
|
||||
const [tagInput, setTagInput] = useState('')
|
||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
const handleExport = async () => {
|
||||
if (!manifest.name) {
|
||||
toast.error('Please provide a package name')
|
||||
return
|
||||
}
|
||||
|
||||
setExporting(true)
|
||||
try {
|
||||
const schemas = await Database.getSchemas()
|
||||
const pages = await Database.getPages()
|
||||
const workflows = await Database.getWorkflows()
|
||||
const luaScripts = await Database.getLuaScripts()
|
||||
const componentHierarchy = await Database.getComponentHierarchy()
|
||||
const componentConfigs = await Database.getComponentConfigs()
|
||||
const cssClasses = await Database.getCssClasses()
|
||||
const dropdownConfigs = await Database.getDropdownConfigs()
|
||||
|
||||
const fullManifest: PackageManifest = {
|
||||
id: `pkg_${Date.now()}`,
|
||||
name: manifest.name!,
|
||||
version: manifest.version || '1.0.0',
|
||||
description: manifest.description || '',
|
||||
author: manifest.author || 'Anonymous',
|
||||
category: manifest.category as any || 'other',
|
||||
icon: '📦',
|
||||
screenshots: [],
|
||||
tags: manifest.tags || [],
|
||||
dependencies: [],
|
||||
createdAt: Date.now(),
|
||||
updatedAt: Date.now(),
|
||||
downloadCount: 0,
|
||||
rating: 0,
|
||||
installed: false,
|
||||
}
|
||||
|
||||
const content: PackageContent = {
|
||||
schemas: exportOptions.includeSchemas ? schemas : [],
|
||||
pages: exportOptions.includePages ? pages : [],
|
||||
workflows: exportOptions.includeWorkflows ? workflows : [],
|
||||
luaScripts: exportOptions.includeLuaScripts ? luaScripts : [],
|
||||
componentHierarchy: exportOptions.includeComponentHierarchy ? componentHierarchy : {},
|
||||
componentConfigs: exportOptions.includeComponentConfigs ? componentConfigs : {},
|
||||
cssClasses: exportOptions.includeCssClasses ? cssClasses : undefined,
|
||||
dropdownConfigs: exportOptions.includeDropdownConfigs ? dropdownConfigs : undefined,
|
||||
}
|
||||
|
||||
const blob = await exportPackageAsZip(fullManifest, content, [], exportOptions)
|
||||
const fileName = `${manifest.name.toLowerCase().replace(/\s+/g, '-')}-${manifest.version}.zip`
|
||||
downloadZip(blob, fileName)
|
||||
|
||||
toast.success('Package exported successfully!')
|
||||
onOpenChange(false)
|
||||
} catch (error) {
|
||||
console.error('Export error:', error)
|
||||
toast.error('Failed to export package')
|
||||
} finally {
|
||||
setExporting(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleExportSnapshot = async () => {
|
||||
setExporting(true)
|
||||
try {
|
||||
const schemas = await Database.getSchemas()
|
||||
const pages = await Database.getPages()
|
||||
const workflows = await Database.getWorkflows()
|
||||
const luaScripts = await Database.getLuaScripts()
|
||||
const componentHierarchy = await Database.getComponentHierarchy()
|
||||
const componentConfigs = await Database.getComponentConfigs()
|
||||
const cssClasses = await Database.getCssClasses()
|
||||
const dropdownConfigs = await Database.getDropdownConfigs()
|
||||
|
||||
const blob = await exportDatabaseSnapshot(
|
||||
schemas,
|
||||
pages,
|
||||
workflows,
|
||||
luaScripts,
|
||||
componentHierarchy,
|
||||
componentConfigs,
|
||||
cssClasses,
|
||||
dropdownConfigs
|
||||
)
|
||||
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, -5)
|
||||
downloadZip(blob, `database-snapshot-${timestamp}.zip`)
|
||||
|
||||
toast.success('Database snapshot exported successfully!')
|
||||
onOpenChange(false)
|
||||
} catch (error) {
|
||||
console.error('Snapshot export error:', error)
|
||||
toast.error('Failed to export database snapshot')
|
||||
} finally {
|
||||
setExporting(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleImport = async (file: File) => {
|
||||
setImporting(true)
|
||||
try {
|
||||
const { manifest: importedManifest, content, assets } = await importPackageFromZip(file)
|
||||
|
||||
const currentSchemas = await Database.getSchemas()
|
||||
const currentPages = await Database.getPages()
|
||||
const currentWorkflows = await Database.getWorkflows()
|
||||
const currentLuaScripts = await Database.getLuaScripts()
|
||||
const currentHierarchy = await Database.getComponentHierarchy()
|
||||
const currentConfigs = await Database.getComponentConfigs()
|
||||
|
||||
const newSchemas = [...currentSchemas, ...content.schemas]
|
||||
const newPages = [...currentPages, ...content.pages]
|
||||
const newWorkflows = [...currentWorkflows, ...content.workflows]
|
||||
const newLuaScripts = [...currentLuaScripts, ...content.luaScripts]
|
||||
const newHierarchy = { ...currentHierarchy, ...content.componentHierarchy }
|
||||
const newConfigs = { ...currentConfigs, ...content.componentConfigs }
|
||||
|
||||
await Database.setSchemas(newSchemas)
|
||||
await Database.setPages(newPages)
|
||||
await Database.setWorkflows(newWorkflows)
|
||||
await Database.setLuaScripts(newLuaScripts)
|
||||
await Database.setComponentHierarchy(newHierarchy)
|
||||
await Database.setComponentConfigs(newConfigs)
|
||||
|
||||
if (content.cssClasses) {
|
||||
const currentCssClasses = await Database.getCssClasses()
|
||||
await Database.setCssClasses([...currentCssClasses, ...content.cssClasses])
|
||||
}
|
||||
|
||||
if (content.dropdownConfigs) {
|
||||
const currentDropdowns = await Database.getDropdownConfigs()
|
||||
await Database.setDropdownConfigs([...currentDropdowns, ...content.dropdownConfigs])
|
||||
}
|
||||
|
||||
if (content.seedData) {
|
||||
await Database.setPackageData(importedManifest.id, content.seedData)
|
||||
}
|
||||
|
||||
const installedPackage = {
|
||||
packageId: importedManifest.id,
|
||||
installedAt: Date.now(),
|
||||
version: importedManifest.version,
|
||||
enabled: true,
|
||||
}
|
||||
|
||||
await Database.installPackage(installedPackage)
|
||||
|
||||
toast.success(`Package "${importedManifest.name}" imported successfully!`)
|
||||
toast.info(`Imported: ${content.schemas.length} schemas, ${content.pages.length} pages, ${content.workflows.length} workflows, ${assets.length} assets`)
|
||||
|
||||
onOpenChange(false)
|
||||
} catch (error) {
|
||||
console.error('Import error:', error)
|
||||
toast.error(`Failed to import package: ${error instanceof Error ? error.message : 'Unknown error'}`)
|
||||
} finally {
|
||||
setImporting(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0]
|
||||
if (file) {
|
||||
if (!file.name.endsWith('.zip')) {
|
||||
toast.error('Please select a .zip file')
|
||||
return
|
||||
}
|
||||
handleImport(file)
|
||||
}
|
||||
}
|
||||
|
||||
const handleAddTag = () => {
|
||||
if (tagInput.trim() && !manifest.tags?.includes(tagInput.trim())) {
|
||||
setManifest(prev => ({
|
||||
...prev,
|
||||
tags: [...(prev.tags || []), tagInput.trim()]
|
||||
}))
|
||||
setTagInput('')
|
||||
}
|
||||
}
|
||||
|
||||
const handleRemoveTag = (tag: string) => {
|
||||
setManifest(prev => ({
|
||||
...prev,
|
||||
tags: (prev.tags || []).filter(t => t !== tag)
|
||||
}))
|
||||
}
|
||||
|
||||
if (mode === 'import') {
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-2xl">
|
||||
<DialogHeader>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-12 h-12 rounded-lg bg-gradient-to-br from-blue-500 to-blue-700 flex items-center justify-center">
|
||||
<ArrowSquareIn size={24} weight="duotone" className="text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<DialogTitle>Import Package</DialogTitle>
|
||||
<DialogDescription>Import a package from a ZIP file</DialogDescription>
|
||||
</div>
|
||||
</div>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4 py-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">Select Package File</CardTitle>
|
||||
<CardDescription>Choose a .zip file containing a MetaBuilder package</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
<div
|
||||
className="border-2 border-dashed rounded-lg p-8 text-center hover:border-primary hover:bg-accent/50 transition-colors cursor-pointer"
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
>
|
||||
<FileArrowUp size={48} className="mx-auto mb-4 text-muted-foreground" />
|
||||
<p className="font-medium mb-1">Click to select a package file</p>
|
||||
<p className="text-sm text-muted-foreground">Supports .zip files only</p>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept=".zip"
|
||||
onChange={handleFileSelect}
|
||||
className="hidden"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{importing && (
|
||||
<div className="flex items-center justify-center gap-2 text-sm text-muted-foreground">
|
||||
<div className="w-4 h-4 border-2 border-primary border-t-transparent rounded-full animate-spin" />
|
||||
<span>Importing package...</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">What's Included in Packages?</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-2 gap-3 text-sm">
|
||||
<div className="flex items-center gap-2">
|
||||
<CheckCircle size={16} className="text-green-500" />
|
||||
<span>Data schemas</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<CheckCircle size={16} className="text-green-500" />
|
||||
<span>Page configurations</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<CheckCircle size={16} className="text-green-500" />
|
||||
<span>Workflows</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<CheckCircle size={16} className="text-green-500" />
|
||||
<span>Lua scripts</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<CheckCircle size={16} className="text-green-500" />
|
||||
<span>Component hierarchies</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<CheckCircle size={16} className="text-green-500" />
|
||||
<span>CSS configurations</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<CheckCircle size={16} className="text-green-500" />
|
||||
<span>Assets (images, etc.)</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<CheckCircle size={16} className="text-green-500" />
|
||||
<span>Seed data</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div className="p-4 rounded-lg bg-yellow-500/10 border border-yellow-500/20 flex items-start gap-3">
|
||||
<Warning size={20} className="text-yellow-600 flex-shrink-0 mt-0.5" />
|
||||
<div className="text-sm">
|
||||
<p className="font-medium text-yellow-900 dark:text-yellow-100 mb-1">Import Warning</p>
|
||||
<p className="text-yellow-800 dark:text-yellow-200">Imported packages will be merged with existing data. Make sure to back up your database before importing.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-3xl max-h-[90vh] overflow-hidden flex flex-col">
|
||||
<DialogHeader>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-12 h-12 rounded-lg bg-gradient-to-br from-green-500 to-green-700 flex items-center justify-center">
|
||||
<Export size={24} weight="duotone" className="text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<DialogTitle>Export Package</DialogTitle>
|
||||
<DialogDescription>Create a shareable package or database snapshot</DialogDescription>
|
||||
</div>
|
||||
</div>
|
||||
</DialogHeader>
|
||||
|
||||
<ScrollArea className="flex-1 -mx-6 px-6">
|
||||
<div className="space-y-6 py-4">
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<Card className="cursor-pointer hover:border-primary transition-colors">
|
||||
<CardHeader>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-lg bg-gradient-to-br from-green-500 to-green-700 flex items-center justify-center">
|
||||
<Package size={20} className="text-white" />
|
||||
</div>
|
||||
<CardTitle className="text-base">Custom Package</CardTitle>
|
||||
</div>
|
||||
<CardDescription>Export selected data as a reusable package</CardDescription>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
className="cursor-pointer hover:border-primary transition-colors"
|
||||
onClick={handleExportSnapshot}
|
||||
>
|
||||
<CardHeader>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-lg bg-gradient-to-br from-blue-500 to-blue-700 flex items-center justify-center">
|
||||
<DatabaseIcon size={20} className="text-white" />
|
||||
</div>
|
||||
<CardTitle className="text-base">Full Snapshot</CardTitle>
|
||||
</div>
|
||||
<CardDescription>Export entire database as backup</CardDescription>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Label htmlFor="package-name">Package Name *</Label>
|
||||
<Input
|
||||
id="package-name"
|
||||
placeholder="My Awesome Package"
|
||||
value={manifest.name}
|
||||
onChange={e => setManifest(prev => ({ ...prev, name: e.target.value }))}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label htmlFor="package-version">Version</Label>
|
||||
<Input
|
||||
id="package-version"
|
||||
placeholder="1.0.0"
|
||||
value={manifest.version}
|
||||
onChange={e => setManifest(prev => ({ ...prev, version: e.target.value }))}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="package-author">Author</Label>
|
||||
<Input
|
||||
id="package-author"
|
||||
placeholder="Your Name"
|
||||
value={manifest.author}
|
||||
onChange={e => setManifest(prev => ({ ...prev, author: e.target.value }))}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="package-description">Description</Label>
|
||||
<Textarea
|
||||
id="package-description"
|
||||
placeholder="Describe what this package does..."
|
||||
value={manifest.description}
|
||||
onChange={e => setManifest(prev => ({ ...prev, description: e.target.value }))}
|
||||
rows={3}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="package-tags">Tags</Label>
|
||||
<div className="flex gap-2 mb-2">
|
||||
<Input
|
||||
id="package-tags"
|
||||
placeholder="Add a tag..."
|
||||
value={tagInput}
|
||||
onChange={e => setTagInput(e.target.value)}
|
||||
onKeyDown={e => e.key === 'Enter' && (e.preventDefault(), handleAddTag())}
|
||||
/>
|
||||
<Button type="button" onClick={handleAddTag}>Add</Button>
|
||||
</div>
|
||||
{manifest.tags && manifest.tags.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{manifest.tags.map(tag => (
|
||||
<div key={tag} className="px-2 py-1 bg-secondary rounded-md text-sm flex items-center gap-2">
|
||||
<span>{tag}</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleRemoveTag(tag)}
|
||||
className="text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div>
|
||||
<Label className="mb-3 block">Export Options</Label>
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox
|
||||
id="export-schemas"
|
||||
checked={exportOptions.includeSchemas}
|
||||
onCheckedChange={checked =>
|
||||
setExportOptions(prev => ({ ...prev, includeSchemas: checked as boolean }))
|
||||
}
|
||||
/>
|
||||
<Label htmlFor="export-schemas" className="font-normal cursor-pointer">
|
||||
Include data schemas
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox
|
||||
id="export-pages"
|
||||
checked={exportOptions.includePages}
|
||||
onCheckedChange={checked =>
|
||||
setExportOptions(prev => ({ ...prev, includePages: checked as boolean }))
|
||||
}
|
||||
/>
|
||||
<Label htmlFor="export-pages" className="font-normal cursor-pointer">
|
||||
Include page configurations
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox
|
||||
id="export-workflows"
|
||||
checked={exportOptions.includeWorkflows}
|
||||
onCheckedChange={checked =>
|
||||
setExportOptions(prev => ({ ...prev, includeWorkflows: checked as boolean }))
|
||||
}
|
||||
/>
|
||||
<Label htmlFor="export-workflows" className="font-normal cursor-pointer">
|
||||
Include workflows
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox
|
||||
id="export-lua"
|
||||
checked={exportOptions.includeLuaScripts}
|
||||
onCheckedChange={checked =>
|
||||
setExportOptions(prev => ({ ...prev, includeLuaScripts: checked as boolean }))
|
||||
}
|
||||
/>
|
||||
<Label htmlFor="export-lua" className="font-normal cursor-pointer">
|
||||
Include Lua scripts
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox
|
||||
id="export-components"
|
||||
checked={exportOptions.includeComponentHierarchy}
|
||||
onCheckedChange={checked =>
|
||||
setExportOptions(prev => ({ ...prev, includeComponentHierarchy: checked as boolean }))
|
||||
}
|
||||
/>
|
||||
<Label htmlFor="export-components" className="font-normal cursor-pointer">
|
||||
Include component hierarchies
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox
|
||||
id="export-configs"
|
||||
checked={exportOptions.includeComponentConfigs}
|
||||
onCheckedChange={checked =>
|
||||
setExportOptions(prev => ({ ...prev, includeComponentConfigs: checked as boolean }))
|
||||
}
|
||||
/>
|
||||
<Label htmlFor="export-configs" className="font-normal cursor-pointer">
|
||||
Include component configurations
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox
|
||||
id="export-css"
|
||||
checked={exportOptions.includeCssClasses}
|
||||
onCheckedChange={checked =>
|
||||
setExportOptions(prev => ({ ...prev, includeCssClasses: checked as boolean }))
|
||||
}
|
||||
/>
|
||||
<Label htmlFor="export-css" className="font-normal cursor-pointer">
|
||||
Include CSS classes
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox
|
||||
id="export-dropdowns"
|
||||
checked={exportOptions.includeDropdownConfigs}
|
||||
onCheckedChange={checked =>
|
||||
setExportOptions(prev => ({ ...prev, includeDropdownConfigs: checked as boolean }))
|
||||
}
|
||||
/>
|
||||
<Label htmlFor="export-dropdowns" className="font-normal cursor-pointer">
|
||||
Include dropdown configurations
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox
|
||||
id="export-seed"
|
||||
checked={exportOptions.includeSeedData}
|
||||
onCheckedChange={checked =>
|
||||
setExportOptions(prev => ({ ...prev, includeSeedData: checked as boolean }))
|
||||
}
|
||||
/>
|
||||
<Label htmlFor="export-seed" className="font-normal cursor-pointer">
|
||||
Include seed data
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox
|
||||
id="export-assets"
|
||||
checked={exportOptions.includeAssets}
|
||||
onCheckedChange={checked =>
|
||||
setExportOptions(prev => ({ ...prev, includeAssets: checked as boolean }))
|
||||
}
|
||||
/>
|
||||
<Label htmlFor="export-assets" className="font-normal cursor-pointer">
|
||||
Include assets (images, videos, audio, documents)
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
|
||||
<DialogFooter className="gap-2">
|
||||
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleExport} disabled={exporting || !manifest.name}>
|
||||
{exporting ? (
|
||||
<>
|
||||
<div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin mr-2" />
|
||||
Exporting...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<FileArrowDown size={16} className="mr-2" />
|
||||
Export Package
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
@@ -12,7 +12,8 @@ import { toast } from 'sonner'
|
||||
import { Database } from '@/lib/database'
|
||||
import { PACKAGE_CATALOG } from '@/lib/package-catalog'
|
||||
import type { PackageManifest, PackageContent, InstalledPackage } from '@/lib/package-types'
|
||||
import { Package, Download, Trash, Power, MagnifyingGlass, Star, Tag, User, TrendUp, Funnel } from '@phosphor-icons/react'
|
||||
import { Package, Download, Trash, Power, MagnifyingGlass, Star, Tag, User, TrendUp, Funnel, Export, ArrowSquareIn } from '@phosphor-icons/react'
|
||||
import { PackageImportExport } from './PackageImportExport'
|
||||
|
||||
interface PackageManagerProps {
|
||||
onClose?: () => void
|
||||
@@ -27,6 +28,8 @@ export function PackageManager({ onClose }: PackageManagerProps) {
|
||||
const [sortBy, setSortBy] = useState<'name' | 'downloads' | 'rating'>('downloads')
|
||||
const [showDetails, setShowDetails] = useState(false)
|
||||
const [installing, setInstalling] = useState(false)
|
||||
const [showImportExport, setShowImportExport] = useState(false)
|
||||
const [importExportMode, setImportExportMode] = useState<'import' | 'export'>('export')
|
||||
|
||||
useEffect(() => {
|
||||
loadPackages()
|
||||
@@ -198,11 +201,33 @@ export function PackageManager({ onClose }: PackageManagerProps) {
|
||||
<p className="text-sm text-muted-foreground">Install pre-built applications and features</p>
|
||||
</div>
|
||||
</div>
|
||||
{onClose && (
|
||||
<Button variant="ghost" onClick={onClose}>
|
||||
Close
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setImportExportMode('import')
|
||||
setShowImportExport(true)
|
||||
}}
|
||||
>
|
||||
<ArrowSquareIn size={16} className="mr-2" />
|
||||
Import
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setImportExportMode('export')
|
||||
setShowImportExport(true)
|
||||
}}
|
||||
>
|
||||
<Export size={16} className="mr-2" />
|
||||
Export
|
||||
</Button>
|
||||
{onClose && (
|
||||
<Button variant="ghost" onClick={onClose}>
|
||||
Close
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-hidden">
|
||||
@@ -453,6 +478,17 @@ export function PackageManager({ onClose }: PackageManagerProps) {
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<PackageImportExport
|
||||
open={showImportExport}
|
||||
onOpenChange={(open) => {
|
||||
setShowImportExport(open)
|
||||
if (!open) {
|
||||
loadPackages()
|
||||
}
|
||||
}}
|
||||
mode={importExportMode}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
254
src/lib/package-export.ts
Normal file
254
src/lib/package-export.ts
Normal file
@@ -0,0 +1,254 @@
|
||||
import JSZip from 'jszip'
|
||||
import type { PackageManifest, PackageContent } from './package-types'
|
||||
import type { DatabaseSchema } from './database'
|
||||
|
||||
export interface ExportPackageOptions {
|
||||
includeAssets?: boolean
|
||||
includeSchemas?: boolean
|
||||
includePages?: boolean
|
||||
includeWorkflows?: boolean
|
||||
includeLuaScripts?: boolean
|
||||
includeComponentHierarchy?: boolean
|
||||
includeComponentConfigs?: boolean
|
||||
includeCssClasses?: boolean
|
||||
includeDropdownConfigs?: boolean
|
||||
includeSeedData?: boolean
|
||||
}
|
||||
|
||||
export interface AssetFile {
|
||||
path: string
|
||||
blob: Blob
|
||||
type: 'image' | 'video' | 'audio' | 'document'
|
||||
}
|
||||
|
||||
export async function exportPackageAsZip(
|
||||
manifest: PackageManifest,
|
||||
content: PackageContent,
|
||||
assets: AssetFile[] = [],
|
||||
options: ExportPackageOptions = {}
|
||||
): Promise<Blob> {
|
||||
const zip = new JSZip()
|
||||
|
||||
const opts = {
|
||||
includeAssets: true,
|
||||
includeSchemas: true,
|
||||
includePages: true,
|
||||
includeWorkflows: true,
|
||||
includeLuaScripts: true,
|
||||
includeComponentHierarchy: true,
|
||||
includeComponentConfigs: true,
|
||||
includeCssClasses: true,
|
||||
includeDropdownConfigs: true,
|
||||
includeSeedData: true,
|
||||
...options,
|
||||
}
|
||||
|
||||
zip.file('manifest.json', JSON.stringify(manifest, null, 2))
|
||||
|
||||
const packageContent: Partial<PackageContent> = {}
|
||||
|
||||
if (opts.includeSchemas && content.schemas) {
|
||||
packageContent.schemas = content.schemas
|
||||
}
|
||||
|
||||
if (opts.includePages && content.pages) {
|
||||
packageContent.pages = content.pages
|
||||
}
|
||||
|
||||
if (opts.includeWorkflows && content.workflows) {
|
||||
packageContent.workflows = content.workflows
|
||||
}
|
||||
|
||||
if (opts.includeLuaScripts && content.luaScripts) {
|
||||
packageContent.luaScripts = content.luaScripts
|
||||
}
|
||||
|
||||
if (opts.includeComponentHierarchy && content.componentHierarchy) {
|
||||
packageContent.componentHierarchy = content.componentHierarchy
|
||||
}
|
||||
|
||||
if (opts.includeComponentConfigs && content.componentConfigs) {
|
||||
packageContent.componentConfigs = content.componentConfigs
|
||||
}
|
||||
|
||||
if (opts.includeCssClasses && content.cssClasses) {
|
||||
packageContent.cssClasses = content.cssClasses
|
||||
}
|
||||
|
||||
if (opts.includeDropdownConfigs && content.dropdownConfigs) {
|
||||
packageContent.dropdownConfigs = content.dropdownConfigs
|
||||
}
|
||||
|
||||
if (opts.includeSeedData && content.seedData) {
|
||||
packageContent.seedData = content.seedData
|
||||
}
|
||||
|
||||
zip.file('content.json', JSON.stringify(packageContent, null, 2))
|
||||
|
||||
if (opts.includeAssets && assets.length > 0) {
|
||||
const assetsFolder = zip.folder('assets')
|
||||
if (assetsFolder) {
|
||||
for (const asset of assets) {
|
||||
const typeFolder = assetsFolder.folder(asset.type + 's')
|
||||
if (typeFolder) {
|
||||
const fileName = asset.path.split('/').pop() || 'unnamed'
|
||||
typeFolder.file(fileName, asset.blob)
|
||||
}
|
||||
}
|
||||
|
||||
const assetManifest = assets.map(asset => ({
|
||||
originalPath: asset.path,
|
||||
type: asset.type,
|
||||
fileName: asset.path.split('/').pop(),
|
||||
}))
|
||||
|
||||
assetsFolder.file('asset-manifest.json', JSON.stringify(assetManifest, null, 2))
|
||||
}
|
||||
}
|
||||
|
||||
zip.file('README.md', generateReadme(manifest, content))
|
||||
|
||||
const blob = await zip.generateAsync({ type: 'blob' })
|
||||
return blob
|
||||
}
|
||||
|
||||
export async function importPackageFromZip(zipFile: File): Promise<{
|
||||
manifest: PackageManifest
|
||||
content: PackageContent
|
||||
assets: Array<{ path: string; blob: Blob; type: 'image' | 'video' | 'audio' | 'document' }>
|
||||
}> {
|
||||
const zip = await JSZip.loadAsync(zipFile)
|
||||
|
||||
const manifestFile = zip.file('manifest.json')
|
||||
if (!manifestFile) {
|
||||
throw new Error('Invalid package: manifest.json not found')
|
||||
}
|
||||
|
||||
const manifestText = await manifestFile.async('text')
|
||||
const manifest: PackageManifest = JSON.parse(manifestText)
|
||||
|
||||
const contentFile = zip.file('content.json')
|
||||
if (!contentFile) {
|
||||
throw new Error('Invalid package: content.json not found')
|
||||
}
|
||||
|
||||
const contentText = await contentFile.async('text')
|
||||
const content: PackageContent = JSON.parse(contentText)
|
||||
|
||||
const assets: Array<{ path: string; blob: Blob; type: 'image' | 'video' | 'audio' | 'document' }> = []
|
||||
|
||||
const assetManifestFile = zip.file('assets/asset-manifest.json')
|
||||
if (assetManifestFile) {
|
||||
const assetManifestText = await assetManifestFile.async('text')
|
||||
const assetManifest: Array<{ originalPath: string; type: string; fileName: string }> = JSON.parse(assetManifestText)
|
||||
|
||||
for (const assetInfo of assetManifest) {
|
||||
const assetPath = `assets/${assetInfo.type}s/${assetInfo.fileName}`
|
||||
const assetFile = zip.file(assetPath)
|
||||
|
||||
if (assetFile) {
|
||||
const blob = await assetFile.async('blob')
|
||||
assets.push({
|
||||
path: assetInfo.originalPath,
|
||||
blob,
|
||||
type: assetInfo.type as 'image' | 'video' | 'audio' | 'document',
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { manifest, content, assets }
|
||||
}
|
||||
|
||||
export function downloadZip(blob: Blob, fileName: string) {
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = fileName
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
document.body.removeChild(a)
|
||||
URL.revokeObjectURL(url)
|
||||
}
|
||||
|
||||
function generateReadme(manifest: PackageManifest, content: PackageContent): string {
|
||||
return `# ${manifest.name}
|
||||
|
||||
Version: ${manifest.version}
|
||||
Author: ${manifest.author}
|
||||
Category: ${manifest.category}
|
||||
|
||||
## Description
|
||||
|
||||
${manifest.description}
|
||||
|
||||
## Contents
|
||||
|
||||
- **Schemas**: ${content.schemas?.length || 0} data models
|
||||
- **Pages**: ${content.pages?.length || 0} page configurations
|
||||
- **Workflows**: ${content.workflows?.length || 0} workflows
|
||||
- **Lua Scripts**: ${content.luaScripts?.length || 0} scripts
|
||||
- **Components**: ${Object.keys(content.componentHierarchy || {}).length} component hierarchies
|
||||
- **CSS Classes**: ${content.cssClasses?.length || 0} CSS categories
|
||||
- **Dropdown Configs**: ${content.dropdownConfigs?.length || 0} dropdown configurations
|
||||
|
||||
## Tags
|
||||
|
||||
${manifest.tags.join(', ')}
|
||||
|
||||
## Installation
|
||||
|
||||
Import this package through the MetaBuilder Package Manager.
|
||||
|
||||
## Dependencies
|
||||
|
||||
${manifest.dependencies.length > 0 ? manifest.dependencies.join(', ') : 'None'}
|
||||
|
||||
---
|
||||
|
||||
Generated by MetaBuilder Package Exporter
|
||||
`
|
||||
}
|
||||
|
||||
export async function exportDatabaseSnapshot(
|
||||
schemas: any[],
|
||||
pages: any[],
|
||||
workflows: any[],
|
||||
luaScripts: any[],
|
||||
componentHierarchy: Record<string, any>,
|
||||
componentConfigs: Record<string, any>,
|
||||
cssClasses: any[],
|
||||
dropdownConfigs: any[],
|
||||
assets: AssetFile[] = []
|
||||
): Promise<Blob> {
|
||||
const manifest: PackageManifest = {
|
||||
id: `snapshot_${Date.now()}`,
|
||||
name: 'Database Snapshot',
|
||||
version: '1.0.0',
|
||||
description: 'Complete database snapshot export',
|
||||
author: 'User Export',
|
||||
category: 'other',
|
||||
icon: '💾',
|
||||
screenshots: [],
|
||||
tags: ['snapshot', 'backup', 'export'],
|
||||
dependencies: [],
|
||||
createdAt: Date.now(),
|
||||
updatedAt: Date.now(),
|
||||
downloadCount: 0,
|
||||
rating: 0,
|
||||
installed: false,
|
||||
}
|
||||
|
||||
const content: PackageContent = {
|
||||
schemas,
|
||||
pages,
|
||||
workflows,
|
||||
luaScripts,
|
||||
componentHierarchy,
|
||||
componentConfigs,
|
||||
cssClasses,
|
||||
dropdownConfigs,
|
||||
}
|
||||
|
||||
return exportPackageAsZip(manifest, content, assets)
|
||||
}
|
||||
Reference in New Issue
Block a user