Initial commit
This commit is contained in:
15
.claude-plugin/plugin.json
Normal file
15
.claude-plugin/plugin.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "odoo-pwa-generator",
|
||||
"description": "Generate offline-first Progressive Web Apps with Odoo Studio backend integration. Supports SvelteKit, React, and Vue with smart caching, IndexedDB storage, and automatic sync.",
|
||||
"version": "1.0.0",
|
||||
"author": {
|
||||
"name": "Jamshid",
|
||||
"url": "https://github.com/jamshu"
|
||||
},
|
||||
"skills": [
|
||||
"./skills"
|
||||
],
|
||||
"commands": [
|
||||
"./commands"
|
||||
]
|
||||
}
|
||||
3
README.md
Normal file
3
README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# odoo-pwa-generator
|
||||
|
||||
Generate offline-first Progressive Web Apps with Odoo Studio backend integration. Supports SvelteKit, React, and Vue with smart caching, IndexedDB storage, and automatic sync.
|
||||
534
commands/add-deployment.md
Normal file
534
commands/add-deployment.md
Normal file
@@ -0,0 +1,534 @@
|
||||
Add a new deployment target to your existing Odoo PWA project.
|
||||
|
||||
## What this command does:
|
||||
- Adds deployment configuration for new platforms
|
||||
- Creates necessary config files
|
||||
- Sets up CI/CD workflows
|
||||
- Provides deployment instructions
|
||||
- Configures environment variables
|
||||
|
||||
## Supported Deployment Targets
|
||||
|
||||
### Primary (Recommended)
|
||||
- ✅ **Vercel** - Best for full-stack PWAs (API routes work)
|
||||
- ✅ **Netlify** - Great for static + serverless functions
|
||||
- ✅ **Cloudflare Pages** - Fast global CDN, edge functions
|
||||
|
||||
### Secondary (Static Only)
|
||||
- ⚠️ **GitHub Pages** - Free, but no server-side code
|
||||
- ⚠️ **Cloudflare Pages (Static)** - Without edge functions
|
||||
- ⚠️ **AWS S3 + CloudFront** - Static hosting only
|
||||
|
||||
---
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before adding deployment:
|
||||
|
||||
```
|
||||
□ Project builds successfully (npm run build)
|
||||
□ Odoo connection tested and working
|
||||
□ Environment variables documented
|
||||
□ Git repository initialized
|
||||
□ Code committed to version control
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Add Vercel Deployment 🔷
|
||||
|
||||
### What You'll Get:
|
||||
- Automatic deployments from Git
|
||||
- Serverless API routes
|
||||
- Preview deployments for PRs
|
||||
- Environment variable management
|
||||
- Custom domains
|
||||
- Analytics and logs
|
||||
|
||||
### Steps:
|
||||
|
||||
#### 1. Install Vercel CLI (Optional)
|
||||
```bash
|
||||
npm install -g vercel
|
||||
```
|
||||
|
||||
#### 2. Create `vercel.json`
|
||||
```json
|
||||
{
|
||||
"buildCommand": "npm run build",
|
||||
"outputDirectory": "build",
|
||||
"framework": "sveltekit",
|
||||
"installCommand": "npm install",
|
||||
"devCommand": "npm run dev",
|
||||
"env": {
|
||||
"VITE_ODOO_URL": "@vite_odoo_url",
|
||||
"VITE_ODOO_DB": "@vite_odoo_db",
|
||||
"VITE_MODEL_NAME": "@vite_model_name",
|
||||
"VITE_MODEL_DISPLAY_NAME": "@vite_model_display_name"
|
||||
},
|
||||
"build": {
|
||||
"env": {
|
||||
"ODOO_API_KEY": "@odoo_api_key",
|
||||
"ODOO_USERNAME": "@odoo_username"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
For React/Vue:
|
||||
```json
|
||||
{
|
||||
"buildCommand": "npm run build",
|
||||
"outputDirectory": "dist",
|
||||
"framework": "vite"
|
||||
}
|
||||
```
|
||||
|
||||
#### 3. Create `.vercelignore`
|
||||
```
|
||||
node_modules
|
||||
.env
|
||||
.env.local
|
||||
*.log
|
||||
.DS_Store
|
||||
```
|
||||
|
||||
#### 4. Deployment Options
|
||||
|
||||
**Option A: Vercel Dashboard (Recommended for first time)**
|
||||
1. Go to https://vercel.com/new
|
||||
2. Connect your Git repository
|
||||
3. Vercel auto-detects framework
|
||||
4. Add environment variables:
|
||||
- `VITE_ODOO_URL`
|
||||
- `VITE_ODOO_DB`
|
||||
- `ODOO_API_KEY`
|
||||
- `ODOO_USERNAME`
|
||||
- `VITE_MODEL_NAME`
|
||||
- `VITE_MODEL_DISPLAY_NAME`
|
||||
5. Click "Deploy"
|
||||
|
||||
**Option B: Vercel CLI**
|
||||
```bash
|
||||
vercel login
|
||||
vercel
|
||||
|
||||
# Follow prompts
|
||||
# Add environment variables when asked
|
||||
# Or add them later in dashboard
|
||||
```
|
||||
|
||||
**Option C: Continuous Deployment**
|
||||
1. Connect repository to Vercel
|
||||
2. Every push to `main` auto-deploys
|
||||
3. PRs get preview deployments
|
||||
|
||||
#### 5. Configure Environment Variables
|
||||
In Vercel Dashboard:
|
||||
1. Go to Project Settings
|
||||
2. Environment Variables tab
|
||||
3. Add each variable:
|
||||
- Production
|
||||
- Preview (optional)
|
||||
- Development (optional)
|
||||
|
||||
#### 6. Test Deployment
|
||||
1. Wait for build to complete
|
||||
2. Visit deployed URL
|
||||
3. Test Odoo connection
|
||||
4. Verify all features work
|
||||
5. Check browser console for errors
|
||||
|
||||
---
|
||||
|
||||
## Add Netlify Deployment 🟢
|
||||
|
||||
### What You'll Get:
|
||||
- Git-based deployments
|
||||
- Serverless functions
|
||||
- Form handling
|
||||
- Split testing
|
||||
- Deploy previews
|
||||
- Custom domains
|
||||
|
||||
### Steps:
|
||||
|
||||
#### 1. Create `netlify.toml`
|
||||
```toml
|
||||
[build]
|
||||
command = "npm run build"
|
||||
publish = "build"
|
||||
|
||||
[build.environment]
|
||||
NODE_VERSION = "18"
|
||||
|
||||
[[redirects]]
|
||||
from = "/*"
|
||||
to = "/index.html"
|
||||
status = 200
|
||||
|
||||
[functions]
|
||||
directory = "netlify/functions"
|
||||
```
|
||||
|
||||
For React/Vue:
|
||||
```toml
|
||||
[build]
|
||||
command = "npm run build"
|
||||
publish = "dist"
|
||||
```
|
||||
|
||||
#### 2. Convert API Routes to Netlify Functions
|
||||
|
||||
Create `netlify/functions/odoo.js`:
|
||||
```javascript
|
||||
// Copy your API route logic here
|
||||
// Netlify functions use different format
|
||||
|
||||
exports.handler = async (event, context) => {
|
||||
// Parse request
|
||||
const body = JSON.parse(event.body);
|
||||
|
||||
// Your Odoo logic here
|
||||
// (copy from src/routes/api/odoo/+server.js)
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
body: JSON.stringify(result)
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
#### 3. Update Client to Use Netlify Function
|
||||
```javascript
|
||||
// Change API endpoint
|
||||
const response = await fetch('/.netlify/functions/odoo', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
```
|
||||
|
||||
#### 4. Deploy
|
||||
|
||||
**Option A: Netlify Dashboard**
|
||||
1. Go to https://app.netlify.com/start
|
||||
2. Connect repository
|
||||
3. Configure build settings
|
||||
4. Add environment variables
|
||||
5. Deploy
|
||||
|
||||
**Option B: Netlify CLI**
|
||||
```bash
|
||||
npm install -g netlify-cli
|
||||
netlify login
|
||||
netlify init
|
||||
netlify deploy --prod
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Add Cloudflare Pages Deployment 🟠
|
||||
|
||||
### What You'll Get:
|
||||
- Global CDN
|
||||
- Unlimited bandwidth
|
||||
- Edge functions
|
||||
- Preview deployments
|
||||
- Web Analytics
|
||||
- Fast performance
|
||||
|
||||
### Steps:
|
||||
|
||||
#### 1. Create `wrangler.toml` (for edge functions)
|
||||
```toml
|
||||
name = "odoo-pwa"
|
||||
compatibility_date = "2025-01-01"
|
||||
|
||||
[build]
|
||||
command = "npm run build"
|
||||
|
||||
[build.upload]
|
||||
format = "service-worker"
|
||||
```
|
||||
|
||||
#### 2. Convert API Routes to Workers
|
||||
|
||||
Create `functions/odoo.js`:
|
||||
```javascript
|
||||
export async function onRequest(context) {
|
||||
const { request, env } = context;
|
||||
|
||||
// Parse request
|
||||
const body = await request.json();
|
||||
|
||||
// Odoo logic here
|
||||
// Access env vars via env.ODOO_API_KEY
|
||||
|
||||
return new Response(JSON.stringify(result), {
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
#### 3. Deploy
|
||||
|
||||
**Option A: Cloudflare Dashboard**
|
||||
1. Go to Cloudflare Pages
|
||||
2. Connect Git repository
|
||||
3. Configure build:
|
||||
- Build command: `npm run build`
|
||||
- Output: `build` or `dist`
|
||||
4. Add environment variables
|
||||
5. Deploy
|
||||
|
||||
**Option B: Wrangler CLI**
|
||||
```bash
|
||||
npm install -g wrangler
|
||||
wrangler login
|
||||
wrangler pages project create odoo-pwa
|
||||
wrangler pages publish build
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Add GitHub Pages Deployment 📘
|
||||
|
||||
### ⚠️ Limitations:
|
||||
- Static hosting only
|
||||
- No server-side API routes
|
||||
- Must modify Odoo client for direct API calls
|
||||
- CORS issues possible
|
||||
|
||||
### When to Use:
|
||||
- Demo projects
|
||||
- Public apps (no sensitive data)
|
||||
- Frontend-only versions
|
||||
|
||||
### Steps:
|
||||
|
||||
#### 1. Update Base Path
|
||||
|
||||
**SvelteKit** (`svelte.config.js`):
|
||||
```javascript
|
||||
const config = {
|
||||
kit: {
|
||||
adapter: adapter({
|
||||
pages: 'build',
|
||||
assets: 'build',
|
||||
fallback: 'index.html'
|
||||
}),
|
||||
paths: {
|
||||
base: process.env.NODE_ENV === 'production'
|
||||
? '/your-repo-name'
|
||||
: ''
|
||||
}
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
**React/Vue** (`vite.config.js`):
|
||||
```javascript
|
||||
export default {
|
||||
base: '/your-repo-name/'
|
||||
};
|
||||
```
|
||||
|
||||
#### 2. Create `.github/workflows/deploy.yml`
|
||||
```yaml
|
||||
name: Deploy to GitHub Pages
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
- run: npm install
|
||||
|
||||
- run: npm run build
|
||||
env:
|
||||
VITE_ODOO_URL: ${{ secrets.VITE_ODOO_URL }}
|
||||
VITE_ODOO_DB: ${{ secrets.VITE_ODOO_DB }}
|
||||
VITE_MODEL_NAME: ${{ secrets.VITE_MODEL_NAME }}
|
||||
|
||||
- uses: actions/upload-pages-artifact@v3
|
||||
with:
|
||||
path: build
|
||||
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
|
||||
steps:
|
||||
- uses: actions/deploy-pages@v4
|
||||
```
|
||||
|
||||
#### 3. Configure Repository
|
||||
1. Go to Settings → Pages
|
||||
2. Source: GitHub Actions
|
||||
3. Save
|
||||
|
||||
#### 4. Add Secrets
|
||||
1. Settings → Secrets and variables → Actions
|
||||
2. Add each environment variable
|
||||
|
||||
#### 5. Push to Deploy
|
||||
```bash
|
||||
git add .
|
||||
git commit -m "Add GitHub Pages deployment"
|
||||
git push origin main
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Environment Variables Reference
|
||||
|
||||
### Required for All Platforms:
|
||||
```bash
|
||||
# Public (VITE_ prefix for client access)
|
||||
VITE_ODOO_URL=https://yourcompany.odoo.com
|
||||
VITE_ODOO_DB=yourcompany-main
|
||||
VITE_MODEL_NAME=x_expense
|
||||
VITE_MODEL_DISPLAY_NAME=Expense
|
||||
|
||||
# Private (server-side only)
|
||||
ODOO_API_KEY=your_production_api_key
|
||||
ODOO_USERNAME=your.email@company.com
|
||||
```
|
||||
|
||||
### Platform-Specific:
|
||||
|
||||
**Vercel:**
|
||||
- Add in Project Settings → Environment Variables
|
||||
- Separate for Production/Preview/Development
|
||||
|
||||
**Netlify:**
|
||||
- Add in Site Settings → Environment Variables
|
||||
- Or in `netlify.toml`
|
||||
|
||||
**Cloudflare:**
|
||||
- Add in Pages → Settings → Environment Variables
|
||||
- Or use Wrangler secrets
|
||||
|
||||
**GitHub Pages:**
|
||||
- Add in Repository Settings → Secrets
|
||||
- Used in GitHub Actions workflow
|
||||
|
||||
---
|
||||
|
||||
## Post-Deployment Checklist ✅
|
||||
|
||||
After deploying to new platform:
|
||||
|
||||
```
|
||||
□ Build completed successfully
|
||||
□ Application loads at deployment URL
|
||||
□ Odoo connection works
|
||||
□ Data syncs correctly
|
||||
□ CRUD operations work
|
||||
□ Offline mode functions
|
||||
□ Service worker registered
|
||||
□ PWA installs correctly
|
||||
□ Environment variables set correctly
|
||||
□ No console errors
|
||||
□ Tested on mobile
|
||||
□ Custom domain configured (if needed)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Multi-Platform Deployment
|
||||
|
||||
### Deploy to Multiple Platforms:
|
||||
You can deploy the same app to multiple platforms for:
|
||||
- Redundancy
|
||||
- A/B testing
|
||||
- Different regions
|
||||
- Different audiences
|
||||
|
||||
### Example Setup:
|
||||
```
|
||||
main branch → Vercel (primary production)
|
||||
staging branch → Netlify (staging)
|
||||
PRs → Vercel preview deployments
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting Deployment
|
||||
|
||||
### Build Fails
|
||||
- Check build logs
|
||||
- Test `npm run build` locally
|
||||
- Verify Node version matches
|
||||
- Check for missing env vars
|
||||
|
||||
### App Loads but Doesn't Work
|
||||
- Check environment variables are set
|
||||
- Look at browser console errors
|
||||
- Verify API routes deployed correctly
|
||||
- Test Odoo connection
|
||||
|
||||
### API Routes Not Working
|
||||
- Verify platform supports server-side code
|
||||
- Check function logs
|
||||
- Ensure correct paths used
|
||||
- Test API endpoint directly
|
||||
|
||||
---
|
||||
|
||||
## Example prompts to use this command:
|
||||
- `/add-deployment` - Add new deployment target
|
||||
- User: "Deploy to Netlify"
|
||||
- User: "Add Cloudflare deployment"
|
||||
- User: "Set up continuous deployment"
|
||||
|
||||
## Related Commands:
|
||||
- `/deploy-vercel` - Deploy to Vercel
|
||||
- `/deploy-github` - Deploy to GitHub Pages
|
||||
- `/test-connection` - Test before deploying
|
||||
- `/optimize` - Optimize before production
|
||||
|
||||
---
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Use Git-Based Deployment**
|
||||
- Automatic on push
|
||||
- Preview deployments
|
||||
- Easy rollbacks
|
||||
|
||||
2. **Separate Environments**
|
||||
- Development
|
||||
- Staging
|
||||
- Production
|
||||
|
||||
3. **Secure Secrets**
|
||||
- Never commit API keys
|
||||
- Use platform secret management
|
||||
- Rotate keys regularly
|
||||
|
||||
4. **Monitor Deployments**
|
||||
- Set up error tracking
|
||||
- Monitor performance
|
||||
- Watch build times
|
||||
|
||||
5. **Test Before Merging**
|
||||
- Use preview deployments
|
||||
- Test all features
|
||||
- Check on real devices
|
||||
46
commands/add-model.md
Normal file
46
commands/add-model.md
Normal file
@@ -0,0 +1,46 @@
|
||||
Add integration for a new Odoo Studio model to an existing PWA project.
|
||||
|
||||
## What this command does:
|
||||
- Invokes the `add-odoo-model` skill
|
||||
- Creates cache store for the new model
|
||||
- Adds API methods to the Odoo client
|
||||
- Generates form and list pages (optional)
|
||||
- Updates navigation and routing
|
||||
- Maintains consistency with existing project structure
|
||||
|
||||
## Required Information:
|
||||
Before starting, gather:
|
||||
1. **Current working directory** - Must be inside an existing Odoo PWA project
|
||||
2. **Framework** - Detect from project files (SvelteKit/React/Vue)
|
||||
3. **New model name** (without `x_` prefix, e.g., "task", "product")
|
||||
4. **Model display name** (human-readable, e.g., "Task", "Product")
|
||||
5. **Generate UI?** - Whether to create form and list pages (yes/no)
|
||||
|
||||
## Steps:
|
||||
1. Verify the current directory is an Odoo PWA project (check for odoo.js, cache stores)
|
||||
2. Detect the framework from project structure
|
||||
3. Ask the user for the new model details
|
||||
4. Create cache store for the model in `src/lib/stores/` or equivalent
|
||||
5. Update the Odoo API client with methods for the new model
|
||||
6. If requested, generate form and list pages/components
|
||||
7. Update navigation/routing if UI was generated
|
||||
8. Update documentation with the new model
|
||||
|
||||
## Example prompts to use this command:
|
||||
- `/add-model` - Interactive mode, will ask for all parameters
|
||||
- User: "Add a task model to my PWA"
|
||||
- User: "Integrate product catalog from Odoo"
|
||||
|
||||
## Validation:
|
||||
Before proceeding, check:
|
||||
- Project has `src/lib/odoo.js` or equivalent
|
||||
- Project has existing cache stores
|
||||
- `.env` file exists with Odoo configuration
|
||||
- Framework can be detected
|
||||
|
||||
## After adding model:
|
||||
Remind the user to:
|
||||
1. Update `.env` with any new model-specific configuration
|
||||
2. Test the new model's CRUD operations
|
||||
3. Verify sync functionality works correctly
|
||||
4. Update any necessary business logic or calculations
|
||||
734
commands/api-reference.md
Normal file
734
commands/api-reference.md
Normal file
@@ -0,0 +1,734 @@
|
||||
Complete API reference for the Odoo client and cache stores in generated PWAs.
|
||||
|
||||
## What this command does:
|
||||
- Provides comprehensive API documentation
|
||||
- Lists all available methods and functions
|
||||
- Shows parameter types and return values
|
||||
- Includes code examples for each method
|
||||
- Helps developers use the generated code effectively
|
||||
|
||||
---
|
||||
|
||||
## Odoo API Client Reference 🔌
|
||||
|
||||
The Odoo API client (`src/lib/odoo.js` or equivalent) provides methods for interacting with Odoo Studio models.
|
||||
|
||||
### Configuration
|
||||
|
||||
#### Environment Variables
|
||||
```bash
|
||||
VITE_ODOO_URL=https://yourcompany.odoo.com
|
||||
VITE_ODOO_DB=yourcompany-main
|
||||
ODOO_API_KEY=your_api_key
|
||||
ODOO_USERNAME=your.email@company.com
|
||||
VITE_MODEL_NAME=x_expense
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## CRUD Operations
|
||||
|
||||
### createRecord()
|
||||
Create a new record in Odoo.
|
||||
|
||||
**Signature:**
|
||||
```javascript
|
||||
async function createRecord(model, fields)
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `model` (string) - Odoo model name (e.g., 'x_expense')
|
||||
- `fields` (object) - Field values to set
|
||||
|
||||
**Returns:** Promise<number> - ID of created record
|
||||
|
||||
**Example:**
|
||||
```javascript
|
||||
const newId = await odoo.createRecord('x_expense', {
|
||||
x_studio_description: 'Team lunch',
|
||||
x_studio_amount: 45.50,
|
||||
x_studio_date: '2025-01-15',
|
||||
x_studio_category: 'meal',
|
||||
x_studio_employee: odoo.formatMany2one(12)
|
||||
});
|
||||
|
||||
console.log(`Created expense with ID: ${newId}`);
|
||||
```
|
||||
|
||||
**Error Handling:**
|
||||
```javascript
|
||||
try {
|
||||
const id = await odoo.createRecord('x_expense', fields);
|
||||
} catch (error) {
|
||||
console.error('Failed to create:', error.message);
|
||||
// Handle error (show message to user, retry, etc.)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### searchRecords()
|
||||
Search and read records from Odoo.
|
||||
|
||||
**Signature:**
|
||||
```javascript
|
||||
async function searchRecords(model, domain, fields, limit = null, offset = null)
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `model` (string) - Odoo model name
|
||||
- `domain` (array) - Odoo domain filter (e.g., [['id', '>', 100]])
|
||||
- `fields` (array) - List of field names to fetch
|
||||
- `limit` (number, optional) - Maximum number of records
|
||||
- `offset` (number, optional) - Number of records to skip
|
||||
|
||||
**Returns:** Promise<Array<Object>> - Array of record objects
|
||||
|
||||
**Example:**
|
||||
```javascript
|
||||
// Get all expenses
|
||||
const allExpenses = await odoo.searchRecords(
|
||||
'x_expense',
|
||||
[],
|
||||
['x_studio_description', 'x_studio_amount', 'x_studio_date']
|
||||
);
|
||||
|
||||
// Get expenses > $100
|
||||
const largeExpenses = await odoo.searchRecords(
|
||||
'x_expense',
|
||||
[['x_studio_amount', '>', 100]],
|
||||
['x_studio_description', 'x_studio_amount']
|
||||
);
|
||||
|
||||
// Get recent 10 expenses
|
||||
const recentExpenses = await odoo.searchRecords(
|
||||
'x_expense',
|
||||
[],
|
||||
['x_studio_description', 'x_studio_date'],
|
||||
10 // limit
|
||||
);
|
||||
|
||||
// Get expenses with pagination
|
||||
const page2 = await odoo.searchRecords(
|
||||
'x_expense',
|
||||
[],
|
||||
fields,
|
||||
20, // limit: 20 per page
|
||||
20 // offset: skip first 20 (page 2)
|
||||
);
|
||||
```
|
||||
|
||||
**Domain Syntax:**
|
||||
```javascript
|
||||
// Equals
|
||||
[['x_studio_status', '=', 'draft']]
|
||||
|
||||
// Greater than
|
||||
[['x_studio_amount', '>', 100]]
|
||||
|
||||
// In list
|
||||
[['x_studio_category', 'in', ['meal', 'travel']]]
|
||||
|
||||
// Multiple conditions (AND)
|
||||
[
|
||||
['x_studio_amount', '>', 50],
|
||||
['x_studio_status', '=', 'draft']
|
||||
]
|
||||
|
||||
// OR conditions
|
||||
['|',
|
||||
['x_studio_amount', '>', 100],
|
||||
['x_studio_category', '=', 'travel']
|
||||
]
|
||||
|
||||
// Complex: (amount > 100 OR category = travel) AND status = draft
|
||||
['&',
|
||||
'|',
|
||||
['x_studio_amount', '>', 100],
|
||||
['x_studio_category', '=', 'travel'],
|
||||
['x_studio_status', '=', 'draft']
|
||||
]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### updateRecord()
|
||||
Update an existing record.
|
||||
|
||||
**Signature:**
|
||||
```javascript
|
||||
async function updateRecord(model, id, values)
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `model` (string) - Odoo model name
|
||||
- `id` (number) - Record ID to update
|
||||
- `values` (object) - Fields to update
|
||||
|
||||
**Returns:** Promise<boolean> - true if successful
|
||||
|
||||
**Example:**
|
||||
```javascript
|
||||
await odoo.updateRecord('x_expense', 123, {
|
||||
x_studio_status: 'approved',
|
||||
x_studio_amount: 55.00
|
||||
});
|
||||
|
||||
// Update multiple fields
|
||||
await odoo.updateRecord('x_expense', 123, {
|
||||
x_studio_description: 'Updated description',
|
||||
x_studio_date: '2025-01-20',
|
||||
x_studio_notes: 'Added receipt'
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### deleteRecord()
|
||||
Delete a record from Odoo.
|
||||
|
||||
**Signature:**
|
||||
```javascript
|
||||
async function deleteRecord(model, id)
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `model` (string) - Odoo model name
|
||||
- `id` (number) - Record ID to delete
|
||||
|
||||
**Returns:** Promise<boolean> - true if successful
|
||||
|
||||
**Example:**
|
||||
```javascript
|
||||
await odoo.deleteRecord('x_expense', 123);
|
||||
|
||||
// With confirmation
|
||||
if (confirm('Are you sure you want to delete this expense?')) {
|
||||
await odoo.deleteRecord('x_expense', expenseId);
|
||||
}
|
||||
```
|
||||
|
||||
**Error Handling:**
|
||||
```javascript
|
||||
try {
|
||||
await odoo.deleteRecord('x_expense', id);
|
||||
console.log('Deleted successfully');
|
||||
} catch (error) {
|
||||
console.error('Failed to delete:', error.message);
|
||||
alert('Could not delete: ' + error.message);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Helper Methods
|
||||
|
||||
### fetchPartners()
|
||||
Fetch partner (res.partner) records.
|
||||
|
||||
**Signature:**
|
||||
```javascript
|
||||
async function fetchPartners(ids = null)
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `ids` (array, optional) - Specific partner IDs to fetch. If null, fetches all.
|
||||
|
||||
**Returns:** Promise<Array<Object>> - Array of partner objects
|
||||
|
||||
**Example:**
|
||||
```javascript
|
||||
// Fetch all partners
|
||||
const allPartners = await odoo.fetchPartners();
|
||||
|
||||
// Fetch specific partners
|
||||
const somePartners = await odoo.fetchPartners([1, 2, 3]);
|
||||
|
||||
// Use in dropdown
|
||||
const partners = await odoo.fetchPartners();
|
||||
// Display: partners.map(p => ({ value: p.id, label: p.name }))
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### formatMany2one()
|
||||
Format a Many2one field value for Odoo.
|
||||
|
||||
**Signature:**
|
||||
```javascript
|
||||
function formatMany2one(id)
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `id` (number | null) - Partner/record ID
|
||||
|
||||
**Returns:** Array<number, boolean> | false - Odoo-formatted value
|
||||
|
||||
**Example:**
|
||||
```javascript
|
||||
// Set employee field
|
||||
const fields = {
|
||||
x_studio_employee: odoo.formatMany2one(12)
|
||||
// Result: [12, false]
|
||||
};
|
||||
|
||||
// Clear employee field
|
||||
const fields = {
|
||||
x_studio_employee: odoo.formatMany2one(null)
|
||||
// Result: false
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### formatMany2many()
|
||||
Format a Many2many field value for Odoo.
|
||||
|
||||
**Signature:**
|
||||
```javascript
|
||||
function formatMany2many(ids)
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `ids` (array) - Array of record IDs
|
||||
|
||||
**Returns:** Array - Odoo command format
|
||||
|
||||
**Example:**
|
||||
```javascript
|
||||
// Set tags (replace all)
|
||||
const fields = {
|
||||
x_studio_tags: odoo.formatMany2many([1, 2, 3])
|
||||
// Result: [[6, 0, [1, 2, 3]]]
|
||||
};
|
||||
|
||||
// Clear tags
|
||||
const fields = {
|
||||
x_studio_tags: odoo.formatMany2many([])
|
||||
// Result: [[6, 0, []]]
|
||||
};
|
||||
```
|
||||
|
||||
**Odoo Many2many Commands:**
|
||||
```javascript
|
||||
// (6, 0, [ids]) - Replace all (what formatMany2many uses)
|
||||
// (4, id) - Add link to id
|
||||
// (3, id) - Remove link to id
|
||||
// (5, 0) - Remove all links
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Cache Store API Reference 💾
|
||||
|
||||
The cache store provides reactive state management with offline-first capabilities.
|
||||
|
||||
### Properties
|
||||
|
||||
#### records
|
||||
**Type:** Reactive Array<Object>
|
||||
|
||||
Current cached records.
|
||||
|
||||
**Example:**
|
||||
```javascript
|
||||
// SvelteKit
|
||||
$: totalAmount = $expenseCache.reduce((sum, e) => sum + e.x_studio_amount, 0);
|
||||
|
||||
// React
|
||||
const totalAmount = useMemo(() =>
|
||||
records.reduce((sum, e) => sum + e.x_studio_amount, 0),
|
||||
[records]
|
||||
);
|
||||
|
||||
// Vue
|
||||
const totalAmount = computed(() =>
|
||||
expenseStore.records.reduce((sum, e) => sum + e.x_studio_amount, 0)
|
||||
);
|
||||
```
|
||||
|
||||
#### isLoading
|
||||
**Type:** Reactive Boolean
|
||||
|
||||
Loading state indicator.
|
||||
|
||||
**Example:**
|
||||
```javascript
|
||||
{#if $expenseCache.isLoading}
|
||||
<LoadingSpinner />
|
||||
{:else}
|
||||
<ExpenseList />
|
||||
{/if}
|
||||
```
|
||||
|
||||
#### error
|
||||
**Type:** Reactive String | null
|
||||
|
||||
Current error message, if any.
|
||||
|
||||
**Example:**
|
||||
```javascript
|
||||
{#if $expenseCache.error}
|
||||
<ErrorAlert message={$expenseCache.error} />
|
||||
{/if}
|
||||
```
|
||||
|
||||
#### lastSync
|
||||
**Type:** Reactive Number (timestamp)
|
||||
|
||||
Timestamp of last successful sync.
|
||||
|
||||
**Example:**
|
||||
```javascript
|
||||
const timeSinceSync = Date.now() - $expenseCache.lastSync;
|
||||
const minutes = Math.floor(timeSinceSync / 60000);
|
||||
// Display: "Last synced ${minutes} minutes ago"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Methods
|
||||
|
||||
### load()
|
||||
Load records from cache and trigger background sync.
|
||||
|
||||
**Signature:**
|
||||
```javascript
|
||||
async function load()
|
||||
```
|
||||
|
||||
**Returns:** Promise<void>
|
||||
|
||||
**Behavior:**
|
||||
1. Loads from cache immediately (instant UI update)
|
||||
2. Checks if cache is stale (> 5 minutes)
|
||||
3. If stale, syncs in background
|
||||
4. Updates UI when new data arrives
|
||||
|
||||
**Example:**
|
||||
```javascript
|
||||
// SvelteKit
|
||||
$effect(() => {
|
||||
expenseCache.load();
|
||||
});
|
||||
|
||||
// React
|
||||
useEffect(() => {
|
||||
expenseCache.load();
|
||||
}, []);
|
||||
|
||||
// Vue
|
||||
onMounted(() => {
|
||||
expenseStore.load();
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### create()
|
||||
Create a new record with optimistic update.
|
||||
|
||||
**Signature:**
|
||||
```javascript
|
||||
async function create(data)
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `data` (object) - Field values for new record
|
||||
|
||||
**Returns:** Promise<number> - ID of created record
|
||||
|
||||
**Behavior:**
|
||||
1. Generates temporary ID
|
||||
2. Adds to cache immediately (optimistic)
|
||||
3. Creates in Odoo (background)
|
||||
4. Replaces temp ID with real ID
|
||||
5. Rolls back on error
|
||||
|
||||
**Example:**
|
||||
```javascript
|
||||
try {
|
||||
const newId = await expenseCache.create({
|
||||
x_studio_description: 'Lunch meeting',
|
||||
x_studio_amount: 45.50,
|
||||
x_studio_date: '2025-01-15',
|
||||
x_studio_category: 'meal'
|
||||
});
|
||||
|
||||
console.log('Created:', newId);
|
||||
navigate(`/expenses/${newId}`);
|
||||
} catch (error) {
|
||||
alert('Failed to create: ' + error.message);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### update()
|
||||
Update an existing record.
|
||||
|
||||
**Signature:**
|
||||
```javascript
|
||||
async function update(id, data)
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `id` (number) - Record ID
|
||||
- `data` (object) - Fields to update
|
||||
|
||||
**Returns:** Promise<boolean>
|
||||
|
||||
**Behavior:**
|
||||
1. Updates cache immediately (optimistic)
|
||||
2. Updates in Odoo (background)
|
||||
3. Rolls back on error
|
||||
|
||||
**Example:**
|
||||
```javascript
|
||||
await expenseCache.update(123, {
|
||||
x_studio_amount: 50.00,
|
||||
x_studio_status: 'submitted'
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### delete()
|
||||
Delete a record.
|
||||
|
||||
**Signature:**
|
||||
```javascript
|
||||
async function remove(id)
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `id` (number) - Record ID to delete
|
||||
|
||||
**Returns:** Promise<boolean>
|
||||
|
||||
**Behavior:**
|
||||
1. Removes from cache immediately
|
||||
2. Deletes from Odoo (background)
|
||||
3. Restores on error
|
||||
|
||||
**Example:**
|
||||
```javascript
|
||||
if (confirm('Delete this expense?')) {
|
||||
try {
|
||||
await expenseCache.remove(123);
|
||||
navigate('/expenses');
|
||||
} catch (error) {
|
||||
alert('Failed to delete: ' + error.message);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### refresh()
|
||||
Force refresh from Odoo.
|
||||
|
||||
**Signature:**
|
||||
```javascript
|
||||
async function refresh()
|
||||
```
|
||||
|
||||
**Returns:** Promise<void>
|
||||
|
||||
**Behavior:**
|
||||
1. Fetches all records from Odoo
|
||||
2. Replaces cache
|
||||
3. Updates UI
|
||||
|
||||
**Example:**
|
||||
```javascript
|
||||
// Manual refresh button
|
||||
<button onclick={() => expenseCache.refresh()}>
|
||||
Refresh
|
||||
</button>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### clearCache()
|
||||
Clear all cached data.
|
||||
|
||||
**Signature:**
|
||||
```javascript
|
||||
function clearCache()
|
||||
```
|
||||
|
||||
**Returns:** void
|
||||
|
||||
**Behavior:**
|
||||
1. Clears localStorage
|
||||
2. Clears IndexedDB
|
||||
3. Resets records to empty array
|
||||
|
||||
**Example:**
|
||||
```javascript
|
||||
// Logout function
|
||||
async function logout() {
|
||||
expenseCache.clearCache();
|
||||
// Clear other caches
|
||||
navigate('/login');
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Advanced Patterns
|
||||
|
||||
### Custom Filters
|
||||
```javascript
|
||||
// Derived store (SvelteKit)
|
||||
import { derived } from 'svelte/store';
|
||||
|
||||
export const draftExpenses = derived(
|
||||
expenseCache,
|
||||
$cache => $cache.filter(e => e.x_studio_status === 'draft')
|
||||
);
|
||||
|
||||
// Hook (React)
|
||||
function useDraftExpenses() {
|
||||
const { records } = useExpense();
|
||||
return useMemo(
|
||||
() => records.filter(e => e.x_studio_status === 'draft'),
|
||||
[records]
|
||||
);
|
||||
}
|
||||
|
||||
// Computed (Vue)
|
||||
const draftExpenses = computed(() =>
|
||||
expenseStore.records.filter(e => e.x_studio_status === 'draft')
|
||||
);
|
||||
```
|
||||
|
||||
### Sorting
|
||||
```javascript
|
||||
export const sortedExpenses = derived(
|
||||
expenseCache,
|
||||
$cache => [...$cache].sort((a, b) =>
|
||||
b.x_studio_date.localeCompare(a.x_studio_date)
|
||||
)
|
||||
);
|
||||
```
|
||||
|
||||
### Search
|
||||
```javascript
|
||||
function searchExpenses(query) {
|
||||
return records.filter(e =>
|
||||
e.x_studio_description.toLowerCase().includes(query.toLowerCase())
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Grouping
|
||||
```javascript
|
||||
function groupByCategory(records) {
|
||||
return records.reduce((groups, record) => {
|
||||
const category = record.x_studio_category;
|
||||
if (!groups[category]) groups[category] = [];
|
||||
groups[category].push(record);
|
||||
return groups;
|
||||
}, {});
|
||||
}
|
||||
```
|
||||
|
||||
### Aggregation
|
||||
```javascript
|
||||
function getTotalByCategory(records) {
|
||||
return records.reduce((totals, record) => {
|
||||
const cat = record.x_studio_category;
|
||||
totals[cat] = (totals[cat] || 0) + record.x_studio_amount;
|
||||
return totals;
|
||||
}, {});
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Server Route API Reference 🔐
|
||||
|
||||
The server route (`src/routes/api/odoo/+server.js`) handles Odoo communication.
|
||||
|
||||
### Endpoint
|
||||
**URL:** `/api/odoo`
|
||||
**Method:** POST
|
||||
**Content-Type:** application/json
|
||||
|
||||
### Request Format
|
||||
```json
|
||||
{
|
||||
"action": "create|search|update|delete",
|
||||
"model": "x_expense",
|
||||
...parameters
|
||||
}
|
||||
```
|
||||
|
||||
### Actions
|
||||
|
||||
#### create
|
||||
```json
|
||||
{
|
||||
"action": "create",
|
||||
"model": "x_expense",
|
||||
"fields": {
|
||||
"x_studio_description": "Lunch",
|
||||
"x_studio_amount": 45.50
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Response:** `{ "id": 123 }`
|
||||
|
||||
#### search
|
||||
```json
|
||||
{
|
||||
"action": "search",
|
||||
"model": "x_expense",
|
||||
"domain": [["id", ">", 100]],
|
||||
"fields": ["x_studio_description", "x_studio_amount"]
|
||||
}
|
||||
```
|
||||
|
||||
**Response:** `{ "records": [...] }`
|
||||
|
||||
#### update
|
||||
```json
|
||||
{
|
||||
"action": "update",
|
||||
"model": "x_expense",
|
||||
"id": 123,
|
||||
"values": {
|
||||
"x_studio_amount": 50.00
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Response:** `{ "success": true }`
|
||||
|
||||
#### delete
|
||||
```json
|
||||
{
|
||||
"action": "delete",
|
||||
"model": "x_expense",
|
||||
"id": 123
|
||||
}
|
||||
```
|
||||
|
||||
**Response:** `{ "success": true }`
|
||||
|
||||
---
|
||||
|
||||
## Example prompts to use this command:
|
||||
- `/api-reference` - Show complete API documentation
|
||||
- User: "What methods are available in the Odoo client?"
|
||||
- User: "How do I use the cache store?"
|
||||
- User: "Show me API examples"
|
||||
|
||||
## Next Steps:
|
||||
- Try the methods in your project
|
||||
- Review `/examples` for practical use cases
|
||||
- See `/architecture` for design patterns
|
||||
- Check `/help` for more information
|
||||
634
commands/architecture.md
Normal file
634
commands/architecture.md
Normal file
@@ -0,0 +1,634 @@
|
||||
Detailed explanation of the Odoo PWA architecture, patterns, and design decisions.
|
||||
|
||||
## What this command does:
|
||||
- Explains the architectural patterns used in generated PWAs
|
||||
- Details the data flow and state management
|
||||
- Describes the caching strategy
|
||||
- Explains offline-first design principles
|
||||
- Provides insights into technical decisions
|
||||
|
||||
---
|
||||
|
||||
## Architecture Overview 🏗️
|
||||
|
||||
The Odoo PWA Generator creates **offline-first Progressive Web Apps** with a **three-layer architecture** that ensures data availability, performance, and seamless Odoo integration.
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
│ UI Layer (Components) │
|
||||
│ - Forms, Lists, Navigation │
|
||||
│ - Framework-specific (Svelte/React/Vue)│
|
||||
└──────────────┬──────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────┐
|
||||
│ State Layer (Cache Stores) │
|
||||
│ - Smart Caching Logic │
|
||||
│ - Dual Storage (localStorage + IndexedDB) │
|
||||
│ - Background Sync │
|
||||
│ - Optimistic Updates │
|
||||
└──────────────┬──────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────┐
|
||||
│ API Layer (Odoo Client) │
|
||||
│ - JSON-RPC Communication │
|
||||
│ - CRUD Operations │
|
||||
│ - Field Formatting │
|
||||
└──────────────┬──────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────┐
|
||||
│ Server Layer (API Routes) │
|
||||
│ - Credential Management │
|
||||
│ - UID Caching │
|
||||
│ - Error Handling │
|
||||
└──────────────┬──────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────┐
|
||||
│ Odoo Backend │
|
||||
│ - Studio Models │
|
||||
│ - Business Logic │
|
||||
│ - Data Persistence │
|
||||
└─────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Layer 1: UI Components 🎨
|
||||
|
||||
### Purpose:
|
||||
Present data to users and capture user input.
|
||||
|
||||
### Responsibilities:
|
||||
- Render data from cache stores
|
||||
- Handle user interactions
|
||||
- Validate form inputs
|
||||
- Display loading and error states
|
||||
- Provide responsive, mobile-friendly interface
|
||||
|
||||
### Framework-Specific Implementation:
|
||||
|
||||
#### SvelteKit
|
||||
```javascript
|
||||
<script>
|
||||
import { expenseCache } from '$lib/stores/expenseCache';
|
||||
|
||||
// Reactive to cache updates
|
||||
$effect(() => {
|
||||
expenseCache.load();
|
||||
});
|
||||
</script>
|
||||
|
||||
{#each $expenseCache as expense}
|
||||
<ExpenseCard {expense} />
|
||||
{/each}
|
||||
```
|
||||
|
||||
#### React
|
||||
```javascript
|
||||
import { useExpense } from './contexts/ExpenseContext';
|
||||
|
||||
function ExpenseList() {
|
||||
const { records, isLoading } = useExpense();
|
||||
|
||||
useEffect(() => {
|
||||
// Load on mount
|
||||
}, []);
|
||||
|
||||
return records.map(expense => (
|
||||
<ExpenseCard key={expense.id} expense={expense} />
|
||||
));
|
||||
}
|
||||
```
|
||||
|
||||
#### Vue
|
||||
```javascript
|
||||
<script setup>
|
||||
import { useExpenseStore } from '@/stores/expenseStore';
|
||||
|
||||
const expenseStore = useExpenseStore();
|
||||
expenseStore.load();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ExpenseCard
|
||||
v-for="expense in expenseStore.records"
|
||||
:key="expense.id"
|
||||
:expense="expense"
|
||||
/>
|
||||
</template>
|
||||
```
|
||||
|
||||
### Design Principles:
|
||||
- **Reactive by default** - UI updates automatically when data changes
|
||||
- **Loading states** - Show skeleton loaders while data fetches
|
||||
- **Error boundaries** - Graceful error handling
|
||||
- **Optimistic UI** - Show changes immediately, sync in background
|
||||
|
||||
---
|
||||
|
||||
## Layer 2: Cache Stores (State Management) 💾
|
||||
|
||||
### Purpose:
|
||||
Manage application state with offline-first caching.
|
||||
|
||||
### The Smart Caching Pattern:
|
||||
|
||||
```javascript
|
||||
// Two-phase load strategy
|
||||
export async function load() {
|
||||
// Phase 1: Load from cache (instant)
|
||||
const cached = await loadFromCache();
|
||||
records.set(cached); // UI shows data immediately
|
||||
|
||||
// Phase 2: Check if stale and sync
|
||||
if (isCacheStale()) {
|
||||
await syncInBackground(); // Update in background
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Dual Storage Strategy:
|
||||
|
||||
#### localStorage (Metadata)
|
||||
Stores lightweight metadata:
|
||||
- `lastSyncTime` - When was last successful sync
|
||||
- `lastRecordId` - Highest ID fetched so far
|
||||
- `version` - Cache schema version
|
||||
|
||||
```javascript
|
||||
localStorage.setItem('expenseCache', JSON.stringify({
|
||||
lastSyncTime: Date.now(),
|
||||
lastRecordId: 123,
|
||||
version: 1
|
||||
}));
|
||||
```
|
||||
|
||||
#### IndexedDB (Master Data)
|
||||
Stores actual records:
|
||||
- All record data
|
||||
- Larger storage capacity
|
||||
- Async API
|
||||
- Structured data
|
||||
|
||||
```javascript
|
||||
await db.expenses.bulkPut(records);
|
||||
```
|
||||
|
||||
### Stale Detection:
|
||||
|
||||
```javascript
|
||||
function isCacheStale() {
|
||||
const cacheData = JSON.parse(localStorage.getItem('expenseCache'));
|
||||
const CACHE_VALIDITY = 5 * 60 * 1000; // 5 minutes
|
||||
|
||||
return !cacheData ||
|
||||
(Date.now() - cacheData.lastSyncTime) > CACHE_VALIDITY;
|
||||
}
|
||||
```
|
||||
|
||||
### Incremental Sync Pattern:
|
||||
|
||||
```javascript
|
||||
// Only fetch new records, not everything
|
||||
async function syncInBackground() {
|
||||
const { lastRecordId } = getCacheMetadata();
|
||||
|
||||
// Fetch only records with id > lastRecordId
|
||||
const newRecords = await odoo.searchRecords(
|
||||
'x_expense',
|
||||
[['id', '>', lastRecordId]],
|
||||
fields
|
||||
);
|
||||
|
||||
if (newRecords.length > 0) {
|
||||
await appendToCache(newRecords);
|
||||
updateLastRecordId(newRecords[newRecords.length - 1].id);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Optimistic Updates:
|
||||
|
||||
```javascript
|
||||
export async function create(data) {
|
||||
// 1. Generate temporary ID
|
||||
const tempId = `temp-${Date.now()}`;
|
||||
const tempRecord = { id: tempId, ...data };
|
||||
|
||||
// 2. Update UI immediately
|
||||
records.update(r => [...r, tempRecord]);
|
||||
|
||||
// 3. Create in Odoo (background)
|
||||
try {
|
||||
const realId = await odoo.createRecord('x_expense', data);
|
||||
|
||||
// 4. Replace temp ID with real ID
|
||||
records.update(r =>
|
||||
r.map(rec => rec.id === tempId ? { ...rec, id: realId } : rec)
|
||||
);
|
||||
} catch (error) {
|
||||
// 5. Rollback on error
|
||||
records.update(r => r.filter(rec => rec.id !== tempId));
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Background Sync Timer:
|
||||
|
||||
```javascript
|
||||
let syncInterval;
|
||||
|
||||
export function startAutoSync() {
|
||||
syncInterval = setInterval(() => {
|
||||
syncInBackground();
|
||||
}, 3 * 60 * 1000); // Every 3 minutes
|
||||
}
|
||||
|
||||
export function stopAutoSync() {
|
||||
clearInterval(syncInterval);
|
||||
}
|
||||
```
|
||||
|
||||
### Partner Resolution Pattern:
|
||||
|
||||
```javascript
|
||||
// Many2one fields return [id, name] or just id
|
||||
// Resolve partner names and cache them
|
||||
|
||||
async function resolvePartnerNames(records) {
|
||||
const partnerIds = new Set();
|
||||
|
||||
// Collect all unique partner IDs
|
||||
records.forEach(record => {
|
||||
if (record.x_studio_employee) {
|
||||
if (Array.isArray(record.x_studio_employee)) {
|
||||
partnerIds.add(record.x_studio_employee[0]);
|
||||
} else {
|
||||
partnerIds.add(record.x_studio_employee);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Fetch partner names in batch
|
||||
const partners = await odoo.fetchPartners(Array.from(partnerIds));
|
||||
const partnerMap = new Map(partners.map(p => [p.id, p.name]));
|
||||
|
||||
// Cache for future use
|
||||
localStorage.setItem('partnerCache', JSON.stringify(
|
||||
Array.from(partnerMap.entries())
|
||||
));
|
||||
|
||||
return partnerMap;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Layer 3: API Client (Odoo Communication) 🔌
|
||||
|
||||
### Purpose:
|
||||
Abstract Odoo API communication with clean, reusable methods.
|
||||
|
||||
### JSON-RPC Communication:
|
||||
|
||||
```javascript
|
||||
async function jsonRpc(url, params) {
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
jsonrpc: '2.0',
|
||||
method: 'call',
|
||||
params: params,
|
||||
id: Math.random()
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.error) {
|
||||
throw new Error(data.error.message);
|
||||
}
|
||||
|
||||
return data.result;
|
||||
}
|
||||
```
|
||||
|
||||
### CRUD Methods:
|
||||
|
||||
#### Create
|
||||
```javascript
|
||||
export async function createRecord(model, fields) {
|
||||
return await fetch('/api/odoo', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
action: 'create',
|
||||
model,
|
||||
fields
|
||||
})
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
#### Read
|
||||
```javascript
|
||||
export async function searchRecords(model, domain, fields) {
|
||||
return await fetch('/api/odoo', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
action: 'search',
|
||||
model,
|
||||
domain,
|
||||
fields
|
||||
})
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
#### Update
|
||||
```javascript
|
||||
export async function updateRecord(model, id, values) {
|
||||
return await fetch('/api/odoo', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
action: 'update',
|
||||
model,
|
||||
id,
|
||||
values
|
||||
})
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
#### Delete
|
||||
```javascript
|
||||
export async function deleteRecord(model, id) {
|
||||
return await fetch('/api/odoo', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
action: 'delete',
|
||||
model,
|
||||
id
|
||||
})
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### Field Formatting Helpers:
|
||||
|
||||
```javascript
|
||||
// Many2one: Convert to Odoo format [id, false]
|
||||
export function formatMany2one(id) {
|
||||
return id ? [id, false] : false;
|
||||
}
|
||||
|
||||
// Many2many: Convert to Odoo command [(6, 0, [ids])]
|
||||
export function formatMany2many(ids) {
|
||||
return ids && ids.length > 0 ? [[6, 0, ids]] : [[6, 0, []]];
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Layer 4: Server Routes (API Proxy) 🔐
|
||||
|
||||
### Purpose:
|
||||
Securely handle Odoo authentication and proxy requests.
|
||||
|
||||
### Why Server-Side?
|
||||
1. **Security** - API keys never exposed to client
|
||||
2. **CORS** - Bypass cross-origin restrictions
|
||||
3. **Caching** - Cache UIDs to reduce auth calls
|
||||
4. **Error Handling** - Centralized error management
|
||||
|
||||
### UID Caching Pattern:
|
||||
|
||||
```javascript
|
||||
let cachedUid = null;
|
||||
|
||||
async function authenticate() {
|
||||
if (cachedUid) {
|
||||
return cachedUid;
|
||||
}
|
||||
|
||||
// Authenticate with Odoo
|
||||
cachedUid = await odooClient.authenticate(
|
||||
db, username, apiKey
|
||||
);
|
||||
|
||||
return cachedUid;
|
||||
}
|
||||
```
|
||||
|
||||
### Action Routing:
|
||||
|
||||
```javascript
|
||||
export async function POST({ request }) {
|
||||
const { action, model, ...params } = await request.json();
|
||||
const uid = await authenticate();
|
||||
|
||||
switch (action) {
|
||||
case 'create':
|
||||
return odooClient.create(model, params.fields);
|
||||
case 'search':
|
||||
return odooClient.searchRead(model, params.domain, params.fields);
|
||||
case 'update':
|
||||
return odooClient.write(model, params.id, params.values);
|
||||
case 'delete':
|
||||
return odooClient.unlink(model, params.id);
|
||||
default:
|
||||
throw new Error(`Unknown action: ${action}`);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Data Flow Examples 🔄
|
||||
|
||||
### Example 1: Loading Data on Page Load
|
||||
|
||||
```
|
||||
1. User navigates to page
|
||||
↓
|
||||
2. Component calls expenseCache.load()
|
||||
↓
|
||||
3. Cache store loads from localStorage/IndexedDB
|
||||
↓
|
||||
4. UI updates with cached data (instant)
|
||||
↓
|
||||
5. Cache checks if data is stale (> 5 min)
|
||||
↓
|
||||
6. If stale, triggers background sync
|
||||
↓
|
||||
7. API client calls /api/odoo
|
||||
↓
|
||||
8. Server authenticates and calls Odoo
|
||||
↓
|
||||
9. New records returned
|
||||
↓
|
||||
10. Cache updated (localStorage + IndexedDB)
|
||||
↓
|
||||
11. UI reactively updates with new data
|
||||
```
|
||||
|
||||
### Example 2: Creating a Record
|
||||
|
||||
```
|
||||
1. User submits form
|
||||
↓
|
||||
2. Component calls expenseCache.create(data)
|
||||
↓
|
||||
3. Cache creates temp record with temp-ID
|
||||
↓
|
||||
4. UI updates immediately (optimistic)
|
||||
↓
|
||||
5. API client calls /api/odoo (background)
|
||||
↓
|
||||
6. Server creates record in Odoo
|
||||
↓
|
||||
7. Real ID returned
|
||||
↓
|
||||
8. Cache replaces temp-ID with real ID
|
||||
↓
|
||||
9. localStorage and IndexedDB updated
|
||||
↓
|
||||
10. UI shows success message
|
||||
```
|
||||
|
||||
### Example 3: Offline Create → Online Sync
|
||||
|
||||
```
|
||||
1. User creates record (offline)
|
||||
↓
|
||||
2. Record saved to cache with temp-ID
|
||||
↓
|
||||
3. API call fails (no network)
|
||||
↓
|
||||
4. Record marked as "pending sync"
|
||||
↓
|
||||
5. UI shows "Will sync when online"
|
||||
↓
|
||||
[User goes online]
|
||||
↓
|
||||
6. Background sync detects pending records
|
||||
↓
|
||||
7. Retries API call
|
||||
↓
|
||||
8. Success! Real ID received
|
||||
↓
|
||||
9. Cache updated
|
||||
↓
|
||||
10. UI shows "Synced" status
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Key Design Patterns 🎯
|
||||
|
||||
### 1. Offline-First
|
||||
- Always load from cache first
|
||||
- Sync in background
|
||||
- Queue operations when offline
|
||||
- Retry on reconnection
|
||||
|
||||
### 2. Optimistic UI
|
||||
- Update UI immediately
|
||||
- Sync with server in background
|
||||
- Rollback on error
|
||||
- Show pending states
|
||||
|
||||
### 3. Incremental Sync
|
||||
- Don't re-fetch all data
|
||||
- Only fetch new records (id > lastRecordId)
|
||||
- Reduces bandwidth
|
||||
- Faster sync times
|
||||
|
||||
### 4. Dual Storage
|
||||
- localStorage for metadata (fast)
|
||||
- IndexedDB for data (large)
|
||||
- Best of both worlds
|
||||
|
||||
### 5. Partner Resolution
|
||||
- Batch fetch related records
|
||||
- Cache partner names
|
||||
- Avoid N+1 queries
|
||||
- Display human-readable names
|
||||
|
||||
### 6. Reactive State
|
||||
- Framework-native reactivity
|
||||
- UI updates automatically
|
||||
- No manual DOM manipulation
|
||||
- Cleaner code
|
||||
|
||||
---
|
||||
|
||||
## Performance Considerations ⚡
|
||||
|
||||
### Initial Load:
|
||||
- Cache-first: Instant data display
|
||||
- Background sync: Fetch updates without blocking
|
||||
- IndexedDB: Fast access to large datasets
|
||||
|
||||
### Network Usage:
|
||||
- Incremental sync: Only new data
|
||||
- Batch operations: Combine requests
|
||||
- UID caching: Reduce auth calls
|
||||
|
||||
### Memory Usage:
|
||||
- Store large data in IndexedDB, not memory
|
||||
- Clean up old data periodically
|
||||
- Lazy load related data
|
||||
|
||||
### Bundle Size:
|
||||
- Framework-specific optimizations
|
||||
- Tree shaking
|
||||
- Code splitting
|
||||
- Lazy load routes
|
||||
|
||||
---
|
||||
|
||||
## Security Patterns 🔒
|
||||
|
||||
### Credential Management:
|
||||
- API keys in environment variables
|
||||
- Server-side authentication
|
||||
- Never expose keys to client
|
||||
- Rotate keys periodically
|
||||
|
||||
### Data Validation:
|
||||
- Validate on client (UX)
|
||||
- Validate on server (security)
|
||||
- Sanitize inputs
|
||||
- Check permissions
|
||||
|
||||
### Error Handling:
|
||||
- Don't expose internal errors to user
|
||||
- Log errors securely
|
||||
- Graceful degradation
|
||||
- User-friendly messages
|
||||
|
||||
---
|
||||
|
||||
## Example prompts to use this command:
|
||||
- `/architecture` - Show architecture details
|
||||
- User: "How does the caching work?"
|
||||
- User: "Explain the data flow"
|
||||
- User: "What design patterns are used?"
|
||||
|
||||
## Next Steps:
|
||||
After understanding the architecture:
|
||||
1. Review generated code in your project
|
||||
2. Read CLAUDE.md in your project for specific details
|
||||
3. Customize patterns for your use case
|
||||
4. Optimize for your specific needs
|
||||
|
||||
For more information, see `/help` or `/examples`!
|
||||
597
commands/clear-cache.md
Normal file
597
commands/clear-cache.md
Normal file
@@ -0,0 +1,597 @@
|
||||
Clear all cached data from your Odoo PWA application.
|
||||
|
||||
## What this command does:
|
||||
- Clears localStorage cache
|
||||
- Clears IndexedDB data
|
||||
- Clears browser cache
|
||||
- Resets service worker cache
|
||||
- Forces fresh data fetch from Odoo
|
||||
- Provides selective clearing options
|
||||
|
||||
## When to Use This Command
|
||||
|
||||
### ✅ Good Reasons:
|
||||
- Data appears corrupted or inconsistent
|
||||
- Testing fresh installation
|
||||
- After Odoo schema changes
|
||||
- Debugging sync issues
|
||||
- After major updates
|
||||
- Stuck with old data
|
||||
|
||||
### ⚠️ Caution:
|
||||
- Clears all offline data
|
||||
- May lose unsyncedchanges
|
||||
- Requires re-download of all data
|
||||
- User will need to be online
|
||||
|
||||
---
|
||||
|
||||
## Quick Clear Options
|
||||
|
||||
### Option 1: Clear from Browser Console (Fastest)
|
||||
```javascript
|
||||
// Clear localStorage
|
||||
localStorage.clear();
|
||||
|
||||
// Clear and refresh
|
||||
localStorage.clear();
|
||||
location.reload();
|
||||
```
|
||||
|
||||
### Option 2: Clear from Cache Store
|
||||
```javascript
|
||||
// Using cache store method
|
||||
expenseCache.clearCache();
|
||||
expenseCache.refresh();
|
||||
```
|
||||
|
||||
### Option 3: Clear from UI
|
||||
Add a button to your app:
|
||||
```javascript
|
||||
<button onclick={() => {
|
||||
if (confirm('Clear all cached data?')) {
|
||||
expenseCache.clearCache();
|
||||
expenseCache.refresh();
|
||||
}
|
||||
}}>
|
||||
Clear Cache
|
||||
</button>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Complete Clear Procedure
|
||||
|
||||
### Step 1: Prepare
|
||||
```
|
||||
□ Save any unsaved work
|
||||
□ Ensure internet connection
|
||||
□ Note current state (for comparison)
|
||||
□ Close other tabs with same app
|
||||
```
|
||||
|
||||
### Step 2: Clear localStorage
|
||||
```javascript
|
||||
// Clear all
|
||||
localStorage.clear();
|
||||
|
||||
// Or clear specific keys
|
||||
localStorage.removeItem('expenseCache');
|
||||
localStorage.removeItem('taskCache');
|
||||
localStorage.removeItem('partnerCache');
|
||||
```
|
||||
|
||||
### Step 3: Clear IndexedDB
|
||||
```javascript
|
||||
// Via DevTools:
|
||||
// 1. Open DevTools (F12)
|
||||
// 2. Go to Application tab
|
||||
// 3. Expand IndexedDB in left sidebar
|
||||
// 4. Right-click on database → Delete
|
||||
|
||||
// Or programmatically:
|
||||
indexedDB.deleteDatabase('odoo-pwa-db');
|
||||
```
|
||||
|
||||
### Step 4: Clear Service Worker Cache
|
||||
```javascript
|
||||
// Unregister service worker
|
||||
navigator.serviceWorker.getRegistrations()
|
||||
.then(registrations => {
|
||||
registrations.forEach(reg => reg.unregister());
|
||||
});
|
||||
|
||||
// Clear all caches
|
||||
caches.keys()
|
||||
.then(keys => Promise.all(
|
||||
keys.map(key => caches.delete(key))
|
||||
));
|
||||
```
|
||||
|
||||
### Step 5: Clear Browser Cache
|
||||
```
|
||||
Chrome/Edge: Ctrl+Shift+Delete → Select cache → Clear
|
||||
Firefox: Ctrl+Shift+Delete → Select cache → Clear now
|
||||
Safari: Cmd+Option+E
|
||||
```
|
||||
|
||||
### Step 6: Hard Refresh
|
||||
```
|
||||
Windows/Linux: Ctrl+Shift+R or Ctrl+F5
|
||||
Mac: Cmd+Shift+R
|
||||
```
|
||||
|
||||
### Step 7: Verify
|
||||
```javascript
|
||||
// Check localStorage is empty
|
||||
console.log('localStorage size:', localStorage.length);
|
||||
|
||||
// Check IndexedDB
|
||||
// DevTools → Application → IndexedDB
|
||||
// Should be empty or recreated
|
||||
|
||||
// Check cache stores
|
||||
console.log('Records:', $expenseCache.length);
|
||||
// Should be 0 or freshly fetched
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Selective Clearing
|
||||
|
||||
### Clear Only Specific Model Cache
|
||||
```javascript
|
||||
// Clear just expense cache
|
||||
localStorage.removeItem('expenseCache');
|
||||
|
||||
// Refresh that cache
|
||||
expenseCache.refresh();
|
||||
```
|
||||
|
||||
### Clear Only Metadata (Keep Records)
|
||||
```javascript
|
||||
// Get current cache data
|
||||
const cacheData = JSON.parse(localStorage.getItem('expenseCache'));
|
||||
|
||||
// Reset only metadata
|
||||
cacheData.lastSyncTime = 0;
|
||||
cacheData.lastRecordId = 0;
|
||||
|
||||
// Save back
|
||||
localStorage.setItem('expenseCache', JSON.stringify(cacheData));
|
||||
|
||||
// Force sync
|
||||
expenseCache.refresh();
|
||||
```
|
||||
|
||||
### Clear Only Old Records
|
||||
```javascript
|
||||
// Keep only recent records (last 30 days)
|
||||
const thirtyDaysAgo = new Date();
|
||||
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
|
||||
|
||||
expenseCache.records.update(records =>
|
||||
records.filter(r => new Date(r.x_studio_date) > thirtyDaysAgo)
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Framework-Specific Clearing
|
||||
|
||||
### SvelteKit
|
||||
```javascript
|
||||
// Clear and reset store
|
||||
import { expenseCache } from '$lib/stores/expenseCache';
|
||||
|
||||
expenseCache.clearCache();
|
||||
expenseCache.load(); // Reload fresh data
|
||||
```
|
||||
|
||||
### React
|
||||
```javascript
|
||||
// Using Context
|
||||
import { useExpense } from './contexts/ExpenseContext';
|
||||
|
||||
function ClearCacheButton() {
|
||||
const { clearCache, refresh } = useExpense();
|
||||
|
||||
return (
|
||||
<button onClick={() => {
|
||||
clearCache();
|
||||
refresh();
|
||||
}}>
|
||||
Clear Cache
|
||||
</button>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Vue
|
||||
```javascript
|
||||
// Using Pinia store
|
||||
import { useExpenseStore } from '@/stores/expenseStore';
|
||||
|
||||
const expenseStore = useExpenseStore();
|
||||
|
||||
function clearAndRefresh() {
|
||||
expenseStore.clearCache();
|
||||
expenseStore.load();
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Advanced Cache Management
|
||||
|
||||
### Schedule Automatic Cache Clear
|
||||
```javascript
|
||||
// Clear cache older than 7 days
|
||||
const CACHE_MAX_AGE = 7 * 24 * 60 * 60 * 1000; // 7 days
|
||||
|
||||
function checkCacheAge() {
|
||||
const cacheData = JSON.parse(localStorage.getItem('expenseCache'));
|
||||
|
||||
if (cacheData && cacheData.lastSyncTime) {
|
||||
const age = Date.now() - cacheData.lastSyncTime;
|
||||
|
||||
if (age > CACHE_MAX_AGE) {
|
||||
console.log('Cache too old, clearing...');
|
||||
expenseCache.clearCache();
|
||||
expenseCache.refresh();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check on app load
|
||||
checkCacheAge();
|
||||
```
|
||||
|
||||
### Clear on Version Change
|
||||
```javascript
|
||||
// In cache store
|
||||
const CACHE_VERSION = 2; // Increment when schema changes
|
||||
|
||||
function checkCacheVersion() {
|
||||
const cacheData = JSON.parse(localStorage.getItem('expenseCache'));
|
||||
|
||||
if (!cacheData || cacheData.version !== CACHE_VERSION) {
|
||||
console.log('Cache version mismatch, clearing...');
|
||||
clearCache();
|
||||
|
||||
// Set new version
|
||||
localStorage.setItem('expenseCache', JSON.stringify({
|
||||
version: CACHE_VERSION,
|
||||
lastSyncTime: 0,
|
||||
lastRecordId: 0
|
||||
}));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Clear Based on Storage Quota
|
||||
```javascript
|
||||
// Check storage usage
|
||||
if (navigator.storage && navigator.storage.estimate) {
|
||||
navigator.storage.estimate().then(estimate => {
|
||||
const percentUsed = (estimate.usage / estimate.quota) * 100;
|
||||
|
||||
console.log(`Storage: ${percentUsed.toFixed(2)}% used`);
|
||||
|
||||
if (percentUsed > 90) {
|
||||
console.warn('Storage almost full, clearing old cache...');
|
||||
clearOldestRecords();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function clearOldestRecords() {
|
||||
// Keep only most recent 100 records
|
||||
expenseCache.records.update(records =>
|
||||
records
|
||||
.sort((a, b) => b.id - a.id)
|
||||
.slice(0, 100)
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Cache Verification
|
||||
|
||||
### After Clearing, Verify:
|
||||
|
||||
#### 1. Storage is Empty
|
||||
```javascript
|
||||
console.log('localStorage keys:', Object.keys(localStorage));
|
||||
// Should be [] or minimal
|
||||
|
||||
console.log('localStorage size:', localStorage.length);
|
||||
// Should be 0 or very small
|
||||
```
|
||||
|
||||
#### 2. IndexedDB is Clear
|
||||
```
|
||||
DevTools → Application → IndexedDB
|
||||
- Check if database exists
|
||||
- Check if tables are empty
|
||||
```
|
||||
|
||||
#### 3. Fresh Data Loads
|
||||
```javascript
|
||||
// Refresh and watch console
|
||||
location.reload();
|
||||
|
||||
// Should see:
|
||||
// "Cache miss, fetching from Odoo..."
|
||||
// "Fetched X records"
|
||||
```
|
||||
|
||||
#### 4. Functionality Works
|
||||
```
|
||||
□ Data loads correctly
|
||||
□ CRUD operations work
|
||||
□ Sync happens
|
||||
□ Offline mode works after re-caching
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting Clear Issues
|
||||
|
||||
### Issue: Cache Won't Clear
|
||||
|
||||
**Solution 1: Force with DevTools**
|
||||
```
|
||||
1. Open DevTools (F12)
|
||||
2. Application tab
|
||||
3. Clear storage section
|
||||
4. Check all boxes
|
||||
5. Click "Clear site data"
|
||||
```
|
||||
|
||||
**Solution 2: Use Private/Incognito**
|
||||
```
|
||||
Open app in private browsing mode
|
||||
- Fresh session, no cache
|
||||
- Test functionality
|
||||
```
|
||||
|
||||
**Solution 3: Different Browser**
|
||||
```
|
||||
Test in different browser
|
||||
- Rules out browser-specific issues
|
||||
```
|
||||
|
||||
### Issue: Data Reappears After Clear
|
||||
|
||||
**Solution: Check Multiple Sources**
|
||||
```javascript
|
||||
// Clear all possible locations
|
||||
localStorage.clear();
|
||||
sessionStorage.clear();
|
||||
indexedDB.deleteDatabase('odoo-pwa-db');
|
||||
|
||||
// Unregister service workers
|
||||
navigator.serviceWorker.getRegistrations()
|
||||
.then(regs => regs.forEach(reg => reg.unregister()));
|
||||
|
||||
// Clear all caches
|
||||
caches.keys()
|
||||
.then(keys => Promise.all(keys.map(k => caches.delete(k))));
|
||||
```
|
||||
|
||||
### Issue: App Breaks After Clear
|
||||
|
||||
**Solution: Ensure Graceful Degradation**
|
||||
```javascript
|
||||
// In cache store, handle missing cache
|
||||
function loadFromCache() {
|
||||
try {
|
||||
const cached = localStorage.getItem('expenseCache');
|
||||
|
||||
if (!cached) {
|
||||
console.log('No cache, will fetch from Odoo');
|
||||
return { records: [], lastRecordId: 0, lastSyncTime: 0 };
|
||||
}
|
||||
|
||||
return JSON.parse(cached);
|
||||
} catch (error) {
|
||||
console.error('Error loading cache:', error);
|
||||
return { records: [], lastRecordId: 0, lastSyncTime: 0 };
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## User-Facing Clear Options
|
||||
|
||||
### Settings Page Example
|
||||
```javascript
|
||||
<script>
|
||||
import { expenseCache, taskCache } from '$lib/stores';
|
||||
|
||||
async function clearAll() {
|
||||
if (confirm('Clear all cached data? This cannot be undone.')) {
|
||||
expenseCache.clearCache();
|
||||
taskCache.clearCache();
|
||||
|
||||
alert('Cache cleared! Refreshing...');
|
||||
location.reload();
|
||||
}
|
||||
}
|
||||
|
||||
async function clearExpenses() {
|
||||
if (confirm('Clear expense cache?')) {
|
||||
expenseCache.clearCache();
|
||||
await expenseCache.refresh();
|
||||
alert('Expense cache cleared!');
|
||||
}
|
||||
}
|
||||
|
||||
function getCacheInfo() {
|
||||
const size = new Blob(Object.values(localStorage)).size;
|
||||
const sizeKB = (size / 1024).toFixed(2);
|
||||
return { count: localStorage.length, sizeKB };
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="settings">
|
||||
<h2>Cache Management</h2>
|
||||
|
||||
<div class="cache-info">
|
||||
<p>Items: {getCacheInfo().count}</p>
|
||||
<p>Size: {getCacheInfo().sizeKB} KB</p>
|
||||
<p>Last sync: {new Date($expenseCache.lastSync).toLocaleString()}</p>
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<button on:click={clearExpenses}>Clear Expense Cache</button>
|
||||
<button on:click={clearAll} class="danger">Clear All Cache</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.danger {
|
||||
background: red;
|
||||
color: white;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Cache Clear Checklist ✅
|
||||
|
||||
Before clearing:
|
||||
```
|
||||
□ Save any unsaved work
|
||||
□ Note current state
|
||||
□ Ensure internet connection
|
||||
□ Close duplicate tabs
|
||||
```
|
||||
|
||||
After clearing:
|
||||
```
|
||||
□ localStorage is empty
|
||||
□ IndexedDB is cleared
|
||||
□ Service worker cache cleared
|
||||
□ Fresh data loaded
|
||||
□ Functionality tested
|
||||
□ Sync works correctly
|
||||
□ Offline mode re-enabled (after cache rebuilt)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Clear Strategically
|
||||
- Don't clear unnecessarily
|
||||
- Clear only what's needed
|
||||
- Keep user data when possible
|
||||
|
||||
### 2. Warn Users
|
||||
```javascript
|
||||
function clearCache() {
|
||||
const message = `
|
||||
This will clear all cached data.
|
||||
You'll need to re-download everything from Odoo.
|
||||
Continue?
|
||||
`;
|
||||
|
||||
if (confirm(message)) {
|
||||
// Proceed
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Provide Progress
|
||||
```javascript
|
||||
async function clearAndRefresh() {
|
||||
alert('Clearing cache...');
|
||||
|
||||
localStorage.clear();
|
||||
|
||||
alert('Fetching fresh data...');
|
||||
|
||||
await expenseCache.refresh();
|
||||
|
||||
alert('Done! Cache rebuilt.');
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Log for Debugging
|
||||
```javascript
|
||||
function clearCache() {
|
||||
console.log('Before clear:', {
|
||||
localStorage: localStorage.length,
|
||||
records: $expenseCache.length
|
||||
});
|
||||
|
||||
localStorage.clear();
|
||||
expenseCache.clearCache();
|
||||
|
||||
console.log('After clear:', {
|
||||
localStorage: localStorage.length,
|
||||
records: $expenseCache.length
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Test Regularly
|
||||
- Test cache clear functionality
|
||||
- Ensure app works after clear
|
||||
- Verify data re-downloads
|
||||
- Check offline mode recovers
|
||||
|
||||
---
|
||||
|
||||
## Example prompts to use this command:
|
||||
- `/clear-cache` - Clear all cached data
|
||||
- User: "Clear my cache"
|
||||
- User: "Data looks wrong, clear everything"
|
||||
- User: "Reset the app"
|
||||
|
||||
## Related Commands:
|
||||
- `/fix-sync` - If sync issues persist
|
||||
- `/test-connection` - Test after clearing
|
||||
- `/troubleshoot` - For other issues
|
||||
- `/help` - Full documentation
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference
|
||||
|
||||
### Clear Everything (Nuclear Option)
|
||||
```javascript
|
||||
// Copy-paste into console
|
||||
localStorage.clear();
|
||||
sessionStorage.clear();
|
||||
indexedDB.databases().then(dbs =>
|
||||
dbs.forEach(db => indexedDB.deleteDatabase(db.name))
|
||||
);
|
||||
navigator.serviceWorker.getRegistrations().then(regs =>
|
||||
regs.forEach(reg => reg.unregister())
|
||||
);
|
||||
caches.keys().then(keys =>
|
||||
keys.forEach(key => caches.delete(key))
|
||||
);
|
||||
location.reload(true);
|
||||
```
|
||||
|
||||
### Clear Specific Model
|
||||
```javascript
|
||||
localStorage.removeItem('expenseCache');
|
||||
expenseCache.refresh();
|
||||
```
|
||||
|
||||
### Reset Metadata Only
|
||||
```javascript
|
||||
const cache = JSON.parse(localStorage.getItem('expenseCache'));
|
||||
cache.lastSyncTime = 0;
|
||||
cache.lastRecordId = 0;
|
||||
localStorage.setItem('expenseCache', JSON.stringify(cache));
|
||||
expenseCache.refresh();
|
||||
```
|
||||
89
commands/create-cache-store.md
Normal file
89
commands/create-cache-store.md
Normal file
@@ -0,0 +1,89 @@
|
||||
Create a smart cache store for an Odoo model with offline-first capabilities.
|
||||
|
||||
## What this command does:
|
||||
- Creates a framework-specific cache store (Svelte store, React Context, or Vue Pinia)
|
||||
- Implements dual storage (localStorage + IndexedDB)
|
||||
- Adds smart caching with stale detection (5-minute validity)
|
||||
- Implements background sync (3-minute intervals)
|
||||
- Includes incremental fetching (only new records)
|
||||
- Adds partner name resolution and caching
|
||||
- Implements optimistic updates
|
||||
|
||||
## Required Information:
|
||||
Before starting, gather:
|
||||
1. **Current working directory** - Must be inside an Odoo PWA project
|
||||
2. **Framework** - Detect from project files (SvelteKit/React/Vue)
|
||||
3. **Model name** (without `x_` prefix, e.g., "expense", "task")
|
||||
4. **Model display name** (human-readable, e.g., "Expense", "Task")
|
||||
5. **Fields to fetch** - Array of Odoo fields (e.g., ["x_studio_name", "x_studio_amount"])
|
||||
|
||||
## Store Features:
|
||||
The generated cache store will include:
|
||||
- `records` - Reactive array of cached records
|
||||
- `isLoading` - Loading state indicator
|
||||
- `error` - Error state
|
||||
- `lastSync` - Timestamp of last successful sync
|
||||
- `load()` - Load from cache and trigger background sync
|
||||
- `create(data)` - Create new record with optimistic update
|
||||
- `update(id, data)` - Update record with optimistic update
|
||||
- `delete(id)` - Delete record with optimistic update
|
||||
- `refresh()` - Force refresh from Odoo
|
||||
- `clearCache()` - Clear all cached data
|
||||
|
||||
## Storage Strategy:
|
||||
1. **localStorage**: Stores metadata (lastSyncTime, lastRecordId, version)
|
||||
2. **IndexedDB**: Stores master data (all records)
|
||||
3. **Stale detection**: Cache considered stale after 5 minutes
|
||||
4. **Background sync**: Automatic sync every 3 minutes
|
||||
5. **Incremental fetch**: Only fetches records with `id > lastRecordId`
|
||||
|
||||
## Steps:
|
||||
1. Verify the current directory is an Odoo PWA project
|
||||
2. Detect the framework from project structure
|
||||
3. Ask the user for model details and fields
|
||||
4. Create cache store file in appropriate location:
|
||||
- SvelteKit: `src/lib/stores/{model}Cache.js`
|
||||
- React: `src/contexts/{Model}Context.jsx`
|
||||
- Vue: `src/stores/{model}Store.js`
|
||||
5. Import and register the store in the appropriate location
|
||||
6. Provide usage examples and documentation
|
||||
|
||||
## Example prompts to use this command:
|
||||
- `/create-cache-store` - Interactive mode
|
||||
- User: "Create a cache store for the task model"
|
||||
- User: "Add caching for product catalog"
|
||||
|
||||
## After creation:
|
||||
Remind the user to:
|
||||
1. Import the store in components that need it
|
||||
2. Call the `load()` method when the component mounts
|
||||
3. Use reactive data bindings to display records
|
||||
4. Test create, update, and delete operations
|
||||
5. Verify offline functionality works correctly
|
||||
|
||||
## Usage Examples:
|
||||
|
||||
### SvelteKit
|
||||
```javascript
|
||||
import { taskCache } from '$lib/stores/taskCache';
|
||||
|
||||
// In +page.svelte
|
||||
$effect(() => {
|
||||
taskCache.load();
|
||||
});
|
||||
```
|
||||
|
||||
### React
|
||||
```javascript
|
||||
import { useTask } from './contexts/TaskContext';
|
||||
|
||||
const { records, create, update } = useTask();
|
||||
```
|
||||
|
||||
### Vue
|
||||
```javascript
|
||||
import { useTaskStore } from '@/stores/taskStore';
|
||||
|
||||
const taskStore = useTaskStore();
|
||||
taskStore.load();
|
||||
```
|
||||
182
commands/deploy-github.md
Normal file
182
commands/deploy-github.md
Normal file
@@ -0,0 +1,182 @@
|
||||
Deploy your Odoo PWA to GitHub Pages with GitHub Actions for continuous deployment.
|
||||
|
||||
## What this command does:
|
||||
- Sets up GitHub Actions workflow for automated deployment
|
||||
- Configures GitHub Pages in repository settings
|
||||
- Sets up repository secrets for environment variables
|
||||
- Deploys the application to GitHub Pages
|
||||
- Provides custom domain setup instructions
|
||||
|
||||
## Prerequisites:
|
||||
Before deploying, verify:
|
||||
1. ✅ Project builds successfully locally (`npm run build`)
|
||||
2. ✅ Git repository exists and is pushed to GitHub
|
||||
3. ✅ User has admin access to the repository
|
||||
4. ✅ GitHub Pages is enabled in repository settings
|
||||
5. ✅ Base URL configuration is correct for GitHub Pages
|
||||
|
||||
## Important: GitHub Pages Limitations
|
||||
⚠️ Note: GitHub Pages is static hosting only. Server-side API routes won't work.
|
||||
|
||||
**Recommendation**: For full Odoo PWA functionality with server-side API proxy:
|
||||
- Use Vercel, Cloudflare Pages, or Netlify instead
|
||||
- Or deploy API routes separately (e.g., Vercel Serverless Functions)
|
||||
|
||||
## If Continuing with GitHub Pages:
|
||||
You'll need to modify the Odoo client to use CORS-enabled direct Odoo API calls or deploy API routes separately.
|
||||
|
||||
## Steps:
|
||||
|
||||
### 1. Configure GitHub Repository
|
||||
```bash
|
||||
# Ensure you're on main branch
|
||||
git checkout main
|
||||
git pull origin main
|
||||
```
|
||||
|
||||
### 2. Set Up GitHub Actions Workflow
|
||||
Check if `.github/workflows/deploy.yml` exists:
|
||||
- If yes: Review and update if needed
|
||||
- If no: Create the workflow file
|
||||
|
||||
### 3. Configure Repository Secrets
|
||||
Go to: Repository → Settings → Secrets and variables → Actions
|
||||
|
||||
Add these secrets:
|
||||
```
|
||||
ODOO_API_KEY=your_production_api_key
|
||||
ODOO_USERNAME=your.email@company.com
|
||||
VITE_ODOO_URL=https://yourcompany.odoo.com
|
||||
VITE_ODOO_DB=yourcompany-main
|
||||
VITE_MODEL_NAME=x_expense
|
||||
VITE_MODEL_DISPLAY_NAME=Expense
|
||||
```
|
||||
|
||||
### 4. Enable GitHub Pages
|
||||
Repository → Settings → Pages:
|
||||
- Source: Deploy from a branch
|
||||
- Branch: `gh-pages` (will be created by Actions)
|
||||
- Folder: `/ (root)`
|
||||
|
||||
### 5. Update Base Path
|
||||
For framework-specific configuration:
|
||||
|
||||
**SvelteKit** (`svelte.config.js`):
|
||||
```javascript
|
||||
paths: {
|
||||
base: process.env.NODE_ENV === 'production' ? '/your-repo-name' : ''
|
||||
}
|
||||
```
|
||||
|
||||
**React/Vue** (`vite.config.js`):
|
||||
```javascript
|
||||
base: process.env.NODE_ENV === 'production' ? '/your-repo-name/' : '/'
|
||||
```
|
||||
|
||||
### 6. Commit and Push
|
||||
```bash
|
||||
git add .
|
||||
git commit -m "Configure GitHub Pages deployment"
|
||||
git push origin main
|
||||
```
|
||||
|
||||
### 7. Monitor Deployment
|
||||
- Go to Actions tab in GitHub
|
||||
- Watch the deployment workflow
|
||||
- Check for any errors
|
||||
|
||||
## GitHub Actions Workflow
|
||||
The workflow should:
|
||||
1. Trigger on push to `main` branch
|
||||
2. Install dependencies
|
||||
3. Build the project with environment variables
|
||||
4. Deploy to `gh-pages` branch
|
||||
5. GitHub Pages automatically serves from `gh-pages`
|
||||
|
||||
## Example Workflow (.github/workflows/deploy.yml):
|
||||
```yaml
|
||||
name: Deploy to GitHub Pages
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
- run: npm install
|
||||
- run: npm run build
|
||||
env:
|
||||
VITE_ODOO_URL: ${{ secrets.VITE_ODOO_URL }}
|
||||
VITE_ODOO_DB: ${{ secrets.VITE_ODOO_DB }}
|
||||
VITE_MODEL_NAME: ${{ secrets.VITE_MODEL_NAME }}
|
||||
VITE_MODEL_DISPLAY_NAME: ${{ secrets.VITE_MODEL_DISPLAY_NAME }}
|
||||
- uses: peaceiris/actions-gh-pages@v3
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
publish_dir: ./build
|
||||
```
|
||||
|
||||
## Custom Domain Setup (Optional):
|
||||
1. Add `CNAME` file to `static` folder with your domain
|
||||
2. Configure DNS records:
|
||||
- A record: Points to GitHub Pages IPs
|
||||
- Or CNAME: Points to `username.github.io`
|
||||
3. Enable HTTPS in repository settings (automatic)
|
||||
|
||||
## Post-Deployment Checks:
|
||||
After deployment, verify:
|
||||
1. ✅ Application loads at `https://username.github.io/repo-name`
|
||||
2. ✅ All assets load correctly (check browser console)
|
||||
3. ✅ Base path is correct for all routes
|
||||
4. ✅ Odoo connection works (may need CORS configuration)
|
||||
5. ✅ PWA installs correctly
|
||||
|
||||
## Example prompts to use this command:
|
||||
- `/deploy-github` - Interactive GitHub Pages deployment
|
||||
- User: "Deploy to GitHub Pages"
|
||||
- User: "Set up GitHub Actions for my PWA"
|
||||
|
||||
## Troubleshooting:
|
||||
|
||||
### Build Fails in Actions
|
||||
- Check Actions logs for specific error
|
||||
- Verify all secrets are set correctly
|
||||
- Ensure Node version is compatible
|
||||
- Test build locally first
|
||||
|
||||
### 404 on GitHub Pages
|
||||
- Verify `gh-pages` branch exists
|
||||
- Check GitHub Pages settings
|
||||
- Ensure base path is configured
|
||||
- Wait a few minutes for DNS propagation
|
||||
|
||||
### Assets Not Loading
|
||||
- Check base path configuration
|
||||
- Verify all asset paths are relative
|
||||
- Look for CORS issues in console
|
||||
- Ensure service worker paths are correct
|
||||
|
||||
### API Routes Don't Work
|
||||
- GitHub Pages doesn't support server-side code
|
||||
- Deploy API routes to Vercel/Netlify separately
|
||||
- Or modify Odoo client for direct API calls with CORS
|
||||
|
||||
## After Deployment:
|
||||
Provide the user with:
|
||||
1. GitHub Pages URL
|
||||
2. Link to Actions tab for monitoring
|
||||
3. Instructions for custom domain setup
|
||||
4. Reminder about server-side limitations
|
||||
5. Alternative hosting recommendations if needed
|
||||
144
commands/deploy-vercel.md
Normal file
144
commands/deploy-vercel.md
Normal file
@@ -0,0 +1,144 @@
|
||||
Deploy your Odoo PWA to Vercel with proper environment variable configuration.
|
||||
|
||||
## What this command does:
|
||||
- Prepares the project for Vercel deployment
|
||||
- Guides through Vercel CLI setup or web deployment
|
||||
- Configures environment variables securely
|
||||
- Sets up continuous deployment from Git
|
||||
- Provides post-deployment verification steps
|
||||
|
||||
## Prerequisites:
|
||||
Before deploying, verify:
|
||||
1. ✅ Project builds successfully locally (`npm run build`)
|
||||
2. ✅ All tests pass
|
||||
3. ✅ `.env` file is configured and working
|
||||
4. ✅ Git repository is initialized and pushed to GitHub/GitLab/Bitbucket
|
||||
5. ✅ Vercel account exists (or guide user to create one)
|
||||
|
||||
## Deployment Options:
|
||||
|
||||
### Option 1: Vercel CLI (Recommended for first deployment)
|
||||
```bash
|
||||
npm install -g vercel
|
||||
vercel login
|
||||
vercel
|
||||
```
|
||||
|
||||
### Option 2: Vercel Dashboard (Recommended for Git integration)
|
||||
1. Go to https://vercel.com/new
|
||||
2. Import your Git repository
|
||||
3. Configure project settings
|
||||
4. Add environment variables
|
||||
5. Deploy
|
||||
|
||||
## Environment Variables to Set in Vercel:
|
||||
Required for production:
|
||||
```
|
||||
VITE_ODOO_URL=https://yourcompany.odoo.com
|
||||
VITE_ODOO_DB=yourcompany-main
|
||||
ODOO_API_KEY=your_production_api_key
|
||||
ODOO_USERNAME=your.email@company.com
|
||||
VITE_MODEL_NAME=x_expense
|
||||
VITE_MODEL_DISPLAY_NAME=Expense
|
||||
```
|
||||
|
||||
## Steps:
|
||||
1. Verify project builds successfully
|
||||
2. Check if Vercel is already configured (look for `vercel.json`)
|
||||
3. Ask user which deployment option they prefer
|
||||
4. Guide through the chosen deployment method
|
||||
5. Help set up environment variables in Vercel dashboard
|
||||
6. Initiate deployment
|
||||
7. Wait for build to complete
|
||||
8. Test the deployed application
|
||||
9. Set up custom domain (if requested)
|
||||
|
||||
## Framework-Specific Configuration:
|
||||
|
||||
### SvelteKit
|
||||
Verify `vercel.json` contains:
|
||||
```json
|
||||
{
|
||||
"buildCommand": "npm run build",
|
||||
"outputDirectory": "build",
|
||||
"framework": "sveltekit"
|
||||
}
|
||||
```
|
||||
|
||||
### React
|
||||
Verify build settings:
|
||||
```json
|
||||
{
|
||||
"buildCommand": "npm run build",
|
||||
"outputDirectory": "dist",
|
||||
"framework": "vite"
|
||||
}
|
||||
```
|
||||
|
||||
### Vue
|
||||
Verify build settings:
|
||||
```json
|
||||
{
|
||||
"buildCommand": "npm run build",
|
||||
"outputDirectory": "dist",
|
||||
"framework": "vite"
|
||||
}
|
||||
```
|
||||
|
||||
## Post-Deployment Checks:
|
||||
After deployment, verify:
|
||||
1. ✅ Application loads correctly
|
||||
2. ✅ Odoo connection works (check browser console)
|
||||
3. ✅ Data syncs from Odoo
|
||||
4. ✅ CRUD operations work
|
||||
5. ✅ Offline functionality works
|
||||
6. ✅ PWA can be installed
|
||||
7. ✅ Service worker is active
|
||||
|
||||
## Example prompts to use this command:
|
||||
- `/deploy-vercel` - Interactive deployment wizard
|
||||
- User: "Deploy my PWA to Vercel"
|
||||
- User: "Help me set up Vercel deployment"
|
||||
|
||||
## Continuous Deployment:
|
||||
Once Git integration is set up:
|
||||
- Every push to `main` branch triggers automatic deployment
|
||||
- Preview deployments for pull requests
|
||||
- Automatic rollback on build failures
|
||||
- Environment variables persist across deployments
|
||||
|
||||
## Custom Domain Setup:
|
||||
If user wants a custom domain:
|
||||
1. Go to Vercel Dashboard → Project → Settings → Domains
|
||||
2. Add custom domain
|
||||
3. Configure DNS records as shown
|
||||
4. Wait for SSL certificate provisioning (automatic)
|
||||
5. Test HTTPS access
|
||||
|
||||
## Troubleshooting:
|
||||
|
||||
### Build Fails
|
||||
- Check build logs in Vercel dashboard
|
||||
- Verify all dependencies are in package.json
|
||||
- Ensure Node version is compatible
|
||||
- Check for environment-specific code
|
||||
|
||||
### Environment Variables Not Working
|
||||
- Verify variables are set in Vercel dashboard
|
||||
- Check variable names match exactly (case-sensitive)
|
||||
- Ensure variables starting with `VITE_` for client-side access
|
||||
- Redeploy after adding new variables
|
||||
|
||||
### API Routes Not Working
|
||||
- Verify serverless functions are in correct directory
|
||||
- Check function size limits (< 50MB)
|
||||
- Review function logs in Vercel dashboard
|
||||
- Ensure API routes use correct paths
|
||||
|
||||
## After Deployment:
|
||||
Provide the user with:
|
||||
1. Deployed URL
|
||||
2. Vercel dashboard link
|
||||
3. Instructions for adding custom domain
|
||||
4. Tips for monitoring performance
|
||||
5. Reminder to update API keys for production
|
||||
467
commands/examples.md
Normal file
467
commands/examples.md
Normal file
@@ -0,0 +1,467 @@
|
||||
Real-world usage examples and scenarios for the Odoo PWA Generator plugin.
|
||||
|
||||
## What this command does:
|
||||
- Provides practical, real-world examples of using the plugin
|
||||
- Shows complete workflows from start to finish
|
||||
- Demonstrates different use cases and scenarios
|
||||
- Includes code samples and best practices
|
||||
- Helps users understand the plugin's capabilities
|
||||
|
||||
## Example 1: Expense Tracking App 💰
|
||||
|
||||
### Business Need:
|
||||
Create a mobile-friendly app for employees to track expenses on-the-go, even without internet connection. Sync with Odoo for approval and reimbursement.
|
||||
|
||||
### Odoo Model Setup:
|
||||
In Odoo Studio, create model `x_expense` with fields:
|
||||
- `x_studio_description` (Char) - Expense description
|
||||
- `x_studio_amount` (Float) - Amount spent
|
||||
- `x_studio_date` (Date) - When expense occurred
|
||||
- `x_studio_category` (Selection) - Meal, Travel, Hotel, Other
|
||||
- `x_studio_receipt` (Binary) - Photo of receipt
|
||||
- `x_studio_employee` (Many2one to res.partner) - Who spent it
|
||||
- `x_studio_status` (Selection) - Draft, Submitted, Approved, Paid
|
||||
|
||||
### Implementation Steps:
|
||||
```
|
||||
1. /new-svelte-pwa
|
||||
- Project name: expense-tracker
|
||||
- Model: expense
|
||||
- Display name: Expense
|
||||
- Deployment: vercel
|
||||
|
||||
2. cd expense-tracker
|
||||
|
||||
3. /init-project
|
||||
- Install dependencies
|
||||
- Configure Odoo credentials
|
||||
- Test connection
|
||||
|
||||
4. Customize the UI:
|
||||
- Add category filter
|
||||
- Display total amount
|
||||
- Add receipt upload
|
||||
- Status badge colors
|
||||
|
||||
5. /deploy-vercel
|
||||
- Deploy to production
|
||||
- Share with team
|
||||
```
|
||||
|
||||
### Key Features:
|
||||
- ✅ Record expenses offline
|
||||
- ✅ Take photos of receipts
|
||||
- ✅ Categorize expenses
|
||||
- ✅ Auto-sync when online
|
||||
- ✅ View approval status
|
||||
- ✅ Calculate monthly totals
|
||||
|
||||
### Code Customization:
|
||||
```javascript
|
||||
// Add total calculation to cache store
|
||||
export const totalExpenses = derived(expenseCache, $cache => {
|
||||
return $cache.reduce((sum, exp) => sum + exp.x_studio_amount, 0);
|
||||
});
|
||||
|
||||
// Add category filter
|
||||
export function filterByCategory(category) {
|
||||
return $expenseCache.filter(e => e.x_studio_category === category);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Example 2: Inventory Management System 📦
|
||||
|
||||
### Business Need:
|
||||
Warehouse staff need to check stock levels, update quantities, and add new inventory items from mobile devices or tablets, even in areas with poor connectivity.
|
||||
|
||||
### Odoo Model Setup:
|
||||
Create model `x_inventory` with fields:
|
||||
- `x_studio_sku` (Char) - Product SKU
|
||||
- `x_studio_name` (Char) - Product name
|
||||
- `x_studio_quantity` (Integer) - Current stock
|
||||
- `x_studio_location` (Char) - Warehouse location
|
||||
- `x_studio_min_quantity` (Integer) - Reorder threshold
|
||||
- `x_studio_supplier` (Many2one to res.partner) - Supplier
|
||||
- `x_studio_last_restock` (Date) - Last restock date
|
||||
|
||||
### Implementation Steps:
|
||||
```
|
||||
1. /new-react-pwa
|
||||
- Project name: inventory-manager
|
||||
- Model: inventory
|
||||
- Display name: Inventory Item
|
||||
- Deployment: vercel
|
||||
|
||||
2. cd inventory-manager
|
||||
|
||||
3. /init-project
|
||||
|
||||
4. Add barcode scanning:
|
||||
- npm install @zxing/library
|
||||
- Add scanner component
|
||||
- Look up items by SKU
|
||||
|
||||
5. Add low stock alerts:
|
||||
- Filter items where quantity < min_quantity
|
||||
- Show notification badge
|
||||
- Sort by urgency
|
||||
|
||||
6. /deploy-vercel
|
||||
```
|
||||
|
||||
### Key Features:
|
||||
- ✅ Scan barcodes to find items
|
||||
- ✅ Update quantities offline
|
||||
- ✅ Low stock alerts
|
||||
- ✅ Search by name or SKU
|
||||
- ✅ Filter by location
|
||||
- ✅ Auto-sync updates
|
||||
|
||||
### Code Customization:
|
||||
```javascript
|
||||
// Add low stock filter
|
||||
const lowStockItems = useMemo(() => {
|
||||
return records.filter(item =>
|
||||
item.x_studio_quantity < item.x_studio_min_quantity
|
||||
);
|
||||
}, [records]);
|
||||
|
||||
// Add barcode lookup
|
||||
async function lookupBySKU(sku) {
|
||||
return records.find(item => item.x_studio_sku === sku);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Example 3: Field Service CRM 🔧
|
||||
|
||||
### Business Need:
|
||||
Field technicians need to view customer information, log service calls, and update job status while on-site, often without reliable internet.
|
||||
|
||||
### Odoo Model Setup:
|
||||
Create model `x_service_call` with fields:
|
||||
- `x_studio_customer` (Many2one to res.partner) - Customer
|
||||
- `x_studio_issue` (Text) - Problem description
|
||||
- `x_studio_status` (Selection) - Scheduled, In Progress, Completed
|
||||
- `x_studio_scheduled_date` (Datetime) - When to visit
|
||||
- `x_studio_technician` (Many2one to res.partner) - Assigned tech
|
||||
- `x_studio_notes` (Text) - Service notes
|
||||
- `x_studio_parts_used` (Char) - Parts replaced
|
||||
- `x_studio_duration` (Float) - Hours spent
|
||||
|
||||
### Implementation Steps:
|
||||
```
|
||||
1. /new-vue-pwa
|
||||
- Project name: field-service-crm
|
||||
- Model: service_call
|
||||
- Display name: Service Call
|
||||
- Deployment: vercel
|
||||
|
||||
2. /init-project
|
||||
|
||||
3. Add customer details:
|
||||
- Create customer cache store
|
||||
- /add-model
|
||||
- Model: customer (use res.partner)
|
||||
- Generate UI: no (use existing)
|
||||
|
||||
4. Add map integration:
|
||||
- npm install @googlemaps/js-api-loader
|
||||
- Show customer locations
|
||||
- Route planning
|
||||
|
||||
5. Add time tracking:
|
||||
- Start/stop timer
|
||||
- Calculate duration
|
||||
- Generate timesheet
|
||||
|
||||
6. /deploy-vercel
|
||||
```
|
||||
|
||||
### Key Features:
|
||||
- ✅ View today's schedule
|
||||
- ✅ Customer contact info
|
||||
- ✅ Log service notes offline
|
||||
- ✅ Track time spent
|
||||
- ✅ Update job status
|
||||
- ✅ View service history
|
||||
|
||||
---
|
||||
|
||||
## Example 4: Sales Order Entry 🛒
|
||||
|
||||
### Business Need:
|
||||
Sales reps at trade shows need to take orders offline and sync them with Odoo when they get back online.
|
||||
|
||||
### Odoo Model Setup:
|
||||
Create model `x_sales_order` with fields:
|
||||
- `x_studio_customer` (Many2one to res.partner)
|
||||
- `x_studio_date` (Date)
|
||||
- `x_studio_items` (Text/JSON) - Line items
|
||||
- `x_studio_total` (Float) - Order total
|
||||
- `x_studio_status` (Selection) - Draft, Sent, Confirmed
|
||||
- `x_studio_notes` (Text) - Special instructions
|
||||
- `x_studio_salesperson` (Many2one to res.partner)
|
||||
|
||||
### Implementation Steps:
|
||||
```
|
||||
1. /new-svelte-pwa
|
||||
- Project name: sales-order-entry
|
||||
- Model: sales_order
|
||||
- Display name: Sales Order
|
||||
|
||||
2. /add-model
|
||||
- Model: customer (res.partner)
|
||||
- Add product catalog model
|
||||
|
||||
3. Build line item editor:
|
||||
- Add/remove products
|
||||
- Quantity and price
|
||||
- Calculate totals
|
||||
|
||||
4. Add customer search:
|
||||
- Autocomplete
|
||||
- Recently viewed
|
||||
- New customer form
|
||||
|
||||
5. /deploy-vercel
|
||||
```
|
||||
|
||||
### Key Features:
|
||||
- ✅ Search products
|
||||
- ✅ Build order offline
|
||||
- ✅ Calculate totals
|
||||
- ✅ Customer lookup
|
||||
- ✅ Sync when online
|
||||
- ✅ Email confirmation
|
||||
|
||||
---
|
||||
|
||||
## Example 5: Multi-Model Project Management 📋
|
||||
|
||||
### Business Need:
|
||||
Manage projects with tasks, time entries, and documents, all syncing with Odoo.
|
||||
|
||||
### Multiple Models:
|
||||
1. `x_project` - Projects
|
||||
2. `x_task` - Tasks
|
||||
3. `x_time_entry` - Time tracking
|
||||
4. `x_document` - File attachments
|
||||
|
||||
### Implementation Steps:
|
||||
```
|
||||
1. /new-svelte-pwa
|
||||
- Project name: project-manager
|
||||
- Model: project
|
||||
- Display name: Project
|
||||
|
||||
2. /add-model
|
||||
- Model: task
|
||||
- Generate UI: yes
|
||||
|
||||
3. /add-model
|
||||
- Model: time_entry
|
||||
- Generate UI: yes
|
||||
|
||||
4. /add-model
|
||||
- Model: document
|
||||
- Generate UI: yes
|
||||
|
||||
5. Add relationships:
|
||||
- Tasks belong to projects
|
||||
- Time entries belong to tasks
|
||||
- Documents belong to projects
|
||||
|
||||
6. Build dashboard:
|
||||
- Project overview
|
||||
- Task list by status
|
||||
- Total hours tracked
|
||||
- Recent documents
|
||||
|
||||
7. /deploy-vercel
|
||||
```
|
||||
|
||||
### Key Features:
|
||||
- ✅ Multiple model types
|
||||
- ✅ Relationships between models
|
||||
- ✅ Aggregate data (total hours)
|
||||
- ✅ Complex filtering
|
||||
- ✅ Dashboard views
|
||||
|
||||
---
|
||||
|
||||
## Example 6: Custom Cache Strategy 🎯
|
||||
|
||||
### Scenario:
|
||||
Need a custom caching strategy for frequently changing data.
|
||||
|
||||
### Implementation:
|
||||
```
|
||||
1. Open existing project
|
||||
|
||||
2. /create-cache-store
|
||||
- Model: notification
|
||||
- Shorter cache timeout (1 minute)
|
||||
- More frequent sync (30 seconds)
|
||||
|
||||
3. Customize the generated store:
|
||||
```
|
||||
|
||||
```javascript
|
||||
// Shorten cache validity
|
||||
const CACHE_VALIDITY = 60 * 1000; // 1 minute
|
||||
|
||||
// More frequent sync
|
||||
const SYNC_INTERVAL = 30 * 1000; // 30 seconds
|
||||
|
||||
// Add real-time refresh
|
||||
export function enableRealTimeSync() {
|
||||
return setInterval(() => {
|
||||
refresh();
|
||||
}, SYNC_INTERVAL);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Example 7: Migrating Existing App 🔄
|
||||
|
||||
### Scenario:
|
||||
Have an existing web app, want to add Odoo integration and offline functionality.
|
||||
|
||||
### Implementation Steps:
|
||||
```
|
||||
1. Generate reference implementation:
|
||||
/new-svelte-pwa
|
||||
- Project name: reference-app
|
||||
- Model: your_model
|
||||
|
||||
2. Study generated code:
|
||||
- Review odoo.js client
|
||||
- Study cache.js pattern
|
||||
- Examine API routes
|
||||
|
||||
3. Copy patterns to existing app:
|
||||
- Copy src/lib/odoo.js
|
||||
- Copy src/routes/api/odoo/+server.js
|
||||
- Adapt cache store to your state management
|
||||
|
||||
4. Test integration:
|
||||
/test-connection
|
||||
|
||||
5. Gradually add features:
|
||||
- Start with read-only
|
||||
- Add create functionality
|
||||
- Add update/delete
|
||||
- Add offline support
|
||||
|
||||
6. /deploy-vercel
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common Customizations:
|
||||
|
||||
### 1. Add Search Functionality
|
||||
```javascript
|
||||
export function searchRecords(query) {
|
||||
return $cache.filter(record =>
|
||||
record.x_studio_name.toLowerCase().includes(query.toLowerCase())
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Add Sorting
|
||||
```javascript
|
||||
export function sortBy(field, direction = 'asc') {
|
||||
return $cache.sort((a, b) => {
|
||||
const valA = a[field];
|
||||
const valB = b[field];
|
||||
return direction === 'asc' ? valA - valB : valB - valA;
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Add Pagination
|
||||
```javascript
|
||||
export function paginate(page, pageSize) {
|
||||
const start = (page - 1) * pageSize;
|
||||
return $cache.slice(start, start + pageSize);
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Add Export to CSV
|
||||
```javascript
|
||||
export function exportToCSV() {
|
||||
const headers = ['ID', 'Name', 'Amount', 'Date'];
|
||||
const rows = $cache.map(r => [
|
||||
r.id,
|
||||
r.x_studio_name,
|
||||
r.x_studio_amount,
|
||||
r.x_studio_date
|
||||
]);
|
||||
// Convert to CSV and download
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Add Bulk Operations
|
||||
```javascript
|
||||
export async function bulkUpdate(ids, fields) {
|
||||
const promises = ids.map(id => update(id, fields));
|
||||
return Promise.all(promises);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Tips for Success:
|
||||
|
||||
### Start Simple
|
||||
1. Generate basic PWA first
|
||||
2. Test with sample data
|
||||
3. Add features incrementally
|
||||
4. Deploy early and often
|
||||
|
||||
### Plan Your Models
|
||||
1. Design Odoo model schema carefully
|
||||
2. Include all necessary fields
|
||||
3. Think about relationships
|
||||
4. Consider mobile UX
|
||||
|
||||
### Test Thoroughly
|
||||
1. Test offline functionality
|
||||
2. Verify sync works correctly
|
||||
3. Check error handling
|
||||
4. Test on real devices
|
||||
|
||||
### Optimize Performance
|
||||
1. Limit initial data load
|
||||
2. Use pagination for large datasets
|
||||
3. Lazy load images
|
||||
4. Minimize bundle size
|
||||
|
||||
### Deploy Confidently
|
||||
1. Test build locally
|
||||
2. Use staging environment
|
||||
3. Monitor errors
|
||||
4. Have rollback plan
|
||||
|
||||
---
|
||||
|
||||
## Example prompts to use this command:
|
||||
- `/examples` - Show all examples
|
||||
- User: "Show me real-world examples"
|
||||
- User: "How do I build an expense tracker?"
|
||||
- User: "Give me ideas for using this plugin"
|
||||
|
||||
## Next Steps:
|
||||
After reviewing these examples:
|
||||
1. Choose a use case similar to your needs
|
||||
2. Follow the implementation steps
|
||||
3. Customize to match your requirements
|
||||
4. Deploy and iterate
|
||||
|
||||
Need help? Run `/help` for more information!
|
||||
596
commands/fix-sync.md
Normal file
596
commands/fix-sync.md
Normal file
@@ -0,0 +1,596 @@
|
||||
Diagnose and fix synchronization issues between your PWA and Odoo.
|
||||
|
||||
## What this command does:
|
||||
- Identifies sync problems
|
||||
- Tests each component of the sync system
|
||||
- Provides step-by-step fixes
|
||||
- Clears problematic cached data
|
||||
- Verifies sync works correctly
|
||||
|
||||
---
|
||||
|
||||
## Quick Diagnosis 🔍
|
||||
|
||||
Run through these quick checks first:
|
||||
|
||||
### 1. Visual Inspection
|
||||
```
|
||||
□ Check browser console for errors
|
||||
□ Look at "Last synced" timestamp
|
||||
□ Try manual refresh
|
||||
□ Check network tab for failed requests
|
||||
```
|
||||
|
||||
### 2. Quick Tests
|
||||
```javascript
|
||||
// In browser console:
|
||||
|
||||
// 1. Check if cache exists
|
||||
localStorage.getItem('expenseCache');
|
||||
|
||||
// 2. Test API endpoint
|
||||
fetch('/api/odoo', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
action: 'search',
|
||||
model: 'res.partner',
|
||||
domain: [],
|
||||
fields: ['name'],
|
||||
limit: 1
|
||||
})
|
||||
}).then(r => r.json()).then(console.log);
|
||||
|
||||
// 3. Force refresh
|
||||
expenseCache.refresh();
|
||||
```
|
||||
|
||||
### 3. Automatic Diagnosis
|
||||
```bash
|
||||
/test-connection
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common Sync Issues
|
||||
|
||||
### Issue 1: Initial Load Works, But No Updates 🔄
|
||||
|
||||
**Symptoms:**
|
||||
- Data loads on first visit
|
||||
- But never updates with new Odoo data
|
||||
- "Last synced" timestamp is old
|
||||
- Background sync not happening
|
||||
|
||||
**Diagnosis:**
|
||||
```javascript
|
||||
// Check if sync timer is running
|
||||
// In cache store, look for:
|
||||
setInterval(() => syncInBackground(), 180000);
|
||||
|
||||
// Check if stale detection works
|
||||
const cacheData = JSON.parse(localStorage.getItem('expenseCache'));
|
||||
console.log('Last sync:', new Date(cacheData?.lastSyncTime));
|
||||
console.log('Is stale?', Date.now() - cacheData?.lastSyncTime > 5 * 60 * 1000);
|
||||
```
|
||||
|
||||
**Solutions:**
|
||||
|
||||
#### Solution A: Restart Background Sync
|
||||
```javascript
|
||||
// In browser console
|
||||
expenseCache.stopAutoSync();
|
||||
expenseCache.startAutoSync();
|
||||
```
|
||||
|
||||
#### Solution B: Check Cache Validity
|
||||
```javascript
|
||||
// In cache store file (e.g., expenseCache.js)
|
||||
const CACHE_VALIDITY = 5 * 60 * 1000; // 5 minutes
|
||||
|
||||
function isCacheStale() {
|
||||
const cacheData = JSON.parse(localStorage.getItem('expenseCache'));
|
||||
if (!cacheData) return true;
|
||||
|
||||
const now = Date.now();
|
||||
const age = now - cacheData.lastSyncTime;
|
||||
|
||||
console.log(`Cache age: ${Math.floor(age / 1000)}s`);
|
||||
|
||||
return age > CACHE_VALIDITY;
|
||||
}
|
||||
```
|
||||
|
||||
#### Solution C: Force Manual Sync
|
||||
```javascript
|
||||
// Add a refresh button to your UI
|
||||
<button onclick={() => expenseCache.refresh()}>
|
||||
Sync Now
|
||||
</button>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Issue 2: Incremental Sync Not Working 📈
|
||||
|
||||
**Symptoms:**
|
||||
- Always fetches all records
|
||||
- Slow sync times
|
||||
- High bandwidth usage
|
||||
- `lastRecordId` not updating
|
||||
|
||||
**Diagnosis:**
|
||||
```javascript
|
||||
// Check lastRecordId
|
||||
const cacheData = JSON.parse(localStorage.getItem('expenseCache'));
|
||||
console.log('Last record ID:', cacheData?.lastRecordId);
|
||||
|
||||
// Check if it's being updated after sync
|
||||
```
|
||||
|
||||
**Solutions:**
|
||||
|
||||
#### Solution A: Verify Domain Filter
|
||||
```javascript
|
||||
// In syncInBackground() function
|
||||
async function syncInBackground() {
|
||||
const { lastRecordId } = getCacheMetadata();
|
||||
|
||||
console.log('Fetching records with id >', lastRecordId);
|
||||
|
||||
const domain = lastRecordId > 0
|
||||
? [['id', '>', lastRecordId]]
|
||||
: [];
|
||||
|
||||
const newRecords = await odoo.searchRecords(
|
||||
MODEL_NAME,
|
||||
domain,
|
||||
fields
|
||||
);
|
||||
|
||||
console.log('Fetched:', newRecords.length, 'new records');
|
||||
|
||||
if (newRecords.length > 0) {
|
||||
// Update lastRecordId
|
||||
const maxId = Math.max(...newRecords.map(r => r.id));
|
||||
updateCacheMetadata({ lastRecordId: maxId });
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Solution B: Reset lastRecordId
|
||||
```javascript
|
||||
// If stuck, reset to fetch all
|
||||
const cacheData = JSON.parse(localStorage.getItem('expenseCache'));
|
||||
cacheData.lastRecordId = 0;
|
||||
localStorage.setItem('expenseCache', JSON.stringify(cacheData));
|
||||
|
||||
// Then refresh
|
||||
expenseCache.refresh();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Issue 3: Optimistic Updates Not Syncing ⚡
|
||||
|
||||
**Symptoms:**
|
||||
- Create/update works in UI
|
||||
- But changes don't save to Odoo
|
||||
- Records disappear on refresh
|
||||
- Temp IDs persist
|
||||
|
||||
**Diagnosis:**
|
||||
```javascript
|
||||
// Check for temp IDs
|
||||
console.log($expenseCache.filter(e => e.id.toString().startsWith('temp-')));
|
||||
|
||||
// Check browser console for API errors
|
||||
```
|
||||
|
||||
**Solutions:**
|
||||
|
||||
#### Solution A: Check API Route
|
||||
```javascript
|
||||
// Test create
|
||||
fetch('/api/odoo', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
action: 'create',
|
||||
model: 'x_expense',
|
||||
fields: {
|
||||
x_studio_description: 'Test',
|
||||
x_studio_amount: 10.00
|
||||
}
|
||||
})
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(console.log)
|
||||
.catch(console.error);
|
||||
```
|
||||
|
||||
#### Solution B: Fix Error Handling
|
||||
```javascript
|
||||
// In cache store create() function
|
||||
export async function create(data) {
|
||||
const tempId = `temp-${Date.now()}`;
|
||||
const tempRecord = { id: tempId, ...data };
|
||||
|
||||
// Add to cache
|
||||
records.update(r => [...r, tempRecord]);
|
||||
|
||||
try {
|
||||
// Create in Odoo
|
||||
const realId = await odoo.createRecord(MODEL_NAME, data);
|
||||
|
||||
// Replace temp ID
|
||||
records.update(r =>
|
||||
r.map(rec => rec.id === tempId ? { ...rec, id: realId } : rec)
|
||||
);
|
||||
|
||||
// Update cache metadata
|
||||
await refresh(); // Sync to get the complete record
|
||||
|
||||
return realId;
|
||||
} catch (error) {
|
||||
console.error('Failed to create record:', error);
|
||||
|
||||
// Rollback
|
||||
records.update(r => r.filter(rec => rec.id !== tempId));
|
||||
|
||||
// Re-throw
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Solution C: Clean Up Temp Records
|
||||
```javascript
|
||||
// Remove stuck temp records
|
||||
expenseCache.records.update(records =>
|
||||
records.filter(r => !r.id.toString().startsWith('temp-'))
|
||||
);
|
||||
|
||||
// Then refresh
|
||||
expenseCache.refresh();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Issue 4: Partner Names Not Resolving 👥
|
||||
|
||||
**Symptoms:**
|
||||
- Partner fields show IDs instead of names
|
||||
- Many2one fields display as arrays
|
||||
- "undefined" or "[object Object]" displayed
|
||||
|
||||
**Diagnosis:**
|
||||
```javascript
|
||||
// Check partner field format
|
||||
const record = $expenseCache[0];
|
||||
console.log('Employee field:', record.x_studio_employee);
|
||||
// Should be: [12, "John Doe"] or 12
|
||||
```
|
||||
|
||||
**Solutions:**
|
||||
|
||||
#### Solution A: Add Partner Resolution
|
||||
```javascript
|
||||
// In syncInBackground()
|
||||
async function syncInBackground() {
|
||||
// ... fetch records ...
|
||||
|
||||
// Resolve partner names
|
||||
const partnerIds = new Set();
|
||||
|
||||
records.forEach(record => {
|
||||
if (record.x_studio_employee) {
|
||||
const id = Array.isArray(record.x_studio_employee)
|
||||
? record.x_studio_employee[0]
|
||||
: record.x_studio_employee;
|
||||
partnerIds.add(id);
|
||||
}
|
||||
});
|
||||
|
||||
if (partnerIds.size > 0) {
|
||||
const partners = await odoo.fetchPartners(Array.from(partnerIds));
|
||||
const partnerMap = new Map(partners.map(p => [p.id, p.name]));
|
||||
|
||||
// Cache partners
|
||||
localStorage.setItem('partnerCache', JSON.stringify(
|
||||
Array.from(partnerMap.entries())
|
||||
));
|
||||
|
||||
// Update records with names
|
||||
records = records.map(record => {
|
||||
if (record.x_studio_employee) {
|
||||
const id = Array.isArray(record.x_studio_employee)
|
||||
? record.x_studio_employee[0]
|
||||
: record.x_studio_employee;
|
||||
|
||||
return {
|
||||
...record,
|
||||
x_studio_employee: [id, partnerMap.get(id) || 'Unknown']
|
||||
};
|
||||
}
|
||||
return record;
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Solution B: Display Helper Function
|
||||
```javascript
|
||||
// Helper to display partner name
|
||||
function getPartnerName(field) {
|
||||
if (!field) return 'None';
|
||||
if (Array.isArray(field)) return field[1] || `ID: ${field[0]}`;
|
||||
return `ID: ${field}`;
|
||||
}
|
||||
|
||||
// In component
|
||||
{getPartnerName(expense.x_studio_employee)}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Issue 5: Duplicate Records 📋📋
|
||||
|
||||
**Symptoms:**
|
||||
- Same record appears multiple times
|
||||
- ID conflicts
|
||||
- Sync creates duplicates
|
||||
|
||||
**Diagnosis:**
|
||||
```javascript
|
||||
// Check for duplicates
|
||||
const ids = $expenseCache.map(r => r.id);
|
||||
const duplicates = ids.filter((id, index) => ids.indexOf(id) !== index);
|
||||
console.log('Duplicate IDs:', duplicates);
|
||||
```
|
||||
|
||||
**Solutions:**
|
||||
|
||||
#### Solution A: Deduplicate Cache
|
||||
```javascript
|
||||
function deduplicateCache() {
|
||||
records.update(currentRecords => {
|
||||
const seen = new Set();
|
||||
return currentRecords.filter(record => {
|
||||
if (seen.has(record.id)) {
|
||||
return false;
|
||||
}
|
||||
seen.add(record.id);
|
||||
return true;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Run deduplication
|
||||
deduplicateCache();
|
||||
```
|
||||
|
||||
#### Solution B: Fix Sync Logic
|
||||
```javascript
|
||||
// When appending new records, check for duplicates
|
||||
async function appendToCache(newRecords) {
|
||||
records.update(currentRecords => {
|
||||
const existingIds = new Set(currentRecords.map(r => r.id));
|
||||
|
||||
// Only add records that don't exist
|
||||
const toAdd = newRecords.filter(r => !existingIds.has(r.id));
|
||||
|
||||
return [...currentRecords, ...toAdd];
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Issue 6: Offline Queue Not Syncing 📴
|
||||
|
||||
**Symptoms:**
|
||||
- Changes made offline
|
||||
- Online now, but changes not syncing
|
||||
- Stuck in "pending" state
|
||||
|
||||
**Solutions:**
|
||||
|
||||
#### Solution A: Implement Offline Queue
|
||||
```javascript
|
||||
// Store failed operations
|
||||
const offlineQueue = writable([]);
|
||||
|
||||
async function queueOperation(operation) {
|
||||
offlineQueue.update(q => [...q, operation]);
|
||||
saveOfflineQueue();
|
||||
}
|
||||
|
||||
async function processOfflineQueue() {
|
||||
const queue = get(offlineQueue);
|
||||
|
||||
for (const operation of queue) {
|
||||
try {
|
||||
if (operation.type === 'create') {
|
||||
await odoo.createRecord(operation.model, operation.data);
|
||||
} else if (operation.type === 'update') {
|
||||
await odoo.updateRecord(operation.model, operation.id, operation.data);
|
||||
} else if (operation.type === 'delete') {
|
||||
await odoo.deleteRecord(operation.model, operation.id);
|
||||
}
|
||||
|
||||
// Remove from queue
|
||||
offlineQueue.update(q => q.filter(op => op !== operation));
|
||||
} catch (error) {
|
||||
console.error('Failed to process queued operation:', error);
|
||||
// Keep in queue, will retry
|
||||
}
|
||||
}
|
||||
|
||||
saveOfflineQueue();
|
||||
}
|
||||
|
||||
// Listen for online event
|
||||
window.addEventListener('online', () => {
|
||||
console.log('Back online, processing queue...');
|
||||
processOfflineQueue();
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step-by-Step Fix Procedure 🔧
|
||||
|
||||
### Step 1: Clear Everything
|
||||
```javascript
|
||||
// Clear all caches
|
||||
localStorage.clear();
|
||||
|
||||
// Clear IndexedDB
|
||||
// DevTools → Application → IndexedDB → Delete database
|
||||
```
|
||||
|
||||
### Step 2: Test API
|
||||
```bash
|
||||
/test-connection
|
||||
```
|
||||
|
||||
### Step 3: Fresh Sync
|
||||
```javascript
|
||||
// Refresh page
|
||||
location.reload();
|
||||
|
||||
// Should fetch all data fresh
|
||||
```
|
||||
|
||||
### Step 4: Monitor Sync
|
||||
```javascript
|
||||
// Watch console for sync messages
|
||||
// Should see:
|
||||
// "Syncing x_expense..."
|
||||
// "Fetched X records"
|
||||
// "Cache updated"
|
||||
```
|
||||
|
||||
### Step 5: Test CRUD
|
||||
```javascript
|
||||
// Test create
|
||||
await expenseCache.create({ /* data */ });
|
||||
|
||||
// Test update
|
||||
await expenseCache.update(id, { /* data */ });
|
||||
|
||||
// Test delete
|
||||
await expenseCache.remove(id);
|
||||
|
||||
// Verify in Odoo
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Sync Debugging Checklist ✅
|
||||
|
||||
```
|
||||
□ Browser console shows no errors
|
||||
□ /api/odoo endpoint responds
|
||||
□ ODOO_API_KEY is valid
|
||||
□ lastSyncTime is updating
|
||||
□ lastRecordId is updating
|
||||
□ Background sync interval is running
|
||||
□ Stale detection works correctly
|
||||
□ Incremental fetch has correct domain
|
||||
□ Optimistic updates resolve temp IDs
|
||||
□ Partner names resolve correctly
|
||||
□ No duplicate records
|
||||
□ Offline queue processes when online
|
||||
□ IndexedDB is storing data
|
||||
□ localStorage has metadata
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Advanced Debugging 🐛
|
||||
|
||||
### Enable Verbose Logging
|
||||
```javascript
|
||||
// Add to cache store
|
||||
const DEBUG = true;
|
||||
|
||||
function log(...args) {
|
||||
if (DEBUG) console.log('[ExpenseCache]', ...args);
|
||||
}
|
||||
|
||||
async function syncInBackground() {
|
||||
log('Starting background sync...');
|
||||
log('lastRecordId:', getLastRecordId());
|
||||
|
||||
const records = await fetch();
|
||||
log('Fetched records:', records.length);
|
||||
|
||||
await saveToCache(records);
|
||||
log('Cache updated');
|
||||
}
|
||||
```
|
||||
|
||||
### Monitor Network
|
||||
```javascript
|
||||
// Log all API calls
|
||||
const originalFetch = window.fetch;
|
||||
window.fetch = async (...args) => {
|
||||
console.log('Fetch:', args[0]);
|
||||
const response = await originalFetch(...args);
|
||||
console.log('Response:', response.status);
|
||||
return response;
|
||||
};
|
||||
```
|
||||
|
||||
### Profile Performance
|
||||
```javascript
|
||||
// Measure sync time
|
||||
console.time('sync');
|
||||
await expenseCache.refresh();
|
||||
console.timeEnd('sync');
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Example prompts to use this command:
|
||||
- `/fix-sync` - Diagnose sync issues
|
||||
- User: "Data is not syncing"
|
||||
- User: "My changes aren't saving"
|
||||
- User: "Sync is broken"
|
||||
|
||||
## Related Commands:
|
||||
- `/test-connection` - Test Odoo connectivity
|
||||
- `/clear-cache` - Clear all cached data
|
||||
- `/troubleshoot` - General troubleshooting
|
||||
- `/help` - Full documentation
|
||||
|
||||
---
|
||||
|
||||
## Prevention Tips 🛡️
|
||||
|
||||
1. **Monitor sync health**
|
||||
- Display "Last synced" timestamp in UI
|
||||
- Show sync status indicator
|
||||
- Alert on sync failures
|
||||
|
||||
2. **Handle errors gracefully**
|
||||
- Catch and log all errors
|
||||
- Show user-friendly messages
|
||||
- Provide retry mechanisms
|
||||
|
||||
3. **Test offline scenarios**
|
||||
- Test creating records offline
|
||||
- Test going offline mid-sync
|
||||
- Test coming back online
|
||||
|
||||
4. **Keep sync simple**
|
||||
- Stick to generated patterns
|
||||
- Don't overcomplicate logic
|
||||
- Follow proven examples
|
||||
|
||||
5. **Regular maintenance**
|
||||
- Clear old data periodically
|
||||
- Update dependencies
|
||||
- Monitor performance
|
||||
322
commands/help.md
Normal file
322
commands/help.md
Normal file
@@ -0,0 +1,322 @@
|
||||
Display comprehensive help and documentation for the Odoo PWA Generator plugin.
|
||||
|
||||
## What this command does:
|
||||
- Provides overview of the plugin and its capabilities
|
||||
- Lists all available commands with descriptions
|
||||
- Explains the plugin's skills and when to use them
|
||||
- Shows common usage patterns and workflows
|
||||
- Links to detailed documentation
|
||||
|
||||
## Plugin Overview:
|
||||
The **odoo-pwa-generator** plugin helps you create offline-first Progressive Web Apps with Odoo Studio backend integration. It supports SvelteKit, React, and Vue frameworks.
|
||||
|
||||
### Key Features:
|
||||
✨ **Quick Project Generation** - Create complete PWAs in minutes
|
||||
📱 **Offline-First** - Smart caching with background sync
|
||||
🔄 **Odoo Integration** - Seamless Odoo Studio connectivity
|
||||
🚀 **Production Ready** - Pre-configured deployment setups
|
||||
⚡ **Framework Support** - SvelteKit, React, and Vue
|
||||
|
||||
### Core Capabilities:
|
||||
- Automatic CRUD operations for Odoo models
|
||||
- Smart caching with localStorage + IndexedDB
|
||||
- Incremental sync (only fetch new records)
|
||||
- Optimistic UI updates
|
||||
- Partner/relation resolution
|
||||
- PWA installability
|
||||
- Offline functionality
|
||||
|
||||
## Available Commands:
|
||||
|
||||
### 🎯 Skill Shortcuts (Project Generation)
|
||||
Create new PWA projects:
|
||||
- `/new-svelte-pwa` - Generate SvelteKit PWA
|
||||
- `/new-react-pwa` - Generate React PWA
|
||||
- `/new-vue-pwa` - Generate Vue PWA
|
||||
- `/add-model` - Add Odoo model to existing PWA
|
||||
- `/create-cache-store` - Create cache store for a model
|
||||
|
||||
### 🔧 Workflow Commands
|
||||
Development and deployment:
|
||||
- `/init-project` - Initialize new PWA project
|
||||
- `/setup-env` - Configure environment variables
|
||||
- `/test-connection` - Test Odoo API connection
|
||||
- `/deploy-vercel` - Deploy to Vercel
|
||||
- `/deploy-github` - Deploy to GitHub Pages
|
||||
|
||||
### 📚 Documentation Commands
|
||||
Help and reference:
|
||||
- `/help` - This help document (you are here!)
|
||||
- `/examples` - Real-world usage examples
|
||||
- `/architecture` - Explain PWA architecture
|
||||
- `/api-reference` - Odoo API client documentation
|
||||
- `/troubleshoot` - Common issues and solutions
|
||||
|
||||
### 🛠 Maintenance Commands
|
||||
Project management:
|
||||
- `/update-deps` - Update project dependencies
|
||||
- `/fix-sync` - Diagnose and fix sync issues
|
||||
- `/clear-cache` - Clear application caches
|
||||
- `/add-deployment` - Add new deployment target
|
||||
- `/optimize` - Run optimization checks
|
||||
|
||||
## Common Workflows:
|
||||
|
||||
### 1️⃣ Starting a New Project
|
||||
```
|
||||
1. /new-svelte-pwa (or /new-react-pwa or /new-vue-pwa)
|
||||
2. Provide project details (name, model, etc.)
|
||||
3. /init-project
|
||||
4. Start coding!
|
||||
```
|
||||
|
||||
### 2️⃣ Setting Up Environment
|
||||
```
|
||||
1. /setup-env
|
||||
2. Provide Odoo credentials
|
||||
3. /test-connection
|
||||
4. Verify everything works
|
||||
```
|
||||
|
||||
### 3️⃣ Adding More Models
|
||||
```
|
||||
1. /add-model
|
||||
2. Specify model name and details
|
||||
3. Test CRUD operations
|
||||
4. Customize UI as needed
|
||||
```
|
||||
|
||||
### 4️⃣ Deploying to Production
|
||||
```
|
||||
1. /deploy-vercel (recommended)
|
||||
OR /deploy-github
|
||||
2. Configure environment variables
|
||||
3. Test deployed application
|
||||
4. Set up custom domain (optional)
|
||||
```
|
||||
|
||||
### 5️⃣ Troubleshooting Issues
|
||||
```
|
||||
1. /test-connection - Check Odoo connectivity
|
||||
2. /troubleshoot - Find specific solutions
|
||||
3. /fix-sync - Diagnose sync problems
|
||||
4. /clear-cache - Reset if needed
|
||||
```
|
||||
|
||||
## Quick Start Guide:
|
||||
|
||||
### Step 1: Generate a New PWA
|
||||
Choose your framework and run the appropriate command:
|
||||
```
|
||||
/new-svelte-pwa
|
||||
```
|
||||
|
||||
### Step 2: Provide Project Details
|
||||
When prompted, provide:
|
||||
- **Project name**: e.g., "expense-tracker"
|
||||
- **Odoo model**: e.g., "expense" (without x_ prefix)
|
||||
- **Display name**: e.g., "Expense"
|
||||
- **Deployment target**: e.g., "vercel" (optional)
|
||||
|
||||
### Step 3: Initialize the Project
|
||||
```
|
||||
cd your-project-name
|
||||
/init-project
|
||||
```
|
||||
|
||||
### Step 4: Configure Odoo Connection
|
||||
```
|
||||
/setup-env
|
||||
```
|
||||
Provide your Odoo URL, database, API key, and username.
|
||||
|
||||
### Step 5: Test Everything
|
||||
```
|
||||
/test-connection
|
||||
```
|
||||
Verify your Odoo connection works correctly.
|
||||
|
||||
### Step 6: Start Developing
|
||||
The dev server should be running. Open your browser and start customizing!
|
||||
|
||||
### Step 7: Deploy (when ready)
|
||||
```
|
||||
/deploy-vercel
|
||||
```
|
||||
Follow the prompts to deploy to production.
|
||||
|
||||
## Plugin Skills:
|
||||
|
||||
### create-odoo-pwa
|
||||
Generates a complete PWA project from scratch.
|
||||
|
||||
**When to use**: Starting a new project
|
||||
|
||||
**What it generates**:
|
||||
- Base configuration (package.json, vite.config, etc.)
|
||||
- Odoo API client
|
||||
- Cache stores with smart syncing
|
||||
- Server-side API proxy
|
||||
- UI components (forms, lists)
|
||||
- PWA manifest and service worker
|
||||
- Deployment configurations
|
||||
- Complete documentation
|
||||
|
||||
### add-odoo-model
|
||||
Adds integration for additional Odoo models.
|
||||
|
||||
**When to use**: Adding new data models to existing project
|
||||
|
||||
**What it generates**:
|
||||
- Cache store for the new model
|
||||
- API methods in Odoo client
|
||||
- Form and list pages (optional)
|
||||
- Navigation updates
|
||||
|
||||
### create-cache-store
|
||||
Creates a standalone cache store.
|
||||
|
||||
**When to use**: Need custom caching logic
|
||||
|
||||
**What it generates**:
|
||||
- Framework-specific cache store
|
||||
- Smart caching logic
|
||||
- Background sync
|
||||
- Optimistic updates
|
||||
|
||||
## Best Practices:
|
||||
|
||||
### Security
|
||||
- ✅ Keep API keys in `.env` file (never commit)
|
||||
- ✅ Use different credentials for dev and production
|
||||
- ✅ Set environment variables in hosting platform
|
||||
- ✅ Rotate API keys periodically
|
||||
|
||||
### Development
|
||||
- ✅ Test Odoo connection before coding
|
||||
- ✅ Use version control (Git)
|
||||
- ✅ Test offline functionality regularly
|
||||
- ✅ Read generated CLAUDE.md for patterns
|
||||
|
||||
### Deployment
|
||||
- ✅ Build and test locally first
|
||||
- ✅ Use Vercel/Netlify for full functionality
|
||||
- ✅ Set up continuous deployment from Git
|
||||
- ✅ Monitor performance and errors
|
||||
|
||||
### Maintenance
|
||||
- ✅ Keep dependencies updated
|
||||
- ✅ Monitor Odoo API changes
|
||||
- ✅ Test after Odoo upgrades
|
||||
- ✅ Clear caches when schema changes
|
||||
|
||||
## Getting More Help:
|
||||
|
||||
### Example prompts to use this command:
|
||||
- `/help` - Show this help document
|
||||
- User: "How do I use the Odoo PWA plugin?"
|
||||
- User: "What commands are available?"
|
||||
|
||||
### In Generated Projects:
|
||||
- `README.md` - Getting started guide
|
||||
- `CLAUDE.md` - Architecture patterns
|
||||
- `API.md` - API client reference
|
||||
|
||||
### External Resources:
|
||||
- Odoo API Documentation: https://www.odoo.com/documentation/
|
||||
- SvelteKit: https://kit.svelte.dev/
|
||||
- React: https://react.dev/
|
||||
- Vue: https://vuejs.org/
|
||||
- PWA Guide: https://web.dev/progressive-web-apps/
|
||||
|
||||
### Troubleshooting:
|
||||
If you encounter issues:
|
||||
1. Run `/test-connection` to diagnose
|
||||
2. Check `/troubleshoot` for common solutions
|
||||
3. Review browser console for errors
|
||||
4. Verify Odoo configuration
|
||||
5. Check generated documentation
|
||||
|
||||
### Support:
|
||||
For bugs or feature requests:
|
||||
- Check existing issues
|
||||
- Review documentation thoroughly
|
||||
- Provide detailed error messages
|
||||
- Include environment details
|
||||
|
||||
## Framework-Specific Notes:
|
||||
|
||||
### SvelteKit (Recommended)
|
||||
- Uses Svelte 5 runes syntax
|
||||
- Server-side API routes work perfectly
|
||||
- Best offline functionality
|
||||
- Smallest bundle size
|
||||
- Easiest to deploy
|
||||
|
||||
### React
|
||||
- Modern React 18+ hooks
|
||||
- Context API for state management
|
||||
- Wide ecosystem support
|
||||
- Popular and familiar
|
||||
|
||||
### Vue
|
||||
- Vue 3 Composition API
|
||||
- Pinia for state management
|
||||
- Great developer experience
|
||||
- Progressive framework
|
||||
|
||||
## Architecture Highlights:
|
||||
|
||||
### Data Flow
|
||||
```
|
||||
Component → Cache Store → API Client → Server Route → Odoo
|
||||
```
|
||||
|
||||
### Caching Strategy
|
||||
1. **Load from cache** (instant, may be stale)
|
||||
2. **Check if stale** (> 5 minutes)
|
||||
3. **Background sync** (fetch new data)
|
||||
4. **Update cache** (localStorage + IndexedDB)
|
||||
5. **Reactive update** (UI updates automatically)
|
||||
|
||||
### Sync Strategy
|
||||
1. **Incremental fetch** - Only `id > lastRecordId`
|
||||
2. **Partner resolution** - Batch fetch related records
|
||||
3. **Optimistic updates** - UI updates before server
|
||||
4. **Error recovery** - Graceful offline handling
|
||||
|
||||
## Tips and Tricks:
|
||||
|
||||
💡 **Use the right command for the job**
|
||||
- Quick start? `/new-svelte-pwa`
|
||||
- Need help? `/help` or `/troubleshoot`
|
||||
- Deploy ready? `/deploy-vercel`
|
||||
|
||||
💡 **Test early and often**
|
||||
- Run `/test-connection` after setup
|
||||
- Test offline mode frequently
|
||||
- Verify sync works correctly
|
||||
|
||||
💡 **Read the generated docs**
|
||||
- CLAUDE.md has architecture details
|
||||
- API.md has client method docs
|
||||
- README.md has setup instructions
|
||||
|
||||
💡 **Customize to your needs**
|
||||
- Start with generated code
|
||||
- Modify UI components
|
||||
- Add business logic
|
||||
- Extend with new features
|
||||
|
||||
💡 **Deploy with confidence**
|
||||
- Test build locally first
|
||||
- Use environment variables properly
|
||||
- Monitor after deployment
|
||||
- Set up error tracking
|
||||
|
||||
## Summary:
|
||||
The odoo-pwa-generator plugin makes it easy to create production-ready PWAs with Odoo integration. Use the commands above to generate, develop, deploy, and maintain your applications.
|
||||
|
||||
For more details on any command, just run that command and follow the prompts!
|
||||
|
||||
Happy coding! 🚀
|
||||
202
commands/init-project.md
Normal file
202
commands/init-project.md
Normal file
@@ -0,0 +1,202 @@
|
||||
Initialize a newly generated Odoo PWA project with all necessary setup steps.
|
||||
|
||||
## What this command does:
|
||||
- Runs `npm install` to install all dependencies
|
||||
- Creates `.env` file from `.env.example`
|
||||
- Guides through environment configuration
|
||||
- Tests the Odoo connection
|
||||
- Starts the development server
|
||||
- Opens the application in browser
|
||||
- Provides next steps and documentation
|
||||
|
||||
## Prerequisites:
|
||||
- Newly generated Odoo PWA project
|
||||
- Node.js installed (v18 or higher)
|
||||
- npm or pnpm package manager
|
||||
- Internet connection for dependencies
|
||||
|
||||
## Steps:
|
||||
|
||||
### 1. Verify Project Structure
|
||||
Check that required files exist:
|
||||
- ✅ `package.json`
|
||||
- ✅ `.env.example`
|
||||
- ✅ `README.md`
|
||||
- ✅ `src/lib/odoo.js` (or equivalent)
|
||||
- ✅ Framework config file (svelte.config.js / vite.config.js)
|
||||
|
||||
### 2. Install Dependencies
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
Show progress and estimated time.
|
||||
|
||||
### 3. Environment Setup
|
||||
Copy `.env.example` to `.env`:
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
Ask the user to provide:
|
||||
1. Odoo URL
|
||||
2. Database name
|
||||
3. API key
|
||||
4. Username
|
||||
5. Model configuration
|
||||
|
||||
Update `.env` file with provided values.
|
||||
|
||||
### 4. Verify Setup
|
||||
Read `.env` and verify all required variables are set.
|
||||
|
||||
### 5. Test Odoo Connection
|
||||
Run a quick connection test:
|
||||
- Test authentication
|
||||
- Verify model access
|
||||
- Check permissions
|
||||
|
||||
If connection fails, offer to run full diagnostics (`/test-connection`).
|
||||
|
||||
### 6. Initialize Git (if not already)
|
||||
```bash
|
||||
git init
|
||||
git add .
|
||||
git commit -m "Initial commit: Odoo PWA generated"
|
||||
```
|
||||
|
||||
### 7. Start Development Server
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Framework-specific commands:
|
||||
- **SvelteKit**: `npm run dev` (default port 5173)
|
||||
- **React**: `npm run dev` (default port 5173)
|
||||
- **Vue**: `npm run dev` (default port 5173)
|
||||
|
||||
### 8. Open in Browser
|
||||
Automatically open browser to `http://localhost:5173`
|
||||
|
||||
## Example prompts to use this command:
|
||||
- `/init-project` - Complete initialization wizard
|
||||
- User: "Set up my new Odoo PWA"
|
||||
- User: "Initialize the project"
|
||||
- User: "Get my PWA running"
|
||||
|
||||
## Post-Initialization Checklist:
|
||||
After successful initialization:
|
||||
|
||||
✅ **Immediate Next Steps**:
|
||||
1. Test the application in the browser
|
||||
2. Verify data loads from Odoo
|
||||
3. Test creating a new record
|
||||
4. Test editing and deleting records
|
||||
5. Verify offline functionality (disable network)
|
||||
|
||||
✅ **Configuration**:
|
||||
1. Review and customize PWA manifest (colors, icons, name)
|
||||
2. Update application metadata
|
||||
3. Configure deployment targets
|
||||
4. Set up version control (Git)
|
||||
|
||||
✅ **Development**:
|
||||
1. Read the generated `CLAUDE.md` for architecture details
|
||||
2. Review `API.md` for Odoo client documentation
|
||||
3. Explore the codebase structure
|
||||
4. Customize UI components and styling
|
||||
|
||||
✅ **Testing**:
|
||||
1. Test all CRUD operations
|
||||
2. Verify sync functionality
|
||||
3. Test offline mode
|
||||
4. Check PWA installability
|
||||
5. Test on mobile devices
|
||||
|
||||
✅ **Deployment Preparation**:
|
||||
1. Review deployment documentation
|
||||
2. Set up hosting platform account (Vercel/Netlify/etc)
|
||||
3. Prepare production Odoo API keys
|
||||
4. Configure environment variables for production
|
||||
|
||||
## Helpful Resources:
|
||||
Provide links to:
|
||||
- Project `README.md`
|
||||
- `CLAUDE.md` (architecture guide)
|
||||
- `API.md` (API documentation)
|
||||
- Odoo documentation
|
||||
- Framework documentation (SvelteKit/React/Vue)
|
||||
- PWA best practices
|
||||
|
||||
## Development Commands:
|
||||
Remind the user of available commands:
|
||||
```bash
|
||||
npm run dev # Start development server
|
||||
npm run build # Build for production
|
||||
npm run preview # Preview production build
|
||||
npm run lint # Run linter
|
||||
npm run format # Format code
|
||||
```
|
||||
|
||||
## Troubleshooting:
|
||||
|
||||
### npm install fails
|
||||
- Check Node.js version (must be v18+)
|
||||
- Clear npm cache: `npm cache clean --force`
|
||||
- Delete `node_modules` and `package-lock.json`, try again
|
||||
- Check internet connection
|
||||
|
||||
### .env configuration issues
|
||||
- Verify all variables are set (no empty values)
|
||||
- Check for typos in variable names
|
||||
- Ensure no spaces around `=` signs
|
||||
- Verify Odoo credentials are correct
|
||||
|
||||
### Development server won't start
|
||||
- Check if port 5173 is already in use
|
||||
- Try different port: `npm run dev -- --port 3000`
|
||||
- Check for syntax errors in config files
|
||||
- Review console error messages
|
||||
|
||||
### Odoo connection fails
|
||||
- Run `/test-connection` for full diagnostics
|
||||
- Verify `.env` file is loaded correctly
|
||||
- Check Odoo server is accessible
|
||||
- Verify API key is valid
|
||||
|
||||
### Build fails
|
||||
- Check for TypeScript errors (if using TypeScript)
|
||||
- Verify all dependencies are installed
|
||||
- Check for missing environment variables
|
||||
- Review build logs for specific errors
|
||||
|
||||
## After Initialization:
|
||||
Display summary:
|
||||
```
|
||||
🎉 Odoo PWA Initialized Successfully!
|
||||
|
||||
📁 Project: [project-name]
|
||||
🚀 Framework: [SvelteKit/React/Vue]
|
||||
🔗 Dev Server: http://localhost:5173
|
||||
📝 Model: [model-name]
|
||||
|
||||
✅ Next Steps:
|
||||
1. Open http://localhost:5173 in your browser
|
||||
2. Test data sync from Odoo
|
||||
3. Read CLAUDE.md for architecture details
|
||||
4. Customize the UI to match your needs
|
||||
5. Run /deploy-vercel when ready to deploy
|
||||
|
||||
📚 Documentation:
|
||||
- README.md - Getting started guide
|
||||
- CLAUDE.md - Architecture and patterns
|
||||
- API.md - Odoo API client reference
|
||||
|
||||
💡 Helpful Commands:
|
||||
- /test-connection - Verify Odoo integration
|
||||
- /add-model - Add more Odoo models
|
||||
- /deploy-vercel - Deploy to production
|
||||
- /odoo-help - Get plugin help
|
||||
|
||||
Happy coding! 🚀
|
||||
```
|
||||
33
commands/new-react-pwa.md
Normal file
33
commands/new-react-pwa.md
Normal file
@@ -0,0 +1,33 @@
|
||||
Generate a new offline-first Progressive Web App using React framework with Odoo Studio backend integration.
|
||||
|
||||
## What this command does:
|
||||
- Invokes the `create-odoo-pwa` skill with React as the framework
|
||||
- Generates a complete PWA project with smart caching, offline support, and Odoo integration
|
||||
- Creates all necessary configuration files, components, hooks, and deployment setups
|
||||
|
||||
## Required Information:
|
||||
Before starting, gather:
|
||||
1. **Project name** (kebab-case, e.g., "expense-tracker", "inventory-manager")
|
||||
2. **Odoo model name** (without `x_` prefix, e.g., "expense", "inventory")
|
||||
3. **Model display name** (human-readable, e.g., "Expense", "Inventory Item")
|
||||
4. **Deployment target** (optional: vercel, github, cloudflare, netlify)
|
||||
|
||||
## Steps:
|
||||
1. Ask the user for the required information listed above
|
||||
2. Invoke the `create-odoo-pwa` skill with framework set to "react"
|
||||
3. Generate the complete React PWA project structure
|
||||
4. Create comprehensive documentation (README.md, CLAUDE.md, API.md)
|
||||
5. Provide next steps for setup and deployment
|
||||
|
||||
## Example prompts to use this command:
|
||||
- `/new-react-pwa` - Interactive mode, will ask for all parameters
|
||||
- User: "Create a React PWA for tracking expenses with Odoo"
|
||||
- User: "Generate an inventory management app using React and Odoo Studio"
|
||||
|
||||
## After generation:
|
||||
Remind the user to:
|
||||
1. Navigate to the project directory
|
||||
2. Copy `.env.example` to `.env` and configure Odoo credentials
|
||||
3. Run `npm install` to install dependencies
|
||||
4. Run `npm run dev` to start development server
|
||||
5. Test the Odoo connection and sync functionality
|
||||
33
commands/new-svelte-pwa.md
Normal file
33
commands/new-svelte-pwa.md
Normal file
@@ -0,0 +1,33 @@
|
||||
Generate a new offline-first Progressive Web App using SvelteKit framework with Odoo Studio backend integration.
|
||||
|
||||
## What this command does:
|
||||
- Invokes the `create-odoo-pwa` skill with SvelteKit as the framework
|
||||
- Generates a complete PWA project with smart caching, offline support, and Odoo integration
|
||||
- Creates all necessary configuration files, routes, components, and deployment setups
|
||||
|
||||
## Required Information:
|
||||
Before starting, gather:
|
||||
1. **Project name** (kebab-case, e.g., "expense-tracker", "inventory-manager")
|
||||
2. **Odoo model name** (without `x_` prefix, e.g., "expense", "inventory")
|
||||
3. **Model display name** (human-readable, e.g., "Expense", "Inventory Item")
|
||||
4. **Deployment target** (optional: vercel, github, cloudflare, netlify)
|
||||
|
||||
## Steps:
|
||||
1. Ask the user for the required information listed above
|
||||
2. Invoke the `create-odoo-pwa` skill with framework set to "sveltekit"
|
||||
3. Generate the complete SvelteKit PWA project structure
|
||||
4. Create comprehensive documentation (README.md, CLAUDE.md, API.md)
|
||||
5. Provide next steps for setup and deployment
|
||||
|
||||
## Example prompts to use this command:
|
||||
- `/new-svelte-pwa` - Interactive mode, will ask for all parameters
|
||||
- User: "Create a SvelteKit PWA for tracking expenses with Odoo"
|
||||
- User: "Generate an inventory management app using SvelteKit and Odoo Studio"
|
||||
|
||||
## After generation:
|
||||
Remind the user to:
|
||||
1. Navigate to the project directory
|
||||
2. Copy `.env.example` to `.env` and configure Odoo credentials
|
||||
3. Run `npm install` to install dependencies
|
||||
4. Run `npm run dev` to start development server
|
||||
5. Test the Odoo connection and sync functionality
|
||||
33
commands/new-vue-pwa.md
Normal file
33
commands/new-vue-pwa.md
Normal file
@@ -0,0 +1,33 @@
|
||||
Generate a new offline-first Progressive Web App using Vue framework with Odoo Studio backend integration.
|
||||
|
||||
## What this command does:
|
||||
- Invokes the `create-odoo-pwa` skill with Vue as the framework
|
||||
- Generates a complete PWA project with smart caching, offline support, and Odoo integration
|
||||
- Creates all necessary configuration files, components, composables, and deployment setups
|
||||
|
||||
## Required Information:
|
||||
Before starting, gather:
|
||||
1. **Project name** (kebab-case, e.g., "expense-tracker", "inventory-manager")
|
||||
2. **Odoo model name** (without `x_` prefix, e.g., "expense", "inventory")
|
||||
3. **Model display name** (human-readable, e.g., "Expense", "Inventory Item")
|
||||
4. **Deployment target** (optional: vercel, github, cloudflare, netlify)
|
||||
|
||||
## Steps:
|
||||
1. Ask the user for the required information listed above
|
||||
2. Invoke the `create-odoo-pwa` skill with framework set to "vue"
|
||||
3. Generate the complete Vue PWA project structure
|
||||
4. Create comprehensive documentation (README.md, CLAUDE.md, API.md)
|
||||
5. Provide next steps for setup and deployment
|
||||
|
||||
## Example prompts to use this command:
|
||||
- `/new-vue-pwa` - Interactive mode, will ask for all parameters
|
||||
- User: "Create a Vue PWA for tracking expenses with Odoo"
|
||||
- User: "Generate an inventory management app using Vue and Odoo Studio"
|
||||
|
||||
## After generation:
|
||||
Remind the user to:
|
||||
1. Navigate to the project directory
|
||||
2. Copy `.env.example` to `.env` and configure Odoo credentials
|
||||
3. Run `npm install` to install dependencies
|
||||
4. Run `npm run dev` to start development server
|
||||
5. Test the Odoo connection and sync functionality
|
||||
642
commands/optimize.md
Normal file
642
commands/optimize.md
Normal file
@@ -0,0 +1,642 @@
|
||||
Analyze and optimize your Odoo PWA for performance, bundle size, and user experience.
|
||||
|
||||
## What this command does:
|
||||
- Analyzes bundle size
|
||||
- Identifies performance bottlenecks
|
||||
- Suggests optimizations
|
||||
- Checks caching efficiency
|
||||
- Reviews PWA configuration
|
||||
- Provides actionable recommendations
|
||||
|
||||
---
|
||||
|
||||
## Quick Performance Check 🚀
|
||||
|
||||
### Run These Commands:
|
||||
```bash
|
||||
# Build for production
|
||||
npm run build
|
||||
|
||||
# Analyze bundle size
|
||||
npm run build -- --analyze
|
||||
|
||||
# Preview production build
|
||||
npm run preview
|
||||
|
||||
# Run Lighthouse audit
|
||||
# (Chrome DevTools → Lighthouse tab)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Bundle Size Analysis 📦
|
||||
|
||||
### Check Current Size
|
||||
```bash
|
||||
# Build and see output
|
||||
npm run build
|
||||
|
||||
# Typical output:
|
||||
# dist/index.html 1.2 kB
|
||||
# dist/assets/index-abc123.js 45.3 kB
|
||||
# dist/assets/vendor-def456.js 120.5 kB
|
||||
```
|
||||
|
||||
### Analyze Bundle Composition
|
||||
```bash
|
||||
# Install analyzer
|
||||
npm install -D rollup-plugin-visualizer
|
||||
|
||||
# Add to vite.config.js
|
||||
import { visualizer } from 'rollup-plugin-visualizer';
|
||||
|
||||
export default {
|
||||
plugins: [
|
||||
// ...other plugins
|
||||
visualizer({
|
||||
open: true,
|
||||
gzipSize: true,
|
||||
brotliSize: true
|
||||
})
|
||||
]
|
||||
};
|
||||
|
||||
# Build and open report
|
||||
npm run build
|
||||
# Opens stats.html in browser
|
||||
```
|
||||
|
||||
### Target Bundle Sizes:
|
||||
- ✅ **Excellent**: < 200 KB (gzipped)
|
||||
- ⚠️ **Good**: 200-500 KB
|
||||
- ❌ **Needs Work**: > 500 KB
|
||||
|
||||
---
|
||||
|
||||
## Optimization Strategies
|
||||
|
||||
### 1. Code Splitting 📂
|
||||
|
||||
#### Lazy Load Routes
|
||||
**SvelteKit** (automatic):
|
||||
```javascript
|
||||
// Routes are auto-split
|
||||
// src/routes/+page.svelte → separate chunk
|
||||
```
|
||||
|
||||
**React**:
|
||||
```javascript
|
||||
import { lazy, Suspense } from 'react';
|
||||
|
||||
const ExpenseList = lazy(() => import('./ExpenseList'));
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<Suspense fallback={<Loading />}>
|
||||
<ExpenseList />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**Vue**:
|
||||
```javascript
|
||||
const ExpenseList = () => import('./ExpenseList.vue');
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: '/expenses',
|
||||
component: ExpenseList // Lazy loaded
|
||||
}
|
||||
];
|
||||
```
|
||||
|
||||
#### Manual Code Splitting
|
||||
```javascript
|
||||
// Split heavy utility into separate chunk
|
||||
const heavyUtil = await import('./heavyUtility.js');
|
||||
heavyUtil.doSomething();
|
||||
```
|
||||
|
||||
### 2. Tree Shaking 🌳
|
||||
|
||||
#### Remove Unused Code
|
||||
```javascript
|
||||
// Bad: Imports everything
|
||||
import * as utils from './utils';
|
||||
|
||||
// Good: Import only what you need
|
||||
import { formatDate, formatCurrency } from './utils';
|
||||
```
|
||||
|
||||
#### Check for Unused Dependencies
|
||||
```bash
|
||||
npm install -g depcheck
|
||||
depcheck
|
||||
|
||||
# Remove unused packages
|
||||
npm uninstall <unused-package>
|
||||
```
|
||||
|
||||
### 3. Optimize Dependencies 📚
|
||||
|
||||
#### Use Lighter Alternatives
|
||||
```javascript
|
||||
// Instead of moment.js (heavy)
|
||||
import moment from 'moment'; // 72 KB
|
||||
|
||||
// Use date-fns (tree-shakeable)
|
||||
import { format } from 'date-fns'; // ~2 KB
|
||||
|
||||
// Or native Date
|
||||
new Date().toLocaleDateString();
|
||||
```
|
||||
|
||||
#### Common Heavy Packages & Alternatives:
|
||||
- ❌ **moment** (72 KB) → ✅ **date-fns** (tree-shakeable)
|
||||
- ❌ **lodash** (whole) → ✅ **lodash-es** (individual imports)
|
||||
- ❌ **axios** → ✅ **fetch** (built-in)
|
||||
- ❌ **uuid** → ✅ **crypto.randomUUID()** (built-in)
|
||||
|
||||
### 4. Optimize Images 🖼️
|
||||
|
||||
#### Compress Images
|
||||
```bash
|
||||
# Install image optimizer
|
||||
npm install -D vite-plugin-imagemin
|
||||
|
||||
# Add to vite.config.js
|
||||
import viteImagemin from 'vite-plugin-imagemin';
|
||||
|
||||
export default {
|
||||
plugins: [
|
||||
viteImagemin({
|
||||
gifsicle: { optimizationLevel: 7 },
|
||||
optipng: { optimizationLevel: 7 },
|
||||
mozjpeg: { quality: 80 },
|
||||
svgo: { plugins: [{ removeViewBox: false }] }
|
||||
})
|
||||
]
|
||||
};
|
||||
```
|
||||
|
||||
#### Lazy Load Images
|
||||
```javascript
|
||||
// Native lazy loading
|
||||
<img src="large-image.jpg" loading="lazy" />
|
||||
|
||||
// Or with IntersectionObserver
|
||||
```
|
||||
|
||||
#### Use Appropriate Formats
|
||||
- **WebP** for photos (smaller than JPEG)
|
||||
- **SVG** for icons/logos
|
||||
- **PNG** only when transparency needed
|
||||
|
||||
### 5. Minimize JavaScript ⚡
|
||||
|
||||
#### Enable Minification (default in Vite)
|
||||
```javascript
|
||||
// vite.config.js
|
||||
export default {
|
||||
build: {
|
||||
minify: 'terser', // or 'esbuild' (faster)
|
||||
terserOptions: {
|
||||
compress: {
|
||||
drop_console: true, // Remove console.logs
|
||||
drop_debugger: true
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
#### Remove Development Code
|
||||
```javascript
|
||||
// Use environment variables
|
||||
if (import.meta.env.DEV) {
|
||||
console.log('Debug info'); // Removed in production
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Caching Optimization 💾
|
||||
|
||||
### 1. Optimize Cache Strategy
|
||||
|
||||
#### Review Cache Settings
|
||||
```javascript
|
||||
// In cache store
|
||||
const CACHE_VALIDITY = 5 * 60 * 1000; // 5 minutes
|
||||
|
||||
// Consider your use case:
|
||||
// - Frequently changing data: 1-2 minutes
|
||||
// - Moderate updates: 5-10 minutes
|
||||
// - Rarely changes: 30-60 minutes
|
||||
```
|
||||
|
||||
#### Limit Initial Load
|
||||
```javascript
|
||||
// Don't fetch everything at once
|
||||
const records = await odoo.searchRecords(
|
||||
model,
|
||||
[],
|
||||
fields,
|
||||
100 // Limit to 100 records initially
|
||||
);
|
||||
|
||||
// Load more on demand (pagination)
|
||||
```
|
||||
|
||||
### 2. Optimize Sync Frequency
|
||||
```javascript
|
||||
// Adjust sync interval based on needs
|
||||
const SYNC_INTERVAL = 5 * 60 * 1000; // 5 minutes
|
||||
|
||||
// Balance:
|
||||
// - More frequent: Better UX, more bandwidth
|
||||
// - Less frequent: Less bandwidth, slightly stale data
|
||||
```
|
||||
|
||||
### 3. Selective Caching
|
||||
```javascript
|
||||
// Only cache fields you need
|
||||
const fields = [
|
||||
'x_studio_name',
|
||||
'x_studio_amount',
|
||||
'x_studio_date'
|
||||
// Don't fetch unnecessary fields
|
||||
];
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## PWA Optimization 📱
|
||||
|
||||
### 1. Service Worker Configuration
|
||||
|
||||
#### Optimize Caching Strategy
|
||||
```javascript
|
||||
// In service worker or vite-plugin-pwa config
|
||||
VitePWA({
|
||||
workbox: {
|
||||
runtimeCaching: [
|
||||
{
|
||||
urlPattern: /^https:\/\/fonts\.googleapis\.com\/.*/,
|
||||
handler: 'CacheFirst',
|
||||
options: {
|
||||
cacheName: 'google-fonts',
|
||||
expiration: {
|
||||
maxEntries: 10,
|
||||
maxAgeSeconds: 60 * 60 * 24 * 365 // 1 year
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
urlPattern: /^https:\/\/.*\.odoo\.com\/.*/,
|
||||
handler: 'NetworkFirst',
|
||||
options: {
|
||||
cacheName: 'odoo-api',
|
||||
networkTimeoutSeconds: 3
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### 2. Optimize Manifest
|
||||
```json
|
||||
{
|
||||
"name": "Expense Tracker",
|
||||
"short_name": "Expenses",
|
||||
"start_url": "/",
|
||||
"display": "standalone",
|
||||
"background_color": "#ffffff",
|
||||
"theme_color": "#0066cc",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/icon-192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "/icon-512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Optimize Icons
|
||||
```bash
|
||||
# Use optimized PNG or WebP
|
||||
# Compress images
|
||||
# Provide multiple sizes
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Network Optimization 🌐
|
||||
|
||||
### 1. Reduce API Calls
|
||||
|
||||
#### Batch Requests
|
||||
```javascript
|
||||
// Bad: Multiple calls
|
||||
const expenses = await fetchExpenses();
|
||||
const tasks = await fetchTasks();
|
||||
const partners = await fetchPartners();
|
||||
|
||||
// Good: Single call (if Odoo supports)
|
||||
const data = await fetchAll(['expenses', 'tasks', 'partners']);
|
||||
```
|
||||
|
||||
#### Cache Partner Names
|
||||
```javascript
|
||||
// Fetch once, reuse
|
||||
const partners = await odoo.fetchPartners();
|
||||
localStorage.setItem('partnerCache', JSON.stringify(partners));
|
||||
|
||||
// Later, use cached data
|
||||
const cached = JSON.parse(localStorage.getItem('partnerCache'));
|
||||
```
|
||||
|
||||
### 2. Incremental Loading
|
||||
```javascript
|
||||
// Load initial data
|
||||
const initial = await fetchExpenses({ limit: 20 });
|
||||
|
||||
// Load more on scroll
|
||||
function onScroll() {
|
||||
if (nearBottom) {
|
||||
loadMore();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Optimize Requests
|
||||
```javascript
|
||||
// Only fetch fields you display
|
||||
const fields = ['id', 'x_studio_name', 'x_studio_amount'];
|
||||
|
||||
// Use appropriate domain filters
|
||||
const domain = [
|
||||
['x_studio_date', '>=', lastMonth],
|
||||
['x_studio_status', '!=', 'deleted']
|
||||
];
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## UI Performance 🎨
|
||||
|
||||
### 1. Virtual Scrolling
|
||||
For long lists:
|
||||
```javascript
|
||||
// SvelteKit
|
||||
import { VirtualList } from 'svelte-virtual-list';
|
||||
|
||||
<VirtualList items={expenses} let:item>
|
||||
<ExpenseCard expense={item} />
|
||||
</VirtualList>
|
||||
|
||||
// React
|
||||
import { FixedSizeList } from 'react-window';
|
||||
|
||||
// Vue
|
||||
import { RecycleScroller } from 'vue-virtual-scroller';
|
||||
```
|
||||
|
||||
### 2. Debounce Search
|
||||
```javascript
|
||||
import { debounce } from './utils';
|
||||
|
||||
const handleSearch = debounce((query) => {
|
||||
searchExpenses(query);
|
||||
}, 300); // Wait 300ms after typing stops
|
||||
```
|
||||
|
||||
### 3. Optimize Rendering
|
||||
```javascript
|
||||
// SvelteKit: Use keyed each blocks
|
||||
{#each expenses as expense (expense.id)}
|
||||
<ExpenseCard {expense} />
|
||||
{/each}
|
||||
|
||||
// React: Use keys and memo
|
||||
const ExpenseCard = memo(({ expense }) => {
|
||||
// ...
|
||||
});
|
||||
|
||||
expenses.map(expense => (
|
||||
<ExpenseCard key={expense.id} expense={expense} />
|
||||
));
|
||||
|
||||
// Vue: Use v-memo
|
||||
<ExpenseCard
|
||||
v-for="expense in expenses"
|
||||
:key="expense.id"
|
||||
:expense="expense"
|
||||
v-memo="[expense.id]"
|
||||
/>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Build Optimization 🔨
|
||||
|
||||
### Vite Config Optimizations
|
||||
```javascript
|
||||
// vite.config.js
|
||||
export default {
|
||||
build: {
|
||||
// Target modern browsers
|
||||
target: 'es2020',
|
||||
|
||||
// Optimize chunks
|
||||
rollupOptions: {
|
||||
output: {
|
||||
manualChunks: {
|
||||
vendor: ['svelte', '@sveltejs/kit'],
|
||||
odoo: ['./src/lib/odoo.js', './src/lib/stores']
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Compression
|
||||
minify: 'esbuild', // Faster than terser
|
||||
|
||||
// Source maps (only for debugging)
|
||||
sourcemap: false
|
||||
},
|
||||
|
||||
// Optimize dependencies
|
||||
optimizeDeps: {
|
||||
include: ['date-fns', 'lodash-es']
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Lighthouse Audit 💡
|
||||
|
||||
### Run Lighthouse
|
||||
1. Open Chrome DevTools
|
||||
2. Go to Lighthouse tab
|
||||
3. Select categories:
|
||||
- ✅ Performance
|
||||
- ✅ Progressive Web App
|
||||
- ✅ Best Practices
|
||||
- ✅ Accessibility
|
||||
- ✅ SEO
|
||||
4. Click "Analyze page load"
|
||||
|
||||
### Target Scores:
|
||||
- **Performance**: > 90
|
||||
- **PWA**: 100
|
||||
- **Best Practices**: > 95
|
||||
- **Accessibility**: > 90
|
||||
- **SEO**: > 90
|
||||
|
||||
### Common Issues & Fixes:
|
||||
|
||||
#### Low Performance Score
|
||||
- Reduce bundle size
|
||||
- Optimize images
|
||||
- Enable caching
|
||||
- Lazy load resources
|
||||
|
||||
#### PWA Issues
|
||||
- Fix manifest.json
|
||||
- Register service worker
|
||||
- Add offline support
|
||||
- Provide app icons
|
||||
|
||||
#### Accessibility Issues
|
||||
- Add alt text to images
|
||||
- Use semantic HTML
|
||||
- Ensure color contrast
|
||||
- Add ARIA labels
|
||||
|
||||
---
|
||||
|
||||
## Performance Monitoring 📊
|
||||
|
||||
### Add Performance Tracking
|
||||
```javascript
|
||||
// Measure initial load
|
||||
window.addEventListener('load', () => {
|
||||
const perfData = performance.getEntriesByType('navigation')[0];
|
||||
|
||||
console.log('Load time:', perfData.loadEventEnd - perfData.fetchStart);
|
||||
console.log('DOM ready:', perfData.domContentLoadedEventEnd - perfData.fetchStart);
|
||||
});
|
||||
|
||||
// Measure sync time
|
||||
console.time('sync');
|
||||
await expenseCache.refresh();
|
||||
console.timeEnd('sync');
|
||||
```
|
||||
|
||||
### Use Web Vitals
|
||||
```bash
|
||||
npm install web-vitals
|
||||
|
||||
# In app
|
||||
import { getCLS, getFID, getFCP, getLCP, getTTFB } from 'web-vitals';
|
||||
|
||||
getCLS(console.log);
|
||||
getFID(console.log);
|
||||
getFCP(console.log);
|
||||
getLCP(console.log);
|
||||
getTTFB(console.log);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Optimization Checklist ✅
|
||||
|
||||
### Bundle Size:
|
||||
```
|
||||
□ Analyzed bundle composition
|
||||
□ Removed unused dependencies
|
||||
□ Lazy loaded routes/components
|
||||
□ Optimized images
|
||||
□ Tree-shaking enabled
|
||||
□ Minification enabled
|
||||
□ Total size < 500 KB (gzipped)
|
||||
```
|
||||
|
||||
### Caching:
|
||||
```
|
||||
□ Optimal cache validity period
|
||||
□ Incremental sync working
|
||||
□ Selective field fetching
|
||||
□ Partner name caching
|
||||
□ Appropriate sync frequency
|
||||
□ IndexedDB optimized
|
||||
```
|
||||
|
||||
### PWA:
|
||||
```
|
||||
□ Service worker registered
|
||||
□ Offline mode works
|
||||
□ App installable
|
||||
□ Icons optimized
|
||||
□ Manifest configured
|
||||
□ Fast load time
|
||||
```
|
||||
|
||||
### Performance:
|
||||
```
|
||||
□ Lighthouse score > 90
|
||||
□ Initial load < 3s
|
||||
□ Sync time < 2s
|
||||
□ No console errors
|
||||
□ Smooth scrolling
|
||||
□ Fast navigation
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Example prompts to use this command:
|
||||
- `/optimize` - Run optimization checks
|
||||
- User: "Make my app faster"
|
||||
- User: "Reduce bundle size"
|
||||
- User: "Optimize performance"
|
||||
|
||||
## Related Commands:
|
||||
- `/test-connection` - Test after optimization
|
||||
- `/update-deps` - Update to faster versions
|
||||
- `/troubleshoot` - Fix performance issues
|
||||
- `/help` - More information
|
||||
|
||||
---
|
||||
|
||||
## Quick Wins 🎯
|
||||
|
||||
### Immediate Optimizations:
|
||||
1. **Enable compression** (already in Vite)
|
||||
2. **Remove console.logs** in production
|
||||
3. **Lazy load routes** (easy with frameworks)
|
||||
4. **Optimize images** (use WebP)
|
||||
5. **Limit initial data fetch** (pagination)
|
||||
|
||||
### Medium Effort:
|
||||
1. **Analyze and split bundles**
|
||||
2. **Replace heavy dependencies**
|
||||
3. **Implement virtual scrolling**
|
||||
4. **Optimize service worker**
|
||||
5. **Add performance monitoring**
|
||||
|
||||
### Long Term:
|
||||
1. **Regular Lighthouse audits**
|
||||
2. **Monitor real user metrics**
|
||||
3. **Continuous optimization**
|
||||
4. **Keep dependencies updated**
|
||||
5. **Review and refactor regularly**
|
||||
87
commands/setup-env.md
Normal file
87
commands/setup-env.md
Normal file
@@ -0,0 +1,87 @@
|
||||
Interactive setup wizard for configuring Odoo PWA environment variables.
|
||||
|
||||
## What this command does:
|
||||
- Guides the user through setting up their `.env` file
|
||||
- Validates Odoo connection credentials
|
||||
- Tests API connectivity
|
||||
- Configures model-specific settings
|
||||
- Provides troubleshooting help if connection fails
|
||||
|
||||
## Required Information:
|
||||
Gather from the user:
|
||||
1. **Odoo Instance URL** (e.g., "https://yourcompany.odoo.com")
|
||||
2. **Database Name** (e.g., "yourcompany-main")
|
||||
3. **API Key** (from Odoo user preferences)
|
||||
4. **Username/Email** (Odoo user email)
|
||||
5. **Primary Model Name** (e.g., "x_expense", "x_inventory")
|
||||
|
||||
## Steps:
|
||||
1. Check if `.env.example` exists in the current directory
|
||||
2. If not, ask if this is an Odoo PWA project
|
||||
3. Ask the user for each environment variable interactively
|
||||
4. Validate URL format (must start with http:// or https://)
|
||||
5. Create or update `.env` file with the provided values
|
||||
6. Test the connection by making a simple API call to Odoo
|
||||
7. If connection fails, provide troubleshooting steps
|
||||
8. Display success message with next steps
|
||||
|
||||
## Environment Variables to Set:
|
||||
```bash
|
||||
# Odoo Instance Configuration
|
||||
VITE_ODOO_URL=https://yourcompany.odoo.com
|
||||
VITE_ODOO_DB=yourcompany-main
|
||||
|
||||
# Authentication (keep these secret!)
|
||||
ODOO_API_KEY=your_api_key_here
|
||||
ODOO_USERNAME=your.email@company.com
|
||||
|
||||
# Model Configuration
|
||||
VITE_MODEL_NAME=x_expense
|
||||
VITE_MODEL_DISPLAY_NAME=Expense
|
||||
```
|
||||
|
||||
## Validation Tests:
|
||||
After creating `.env`, run these checks:
|
||||
1. Test Odoo URL is reachable
|
||||
2. Verify API key is valid
|
||||
3. Check if the model exists in Odoo
|
||||
4. Test read permissions on the model
|
||||
5. Verify required fields are accessible
|
||||
|
||||
## Example prompts to use this command:
|
||||
- `/setup-env` - Interactive setup wizard
|
||||
- User: "Help me configure my Odoo credentials"
|
||||
- User: "Set up environment variables for Odoo PWA"
|
||||
|
||||
## Security Reminders:
|
||||
After setup, remind the user:
|
||||
1. ✅ `.env` should be in `.gitignore` (verify this)
|
||||
2. ✅ Never commit API keys to version control
|
||||
3. ✅ Use different credentials for development and production
|
||||
4. ✅ For deployment, set environment variables in the hosting platform
|
||||
5. ✅ Rotate API keys periodically
|
||||
|
||||
## Troubleshooting Common Issues:
|
||||
|
||||
### Connection Failed
|
||||
- Verify Odoo URL is correct and accessible
|
||||
- Check if API key is valid (generate new one in Odoo)
|
||||
- Ensure username matches the API key owner
|
||||
- Check firewall/network restrictions
|
||||
|
||||
### Model Not Found
|
||||
- Verify the model exists in Odoo Studio
|
||||
- Check model name has `x_` prefix
|
||||
- Ensure user has access permissions to the model
|
||||
|
||||
### Authentication Error
|
||||
- Regenerate API key in Odoo (Settings → Users → API Keys)
|
||||
- Verify database name is correct
|
||||
- Check if account is active and not locked
|
||||
|
||||
## After Setup:
|
||||
Remind the user to:
|
||||
1. Restart the development server to load new environment variables
|
||||
2. Test the application and verify data loads correctly
|
||||
3. Keep the `.env.example` file updated for team members
|
||||
4. Document any custom configuration in project README
|
||||
230
commands/test-connection.md
Normal file
230
commands/test-connection.md
Normal file
@@ -0,0 +1,230 @@
|
||||
Test Odoo API connection and sync functionality to diagnose issues.
|
||||
|
||||
## What this command does:
|
||||
- Validates Odoo connection credentials
|
||||
- Tests API authentication
|
||||
- Verifies model access and permissions
|
||||
- Checks sync functionality
|
||||
- Runs diagnostic tests on cache stores
|
||||
- Provides detailed error reporting and solutions
|
||||
|
||||
## Prerequisites:
|
||||
Before testing, ensure:
|
||||
1. ✅ `.env` file exists and is configured
|
||||
2. ✅ Current directory is an Odoo PWA project
|
||||
3. ✅ Development server can be started
|
||||
4. ✅ Internet connection is available
|
||||
|
||||
## Diagnostic Tests to Run:
|
||||
|
||||
### 1. Environment Configuration Check
|
||||
Verify all required environment variables are set:
|
||||
- `VITE_ODOO_URL` - Odoo instance URL
|
||||
- `VITE_ODOO_DB` - Database name
|
||||
- `ODOO_API_KEY` - API authentication key
|
||||
- `ODOO_USERNAME` - User email/username
|
||||
- `VITE_MODEL_NAME` - Primary model name
|
||||
|
||||
### 2. Network Connectivity Test
|
||||
```bash
|
||||
# Test if Odoo URL is reachable
|
||||
curl -I [ODOO_URL]
|
||||
```
|
||||
|
||||
Expected: HTTP 200 or 301/302 redirect
|
||||
|
||||
### 3. API Authentication Test
|
||||
Make a test API call to verify credentials:
|
||||
```javascript
|
||||
POST /api/odoo
|
||||
{
|
||||
"action": "search",
|
||||
"model": "res.partner",
|
||||
"domain": [],
|
||||
"fields": ["id", "name"],
|
||||
"limit": 1
|
||||
}
|
||||
```
|
||||
|
||||
Expected: Returns at least one partner record
|
||||
|
||||
### 4. Model Access Test
|
||||
Verify the configured model exists and is accessible:
|
||||
```javascript
|
||||
POST /api/odoo
|
||||
{
|
||||
"action": "search_model",
|
||||
"model": "[VITE_MODEL_NAME]",
|
||||
"domain": [],
|
||||
"fields": ["id"],
|
||||
"limit": 1
|
||||
}
|
||||
```
|
||||
|
||||
Expected: Returns records or empty array (not an error)
|
||||
|
||||
### 5. CRUD Operations Test
|
||||
Test each operation:
|
||||
- **Create**: Create a test record
|
||||
- **Read**: Fetch the created record
|
||||
- **Update**: Modify the record
|
||||
- **Delete**: Remove the test record
|
||||
|
||||
### 6. Cache Functionality Test
|
||||
Verify cache stores work correctly:
|
||||
- localStorage read/write
|
||||
- IndexedDB read/write
|
||||
- Cache expiration logic
|
||||
- Sync mechanism
|
||||
|
||||
### 7. Sync Performance Test
|
||||
Measure sync performance:
|
||||
- Initial load time
|
||||
- Incremental sync time
|
||||
- Number of records synced
|
||||
- Network request count
|
||||
|
||||
## Steps:
|
||||
1. Read and validate `.env` file
|
||||
2. Parse environment variables
|
||||
3. Run each diagnostic test in sequence
|
||||
4. Log results with timestamps
|
||||
5. Identify failing tests
|
||||
6. Provide specific solutions for failures
|
||||
7. Generate diagnostic report
|
||||
|
||||
## Output Format:
|
||||
```
|
||||
🧪 Odoo PWA Connection Diagnostics
|
||||
================================
|
||||
|
||||
✅ Environment Configuration: PASSED
|
||||
- ODOO_URL: https://yourcompany.odoo.com
|
||||
- DATABASE: yourcompany-main
|
||||
- MODEL: x_expense
|
||||
|
||||
✅ Network Connectivity: PASSED
|
||||
- Odoo server reachable
|
||||
- Response time: 234ms
|
||||
|
||||
✅ API Authentication: PASSED
|
||||
- API key valid
|
||||
- User authenticated: your.email@company.com
|
||||
|
||||
✅ Model Access: PASSED
|
||||
- Model exists: x_expense
|
||||
- Read permission: Yes
|
||||
- Write permission: Yes
|
||||
|
||||
✅ CRUD Operations: PASSED
|
||||
- Create: ✅ Record created (ID: 123)
|
||||
- Read: ✅ Record fetched
|
||||
- Update: ✅ Record updated
|
||||
- Delete: ✅ Record deleted
|
||||
|
||||
✅ Cache Functionality: PASSED
|
||||
- localStorage: Working
|
||||
- IndexedDB: Working
|
||||
- Sync interval: 3 minutes
|
||||
|
||||
✅ Sync Performance: PASSED
|
||||
- Initial load: 1.2s (45 records)
|
||||
- Incremental sync: 0.3s (2 new records)
|
||||
- Network requests: 3
|
||||
|
||||
================================
|
||||
🎉 All tests passed! Your Odoo PWA is working correctly.
|
||||
```
|
||||
|
||||
## Example prompts to use this command:
|
||||
- `/test-connection` - Run full diagnostic suite
|
||||
- User: "Test my Odoo connection"
|
||||
- User: "Why isn't data syncing from Odoo?"
|
||||
- User: "Diagnose Odoo API issues"
|
||||
|
||||
## Common Issues and Solutions:
|
||||
|
||||
### ❌ Connection Failed
|
||||
**Error**: Cannot reach Odoo URL
|
||||
**Solutions**:
|
||||
- Verify URL is correct (include https://)
|
||||
- Check internet connection
|
||||
- Test URL in browser
|
||||
- Check firewall/VPN settings
|
||||
|
||||
### ❌ Authentication Failed
|
||||
**Error**: Invalid API key or credentials
|
||||
**Solutions**:
|
||||
- Regenerate API key in Odoo (Settings → Users → API Keys)
|
||||
- Verify username matches API key owner
|
||||
- Check for typos in `.env` file
|
||||
- Ensure API key has not expired
|
||||
|
||||
### ❌ Model Not Found
|
||||
**Error**: Model doesn't exist or no access
|
||||
**Solutions**:
|
||||
- Verify model exists in Odoo Studio
|
||||
- Check model name includes `x_` prefix
|
||||
- Verify user has read/write permissions
|
||||
- Check model is published (not in draft mode)
|
||||
|
||||
### ❌ CORS Error
|
||||
**Error**: Blocked by CORS policy
|
||||
**Solutions**:
|
||||
- Use server-side API proxy (recommended)
|
||||
- Configure CORS in Odoo (not recommended)
|
||||
- Check API route is working correctly
|
||||
|
||||
### ❌ Sync Not Working
|
||||
**Error**: Records not updating
|
||||
**Solutions**:
|
||||
- Check browser console for errors
|
||||
- Verify sync interval is running
|
||||
- Clear cache and try again
|
||||
- Check Odoo server is not down
|
||||
|
||||
### ❌ Permission Denied
|
||||
**Error**: Cannot create/update records
|
||||
**Solutions**:
|
||||
- Verify user has write permission on model
|
||||
- Check required fields are included
|
||||
- Verify field types match expectations
|
||||
- Check for validation rules in Odoo
|
||||
|
||||
## Development Tools:
|
||||
Provide user with helpful commands:
|
||||
```bash
|
||||
# Watch network requests
|
||||
# Open browser DevTools → Network tab
|
||||
|
||||
# View cache contents
|
||||
localStorage.getItem('[model]Cache')
|
||||
|
||||
# Force cache clear
|
||||
localStorage.clear()
|
||||
|
||||
# Monitor sync in console
|
||||
# Look for "Syncing..." messages
|
||||
```
|
||||
|
||||
## After Testing:
|
||||
If all tests pass:
|
||||
- Confirm the application is working correctly
|
||||
- Suggest running tests periodically
|
||||
- Recommend monitoring in production
|
||||
|
||||
If tests fail:
|
||||
- Provide specific error messages
|
||||
- Offer step-by-step troubleshooting
|
||||
- Suggest checking Odoo server logs
|
||||
- Offer to help fix configuration
|
||||
|
||||
## Advanced Diagnostics:
|
||||
For complex issues:
|
||||
1. Export full diagnostic report
|
||||
2. Check Odoo server logs
|
||||
3. Review browser console errors
|
||||
4. Analyze network traffic
|
||||
5. Test with different API keys
|
||||
6. Try with different models
|
||||
7. Compare with working examples
|
||||
694
commands/troubleshoot.md
Normal file
694
commands/troubleshoot.md
Normal file
@@ -0,0 +1,694 @@
|
||||
Common issues and solutions for Odoo PWA projects.
|
||||
|
||||
## What this command does:
|
||||
- Lists common problems and their solutions
|
||||
- Provides step-by-step debugging guides
|
||||
- Offers troubleshooting workflows
|
||||
- Helps diagnose connection, sync, and build issues
|
||||
- Links to relevant documentation and tools
|
||||
|
||||
---
|
||||
|
||||
## Quick Diagnostic Checklist ✅
|
||||
|
||||
Before diving into specific issues, run through this checklist:
|
||||
|
||||
```
|
||||
□ .env file exists and has all required variables
|
||||
□ Odoo URL is correct and reachable
|
||||
□ API key is valid and not expired
|
||||
□ Database name is correct
|
||||
□ Model name includes x_ prefix
|
||||
□ Development server is running
|
||||
□ No errors in browser console
|
||||
□ Internet connection is working
|
||||
□ Node.js version is 18 or higher
|
||||
```
|
||||
|
||||
**Quick Test:** Run `/test-connection` to diagnose most issues automatically.
|
||||
|
||||
---
|
||||
|
||||
## Connection Issues 🔌
|
||||
|
||||
### Problem: Cannot connect to Odoo
|
||||
**Symptoms:**
|
||||
- "Connection refused" error
|
||||
- "Network error" in console
|
||||
- Timeout errors
|
||||
- No data loading
|
||||
|
||||
**Solutions:**
|
||||
|
||||
#### 1. Verify Odoo URL
|
||||
```bash
|
||||
# Test if URL is reachable
|
||||
curl -I https://yourcompany.odoo.com
|
||||
|
||||
# Expected: HTTP 200 or 301/302
|
||||
```
|
||||
|
||||
**Common mistakes:**
|
||||
- Missing `https://`
|
||||
- Extra trailing slash
|
||||
- Wrong subdomain
|
||||
- Typo in URL
|
||||
|
||||
**Fix:**
|
||||
```bash
|
||||
# Wrong
|
||||
VITE_ODOO_URL=yourcompany.odoo.com
|
||||
VITE_ODOO_URL=https://yourcompany.odoo.com/
|
||||
|
||||
# Right
|
||||
VITE_ODOO_URL=https://yourcompany.odoo.com
|
||||
```
|
||||
|
||||
#### 2. Check firewall / VPN
|
||||
- Try accessing Odoo URL in browser
|
||||
- Disable VPN temporarily
|
||||
- Check corporate firewall rules
|
||||
- Try from different network
|
||||
|
||||
#### 3. Verify environment variables loaded
|
||||
```javascript
|
||||
// Add to your code temporarily
|
||||
console.log('Odoo URL:', import.meta.env.VITE_ODOO_URL);
|
||||
console.log('Database:', import.meta.env.VITE_ODOO_DB);
|
||||
```
|
||||
|
||||
**If undefined:**
|
||||
- Restart development server
|
||||
- Check variable names (case-sensitive)
|
||||
- Ensure variables start with `VITE_`
|
||||
|
||||
---
|
||||
|
||||
## Authentication Issues 🔐
|
||||
|
||||
### Problem: Invalid API key / Authentication failed
|
||||
**Symptoms:**
|
||||
- "Invalid credentials" error
|
||||
- "Access denied" message
|
||||
- 401 Unauthorized errors
|
||||
- UID is null
|
||||
|
||||
**Solutions:**
|
||||
|
||||
#### 1. Regenerate API Key
|
||||
1. Log into Odoo
|
||||
2. Go to Settings → Users & Companies → Users
|
||||
3. Open your user record
|
||||
4. Go to "API Keys" tab
|
||||
5. Click "New API Key"
|
||||
6. Copy the key immediately (shown only once!)
|
||||
7. Update `.env` file:
|
||||
```bash
|
||||
ODOO_API_KEY=your_new_api_key_here
|
||||
```
|
||||
8. Restart development server
|
||||
|
||||
#### 2. Verify username matches
|
||||
```bash
|
||||
# Username must match the API key owner
|
||||
ODOO_USERNAME=john.doe@company.com # ✅ Correct
|
||||
ODOO_USERNAME=John Doe # ❌ Wrong
|
||||
```
|
||||
|
||||
#### 3. Check user permissions
|
||||
- Ensure user has access to the model
|
||||
- Verify read/write permissions
|
||||
- Check if user account is active
|
||||
- Verify not locked out
|
||||
|
||||
#### 4. Test authentication manually
|
||||
```javascript
|
||||
// In browser console
|
||||
fetch('/api/odoo', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
action: 'search',
|
||||
model: 'res.partner',
|
||||
domain: [],
|
||||
fields: ['name'],
|
||||
limit: 1
|
||||
})
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(console.log);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Model Access Issues 📋
|
||||
|
||||
### Problem: Model not found / No records returned
|
||||
**Symptoms:**
|
||||
- "Model does not exist" error
|
||||
- Empty array returned
|
||||
- "Access rights" error
|
||||
- Records don't appear
|
||||
|
||||
**Solutions:**
|
||||
|
||||
#### 1. Verify model name
|
||||
```bash
|
||||
# Check model name includes x_ prefix
|
||||
VITE_MODEL_NAME=x_expense # ✅ Correct
|
||||
VITE_MODEL_NAME=expense # ❌ Wrong
|
||||
```
|
||||
|
||||
#### 2. Check model exists in Odoo
|
||||
1. Log into Odoo
|
||||
2. Go to Settings → Technical → Database Structure → Models
|
||||
3. Search for your model name
|
||||
4. Verify it exists and is active
|
||||
|
||||
#### 3. Check field names
|
||||
```javascript
|
||||
// Fields must use full name with x_studio_ prefix
|
||||
const fields = [
|
||||
'x_studio_name', // ✅ Correct
|
||||
'x_studio_amount', // ✅ Correct
|
||||
'name', // ❌ Wrong (unless it's a standard field)
|
||||
'amount' // ❌ Wrong
|
||||
];
|
||||
```
|
||||
|
||||
#### 4. Verify permissions
|
||||
- User must have read access to model
|
||||
- Check access rights in Odoo Studio
|
||||
- Verify record rules don't filter all records
|
||||
- Test with admin user
|
||||
|
||||
#### 5. Check if model is published
|
||||
- In Odoo Studio, verify model is not in draft mode
|
||||
- Ensure model is accessible via API
|
||||
- Check if model requires special access
|
||||
|
||||
---
|
||||
|
||||
## Sync Issues 🔄
|
||||
|
||||
### Problem: Data not syncing
|
||||
**Symptoms:**
|
||||
- Old data displayed
|
||||
- Changes don't appear
|
||||
- "Last synced" time not updating
|
||||
- Background sync not working
|
||||
|
||||
**Solutions:**
|
||||
|
||||
#### 1. Check browser console
|
||||
Look for:
|
||||
- Network errors
|
||||
- JavaScript errors
|
||||
- CORS errors
|
||||
- Authentication errors
|
||||
|
||||
#### 2. Verify sync mechanism
|
||||
```javascript
|
||||
// Check if sync is running
|
||||
// Look for these console logs:
|
||||
"Syncing x_expense..."
|
||||
"Fetched X new records"
|
||||
"Cache updated"
|
||||
```
|
||||
|
||||
#### 3. Force refresh
|
||||
```javascript
|
||||
// In browser console
|
||||
expenseCache.refresh();
|
||||
```
|
||||
|
||||
#### 4. Clear cache and reload
|
||||
```javascript
|
||||
// In browser console
|
||||
localStorage.clear();
|
||||
// Then refresh page
|
||||
```
|
||||
|
||||
#### 5. Check sync interval
|
||||
```javascript
|
||||
// Verify sync timer is running
|
||||
// Default: 3 minutes (180,000ms)
|
||||
// Look in cache store for:
|
||||
setInterval(() => sync(), 180000);
|
||||
```
|
||||
|
||||
#### 6. Test incremental sync
|
||||
```javascript
|
||||
// Check lastRecordId is updating
|
||||
const cacheData = localStorage.getItem('expenseCache');
|
||||
console.log(JSON.parse(cacheData));
|
||||
// Should show: { lastRecordId: X, lastSyncTime: Y }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Offline Mode Issues 📵
|
||||
|
||||
### Problem: Offline mode not working
|
||||
**Symptoms:**
|
||||
- App doesn't work without internet
|
||||
- "No connection" errors when offline
|
||||
- Service worker not registered
|
||||
- Data not available offline
|
||||
|
||||
**Solutions:**
|
||||
|
||||
#### 1. Verify service worker registered
|
||||
```javascript
|
||||
// In browser console
|
||||
navigator.serviceWorker.getRegistration()
|
||||
.then(reg => console.log('Service Worker:', reg));
|
||||
```
|
||||
|
||||
#### 2. Check cache stores
|
||||
```javascript
|
||||
// Verify data is cached
|
||||
localStorage.getItem('expenseCache');
|
||||
// Should return JSON string
|
||||
|
||||
// Check IndexedDB
|
||||
// Open DevTools → Application → IndexedDB
|
||||
// Look for your database and tables
|
||||
```
|
||||
|
||||
#### 3. Test offline mode
|
||||
1. Load app while online
|
||||
2. Open DevTools → Network tab
|
||||
3. Enable "Offline" checkbox
|
||||
4. Refresh page
|
||||
5. App should still work
|
||||
|
||||
#### 4. Build and test production
|
||||
```bash
|
||||
# Offline mode works better in production build
|
||||
npm run build
|
||||
npm run preview
|
||||
```
|
||||
|
||||
#### 5. Check manifest.json
|
||||
Verify PWA manifest is correct:
|
||||
- Located in `/static/manifest.json`
|
||||
- Has correct `start_url`
|
||||
- Has valid icons
|
||||
|
||||
---
|
||||
|
||||
## Build / Deployment Issues 🚀
|
||||
|
||||
### Problem: Build fails
|
||||
**Symptoms:**
|
||||
- `npm run build` returns errors
|
||||
- TypeScript errors
|
||||
- Module not found errors
|
||||
- Build process hangs
|
||||
|
||||
**Solutions:**
|
||||
|
||||
#### 1. Check Node version
|
||||
```bash
|
||||
node --version
|
||||
# Must be v18 or higher
|
||||
|
||||
# If too old:
|
||||
nvm install 18
|
||||
nvm use 18
|
||||
```
|
||||
|
||||
#### 2. Clean install
|
||||
```bash
|
||||
rm -rf node_modules package-lock.json
|
||||
npm install
|
||||
```
|
||||
|
||||
#### 3. Check for errors
|
||||
```bash
|
||||
# Run build with verbose output
|
||||
npm run build -- --verbose
|
||||
```
|
||||
|
||||
#### 4. Verify environment variables
|
||||
```bash
|
||||
# For production build, set env vars:
|
||||
VITE_ODOO_URL=https://yourcompany.odoo.com \
|
||||
VITE_ODOO_DB=yourcompany-main \
|
||||
npm run build
|
||||
```
|
||||
|
||||
#### 5. Check imports
|
||||
- Verify all imports are correct
|
||||
- Check for circular dependencies
|
||||
- Ensure all files exist
|
||||
|
||||
---
|
||||
|
||||
### Problem: Deployment fails
|
||||
**Symptoms:**
|
||||
- Vercel/Netlify build fails
|
||||
- Environment variables not working
|
||||
- 404 errors in production
|
||||
- Blank page after deployment
|
||||
|
||||
**Solutions:**
|
||||
|
||||
#### 1. Set environment variables
|
||||
In hosting dashboard:
|
||||
- Add all `VITE_*` variables
|
||||
- Add `ODOO_API_KEY`
|
||||
- Add `ODOO_USERNAME`
|
||||
- Redeploy after adding
|
||||
|
||||
#### 2. Check build logs
|
||||
- Review deployment logs
|
||||
- Look for specific errors
|
||||
- Check Node version in platform
|
||||
|
||||
#### 3. Test build locally
|
||||
```bash
|
||||
npm run build
|
||||
npm run preview
|
||||
# Test at http://localhost:4173
|
||||
```
|
||||
|
||||
#### 4. Verify base path
|
||||
```javascript
|
||||
// For GitHub Pages or subpath deployment
|
||||
// vite.config.js
|
||||
export default {
|
||||
base: '/your-repo-name/'
|
||||
};
|
||||
```
|
||||
|
||||
#### 5. Check API routes
|
||||
- Ensure serverless functions deploy correctly
|
||||
- Verify function size < 50MB
|
||||
- Check function logs in platform dashboard
|
||||
|
||||
---
|
||||
|
||||
## Performance Issues ⚡
|
||||
|
||||
### Problem: App is slow
|
||||
**Symptoms:**
|
||||
- Slow initial load
|
||||
- Laggy UI
|
||||
- Long sync times
|
||||
- High memory usage
|
||||
|
||||
**Solutions:**
|
||||
|
||||
#### 1. Optimize initial load
|
||||
```javascript
|
||||
// Limit initial fetch
|
||||
const records = await odoo.searchRecords(
|
||||
model,
|
||||
[],
|
||||
fields,
|
||||
100 // Only fetch first 100
|
||||
);
|
||||
```
|
||||
|
||||
#### 2. Add pagination
|
||||
```javascript
|
||||
// Load more on scroll
|
||||
let page = 1;
|
||||
const pageSize = 20;
|
||||
|
||||
async function loadMore() {
|
||||
const offset = (page - 1) * pageSize;
|
||||
const more = await odoo.searchRecords(
|
||||
model, [], fields, pageSize, offset
|
||||
);
|
||||
page++;
|
||||
return more;
|
||||
}
|
||||
```
|
||||
|
||||
#### 3. Optimize bundle size
|
||||
```bash
|
||||
# Analyze bundle
|
||||
npm run build -- --analyze
|
||||
|
||||
# Lazy load routes
|
||||
// SvelteKit (automatic)
|
||||
// React
|
||||
const ExpenseList = lazy(() => import('./ExpenseList'));
|
||||
|
||||
// Vue
|
||||
const ExpenseList = () => import('./ExpenseList.vue');
|
||||
```
|
||||
|
||||
#### 4. Reduce sync frequency
|
||||
```javascript
|
||||
// Increase sync interval
|
||||
const SYNC_INTERVAL = 5 * 60 * 1000; // 5 minutes instead of 3
|
||||
```
|
||||
|
||||
#### 5. Optimize images
|
||||
- Compress images before upload
|
||||
- Use appropriate image formats
|
||||
- Lazy load images
|
||||
|
||||
---
|
||||
|
||||
## Data Issues 📊
|
||||
|
||||
### Problem: Wrong data displayed
|
||||
**Symptoms:**
|
||||
- Incorrect values shown
|
||||
- Missing fields
|
||||
- Corrupted data
|
||||
- Date format issues
|
||||
|
||||
**Solutions:**
|
||||
|
||||
#### 1. Clear cache
|
||||
```javascript
|
||||
expenseCache.clearCache();
|
||||
expenseCache.refresh();
|
||||
```
|
||||
|
||||
#### 2. Verify field mapping
|
||||
```javascript
|
||||
// Check if fields are correctly named
|
||||
console.log(records[0]);
|
||||
// Should show x_studio_* fields
|
||||
```
|
||||
|
||||
#### 3. Check data types
|
||||
```javascript
|
||||
// Ensure correct types
|
||||
const expense = {
|
||||
x_studio_amount: 45.50, // number ✅
|
||||
x_studio_date: '2025-01-15', // string (ISO) ✅
|
||||
x_studio_employee: [12, false] // Many2one ✅
|
||||
};
|
||||
```
|
||||
|
||||
#### 4. Format dates correctly
|
||||
```javascript
|
||||
// Store as ISO string
|
||||
const dateString = '2025-01-15';
|
||||
|
||||
// Display formatted
|
||||
new Date(dateString).toLocaleDateString();
|
||||
```
|
||||
|
||||
#### 5. Handle Many2one fields
|
||||
```javascript
|
||||
// Many2one can be [id, name] or just id
|
||||
function getPartnerId(field) {
|
||||
return Array.isArray(field) ? field[0] : field;
|
||||
}
|
||||
|
||||
function getPartnerName(field) {
|
||||
return Array.isArray(field) ? field[1] : null;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## CORS Issues 🚫
|
||||
|
||||
### Problem: CORS error
|
||||
**Symptoms:**
|
||||
- "Blocked by CORS policy" in console
|
||||
- Requests fail in browser but work in Postman
|
||||
- Preflight errors (OPTIONS)
|
||||
|
||||
**Solutions:**
|
||||
|
||||
#### 1. Use server-side proxy (recommended)
|
||||
All generated PWAs include server-side API routes. Ensure you're using them:
|
||||
```javascript
|
||||
// ✅ Good: Goes through server proxy
|
||||
fetch('/api/odoo', { ... });
|
||||
|
||||
// ❌ Bad: Direct call to Odoo (CORS error)
|
||||
fetch('https://yourcompany.odoo.com/jsonrpc', { ... });
|
||||
```
|
||||
|
||||
#### 2. Verify API route works
|
||||
```javascript
|
||||
// Test in browser console
|
||||
fetch('/api/odoo', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
action: 'search',
|
||||
model: 'res.partner',
|
||||
domain: [],
|
||||
fields: ['name'],
|
||||
limit: 1
|
||||
})
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(console.log);
|
||||
```
|
||||
|
||||
#### 3. Check if deployed correctly
|
||||
- Verify serverless functions deployed
|
||||
- Check function logs
|
||||
- Ensure environment variables set
|
||||
|
||||
---
|
||||
|
||||
## Browser-Specific Issues 🌐
|
||||
|
||||
### Problem: Works in Chrome but not Safari/Firefox
|
||||
**Symptoms:**
|
||||
- Different behavior across browsers
|
||||
- Safari-specific errors
|
||||
- Mobile browser issues
|
||||
|
||||
**Solutions:**
|
||||
|
||||
#### 1. Check browser compatibility
|
||||
- Test in multiple browsers
|
||||
- Check for console errors in each
|
||||
- Verify IndexedDB support
|
||||
|
||||
#### 2. Safari-specific
|
||||
- Safari has stricter storage limits
|
||||
- Check if localStorage/IndexedDB working
|
||||
- Test in private mode
|
||||
|
||||
#### 3. Mobile browsers
|
||||
- Test on actual devices
|
||||
- Check responsive design
|
||||
- Verify touch interactions work
|
||||
|
||||
---
|
||||
|
||||
## Debugging Tools & Commands 🔧
|
||||
|
||||
### Essential Commands
|
||||
```bash
|
||||
# Test connection
|
||||
/test-connection
|
||||
|
||||
# View full help
|
||||
/help
|
||||
|
||||
# See architecture details
|
||||
/architecture
|
||||
|
||||
# Clear cache and start fresh
|
||||
/clear-cache
|
||||
|
||||
# Fix sync issues
|
||||
/fix-sync
|
||||
```
|
||||
|
||||
### Browser DevTools
|
||||
```javascript
|
||||
// Network tab
|
||||
// - See all API calls
|
||||
// - Check request/response
|
||||
// - View timing
|
||||
|
||||
// Console tab
|
||||
// - See error messages
|
||||
// - Run test commands
|
||||
// - Inspect data
|
||||
|
||||
// Application tab
|
||||
// - View localStorage
|
||||
// - Inspect IndexedDB
|
||||
// - Check Service Workers
|
||||
// - View Cache Storage
|
||||
|
||||
// Performance tab
|
||||
// - Profile slow operations
|
||||
// - Find bottlenecks
|
||||
```
|
||||
|
||||
### Useful Console Commands
|
||||
```javascript
|
||||
// View cache
|
||||
localStorage.getItem('expenseCache');
|
||||
|
||||
// Clear cache
|
||||
localStorage.clear();
|
||||
|
||||
// Force refresh
|
||||
expenseCache.refresh();
|
||||
|
||||
// View current records
|
||||
console.log($expenseCache); // SvelteKit
|
||||
|
||||
// Test API
|
||||
fetch('/api/odoo', { /* ... */ })
|
||||
.then(r => r.json())
|
||||
.then(console.log);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Getting Help 💬
|
||||
|
||||
### Before asking for help:
|
||||
1. Run `/test-connection`
|
||||
2. Check browser console for errors
|
||||
3. Review this troubleshooting guide
|
||||
4. Try solutions listed above
|
||||
5. Test with minimal example
|
||||
|
||||
### When reporting issues:
|
||||
Include:
|
||||
- Exact error message
|
||||
- Browser and version
|
||||
- Node.js version
|
||||
- Steps to reproduce
|
||||
- What you've already tried
|
||||
- Relevant code snippets
|
||||
|
||||
### Helpful resources:
|
||||
- `/help` - Plugin documentation
|
||||
- `/examples` - Usage examples
|
||||
- `/architecture` - Design patterns
|
||||
- `/api-reference` - API documentation
|
||||
- Odoo docs: https://www.odoo.com/documentation/
|
||||
- Framework docs (SvelteKit/React/Vue)
|
||||
|
||||
---
|
||||
|
||||
## Example prompts to use this command:
|
||||
- `/troubleshoot` - Show troubleshooting guide
|
||||
- User: "Why isn't my data syncing?"
|
||||
- User: "I'm getting a connection error"
|
||||
- User: "Help! Nothing works!"
|
||||
|
||||
## Still Stuck?
|
||||
If these solutions don't help:
|
||||
1. Run `/test-connection` for detailed diagnostics
|
||||
2. Check generated project's CLAUDE.md
|
||||
3. Review Odoo server logs
|
||||
4. Test with different Odoo model
|
||||
5. Try generating fresh project to compare
|
||||
|
||||
Most issues are related to configuration or permissions. Double-check your `.env` file and Odoo settings!
|
||||
505
commands/update-deps.md
Normal file
505
commands/update-deps.md
Normal file
@@ -0,0 +1,505 @@
|
||||
Update dependencies in your Odoo PWA project to latest compatible versions.
|
||||
|
||||
## What this command does:
|
||||
- Checks for outdated dependencies
|
||||
- Updates packages to latest versions
|
||||
- Tests the application after updates
|
||||
- Fixes breaking changes if needed
|
||||
- Updates lock files
|
||||
- Verifies everything still works
|
||||
|
||||
## Prerequisites:
|
||||
- Current directory is an Odoo PWA project
|
||||
- Git repository (recommended for easy rollback)
|
||||
- Latest npm installed
|
||||
|
||||
---
|
||||
|
||||
## Update Strategy
|
||||
|
||||
### Safe Update (Recommended)
|
||||
Updates to latest compatible versions within semver ranges.
|
||||
|
||||
```bash
|
||||
npm update
|
||||
```
|
||||
|
||||
### Major Update (Requires testing)
|
||||
Updates to latest versions including major releases.
|
||||
|
||||
```bash
|
||||
npm outdated # See what's outdated
|
||||
npm update # Update minor/patch
|
||||
npx npm-check-updates -u # Update majors
|
||||
npm install
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Steps:
|
||||
|
||||
### 1. Check Current Status
|
||||
```bash
|
||||
# See current versions
|
||||
npm list --depth=0
|
||||
|
||||
# See outdated packages
|
||||
npm outdated
|
||||
```
|
||||
|
||||
### 2. Create Backup
|
||||
```bash
|
||||
# Commit current state
|
||||
git add .
|
||||
git commit -m "Pre-dependency update checkpoint"
|
||||
|
||||
# Or backup package files
|
||||
cp package.json package.json.backup
|
||||
cp package-lock.json package-lock.json.backup
|
||||
```
|
||||
|
||||
### 3. Update Dependencies
|
||||
|
||||
#### Minor/Patch Updates (Safe)
|
||||
```bash
|
||||
npm update
|
||||
npm install
|
||||
```
|
||||
|
||||
#### Major Updates (Test carefully)
|
||||
```bash
|
||||
# Install npm-check-updates if needed
|
||||
npm install -g npm-check-updates
|
||||
|
||||
# Preview updates
|
||||
ncu
|
||||
|
||||
# Update package.json
|
||||
ncu -u
|
||||
|
||||
# Install new versions
|
||||
npm install
|
||||
```
|
||||
|
||||
### 4. Framework-Specific Updates
|
||||
|
||||
#### SvelteKit
|
||||
```bash
|
||||
# Update Svelte + SvelteKit
|
||||
npm update @sveltejs/kit @sveltejs/adapter-static
|
||||
npm update svelte
|
||||
|
||||
# Check for breaking changes
|
||||
# https://github.com/sveltejs/kit/blob/master/CHANGELOG.md
|
||||
```
|
||||
|
||||
#### React
|
||||
```bash
|
||||
# Update React
|
||||
npm update react react-dom
|
||||
|
||||
# Update related packages
|
||||
npm update @types/react @types/react-dom
|
||||
```
|
||||
|
||||
#### Vue
|
||||
```bash
|
||||
# Update Vue
|
||||
npm update vue
|
||||
|
||||
# Update related packages
|
||||
npm update @vitejs/plugin-vue
|
||||
```
|
||||
|
||||
### 5. Update Build Tools
|
||||
```bash
|
||||
# Update Vite
|
||||
npm update vite
|
||||
|
||||
# Update PWA plugin
|
||||
npm update vite-plugin-pwa @vite-pwa/sveltekit
|
||||
```
|
||||
|
||||
### 6. Test Everything
|
||||
|
||||
#### Run Development Server
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Check:
|
||||
- ✅ Server starts without errors
|
||||
- ✅ App loads correctly
|
||||
- ✅ No console errors
|
||||
- ✅ All routes work
|
||||
|
||||
#### Test Core Functionality
|
||||
- ✅ Odoo connection works
|
||||
- ✅ Data loads from cache
|
||||
- ✅ Sync works
|
||||
- ✅ CRUD operations work
|
||||
- ✅ Offline mode works
|
||||
|
||||
#### Build for Production
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
Check:
|
||||
- ✅ Build completes without errors
|
||||
- ✅ No warnings about deprecations
|
||||
- ✅ Bundle size is reasonable
|
||||
|
||||
#### Preview Production Build
|
||||
```bash
|
||||
npm run preview
|
||||
```
|
||||
|
||||
Test the same functionality in production mode.
|
||||
|
||||
### 7. Run Connection Test
|
||||
```bash
|
||||
# If /test-connection command is available
|
||||
/test-connection
|
||||
```
|
||||
|
||||
### 8. Commit Changes
|
||||
```bash
|
||||
git add package.json package-lock.json
|
||||
git commit -m "Update dependencies to latest versions"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common Issues & Solutions
|
||||
|
||||
### Issue: Build fails after update
|
||||
|
||||
**Solution:**
|
||||
```bash
|
||||
# Revert updates
|
||||
git checkout package.json package-lock.json
|
||||
npm install
|
||||
|
||||
# Try updating one package at a time
|
||||
npm update vite
|
||||
npm run build # Test
|
||||
|
||||
npm update @sveltejs/kit
|
||||
npm run build # Test
|
||||
|
||||
# Continue one by one
|
||||
```
|
||||
|
||||
### Issue: TypeScript errors
|
||||
|
||||
**Solution:**
|
||||
```bash
|
||||
# Update TypeScript
|
||||
npm update typescript
|
||||
|
||||
# Update type definitions
|
||||
npm update @types/node
|
||||
|
||||
# Regenerate tsconfig
|
||||
npx tsc --init
|
||||
```
|
||||
|
||||
### Issue: Import errors
|
||||
|
||||
**Solution:**
|
||||
```javascript
|
||||
// Old import might be:
|
||||
import { foo } from 'package';
|
||||
|
||||
// New import might be:
|
||||
import { foo } from 'package/foo';
|
||||
|
||||
// Check package's CHANGELOG for migration guide
|
||||
```
|
||||
|
||||
### Issue: Service Worker errors
|
||||
|
||||
**Solution:**
|
||||
```bash
|
||||
# Update PWA plugin
|
||||
npm update vite-plugin-pwa
|
||||
|
||||
# Check configuration
|
||||
# vite.config.js
|
||||
VitePWA({
|
||||
// Update config as needed
|
||||
})
|
||||
```
|
||||
|
||||
### Issue: Peer dependency warnings
|
||||
|
||||
**Solution:**
|
||||
```bash
|
||||
# Install peer dependencies
|
||||
npm install <missing-peer-dep>
|
||||
|
||||
# Or use --force (not recommended)
|
||||
npm install --force
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Breaking Changes to Watch For
|
||||
|
||||
### Vite 5+
|
||||
- May require Node 18+
|
||||
- ESM-only packages
|
||||
- Updated config format
|
||||
|
||||
### SvelteKit 2+
|
||||
- Svelte 5 syntax (runes)
|
||||
- Updated adapter config
|
||||
- New routing conventions
|
||||
|
||||
### React 19+
|
||||
- New JSX transform
|
||||
- Updated hooks behavior
|
||||
- Server components
|
||||
|
||||
### Vue 3.4+
|
||||
- New compiler optimizations
|
||||
- Updated Composition API
|
||||
- Better TypeScript support
|
||||
|
||||
---
|
||||
|
||||
## Update Checklist
|
||||
|
||||
After updating, verify:
|
||||
|
||||
```
|
||||
□ npm run dev works
|
||||
□ npm run build succeeds
|
||||
□ npm run preview works
|
||||
□ No console errors
|
||||
□ Odoo connection works
|
||||
□ Data syncs correctly
|
||||
□ CRUD operations work
|
||||
□ Offline mode works
|
||||
□ Service worker registered
|
||||
□ Tests pass (if any)
|
||||
□ No TypeScript errors
|
||||
□ No ESLint warnings
|
||||
□ Documentation updated
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Selective Updates
|
||||
|
||||
### Update Only Production Dependencies
|
||||
```bash
|
||||
npm update --prod
|
||||
```
|
||||
|
||||
### Update Specific Package
|
||||
```bash
|
||||
npm update vite
|
||||
npm update @sveltejs/kit
|
||||
```
|
||||
|
||||
### Update to Specific Version
|
||||
```bash
|
||||
npm install vite@5.0.0
|
||||
npm install svelte@5.0.0
|
||||
```
|
||||
|
||||
### Keep Package at Current Version
|
||||
```json
|
||||
// package.json
|
||||
{
|
||||
"dependencies": {
|
||||
"some-package": "1.2.3" // No ^ or ~ = exact version
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Update Regularly
|
||||
- Check weekly for security updates
|
||||
- Update monthly for feature updates
|
||||
- Test before deploying
|
||||
|
||||
### 2. Read Changelogs
|
||||
- Check CHANGELOG.md for breaking changes
|
||||
- Review migration guides
|
||||
- Test thoroughly
|
||||
|
||||
### 3. Update One Category at a Time
|
||||
```bash
|
||||
# 1. Framework
|
||||
npm update svelte @sveltejs/kit
|
||||
|
||||
# 2. Build tools
|
||||
npm update vite
|
||||
|
||||
# 3. Dependencies
|
||||
npm update # All others
|
||||
```
|
||||
|
||||
### 4. Keep Lock Files
|
||||
- Commit `package-lock.json`
|
||||
- Ensure consistent installs
|
||||
- Track exact versions
|
||||
|
||||
### 5. Test in Staging
|
||||
- Deploy to staging first
|
||||
- Test all functionality
|
||||
- Then deploy to production
|
||||
|
||||
---
|
||||
|
||||
## Security Updates
|
||||
|
||||
### Check for Vulnerabilities
|
||||
```bash
|
||||
npm audit
|
||||
```
|
||||
|
||||
### Fix Automatically
|
||||
```bash
|
||||
npm audit fix
|
||||
```
|
||||
|
||||
### Fix with Breaking Changes
|
||||
```bash
|
||||
npm audit fix --force
|
||||
```
|
||||
|
||||
### Review Details
|
||||
```bash
|
||||
npm audit --json
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Automated Updates (Optional)
|
||||
|
||||
### Using Dependabot (GitHub)
|
||||
Create `.github/dependabot.yml`:
|
||||
```yaml
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
open-pull-requests-limit: 10
|
||||
```
|
||||
|
||||
### Using Renovate
|
||||
Create `renovate.json`:
|
||||
```json
|
||||
{
|
||||
"extends": ["config:base"],
|
||||
"schedule": ["before 10am on monday"]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Rollback Plan
|
||||
|
||||
### If Something Breaks
|
||||
|
||||
#### Quick Rollback
|
||||
```bash
|
||||
git checkout package.json package-lock.json
|
||||
npm install
|
||||
```
|
||||
|
||||
#### Restore from Backup
|
||||
```bash
|
||||
cp package.json.backup package.json
|
||||
cp package-lock.json.backup package-lock.json
|
||||
npm install
|
||||
```
|
||||
|
||||
#### Git Reset
|
||||
```bash
|
||||
git reset --hard HEAD~1
|
||||
npm install
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## After Update
|
||||
|
||||
### Update Documentation
|
||||
- Note any breaking changes
|
||||
- Update README if needed
|
||||
- Document new features used
|
||||
|
||||
### Notify Team
|
||||
- Share what was updated
|
||||
- Note any changes in behavior
|
||||
- Update staging/production
|
||||
|
||||
### Monitor Production
|
||||
- Watch error logs
|
||||
- Check performance metrics
|
||||
- Monitor user reports
|
||||
|
||||
---
|
||||
|
||||
## Example prompts to use this command:
|
||||
- `/update-deps` - Update all dependencies
|
||||
- User: "Update my dependencies"
|
||||
- User: "Check for package updates"
|
||||
- User: "My packages are outdated"
|
||||
|
||||
## Related Commands:
|
||||
- `/fix-sync` - If sync breaks after update
|
||||
- `/test-connection` - Verify Odoo still works
|
||||
- `/troubleshoot` - Fix issues after update
|
||||
- `/optimize` - Check for optimizations after update
|
||||
|
||||
---
|
||||
|
||||
## Quick Update Script
|
||||
|
||||
Save as `update.sh`:
|
||||
```bash
|
||||
#!/bin/bash
|
||||
|
||||
echo "Backing up..."
|
||||
cp package.json package.json.backup
|
||||
cp package-lock.json package-lock.json.backup
|
||||
|
||||
echo "Checking for updates..."
|
||||
npm outdated
|
||||
|
||||
echo "Updating..."
|
||||
npm update
|
||||
|
||||
echo "Installing..."
|
||||
npm install
|
||||
|
||||
echo "Testing..."
|
||||
npm run build
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "✅ Build successful!"
|
||||
echo "Run: npm run dev to test"
|
||||
else
|
||||
echo "❌ Build failed! Rolling back..."
|
||||
cp package.json.backup package.json
|
||||
cp package-lock.json.backup package-lock.json
|
||||
npm install
|
||||
fi
|
||||
```
|
||||
|
||||
Usage:
|
||||
```bash
|
||||
chmod +x update.sh
|
||||
./update.sh
|
||||
```
|
||||
317
plugin.lock.json
Normal file
317
plugin.lock.json
Normal file
@@ -0,0 +1,317 @@
|
||||
{
|
||||
"$schema": "internal://schemas/plugin.lock.v1.json",
|
||||
"pluginId": "gh:jamshu/jamshi-marketplace:plugins/odoo-pwa-generator",
|
||||
"normalized": {
|
||||
"repo": null,
|
||||
"ref": "refs/tags/v20251128.0",
|
||||
"commit": "5a22a60b81300ee2a78ccc875705853c095f4309",
|
||||
"treeHash": "9b11048764766904a04057b43ba25a1e814d8ba8cc0ba6a060d2a5fa3f091d7b",
|
||||
"generatedAt": "2025-11-28T10:17:57.515365Z",
|
||||
"toolVersion": "publish_plugins.py@0.2.0"
|
||||
},
|
||||
"origin": {
|
||||
"remote": "git@github.com:zhongweili/42plugin-data.git",
|
||||
"branch": "master",
|
||||
"commit": "aa1497ed0949fd50e99e70d6324a29c5b34f9390",
|
||||
"repoRoot": "/Users/zhongweili/projects/openmind/42plugin-data"
|
||||
},
|
||||
"manifest": {
|
||||
"name": "odoo-pwa-generator",
|
||||
"description": "Generate offline-first Progressive Web Apps with Odoo Studio backend integration. Supports SvelteKit, React, and Vue with smart caching, IndexedDB storage, and automatic sync.",
|
||||
"version": "1.0.0"
|
||||
},
|
||||
"content": {
|
||||
"files": [
|
||||
{
|
||||
"path": "README.md",
|
||||
"sha256": "ef03b999ee46b25f1d3b2d72239e7d71f2b58ed77fc9cd3588ac501f257ee670"
|
||||
},
|
||||
{
|
||||
"path": ".claude-plugin/plugin.json",
|
||||
"sha256": "4895928ae8a33f11eebdcca2a0a282d66d40ae9af5740bd39d75a9719726e672"
|
||||
},
|
||||
{
|
||||
"path": "commands/examples.md",
|
||||
"sha256": "e140de867507ad7c2c67ebc4f2983419f6bc678b9eaf656a65bee3fd37170966"
|
||||
},
|
||||
{
|
||||
"path": "commands/update-deps.md",
|
||||
"sha256": "db7ce2f1d33b4e3be180d2a4129b2a48944034aec6c1295105949e25153c4119"
|
||||
},
|
||||
{
|
||||
"path": "commands/architecture.md",
|
||||
"sha256": "1126a8d2fc73b2697782ea656af21d6e8fe714ed0751fde16d1d6ac4bdf8abd9"
|
||||
},
|
||||
{
|
||||
"path": "commands/troubleshoot.md",
|
||||
"sha256": "8461b27091eca54d44425f64a64036718098b0020604867850dde13424362811"
|
||||
},
|
||||
{
|
||||
"path": "commands/deploy-github.md",
|
||||
"sha256": "4d1c015430ef38ae6512fdd726d9bae2e6f65f4c678b75c05274d2b59a2d4224"
|
||||
},
|
||||
{
|
||||
"path": "commands/setup-env.md",
|
||||
"sha256": "e46e36fc6897149548a3f1803a6a2bf875469eca8d5874c4097c8ebb692cb097"
|
||||
},
|
||||
{
|
||||
"path": "commands/clear-cache.md",
|
||||
"sha256": "57b445c3689e9aa34cbc3de708ee373c2f22575e42214566ef2e67215d505b9d"
|
||||
},
|
||||
{
|
||||
"path": "commands/add-deployment.md",
|
||||
"sha256": "65a4344771062f817fac5afc78377f9aaa2d74c25846334dba02dca9964ca6b3"
|
||||
},
|
||||
{
|
||||
"path": "commands/help.md",
|
||||
"sha256": "401ec2e63f86494dbeed65c79aac1ca9ba7e088203e9dc8ffd3954b86709a05b"
|
||||
},
|
||||
{
|
||||
"path": "commands/new-svelte-pwa.md",
|
||||
"sha256": "6a3eb3535714f34c60c5ffc585fab1c96cd4e6f1849b8deee6c459e0788ec361"
|
||||
},
|
||||
{
|
||||
"path": "commands/fix-sync.md",
|
||||
"sha256": "e6ebc3c57ffc6186640270a6ddd57bf91060b6270a85b900956846c086d9ad13"
|
||||
},
|
||||
{
|
||||
"path": "commands/init-project.md",
|
||||
"sha256": "35585e5b456a9f9bb58e07bc56121942649d0d0db9092fb21a1eadcaf3884c33"
|
||||
},
|
||||
{
|
||||
"path": "commands/optimize.md",
|
||||
"sha256": "0a8f5de1577f6f5e4d844fe247656ff62c559ca40d5b8a6badeafc7348600c65"
|
||||
},
|
||||
{
|
||||
"path": "commands/test-connection.md",
|
||||
"sha256": "001116c2c4c6d05c9feb4c4976004ad5949b92dc409275a45546b788e7791ab7"
|
||||
},
|
||||
{
|
||||
"path": "commands/deploy-vercel.md",
|
||||
"sha256": "0d82d6cf2553b611c8e00ab327368051cc6cf9a1a402bd03b33fe7872f5ef35d"
|
||||
},
|
||||
{
|
||||
"path": "commands/new-react-pwa.md",
|
||||
"sha256": "b762aec24b9fdcb1a01eecb257f420cb75f6988f5414bd520ebd1758ffbb0278"
|
||||
},
|
||||
{
|
||||
"path": "commands/create-cache-store.md",
|
||||
"sha256": "8eb3a44c1f8f143542541c48c38b3e4205bebc4d4f1d2ebcaa1c7a05f43bab79"
|
||||
},
|
||||
{
|
||||
"path": "commands/new-vue-pwa.md",
|
||||
"sha256": "80134258e0aa6978bcd1723ef56e3303451953d6ecc0e41b604cee310f695134"
|
||||
},
|
||||
{
|
||||
"path": "commands/api-reference.md",
|
||||
"sha256": "c0cca235884ed3064c68d597a058115b9fd8cc2c977f7fc81a8c429cc1e1d6dd"
|
||||
},
|
||||
{
|
||||
"path": "commands/add-model.md",
|
||||
"sha256": "86b0bc93c3dfb08a8369adc9f64c551bd0975fd32ceedb7ba3fada71e9faef60"
|
||||
},
|
||||
{
|
||||
"path": "skills/add-odoo-model/SKILL.md",
|
||||
"sha256": "99b766d9b01e5c92293d9c0b585c748060192a97331f1a33066fc6f0abba09ba"
|
||||
},
|
||||
{
|
||||
"path": "skills/create-odoo-pwa/SKILL.md",
|
||||
"sha256": "9eeee7636a24f9051b1441b3f4f14ee0c4ab6b89eb571100912ab6a18c652575"
|
||||
},
|
||||
{
|
||||
"path": "skills/create-odoo-pwa/templates/vue/base/index.html.template",
|
||||
"sha256": "7ee408b732e99165bd567a9f363b586be6b2a4cd6af56db40f7db90345861a80"
|
||||
},
|
||||
{
|
||||
"path": "skills/create-odoo-pwa/templates/vue/base/.gitignore.template",
|
||||
"sha256": "e6e6490c928492f52d02a247361a98b061d7c6bb929bfde885cd9de760826b13"
|
||||
},
|
||||
{
|
||||
"path": "skills/create-odoo-pwa/templates/vue/base/.env.example.template",
|
||||
"sha256": "a37594d8960fb1274c8163c25b285e7e6530a43f65ff8035805196167eec8be2"
|
||||
},
|
||||
{
|
||||
"path": "skills/create-odoo-pwa/templates/vue/base/package.json.template",
|
||||
"sha256": "2c9d9a42b41d58bb03d3465879462f76e088a11cd9571932f3a8eee285c1c44c"
|
||||
},
|
||||
{
|
||||
"path": "skills/create-odoo-pwa/templates/vue/base/vite.config.js.template",
|
||||
"sha256": "3a97a9170c8a96568dcbd2eb58c96e48e549f99b4d74c5d6fbe8affdf4ce25d1"
|
||||
},
|
||||
{
|
||||
"path": "skills/create-odoo-pwa/templates/vue/src/App.vue.template",
|
||||
"sha256": "dc371a60731c01a3ceb208c0129b3bb0aff9884704da4a5f7384cf6afb1b81e7"
|
||||
},
|
||||
{
|
||||
"path": "skills/create-odoo-pwa/templates/vue/src/style.css.template",
|
||||
"sha256": "991f6f22fdea623349431e7fac4c1936efd7c5d6c506a1819e7818c2cb475420"
|
||||
},
|
||||
{
|
||||
"path": "skills/create-odoo-pwa/templates/vue/src/main.js.template",
|
||||
"sha256": "8ef1f40fcd7ced0ce98adafc627e7b612994db9977b1c6cec0ac3067a0324c7a"
|
||||
},
|
||||
{
|
||||
"path": "skills/create-odoo-pwa/templates/vue/src/composables/useTaskCache.js.template",
|
||||
"sha256": "b3a7432895bda00cd3ff155c69ca8303bf4815871dc6be8401d14d59ff8a44d7"
|
||||
},
|
||||
{
|
||||
"path": "skills/create-odoo-pwa/templates/vue/src/components/OfflineBanner.vue.template",
|
||||
"sha256": "d78bc155fdc9f1092c80719116d9e43d5cea53814a0a107191bf66f78d88277d"
|
||||
},
|
||||
{
|
||||
"path": "skills/create-odoo-pwa/templates/vue/src/api/odoo.js.template",
|
||||
"sha256": "319c133ea305e1f4beffb2040a7b0ed8b73c9f2d1ca9fe5c5af0383aead985e4"
|
||||
},
|
||||
{
|
||||
"path": "skills/create-odoo-pwa/templates/vue/src/pages/AddPage.vue.template",
|
||||
"sha256": "466a6e49d6a5f62df3eae5aca06ed1179e5c6c3a9f8fc66cba3c6aaf20e40d75"
|
||||
},
|
||||
{
|
||||
"path": "skills/create-odoo-pwa/templates/vue/src/pages/ListPage.vue.template",
|
||||
"sha256": "b4edaebaaa80abeeb6ca554f121344d15140223c7148e071a7b1f02c9a5f0c02"
|
||||
},
|
||||
{
|
||||
"path": "skills/create-odoo-pwa/templates/sveltekit/docs/README.md.template",
|
||||
"sha256": "e4422ca8c0f271120db4fdb098e14e6e7c7e15bb4159c1154e321178419dade6"
|
||||
},
|
||||
{
|
||||
"path": "skills/create-odoo-pwa/templates/sveltekit/docs/CLAUDE.md.template",
|
||||
"sha256": "a0f12c0f571d948ff4f06d662f3e0896f1a6f60ba78d6ccf6958b83708745011"
|
||||
},
|
||||
{
|
||||
"path": "skills/create-odoo-pwa/templates/sveltekit/static/manifest.json.template",
|
||||
"sha256": "8f90232388985278249df5a00661e1b14522458db4af314d00e77320e865a624"
|
||||
},
|
||||
{
|
||||
"path": "skills/create-odoo-pwa/templates/sveltekit/deployment/vercel.json.template",
|
||||
"sha256": "65537c0d3e47092c1347a3924208a96f747205274c384edc97ba9868095caab4"
|
||||
},
|
||||
{
|
||||
"path": "skills/create-odoo-pwa/templates/sveltekit/deployment/github-actions.yml.template",
|
||||
"sha256": "16f0a42030fe74893cfeffc42f12a499cf62e05c3fa382932d595fa9240b971c"
|
||||
},
|
||||
{
|
||||
"path": "skills/create-odoo-pwa/templates/sveltekit/lib/utils.js.template",
|
||||
"sha256": "337a6cbda073f0a172f55e4f7d7dbcc56c1a8a837856d95de57431bfeecfdafe"
|
||||
},
|
||||
{
|
||||
"path": "skills/create-odoo-pwa/templates/sveltekit/lib/odoo.js.template",
|
||||
"sha256": "cbf095ee000861d15a13d5b23a7298a3f0eae53e09c91d3d59d756fc5c1858c8"
|
||||
},
|
||||
{
|
||||
"path": "skills/create-odoo-pwa/templates/sveltekit/lib/db.js.template",
|
||||
"sha256": "4a00e2dcc2271eef4c565bdf73b85819d50a44ec205788c84d5a9e16be93602c"
|
||||
},
|
||||
{
|
||||
"path": "skills/create-odoo-pwa/templates/sveltekit/lib/stores/cache.js.template",
|
||||
"sha256": "0292bf52aaac01af04822d154d03eec3b5ea00aaf66eddadb75ddb60481524ee"
|
||||
},
|
||||
{
|
||||
"path": "skills/create-odoo-pwa/templates/sveltekit/routes/list/+page.svelte.template",
|
||||
"sha256": "0bb94af459fa4760ab7e204f6a662297e8991c806e73da33fa47977770d54a98"
|
||||
},
|
||||
{
|
||||
"path": "skills/create-odoo-pwa/templates/sveltekit/routes/root/+layout.svelte.template",
|
||||
"sha256": "3fd8d0e4ea5eac6f952699eec2fafe89b207d7ea4006a8fb85c9b9f876145117"
|
||||
},
|
||||
{
|
||||
"path": "skills/create-odoo-pwa/templates/sveltekit/routes/root/+layout.js.template",
|
||||
"sha256": "f85e6d24b47d64a5f21dc397706ff1e118d23eadb1ab65da8b6ae11e7a1ed672"
|
||||
},
|
||||
{
|
||||
"path": "skills/create-odoo-pwa/templates/sveltekit/routes/root/+page.svelte.template",
|
||||
"sha256": "29cefe347404b327b55e043017f06a7bf0505452c77636bfc9f7dd0531b89acf"
|
||||
},
|
||||
{
|
||||
"path": "skills/create-odoo-pwa/templates/sveltekit/routes/api/odoo/+server.js.template",
|
||||
"sha256": "c919559346017e1bbdfdef8d71671e194d0915e998430740177feeee944665ca"
|
||||
},
|
||||
{
|
||||
"path": "skills/create-odoo-pwa/templates/sveltekit/base/jsconfig.json.template",
|
||||
"sha256": "9b8291d8f3570eee2faf4c3078f465278aa59ca7fbffe042231c4f268cf2beb4"
|
||||
},
|
||||
{
|
||||
"path": "skills/create-odoo-pwa/templates/sveltekit/base/.gitignore.template",
|
||||
"sha256": "5b738b6a9aef748f85e1ee779fd04d115777e112f7532635d0b06d3b6fb763bf"
|
||||
},
|
||||
{
|
||||
"path": "skills/create-odoo-pwa/templates/sveltekit/base/.env.example.template",
|
||||
"sha256": "1d3c65be859aba390caccc4434f869a8a3d9024de4b04ca8d831e008ab274d64"
|
||||
},
|
||||
{
|
||||
"path": "skills/create-odoo-pwa/templates/sveltekit/base/package.json.template",
|
||||
"sha256": "7e360709bf19cb20f1f74c6edec053394e705b00a65f49de1bce8c9435f2ee6a"
|
||||
},
|
||||
{
|
||||
"path": "skills/create-odoo-pwa/templates/sveltekit/base/svelte.config.js.template",
|
||||
"sha256": "d6b70c65b52e045ea9e034e5e81f7dbbc8461e802fcb366a4dd5f2d6f78801f9"
|
||||
},
|
||||
{
|
||||
"path": "skills/create-odoo-pwa/templates/sveltekit/base/vite.config.js.template",
|
||||
"sha256": "c34481be0af763f357c2177b794e52b773ffa13589b8fc46ea98e172862427a1"
|
||||
},
|
||||
{
|
||||
"path": "skills/create-odoo-pwa/templates/react/base/index.html.template",
|
||||
"sha256": "5bb0a55917570737010e6411ebd5981abab1ca87e0c5ae505658165b8889974a"
|
||||
},
|
||||
{
|
||||
"path": "skills/create-odoo-pwa/templates/react/base/.gitignore.template",
|
||||
"sha256": "4b32e84abb8c51d6f119b494f0f760b85c754686920b731a74dbac46262c9975"
|
||||
},
|
||||
{
|
||||
"path": "skills/create-odoo-pwa/templates/react/base/.env.example.template",
|
||||
"sha256": "a37594d8960fb1274c8163c25b285e7e6530a43f65ff8035805196167eec8be2"
|
||||
},
|
||||
{
|
||||
"path": "skills/create-odoo-pwa/templates/react/base/package.json.template",
|
||||
"sha256": "fdb850ea90165735240548675cb98649f10ed2c4c6926a72b0ee710554fee7cc"
|
||||
},
|
||||
{
|
||||
"path": "skills/create-odoo-pwa/templates/react/base/vite.config.js.template",
|
||||
"sha256": "0817516a855a3dce4f27d99dbbde48f8e7a8de85033c8703d160addc25a5118c"
|
||||
},
|
||||
{
|
||||
"path": "skills/create-odoo-pwa/templates/react/src/App.css.template",
|
||||
"sha256": "2ddbf5fdd9d6fc317ceb6b79e135e92baa19c20b3b071b014ef0b581af07948b"
|
||||
},
|
||||
{
|
||||
"path": "skills/create-odoo-pwa/templates/react/src/main.jsx.template",
|
||||
"sha256": "5e6a57f0d1a6a2c4165d99d69545a0a63c20f3da541c913afac249922dda4771"
|
||||
},
|
||||
{
|
||||
"path": "skills/create-odoo-pwa/templates/react/src/index.css.template",
|
||||
"sha256": "370b1bc983221cdbf0f1c7930c919dc89353f37991b7468e4014fb6365991b0a"
|
||||
},
|
||||
{
|
||||
"path": "skills/create-odoo-pwa/templates/react/src/App.jsx.template",
|
||||
"sha256": "3358e0f975acf703fd5387678d39e6495e148c94bb3fc87837df51ea71d5b5aa"
|
||||
},
|
||||
{
|
||||
"path": "skills/create-odoo-pwa/templates/react/src/context/TaskContext.jsx.template",
|
||||
"sha256": "6691f5c18ea894ceddfa03bc38306f466f2d31b8c3b931ce97316f43ca4f0e1f"
|
||||
},
|
||||
{
|
||||
"path": "skills/create-odoo-pwa/templates/react/src/components/OfflineBanner.jsx.template",
|
||||
"sha256": "3a6c980142d8173898759450529244d321c2b9550c6fda52fca94e9d264b48ab"
|
||||
},
|
||||
{
|
||||
"path": "skills/create-odoo-pwa/templates/react/src/api/odoo.js.template",
|
||||
"sha256": "319c133ea305e1f4beffb2040a7b0ed8b73c9f2d1ca9fe5c5af0383aead985e4"
|
||||
},
|
||||
{
|
||||
"path": "skills/create-odoo-pwa/templates/react/src/pages/ListPage.jsx.template",
|
||||
"sha256": "3e63a08e04d2eed445028b4fb16195b24f384339757835a6166e5cb5db15d5b3"
|
||||
},
|
||||
{
|
||||
"path": "skills/create-odoo-pwa/templates/react/src/pages/AddPage.jsx.template",
|
||||
"sha256": "f94e5683647e7189751843ef5e4a4181536645ad5d93a0087e4d2261ccf76004"
|
||||
}
|
||||
],
|
||||
"dirSha256": "9b11048764766904a04057b43ba25a1e814d8ba8cc0ba6a060d2a5fa3f091d7b"
|
||||
},
|
||||
"security": {
|
||||
"scannedAt": null,
|
||||
"scannerVersion": null,
|
||||
"flags": []
|
||||
}
|
||||
}
|
||||
204
skills/add-odoo-model/SKILL.md
Normal file
204
skills/add-odoo-model/SKILL.md
Normal file
@@ -0,0 +1,204 @@
|
||||
---
|
||||
name: add-odoo-model
|
||||
description: Add integration for an additional Odoo Studio model to an existing Odoo PWA project. Use when user wants to add support for another model, mentions "add new model", "integrate another Odoo model", or similar.
|
||||
allowed-tools: Read, Write, Edit, Glob
|
||||
---
|
||||
|
||||
# Add Odoo Model Integration
|
||||
|
||||
Add a new Odoo model integration to an existing Odoo PWA project, creating cache stores, API methods, and UI components.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Existing Odoo PWA project (generated with create-odoo-pwa skill)
|
||||
- New Odoo Studio model created with `x_` prefix
|
||||
- Model name and display name from user
|
||||
|
||||
## Required User Input
|
||||
|
||||
Ask the user for:
|
||||
|
||||
1. **Model name** (required)
|
||||
- Format: without `x_` prefix (e.g., "inventory", "tasks")
|
||||
- Example: If Odoo model is `x_inventory`, user provides: `inventory`
|
||||
|
||||
2. **Model display name** (required)
|
||||
- Human-readable singular name (e.g., "Inventory Item", "Task")
|
||||
|
||||
3. **Create UI pages** (optional)
|
||||
- Ask if user wants to generate form and list pages
|
||||
- Default: yes
|
||||
|
||||
## Detection Steps
|
||||
|
||||
Before generating, detect the project structure:
|
||||
|
||||
1. **Detect framework**:
|
||||
- Check for `svelte.config.js` → SvelteKit
|
||||
- Check for `vite.config.ts` with React → React
|
||||
- Check for `nuxt.config.ts` → Vue/Nuxt
|
||||
|
||||
2. **Find existing files**:
|
||||
- Locate `src/lib/odoo.js` (or equivalent)
|
||||
- Find existing cache stores in `src/lib/stores/`
|
||||
- Check routes structure
|
||||
|
||||
3. **Verify Odoo connection**:
|
||||
- Check `.env` file has ODOO_URL and credentials
|
||||
|
||||
## Generation Steps
|
||||
|
||||
### Step 1: Create Cache Store
|
||||
|
||||
Generate `src/lib/stores/{{MODEL_NAME}}Cache.js`:
|
||||
|
||||
- Based on existing cache store pattern
|
||||
- Replace model name throughout
|
||||
- Update fields array with model-specific fields
|
||||
- Include CRUD methods
|
||||
|
||||
### Step 2: Update Odoo API Client
|
||||
|
||||
Add model-specific methods to `src/lib/odoo.js`:
|
||||
|
||||
```javascript
|
||||
/**
|
||||
* Fetch {{MODEL_DISPLAY_NAME}} records
|
||||
*/
|
||||
async fetch{{MODEL_NAME|capitalize}}s(domain = [], fields = []) {
|
||||
return await this.searchRecords('x_{{MODEL_NAME}}', domain, fields);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create {{MODEL_DISPLAY_NAME}}
|
||||
*/
|
||||
async create{{MODEL_NAME|capitalize}}(fields) {
|
||||
return await this.createRecord('x_{{MODEL_NAME}}', fields);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update {{MODEL_DISPLAY_NAME}}
|
||||
*/
|
||||
async update{{MODEL_NAME|capitalize}}(id, values) {
|
||||
return await this.updateRecord('x_{{MODEL_NAME}}', id, values);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete {{MODEL_DISPLAY_NAME}}
|
||||
*/
|
||||
async delete{{MODEL_NAME|capitalize}}(id) {
|
||||
return await this.deleteRecord('x_{{MODEL_NAME}}', id);
|
||||
}
|
||||
```
|
||||
|
||||
### Step 3: Create UI Pages (if requested)
|
||||
|
||||
#### Add Form Page: `src/routes/{{MODEL_NAME}}/+page.svelte`
|
||||
|
||||
Generate form component:
|
||||
- Import cache store
|
||||
- Form fields for model
|
||||
- Handle offline/online states
|
||||
- Submit handler with validation
|
||||
|
||||
#### List Page: `src/routes/{{MODEL_NAME}}/list/+page.svelte`
|
||||
|
||||
Generate list component:
|
||||
- Display records in table/card format
|
||||
- Search/filter functionality
|
||||
- Delete actions
|
||||
- Sync status
|
||||
|
||||
### Step 4: Update Navigation
|
||||
|
||||
Update navigation in main layout or existing pages:
|
||||
|
||||
```svelte
|
||||
<nav>
|
||||
<!-- Existing links -->
|
||||
<a href="/{{MODEL_NAME}}">{{MODEL_DISPLAY_NAME}}s</a>
|
||||
</nav>
|
||||
```
|
||||
|
||||
### Step 5: Update Environment Variables
|
||||
|
||||
Add to `.env.example` (if needed):
|
||||
```env
|
||||
# {{MODEL_DISPLAY_NAME}} Model
|
||||
ODOO_{{MODEL_NAME|uppercase}}_MODEL=x_{{MODEL_NAME}}
|
||||
```
|
||||
|
||||
## Post-Generation Instructions
|
||||
|
||||
Provide user with:
|
||||
|
||||
```
|
||||
✅ {{MODEL_DISPLAY_NAME}} integration added successfully!
|
||||
|
||||
📋 Next Steps:
|
||||
|
||||
1. Verify Odoo Model Setup:
|
||||
- Model name: x_{{MODEL_NAME}}
|
||||
- Add custom fields with x_studio_ prefix in Odoo Studio
|
||||
|
||||
2. Update Cache Store:
|
||||
- Edit src/lib/stores/{{MODEL_NAME}}Cache.js
|
||||
- Add all model fields to the 'fields' array
|
||||
|
||||
3. Customize UI:
|
||||
- Edit src/routes/{{MODEL_NAME}}/+page.svelte for form
|
||||
- Edit src/routes/{{MODEL_NAME}}/list/+page.svelte for list view
|
||||
- Add model-specific fields and validation
|
||||
|
||||
4. Test Integration:
|
||||
npm run dev
|
||||
- Navigate to /{{MODEL_NAME}}
|
||||
- Test create, read, update, delete operations
|
||||
- Verify offline functionality
|
||||
|
||||
📚 Model-Specific Files Created:
|
||||
- src/lib/stores/{{MODEL_NAME}}Cache.js - Cache and sync logic
|
||||
- src/routes/{{MODEL_NAME}}/+page.svelte - Add form
|
||||
- src/routes/{{MODEL_NAME}}/list/+page.svelte - List view
|
||||
|
||||
🔗 Access:
|
||||
- Add: http://localhost:5173/{{MODEL_NAME}}
|
||||
- List: http://localhost:5173/{{MODEL_NAME}}/list
|
||||
```
|
||||
|
||||
## Framework-Specific Notes
|
||||
|
||||
### SvelteKit
|
||||
- Use Svelte 5 syntax with `$state`, `$derived`, `$effect`
|
||||
- Cache stores use Svelte stores pattern
|
||||
- Routes in `src/routes/`
|
||||
|
||||
### React
|
||||
- Use React hooks (useState, useEffect)
|
||||
- Context API for cache
|
||||
- Routes configuration depends on router (React Router, etc.)
|
||||
|
||||
### Vue
|
||||
- Use Vue 3 Composition API
|
||||
- Composables for cache logic
|
||||
- Routes in `src/pages/` or as configured
|
||||
|
||||
## Error Handling
|
||||
|
||||
If generation fails:
|
||||
- Verify project has Odoo PWA structure
|
||||
- Check for existing odoo.js file
|
||||
- Ensure proper permissions for file creation
|
||||
- Provide clear error messages
|
||||
|
||||
## Examples
|
||||
|
||||
User: "Add inventory model to track items"
|
||||
- Model name: inventory
|
||||
- Display name: Inventory Item
|
||||
- Creates: inventoryCache.js, /inventory pages, API methods
|
||||
|
||||
User: "Integrate task management"
|
||||
- Model name: task
|
||||
- Display name: Task
|
||||
- Creates: taskCache.js, /task pages, API methods
|
||||
411
skills/create-odoo-pwa/SKILL.md
Normal file
411
skills/create-odoo-pwa/SKILL.md
Normal file
@@ -0,0 +1,411 @@
|
||||
---
|
||||
name: create-odoo-pwa
|
||||
description: Generate an offline-first Progressive Web App with Odoo Studio backend integration. Use when user wants to create new Odoo-backed application, mentions "PWA with Odoo", "offline Odoo app", "Odoo Studio PWA", or similar terms. Supports SvelteKit, React, and Vue frameworks.
|
||||
allowed-tools: Read, Write, Glob, Bash
|
||||
---
|
||||
|
||||
# Create Odoo PWA Application
|
||||
|
||||
Generate a production-ready Progressive Web App with Odoo Studio backend, featuring offline-first architecture, smart caching, and automatic synchronization.
|
||||
|
||||
## Before You Start
|
||||
|
||||
This skill generates a complete PWA project following proven architectural patterns:
|
||||
- **Three-layer data flow**: Component → Cache Store → API Client → Server Route → Odoo
|
||||
- **Offline-first**: IndexedDB/localStorage with background sync
|
||||
- **Smart caching**: Incremental fetch, stale detection, optimistic updates
|
||||
- **PWA-ready**: Service workers, manifest, installable
|
||||
|
||||
## Required User Input
|
||||
|
||||
Ask the user for the following information before generating:
|
||||
|
||||
1. **Project name** (required)
|
||||
- Format: kebab-case (e.g., "inventory-tracker", "expense-manager")
|
||||
- Used for directory name and package.json
|
||||
|
||||
2. **Framework** (required)
|
||||
- Options: `sveltekit` (recommended), `react`, `vue`
|
||||
- Default: sveltekit if not specified
|
||||
|
||||
3. **Primary Odoo model** (required)
|
||||
- The main custom model name WITHOUT the `x_` prefix
|
||||
- Example: If Odoo model is `x_inventory`, user provides: `inventory`
|
||||
- Will automatically add `x_` prefix in code
|
||||
|
||||
4. **Model display name** (required)
|
||||
- Human-readable singular name (e.g., "Inventory Item", "Expense")
|
||||
|
||||
5. **Deployment target** (optional)
|
||||
- Options: `vercel`, `github-pages`, `cloudflare`, `netlify`
|
||||
- Default: vercel if not specified
|
||||
|
||||
## Generation Steps
|
||||
|
||||
### Step 1: Project Initialization
|
||||
|
||||
Create the project directory and initialize the structure:
|
||||
|
||||
```bash
|
||||
mkdir {{PROJECT_NAME}}
|
||||
cd {{PROJECT_NAME}}
|
||||
```
|
||||
|
||||
Generate the appropriate structure based on framework:
|
||||
- **SvelteKit**: Use SvelteKit 2.x structure with `src/` directory
|
||||
- **React**: Use Vite + React structure
|
||||
- **Vue**: Use Vite + Vue structure
|
||||
|
||||
### Step 2: Base Configuration Files
|
||||
|
||||
Generate these files using templates from `skills/create-odoo-pwa/templates/{{FRAMEWORK}}/base/`:
|
||||
|
||||
#### For SvelteKit:
|
||||
- `package.json` - Dependencies including @sveltejs/kit, @vite-pwa/sveltekit, @sveltejs/adapter-static
|
||||
- `svelte.config.js` - SvelteKit configuration with adapter-static
|
||||
- `vite.config.js` - Vite + PWA plugin configuration
|
||||
- `jsconfig.json` or `tsconfig.json` - Path aliases and compiler options
|
||||
|
||||
#### For React:
|
||||
- `package.json` - Dependencies including React 18, Vite, vite-plugin-pwa
|
||||
- `vite.config.js` - React + PWA plugin configuration
|
||||
- `tsconfig.json` - TypeScript configuration
|
||||
|
||||
#### For Vue:
|
||||
- `package.json` - Dependencies including Vue 3, Vite, vite-plugin-pwa
|
||||
- `vite.config.js` - Vue + PWA plugin configuration
|
||||
- `tsconfig.json` - TypeScript configuration
|
||||
|
||||
### Step 3: Environment and Git Configuration
|
||||
|
||||
Create `.env.example`:
|
||||
```env
|
||||
# Odoo Instance Configuration
|
||||
ODOO_URL=https://your-instance.odoo.com
|
||||
ODOO_DB=your-database-name
|
||||
ODOO_USERNAME=your-username
|
||||
ODOO_API_KEY=your-api-key
|
||||
|
||||
# Primary Model (use x_ prefix)
|
||||
ODOO_PRIMARY_MODEL=x_{{MODEL_NAME}}
|
||||
|
||||
# Optional: For static hosting (GitHub Pages, etc.)
|
||||
PUBLIC_API_URL=
|
||||
```
|
||||
|
||||
Create `.gitignore`:
|
||||
```
|
||||
node_modules/
|
||||
.env
|
||||
dist/
|
||||
build/
|
||||
.svelte-kit/
|
||||
.vercel/
|
||||
.DS_Store
|
||||
*.log
|
||||
```
|
||||
|
||||
### Step 4: Core Library Files
|
||||
|
||||
Generate these essential files from templates:
|
||||
|
||||
#### A. Odoo API Client (`src/lib/odoo.js`)
|
||||
Features:
|
||||
- API URL configuration (supports PUBLIC_API_URL for static hosts)
|
||||
- `callApi(action, data)` - Core API communication
|
||||
- `createRecord(model, fields)` - Create records
|
||||
- `searchRecords(model, domain, fields)` - Search/read records
|
||||
- `updateRecord(model, id, values)` - Update records
|
||||
- `deleteRecord(model, id)` - Delete records
|
||||
- `formatMany2one(id)` - Format single relation fields
|
||||
- `formatMany2many(ids)` - Format multi-relation fields
|
||||
- Model-specific convenience methods
|
||||
|
||||
#### B. IndexedDB Manager (`src/lib/db.js`)
|
||||
Features:
|
||||
- Database initialization with versioning
|
||||
- Store definitions for master data (partners, categories, config)
|
||||
- CRUD operations: `add()`, `get()`, `getAll()`, `update()`, `remove()`
|
||||
- Transaction helpers
|
||||
- Error handling
|
||||
|
||||
#### C. Smart Cache Store (`src/lib/stores/{{MODEL_NAME}}Cache.js`)
|
||||
Features:
|
||||
- Framework-specific store pattern (Svelte store/React context/Vue composable)
|
||||
- Dual storage strategy (localStorage for metadata, IndexedDB for master data)
|
||||
- `initialize()` - Load cache and start background sync
|
||||
- `sync(forceFullRefresh)` - Incremental sync with Odoo
|
||||
- `forceRefresh()` - Clear cache and full sync
|
||||
- Partner name resolution with caching
|
||||
- Optimistic UI updates
|
||||
- Stale detection (5-minute cache validity)
|
||||
- Background sync (3-minute intervals)
|
||||
- Derived stores for UI states
|
||||
|
||||
#### D. Utility Functions (`src/lib/{{MODEL_NAME}}Utils.js`)
|
||||
Features:
|
||||
- Business logic calculations
|
||||
- Data normalization helpers
|
||||
- Field formatters
|
||||
|
||||
### Step 5: Server-Side API Proxy
|
||||
|
||||
#### For SvelteKit: `src/routes/api/odoo/+server.js`
|
||||
Features:
|
||||
- JSON-RPC client for Odoo
|
||||
- UID caching to reduce auth calls
|
||||
- `execute(model, method, args, kwargs)` helper
|
||||
- POST request handler with actions:
|
||||
- `create` - Create new record
|
||||
- `search` - Search and read records
|
||||
- `search_model` - Search any Odoo model
|
||||
- `update` - Update existing record
|
||||
- `delete` - Delete record
|
||||
- Error handling with descriptive messages
|
||||
- Environment variable access
|
||||
|
||||
#### For React/Vue: `src/api/odoo.js` (server endpoint)
|
||||
Similar functionality adapted to framework conventions
|
||||
|
||||
### Step 6: UI Components and Routes
|
||||
|
||||
Generate starter components and pages:
|
||||
|
||||
#### SvelteKit Routes:
|
||||
- `src/routes/+layout.svelte` - Root layout with navigation
|
||||
- `src/routes/+layout.js` - SSR/CSR configuration (ssr: false, csr: true)
|
||||
- `src/routes/+page.svelte` - Main form for creating records
|
||||
- `src/routes/list/+page.svelte` - List/table view with filtering
|
||||
- `src/app.html` - HTML template with PWA meta tags
|
||||
|
||||
#### React/Vue Pages:
|
||||
Equivalent component structure adapted to framework conventions
|
||||
|
||||
#### Shared Components:
|
||||
- `OfflineBanner` - Shows online/offline status
|
||||
- `SyncStatus` - Displays sync state and last sync time
|
||||
- `LoadingSpinner` - Loading indicator
|
||||
- Form components with offline support
|
||||
|
||||
### Step 7: PWA Configuration
|
||||
|
||||
Generate PWA files:
|
||||
|
||||
#### `static/manifest.json` (or `public/manifest.json`):
|
||||
```json
|
||||
{
|
||||
"name": "{{PROJECT_NAME}}",
|
||||
"short_name": "{{PROJECT_NAME}}",
|
||||
"description": "{{MODEL_DISPLAY_NAME}} management app",
|
||||
"start_url": "/",
|
||||
"display": "standalone",
|
||||
"background_color": "#ffffff",
|
||||
"theme_color": "#667eea",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/icon-192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/icon-512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Configure service worker in `vite.config.js`:
|
||||
- Auto-update strategy
|
||||
- Cache all static assets
|
||||
- Offline support
|
||||
|
||||
### Step 8: Deployment Configuration
|
||||
|
||||
Generate deployment files based on target:
|
||||
|
||||
#### Vercel (`vercel.json`):
|
||||
```json
|
||||
{
|
||||
"buildCommand": "npm run build",
|
||||
"outputDirectory": "build",
|
||||
"framework": "sveltekit",
|
||||
"regions": ["iad1"]
|
||||
}
|
||||
```
|
||||
|
||||
#### GitHub Pages (`.github/workflows/deploy.yml`):
|
||||
```yaml
|
||||
name: Deploy to GitHub Pages
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
- run: npm ci
|
||||
- run: npm run build
|
||||
- uses: actions/upload-pages-artifact@v2
|
||||
with:
|
||||
path: build
|
||||
```
|
||||
|
||||
#### Cloudflare/Netlify:
|
||||
Generate appropriate configuration files
|
||||
|
||||
### Step 9: Documentation
|
||||
|
||||
Generate comprehensive documentation:
|
||||
|
||||
#### `README.md`:
|
||||
- Project overview
|
||||
- Prerequisites (Node.js, Odoo account)
|
||||
- Installation steps
|
||||
- Odoo Studio model setup instructions
|
||||
- Development commands
|
||||
- Deployment guide
|
||||
- Architecture overview
|
||||
|
||||
#### `CLAUDE.md`:
|
||||
Complete architecture documentation following the expense-split-pwa pattern:
|
||||
- Project overview
|
||||
- Development commands
|
||||
- Environment setup
|
||||
- Architecture diagrams
|
||||
- Key architectural patterns
|
||||
- Odoo model structure
|
||||
- Important development notes
|
||||
- Common gotchas
|
||||
|
||||
#### `API.md`:
|
||||
- Odoo integration patterns
|
||||
- Available API methods
|
||||
- Field formatting examples
|
||||
- Common operations
|
||||
|
||||
### Step 10: Post-Generation Instructions
|
||||
|
||||
After generating all files, provide the user with:
|
||||
|
||||
```
|
||||
✅ Project '{{PROJECT_NAME}}' generated successfully!
|
||||
|
||||
📋 Next Steps:
|
||||
|
||||
1. Navigate to the project:
|
||||
cd {{PROJECT_NAME}}
|
||||
|
||||
2. Install dependencies:
|
||||
npm install
|
||||
|
||||
3. Configure Odoo credentials:
|
||||
cp .env.example .env
|
||||
# Edit .env with your Odoo instance details
|
||||
|
||||
4. Create Odoo Studio Model:
|
||||
- Log into your Odoo instance
|
||||
- Go to Studio
|
||||
- Create a new model named: x_{{MODEL_NAME}}
|
||||
- Add custom fields with x_studio_ prefix
|
||||
- Example fields:
|
||||
* x_name (Char) - Required
|
||||
* x_studio_description (Text)
|
||||
* x_studio_value (Float)
|
||||
* x_studio_date (Date)
|
||||
* x_studio_category (Many2one → res.partner or custom)
|
||||
|
||||
5. Start development server:
|
||||
npm run dev
|
||||
|
||||
6. Generate PWA icons:
|
||||
- Create 192x192 and 512x512 PNG icons
|
||||
- Place in static/ or public/ directory
|
||||
- Name them icon-192.png and icon-512.png
|
||||
|
||||
7. Deploy (optional):
|
||||
- Vercel: vercel
|
||||
- GitHub Pages: Push to main branch
|
||||
- Cloudflare: wrangler deploy
|
||||
- Netlify: netlify deploy
|
||||
|
||||
📚 Documentation:
|
||||
- README.md - Getting started guide
|
||||
- CLAUDE.md - Architecture documentation
|
||||
- API.md - Odoo integration patterns
|
||||
|
||||
🔗 Resources:
|
||||
- Odoo API Docs: https://www.odoo.com/documentation/
|
||||
- SvelteKit Docs: https://kit.svelte.dev/
|
||||
```
|
||||
|
||||
## Template Variables
|
||||
|
||||
When generating files, replace these placeholders:
|
||||
|
||||
- `{{PROJECT_NAME}}` - User's project name (kebab-case)
|
||||
- `{{MODEL_NAME}}` - Odoo model name without x_ prefix
|
||||
- `{{MODEL_DISPLAY_NAME}}` - Human-readable model name
|
||||
- `{{FRAMEWORK}}` - sveltekit/react/vue
|
||||
- `{{DEPLOYMENT_TARGET}}` - vercel/github-pages/cloudflare/netlify
|
||||
- `{{AUTHOR_NAME}}` - User's name (if provided)
|
||||
|
||||
## Common Patterns from expense-split-pwa
|
||||
|
||||
This skill implements proven patterns from the expense-split-pwa project:
|
||||
|
||||
1. **Smart Caching**: Load from cache immediately, sync in background if stale
|
||||
2. **Incremental Fetch**: Only fetch records with `id > lastRecordId`
|
||||
3. **Partner Resolution**: Batch-fetch and cache partner names
|
||||
4. **Dual-Phase Calculation**: Process settled/unsettled records separately
|
||||
5. **Optimistic Updates**: Update UI immediately, sync to server in background
|
||||
6. **Error Recovery**: Graceful degradation when offline
|
||||
|
||||
## Error Handling
|
||||
|
||||
If generation fails:
|
||||
- Verify all required input is provided
|
||||
- Check template files exist
|
||||
- Ensure proper permissions for file creation
|
||||
- Provide clear error messages to user
|
||||
|
||||
## Framework-Specific Notes
|
||||
|
||||
### SvelteKit
|
||||
- Use Svelte 5 runes syntax (`$state`, `$derived`, `$effect`)
|
||||
- Configure adapter-static for static deployment
|
||||
- Set `ssr: false` for client-side only apps
|
||||
- Use `$app/paths` for base path support
|
||||
|
||||
### React
|
||||
- Use React 18+ with hooks
|
||||
- Context API for global state
|
||||
- React Query for server state (optional)
|
||||
- Vite for build tooling
|
||||
|
||||
### Vue
|
||||
- Use Vue 3 Composition API
|
||||
- Pinia for state management
|
||||
- Composables for reusable logic
|
||||
- Vite for build tooling
|
||||
|
||||
## Testing the Generated Project
|
||||
|
||||
After generation, verify:
|
||||
1. `npm install` completes without errors
|
||||
2. `npm run dev` starts development server
|
||||
3. Form renders correctly
|
||||
4. Offline banner appears when disconnecting
|
||||
5. Data persists in localStorage/IndexedDB
|
||||
6. Sync works when back online
|
||||
|
||||
## Support
|
||||
|
||||
For issues or questions:
|
||||
- Check CLAUDE.md for architecture details
|
||||
- Review generated code comments
|
||||
- Consult Odoo API documentation
|
||||
- Verify environment variables are set correctly
|
||||
@@ -0,0 +1,11 @@
|
||||
# Odoo Instance Configuration (for backend server)
|
||||
ODOO_URL=https://your-instance.odoo.com
|
||||
ODOO_DB=your-database-name
|
||||
ODOO_USERNAME=your-username
|
||||
ODOO_API_KEY=your-api-key
|
||||
|
||||
# Primary Model (use x_ prefix for Odoo Studio models)
|
||||
ODOO_PRIMARY_MODEL=x_{{MODEL_NAME}}
|
||||
|
||||
# Frontend: API endpoint (if frontend and backend are separate)
|
||||
VITE_API_URL=http://localhost:3000
|
||||
@@ -0,0 +1,29 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
# Environment
|
||||
.env
|
||||
.env.local
|
||||
.env.production
|
||||
@@ -0,0 +1,16 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="theme-color" content="#667eea" />
|
||||
<meta name="description" content="{{MODEL_DISPLAY_NAME}} management PWA with Odoo integration" />
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
<title>{{PROJECT_NAME}}</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.jsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"name": "{{PROJECT_NAME}}",
|
||||
"private": true,
|
||||
"version": "0.0.1",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-router-dom": "^6.26.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.3.3",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@vitejs/plugin-react": "^4.3.1",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-plugin-react": "^7.34.2",
|
||||
"eslint-plugin-react-hooks": "^4.6.2",
|
||||
"eslint-plugin-react-refresh": "^0.4.7",
|
||||
"vite": "^5.3.4",
|
||||
"vite-plugin-pwa": "^0.20.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
import { VitePWA } from 'vite-plugin-pwa';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
react(),
|
||||
VitePWA({
|
||||
registerType: 'autoUpdate',
|
||||
workbox: {
|
||||
globPatterns: ['**/*.{js,css,html,ico,png,svg,json,webp}']
|
||||
},
|
||||
manifest: {
|
||||
name: '{{PROJECT_NAME}}',
|
||||
short_name: '{{PROJECT_NAME}}',
|
||||
description: 'PWA for {{MODEL_DISPLAY_NAME}} management with Odoo integration',
|
||||
theme_color: '#667eea',
|
||||
background_color: '#ffffff',
|
||||
display: 'standalone',
|
||||
icons: [
|
||||
{
|
||||
src: '/icon-192.png',
|
||||
sizes: '192x192',
|
||||
type: 'image/png'
|
||||
},
|
||||
{
|
||||
src: '/icon-512.png',
|
||||
sizes: '512x512',
|
||||
type: 'image/png'
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': '/src'
|
||||
}
|
||||
},
|
||||
server: {
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: process.env.VITE_API_URL || 'http://localhost:3000',
|
||||
changeOrigin: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
311
skills/create-odoo-pwa/templates/react/src/App.css.template
Normal file
311
skills/create-odoo-pwa/templates/react/src/App.css.template
Normal file
@@ -0,0 +1,311 @@
|
||||
:root {
|
||||
--primary-color: #667eea;
|
||||
--primary-dark: #5568d3;
|
||||
--bg-gradient-start: #667eea;
|
||||
--bg-gradient-end: #764ba2;
|
||||
--error-bg: #f8d7da;
|
||||
--error-text: #721c24;
|
||||
--success-bg: #d4edda;
|
||||
--success-text: #155724;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu,
|
||||
Cantarell, 'Helvetica Neue', sans-serif;
|
||||
background: linear-gradient(135deg, var(--bg-gradient-start) 0%, var(--bg-gradient-end) 100%);
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.app {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: white;
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
font-size: 2.5em;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
color: #333;
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
.nav {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-bottom: 30px;
|
||||
background: white;
|
||||
border-radius: 10px;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.nav a {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
padding: 12px;
|
||||
text-decoration: none;
|
||||
color: var(--primary-color);
|
||||
border-radius: 8px;
|
||||
font-weight: 600;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.nav a.active {
|
||||
background: var(--primary-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.offline-banner {
|
||||
background: #e3f2fd;
|
||||
color: #1565c0;
|
||||
padding: 12px 20px;
|
||||
border-radius: 10px;
|
||||
margin-bottom: 20px;
|
||||
text-align: center;
|
||||
font-weight: 600;
|
||||
border: 2px solid #64b5f6;
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.8; }
|
||||
}
|
||||
|
||||
.form, .list-container {
|
||||
background: white;
|
||||
padding: 24px;
|
||||
border-radius: 15px;
|
||||
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
border: 2px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
transition: border-color 0.3s;
|
||||
}
|
||||
|
||||
input:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
.button-group {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
button {
|
||||
flex: 1;
|
||||
min-width: 150px;
|
||||
padding: 15px;
|
||||
background: var(--primary-color);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
button:hover:not(:disabled) {
|
||||
background: var(--primary-dark);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 5px 20px rgba(102, 126, 234, 0.4);
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.refresh-btn {
|
||||
background: #f0f0f0;
|
||||
color: var(--primary-color);
|
||||
flex: 0 1 auto;
|
||||
}
|
||||
|
||||
.refresh-btn:hover:not(:disabled) {
|
||||
background: #e0e0e0;
|
||||
color: var(--primary-dark);
|
||||
}
|
||||
|
||||
.message {
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 15px;
|
||||
background: var(--success-bg);
|
||||
color: var(--success-text);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.message.error {
|
||||
background: var(--error-bg);
|
||||
color: var(--error-text);
|
||||
}
|
||||
|
||||
.list-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
flex-wrap: wrap;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
padding: 10px;
|
||||
border: 2px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.refresh-btn-small {
|
||||
padding: 10px 15px;
|
||||
background: #f0f0f0;
|
||||
color: var(--primary-color);
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 18px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
min-width: auto;
|
||||
flex: 0;
|
||||
}
|
||||
|
||||
.refresh-btn-small:hover:not(:disabled) {
|
||||
background: #e0e0e0;
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
|
||||
.loading, .empty {
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
color: #666;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.record-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.record-card {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px;
|
||||
border: 2px solid #e0e0e0;
|
||||
border-radius: 10px;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.record-card:hover {
|
||||
border-color: var(--primary-color);
|
||||
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.1);
|
||||
}
|
||||
|
||||
.record-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.record-content h3 {
|
||||
margin: 0 0 8px 0;
|
||||
color: #333;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
.record-meta {
|
||||
margin: 0;
|
||||
color: #666;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.record-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.delete-btn {
|
||||
padding: 8px 12px;
|
||||
background: #fff5f5;
|
||||
color: #e53e3e;
|
||||
border: 1px solid #feb2b2;
|
||||
border-radius: 6px;
|
||||
font-size: 18px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
min-width: auto;
|
||||
flex: 0;
|
||||
}
|
||||
|
||||
.delete-btn:hover {
|
||||
background: #fed7d7;
|
||||
transform: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.list-header {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
min-width: auto;
|
||||
}
|
||||
|
||||
.record-card {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.record-actions {
|
||||
align-self: flex-end;
|
||||
}
|
||||
}
|
||||
41
skills/create-odoo-pwa/templates/react/src/App.jsx.template
Normal file
41
skills/create-odoo-pwa/templates/react/src/App.jsx.template
Normal file
@@ -0,0 +1,41 @@
|
||||
import { Routes, Route, Link, useLocation } from 'react-router-dom';
|
||||
import AddPage from './pages/AddPage';
|
||||
import ListPage from './pages/ListPage';
|
||||
import OfflineBanner from './components/OfflineBanner';
|
||||
import './App.css';
|
||||
|
||||
function App() {
|
||||
const location = useLocation();
|
||||
|
||||
return (
|
||||
<div className="app">
|
||||
<div className="container">
|
||||
<h1>📋 {{PROJECT_NAME}}</h1>
|
||||
|
||||
<OfflineBanner />
|
||||
|
||||
<nav className="nav">
|
||||
<Link
|
||||
to="/"
|
||||
className={location.pathname === '/' ? 'active' : ''}
|
||||
>
|
||||
Add {{MODEL_DISPLAY_NAME}}
|
||||
</Link>
|
||||
<Link
|
||||
to="/list"
|
||||
className={location.pathname === '/list' ? 'active' : ''}
|
||||
>
|
||||
View All
|
||||
</Link>
|
||||
</nav>
|
||||
|
||||
<Routes>
|
||||
<Route path="/" element={<AddPage />} />
|
||||
<Route path="/list" element={<ListPage />} />
|
||||
</Routes>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
123
skills/create-odoo-pwa/templates/react/src/api/odoo.js.template
Normal file
123
skills/create-odoo-pwa/templates/react/src/api/odoo.js.template
Normal file
@@ -0,0 +1,123 @@
|
||||
/**
|
||||
* Odoo API Client for React
|
||||
* Communicates with backend server that proxies to Odoo
|
||||
*/
|
||||
|
||||
class OdooAPI {
|
||||
constructor() {
|
||||
this.apiUrl = import.meta.env.VITE_API_URL
|
||||
? `${import.meta.env.VITE_API_URL}/api/odoo`
|
||||
: '/api/odoo';
|
||||
}
|
||||
|
||||
/**
|
||||
* Call the server-side API
|
||||
* @param {string} action
|
||||
* @param {any} data
|
||||
* @returns {Promise<any>}
|
||||
*/
|
||||
async callApi(action, data) {
|
||||
const response = await fetch(this.apiUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ action, data })
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'API Error');
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new record in specified model
|
||||
* @param {string} model - Odoo model name
|
||||
* @param {Record<string, any>} fields - Record fields
|
||||
* @returns {Promise<number>} - Created record ID
|
||||
*/
|
||||
async createRecord(model, fields) {
|
||||
const result = await this.callApi('create', { model, fields });
|
||||
return result.id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search and read records from specified model
|
||||
* @param {string} model - Odoo model name
|
||||
* @param {any[]} domain - Odoo domain filter
|
||||
* @param {string[]} fields - Fields to retrieve
|
||||
* @returns {Promise<any[]>} - Array of records
|
||||
*/
|
||||
async searchRecords(model, domain = [], fields = []) {
|
||||
const result = await this.callApi('search', { model, domain, fields });
|
||||
return result.results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic search_read for any model
|
||||
* @param {string} model
|
||||
* @param {any[]} domain
|
||||
* @param {string[]} fields
|
||||
* @returns {Promise<any[]>}
|
||||
*/
|
||||
async searchModel(model, domain = [], fields = []) {
|
||||
const result = await this.callApi('search_model', { model, domain, fields });
|
||||
return result.results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch partner list (common operation)
|
||||
* @returns {Promise<Array<{id:number, display_name:string}>>}
|
||||
*/
|
||||
async fetchPartners() {
|
||||
return await this.searchModel('res.partner', [], ['id', 'display_name']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a many2one field value
|
||||
* @param {number|string|null|undefined} id
|
||||
* @returns {number|false}
|
||||
*/
|
||||
formatMany2one(id) {
|
||||
return id ? Number(id) : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a many2many field using the (6,0,[ids]) command
|
||||
* @param {Array<number|string>} ids
|
||||
* @returns {any[]}
|
||||
*/
|
||||
formatMany2many(ids) {
|
||||
if (!Array.isArray(ids) || ids.length === 0) return [];
|
||||
return [[6, 0, ids.map((i) => Number(i))]];
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a record
|
||||
* @param {string} model - Odoo model name
|
||||
* @param {number} id - Record ID
|
||||
* @param {Record<string, any>} values - Fields to update
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
async updateRecord(model, id, values) {
|
||||
const result = await this.callApi('update', { model, id, values });
|
||||
return result.result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a record
|
||||
* @param {string} model - Odoo model name
|
||||
* @param {number} id - Record ID
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
async deleteRecord(model, id) {
|
||||
const result = await this.callApi('delete', { model, id });
|
||||
return result.result;
|
||||
}
|
||||
}
|
||||
|
||||
export const odooClient = new OdooAPI();
|
||||
@@ -0,0 +1,28 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
function OfflineBanner() {
|
||||
const [isOffline, setIsOffline] = useState(!navigator.onLine);
|
||||
|
||||
useEffect(() => {
|
||||
const handleOnline = () => setIsOffline(false);
|
||||
const handleOffline = () => setIsOffline(true);
|
||||
|
||||
window.addEventListener('online', handleOnline);
|
||||
window.addEventListener('offline', handleOffline);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('online', handleOnline);
|
||||
window.removeEventListener('offline', handleOffline);
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (!isOffline) return null;
|
||||
|
||||
return (
|
||||
<div className="offline-banner">
|
||||
📡 Offline Mode - Data will be synced when you're back online
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default OfflineBanner;
|
||||
@@ -0,0 +1,243 @@
|
||||
import { createContext, useContext, useState, useEffect, useCallback } from 'react';
|
||||
import { odooClient } from '../api/odoo';
|
||||
|
||||
const TaskContext = createContext();
|
||||
|
||||
// Cache configuration
|
||||
const CACHE_KEY = '{{MODEL_NAME}}_cache_v1';
|
||||
const CACHE_META_KEY = '{{MODEL_NAME}}_cache_meta_v1';
|
||||
const CACHE_DURATION_MS = 5 * 60 * 1000; // 5 minutes cache validity
|
||||
const SYNC_INTERVAL_MS = 3 * 60 * 1000; // Background sync every 3 minutes
|
||||
|
||||
// Helper functions for localStorage
|
||||
function loadFromStorage() {
|
||||
try {
|
||||
const cachedData = localStorage.getItem(CACHE_KEY);
|
||||
const meta = localStorage.getItem(CACHE_META_KEY);
|
||||
|
||||
if (cachedData && meta) {
|
||||
const records = JSON.parse(cachedData);
|
||||
const metaData = JSON.parse(meta);
|
||||
|
||||
const now = Date.now();
|
||||
const isStale = now - metaData.lastSyncTime > CACHE_DURATION_MS;
|
||||
|
||||
return { records, meta: { ...metaData, isStale } };
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Failed to load cache from storage:', e);
|
||||
}
|
||||
|
||||
return {
|
||||
records: [],
|
||||
meta: {
|
||||
lastSyncTime: 0,
|
||||
lastRecordId: 0,
|
||||
recordCount: 0,
|
||||
isStale: true
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function saveToStorage(records, meta) {
|
||||
try {
|
||||
localStorage.setItem(CACHE_KEY, JSON.stringify(records));
|
||||
localStorage.setItem(CACHE_META_KEY, JSON.stringify({
|
||||
lastSyncTime: meta.lastSyncTime || Date.now(),
|
||||
lastRecordId: meta.lastRecordId || 0,
|
||||
recordCount: records.length,
|
||||
isStale: false
|
||||
}));
|
||||
} catch (e) {
|
||||
console.warn('Failed to save cache to storage:', e);
|
||||
}
|
||||
}
|
||||
|
||||
export function TaskProvider({ children }) {
|
||||
const [records, setRecords] = useState([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [syncing, setSyncing] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const [meta, setMeta] = useState({
|
||||
lastSyncTime: 0,
|
||||
lastRecordId: 0,
|
||||
recordCount: 0,
|
||||
isStale: true
|
||||
});
|
||||
|
||||
// Sync function - fetches new data from server
|
||||
const sync = useCallback(async (forceFullRefresh = false) => {
|
||||
setSyncing(true);
|
||||
setError('');
|
||||
|
||||
try {
|
||||
const fields = ['id', 'x_name'];
|
||||
let domain = [];
|
||||
let fetchedRecords = [];
|
||||
|
||||
if (!forceFullRefresh && meta.lastRecordId > 0) {
|
||||
// Incremental fetch
|
||||
domain = [['id', '>', meta.lastRecordId]];
|
||||
try {
|
||||
fetchedRecords = await odooClient.searchRecords('x_{{MODEL_NAME}}', domain, fields);
|
||||
} catch (err) {
|
||||
console.warn('Incremental fetch failed, falling back to full fetch:', err);
|
||||
forceFullRefresh = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (forceFullRefresh || meta.lastRecordId === 0) {
|
||||
// Full refresh
|
||||
fetchedRecords = await odooClient.searchRecords('x_{{MODEL_NAME}}', [], fields);
|
||||
}
|
||||
|
||||
// Merge or replace records
|
||||
let mergedRecords;
|
||||
if (forceFullRefresh || meta.lastRecordId === 0) {
|
||||
mergedRecords = fetchedRecords;
|
||||
} else {
|
||||
const existingIds = new Set(records.map(r => r.id));
|
||||
const newRecords = fetchedRecords.filter(r => !existingIds.has(r.id));
|
||||
mergedRecords = [...records, ...newRecords];
|
||||
}
|
||||
|
||||
// Sort by ID
|
||||
mergedRecords.sort((a, b) => a.id - b.id);
|
||||
|
||||
// Calculate new metadata
|
||||
const lastRecordId = mergedRecords.length > 0
|
||||
? Math.max(...mergedRecords.map(r => r.id))
|
||||
: 0;
|
||||
|
||||
const newMeta = {
|
||||
lastSyncTime: Date.now(),
|
||||
lastRecordId,
|
||||
recordCount: mergedRecords.length,
|
||||
isStale: false
|
||||
};
|
||||
|
||||
// Save to storage
|
||||
saveToStorage(mergedRecords, newMeta);
|
||||
|
||||
// Update state
|
||||
setRecords(mergedRecords);
|
||||
setMeta(newMeta);
|
||||
} catch (error) {
|
||||
console.error('Sync failed:', error);
|
||||
setError(error.message || 'Failed to sync data');
|
||||
} finally {
|
||||
setSyncing(false);
|
||||
}
|
||||
}, [meta.lastRecordId, records]);
|
||||
|
||||
// Initialize - load cache and start background sync
|
||||
useEffect(() => {
|
||||
const cachedState = loadFromStorage();
|
||||
|
||||
if (cachedState.records.length > 0) {
|
||||
setRecords(cachedState.records);
|
||||
setMeta(cachedState.meta);
|
||||
|
||||
// Sync in background if stale
|
||||
if (cachedState.meta.isStale) {
|
||||
sync();
|
||||
}
|
||||
} else {
|
||||
setLoading(true);
|
||||
sync(true).finally(() => setLoading(false));
|
||||
}
|
||||
|
||||
// Set up periodic background sync
|
||||
const syncInterval = setInterval(() => {
|
||||
sync();
|
||||
}, SYNC_INTERVAL_MS);
|
||||
|
||||
return () => clearInterval(syncInterval);
|
||||
}, []);
|
||||
|
||||
// Force refresh
|
||||
const forceRefresh = useCallback(async () => {
|
||||
await sync(true);
|
||||
}, [sync]);
|
||||
|
||||
// Create record
|
||||
const createRecord = useCallback(async (fields) => {
|
||||
try {
|
||||
const id = await odooClient.createRecord('x_{{MODEL_NAME}}', fields);
|
||||
|
||||
// Optimistically add to cache
|
||||
const newRecord = { id, ...fields };
|
||||
setRecords(prev => [...prev, newRecord]);
|
||||
|
||||
// Sync to get full record
|
||||
await sync();
|
||||
|
||||
return id;
|
||||
} catch (error) {
|
||||
console.error('Failed to create record:', error);
|
||||
throw error;
|
||||
}
|
||||
}, [sync]);
|
||||
|
||||
// Update record
|
||||
const updateRecord = useCallback(async (id, values) => {
|
||||
try {
|
||||
await odooClient.updateRecord('x_{{MODEL_NAME}}', id, values);
|
||||
|
||||
// Optimistically update cache
|
||||
setRecords(prev =>
|
||||
prev.map(r => r.id === id ? { ...r, ...values } : r)
|
||||
);
|
||||
|
||||
// Sync to get full updated record
|
||||
await sync();
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Failed to update record:', error);
|
||||
throw error;
|
||||
}
|
||||
}, [sync]);
|
||||
|
||||
// Delete record
|
||||
const deleteRecord = useCallback(async (id) => {
|
||||
try {
|
||||
await odooClient.deleteRecord('x_{{MODEL_NAME}}', id);
|
||||
|
||||
// Optimistically remove from cache
|
||||
setRecords(prev => prev.filter(r => r.id !== id));
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Failed to delete record:', error);
|
||||
throw error;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const value = {
|
||||
records,
|
||||
loading,
|
||||
syncing,
|
||||
error,
|
||||
meta,
|
||||
sync,
|
||||
forceRefresh,
|
||||
createRecord,
|
||||
updateRecord,
|
||||
deleteRecord
|
||||
};
|
||||
|
||||
return (
|
||||
<TaskContext.Provider value={value}>
|
||||
{children}
|
||||
</TaskContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useTask() {
|
||||
const context = useContext(TaskContext);
|
||||
if (!context) {
|
||||
throw new Error('useTask must be used within TaskProvider');
|
||||
}
|
||||
return context;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
:root {
|
||||
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
|
||||
color-scheme: light dark;
|
||||
color: rgba(255, 255, 255, 0.87);
|
||||
background-color: #242424;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
place-items: center;
|
||||
min-width: 320px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
#root {
|
||||
width: 100%;
|
||||
}
|
||||
16
skills/create-odoo-pwa/templates/react/src/main.jsx.template
Normal file
16
skills/create-odoo-pwa/templates/react/src/main.jsx.template
Normal file
@@ -0,0 +1,16 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import App from './App';
|
||||
import { TaskProvider } from './context/TaskContext';
|
||||
import './index.css';
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')).render(
|
||||
<React.StrictMode>
|
||||
<BrowserRouter>
|
||||
<TaskProvider>
|
||||
<App />
|
||||
</TaskProvider>
|
||||
</BrowserRouter>
|
||||
</React.StrictMode>
|
||||
);
|
||||
@@ -0,0 +1,78 @@
|
||||
import { useState } from 'react';
|
||||
import { useTask } from '../context/TaskContext';
|
||||
|
||||
function AddPage() {
|
||||
const [name, setName] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [message, setMessage] = useState('');
|
||||
const { createRecord, forceRefresh } = useTask();
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!name.trim()) {
|
||||
setMessage('⚠️ Please fill in the name field');
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
setMessage('');
|
||||
|
||||
try {
|
||||
const payload = { x_name: name };
|
||||
await createRecord(payload);
|
||||
|
||||
if (navigator.onLine) {
|
||||
setMessage('✅ {{MODEL_DISPLAY_NAME}} added successfully!');
|
||||
} else {
|
||||
setMessage('✅ {{MODEL_DISPLAY_NAME}} saved locally! Will sync when online.');
|
||||
}
|
||||
|
||||
// Reset form
|
||||
setName('');
|
||||
} catch (error) {
|
||||
setMessage(`❌ Error: ${error.message}`);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className="form">
|
||||
<div className="form-group">
|
||||
<label htmlFor="name">Name</label>
|
||||
<input
|
||||
type="text"
|
||||
id="name"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder="Enter {{MODEL_DISPLAY_NAME}} name"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
{message && (
|
||||
<div className={`message ${message.includes('❌') ? 'error' : ''}`}>
|
||||
{message}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="button-group">
|
||||
<button type="submit" disabled={loading}>
|
||||
{loading ? '⏳ Adding...' : '➕ Add {{MODEL_DISPLAY_NAME}}'}
|
||||
</button>
|
||||
{navigator.onLine && (
|
||||
<button
|
||||
type="button"
|
||||
className="refresh-btn"
|
||||
onClick={forceRefresh}
|
||||
>
|
||||
🔄 Refresh Data
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
export default AddPage;
|
||||
@@ -0,0 +1,76 @@
|
||||
import { useState } from 'react';
|
||||
import { useTask } from '../context/TaskContext';
|
||||
|
||||
function ListPage() {
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const { records, loading, syncing, forceRefresh, deleteRecord } = useTask();
|
||||
|
||||
const filteredRecords = records.filter(record =>
|
||||
record.x_name?.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
);
|
||||
|
||||
const handleDelete = async (id) => {
|
||||
if (!confirm('Are you sure you want to delete this {{MODEL_DISPLAY_NAME}}?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await deleteRecord(id);
|
||||
} catch (error) {
|
||||
alert(`Failed to delete: ${error.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="list-container">
|
||||
<div className="list-header">
|
||||
<h2>All {{MODEL_DISPLAY_NAME}}s</h2>
|
||||
<div className="header-actions">
|
||||
<input
|
||||
type="search"
|
||||
placeholder="Search..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="search-input"
|
||||
/>
|
||||
<button
|
||||
className="refresh-btn-small"
|
||||
onClick={forceRefresh}
|
||||
disabled={syncing}
|
||||
>
|
||||
{syncing ? '⏳' : '🔄'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<div className="loading">Loading...</div>
|
||||
) : filteredRecords.length === 0 ? (
|
||||
<div className="empty">
|
||||
{searchTerm ? 'No matching records found' : 'No {{MODEL_DISPLAY_NAME}}s yet. Add your first one!'}
|
||||
</div>
|
||||
) : (
|
||||
<div className="record-list">
|
||||
{filteredRecords.map(record => (
|
||||
<div key={record.id} className="record-card">
|
||||
<div className="record-content">
|
||||
<h3>{record.x_name}</h3>
|
||||
<p className="record-meta">ID: {record.id}</p>
|
||||
</div>
|
||||
<div className="record-actions">
|
||||
<button
|
||||
className="delete-btn"
|
||||
onClick={() => handleDelete(record.id)}
|
||||
>
|
||||
🗑️
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ListPage;
|
||||
@@ -0,0 +1,12 @@
|
||||
# Odoo Instance Configuration
|
||||
ODOO_URL=https://your-instance.odoo.com
|
||||
ODOO_DB=your-database-name
|
||||
ODOO_USERNAME=your-username
|
||||
ODOO_API_KEY=your-api-key
|
||||
|
||||
# Primary Model (use x_ prefix for Odoo Studio models)
|
||||
ODOO_PRIMARY_MODEL=x_{{MODEL_NAME}}
|
||||
|
||||
# Optional: For static hosting (GitHub Pages, Cloudflare Pages, etc.)
|
||||
# Set this to the full URL of your API server if frontend and backend are on different domains
|
||||
PUBLIC_API_URL=
|
||||
@@ -0,0 +1,15 @@
|
||||
node_modules/
|
||||
.env
|
||||
.env.local
|
||||
dist/
|
||||
build/
|
||||
.svelte-kit/
|
||||
.vercel/
|
||||
.netlify/
|
||||
.DS_Store
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"extends": "./.svelte-kit/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"skipLibCheck": true,
|
||||
"sourceMap": true,
|
||||
"strict": false,
|
||||
"moduleResolution": "bundler"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"name": "{{PROJECT_NAME}}",
|
||||
"private": true,
|
||||
"version": "0.0.1",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json",
|
||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json --watch"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/adapter-auto": "^6.1.0",
|
||||
"@sveltejs/adapter-static": "^3.0.10",
|
||||
"@sveltejs/kit": "^2.43.2",
|
||||
"@sveltejs/vite-plugin-svelte": "^6.2.0",
|
||||
"@vite-pwa/sveltekit": "^1.0.1",
|
||||
"svelte": "^5.39.5",
|
||||
"svelte-check": "^4.3.2",
|
||||
"typescript": "^5.9.2",
|
||||
"vite": "^7.1.7"
|
||||
},
|
||||
"dependencies": {
|
||||
"vite-plugin-pwa": "^1.1.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import adapter from '@sveltejs/adapter-static';
|
||||
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
|
||||
|
||||
/** @type {import('@sveltejs/kit').Config} */
|
||||
const config = {
|
||||
preprocess: vitePreprocess(),
|
||||
|
||||
kit: {
|
||||
adapter: adapter({
|
||||
pages: 'build',
|
||||
assets: 'build',
|
||||
fallback: undefined,
|
||||
precompress: false,
|
||||
strict: true
|
||||
}),
|
||||
// Configure base path for GitHub Pages deployment
|
||||
paths: {
|
||||
base: process.argv.includes('dev') ? '' : process.env.PUBLIC_BASE_PATH || ''
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -0,0 +1,40 @@
|
||||
import { sveltekit } from '@sveltejs/kit/vite';
|
||||
import { defineConfig } from 'vite';
|
||||
import { VitePWA } from 'vite-plugin-pwa';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
sveltekit(),
|
||||
VitePWA({
|
||||
registerType: 'autoUpdate',
|
||||
workbox: {
|
||||
globPatterns: ['**/*.{js,css,html,ico,png,svg,json,webp}']
|
||||
},
|
||||
manifest: {
|
||||
id: '/',
|
||||
name: '{{PROJECT_NAME}}',
|
||||
short_name: '{{PROJECT_NAME}}',
|
||||
description: 'PWA for {{MODEL_DISPLAY_NAME}} management with Odoo integration',
|
||||
start_url: '/',
|
||||
scope: '/',
|
||||
display: 'standalone',
|
||||
background_color: '#ffffff',
|
||||
theme_color: '#667eea',
|
||||
icons: [
|
||||
{
|
||||
src: '/icon-192.png',
|
||||
sizes: '192x192',
|
||||
type: 'image/png',
|
||||
purpose: 'any maskable'
|
||||
},
|
||||
{
|
||||
src: '/icon-512.png',
|
||||
sizes: '512x512',
|
||||
type: 'image/png',
|
||||
purpose: 'any maskable'
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
]
|
||||
});
|
||||
@@ -0,0 +1,48 @@
|
||||
name: Deploy to GitHub Pages
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
|
||||
concurrency:
|
||||
group: "pages"
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
cache: 'npm'
|
||||
- run: npm ci
|
||||
- run: npm run build
|
||||
env:
|
||||
PUBLIC_BASE_PATH: /${{ github.event.repository.name }}
|
||||
ODOO_URL: ${{ secrets.ODOO_URL }}
|
||||
ODOO_DB: ${{ secrets.ODOO_DB }}
|
||||
ODOO_USERNAME: ${{ secrets.ODOO_USERNAME }}
|
||||
ODOO_API_KEY: ${{ secrets.ODOO_API_KEY }}
|
||||
ODOO_PRIMARY_MODEL: ${{ secrets.ODOO_PRIMARY_MODEL }}
|
||||
- uses: actions/upload-pages-artifact@v2
|
||||
with:
|
||||
path: build
|
||||
|
||||
deploy:
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
steps:
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v2
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"buildCommand": "npm run build",
|
||||
"outputDirectory": "build",
|
||||
"framework": "sveltekit",
|
||||
"regions": ["iad1"]
|
||||
}
|
||||
@@ -0,0 +1,243 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code when working with this {{PROJECT_NAME}} codebase.
|
||||
|
||||
## Project Overview
|
||||
|
||||
**{{PROJECT_NAME}}** - An offline-first Progressive Web App for {{MODEL_DISPLAY_NAME}} management, built with SvelteKit frontend and Odoo Studio backend.
|
||||
|
||||
## Development Commands
|
||||
|
||||
### Development
|
||||
```bash
|
||||
npm run dev # Start dev server at http://localhost:5173
|
||||
```
|
||||
|
||||
### Building
|
||||
```bash
|
||||
npm run build # Production build (outputs to /build)
|
||||
npm run preview # Preview production build locally
|
||||
```
|
||||
|
||||
### Type Checking
|
||||
```bash
|
||||
npm run check # Run svelte-check for type errors
|
||||
npm run check:watch # Watch mode for type checking
|
||||
```
|
||||
|
||||
## Environment Setup
|
||||
|
||||
Required environment variables in `.env`:
|
||||
```env
|
||||
ODOO_URL=https://your-instance.odoo.com
|
||||
ODOO_DB=your-database-name
|
||||
ODOO_USERNAME=your-username
|
||||
ODOO_API_KEY=your-api-key
|
||||
ODOO_PRIMARY_MODEL=x_{{MODEL_NAME}}
|
||||
```
|
||||
|
||||
**Important**: The app uses API keys (not passwords) for Odoo authentication. These are server-side only and never exposed to the client.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Three-Layer Data Flow
|
||||
|
||||
```
|
||||
Frontend Component (Svelte)
|
||||
↓
|
||||
Cache Store (Svelte Store)
|
||||
↓
|
||||
Odoo API Client (src/lib/odoo.js)
|
||||
↓
|
||||
Server Route (src/routes/api/odoo/+server.js)
|
||||
↓
|
||||
Odoo JSON-RPC Backend
|
||||
```
|
||||
|
||||
Data also persists to localStorage for offline access.
|
||||
|
||||
### Key Architectural Patterns
|
||||
|
||||
#### 1. **Smart Caching Layer** (`src/lib/stores/cache.js`)
|
||||
|
||||
The centerpiece of the frontend architecture. This Svelte store provides:
|
||||
|
||||
- **Immediate data availability**: Shows cached data from localStorage instantly on page load
|
||||
- **Background sync**: Automatically syncs with Odoo every 3 minutes if cache is stale (>5 minutes old)
|
||||
- **Incremental fetching**: Only fetches records with `id > lastRecordId` to minimize API calls
|
||||
- **Optimistic updates**: UI updates immediately, syncs to server in background
|
||||
|
||||
**Key functions**:
|
||||
- `initialize()` - Call in `onMount()`, loads cache and starts background sync
|
||||
- `sync()` - Incremental sync with Odoo
|
||||
- `forceRefresh()` - Clears cache and does full sync
|
||||
- `createRecord()`, `updateRecord()`, `deleteRecord()` - CRUD operations with optimistic updates
|
||||
|
||||
#### 2. **Server-Side API Proxy** (`src/routes/api/odoo/+server.js`)
|
||||
|
||||
SvelteKit server route that acts as a JSON-RPC proxy to Odoo. This pattern:
|
||||
|
||||
- Keeps credentials server-side (never exposed to client)
|
||||
- Caches Odoo UID to reduce authentication calls
|
||||
- Provides a simple `{ action, data }` interface for the frontend
|
||||
|
||||
**Supported actions**: `create`, `search`, `search_model`, `update`, `delete`
|
||||
|
||||
#### 3. **Odoo Field Formatting**
|
||||
|
||||
Odoo uses specific formats for relational fields:
|
||||
|
||||
- **Many2One (single relation)**: Send as integer ID, receive as `[id, "display_name"]` tuple
|
||||
- **Many2Many (multiple relations)**: Send as `[[6, 0, [id1, id2, ...]]]`, receive as array of tuples
|
||||
|
||||
Helper functions in `src/lib/odoo.js`:
|
||||
- `formatMany2one(id)` - Converts ID to integer or `false`
|
||||
- `formatMany2many(ids)` - Wraps IDs in Odoo command format `[[6, 0, [...]]]`
|
||||
|
||||
#### 4. **Offline-First Strategy**
|
||||
|
||||
Two-phase data loading:
|
||||
|
||||
1. **Immediate Load**: Show cached data from localStorage
|
||||
2. **Background Sync**: Fetch new data if cache is stale
|
||||
|
||||
```javascript
|
||||
// Phase 1: Load from cache immediately
|
||||
const cachedData = loadFromStorage();
|
||||
updateUI(cachedData);
|
||||
|
||||
// Phase 2: Background sync if stale
|
||||
if (cachedData.isStale) {
|
||||
syncInBackground();
|
||||
}
|
||||
```
|
||||
|
||||
### SvelteKit Configuration
|
||||
|
||||
- **Rendering**: `ssr: false`, `csr: true`, `prerender: true` in `src/routes/+layout.js`
|
||||
- **Adapter**: `adapter-static` configured for static output to `/build` directory
|
||||
- **Base path**: Configurable via `PUBLIC_BASE_PATH` env var (for GitHub Pages deployment)
|
||||
|
||||
### PWA Features
|
||||
|
||||
Configured in `vite.config.js`:
|
||||
- **Service Worker**: Auto-generated with Workbox, caches all static assets
|
||||
- **Auto-update**: New versions automatically activate
|
||||
- **Manifest**: Configured for standalone mode, installable on mobile
|
||||
- **Icons**: 192x192 and 512x512 PNG icons in `/static`
|
||||
|
||||
## Odoo Model Structure
|
||||
|
||||
### Main Model: `x_{{MODEL_NAME}}`
|
||||
|
||||
| Field | Type | Purpose |
|
||||
|-------|------|---------|
|
||||
| `x_name` | Char | Record name |
|
||||
| Add your custom fields with x_studio_ prefix |
|
||||
|
||||
## Important Development Notes
|
||||
|
||||
### Working with the Cache
|
||||
|
||||
When modifying record-related features:
|
||||
|
||||
1. **Always call `{{MODEL_NAME}}Cache.sync()` after mutations** (create/update/delete)
|
||||
2. **Initialize the cache in page components**: `onMount(() => {{MODEL_NAME}}Cache.initialize())`
|
||||
3. **Clean up on unmount**: `onDestroy(() => {{MODEL_NAME}}Cache.destroy())`
|
||||
4. **Cache invalidation**: Increment cache version in `cache.js` if store schema changes
|
||||
|
||||
### Odoo API Patterns
|
||||
|
||||
When adding new Odoo operations:
|
||||
|
||||
1. **Frontend**: Add method to `src/lib/odoo.js` (calls `/api/odoo` endpoint)
|
||||
2. **Backend**: Add new action case in `src/routes/api/odoo/+server.js`
|
||||
3. **Use `execute()` helper** for model operations (wraps authentication)
|
||||
|
||||
Example:
|
||||
```javascript
|
||||
// Frontend (odoo.js)
|
||||
async customOperation(id) {
|
||||
return this.callApi('custom', { id });
|
||||
}
|
||||
|
||||
// Backend (+server.js)
|
||||
case 'custom':
|
||||
const result = await execute('x_{{MODEL_NAME}}', 'write', [[data.id], { custom_field: true }]);
|
||||
return json({ success: true, result });
|
||||
```
|
||||
|
||||
### PWA Manifest Updates
|
||||
|
||||
When changing app name, icons, or theme:
|
||||
|
||||
1. Update `vite.config.js` manifest section
|
||||
2. Update `/static/manifest.json`
|
||||
3. Replace `/static/icon-192.png` and `/static/icon-512.png`
|
||||
4. Run `npm run build` to regenerate service worker
|
||||
|
||||
### Deployment
|
||||
|
||||
**Vercel** (primary):
|
||||
- Automatically deploys from `main` branch
|
||||
- Set environment variables in Vercel dashboard
|
||||
- Outputs to `/build` directory (configured in `vercel.json`)
|
||||
|
||||
**GitHub Pages**:
|
||||
- Set `PUBLIC_BASE_PATH=/repo-name` in GitHub Actions secrets
|
||||
- Configure GitHub Pages source as "GitHub Actions"
|
||||
- Workflow auto-deploys on push to `main`
|
||||
|
||||
## File Structure Reference
|
||||
|
||||
```
|
||||
src/
|
||||
├── lib/
|
||||
│ ├── stores/
|
||||
│ │ └── cache.js # Core caching & sync logic
|
||||
│ ├── odoo.js # Frontend API client
|
||||
│ ├── db.js # IndexedDB manager
|
||||
│ └── utils.js # Utility functions
|
||||
├── routes/
|
||||
│ ├── +layout.js # Root layout config (SSR/CSR settings)
|
||||
│ ├── +layout.svelte # Root layout component
|
||||
│ ├── +page.svelte # Add record form
|
||||
│ ├── list/+page.svelte # List all records
|
||||
│ └── api/odoo/+server.js # Odoo JSON-RPC proxy endpoint
|
||||
└── app.html # HTML template with PWA meta tags
|
||||
```
|
||||
|
||||
## Common Gotchas
|
||||
|
||||
1. **Odoo field naming**: All custom fields use `x_studio_` prefix (Odoo Studio convention)
|
||||
2. **Partner name format**: Odoo returns `[id, "name"]` tuples, must extract display name for UI
|
||||
3. **localStorage limits**: Browser typically allows 5-10MB, sufficient for hundreds of records
|
||||
4. **Service worker caching**: After PWA updates, users may need to close all tabs and reopen
|
||||
5. **Base path in production**: GitHub Pages deployments require `PUBLIC_BASE_PATH` env var set
|
||||
6. **API authentication**: Use API keys, not passwords. Generate in Odoo user settings.
|
||||
|
||||
## Adding New Features
|
||||
|
||||
### Add a New Field
|
||||
|
||||
1. Add field in Odoo Studio with `x_studio_` prefix
|
||||
2. Update `fields` array in `src/lib/stores/cache.js`
|
||||
3. Add form input in `src/routes/+page.svelte`
|
||||
4. Display field in `src/routes/list/+page.svelte`
|
||||
|
||||
### Add a New Page
|
||||
|
||||
1. Create `src/routes/new-page/+page.svelte`
|
||||
2. Add navigation link in layout or other pages
|
||||
3. Import and use cache store if accessing data
|
||||
|
||||
### Add Partner/Relation Field
|
||||
|
||||
1. Add Many2one or Many2many field in Odoo
|
||||
2. Use `odooClient.formatMany2one()` or `formatMany2many()` when saving
|
||||
3. Use `odooClient.fetchPartners()` to load options
|
||||
4. Display resolved names in UI
|
||||
|
||||
---
|
||||
|
||||
**Generated with Odoo PWA Generator**
|
||||
@@ -0,0 +1,225 @@
|
||||
# {{PROJECT_NAME}}
|
||||
|
||||
An offline-first Progressive Web App for {{MODEL_DISPLAY_NAME}} management with Odoo Studio backend integration.
|
||||
|
||||
## ✨ Features
|
||||
|
||||
- 📱 **Progressive Web App** - Installable on mobile and desktop
|
||||
- 🔌 **Offline-first** - Works without internet connection
|
||||
- 🔄 **Auto-sync** - Background synchronization with Odoo
|
||||
- 💾 **Smart caching** - localStorage + IndexedDB for optimal performance
|
||||
- 🎨 **Responsive UI** - Works on all devices
|
||||
- ⚡ **Fast** - Instant UI updates with optimistic rendering
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Node.js 18+ installed
|
||||
- Odoo Studio account with API access
|
||||
- Custom Odoo model created: `x_{{MODEL_NAME}}`
|
||||
|
||||
### Installation
|
||||
|
||||
1. Clone this repository:
|
||||
```bash
|
||||
git clone <repository-url>
|
||||
cd {{PROJECT_NAME}}
|
||||
```
|
||||
|
||||
2. Install dependencies:
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
3. Configure environment:
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
4. Edit `.env` with your Odoo credentials:
|
||||
```env
|
||||
ODOO_URL=https://your-instance.odoo.com
|
||||
ODOO_DB=your-database-name
|
||||
ODOO_USERNAME=your-username
|
||||
ODOO_API_KEY=your-api-key
|
||||
ODOO_PRIMARY_MODEL=x_{{MODEL_NAME}}
|
||||
```
|
||||
|
||||
5. Start development server:
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
6. Open [http://localhost:5173](http://localhost:5173)
|
||||
|
||||
## 🔧 Odoo Studio Setup
|
||||
|
||||
### Create Custom Model
|
||||
|
||||
1. Log into your Odoo instance
|
||||
2. Navigate to **Studio**
|
||||
3. Create a new model: `x_{{MODEL_NAME}}`
|
||||
4. Add these recommended fields:
|
||||
- `x_name` (Char) - Required - Record name
|
||||
- `x_studio_description` (Text) - Description
|
||||
- `x_studio_date` (Date) - Date field
|
||||
- `x_studio_value` (Float) - Numeric value
|
||||
- Add more custom fields as needed with `x_studio_` prefix
|
||||
|
||||
### Get API Credentials
|
||||
|
||||
1. Go to **Settings** → **Users & Companies** → **Users**
|
||||
2. Select your user
|
||||
3. Click **Generate API Key**
|
||||
4. Copy the key and add to `.env` file
|
||||
|
||||
## 📦 Build & Deployment
|
||||
|
||||
### Build for Production
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
### Deploy to Vercel
|
||||
|
||||
```bash
|
||||
npm install -g vercel
|
||||
vercel
|
||||
```
|
||||
|
||||
Set environment variables in Vercel dashboard:
|
||||
- `ODOO_URL`
|
||||
- `ODOO_DB`
|
||||
- `ODOO_USERNAME`
|
||||
- `ODOO_API_KEY`
|
||||
- `ODOO_PRIMARY_MODEL`
|
||||
|
||||
### Deploy to GitHub Pages
|
||||
|
||||
1. Add secrets to GitHub repository:
|
||||
- `ODOO_URL`, `ODOO_DB`, `ODOO_USERNAME`, `ODOO_API_KEY`, `ODOO_PRIMARY_MODEL`
|
||||
|
||||
2. Enable GitHub Pages in repository settings:
|
||||
- Source: GitHub Actions
|
||||
|
||||
3. Push to main branch - auto-deploys via GitHub Actions
|
||||
|
||||
### Deploy to Cloudflare Pages
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
wrangler pages publish build
|
||||
```
|
||||
|
||||
## 🏗️ Architecture
|
||||
|
||||
This app follows a three-layer architecture:
|
||||
|
||||
```
|
||||
Frontend Component
|
||||
↓
|
||||
Cache Store (Svelte Store)
|
||||
↓
|
||||
Odoo API Client (Frontend)
|
||||
↓
|
||||
Server Route (/api/odoo)
|
||||
↓
|
||||
Odoo JSON-RPC
|
||||
```
|
||||
|
||||
### Key Features:
|
||||
|
||||
- **Smart Caching**: Loads from cache instantly, syncs in background
|
||||
- **Incremental Sync**: Only fetches new records since last sync
|
||||
- **Offline Queue**: Saves changes locally when offline, syncs when online
|
||||
- **PWA**: Service worker caches all assets for offline use
|
||||
|
||||
See `CLAUDE.md` for detailed architecture documentation.
|
||||
|
||||
## 🛠️ Development
|
||||
|
||||
### Available Scripts
|
||||
|
||||
- `npm run dev` - Start development server
|
||||
- `npm run build` - Build for production
|
||||
- `npm run preview` - Preview production build
|
||||
- `npm run check` - Type check
|
||||
|
||||
### Project Structure
|
||||
|
||||
```
|
||||
{{PROJECT_NAME}}/
|
||||
├── src/
|
||||
│ ├── lib/
|
||||
│ │ ├── odoo.js # Odoo API client
|
||||
│ │ ├── db.js # IndexedDB manager
|
||||
│ │ ├── utils.js # Utility functions
|
||||
│ │ └── stores/
|
||||
│ │ └── cache.js # Smart cache store
|
||||
│ ├── routes/
|
||||
│ │ ├── +layout.svelte # Root layout
|
||||
│ │ ├── +page.svelte # Add record form
|
||||
│ │ ├── list/+page.svelte # List all records
|
||||
│ │ └── api/odoo/+server.js # Server API proxy
|
||||
│ └── app.html # HTML template
|
||||
├── static/
|
||||
│ ├── manifest.json # PWA manifest
|
||||
│ ├── icon-192.png # App icon 192x192
|
||||
│ └── icon-512.png # App icon 512x512
|
||||
├── .env # Environment variables (gitignored)
|
||||
├── .env.example # Environment template
|
||||
├── svelte.config.js # SvelteKit configuration
|
||||
├── vite.config.js # Vite + PWA configuration
|
||||
└── package.json # Dependencies
|
||||
```
|
||||
|
||||
## 📝 Adding Fields
|
||||
|
||||
To add custom fields to your model:
|
||||
|
||||
1. Add field in Odoo Studio (use `x_studio_` prefix)
|
||||
2. Update `src/lib/stores/cache.js` - add field to `fields` array
|
||||
3. Update `src/routes/+page.svelte` - add form input
|
||||
4. Update `src/routes/list/+page.svelte` - display field
|
||||
|
||||
## 🐛 Troubleshooting
|
||||
|
||||
### "Authentication failed"
|
||||
- Verify `ODOO_API_KEY` is correct
|
||||
- Check `ODOO_USERNAME` matches your Odoo user
|
||||
- Ensure API access is enabled in Odoo
|
||||
|
||||
### "Model not found"
|
||||
- Verify model name uses `x_` prefix: `x_{{MODEL_NAME}}`
|
||||
- Check model exists in Odoo Studio
|
||||
- Ensure model is published
|
||||
|
||||
### Offline mode not working
|
||||
- Check service worker is registered (DevTools → Application)
|
||||
- Verify PWA manifest is loaded
|
||||
- Clear browser cache and reload
|
||||
|
||||
### Data not syncing
|
||||
- Check browser console for errors
|
||||
- Verify internet connection
|
||||
- Check Odoo API is accessible
|
||||
|
||||
## 📚 Learn More
|
||||
|
||||
- [SvelteKit Documentation](https://kit.svelte.dev/)
|
||||
- [Odoo API Documentation](https://www.odoo.com/documentation/)
|
||||
- [PWA Guide](https://web.dev/progressive-web-apps/)
|
||||
|
||||
## 📄 License
|
||||
|
||||
MIT
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
Contributions welcome! Please open an issue or PR.
|
||||
|
||||
---
|
||||
|
||||
**Generated with [Odoo PWA Generator](https://github.com/jamshid/odoo-pwa-generator)** 🚀
|
||||
@@ -0,0 +1,134 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import {
|
||||
ODOO_URL,
|
||||
ODOO_DB,
|
||||
ODOO_USERNAME,
|
||||
ODOO_API_KEY
|
||||
} from '$env/static/private';
|
||||
|
||||
/** @type {number|null} */
|
||||
let cachedUid = null;
|
||||
|
||||
/**
|
||||
* Make JSON-RPC call to Odoo
|
||||
* @param {string} service
|
||||
* @param {string} method
|
||||
* @param {any[]} args
|
||||
*/
|
||||
async function callOdoo(service, method, args) {
|
||||
const response = await fetch(`${ODOO_URL}/jsonrpc`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
jsonrpc: '2.0',
|
||||
method: 'call',
|
||||
params: {
|
||||
service: service,
|
||||
method: method,
|
||||
args: args
|
||||
},
|
||||
id: Math.floor(Math.random() * 1000000)
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.error) {
|
||||
throw new Error(data.error.data?.message || data.error.message || 'Odoo API Error');
|
||||
}
|
||||
|
||||
return data.result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticate with Odoo and get UID
|
||||
*/
|
||||
async function authenticate() {
|
||||
if (cachedUid) return cachedUid;
|
||||
|
||||
const authMethod = ODOO_API_KEY;
|
||||
const uid = await callOdoo('common', 'login', [ODOO_DB, ODOO_USERNAME, authMethod]);
|
||||
|
||||
if (!uid) {
|
||||
throw new Error('Authentication failed');
|
||||
}
|
||||
|
||||
cachedUid = uid;
|
||||
return uid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a method on Odoo model
|
||||
* @param {string} model
|
||||
* @param {string} method
|
||||
* @param {any[]} args
|
||||
* @param {Record<string, any>} kwargs
|
||||
*/
|
||||
async function execute(model, method, args = [], kwargs = {}) {
|
||||
const uid = await authenticate();
|
||||
const authMethod = ODOO_API_KEY;
|
||||
|
||||
return await callOdoo('object', 'execute_kw', [
|
||||
ODOO_DB,
|
||||
uid,
|
||||
authMethod,
|
||||
model,
|
||||
method,
|
||||
args,
|
||||
kwargs
|
||||
]);
|
||||
}
|
||||
|
||||
/** @type {import('./$types').RequestHandler} */
|
||||
export async function POST({ request }) {
|
||||
try {
|
||||
const { action, data } = await request.json();
|
||||
|
||||
switch (action) {
|
||||
case 'create': {
|
||||
const { model, fields } = data;
|
||||
const id = await execute(model, 'create', [fields]);
|
||||
return json({ success: true, id });
|
||||
}
|
||||
|
||||
case 'search': {
|
||||
const { model, domain = [], fields = [] } = data;
|
||||
const results = await execute(model, 'search_read', [domain], { fields });
|
||||
return json({ success: true, results });
|
||||
}
|
||||
|
||||
// Search any model (used by frontend to load res.partner list, etc.)
|
||||
case 'search_model': {
|
||||
const { model, domain = [], fields = [] } = data;
|
||||
const results = await execute(model, 'search_read', [domain], { fields });
|
||||
return json({ success: true, results });
|
||||
}
|
||||
|
||||
case 'update': {
|
||||
const { model, id, values } = data;
|
||||
const result = await execute(model, 'write', [[id], values]);
|
||||
return json({ success: true, result });
|
||||
}
|
||||
|
||||
case 'delete': {
|
||||
const { model, id } = data;
|
||||
const result = await execute(model, 'unlink', [[id]]);
|
||||
return json({ success: true, result });
|
||||
}
|
||||
|
||||
default:
|
||||
return json({ success: false, error: 'Invalid action' }, { status: 400 });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Odoo API Error:', error);
|
||||
return json(
|
||||
{
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error'
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,311 @@
|
||||
<script>
|
||||
import { {{MODEL_NAME}}Cache, cacheStatus } from '$lib/stores/cache';
|
||||
import { filterRecords } from '$lib/utils';
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
|
||||
let searchTerm = $state('');
|
||||
let records = $derived(${{MODEL_NAME}}Cache.records);
|
||||
let filteredRecords = $derived(filterRecords(records, searchTerm, ['x_name']));
|
||||
let status = $derived($cacheStatus);
|
||||
|
||||
onMount(async () => {
|
||||
await {{MODEL_NAME}}Cache.initialize();
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
{{MODEL_NAME}}Cache.destroy();
|
||||
});
|
||||
|
||||
async function handleDelete(id) {
|
||||
if (!confirm('Are you sure you want to delete this {{MODEL_DISPLAY_NAME}}?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await {{MODEL_NAME}}Cache.deleteRecord(id);
|
||||
} catch (error) {
|
||||
alert(`Failed to delete: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
function handleRefresh() {
|
||||
{{MODEL_NAME}}Cache.forceRefresh();
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>All {{MODEL_DISPLAY_NAME}}s - {{PROJECT_NAME}}</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="container">
|
||||
<h1>📋 {{PROJECT_NAME}}</h1>
|
||||
|
||||
<nav>
|
||||
<a href="/">Add {{MODEL_DISPLAY_NAME}}</a>
|
||||
<a href="/list" class="active">View All</a>
|
||||
</nav>
|
||||
|
||||
<div class="list-container">
|
||||
<div class="list-header">
|
||||
<h2>All {{MODEL_DISPLAY_NAME}}s</h2>
|
||||
<div class="header-actions">
|
||||
<input
|
||||
type="search"
|
||||
placeholder="Search..."
|
||||
bind:value={searchTerm}
|
||||
class="search-input"
|
||||
/>
|
||||
<button class="refresh-btn" onclick={handleRefresh} disabled={status.isSyncing}>
|
||||
{status.isSyncing ? '⏳' : '🔄'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if status.isLoading}
|
||||
<div class="loading">Loading...</div>
|
||||
{:else if filteredRecords.length === 0}
|
||||
<div class="empty">
|
||||
{searchTerm ? 'No matching records found' : 'No {{MODEL_DISPLAY_NAME}}s yet. Add your first one!'}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="record-list">
|
||||
{#each filteredRecords as record (record.id)}
|
||||
<div class="record-card">
|
||||
<div class="record-content">
|
||||
<h3>{record.x_name}</h3>
|
||||
<!-- Add more fields to display -->
|
||||
<p class="record-meta">ID: {record.id}</p>
|
||||
</div>
|
||||
<div class="record-actions">
|
||||
<button class="delete-btn" onclick={() => handleDelete(record.id)}>
|
||||
🗑️
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if status.lastSync > 0}
|
||||
<div class="sync-info">
|
||||
Last synced: {new Date(status.lastSync).toLocaleString()}
|
||||
{#if status.isStale}
|
||||
<span class="stale-badge">Stale</span>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.container {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: white;
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
font-size: 2.5em;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
color: #333;
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
nav {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-bottom: 30px;
|
||||
background: white;
|
||||
border-radius: 10px;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
nav a {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
padding: 12px;
|
||||
text-decoration: none;
|
||||
color: #667eea;
|
||||
border-radius: 8px;
|
||||
font-weight: 600;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
nav a.active {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.list-container {
|
||||
background: white;
|
||||
padding: 24px;
|
||||
border-radius: 15px;
|
||||
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.list-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
flex-wrap: wrap;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
padding: 10px;
|
||||
border: 2px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.search-input:focus {
|
||||
outline: none;
|
||||
border-color: #667eea;
|
||||
}
|
||||
|
||||
.refresh-btn {
|
||||
padding: 10px 15px;
|
||||
background: #f0f0f0;
|
||||
color: #667eea;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 18px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.refresh-btn:hover:not(:disabled) {
|
||||
background: #e0e0e0;
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
|
||||
.refresh-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.loading,
|
||||
.empty {
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
color: #666;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.record-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.record-card {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px;
|
||||
border: 2px solid #e0e0e0;
|
||||
border-radius: 10px;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.record-card:hover {
|
||||
border-color: #667eea;
|
||||
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.1);
|
||||
}
|
||||
|
||||
.record-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.record-content h3 {
|
||||
margin: 0 0 8px 0;
|
||||
color: #333;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
.record-meta {
|
||||
margin: 0;
|
||||
color: #666;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.record-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.delete-btn {
|
||||
padding: 8px 12px;
|
||||
background: #fff5f5;
|
||||
color: #e53e3e;
|
||||
border: 1px solid #feb2b2;
|
||||
border-radius: 6px;
|
||||
font-size: 18px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.delete-btn:hover {
|
||||
background: #fed7d7;
|
||||
}
|
||||
|
||||
.sync-info {
|
||||
margin-top: 20px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid #e0e0e0;
|
||||
text-align: center;
|
||||
color: #666;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.stale-badge {
|
||||
display: inline-block;
|
||||
margin-left: 10px;
|
||||
padding: 2px 8px;
|
||||
background: #fef5e7;
|
||||
color: #f39c12;
|
||||
border-radius: 4px;
|
||||
font-size: 0.85em;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.list-header {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
min-width: auto;
|
||||
}
|
||||
|
||||
.record-card {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.record-actions {
|
||||
align-self: flex-end;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,4 @@
|
||||
// Disable SSR for this app (client-side only with Odoo backend)
|
||||
export const ssr = false;
|
||||
export const csr = true;
|
||||
export const prerender = true;
|
||||
@@ -0,0 +1,24 @@
|
||||
<script>
|
||||
let { children } = $props();
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{{PROJECT_NAME}} - {{MODEL_DISPLAY_NAME}} Manager</title>
|
||||
<meta name="description" content="Offline-first {{MODEL_DISPLAY_NAME}} management app with Odoo integration" />
|
||||
</svelte:head>
|
||||
|
||||
{@render children?.()}
|
||||
|
||||
<style>
|
||||
:global(body) {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu,
|
||||
Cantarell, 'Helvetica Neue', sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
:global(*) {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,271 @@
|
||||
<script>
|
||||
import { {{MODEL_NAME}}Cache } from '$lib/stores/cache';
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
|
||||
let name = $state('');
|
||||
let loading = $state(false);
|
||||
let message = $state('');
|
||||
let isOffline = $state(!navigator.onLine);
|
||||
|
||||
// Listen for online/offline events
|
||||
if (typeof window !== 'undefined') {
|
||||
window.addEventListener('online', () => { isOffline = false; });
|
||||
window.addEventListener('offline', () => { isOffline = true; });
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
await {{MODEL_NAME}}Cache.initialize();
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
{{MODEL_NAME}}Cache.destroy();
|
||||
});
|
||||
|
||||
async function handleSubmit() {
|
||||
if (!name.trim()) {
|
||||
message = '⚠️ Please fill in the name field';
|
||||
return;
|
||||
}
|
||||
|
||||
loading = true;
|
||||
message = '';
|
||||
|
||||
try {
|
||||
const payload = {
|
||||
x_name: name
|
||||
// Add more fields as needed
|
||||
};
|
||||
|
||||
await {{MODEL_NAME}}Cache.createRecord(payload);
|
||||
|
||||
if (navigator.onLine) {
|
||||
message = '✅ {{MODEL_DISPLAY_NAME}} added successfully!';
|
||||
} else {
|
||||
message = '✅ {{MODEL_DISPLAY_NAME}} saved locally! Will sync when online.';
|
||||
}
|
||||
|
||||
// Reset form
|
||||
name = '';
|
||||
} catch (error) {
|
||||
message = `❌ Error: ${error.message}`;
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleRefresh() {
|
||||
{{MODEL_NAME}}Cache.forceRefresh();
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Add {{MODEL_DISPLAY_NAME}} - {{PROJECT_NAME}}</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="container">
|
||||
<h1>📋 {{PROJECT_NAME}}</h1>
|
||||
|
||||
<!-- Offline Indicator -->
|
||||
{#if isOffline}
|
||||
<div class="offline-banner">
|
||||
📡 Offline Mode - Data will be synced when you're back online
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<nav>
|
||||
<a href="/" class="active">Add {{MODEL_DISPLAY_NAME}}</a>
|
||||
<a href="/list">View All</a>
|
||||
</nav>
|
||||
|
||||
<form onsubmit={(e) => { e.preventDefault(); handleSubmit(); }}>
|
||||
<div class="form-group">
|
||||
<label for="name">Name</label>
|
||||
<input
|
||||
type="text"
|
||||
id="name"
|
||||
bind:value={name}
|
||||
placeholder="Enter {{MODEL_DISPLAY_NAME}} name"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Add more form fields based on your Odoo model -->
|
||||
|
||||
{#if message}
|
||||
<div class="message" class:error={message.includes('❌')}>{message}</div>
|
||||
{/if}
|
||||
|
||||
<div class="button-group">
|
||||
<button type="submit" disabled={loading}>
|
||||
{loading ? '⏳ Adding...' : '➕ Add {{MODEL_DISPLAY_NAME}}'}
|
||||
</button>
|
||||
{#if !isOffline}
|
||||
<button type="button" class="refresh-btn" onclick={handleRefresh}>
|
||||
🔄 Refresh Data
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.container {
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.container {
|
||||
padding: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: white;
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
font-size: 2.5em;
|
||||
}
|
||||
|
||||
nav {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-bottom: 30px;
|
||||
background: white;
|
||||
border-radius: 10px;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
nav a {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
padding: 12px;
|
||||
text-decoration: none;
|
||||
color: #667eea;
|
||||
border-radius: 8px;
|
||||
font-weight: 600;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
nav a.active {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.offline-banner {
|
||||
background: #e3f2fd;
|
||||
color: #1565c0;
|
||||
padding: 12px 20px;
|
||||
border-radius: 10px;
|
||||
margin-bottom: 20px;
|
||||
text-align: center;
|
||||
font-weight: 600;
|
||||
border: 2px solid #64b5f6;
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.8; }
|
||||
}
|
||||
|
||||
form {
|
||||
background: white;
|
||||
padding: 24px;
|
||||
border-radius: 15px;
|
||||
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
form {
|
||||
padding: 16px;
|
||||
border-radius: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
input,
|
||||
select,
|
||||
textarea {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
border: 2px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
transition: border-color 0.3s;
|
||||
}
|
||||
|
||||
input:focus,
|
||||
select:focus,
|
||||
textarea:focus {
|
||||
outline: none;
|
||||
border-color: #667eea;
|
||||
}
|
||||
|
||||
.button-group {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
button {
|
||||
flex: 1;
|
||||
min-width: 150px;
|
||||
padding: 15px;
|
||||
background: #667eea;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
button:hover:not(:disabled) {
|
||||
background: #5568d3;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 5px 20px rgba(102, 126, 234, 0.4);
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.refresh-btn {
|
||||
background: #f0f0f0;
|
||||
color: #667eea;
|
||||
flex: 0 1 auto;
|
||||
}
|
||||
|
||||
.refresh-btn:hover:not(:disabled) {
|
||||
background: #e0e0e0;
|
||||
color: #5568d3;
|
||||
}
|
||||
|
||||
.message {
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 15px;
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.message.error {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "{{PROJECT_NAME}}",
|
||||
"short_name": "{{PROJECT_NAME}}",
|
||||
"description": "{{MODEL_DISPLAY_NAME}} management app with Odoo integration",
|
||||
"start_url": "/",
|
||||
"scope": "/",
|
||||
"display": "standalone",
|
||||
"background_color": "#ffffff",
|
||||
"theme_color": "#667eea",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/icon-192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "/icon-512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
# Odoo Instance Configuration (for backend server)
|
||||
ODOO_URL=https://your-instance.odoo.com
|
||||
ODOO_DB=your-database-name
|
||||
ODOO_USERNAME=your-username
|
||||
ODOO_API_KEY=your-api-key
|
||||
|
||||
# Primary Model (use x_ prefix for Odoo Studio models)
|
||||
ODOO_PRIMARY_MODEL=x_{{MODEL_NAME}}
|
||||
|
||||
# Frontend: API endpoint (if frontend and backend are separate)
|
||||
VITE_API_URL=http://localhost:3000
|
||||
@@ -0,0 +1,35 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
.DS_Store
|
||||
dist
|
||||
dist-ssr
|
||||
coverage
|
||||
*.local
|
||||
|
||||
/cypress/videos/
|
||||
/cypress/screenshots/
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
*.tsbuildinfo
|
||||
|
||||
# Environment
|
||||
.env
|
||||
.env.local
|
||||
.env.production
|
||||
@@ -0,0 +1,16 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="theme-color" content="#667eea" />
|
||||
<meta name="description" content="{{MODEL_DISPLAY_NAME}} management PWA with Odoo integration" />
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
<title>{{PROJECT_NAME}}</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "{{PROJECT_NAME}}",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"vue": "^3.4.29",
|
||||
"vue-router": "^4.3.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^5.0.5",
|
||||
"vite": "^5.3.1",
|
||||
"vite-plugin-pwa": "^0.20.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import vue from '@vitejs/plugin-vue';
|
||||
import { VitePWA } from 'vite-plugin-pwa';
|
||||
import { fileURLToPath, URL } from 'node:url';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
vue(),
|
||||
VitePWA({
|
||||
registerType: 'autoUpdate',
|
||||
workbox: {
|
||||
globPatterns: ['**/*.{js,css,html,ico,png,svg,json,webp}']
|
||||
},
|
||||
manifest: {
|
||||
name: '{{PROJECT_NAME}}',
|
||||
short_name: '{{PROJECT_NAME}}',
|
||||
description: 'PWA for {{MODEL_DISPLAY_NAME}} management with Odoo integration',
|
||||
theme_color: '#667eea',
|
||||
background_color: '#ffffff',
|
||||
display: 'standalone',
|
||||
icons: [
|
||||
{
|
||||
src: '/icon-192.png',
|
||||
sizes: '192x192',
|
||||
type: 'image/png'
|
||||
},
|
||||
{
|
||||
src: '/icon-512.png',
|
||||
sizes: '512x512',
|
||||
type: 'image/png'
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||
}
|
||||
},
|
||||
server: {
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: process.env.VITE_API_URL || 'http://localhost:3000',
|
||||
changeOrigin: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
74
skills/create-odoo-pwa/templates/vue/src/App.vue.template
Normal file
74
skills/create-odoo-pwa/templates/vue/src/App.vue.template
Normal file
@@ -0,0 +1,74 @@
|
||||
<template>
|
||||
<div class="app">
|
||||
<div class="container">
|
||||
<h1>📋 {{PROJECT_NAME}}</h1>
|
||||
|
||||
<OfflineBanner />
|
||||
|
||||
<nav class="nav">
|
||||
<router-link
|
||||
to="/"
|
||||
:class="{ active: $route.path === '/' }"
|
||||
>
|
||||
Add {{MODEL_DISPLAY_NAME}}
|
||||
</router-link>
|
||||
<router-link
|
||||
to="/list"
|
||||
:class="{ active: $route.path === '/list' }"
|
||||
>
|
||||
View All
|
||||
</router-link>
|
||||
</nav>
|
||||
|
||||
<router-view />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import OfflineBanner from './components/OfflineBanner.vue';
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.app {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: white;
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
font-size: 2.5em;
|
||||
}
|
||||
|
||||
.nav {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-bottom: 30px;
|
||||
background: white;
|
||||
border-radius: 10px;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.nav a {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
padding: 12px;
|
||||
text-decoration: none;
|
||||
color: #667eea;
|
||||
border-radius: 8px;
|
||||
font-weight: 600;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.nav a.active {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
}
|
||||
</style>
|
||||
123
skills/create-odoo-pwa/templates/vue/src/api/odoo.js.template
Normal file
123
skills/create-odoo-pwa/templates/vue/src/api/odoo.js.template
Normal file
@@ -0,0 +1,123 @@
|
||||
/**
|
||||
* Odoo API Client for React
|
||||
* Communicates with backend server that proxies to Odoo
|
||||
*/
|
||||
|
||||
class OdooAPI {
|
||||
constructor() {
|
||||
this.apiUrl = import.meta.env.VITE_API_URL
|
||||
? `${import.meta.env.VITE_API_URL}/api/odoo`
|
||||
: '/api/odoo';
|
||||
}
|
||||
|
||||
/**
|
||||
* Call the server-side API
|
||||
* @param {string} action
|
||||
* @param {any} data
|
||||
* @returns {Promise<any>}
|
||||
*/
|
||||
async callApi(action, data) {
|
||||
const response = await fetch(this.apiUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ action, data })
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'API Error');
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new record in specified model
|
||||
* @param {string} model - Odoo model name
|
||||
* @param {Record<string, any>} fields - Record fields
|
||||
* @returns {Promise<number>} - Created record ID
|
||||
*/
|
||||
async createRecord(model, fields) {
|
||||
const result = await this.callApi('create', { model, fields });
|
||||
return result.id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search and read records from specified model
|
||||
* @param {string} model - Odoo model name
|
||||
* @param {any[]} domain - Odoo domain filter
|
||||
* @param {string[]} fields - Fields to retrieve
|
||||
* @returns {Promise<any[]>} - Array of records
|
||||
*/
|
||||
async searchRecords(model, domain = [], fields = []) {
|
||||
const result = await this.callApi('search', { model, domain, fields });
|
||||
return result.results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic search_read for any model
|
||||
* @param {string} model
|
||||
* @param {any[]} domain
|
||||
* @param {string[]} fields
|
||||
* @returns {Promise<any[]>}
|
||||
*/
|
||||
async searchModel(model, domain = [], fields = []) {
|
||||
const result = await this.callApi('search_model', { model, domain, fields });
|
||||
return result.results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch partner list (common operation)
|
||||
* @returns {Promise<Array<{id:number, display_name:string}>>}
|
||||
*/
|
||||
async fetchPartners() {
|
||||
return await this.searchModel('res.partner', [], ['id', 'display_name']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a many2one field value
|
||||
* @param {number|string|null|undefined} id
|
||||
* @returns {number|false}
|
||||
*/
|
||||
formatMany2one(id) {
|
||||
return id ? Number(id) : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a many2many field using the (6,0,[ids]) command
|
||||
* @param {Array<number|string>} ids
|
||||
* @returns {any[]}
|
||||
*/
|
||||
formatMany2many(ids) {
|
||||
if (!Array.isArray(ids) || ids.length === 0) return [];
|
||||
return [[6, 0, ids.map((i) => Number(i))]];
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a record
|
||||
* @param {string} model - Odoo model name
|
||||
* @param {number} id - Record ID
|
||||
* @param {Record<string, any>} values - Fields to update
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
async updateRecord(model, id, values) {
|
||||
const result = await this.callApi('update', { model, id, values });
|
||||
return result.result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a record
|
||||
* @param {string} model - Odoo model name
|
||||
* @param {number} id - Record ID
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
async deleteRecord(model, id) {
|
||||
const result = await this.callApi('delete', { model, id });
|
||||
return result.result;
|
||||
}
|
||||
}
|
||||
|
||||
export const odooClient = new OdooAPI();
|
||||
@@ -0,0 +1,48 @@
|
||||
<template>
|
||||
<div v-if="isOffline" class="offline-banner">
|
||||
📡 Offline Mode - Data will be synced when you're back online
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onUnmounted } from 'vue';
|
||||
|
||||
const isOffline = ref(!navigator.onLine);
|
||||
|
||||
const handleOnline = () => isOffline.value = false;
|
||||
const handleOffline = () => isOffline.value = true;
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('online', handleOnline);
|
||||
window.addEventListener('offline', handleOffline);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('online', handleOnline);
|
||||
window.removeEventListener('offline', handleOffline);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.offline-banner {
|
||||
background: #e3f2fd;
|
||||
color: #1565c0;
|
||||
padding: 12px 20px;
|
||||
border-radius: 10px;
|
||||
margin-bottom: 20px;
|
||||
text-align: center;
|
||||
font-weight: 600;
|
||||
border: 2px solid #64b5f6;
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,224 @@
|
||||
import { ref, onMounted, onUnmounted } from 'vue';
|
||||
import { odooClient } from '../api/odoo';
|
||||
|
||||
// Cache configuration
|
||||
const CACHE_KEY = '{{MODEL_NAME}}_cache_v1';
|
||||
const CACHE_META_KEY = '{{MODEL_NAME}}_cache_meta_v1';
|
||||
const CACHE_DURATION_MS = 5 * 60 * 1000; // 5 minutes
|
||||
const SYNC_INTERVAL_MS = 3 * 60 * 1000; // 3 minutes
|
||||
|
||||
// Helper functions for localStorage
|
||||
function loadFromStorage() {
|
||||
try {
|
||||
const cachedData = localStorage.getItem(CACHE_KEY);
|
||||
const meta = localStorage.getItem(CACHE_META_KEY);
|
||||
|
||||
if (cachedData && meta) {
|
||||
const records = JSON.parse(cachedData);
|
||||
const metaData = JSON.parse(meta);
|
||||
|
||||
const now = Date.now();
|
||||
const isStale = now - metaData.lastSyncTime > CACHE_DURATION_MS;
|
||||
|
||||
return { records, meta: { ...metaData, isStale } };
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Failed to load cache:', e);
|
||||
}
|
||||
|
||||
return {
|
||||
records: [],
|
||||
meta: {
|
||||
lastSyncTime: 0,
|
||||
lastRecordId: 0,
|
||||
recordCount: 0,
|
||||
isStale: true
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function saveToStorage(records, meta) {
|
||||
try {
|
||||
localStorage.setItem(CACHE_KEY, JSON.stringify(records));
|
||||
localStorage.setItem(CACHE_META_KEY, JSON.stringify({
|
||||
lastSyncTime: meta.lastSyncTime || Date.now(),
|
||||
lastRecordId: meta.lastRecordId || 0,
|
||||
recordCount: records.length,
|
||||
isStale: false
|
||||
}));
|
||||
} catch (e) {
|
||||
console.warn('Failed to save cache:', e);
|
||||
}
|
||||
}
|
||||
|
||||
export function useTaskCache() {
|
||||
const records = ref([]);
|
||||
const loading = ref(false);
|
||||
const syncing = ref(false);
|
||||
const error = ref('');
|
||||
const meta = ref({
|
||||
lastSyncTime: 0,
|
||||
lastRecordId: 0,
|
||||
recordCount: 0,
|
||||
isStale: true
|
||||
});
|
||||
|
||||
let syncInterval = null;
|
||||
|
||||
// Sync function
|
||||
const sync = async (forceFullRefresh = false) => {
|
||||
syncing.value = true;
|
||||
error.value = '';
|
||||
|
||||
try {
|
||||
const fields = ['id', 'x_name'];
|
||||
let domain = [];
|
||||
let fetchedRecords = [];
|
||||
|
||||
if (!forceFullRefresh && meta.value.lastRecordId > 0) {
|
||||
domain = [['id', '>', meta.value.lastRecordId]];
|
||||
try {
|
||||
fetchedRecords = await odooClient.searchRecords('x_{{MODEL_NAME}}', domain, fields);
|
||||
} catch (err) {
|
||||
console.warn('Incremental fetch failed:', err);
|
||||
forceFullRefresh = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (forceFullRefresh || meta.value.lastRecordId === 0) {
|
||||
fetchedRecords = await odooClient.searchRecords('x_{{MODEL_NAME}}', [], fields);
|
||||
}
|
||||
|
||||
let mergedRecords;
|
||||
if (forceFullRefresh || meta.value.lastRecordId === 0) {
|
||||
mergedRecords = fetchedRecords;
|
||||
} else {
|
||||
const existingIds = new Set(records.value.map(r => r.id));
|
||||
const newRecords = fetchedRecords.filter(r => !existingIds.has(r.id));
|
||||
mergedRecords = [...records.value, ...newRecords];
|
||||
}
|
||||
|
||||
mergedRecords.sort((a, b) => a.id - b.id);
|
||||
|
||||
const lastRecordId = mergedRecords.length > 0
|
||||
? Math.max(...mergedRecords.map(r => r.id))
|
||||
: 0;
|
||||
|
||||
const newMeta = {
|
||||
lastSyncTime: Date.now(),
|
||||
lastRecordId,
|
||||
recordCount: mergedRecords.length,
|
||||
isStale: false
|
||||
};
|
||||
|
||||
saveToStorage(mergedRecords, newMeta);
|
||||
|
||||
records.value = mergedRecords;
|
||||
meta.value = newMeta;
|
||||
} catch (err) {
|
||||
console.error('Sync failed:', err);
|
||||
error.value = err.message || 'Failed to sync data';
|
||||
} finally {
|
||||
syncing.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// Initialize
|
||||
const initialize = async () => {
|
||||
const cachedState = loadFromStorage();
|
||||
|
||||
if (cachedState.records.length > 0) {
|
||||
records.value = cachedState.records;
|
||||
meta.value = cachedState.meta;
|
||||
|
||||
if (cachedState.meta.isStale) {
|
||||
await sync();
|
||||
}
|
||||
} else {
|
||||
loading.value = true;
|
||||
await sync(true);
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
// Set up periodic sync
|
||||
syncInterval = setInterval(() => {
|
||||
sync();
|
||||
}, SYNC_INTERVAL_MS);
|
||||
};
|
||||
|
||||
// Cleanup
|
||||
const cleanup = () => {
|
||||
if (syncInterval) {
|
||||
clearInterval(syncInterval);
|
||||
syncInterval = null;
|
||||
}
|
||||
};
|
||||
|
||||
// Force refresh
|
||||
const forceRefresh = async () => {
|
||||
await sync(true);
|
||||
};
|
||||
|
||||
// Create record
|
||||
const createRecord = async (fields) => {
|
||||
try {
|
||||
const id = await odooClient.createRecord('x_{{MODEL_NAME}}', fields);
|
||||
const newRecord = { id, ...fields };
|
||||
records.value = [...records.value, newRecord];
|
||||
await sync();
|
||||
return id;
|
||||
} catch (err) {
|
||||
console.error('Failed to create record:', err);
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
// Update record
|
||||
const updateRecord = async (id, values) => {
|
||||
try {
|
||||
await odooClient.updateRecord('x_{{MODEL_NAME}}', id, values);
|
||||
records.value = records.value.map(r =>
|
||||
r.id === id ? { ...r, ...values } : r
|
||||
);
|
||||
await sync();
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.error('Failed to update record:', err);
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
// Delete record
|
||||
const deleteRecord = async (id) => {
|
||||
try {
|
||||
await odooClient.deleteRecord('x_{{MODEL_NAME}}', id);
|
||||
records.value = records.value.filter(r => r.id !== id);
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.error('Failed to delete record:', err);
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
// Auto-initialize and cleanup on component lifecycle
|
||||
onMounted(() => {
|
||||
initialize();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
return {
|
||||
records,
|
||||
loading,
|
||||
syncing,
|
||||
error,
|
||||
meta,
|
||||
sync,
|
||||
forceRefresh,
|
||||
createRecord,
|
||||
updateRecord,
|
||||
deleteRecord
|
||||
};
|
||||
}
|
||||
18
skills/create-odoo-pwa/templates/vue/src/main.js.template
Normal file
18
skills/create-odoo-pwa/templates/vue/src/main.js.template
Normal file
@@ -0,0 +1,18 @@
|
||||
import { createApp } from 'vue';
|
||||
import { createRouter, createWebHistory } from 'vue-router';
|
||||
import App from './App.vue';
|
||||
import AddPage from './pages/AddPage.vue';
|
||||
import ListPage from './pages/ListPage.vue';
|
||||
import './style.css';
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes: [
|
||||
{ path: '/', component: AddPage },
|
||||
{ path: '/list', component: ListPage }
|
||||
]
|
||||
});
|
||||
|
||||
createApp(App)
|
||||
.use(router)
|
||||
.mount('#app');
|
||||
@@ -0,0 +1,161 @@
|
||||
<template>
|
||||
<form @submit.prevent="handleSubmit" class="form">
|
||||
<div class="form-group">
|
||||
<label for="name">Name</label>
|
||||
<input
|
||||
type="text"
|
||||
id="name"
|
||||
v-model="name"
|
||||
placeholder="Enter {{MODEL_DISPLAY_NAME}} name"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="message" :class="['message', { error: message.includes('❌') }]">
|
||||
{{ message }}
|
||||
</div>
|
||||
|
||||
<div class="button-group">
|
||||
<button type="submit" :disabled="loading">
|
||||
{{ loading ? '⏳ Adding...' : '➕ Add {{MODEL_DISPLAY_NAME}}' }}
|
||||
</button>
|
||||
<button
|
||||
v-if="isOnline"
|
||||
type="button"
|
||||
class="refresh-btn"
|
||||
@click="forceRefresh"
|
||||
>
|
||||
🔄 Refresh Data
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import { useTaskCache } from '../composables/useTaskCache';
|
||||
|
||||
const { createRecord, forceRefresh } = useTaskCache();
|
||||
|
||||
const name = ref('');
|
||||
const loading = ref(false);
|
||||
const message = ref('');
|
||||
const isOnline = computed(() => navigator.onLine);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!name.value.trim()) {
|
||||
message.value = '⚠️ Please fill in the name field';
|
||||
return;
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
message.value = '';
|
||||
|
||||
try {
|
||||
const payload = { x_name: name.value };
|
||||
await createRecord(payload);
|
||||
|
||||
if (navigator.onLine) {
|
||||
message.value = '✅ {{MODEL_DISPLAY_NAME}} added successfully!';
|
||||
} else {
|
||||
message.value = '✅ {{MODEL_DISPLAY_NAME}} saved locally! Will sync when online.';
|
||||
}
|
||||
|
||||
name.value = '';
|
||||
} catch (error) {
|
||||
message.value = `❌ Error: ${error.message}`;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.form {
|
||||
background: white;
|
||||
padding: 24px;
|
||||
border-radius: 15px;
|
||||
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
border: 2px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
transition: border-color 0.3s;
|
||||
}
|
||||
|
||||
input:focus {
|
||||
outline: none;
|
||||
border-color: #667eea;
|
||||
}
|
||||
|
||||
.button-group {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
button {
|
||||
flex: 1;
|
||||
min-width: 150px;
|
||||
padding: 15px;
|
||||
background: #667eea;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
button:hover:not(:disabled) {
|
||||
background: #5568d3;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 5px 20px rgba(102, 126, 234, 0.4);
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.refresh-btn {
|
||||
background: #f0f0f0;
|
||||
color: #667eea;
|
||||
flex: 0 1 auto;
|
||||
}
|
||||
|
||||
.refresh-btn:hover:not(:disabled) {
|
||||
background: #e0e0e0;
|
||||
color: #5568d3;
|
||||
}
|
||||
|
||||
.message {
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 15px;
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.message.error {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,200 @@
|
||||
<template>
|
||||
<div class="list-container">
|
||||
<div class="list-header">
|
||||
<h2>All {{MODEL_DISPLAY_NAME}}s</h2>
|
||||
<div class="header-actions">
|
||||
<input
|
||||
type="search"
|
||||
v-model="searchTerm"
|
||||
placeholder="Search..."
|
||||
class="search-input"
|
||||
/>
|
||||
<button
|
||||
class="refresh-btn-small"
|
||||
@click="forceRefresh"
|
||||
:disabled="syncing"
|
||||
>
|
||||
{{ syncing ? '⏳' : '🔄' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="loading" class="loading">Loading...</div>
|
||||
<div v-else-if="filteredRecords.length === 0" class="empty">
|
||||
{{ searchTerm ? 'No matching records found' : 'No {{MODEL_DISPLAY_NAME}}s yet. Add your first one!' }}
|
||||
</div>
|
||||
<div v-else class="record-list">
|
||||
<div
|
||||
v-for="record in filteredRecords"
|
||||
:key="record.id"
|
||||
class="record-card"
|
||||
>
|
||||
<div class="record-content">
|
||||
<h3>{{ record.x_name }}</h3>
|
||||
<p class="record-meta">ID: {{ record.id }}</p>
|
||||
</div>
|
||||
<div class="record-actions">
|
||||
<button class="delete-btn" @click="handleDelete(record.id)">
|
||||
🗑️
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import { useTaskCache } from '../composables/useTaskCache';
|
||||
|
||||
const { records, loading, syncing, forceRefresh, deleteRecord } = useTaskCache();
|
||||
|
||||
const searchTerm = ref('');
|
||||
|
||||
const filteredRecords = computed(() => {
|
||||
return records.value.filter(record =>
|
||||
record.x_name?.toLowerCase().includes(searchTerm.value.toLowerCase())
|
||||
);
|
||||
});
|
||||
|
||||
const handleDelete = async (id) => {
|
||||
if (!confirm('Are you sure you want to delete this {{MODEL_DISPLAY_NAME}}?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await deleteRecord(id);
|
||||
} catch (error) {
|
||||
alert(`Failed to delete: ${error.message}`);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.list-container {
|
||||
background: white;
|
||||
padding: 24px;
|
||||
border-radius: 15px;
|
||||
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.list-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
flex-wrap: wrap;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
color: #333;
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
padding: 10px;
|
||||
border: 2px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.search-input:focus {
|
||||
outline: none;
|
||||
border-color: #667eea;
|
||||
}
|
||||
|
||||
.refresh-btn-small {
|
||||
padding: 10px 15px;
|
||||
background: #f0f0f0;
|
||||
color: #667eea;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 18px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.refresh-btn-small:hover:not(:disabled) {
|
||||
background: #e0e0e0;
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
|
||||
.refresh-btn-small:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.loading,
|
||||
.empty {
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
color: #666;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.record-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.record-card {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px;
|
||||
border: 2px solid #e0e0e0;
|
||||
border-radius: 10px;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.record-card:hover {
|
||||
border-color: #667eea;
|
||||
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.1);
|
||||
}
|
||||
|
||||
.record-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.record-content h3 {
|
||||
margin: 0 0 8px 0;
|
||||
color: #333;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
.record-meta {
|
||||
margin: 0;
|
||||
color: #666;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.record-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.delete-btn {
|
||||
padding: 8px 12px;
|
||||
background: #fff5f5;
|
||||
color: #e53e3e;
|
||||
border: 1px solid #feb2b2;
|
||||
border-radius: 6px;
|
||||
font-size: 18px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.delete-btn:hover {
|
||||
background: #fed7d7;
|
||||
}
|
||||
</style>
|
||||
29
skills/create-odoo-pwa/templates/vue/src/style.css.template
Normal file
29
skills/create-odoo-pwa/templates/vue/src/style.css.template
Normal file
@@ -0,0 +1,29 @@
|
||||
:root {
|
||||
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu,
|
||||
Cantarell, 'Helvetica Neue', sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
#app {
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
}
|
||||
Reference in New Issue
Block a user