mirror of
https://github.com/stan-smith/FossFLOW.git
synced 2025-12-24 06:58:48 -05:00
feat: add LLM-friendly export features and format code with Prettier
This commit is contained in:
315
LLM-GENERATION-GUIDE.md
Normal file
315
LLM-GENERATION-GUIDE.md
Normal file
@@ -0,0 +1,315 @@
|
||||
# LLM Generation Guide for Isoflow Compact Format
|
||||
|
||||
## Overview
|
||||
This guide explains how to generate JSON files in the Isoflow compact format that can be successfully imported into the application. The compact format is designed for LLM generation with minimal token usage while preserving all essential diagram information.
|
||||
|
||||
## Format Structure
|
||||
|
||||
The compact format uses this JSON structure:
|
||||
|
||||
```json
|
||||
{
|
||||
"t": "Diagram Title (max 40 chars)",
|
||||
"i": [
|
||||
["Item Name (max 30 chars)", "icon_name", "Description (max 100 chars)"],
|
||||
["Another Item", "storage", "Database server for user data"]
|
||||
],
|
||||
"v": [
|
||||
[
|
||||
[[0, 2, 4], [1, -2, 6]],
|
||||
[[0, 1], [1, 0]]
|
||||
]
|
||||
],
|
||||
"_": { "f": "compact", "v": "1.0" }
|
||||
}
|
||||
```
|
||||
|
||||
## Structure Explanation
|
||||
|
||||
### Root Level
|
||||
- `t`: **Title** - Short diagram title (max 40 characters)
|
||||
- `i`: **Items** - Array of diagram elements
|
||||
- `v`: **Views** - Array of views (usually just one)
|
||||
- `_`: **Metadata** - Format identifier (always `{"f": "compact", "v": "1.0"}`)
|
||||
|
||||
### Items Array (`i`)
|
||||
Each item is an array with 3 elements:
|
||||
1. **Name** (string, max 30 chars): Display name of the item
|
||||
2. **Icon** (string): Icon identifier from available icons
|
||||
3. **Description** (string, max 100 chars): Brief description
|
||||
|
||||
### Views Array (`v`)
|
||||
Each view contains:
|
||||
1. **Positions Array**: `[[itemIndex, x, y], ...]` - Position of each item
|
||||
2. **Connections Array**: `[[fromIndex, toIndex], ...]` - Connections between items
|
||||
|
||||
## Available Icons
|
||||
|
||||
### Basic Icons (ISOFLOW Collection)
|
||||
Common icons for general use:
|
||||
- `storage` - Database/storage
|
||||
- `server` - Generic server
|
||||
- `user` - User/person
|
||||
- `cloud` - Cloud services
|
||||
- `network` - Network component
|
||||
- `security` - Security/firewall
|
||||
- `api` - API/interface
|
||||
- `queue` - Message queue
|
||||
- `cache` - Caching system
|
||||
- `function` - Function/lambda
|
||||
- `mobile` - Mobile device
|
||||
- `web` - Web application
|
||||
- `email` - Email service
|
||||
- `analytics` - Analytics/monitoring
|
||||
- `backup` - Backup system
|
||||
- `load-balancer` - Load balancer
|
||||
- `cdn` - Content delivery network
|
||||
- `vpn` - VPN connection
|
||||
- `firewall` - Firewall/security
|
||||
- `monitor` - Monitoring system
|
||||
|
||||
### AWS Icons (320 available)
|
||||
Use `aws-` prefix for AWS services:
|
||||
- `aws-ec2` - EC2 instances
|
||||
- `aws-s3` - S3 storage
|
||||
- `aws-rds` - RDS database
|
||||
- `aws-lambda` - Lambda functions
|
||||
- `aws-api-gateway` - API Gateway
|
||||
- `aws-cloudfront` - CloudFront CDN
|
||||
- `aws-route-53` - Route 53 DNS
|
||||
- `aws-vpc` - Virtual Private Cloud
|
||||
- `aws-elb` - Load balancer
|
||||
- `aws-iam` - Identity management
|
||||
- `aws-cloudwatch` - Monitoring
|
||||
- `aws-sns` - Simple notification
|
||||
- `aws-sqs` - Simple queue
|
||||
- `aws-dynamodb` - DynamoDB database
|
||||
- `aws-elasticsearch` - Elasticsearch
|
||||
- `aws-redshift` - Data warehouse
|
||||
- `aws-kinesis` - Data streaming
|
||||
- `aws-eks` - Kubernetes service
|
||||
- `aws-fargate` - Container service
|
||||
- `aws-cognito` - User authentication
|
||||
|
||||
### Azure Icons (369 available)
|
||||
Use `azure-` prefix for Azure services:
|
||||
- `azure-virtual-machine` - Virtual machines
|
||||
- `azure-storage-account` - Storage
|
||||
- `azure-sql-database` - SQL database
|
||||
- `azure-app-service` - Web apps
|
||||
- `azure-function-app` - Functions
|
||||
- `azure-api-management` - API management
|
||||
- `azure-cdn` - Content delivery
|
||||
- `azure-dns` - DNS service
|
||||
- `azure-load-balancer` - Load balancer
|
||||
- `azure-active-directory` - Identity
|
||||
- `azure-monitor` - Monitoring
|
||||
- `azure-service-bus` - Message bus
|
||||
- `azure-cosmos-db` - NoSQL database
|
||||
- `azure-redis-cache` - Redis cache
|
||||
- `azure-kubernetes-service` - Kubernetes
|
||||
- `azure-container-instances` - Containers
|
||||
- `azure-logic-apps` - Logic apps
|
||||
- `azure-data-factory` - Data pipeline
|
||||
- `azure-key-vault` - Key management
|
||||
- `azure-cognitive-services` - AI services
|
||||
|
||||
### GCP Icons (280 available)
|
||||
Use `gcp-` prefix for Google Cloud services:
|
||||
- `gcp-compute-engine` - Virtual machines
|
||||
- `gcp-cloud-storage` - Storage
|
||||
- `gcp-cloud-sql` - SQL database
|
||||
- `gcp-app-engine` - Web apps
|
||||
- `gcp-cloud-functions` - Functions
|
||||
- `gcp-api-gateway` - API gateway
|
||||
- `gcp-cloud-cdn` - Content delivery
|
||||
- `gcp-cloud-dns` - DNS service
|
||||
- `gcp-cloud-load-balancing` - Load balancer
|
||||
- `gcp-identity-access-management` - IAM
|
||||
- `gcp-cloud-monitoring` - Monitoring
|
||||
- `gcp-cloud-pub-sub` - Message queue
|
||||
- `gcp-cloud-firestore` - NoSQL database
|
||||
- `gcp-memorystore` - Redis cache
|
||||
- `gcp-kubernetes-engine` - Kubernetes
|
||||
- `gcp-cloud-run` - Container service
|
||||
- `gcp-cloud-workflows` - Workflows
|
||||
- `gcp-cloud-dataflow` - Data pipeline
|
||||
- `gcp-secret-manager` - Secret management
|
||||
- `gcp-ai-platform` - AI/ML platform
|
||||
|
||||
### Kubernetes Icons (56 available)
|
||||
Use `k8s-` prefix for Kubernetes resources:
|
||||
- `k8s-pod` - Pods
|
||||
- `k8s-service` - Services
|
||||
- `k8s-deployment` - Deployments
|
||||
- `k8s-configmap` - ConfigMaps
|
||||
- `k8s-secret` - Secrets
|
||||
- `k8s-ingress` - Ingress
|
||||
- `k8s-namespace` - Namespaces
|
||||
- `k8s-node` - Nodes
|
||||
- `k8s-persistent-volume` - Storage
|
||||
- `k8s-daemonset` - DaemonSets
|
||||
- `k8s-statefulset` - StatefulSets
|
||||
- `k8s-job` - Jobs
|
||||
- `k8s-cronjob` - CronJobs
|
||||
- `k8s-hpa` - Auto-scaling
|
||||
- `k8s-rbac` - Role-based access
|
||||
|
||||
## Positioning System
|
||||
|
||||
The positioning system uses a grid-based coordinate system:
|
||||
- **X-axis**: Horizontal position (negative = left, positive = right)
|
||||
- **Y-axis**: Vertical position (negative = up, positive = down)
|
||||
- **Grid spacing**: Each unit represents one grid cell
|
||||
- **Typical range**: -20 to +20 for both axes
|
||||
|
||||
### Positioning Guidelines:
|
||||
- Start with main components around (0, 0)
|
||||
- Place related components close together
|
||||
- Use consistent spacing (3-5 units between components)
|
||||
- Arrange in logical flow (left to right, top to bottom)
|
||||
|
||||
## Connection Guidelines
|
||||
|
||||
Connections are defined as `[fromIndex, toIndex]` pairs:
|
||||
- **fromIndex**: Index of source item in items array
|
||||
- **toIndex**: Index of destination item in items array
|
||||
- **Direction**: Connections are directional (from → to)
|
||||
|
||||
### Common Connection Patterns:
|
||||
- **Linear flow**: [0,1], [1,2], [2,3]
|
||||
- **Hub and spoke**: [0,1], [0,2], [0,3]
|
||||
- **Mesh**: Multiple bidirectional connections
|
||||
- **Layered**: Connections between architectural layers
|
||||
|
||||
## Generation Examples
|
||||
|
||||
### Example 1: Simple Web Application
|
||||
|
||||
```json
|
||||
{
|
||||
"t": "Simple Web App Architecture",
|
||||
"i": [
|
||||
["Web App", "web", "Frontend application"],
|
||||
["API Gateway", "api", "API management layer"],
|
||||
["Database", "storage", "User data storage"],
|
||||
["Cache", "cache", "Redis caching layer"]
|
||||
],
|
||||
"v": [
|
||||
[
|
||||
[[0, -6, 0], [1, 0, 0], [2, 6, 0], [3, 0, -4]],
|
||||
[[0, 1], [1, 2], [1, 3]]
|
||||
]
|
||||
],
|
||||
"_": { "f": "compact", "v": "1.0" }
|
||||
}
|
||||
```
|
||||
|
||||
### Example 2: AWS Architecture
|
||||
|
||||
```json
|
||||
{
|
||||
"t": "AWS Serverless Architecture",
|
||||
"i": [
|
||||
["CloudFront", "aws-cloudfront", "Content delivery network"],
|
||||
["API Gateway", "aws-api-gateway", "API management"],
|
||||
["Lambda", "aws-lambda", "Serverless functions"],
|
||||
["DynamoDB", "aws-dynamodb", "NoSQL database"],
|
||||
["S3", "aws-s3", "Static file storage"]
|
||||
],
|
||||
"v": [
|
||||
[
|
||||
[[0, -8, -4], [1, 0, 0], [2, 0, 4], [3, 8, 4], [4, 8, -4]],
|
||||
[[0, 1], [1, 2], [2, 3], [0, 4]]
|
||||
]
|
||||
],
|
||||
"_": { "f": "compact", "v": "1.0" }
|
||||
}
|
||||
```
|
||||
|
||||
### Example 3: Kubernetes Architecture
|
||||
|
||||
```json
|
||||
{
|
||||
"t": "Kubernetes Application",
|
||||
"i": [
|
||||
["Ingress", "k8s-ingress", "Traffic routing"],
|
||||
["Frontend", "k8s-pod", "React application"],
|
||||
["API Service", "k8s-service", "Backend API"],
|
||||
["Database", "k8s-pod", "PostgreSQL database"],
|
||||
["ConfigMap", "k8s-configmap", "Configuration data"]
|
||||
],
|
||||
"v": [
|
||||
[
|
||||
[[0, 0, -6], [1, -4, 0], [2, 4, 0], [3, 4, 6], [4, -4, 6]],
|
||||
[[0, 1], [0, 2], [2, 3], [4, 1], [4, 2]]
|
||||
]
|
||||
],
|
||||
"_": { "f": "compact", "v": "1.0" }
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices for LLM Generation
|
||||
|
||||
### 1. Icon Selection
|
||||
- Use specific cloud provider icons when targeting that platform
|
||||
- Use generic icons for platform-agnostic diagrams
|
||||
- Match icon semantics to component function
|
||||
- Prefer well-known service icons over generic ones
|
||||
|
||||
### 2. Naming
|
||||
- Keep names concise but descriptive
|
||||
- Use standard terminology for components
|
||||
- Include version/type info when relevant
|
||||
- Avoid special characters in names
|
||||
|
||||
### 3. Descriptions
|
||||
- Provide context about component purpose
|
||||
- Include key technologies/versions
|
||||
- Mention important configurations
|
||||
- Keep under 100 characters
|
||||
|
||||
### 4. Layout
|
||||
- Group related components together
|
||||
- Use consistent spacing between layers
|
||||
- Consider data flow direction
|
||||
- Leave space for connection lines
|
||||
|
||||
### 5. Connections
|
||||
- Model actual data/control flow
|
||||
- Avoid crossing connections when possible
|
||||
- Use consistent connection semantics
|
||||
- Consider bidirectional vs unidirectional flows
|
||||
|
||||
## Validation Checklist
|
||||
|
||||
Before generating, ensure:
|
||||
- [ ] All icon names exist in available icons list
|
||||
- [ ] Item names are ≤ 30 characters
|
||||
- [ ] Descriptions are ≤ 100 characters
|
||||
- [ ] Title is ≤ 40 characters
|
||||
- [ ] Position coordinates are reasonable (-20 to +20)
|
||||
- [ ] Connection indices reference valid items
|
||||
- [ ] Metadata format is exactly `{"f": "compact", "v": "1.0"}`
|
||||
- [ ] JSON structure matches the required format
|
||||
- [ ] All required fields are present
|
||||
|
||||
## Common Pitfalls to Avoid
|
||||
|
||||
1. **Invalid icon names**: Always use exact icon IDs from the available list
|
||||
2. **Missing descriptions**: Always provide the third element in item arrays
|
||||
3. **Incorrect metadata**: Use exact format `{"f": "compact", "v": "1.0"}`
|
||||
4. **Invalid connections**: Ensure indices refer to existing items
|
||||
5. **Extreme coordinates**: Keep positions within reasonable bounds
|
||||
6. **Missing views**: Always include at least one view with positions
|
||||
7. **Inconsistent arrays**: Ensure positions and items arrays align
|
||||
|
||||
## Token Optimization Tips
|
||||
|
||||
- Use shorter but meaningful names
|
||||
- Truncate descriptions to essential info
|
||||
- Use efficient coordinate values
|
||||
- Minimize redundant connections
|
||||
- Group related components to reduce positioning complexity
|
||||
|
||||
This format typically uses 70-90% fewer tokens than the full JSON format while maintaining complete functionality and visual fidelity when imported into the Isoflow application.
|
||||
1139
icon-list-generation-guide.md
Normal file
1139
icon-list-generation-guide.md
Normal file
File diff suppressed because it is too large
Load Diff
@@ -50,7 +50,7 @@ export const ContextMenuManager = ({ anchorEl }: Props) => {
|
||||
if (model.icons.length > 0) {
|
||||
const modelItemId = generateId();
|
||||
const firstIcon = model.icons[0];
|
||||
|
||||
|
||||
scene.placeIcon({
|
||||
modelItem: {
|
||||
id: modelItemId,
|
||||
|
||||
@@ -58,7 +58,10 @@ export const DebugUtils = () => {
|
||||
value={`${scene.items.length} items in scene`}
|
||||
/>
|
||||
<LineItem title="Mode" value={uiState.mode.type} />
|
||||
<LineItem title="Mode data" value={JSON.stringify(uiState.mode, null, 2)} />
|
||||
<LineItem
|
||||
title="Mode data"
|
||||
value={JSON.stringify(uiState.mode, null, 2)}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
import React from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { DebugUtils } from '../DebugUtils';
|
||||
import { ThemeProvider } from '@mui/material/styles';
|
||||
import { theme } from 'src/styles/theme';
|
||||
import { ModelProvider } from 'src/stores/modelStore';
|
||||
import { SceneProvider } from 'src/stores/sceneStore';
|
||||
import { UiStateProvider } from 'src/stores/uiStateStore';
|
||||
import { DebugUtils } from '../DebugUtils';
|
||||
|
||||
describe('DebugUtils', () => {
|
||||
const Providers: React.FC<{ children: React.ReactNode }> = ({ children }) => (
|
||||
<ThemeProvider theme={theme}>
|
||||
<ModelProvider>
|
||||
<SceneProvider>
|
||||
<UiStateProvider>
|
||||
{children}
|
||||
</UiStateProvider>
|
||||
</SceneProvider>
|
||||
</ModelProvider>
|
||||
</ThemeProvider>
|
||||
);
|
||||
const Providers: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
return (
|
||||
<ThemeProvider theme={theme}>
|
||||
<ModelProvider>
|
||||
<SceneProvider>
|
||||
<UiStateProvider>{children}</UiStateProvider>
|
||||
</SceneProvider>
|
||||
</ModelProvider>
|
||||
</ThemeProvider>
|
||||
);
|
||||
};
|
||||
|
||||
it('renders without crashing', () => {
|
||||
render(
|
||||
@@ -37,4 +37,4 @@ describe('DebugUtils', () => {
|
||||
);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -10,7 +10,9 @@ describe('LineItem', () => {
|
||||
});
|
||||
|
||||
it('matches snapshot', () => {
|
||||
const { asFragment } = render(<LineItem title="Snapshot Title" value="Snapshot Value" />);
|
||||
const { asFragment } = render(
|
||||
<LineItem title="Snapshot Title" value="Snapshot Value" />
|
||||
);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
import React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import { SizeIndicator } from '../SizeIndicator';
|
||||
import { ThemeProvider } from '@mui/material/styles';
|
||||
import { theme } from 'src/styles/theme';
|
||||
import { ModelProvider } from 'src/stores/modelStore';
|
||||
import { SceneProvider } from 'src/stores/sceneStore';
|
||||
import { UiStateProvider } from 'src/stores/uiStateStore';
|
||||
import { SizeIndicator } from '../SizeIndicator';
|
||||
|
||||
describe('SizeIndicator', () => {
|
||||
const Providers: React.FC<{ children: React.ReactNode }> = ({ children }) => (
|
||||
<ThemeProvider theme={theme}>
|
||||
<ModelProvider>
|
||||
<SceneProvider>
|
||||
<UiStateProvider>
|
||||
{children}
|
||||
</UiStateProvider>
|
||||
</SceneProvider>
|
||||
</ModelProvider>
|
||||
</ThemeProvider>
|
||||
);
|
||||
const Providers: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
return (
|
||||
<ThemeProvider theme={theme}>
|
||||
<ModelProvider>
|
||||
<SceneProvider>
|
||||
<UiStateProvider>{children}</UiStateProvider>
|
||||
</SceneProvider>
|
||||
</ModelProvider>
|
||||
</ThemeProvider>
|
||||
);
|
||||
};
|
||||
|
||||
it('renders without crashing', () => {
|
||||
const { container } = render(
|
||||
@@ -39,4 +39,4 @@ describe('SizeIndicator', () => {
|
||||
);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -12,4 +12,4 @@ describe('Value', () => {
|
||||
const { asFragment } = render(<Value value="Snapshot Value" />);
|
||||
expect(asFragment()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -103,8 +103,12 @@ const mouseInteractions: ShortcutItem[] = [
|
||||
];
|
||||
|
||||
export const HelpDialog = () => {
|
||||
const dialog = useUiStateStore((state) => state.dialog);
|
||||
const setDialog = useUiStateStore((state) => state.actions.setDialog);
|
||||
const dialog = useUiStateStore((state) => {
|
||||
return state.dialog;
|
||||
});
|
||||
const setDialog = useUiStateStore((state) => {
|
||||
return state.actions.setDialog;
|
||||
});
|
||||
|
||||
const isOpen = dialog === DialogTypeEnum.HELP;
|
||||
|
||||
@@ -131,7 +135,15 @@ export const HelpDialog = () => {
|
||||
</Typography>
|
||||
<Button
|
||||
onClick={handleClose}
|
||||
sx={{ minWidth: 'auto', p: 1, bgcolor: 'transparent', boxShadow: 'none', '&:hover': { bgcolor: 'transparent' }, '&:focus': { bgcolor: 'transparent' }, '&:active': { bgcolor: 'transparent' } }}
|
||||
sx={{
|
||||
minWidth: 'auto',
|
||||
p: 1,
|
||||
bgcolor: 'transparent',
|
||||
boxShadow: 'none',
|
||||
'&:hover': { bgcolor: 'transparent' },
|
||||
'&:focus': { bgcolor: 'transparent' },
|
||||
'&:active': { bgcolor: 'transparent' }
|
||||
}}
|
||||
>
|
||||
<CloseIcon />
|
||||
</Button>
|
||||
@@ -153,22 +165,26 @@ export const HelpDialog = () => {
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{keyboardShortcuts.map((shortcut, index) => (
|
||||
<TableRow key={index}>
|
||||
<TableCell>{shortcut.action}</TableCell>
|
||||
<TableCell>
|
||||
<code style={{
|
||||
backgroundColor: '#f5f5f5',
|
||||
padding: '2px 6px',
|
||||
borderRadius: '4px',
|
||||
fontFamily: 'monospace'
|
||||
}}>
|
||||
{shortcut.shortcut}
|
||||
</code>
|
||||
</TableCell>
|
||||
<TableCell>{shortcut.description}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
{keyboardShortcuts.map((shortcut, index) => {
|
||||
return (
|
||||
<TableRow key={index}>
|
||||
<TableCell>{shortcut.action}</TableCell>
|
||||
<TableCell>
|
||||
<code
|
||||
style={{
|
||||
backgroundColor: '#f5f5f5',
|
||||
padding: '2px 6px',
|
||||
borderRadius: '4px',
|
||||
fontFamily: 'monospace'
|
||||
}}
|
||||
>
|
||||
{shortcut.shortcut}
|
||||
</code>
|
||||
</TableCell>
|
||||
<TableCell>{shortcut.description}</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
@@ -190,22 +206,26 @@ export const HelpDialog = () => {
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{mouseInteractions.map((interaction, index) => (
|
||||
<TableRow key={index}>
|
||||
<TableCell>{interaction.action}</TableCell>
|
||||
<TableCell>
|
||||
<code style={{
|
||||
backgroundColor: '#f5f5f5',
|
||||
padding: '2px 6px',
|
||||
borderRadius: '4px',
|
||||
fontFamily: 'monospace'
|
||||
}}>
|
||||
{interaction.shortcut}
|
||||
</code>
|
||||
</TableCell>
|
||||
<TableCell>{interaction.description}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
{mouseInteractions.map((interaction, index) => {
|
||||
return (
|
||||
<TableRow key={index}>
|
||||
<TableCell>{interaction.action}</TableCell>
|
||||
<TableCell>
|
||||
<code
|
||||
style={{
|
||||
backgroundColor: '#f5f5f5',
|
||||
padding: '2px 6px',
|
||||
borderRadius: '4px',
|
||||
fontFamily: 'monospace'
|
||||
}}
|
||||
>
|
||||
{interaction.shortcut}
|
||||
</code>
|
||||
</TableCell>
|
||||
<TableCell>{interaction.description}</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
@@ -213,8 +233,9 @@ export const HelpDialog = () => {
|
||||
|
||||
<Box sx={{ mt: 3, p: 2, bgcolor: 'info.light', borderRadius: 1 }}>
|
||||
<Typography variant="body2" color="info.contrastText">
|
||||
<strong>Note:</strong> Keyboard shortcuts are disabled when typing in input fields,
|
||||
text areas, or content-editable elements to prevent conflicts.
|
||||
<strong>Note:</strong> Keyboard shortcuts are disabled when typing
|
||||
in input fields, text areas, or content-editable elements to prevent
|
||||
conflicts.
|
||||
</Typography>
|
||||
</Box>
|
||||
</DialogContent>
|
||||
@@ -226,4 +247,4 @@ export const HelpDialog = () => {
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,14 +1,21 @@
|
||||
import React from 'react';
|
||||
import { Connector, connectorStyleOptions } from 'src/types';
|
||||
import { Box, Slider, Select, MenuItem, TextField, IconButton as MUIIconButton } from '@mui/material';
|
||||
import {
|
||||
Box,
|
||||
Slider,
|
||||
Select,
|
||||
MenuItem,
|
||||
TextField,
|
||||
IconButton as MUIIconButton
|
||||
} from '@mui/material';
|
||||
import { useConnector } from 'src/hooks/useConnector';
|
||||
import { ColorSelector } from 'src/components/ColorSelector/ColorSelector';
|
||||
import { useUiStateStore } from 'src/stores/uiStateStore';
|
||||
import { useScene } from 'src/hooks/useScene';
|
||||
import { Close as CloseIcon } from '@mui/icons-material';
|
||||
import { ControlsContainer } from '../components/ControlsContainer';
|
||||
import { Section } from '../components/Section';
|
||||
import { DeleteButton } from '../components/DeleteButton';
|
||||
import { Close as CloseIcon } from '@mui/icons-material';
|
||||
|
||||
interface Props {
|
||||
id: string;
|
||||
@@ -32,12 +39,14 @@ export const ConnectorControls = ({ id }: Props) => {
|
||||
{/* Close button */}
|
||||
<MUIIconButton
|
||||
aria-label="Close"
|
||||
onClick={() => uiStateActions.setItemControls(null)}
|
||||
onClick={() => {
|
||||
return uiStateActions.setItemControls(null);
|
||||
}}
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: 16,
|
||||
right: 16,
|
||||
zIndex: 2,
|
||||
zIndex: 2
|
||||
}}
|
||||
size="small"
|
||||
>
|
||||
|
||||
@@ -7,9 +7,9 @@ import { Section } from 'src/components/ItemControls/components/Section';
|
||||
import { Searchbox } from 'src/components/ItemControls/IconSelectionControls/Searchbox';
|
||||
import { useIconFiltering } from 'src/hooks/useIconFiltering';
|
||||
import { useIconCategories } from 'src/hooks/useIconCategories';
|
||||
import { Close as CloseIcon } from '@mui/icons-material';
|
||||
import { Icons } from './Icons';
|
||||
import { IconGrid } from './IconGrid';
|
||||
import { Close as CloseIcon } from '@mui/icons-material';
|
||||
|
||||
export const IconSelectionControls = () => {
|
||||
const uiStateActions = useUiStateStore((state) => {
|
||||
@@ -37,18 +37,28 @@ export const IconSelectionControls = () => {
|
||||
return (
|
||||
<ControlsContainer
|
||||
header={
|
||||
<Section sx={{ top: 0, pt: 6, pb: 3, position: 'relative', paddingTop: '32px' }}>
|
||||
<Section
|
||||
sx={{
|
||||
top: 0,
|
||||
pt: 6,
|
||||
pb: 3,
|
||||
position: 'relative',
|
||||
paddingTop: '32px'
|
||||
}}
|
||||
>
|
||||
{/* Close button */}
|
||||
<MUIIconButton
|
||||
aria-label="Close"
|
||||
onClick={() => uiStateActions.setItemControls(null)}
|
||||
onClick={() => {
|
||||
return uiStateActions.setItemControls(null);
|
||||
}}
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: 12,
|
||||
right: 12,
|
||||
zIndex: 2,
|
||||
padding: 0,
|
||||
background: 'none',
|
||||
background: 'none'
|
||||
}}
|
||||
size="small"
|
||||
>
|
||||
|
||||
@@ -51,19 +51,23 @@ export const NodeControls = ({ id }: Props) => {
|
||||
<ControlsContainer>
|
||||
<Box
|
||||
sx={{
|
||||
bgcolor: (theme) => theme.customVars.customPalette.diagramBg,
|
||||
position: 'relative',
|
||||
bgcolor: (theme) => {
|
||||
return theme.customVars.customPalette.diagramBg;
|
||||
},
|
||||
position: 'relative'
|
||||
}}
|
||||
>
|
||||
{/* Close button */}
|
||||
<MUIIconButton
|
||||
aria-label="Close"
|
||||
onClick={() => uiStateActions.setItemControls(null)}
|
||||
onClick={() => {
|
||||
return uiStateActions.setItemControls(null);
|
||||
}}
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: 8,
|
||||
right: 8,
|
||||
zIndex: 2,
|
||||
zIndex: 2
|
||||
}}
|
||||
size="small"
|
||||
>
|
||||
@@ -134,4 +138,3 @@ export const NodeControls = ({ id }: Props) => {
|
||||
</ControlsContainer>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -4,10 +4,10 @@ import { useRectangle } from 'src/hooks/useRectangle';
|
||||
import { ColorSelector } from 'src/components/ColorSelector/ColorSelector';
|
||||
import { useUiStateStore } from 'src/stores/uiStateStore';
|
||||
import { useScene } from 'src/hooks/useScene';
|
||||
import { Close as CloseIcon } from '@mui/icons-material';
|
||||
import { ControlsContainer } from '../components/ControlsContainer';
|
||||
import { Section } from '../components/Section';
|
||||
import { DeleteButton } from '../components/DeleteButton';
|
||||
import { Close as CloseIcon } from '@mui/icons-material';
|
||||
|
||||
interface Props {
|
||||
id: string;
|
||||
@@ -31,12 +31,14 @@ export const RectangleControls = ({ id }: Props) => {
|
||||
{/* Close button */}
|
||||
<MUIIconButton
|
||||
aria-label="Close"
|
||||
onClick={() => uiStateActions.setItemControls(null)}
|
||||
onClick={() => {
|
||||
return uiStateActions.setItemControls(null);
|
||||
}}
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: 8,
|
||||
right: 8,
|
||||
zIndex: 2,
|
||||
zIndex: 2
|
||||
}}
|
||||
size="small"
|
||||
>
|
||||
|
||||
@@ -8,7 +8,10 @@ import {
|
||||
Slider,
|
||||
IconButton as MUIIconButton
|
||||
} from '@mui/material';
|
||||
import { TextRotationNone as TextRotationNoneIcon, Close as CloseIcon } from '@mui/icons-material';
|
||||
import {
|
||||
TextRotationNone as TextRotationNoneIcon,
|
||||
Close as CloseIcon
|
||||
} from '@mui/icons-material';
|
||||
import { useTextBox } from 'src/hooks/useTextBox';
|
||||
import { useUiStateStore } from 'src/stores/uiStateStore';
|
||||
import { getIsoProjectionCss } from 'src/utils';
|
||||
@@ -39,12 +42,14 @@ export const TextBoxControls = ({ id }: Props) => {
|
||||
{/* Close button */}
|
||||
<MUIIconButton
|
||||
aria-label="Close"
|
||||
onClick={() => uiStateActions.setItemControls(null)}
|
||||
onClick={() => {
|
||||
return uiStateActions.setItemControls(null);
|
||||
}}
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: 16,
|
||||
right: 16,
|
||||
zIndex: 2,
|
||||
zIndex: 2
|
||||
}}
|
||||
size="small"
|
||||
>
|
||||
|
||||
@@ -14,12 +14,17 @@ import {
|
||||
import { UiElement } from 'src/components/UiElement/UiElement';
|
||||
import { IconButton } from 'src/components/IconButton/IconButton';
|
||||
import { useUiStateStore } from 'src/stores/uiStateStore';
|
||||
import { exportAsJSON, modelFromModelStore } from 'src/utils';
|
||||
import {
|
||||
exportAsJSON,
|
||||
exportAsCompactJSON,
|
||||
transformFromCompactFormat
|
||||
} from 'src/utils/exportOptions';
|
||||
import { modelFromModelStore } from 'src/utils';
|
||||
import { useInitialDataManager } from 'src/hooks/useInitialDataManager';
|
||||
import { useModelStore } from 'src/stores/modelStore';
|
||||
import { useHistory } from 'src/hooks/useHistory';
|
||||
import { MenuItem } from './MenuItem';
|
||||
import { DialogTypeEnum } from 'src/types/ui';
|
||||
import { MenuItem } from './MenuItem';
|
||||
|
||||
export const MainMenu = () => {
|
||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
||||
@@ -67,7 +72,14 @@ export const MainMenu = () => {
|
||||
const fileReader = new FileReader();
|
||||
|
||||
fileReader.onload = async (e) => {
|
||||
const modelData = JSON.parse(e.target?.result as string);
|
||||
const rawData = JSON.parse(e.target?.result as string);
|
||||
let modelData = rawData;
|
||||
|
||||
// Check format and transform if needed
|
||||
if (rawData._?.f === 'compact') {
|
||||
modelData = transformFromCompactFormat(rawData);
|
||||
}
|
||||
|
||||
load(modelData);
|
||||
clearHistory(); // Clear history when loading new model
|
||||
};
|
||||
@@ -85,6 +97,11 @@ export const MainMenu = () => {
|
||||
uiStateActions.setIsMainMenuOpen(false);
|
||||
}, [model, uiStateActions]);
|
||||
|
||||
const onExportAsCompactJSON = useCallback(async () => {
|
||||
exportAsCompactJSON(model);
|
||||
uiStateActions.setIsMainMenuOpen(false);
|
||||
}, [model, uiStateActions]);
|
||||
|
||||
const onExportAsImage = useCallback(() => {
|
||||
uiStateActions.setIsMainMenuOpen(false);
|
||||
uiStateActions.setDialog(DialogTypeEnum.EXPORT_IMAGE);
|
||||
@@ -130,7 +147,12 @@ export const MainMenu = () => {
|
||||
|
||||
return (
|
||||
<UiElement>
|
||||
<IconButton Icon={<MenuIcon />} name="Main menu" onClick={onToggleMenu} isActive={isMainMenuOpen} />
|
||||
<IconButton
|
||||
Icon={<MenuIcon />}
|
||||
name="Main menu"
|
||||
onClick={onToggleMenu}
|
||||
isActive={isMainMenuOpen}
|
||||
/>
|
||||
|
||||
<Menu
|
||||
anchorEl={anchorEl}
|
||||
@@ -182,6 +204,12 @@ export const MainMenu = () => {
|
||||
</MenuItem>
|
||||
)}
|
||||
|
||||
{mainMenuOptions.includes('EXPORT.JSON') && (
|
||||
<MenuItem onClick={onExportAsCompactJSON} Icon={<ExportJsonIcon />}>
|
||||
Export as Compact JSON
|
||||
</MenuItem>
|
||||
)}
|
||||
|
||||
{mainMenuOptions.includes('EXPORT.PNG') && (
|
||||
<MenuItem onClick={onExportAsImage} Icon={<ExportImageIcon />}>
|
||||
Export as image
|
||||
|
||||
@@ -23,11 +23,11 @@ export const Connector = ({ connector: _connector, isSelected }: Props) => {
|
||||
const color = useColor(_connector.color);
|
||||
const { currentView } = useScene();
|
||||
const connector = useConnector(_connector.id);
|
||||
|
||||
|
||||
if (!connector || !color) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
const { css, pxSize } = useIsoProjection({
|
||||
...connector.path.rectangle
|
||||
});
|
||||
|
||||
@@ -66,7 +66,9 @@ export const ZoomControls = () => {
|
||||
<IconButton
|
||||
name="Help (F1)"
|
||||
Icon={<HelpIcon />}
|
||||
onClick={() => uiStateStoreActions.setDialog(DialogTypeEnum.HELP)}
|
||||
onClick={() => {
|
||||
return uiStateStoreActions.setDialog(DialogTypeEnum.HELP);
|
||||
}}
|
||||
/>
|
||||
</UiElement>
|
||||
</Stack>
|
||||
|
||||
@@ -92,8 +92,6 @@ const mousedown: ModeActionsAction = ({
|
||||
draft.mousedownItem = itemAtTile;
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
} else {
|
||||
uiState.actions.setMode(
|
||||
produce(uiState.mode, (draft) => {
|
||||
@@ -102,7 +100,7 @@ const mousedown: ModeActionsAction = ({
|
||||
);
|
||||
|
||||
uiState.actions.setItemControls(null);
|
||||
|
||||
|
||||
// Show context menu for empty space on left click
|
||||
uiState.actions.setContextMenu({
|
||||
type: 'EMPTY',
|
||||
@@ -158,7 +156,7 @@ export const Cursor: ModeActions = {
|
||||
if (uiState.mode.type !== 'CURSOR' || !isRendererInteraction) return;
|
||||
|
||||
const hasMoved = uiState.mouse.mousedown && hasMovedTile(uiState.mouse);
|
||||
|
||||
|
||||
if (uiState.mode.mousedownItem && !hasMoved) {
|
||||
if (uiState.mode.mousedownItem.type === 'ITEM') {
|
||||
uiState.actions.setItemControls({
|
||||
|
||||
@@ -10,7 +10,11 @@ describe('colorSchema', () => {
|
||||
const result = colorSchema.safeParse(invalid);
|
||||
expect(result.success).toBe(false);
|
||||
if (!result.success) {
|
||||
expect(result.error.issues.some((issue: any) => issue.path.includes('value'))).toBe(true);
|
||||
expect(
|
||||
result.error.issues.some((issue: any) => {
|
||||
return issue.path.includes('value');
|
||||
})
|
||||
).toBe(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -31,7 +35,11 @@ describe('colorsSchema', () => {
|
||||
const result = colorsSchema.safeParse(invalid);
|
||||
expect(result.success).toBe(false);
|
||||
if (!result.success) {
|
||||
expect(result.error.issues.some((issue: any) => issue.path.includes('value'))).toBe(true);
|
||||
expect(
|
||||
result.error.issues.some((issue: any) => {
|
||||
return issue.path.includes('value');
|
||||
})
|
||||
).toBe(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -10,7 +10,11 @@ describe('anchorSchema', () => {
|
||||
const result = anchorSchema.safeParse(invalid);
|
||||
expect(result.success).toBe(false);
|
||||
if (!result.success) {
|
||||
expect(result.error.issues.some((issue: any) => issue.path.includes('id'))).toBe(true);
|
||||
expect(
|
||||
result.error.issues.some((issue: any) => {
|
||||
return issue.path.includes('id');
|
||||
})
|
||||
).toBe(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -25,7 +29,11 @@ describe('connectorSchema', () => {
|
||||
const result = connectorSchema.safeParse(invalid);
|
||||
expect(result.success).toBe(false);
|
||||
if (!result.success) {
|
||||
expect(result.error.issues.some((issue: any) => issue.path.includes('anchors'))).toBe(true);
|
||||
expect(
|
||||
result.error.issues.some((issue: any) => {
|
||||
return issue.path.includes('anchors');
|
||||
})
|
||||
).toBe(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -10,7 +10,11 @@ describe('iconSchema', () => {
|
||||
const result = iconSchema.safeParse(invalid);
|
||||
expect(result.success).toBe(false);
|
||||
if (!result.success) {
|
||||
expect(result.error.issues.some((issue: any) => issue.path.includes('id'))).toBe(true);
|
||||
expect(
|
||||
result.error.issues.some((issue: any) => {
|
||||
return issue.path.includes('id');
|
||||
})
|
||||
).toBe(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -31,7 +35,11 @@ describe('iconsSchema', () => {
|
||||
const result = iconsSchema.safeParse(invalid);
|
||||
expect(result.success).toBe(false);
|
||||
if (!result.success) {
|
||||
expect(result.error.issues.some((issue: any) => issue.path.includes('id'))).toBe(true);
|
||||
expect(
|
||||
result.error.issues.some((issue: any) => {
|
||||
return issue.path.includes('id');
|
||||
})
|
||||
).toBe(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,7 +2,12 @@ import { modelItemSchema, modelItemsSchema } from '../modelItems';
|
||||
|
||||
describe('modelItemSchema', () => {
|
||||
it('validates a correct model item', () => {
|
||||
const valid = { id: 'item1', name: 'Test', icon: 'icon1', description: 'desc' };
|
||||
const valid = {
|
||||
id: 'item1',
|
||||
name: 'Test',
|
||||
icon: 'icon1',
|
||||
description: 'desc'
|
||||
};
|
||||
expect(modelItemSchema.safeParse(valid).success).toBe(true);
|
||||
});
|
||||
it('fails if required fields are missing', () => {
|
||||
@@ -10,7 +15,11 @@ describe('modelItemSchema', () => {
|
||||
const result = modelItemSchema.safeParse(invalid);
|
||||
expect(result.success).toBe(false);
|
||||
if (!result.success) {
|
||||
expect(result.error.issues.some((issue: any) => issue.path.includes('id'))).toBe(true);
|
||||
expect(
|
||||
result.error.issues.some((issue: any) => {
|
||||
return issue.path.includes('id');
|
||||
})
|
||||
).toBe(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -24,14 +33,15 @@ describe('modelItemsSchema', () => {
|
||||
expect(modelItemsSchema.safeParse(valid).success).toBe(true);
|
||||
});
|
||||
it('fails if any item is invalid', () => {
|
||||
const invalid = [
|
||||
{ id: 'item1', name: 'Test1' },
|
||||
{ name: 'MissingId' }
|
||||
];
|
||||
const invalid = [{ id: 'item1', name: 'Test1' }, { name: 'MissingId' }];
|
||||
const result = modelItemsSchema.safeParse(invalid);
|
||||
expect(result.success).toBe(false);
|
||||
if (!result.success) {
|
||||
expect(result.error.issues.some((issue: any) => issue.path.includes('id'))).toBe(true);
|
||||
expect(
|
||||
result.error.issues.some((issue: any) => {
|
||||
return issue.path.includes('id');
|
||||
})
|
||||
).toBe(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -10,7 +10,11 @@ describe('rectangleSchema', () => {
|
||||
const result = rectangleSchema.safeParse(invalid);
|
||||
expect(result.success).toBe(false);
|
||||
if (!result.success) {
|
||||
expect(result.error.issues.some((issue: any) => issue.path.includes('from'))).toBe(true);
|
||||
expect(
|
||||
result.error.issues.some((issue: any) => {
|
||||
return issue.path.includes('from');
|
||||
})
|
||||
).toBe(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -10,7 +10,11 @@ describe('textBoxSchema', () => {
|
||||
const result = textBoxSchema.safeParse(invalid);
|
||||
expect(result.success).toBe(false);
|
||||
if (!result.success) {
|
||||
expect(result.error.issues.some((issue: any) => issue.path.includes('content'))).toBe(true);
|
||||
expect(
|
||||
result.error.issues.some((issue: any) => {
|
||||
return issue.path.includes('content');
|
||||
})
|
||||
).toBe(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -129,33 +129,47 @@ describe('modelSchema Zod validation', () => {
|
||||
delete invalidModel.title;
|
||||
const result = require('../model').modelSchema.safeParse(invalidModel);
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error.issues.some((issue: any) => issue.path.includes('title'))).toBe(true);
|
||||
expect(
|
||||
result.error.issues.some((issue: any) => {
|
||||
return issue.path.includes('title');
|
||||
})
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test('Model with invalid color reference fails modelSchema validation', () => {
|
||||
const { ...invalidModel } = model;
|
||||
// Add a rectangle with an invalid color to the first view
|
||||
invalidModel.views = invalidModel.views.map((view: any, i: number) =>
|
||||
i === 0
|
||||
invalidModel.views = invalidModel.views.map((view: any, i: number) => {
|
||||
return i === 0
|
||||
? {
|
||||
...view,
|
||||
rectangles: [
|
||||
...(view.rectangles || []),
|
||||
{ id: 'rect-invalid', color: 'notAColor', from: { x: 0, y: 0 }, to: { x: 1, y: 1 } }
|
||||
{
|
||||
id: 'rect-invalid',
|
||||
color: 'notAColor',
|
||||
from: { x: 0, y: 0 },
|
||||
to: { x: 1, y: 1 }
|
||||
}
|
||||
]
|
||||
}
|
||||
: view
|
||||
);
|
||||
: view;
|
||||
});
|
||||
const result = require('../model').modelSchema.safeParse(invalidModel);
|
||||
expect(result.success).toBe(false);
|
||||
if (!result.success) {
|
||||
// Print all issues for debugging
|
||||
console.log('Zod issues:', result.error.issues);
|
||||
expect(result.error.issues.some((issue: any) =>
|
||||
issue.message === 'Rectangle references a color that does not exist in the model.' &&
|
||||
issue.params &&
|
||||
issue.params.rectangle === 'rect-invalid'
|
||||
)).toBe(true);
|
||||
expect(
|
||||
result.error.issues.some((issue: any) => {
|
||||
return (
|
||||
issue.message ===
|
||||
'Rectangle references a color that does not exist in the model.' &&
|
||||
issue.params &&
|
||||
issue.params.rectangle === 'rect-invalid'
|
||||
);
|
||||
})
|
||||
).toBe(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -10,7 +10,11 @@ describe('viewItemSchema', () => {
|
||||
const result = viewItemSchema.safeParse(invalid);
|
||||
expect(result.success).toBe(false);
|
||||
if (!result.success) {
|
||||
expect(result.error.issues.some((issue: any) => issue.path.includes('id'))).toBe(true);
|
||||
expect(
|
||||
result.error.issues.some((issue: any) => {
|
||||
return issue.path.includes('id');
|
||||
})
|
||||
).toBe(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -29,7 +33,11 @@ describe('viewSchema', () => {
|
||||
const result = viewSchema.safeParse(invalid);
|
||||
expect(result.success).toBe(false);
|
||||
if (!result.success) {
|
||||
expect(result.error.issues.some((issue: any) => issue.path.includes('items'))).toBe(true);
|
||||
expect(
|
||||
result.error.issues.some((issue: any) => {
|
||||
return issue.path.includes('items');
|
||||
})
|
||||
).toBe(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -37,19 +45,31 @@ describe('viewSchema', () => {
|
||||
describe('viewsSchema', () => {
|
||||
it('validates an array of views', () => {
|
||||
const valid = [
|
||||
{ id: 'view1', name: 'View', items: [{ id: 'item1', tile: { x: 0, y: 0 } }] }
|
||||
{
|
||||
id: 'view1',
|
||||
name: 'View',
|
||||
items: [{ id: 'item1', tile: { x: 0, y: 0 } }]
|
||||
}
|
||||
];
|
||||
expect(viewsSchema.safeParse(valid).success).toBe(true);
|
||||
});
|
||||
it('fails if any view is invalid', () => {
|
||||
const invalid = [
|
||||
{ id: 'view1', name: 'View', items: [{ id: 'item1', tile: { x: 0, y: 0 } }] },
|
||||
{
|
||||
id: 'view1',
|
||||
name: 'View',
|
||||
items: [{ id: 'item1', tile: { x: 0, y: 0 } }]
|
||||
},
|
||||
{ id: 'view2', name: 'View2' }
|
||||
];
|
||||
const result = viewsSchema.safeParse(invalid);
|
||||
expect(result.success).toBe(false);
|
||||
if (!result.success) {
|
||||
expect(result.error.issues.some((issue: any) => issue.path.includes('items'))).toBe(true);
|
||||
expect(
|
||||
result.error.issues.some((issue: any) => {
|
||||
return issue.path.includes('items');
|
||||
})
|
||||
).toBe(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import domtoimage from 'dom-to-image';
|
||||
import FileSaver from 'file-saver';
|
||||
import { Model, Size } from '../types';
|
||||
import { icons as availableIcons } from '../examples/initialData';
|
||||
|
||||
export const generateGenericFilename = (extension: string) => {
|
||||
return `isoflow-export-${new Date().toISOString()}.${extension}`;
|
||||
@@ -36,6 +37,127 @@ export const downloadFile = (data: Blob, filename: string) => {
|
||||
FileSaver.saveAs(data, filename);
|
||||
};
|
||||
|
||||
export const transformToCompactFormat = (model: Model) => {
|
||||
const { items, views, icons, title } = model;
|
||||
|
||||
// Compact format: ultra-minimal for LLM generation
|
||||
const compactItems = items.map((item, index) => [
|
||||
item.name.substring(0, 30), // Truncated name
|
||||
item.icon || 'block', // Icon reference only (no base64)
|
||||
item.description?.substring(0, 100) || '' // Truncated description
|
||||
]);
|
||||
|
||||
const compactViews = views.map((view) => {
|
||||
const positions = view.items.map((viewItem) => {
|
||||
const itemIndex = items.findIndex(item => item.id === viewItem.id);
|
||||
return [itemIndex, viewItem.tile.x, viewItem.tile.y];
|
||||
});
|
||||
|
||||
const connections = view.connectors?.map((connector) => {
|
||||
const fromIndex = items.findIndex(item => item.id === connector.anchors[0]?.ref.item);
|
||||
const toIndex = items.findIndex(item => item.id === connector.anchors[connector.anchors.length - 1]?.ref.item);
|
||||
return [fromIndex, toIndex];
|
||||
}).filter(conn => conn[0] !== -1 && conn[1] !== -1) || [];
|
||||
|
||||
return [positions, connections];
|
||||
});
|
||||
|
||||
return {
|
||||
t: title?.substring(0, 40) || 'Untitled',
|
||||
i: compactItems,
|
||||
v: compactViews,
|
||||
_: { f: 'compact', v: '1.0' }
|
||||
};
|
||||
};
|
||||
|
||||
export const transformFromCompactFormat = (compactModel: any): Model => {
|
||||
const { t, i, v, _ } = compactModel;
|
||||
|
||||
// Restore from compact format
|
||||
const fullItems = i.map((item: any, index: number) => ({
|
||||
id: `item_${index}`,
|
||||
name: item[0],
|
||||
icon: item[1],
|
||||
description: item[2] || '' // Restore description if available
|
||||
}));
|
||||
|
||||
// Resolve icons from the internal icon library
|
||||
const iconSet = new Set<string>();
|
||||
i.forEach((item: any) => {
|
||||
if (item[1]) iconSet.add(item[1]);
|
||||
});
|
||||
|
||||
const fullIcons = Array.from(iconSet).map(iconName => {
|
||||
// Find the icon in the available icons library
|
||||
const existingIcon = availableIcons.find(icon => icon.id === iconName || icon.name === iconName);
|
||||
|
||||
if (existingIcon) {
|
||||
// Use the existing icon data with proper URL
|
||||
return {
|
||||
id: iconName,
|
||||
name: existingIcon.name,
|
||||
url: existingIcon.url,
|
||||
collection: existingIcon.collection,
|
||||
isIsometric: existingIcon.isIsometric ?? true
|
||||
};
|
||||
} else {
|
||||
// Fallback for unknown icons
|
||||
return {
|
||||
id: iconName,
|
||||
name: iconName,
|
||||
url: '', // App will use default icon
|
||||
isIsometric: true
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
const fullViews = v.map((view: any, viewIndex: number) => {
|
||||
const [positions, connections] = view;
|
||||
|
||||
const viewItems = positions.map((pos: any) => {
|
||||
const [itemIndex, x, y] = pos;
|
||||
return {
|
||||
id: `item_${itemIndex}`,
|
||||
tile: { x, y },
|
||||
labelHeight: 80
|
||||
};
|
||||
});
|
||||
|
||||
const connectors = connections.map((conn: any, connIndex: number) => {
|
||||
const [fromIndex, toIndex] = conn;
|
||||
return {
|
||||
id: `conn_${viewIndex}_${connIndex}`,
|
||||
color: 'color1',
|
||||
anchors: [
|
||||
{ id: `a_${viewIndex}_${connIndex}_0`, ref: { item: `item_${fromIndex}` } },
|
||||
{ id: `a_${viewIndex}_${connIndex}_1`, ref: { item: `item_${toIndex}` } }
|
||||
],
|
||||
width: 10,
|
||||
description: '',
|
||||
style: 'SOLID'
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
id: `view_${viewIndex}`,
|
||||
name: `View ${viewIndex + 1}`,
|
||||
items: viewItems,
|
||||
connectors,
|
||||
rectangles: [],
|
||||
textBoxes: []
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
title: t,
|
||||
version: '1.0',
|
||||
items: fullItems,
|
||||
views: fullViews,
|
||||
icons: fullIcons,
|
||||
colors: [{ id: 'color1', value: '#a5b8f3' }]
|
||||
};
|
||||
};
|
||||
|
||||
export const exportAsJSON = (model: Model) => {
|
||||
const data = new Blob([JSON.stringify(model)], {
|
||||
type: 'application/json;charset=utf-8'
|
||||
@@ -44,6 +166,15 @@ export const exportAsJSON = (model: Model) => {
|
||||
downloadFile(data, generateGenericFilename('json'));
|
||||
};
|
||||
|
||||
export const exportAsCompactJSON = (model: Model) => {
|
||||
const compactModel = transformToCompactFormat(model);
|
||||
const data = new Blob([JSON.stringify(compactModel)], {
|
||||
type: 'application/json;charset=utf-8'
|
||||
});
|
||||
|
||||
downloadFile(data, generateGenericFilename('compact.json'));
|
||||
};
|
||||
|
||||
export const exportAsImage = async (el: HTMLDivElement, size?: Size) => {
|
||||
const imageData = await domtoimage.toPng(el, {
|
||||
...size,
|
||||
|
||||
7
tsconfig.dev.json
Normal file
7
tsconfig.dev.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"declaration": false,
|
||||
"emitDeclarationOnly": false
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,12 @@ module.exports = {
|
||||
rules: [
|
||||
{
|
||||
test: /\.(ts|tsx)$/,
|
||||
use: 'ts-loader',
|
||||
use: {
|
||||
loader: 'ts-loader',
|
||||
options: {
|
||||
configFile: 'tsconfig.dev.json'
|
||||
}
|
||||
},
|
||||
exclude: /node_modules/
|
||||
},
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user