@objectql/studio 1.3.1 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,179 @@
1
+ import { useEffect, useState, useMemo } from 'react';
2
+ import { AgGridReact } from 'ag-grid-react';
3
+ import { ColDef } from 'ag-grid-community';
4
+ import 'ag-grid-community/styles/ag-grid.css';
5
+ import 'ag-grid-community/styles/ag-theme-quartz.css';
6
+ import { Button } from '@/components/ui/button';
7
+ import { RefreshCw, Plus, Table2, FileCode } from 'lucide-react';
8
+ import { cn } from '@/lib/utils';
9
+ import { FileEditor } from '@/components/FileEditor';
10
+
11
+ interface ObjectViewProps {
12
+ objectName: string;
13
+ }
14
+
15
+ export function ObjectView({ objectName }: ObjectViewProps) {
16
+ const [rowData, setRowData] = useState<any[]>([]);
17
+ const [columnDefs, setColumnDefs] = useState<ColDef[]>([]);
18
+ const [loading, setLoading] = useState(false);
19
+ const [activeTab, setActiveTab] = useState<'data' | 'schema'>('data');
20
+ const [schemaFile, setSchemaFile] = useState<string | null>(null);
21
+
22
+ const fetchData = async () => {
23
+ setLoading(true);
24
+ try {
25
+ // 1. Fetch Schema to build columns
26
+ // Ideally we should cache metadata, but fetching it here is safer for now
27
+ const metaRes = await fetch('/api/metadata');
28
+ const meta = await metaRes.json();
29
+ const objects = Array.isArray(meta) ? meta : meta.objects;
30
+ const currentObj = objects.find((o: any) => o.name === objectName);
31
+
32
+ if (currentObj) {
33
+ const cols: ColDef[] = [
34
+ { field: 'id', headerName: 'ID', width: 100, pinned: 'left' }
35
+ ];
36
+
37
+ Object.entries(currentObj.fields).forEach(([key, field]: [string, any]) => {
38
+ cols.push({
39
+ field: key,
40
+ headerName: field.label || key,
41
+ flex: 1,
42
+ filter: true
43
+ });
44
+ });
45
+
46
+ // Add system fields if not present
47
+ if (!cols.find(c => c.field === 'createdAt')) cols.push({ field: 'createdAt', hide: true });
48
+ if (!cols.find(c => c.field === 'updatedAt')) cols.push({ field: 'updatedAt', hide: true });
49
+
50
+ setColumnDefs(cols);
51
+ }
52
+
53
+ // 2. Fetch Data
54
+ const res = await fetch(`/api/objectql/${objectName}`);
55
+ const data = await res.json();
56
+ setRowData(Array.isArray(data) ? data : []);
57
+
58
+ } catch (e) {
59
+ console.error(e);
60
+ } finally {
61
+ setLoading(false);
62
+ }
63
+ };
64
+
65
+ const fetchSchemaFile = async () => {
66
+ // If we already have the file for this object, don't re-fetch unless objectName changed
67
+ // But here we rely on useEffect deps
68
+ try {
69
+ const res = await fetch(`/api/schema/find?object=${objectName}`);
70
+ if (res.ok) {
71
+ const data = await res.json();
72
+ setSchemaFile(data.file);
73
+ } else {
74
+ setSchemaFile(null);
75
+ console.error('Failed to find schema file');
76
+ }
77
+ } catch (e) {
78
+ console.error(e);
79
+ }
80
+ };
81
+
82
+ useEffect(() => {
83
+ setActiveTab('data');
84
+ }, [objectName]);
85
+
86
+ useEffect(() => {
87
+ if (activeTab === 'data') {
88
+ fetchData();
89
+ } else {
90
+ fetchSchemaFile();
91
+ }
92
+ }, [objectName, activeTab]);
93
+
94
+ const defaultColDef = useMemo(() => {
95
+ return {
96
+ sortable: true,
97
+ filter: true,
98
+ resizable: true,
99
+ };
100
+ }, []);
101
+
102
+ return (
103
+ <div className="flex h-full flex-col">
104
+ {/* Header */}
105
+ <div className="border-b px-6 py-4 flex items-center justify-between bg-card text-card-foreground">
106
+ <div className="flex flex-col gap-1">
107
+ <h1 className="text-2xl font-bold tracking-tight capitalize">{objectName}</h1>
108
+ <div className="flex space-x-1">
109
+ <button
110
+ onClick={() => setActiveTab('data')}
111
+ className={cn(
112
+ "flex items-center space-x-2 px-3 py-1.5 text-sm font-medium rounded-md transition-colors",
113
+ activeTab === 'data'
114
+ ? "bg-primary/10 text-primary"
115
+ : "text-muted-foreground hover:bg-muted hover:text-foreground"
116
+ )}
117
+ >
118
+ <Table2 className="h-4 w-4" />
119
+ <span>Data</span>
120
+ </button>
121
+ <button
122
+ onClick={() => setActiveTab('schema')}
123
+ className={cn(
124
+ "flex items-center space-x-2 px-3 py-1.5 text-sm font-medium rounded-md transition-colors",
125
+ activeTab === 'schema'
126
+ ? "bg-primary/10 text-primary"
127
+ : "text-muted-foreground hover:bg-muted hover:text-foreground"
128
+ )}
129
+ >
130
+ <FileCode className="h-4 w-4" />
131
+ <span>Schema</span>
132
+ </button>
133
+ </div>
134
+ </div>
135
+
136
+ {activeTab === 'data' && (
137
+ <div className="flex items-center space-x-2">
138
+ <Button variant="outline" size="sm" onClick={fetchData} disabled={loading}>
139
+ <RefreshCw className={cn("mr-2 h-4 w-4", loading && "animate-spin")} />
140
+ Refresh
141
+ </Button>
142
+ <Button size="sm">
143
+ <Plus className="mr-2 h-4 w-4" />
144
+ New Record
145
+ </Button>
146
+ </div>
147
+ )}
148
+ </div>
149
+
150
+ {/* Content */}
151
+ <div className="flex-1 overflow-hidden p-6 bg-muted/20">
152
+ {activeTab === 'data' ? (
153
+ <div className="border rounded-md overflow-hidden bg-card h-full" style={{opacity: loading ? 0.6 : 1}}>
154
+ <div className="ag-theme-quartz h-full w-full">
155
+ <AgGridReact
156
+ rowData={rowData}
157
+ columnDefs={columnDefs}
158
+ defaultColDef={defaultColDef}
159
+ pagination={true}
160
+ paginationPageSize={20}
161
+ rowSelection="multiple"
162
+ />
163
+ </div>
164
+ </div>
165
+ ) : (
166
+ <div className="h-full bg-card rounded-md border">
167
+ {schemaFile ? (
168
+ <FileEditor filePath={schemaFile} className="h-full border-0" />
169
+ ) : (
170
+ <div className="flex h-full items-center justify-center text-muted-foreground">
171
+ {loading ? 'Loading schema...' : 'Could not find definition file for this object.'}
172
+ </div>
173
+ )}
174
+ </div>
175
+ )}
176
+ </div>
177
+ </div>
178
+ );
179
+ }
@@ -0,0 +1,85 @@
1
+ import { useState, useEffect } from 'react';
2
+ import { Button } from '@/components/ui/button';
3
+ import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
4
+ import { FileCode, RefreshCw } from 'lucide-react';
5
+ import { cn } from '@/lib/utils';
6
+ import { FileEditor } from '@/components/FileEditor';
7
+
8
+ export function SchemaEditor() {
9
+ const [files, setFiles] = useState<string[]>([]);
10
+ const [selectedFile, setSelectedFile] = useState<string | null>(null);
11
+ const [error, setError] = useState<string | null>(null);
12
+
13
+ useEffect(() => {
14
+ fetchFiles();
15
+ }, []);
16
+
17
+ const fetchFiles = async () => {
18
+ try {
19
+ const res = await fetch('/api/schema/files');
20
+ if (!res.ok) throw new Error('Failed to fetch files');
21
+ const data = await res.json();
22
+ setFiles(data.files || []);
23
+ } catch (e) {
24
+ console.error(e);
25
+ setError('Failed to load file list');
26
+ }
27
+ };
28
+
29
+ return (
30
+ <div className="flex h-full flex-col p-6 space-y-6">
31
+ <div className="flex items-center justify-between">
32
+ <div className="flex flex-col gap-1">
33
+ <h2 className="text-3xl font-bold tracking-tight">Schema Editor</h2>
34
+ {error && <p className="text-sm text-red-500">{error}</p>}
35
+ </div>
36
+ <Button variant="outline" size="sm" onClick={fetchFiles}>
37
+ <RefreshCw className="mr-2 h-4 w-4" /> Refresh
38
+ </Button>
39
+ </div>
40
+
41
+ <div className="grid grid-cols-1 md:grid-cols-4 gap-6 h-full min-h-[500px]">
42
+ {/* File List */}
43
+ <Card className="md:col-span-1 flex flex-col">
44
+ <CardHeader className="py-4">
45
+ <CardTitle className="text-lg">Files</CardTitle>
46
+ <CardDescription>Select a schema file to edit</CardDescription>
47
+ </CardHeader>
48
+ <CardContent className="flex-1 overflow-auto p-2">
49
+ <div className="space-y-1">
50
+ {files.map(file => (
51
+ <button
52
+ key={file}
53
+ onClick={() => setSelectedFile(file)}
54
+ className={cn(
55
+ "w-full text-left px-3 py-2 text-sm rounded-md transition-colors flex items-center",
56
+ selectedFile === file
57
+ ? "bg-primary text-primary-foreground font-medium"
58
+ : "hover:bg-muted"
59
+ )}
60
+ >
61
+ <FileCode className="mr-2 h-4 w-4 opacity-70" />
62
+ {file}
63
+ </button>
64
+ ))}
65
+ {files.length === 0 && (
66
+ <div className="text-sm text-muted-foreground p-2">No schema files found</div>
67
+ )}
68
+ </div>
69
+ </CardContent>
70
+ </Card>
71
+
72
+ {/* Editor */}
73
+ <div className="md:col-span-3 h-full">
74
+ {selectedFile ? (
75
+ <FileEditor filePath={selectedFile} />
76
+ ) : (
77
+ <Card className="h-full flex items-center justify-center">
78
+ <div className="text-muted-foreground">Select a file to view content</div>
79
+ </Card>
80
+ )}
81
+ </div>
82
+ </div>
83
+ </div>
84
+ );
85
+ }
@@ -0,0 +1,77 @@
1
+ /** @type {import('tailwindcss').Config} */
2
+ module.exports = {
3
+ darkMode: ["class"],
4
+ content: [
5
+ './pages/**/*.{ts,tsx}',
6
+ './components/**/*.{ts,tsx}',
7
+ './app/**/*.{ts,tsx}',
8
+ './src/**/*.{ts,tsx}',
9
+ ],
10
+ prefix: "",
11
+ theme: {
12
+ container: {
13
+ center: true,
14
+ padding: "2rem",
15
+ screens: {
16
+ "2xl": "1400px",
17
+ },
18
+ },
19
+ extend: {
20
+ colors: {
21
+ border: "hsl(var(--border))",
22
+ input: "hsl(var(--input))",
23
+ ring: "hsl(var(--ring))",
24
+ background: "hsl(var(--background))",
25
+ foreground: "hsl(var(--foreground))",
26
+ primary: {
27
+ DEFAULT: "hsl(var(--primary))",
28
+ foreground: "hsl(var(--primary-foreground))",
29
+ },
30
+ secondary: {
31
+ DEFAULT: "hsl(var(--secondary))",
32
+ foreground: "hsl(var(--secondary-foreground))",
33
+ },
34
+ destructive: {
35
+ DEFAULT: "hsl(var(--destructive))",
36
+ foreground: "hsl(var(--destructive-foreground))",
37
+ },
38
+ muted: {
39
+ DEFAULT: "hsl(var(--muted))",
40
+ foreground: "hsl(var(--muted-foreground))",
41
+ },
42
+ accent: {
43
+ DEFAULT: "hsl(var(--accent))",
44
+ foreground: "hsl(var(--accent-foreground))",
45
+ },
46
+ popover: {
47
+ DEFAULT: "hsl(var(--popover))",
48
+ foreground: "hsl(var(--popover-foreground))",
49
+ },
50
+ card: {
51
+ DEFAULT: "hsl(var(--card))",
52
+ foreground: "hsl(var(--card-foreground))",
53
+ },
54
+ },
55
+ borderRadius: {
56
+ lg: "var(--radius)",
57
+ md: "calc(var(--radius) - 2px)",
58
+ sm: "calc(var(--radius) - 4px)",
59
+ },
60
+ keyframes: {
61
+ "accordion-down": {
62
+ from: { height: "0" },
63
+ to: { height: "var(--radix-accordion-content-height)" },
64
+ },
65
+ "accordion-up": {
66
+ from: { height: "var(--radix-accordion-content-height)" },
67
+ to: { height: "0" },
68
+ },
69
+ },
70
+ animation: {
71
+ "accordion-down": "accordion-down 0.2s ease-out",
72
+ "accordion-up": "accordion-up 0.2s ease-out",
73
+ },
74
+ },
75
+ },
76
+ plugins: [require("tailwindcss-animate")],
77
+ }
package/tsconfig.json CHANGED
@@ -15,7 +15,11 @@
15
15
  "strict": true,
16
16
  "noUnusedLocals": true,
17
17
  "noUnusedParameters": true,
18
- "noFallthroughCasesInSwitch": true
18
+ "noFallthroughCasesInSwitch": true,
19
+ "baseUrl": ".",
20
+ "paths": {
21
+ "@/*": ["./src/*"]
22
+ }
19
23
  },
20
24
  "include": ["src"]
21
25
  }