@cisri/db-schema-editor 1.0.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.
package/README.md ADDED
@@ -0,0 +1,70 @@
1
+ # @cisri/db-schema-editor
2
+
3
+ A database table schema editor based on shadcn/ui. Structurally express a database table — table name, columns, types, and constraints — and edit it through a tree/table UI.
4
+
5
+ ## 安装
6
+
7
+ ```bash
8
+ npm install @cisri/db-schema-editor
9
+ ```
10
+
11
+ ## Peer Dependencies
12
+
13
+ 本组件依赖以下库,请确保你的项目已安装:
14
+
15
+ ```bash
16
+ npm install react react-dom @radix-ui/react-checkbox @radix-ui/react-select @radix-ui/react-slot class-variance-authority clsx lucide-react tailwind-merge
17
+ ```
18
+
19
+ ## 使用
20
+
21
+ ```tsx
22
+ import { useState } from 'react';
23
+ import { DbSchemaEditor } from '@cisri/db-schema-editor';
24
+ import type { DbTable } from '@cisri/db-schema-core';
25
+
26
+ export default function App() {
27
+ const [table, setTable] = useState<DbTable>({
28
+ id: 't1',
29
+ name: 'users',
30
+ columns: [
31
+ { id: 'c1', name: 'id', type: 'uuid', nullable: false, primaryKey: true },
32
+ { id: 'c2', name: 'email', type: 'varchar', nullable: false, unique: true },
33
+ ],
34
+ });
35
+
36
+ return <DbSchemaEditor value={table} onChange={setTable} />;
37
+ }
38
+ ```
39
+
40
+ ## Tailwind 配置
41
+
42
+ 确保 `tailwind.config.ts` 的 `content` 包含以下路径,否则组件的 Tailwind 类名不会被扫描到:
43
+
44
+ ```ts
45
+ content: [
46
+ './node_modules/@cisri/db-schema-editor/dist/**/*.{js,cjs}',
47
+ ];
48
+ ```
49
+
50
+ 如果同时使用多个 `@cisri/*` 组件,可以使用通配符:
51
+
52
+ ```ts
53
+ content: [
54
+ './node_modules/@cisri/*/dist/**/*.{js,cjs}',
55
+ ];
56
+ ```
57
+
58
+ ## CSS 变量
59
+
60
+ 本组件使用 shadcn/ui 标准 CSS 变量,可通过覆盖 `:root` 中的变量自定义主题。
61
+
62
+ ## 自定义样式
63
+
64
+ ```tsx
65
+ <DbSchemaEditor
66
+ value={table}
67
+ onChange={setTable}
68
+ className="w-full max-w-2xl border-primary"
69
+ />
70
+ ```
@@ -0,0 +1,10 @@
1
+ import { type DbTable } from '@cisri/db-schema-core';
2
+ export interface DbSchemaEditorProps {
3
+ value: DbTable;
4
+ onChange: (value: DbTable) => void;
5
+ onSave?: (value: DbTable) => void;
6
+ readOnly?: boolean;
7
+ className?: string;
8
+ }
9
+ export declare function DbSchemaEditor({ value, onChange, onSave, readOnly, className, }: DbSchemaEditorProps): import("react").JSX.Element;
10
+ //# sourceMappingURL=db-schema-editor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"db-schema-editor.d.ts","sourceRoot":"","sources":["../src/db-schema-editor.tsx"],"names":[],"mappings":"AAoCA,OAAO,EAIL,KAAK,OAAO,EACb,MAAM,uBAAuB,CAAC;AAY/B,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE,OAAO,CAAC;IACf,QAAQ,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;IACnC,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;IAClC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAsKD,wBAAgB,cAAc,CAAC,EAC7B,KAAK,EACL,QAAQ,EACR,MAAM,EACN,QAAQ,EACR,SAAS,GACV,EAAE,mBAAmB,+BAuOrB"}
package/dist/index.cjs ADDED
@@ -0,0 +1 @@
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const e=require("react/jsx-runtime"),m=require("react"),p=require("@dnd-kit/core"),w=require("@dnd-kit/sortable"),B=require("@dnd-kit/modifiers"),A=require("@dnd-kit/utilities"),t=require("@cisri/shadcn"),k=require("@cisri/core"),N=require("lucide-react"),v=require("@cisri/db-schema-core");function V(s,n,c){return{...s,columns:s.columns.map(a=>a.id===n?{...a,...c}:a)}}function T(s,n){const c=s.columns.findIndex(x=>x.id===n);if(c<0)return s;const a=[...s.columns];return a.splice(c+1,0,v.createEmptyColumn()),{...s,columns:a}}function z(s,n){return{...s,columns:s.columns.filter(c=>c.id!==n)}}function P(s,n,c){const a=s.columns.findIndex(l=>l.id===n),x=s.columns.findIndex(l=>l.id===c);if(a<0||x<0||a===x)return s;const d=[...s.columns],[h]=d.splice(a,1);return d.splice(x,0,h),{...s,columns:d}}function K(s,n){return{...s,name:n}}function F(s,n){return{...s,description:n}}function y(s,n){if(s===n)return!0;if(typeof s!="object"||typeof n!="object"||s==null||n==null)return!1;const c=Array.isArray(s),a=Array.isArray(n);if(c!==a)return!1;if(c){const b=s,j=n;return b.length===j.length&&b.every((f,C)=>y(f,j[C]))}const x=s,d=n,h=Object.keys(x),l=Object.keys(d);return h.length===l.length&&h.every(b=>y(x[b],d[b]))}const G=["varchar","text","integer","bigint","decimal","boolean","date","timestamp","uuid","jsonb"];function S({column:s,readOnly:n,disableDelete:c,onUpdate:a,onInsertBelow:x,onDelete:d}){const{attributes:h,listeners:l,setNodeRef:b,transform:j,transition:f,isDragging:C}=w.useSortable({id:s.id,disabled:n}),i=s.name||"未命名";return e.jsxs(t.TableRow,{ref:b,style:{transform:A.CSS.Transform.toString(j),transition:f},className:k.cn(C&&"opacity-50"),children:[e.jsx(t.TableCell,{className:"w-10 px-2",children:n?e.jsx("span",{className:"flex h-7 w-7 items-center justify-center text-muted-foreground/30",children:e.jsx(N.GripVertical,{className:"h-4 w-4"})}):e.jsx("button",{type:"button",className:"flex h-7 w-7 cursor-grab items-center justify-center text-muted-foreground active:cursor-grabbing","aria-label":`拖动列 ${i} 排序`,...h,...l,children:e.jsx(N.GripVertical,{className:"pointer-events-none h-4 w-4"})})}),e.jsx(t.TableCell,{children:e.jsx(t.Input,{"aria-label":`列 ${i} 名称`,autoComplete:"off",disabled:n,className:"h-7",value:s.name,onChange:o=>a({name:o.target.value})})}),e.jsx(t.TableCell,{children:e.jsxs(t.Select,{value:s.type,onValueChange:o=>a({type:o}),disabled:n,children:[e.jsx(t.SelectTrigger,{"aria-label":`列 ${i} 类型`,className:"h-7 w-28 text-xs",children:e.jsx(t.SelectValue,{})}),e.jsx(t.SelectContent,{children:G.map(o=>e.jsx(t.SelectItem,{value:o,className:"text-xs",children:o},o))})]})}),e.jsx(t.TableCell,{children:e.jsx(t.Checkbox,{"aria-label":`列 ${i} 可空`,checked:s.nullable,disabled:n,onCheckedChange:o=>a({nullable:o===!0})})}),e.jsx(t.TableCell,{children:e.jsx(t.Checkbox,{"aria-label":`列 ${i} 主键`,checked:s.primaryKey,disabled:n,onCheckedChange:o=>a({primaryKey:o===!0})})}),e.jsx(t.TableCell,{children:e.jsx(t.Checkbox,{"aria-label":`列 ${i} 唯一`,checked:s.unique,disabled:n,onCheckedChange:o=>a({unique:o===!0})})}),e.jsx(t.TableCell,{children:e.jsx(t.Input,{"aria-label":`列 ${i} 默认值`,autoComplete:"off",disabled:n,className:"h-7",value:s.defaultValue??"",onChange:o=>a({defaultValue:o.target.value||void 0})})}),e.jsx(t.TableCell,{children:e.jsx(t.Input,{"aria-label":`列 ${i} 描述`,autoComplete:"off",disabled:n,className:"h-7",value:s.description??"",onChange:o=>a({description:o.target.value||void 0})})}),e.jsx(t.TableCell,{children:!n&&e.jsxs("div",{className:"flex items-center justify-end gap-1",children:[e.jsx(t.Button,{type:"button",variant:"ghost",size:"icon",className:"h-7 w-7",onClick:x,"aria-label":`在列 ${i} 下方添加列`,children:e.jsx(N.Plus,{className:"h-4 w-4"})}),e.jsx(t.Button,{type:"button",variant:"ghost",size:"icon",className:"h-7 w-7",disabled:c,onClick:d,"aria-label":`删除列 ${i}`,children:e.jsx(N.Trash2,{className:"h-4 w-4 text-destructive"})})]})})]})}function L({value:s,onChange:n,onSave:c,readOnly:a,className:x}){const[d,h]=m.useState(()=>({history:[s],index:0})),l=d.history[d.index],b=m.useRef(d);m.useEffect(()=>{b.current=d},[d]);const j=m.useRef(s),[f]=m.useState(()=>v.createEmptyColumn());m.useEffect(()=>{y(s,j.current)||(h({history:[s],index:0}),j.current=s)},[s]);const C=m.useCallback(r=>{h(u=>y(u.history[u.index],r)?u:{history:[...u.history.slice(0,u.index+1),r],index:u.index+1})},[]),i=m.useCallback(r=>{j.current=r,C(r),n(r)},[n,C]),o=d.index>0,I=d.index<d.history.length-1,R=m.useCallback(()=>{const r=b.current;if(r.index<=0)return;const u=r.index-1,g=r.history[u];h({...r,index:u}),j.current=g,n(g)},[n]),q=m.useCallback(()=>{const r=b.current;if(r.index>=r.history.length-1)return;const u=r.index+1,g=r.history[u];h({...r,index:u}),j.current=g,n(g)},[n]),E=m.useCallback(()=>{c==null||c(l)},[c,l]),D=p.useSensors(p.useSensor(p.PointerSensor,{activationConstraint:{distance:5}}),p.useSensor(p.KeyboardSensor,{coordinateGetter:w.sortableKeyboardCoordinates})),$=m.useCallback(r=>{const{active:u,over:g}=r;!g||u.id===g.id||i(P(l,String(u.id),String(g.id)))},[i,l]),H=l.columns.length>0?l.columns.map(r=>r.id):a?[]:[f.id];return e.jsxs("div",{className:k.cn("rounded-md border border-border bg-card text-card-foreground",x),children:[e.jsxs("div",{className:"flex flex-wrap items-center justify-between gap-2 border-b border-border p-2",children:[e.jsx("div",{className:"text-sm font-medium",children:"数据库表编辑器"}),e.jsxs("div",{className:"flex items-center gap-2",children:[!a&&c&&e.jsx(t.Button,{type:"button",size:"sm",onClick:E,className:"h-7 text-xs",children:"保存"}),!a&&e.jsxs(e.Fragment,{children:[e.jsx(t.Button,{type:"button",variant:"ghost",size:"icon",onClick:R,disabled:!o,className:"h-7 w-7","aria-label":"撤销",children:e.jsx(N.Undo2,{className:"h-4 w-4"})}),e.jsx(t.Button,{type:"button",variant:"ghost",size:"icon",onClick:q,disabled:!I,className:"h-7 w-7","aria-label":"恢复",children:e.jsx(N.Redo2,{className:"h-4 w-4"})})]})]})]}),e.jsxs("div",{className:"space-y-2 border-b border-border p-2",children:[e.jsxs("div",{className:"flex items-center gap-2",children:[e.jsx("label",{htmlFor:"db-table-name",className:"w-10 shrink-0 text-xs text-muted-foreground",children:"表名"}),e.jsx(t.Input,{id:"db-table-name",autoComplete:"off",disabled:a,className:"h-7",value:l.name,onChange:r=>i(K(l,r.target.value))})]}),e.jsxs("div",{className:"flex items-center gap-2",children:[e.jsx("label",{htmlFor:"db-table-description",className:"w-10 shrink-0 text-xs text-muted-foreground",children:"描述"}),e.jsx(t.Input,{id:"db-table-description",autoComplete:"off",disabled:a,className:"h-7",value:l.description??"",onChange:r=>i(F(l,r.target.value||void 0))})]})]}),e.jsx(p.DndContext,{sensors:D,collisionDetection:p.closestCenter,modifiers:[B.restrictToVerticalAxis],onDragEnd:$,children:e.jsxs(t.Table,{children:[e.jsx(t.TableHeader,{children:e.jsxs(t.TableRow,{children:[e.jsx(t.TableHead,{className:"w-10"}),e.jsx(t.TableHead,{className:"w-[20%]",children:"名称"}),e.jsx(t.TableHead,{className:"w-[12%]",children:"类型"}),e.jsx(t.TableHead,{className:"w-[8%]",children:"可空"}),e.jsx(t.TableHead,{className:"w-[8%]",children:"主键"}),e.jsx(t.TableHead,{className:"w-[8%]",children:"唯一"}),e.jsx(t.TableHead,{className:"w-[14%]",children:"默认值"}),e.jsx(t.TableHead,{className:"w-[20%]",children:"描述"}),e.jsx(t.TableHead,{className:"w-[10%]"})]})}),e.jsx(t.TableBody,{children:e.jsx(w.SortableContext,{items:H,strategy:w.verticalListSortingStrategy,children:l.columns.length===0?a?e.jsx(t.TableRow,{children:e.jsx(t.TableCell,{colSpan:9,className:"h-12 text-center text-sm text-muted-foreground",children:"暂无列"})}):e.jsx(S,{column:f,disableDelete:!0,onUpdate:r=>i({...l,columns:[{...f,...r}]}),onInsertBelow:()=>i(T({...l,columns:[f]},f.id)),onDelete:()=>{}},f.id):l.columns.map(r=>e.jsx(S,{column:r,readOnly:a,onUpdate:u=>i(V(l,r.id,u)),onInsertBelow:()=>i(T(l,r.id)),onDelete:()=>i(z(l,r.id))},r.id))})})]})})]})}exports.DbSchemaEditor=L;
@@ -0,0 +1,4 @@
1
+ export { DbSchemaEditor } from './db-schema-editor';
2
+ export type { DbSchemaEditorProps } from './db-schema-editor';
3
+ export type { DbTable, DbColumn, DbColumnType } from '@cisri/db-schema-core';
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACpD,YAAY,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAC9D,YAAY,EAAE,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,373 @@
1
+ import { jsxs as u, jsx as e, Fragment as L } from "react/jsx-runtime";
2
+ import { useState as j, useRef as I, useEffect as D, useCallback as w } from "react";
3
+ import { useSensors as Y, useSensor as $, PointerSensor as _, KeyboardSensor as J, DndContext as M, closestCenter as Q } from "@dnd-kit/core";
4
+ import { sortableKeyboardCoordinates as W, SortableContext as X, verticalListSortingStrategy as Z, useSortable as U } from "@dnd-kit/sortable";
5
+ import { restrictToVerticalAxis as O } from "@dnd-kit/modifiers";
6
+ import { CSS as ee } from "@dnd-kit/utilities";
7
+ import { Button as y, Input as v, Table as te, TableHeader as ne, TableRow as T, TableHead as N, TableBody as re, TableCell as p, Select as se, SelectTrigger as ie, SelectValue as ae, SelectContent as oe, SelectItem as le, Checkbox as k } from "@cisri/shadcn";
8
+ import { cn as V } from "@cisri/core";
9
+ import { Undo2 as ce, Redo2 as de, GripVertical as E, Plus as me, Trash2 as ue } from "lucide-react";
10
+ import { createEmptyColumn as z } from "@cisri/db-schema-core";
11
+ function he(t, n, o) {
12
+ return {
13
+ ...t,
14
+ columns: t.columns.map((s) => s.id === n ? { ...s, ...o } : s)
15
+ };
16
+ }
17
+ function R(t, n) {
18
+ const o = t.columns.findIndex((m) => m.id === n);
19
+ if (o < 0) return t;
20
+ const s = [...t.columns];
21
+ return s.splice(o + 1, 0, z()), { ...t, columns: s };
22
+ }
23
+ function fe(t, n) {
24
+ return { ...t, columns: t.columns.filter((o) => o.id !== n) };
25
+ }
26
+ function be(t, n, o) {
27
+ const s = t.columns.findIndex((i) => i.id === n), m = t.columns.findIndex((i) => i.id === o);
28
+ if (s < 0 || m < 0 || s === m) return t;
29
+ const c = [...t.columns], [h] = c.splice(s, 1);
30
+ return c.splice(m, 0, h), { ...t, columns: c };
31
+ }
32
+ function xe(t, n) {
33
+ return { ...t, name: n };
34
+ }
35
+ function pe(t, n) {
36
+ return { ...t, description: n };
37
+ }
38
+ function S(t, n) {
39
+ if (t === n) return !0;
40
+ if (typeof t != "object" || typeof n != "object" || t == null || n == null) return !1;
41
+ const o = Array.isArray(t), s = Array.isArray(n);
42
+ if (o !== s) return !1;
43
+ if (o) {
44
+ const f = t, b = n;
45
+ return f.length === b.length && f.every((x, C) => S(x, b[C]));
46
+ }
47
+ const m = t, c = n, h = Object.keys(m), i = Object.keys(c);
48
+ return h.length === i.length && h.every((f) => S(m[f], c[f]));
49
+ }
50
+ const ge = [
51
+ "varchar",
52
+ "text",
53
+ "integer",
54
+ "bigint",
55
+ "decimal",
56
+ "boolean",
57
+ "date",
58
+ "timestamp",
59
+ "uuid",
60
+ "jsonb"
61
+ ];
62
+ function A({
63
+ column: t,
64
+ readOnly: n,
65
+ disableDelete: o,
66
+ onUpdate: s,
67
+ onInsertBelow: m,
68
+ onDelete: c
69
+ }) {
70
+ const { attributes: h, listeners: i, setNodeRef: f, transform: b, transition: x, isDragging: C } = U({ id: t.id, disabled: n }), a = t.name || "未命名";
71
+ return /* @__PURE__ */ u(
72
+ T,
73
+ {
74
+ ref: f,
75
+ style: { transform: ee.Transform.toString(b), transition: x },
76
+ className: V(C && "opacity-50"),
77
+ children: [
78
+ /* @__PURE__ */ e(p, { className: "w-10 px-2", children: n ? /* @__PURE__ */ e("span", { className: "flex h-7 w-7 items-center justify-center text-muted-foreground/30", children: /* @__PURE__ */ e(E, { className: "h-4 w-4" }) }) : /* @__PURE__ */ e(
79
+ "button",
80
+ {
81
+ type: "button",
82
+ className: "flex h-7 w-7 cursor-grab items-center justify-center text-muted-foreground active:cursor-grabbing",
83
+ "aria-label": `拖动列 ${a} 排序`,
84
+ ...h,
85
+ ...i,
86
+ children: /* @__PURE__ */ e(E, { className: "pointer-events-none h-4 w-4" })
87
+ }
88
+ ) }),
89
+ /* @__PURE__ */ e(p, { children: /* @__PURE__ */ e(
90
+ v,
91
+ {
92
+ "aria-label": `列 ${a} 名称`,
93
+ autoComplete: "off",
94
+ disabled: n,
95
+ className: "h-7",
96
+ value: t.name,
97
+ onChange: (l) => s({ name: l.target.value })
98
+ }
99
+ ) }),
100
+ /* @__PURE__ */ e(p, { children: /* @__PURE__ */ u(
101
+ se,
102
+ {
103
+ value: t.type,
104
+ onValueChange: (l) => s({ type: l }),
105
+ disabled: n,
106
+ children: [
107
+ /* @__PURE__ */ e(ie, { "aria-label": `列 ${a} 类型`, className: "h-7 w-28 text-xs", children: /* @__PURE__ */ e(ae, {}) }),
108
+ /* @__PURE__ */ e(oe, { children: ge.map((l) => /* @__PURE__ */ e(le, { value: l, className: "text-xs", children: l }, l)) })
109
+ ]
110
+ }
111
+ ) }),
112
+ /* @__PURE__ */ e(p, { children: /* @__PURE__ */ e(
113
+ k,
114
+ {
115
+ "aria-label": `列 ${a} 可空`,
116
+ checked: t.nullable,
117
+ disabled: n,
118
+ onCheckedChange: (l) => s({ nullable: l === !0 })
119
+ }
120
+ ) }),
121
+ /* @__PURE__ */ e(p, { children: /* @__PURE__ */ e(
122
+ k,
123
+ {
124
+ "aria-label": `列 ${a} 主键`,
125
+ checked: t.primaryKey,
126
+ disabled: n,
127
+ onCheckedChange: (l) => s({ primaryKey: l === !0 })
128
+ }
129
+ ) }),
130
+ /* @__PURE__ */ e(p, { children: /* @__PURE__ */ e(
131
+ k,
132
+ {
133
+ "aria-label": `列 ${a} 唯一`,
134
+ checked: t.unique,
135
+ disabled: n,
136
+ onCheckedChange: (l) => s({ unique: l === !0 })
137
+ }
138
+ ) }),
139
+ /* @__PURE__ */ e(p, { children: /* @__PURE__ */ e(
140
+ v,
141
+ {
142
+ "aria-label": `列 ${a} 默认值`,
143
+ autoComplete: "off",
144
+ disabled: n,
145
+ className: "h-7",
146
+ value: t.defaultValue ?? "",
147
+ onChange: (l) => s({ defaultValue: l.target.value || void 0 })
148
+ }
149
+ ) }),
150
+ /* @__PURE__ */ e(p, { children: /* @__PURE__ */ e(
151
+ v,
152
+ {
153
+ "aria-label": `列 ${a} 描述`,
154
+ autoComplete: "off",
155
+ disabled: n,
156
+ className: "h-7",
157
+ value: t.description ?? "",
158
+ onChange: (l) => s({ description: l.target.value || void 0 })
159
+ }
160
+ ) }),
161
+ /* @__PURE__ */ e(p, { children: !n && /* @__PURE__ */ u("div", { className: "flex items-center justify-end gap-1", children: [
162
+ /* @__PURE__ */ e(
163
+ y,
164
+ {
165
+ type: "button",
166
+ variant: "ghost",
167
+ size: "icon",
168
+ className: "h-7 w-7",
169
+ onClick: m,
170
+ "aria-label": `在列 ${a} 下方添加列`,
171
+ children: /* @__PURE__ */ e(me, { className: "h-4 w-4" })
172
+ }
173
+ ),
174
+ /* @__PURE__ */ e(
175
+ y,
176
+ {
177
+ type: "button",
178
+ variant: "ghost",
179
+ size: "icon",
180
+ className: "h-7 w-7",
181
+ disabled: o,
182
+ onClick: c,
183
+ "aria-label": `删除列 ${a}`,
184
+ children: /* @__PURE__ */ e(ue, { className: "h-4 w-4 text-destructive" })
185
+ }
186
+ )
187
+ ] }) })
188
+ ]
189
+ }
190
+ );
191
+ }
192
+ function De({
193
+ value: t,
194
+ onChange: n,
195
+ onSave: o,
196
+ readOnly: s,
197
+ className: m
198
+ }) {
199
+ const [c, h] = j(() => ({
200
+ history: [t],
201
+ index: 0
202
+ })), i = c.history[c.index], f = I(c);
203
+ D(() => {
204
+ f.current = c;
205
+ }, [c]);
206
+ const b = I(t), [x] = j(() => z());
207
+ D(() => {
208
+ S(t, b.current) || (h({ history: [t], index: 0 }), b.current = t);
209
+ }, [t]);
210
+ const C = w((r) => {
211
+ h((d) => S(d.history[d.index], r) ? d : {
212
+ history: [...d.history.slice(0, d.index + 1), r],
213
+ index: d.index + 1
214
+ });
215
+ }, []), a = w(
216
+ (r) => {
217
+ b.current = r, C(r), n(r);
218
+ },
219
+ [n, C]
220
+ ), l = c.index > 0, B = c.index < c.history.length - 1, K = w(() => {
221
+ const r = f.current;
222
+ if (r.index <= 0) return;
223
+ const d = r.index - 1, g = r.history[d];
224
+ h({ ...r, index: d }), b.current = g, n(g);
225
+ }, [n]), P = w(() => {
226
+ const r = f.current;
227
+ if (r.index >= r.history.length - 1) return;
228
+ const d = r.index + 1, g = r.history[d];
229
+ h({ ...r, index: d }), b.current = g, n(g);
230
+ }, [n]), q = w(() => {
231
+ o == null || o(i);
232
+ }, [o, i]), F = Y(
233
+ $(_, { activationConstraint: { distance: 5 } }),
234
+ $(J, { coordinateGetter: W })
235
+ ), G = w(
236
+ (r) => {
237
+ const { active: d, over: g } = r;
238
+ !g || d.id === g.id || a(be(i, String(d.id), String(g.id)));
239
+ },
240
+ [a, i]
241
+ ), H = i.columns.length > 0 ? i.columns.map((r) => r.id) : s ? [] : [x.id];
242
+ return /* @__PURE__ */ u(
243
+ "div",
244
+ {
245
+ className: V(
246
+ "rounded-md border border-border bg-card text-card-foreground",
247
+ m
248
+ ),
249
+ children: [
250
+ /* @__PURE__ */ u("div", { className: "flex flex-wrap items-center justify-between gap-2 border-b border-border p-2", children: [
251
+ /* @__PURE__ */ e("div", { className: "text-sm font-medium", children: "数据库表编辑器" }),
252
+ /* @__PURE__ */ u("div", { className: "flex items-center gap-2", children: [
253
+ !s && o && /* @__PURE__ */ e(y, { type: "button", size: "sm", onClick: q, className: "h-7 text-xs", children: "保存" }),
254
+ !s && /* @__PURE__ */ u(L, { children: [
255
+ /* @__PURE__ */ e(
256
+ y,
257
+ {
258
+ type: "button",
259
+ variant: "ghost",
260
+ size: "icon",
261
+ onClick: K,
262
+ disabled: !l,
263
+ className: "h-7 w-7",
264
+ "aria-label": "撤销",
265
+ children: /* @__PURE__ */ e(ce, { className: "h-4 w-4" })
266
+ }
267
+ ),
268
+ /* @__PURE__ */ e(
269
+ y,
270
+ {
271
+ type: "button",
272
+ variant: "ghost",
273
+ size: "icon",
274
+ onClick: P,
275
+ disabled: !B,
276
+ className: "h-7 w-7",
277
+ "aria-label": "恢复",
278
+ children: /* @__PURE__ */ e(de, { className: "h-4 w-4" })
279
+ }
280
+ )
281
+ ] })
282
+ ] })
283
+ ] }),
284
+ /* @__PURE__ */ u("div", { className: "space-y-2 border-b border-border p-2", children: [
285
+ /* @__PURE__ */ u("div", { className: "flex items-center gap-2", children: [
286
+ /* @__PURE__ */ e("label", { htmlFor: "db-table-name", className: "w-10 shrink-0 text-xs text-muted-foreground", children: "表名" }),
287
+ /* @__PURE__ */ e(
288
+ v,
289
+ {
290
+ id: "db-table-name",
291
+ autoComplete: "off",
292
+ disabled: s,
293
+ className: "h-7",
294
+ value: i.name,
295
+ onChange: (r) => a(xe(i, r.target.value))
296
+ }
297
+ )
298
+ ] }),
299
+ /* @__PURE__ */ u("div", { className: "flex items-center gap-2", children: [
300
+ /* @__PURE__ */ e(
301
+ "label",
302
+ {
303
+ htmlFor: "db-table-description",
304
+ className: "w-10 shrink-0 text-xs text-muted-foreground",
305
+ children: "描述"
306
+ }
307
+ ),
308
+ /* @__PURE__ */ e(
309
+ v,
310
+ {
311
+ id: "db-table-description",
312
+ autoComplete: "off",
313
+ disabled: s,
314
+ className: "h-7",
315
+ value: i.description ?? "",
316
+ onChange: (r) => a(pe(i, r.target.value || void 0))
317
+ }
318
+ )
319
+ ] })
320
+ ] }),
321
+ /* @__PURE__ */ e(
322
+ M,
323
+ {
324
+ sensors: F,
325
+ collisionDetection: Q,
326
+ modifiers: [O],
327
+ onDragEnd: G,
328
+ children: /* @__PURE__ */ u(te, { children: [
329
+ /* @__PURE__ */ e(ne, { children: /* @__PURE__ */ u(T, { children: [
330
+ /* @__PURE__ */ e(N, { className: "w-10" }),
331
+ /* @__PURE__ */ e(N, { className: "w-[20%]", children: "名称" }),
332
+ /* @__PURE__ */ e(N, { className: "w-[12%]", children: "类型" }),
333
+ /* @__PURE__ */ e(N, { className: "w-[8%]", children: "可空" }),
334
+ /* @__PURE__ */ e(N, { className: "w-[8%]", children: "主键" }),
335
+ /* @__PURE__ */ e(N, { className: "w-[8%]", children: "唯一" }),
336
+ /* @__PURE__ */ e(N, { className: "w-[14%]", children: "默认值" }),
337
+ /* @__PURE__ */ e(N, { className: "w-[20%]", children: "描述" }),
338
+ /* @__PURE__ */ e(N, { className: "w-[10%]" })
339
+ ] }) }),
340
+ /* @__PURE__ */ e(re, { children: /* @__PURE__ */ e(X, { items: H, strategy: Z, children: i.columns.length === 0 ? s ? /* @__PURE__ */ e(T, { children: /* @__PURE__ */ e(p, { colSpan: 9, className: "h-12 text-center text-sm text-muted-foreground", children: "暂无列" }) }) : /* @__PURE__ */ e(
341
+ A,
342
+ {
343
+ column: x,
344
+ disableDelete: !0,
345
+ onUpdate: (r) => a({ ...i, columns: [{ ...x, ...r }] }),
346
+ onInsertBelow: () => a(
347
+ R({ ...i, columns: [x] }, x.id)
348
+ ),
349
+ onDelete: () => {
350
+ }
351
+ },
352
+ x.id
353
+ ) : i.columns.map((r) => /* @__PURE__ */ e(
354
+ A,
355
+ {
356
+ column: r,
357
+ readOnly: s,
358
+ onUpdate: (d) => a(he(i, r.id, d)),
359
+ onInsertBelow: () => a(R(i, r.id)),
360
+ onDelete: () => a(fe(i, r.id))
361
+ },
362
+ r.id
363
+ )) }) })
364
+ ] })
365
+ }
366
+ )
367
+ ]
368
+ }
369
+ );
370
+ }
371
+ export {
372
+ De as DbSchemaEditor
373
+ };
@@ -0,0 +1,20 @@
1
+ import type { DbColumn, DbTable } from '@cisri/db-schema-core';
2
+ /** 列字段的部分更新(不可更新 id)。 */
3
+ export type ColumnPatch = Partial<Omit<DbColumn, 'id'>>;
4
+ /** 更新指定列的字段,返回新表。 */
5
+ export declare function updateColumn(table: DbTable, id: string, patch: ColumnPatch): DbTable;
6
+ /** 追加一个空列(默认 varchar、可空)。 */
7
+ export declare function addColumn(table: DbTable, name?: string): DbTable;
8
+ /** 在指定列下方插入一个空列;找不到 id 时返回原表。 */
9
+ export declare function insertColumnBelow(table: DbTable, id: string): DbTable;
10
+ /** 按 id 删除列。 */
11
+ export declare function deleteColumn(table: DbTable, id: string): DbTable;
12
+ /** 将 fromId 列移动到 toId 列的位置(dnd-kit onDragEnd 语义:active 落到 over 的位置);任一 id 不存在或相同时返回原表。 */
13
+ export declare function reorderColumns(table: DbTable, fromId: string, toId: string): DbTable;
14
+ /** 更新表名。 */
15
+ export declare function updateTableName(table: DbTable, name: string): DbTable;
16
+ /** 更新表描述(传 undefined 清除)。 */
17
+ export declare function updateTableDescription(table: DbTable, description: string | undefined): DbTable;
18
+ /** 深比较,用于历史去重与外部 value 同步判断。 */
19
+ export declare function deepEqual(a: unknown, b: unknown): boolean;
20
+ //# sourceMappingURL=schema-utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema-utils.d.ts","sourceRoot":"","sources":["../src/schema-utils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,uBAAuB,CAAC;AAG/D,yBAAyB;AACzB,MAAM,MAAM,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC,CAAC;AAExD,qBAAqB;AACrB,wBAAgB,YAAY,CAC1B,KAAK,EAAE,OAAO,EACd,EAAE,EAAE,MAAM,EACV,KAAK,EAAE,WAAW,GACjB,OAAO,CAKT;AAED,6BAA6B;AAC7B,wBAAgB,SAAS,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,SAAK,GAAG,OAAO,CAE5D;AAED,iCAAiC;AACjC,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,OAAO,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAMrE;AAED,gBAAgB;AAChB,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAEhE;AAED,0FAA0F;AAC1F,wBAAgB,cAAc,CAAC,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAQpF;AAED,YAAY;AACZ,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAErE;AAED,6BAA6B;AAC7B,wBAAgB,sBAAsB,CACpC,KAAK,EAAE,OAAO,EACd,WAAW,EAAE,MAAM,GAAG,SAAS,GAC9B,OAAO,CAET;AAED,gCAAgC;AAChC,wBAAgB,SAAS,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,OAAO,GAAG,OAAO,CAgBzD"}
package/package.json ADDED
@@ -0,0 +1,73 @@
1
+ {
2
+ "name": "@cisri/db-schema-editor",
3
+ "version": "1.0.0",
4
+ "description": "Database table schema editor business component based on shadcn/ui",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js",
13
+ "require": "./dist/index.cjs"
14
+ }
15
+ },
16
+ "files": [
17
+ "dist"
18
+ ],
19
+ "sideEffects": false,
20
+ "publishConfig": {
21
+ "access": "public"
22
+ },
23
+ "dependencies": {
24
+ "@cisri/core": "1.0.0",
25
+ "@cisri/db-schema-core": "1.0.0",
26
+ "@cisri/shadcn": "1.0.0"
27
+ },
28
+ "peerDependencies": {
29
+ "react": "^18.0.0 || ^19.0.0",
30
+ "react-dom": "^18.0.0 || ^19.0.0",
31
+ "lucide-react": "^0.300.0",
32
+ "@dnd-kit/core": "^6.0.0",
33
+ "@dnd-kit/sortable": "^8.0.0",
34
+ "@dnd-kit/modifiers": "^7.0.0",
35
+ "@dnd-kit/utilities": "^3.2.0"
36
+ },
37
+ "peerDependenciesMeta": {
38
+ "lucide-react": {
39
+ "optional": false
40
+ },
41
+ "@dnd-kit/core": {
42
+ "optional": false
43
+ },
44
+ "@dnd-kit/sortable": {
45
+ "optional": false
46
+ },
47
+ "@dnd-kit/modifiers": {
48
+ "optional": false
49
+ },
50
+ "@dnd-kit/utilities": {
51
+ "optional": false
52
+ }
53
+ },
54
+ "devDependencies": {
55
+ "@testing-library/jest-dom": "^6.4.0",
56
+ "@testing-library/react": "^15.0.0",
57
+ "@testing-library/user-event": "^14.5.0",
58
+ "@types/react": "^18.3.0",
59
+ "@types/react-dom": "^18.3.0",
60
+ "jsdom": "^24.0.0",
61
+ "react": "^18.3.1",
62
+ "react-dom": "^18.3.1",
63
+ "typescript": "^5.4.5",
64
+ "vite": "^5.2.11",
65
+ "vitest": "^1.6.0",
66
+ "@vitejs/plugin-react": "^4.2.0"
67
+ },
68
+ "scripts": {
69
+ "build": "tsc && vite build",
70
+ "test": "vitest run",
71
+ "test:watch": "vitest"
72
+ }
73
+ }