mirror of
https://github.com/chartdb/chartdb.git
synced 2025-11-04 14:03:15 +00:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
44eac7daff | ||
|
|
502472b083 | ||
|
|
52d2ea596c | ||
|
|
bd67ccfbcf | ||
|
|
62beb68fa1 | ||
|
|
09b1275475 | ||
|
|
5dd7fe75d1 | ||
|
|
2939320a15 | ||
|
|
a643852837 |
17
CHANGELOG.md
17
CHANGELOG.md
@@ -1,5 +1,22 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## [1.9.0](https://github.com/chartdb/chartdb/compare/v1.8.1...v1.9.0) (2025-03-13)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **canvas:** highlight the Show-All button when No-Tables are visible in the canvas ([#612](https://github.com/chartdb/chartdb/issues/612)) ([62beb68](https://github.com/chartdb/chartdb/commit/62beb68fa1ec22ccd4fe5e59a8ceb9d3e8f6d374))
|
||||||
|
* **chart max length:** add support for edit char max length ([#613](https://github.com/chartdb/chartdb/issues/613)) ([09b1275](https://github.com/chartdb/chartdb/commit/09b12754757b9625ca287d91a92cf0d83c9e2b89))
|
||||||
|
* **chart max length:** enable edit length from data type select box ([#616](https://github.com/chartdb/chartdb/issues/616)) ([bd67ccf](https://github.com/chartdb/chartdb/commit/bd67ccfbcf66b919453ca6c0bfd71e16772b3d8e))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **cardinality:** set true as default ([#583](https://github.com/chartdb/chartdb/issues/583)) ([2939320](https://github.com/chartdb/chartdb/commit/2939320a15a9ccd9eccfe46c26e04ca1edca2420))
|
||||||
|
* **performance:** Optimize performance of field comments editing ([#610](https://github.com/chartdb/chartdb/issues/610)) ([5dd7fe7](https://github.com/chartdb/chartdb/commit/5dd7fe75d1b0378ba406c75183c5e2356730c3b4))
|
||||||
|
* remove Buckle dialog ([#617](https://github.com/chartdb/chartdb/issues/617)) ([502472b](https://github.com/chartdb/chartdb/commit/502472b08342be425e66e2b6c94e5fe37ba14aa9))
|
||||||
|
* **shorcuts:** add shortcut to toggle the theme ([#602](https://github.com/chartdb/chartdb/issues/602)) ([a643852](https://github.com/chartdb/chartdb/commit/a6438528375ab54d3ec7d80ac6b6ddd65ea8cf1e))
|
||||||
|
|
||||||
## [1.8.1](https://github.com/chartdb/chartdb/compare/v1.8.0...v1.8.1) (2025-03-02)
|
## [1.8.1](https://github.com/chartdb/chartdb/compare/v1.8.0...v1.8.1) (2025-03-02)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "chartdb",
|
"name": "chartdb",
|
||||||
"version": "1.8.1",
|
"version": "1.9.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "chartdb",
|
"name": "chartdb",
|
||||||
"version": "1.8.1",
|
"version": "1.9.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/openai": "^0.0.51",
|
"@ai-sdk/openai": "^0.0.51",
|
||||||
"@dbml/core": "^3.9.5",
|
"@dbml/core": "^3.9.5",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "chartdb",
|
"name": "chartdb",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.8.1",
|
"version": "1.9.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -24,12 +24,19 @@ export interface SelectBoxOption {
|
|||||||
value: string;
|
value: string;
|
||||||
label: string;
|
label: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
|
regex?: string;
|
||||||
|
extractRegex?: RegExp;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SelectBoxProps {
|
export interface SelectBoxProps {
|
||||||
options: SelectBoxOption[];
|
options: SelectBoxOption[];
|
||||||
value?: string[] | string;
|
value?: string[] | string;
|
||||||
onChange?: (values: string[] | string) => void;
|
valueSuffix?: string;
|
||||||
|
optionSuffix?: (option: SelectBoxOption) => string;
|
||||||
|
onChange?: (
|
||||||
|
values: string[] | string,
|
||||||
|
regexMatches?: string[] | string
|
||||||
|
) => void;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
inputPlaceholder?: string;
|
inputPlaceholder?: string;
|
||||||
emptyPlaceholder?: string;
|
emptyPlaceholder?: string;
|
||||||
@@ -55,10 +62,12 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
|
|||||||
className,
|
className,
|
||||||
options,
|
options,
|
||||||
value,
|
value,
|
||||||
|
valueSuffix,
|
||||||
onChange,
|
onChange,
|
||||||
multiple,
|
multiple,
|
||||||
oneLine,
|
oneLine,
|
||||||
selectAll,
|
selectAll,
|
||||||
|
optionSuffix,
|
||||||
deselectAll,
|
deselectAll,
|
||||||
clearText,
|
clearText,
|
||||||
showClear,
|
showClear,
|
||||||
@@ -86,7 +95,7 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
|
|||||||
);
|
);
|
||||||
|
|
||||||
const handleSelect = React.useCallback(
|
const handleSelect = React.useCallback(
|
||||||
(selectedValue: string) => {
|
(selectedValue: string, regexMatches?: string[]) => {
|
||||||
if (multiple) {
|
if (multiple) {
|
||||||
const newValue =
|
const newValue =
|
||||||
value?.includes(selectedValue) && Array.isArray(value)
|
value?.includes(selectedValue) && Array.isArray(value)
|
||||||
@@ -94,7 +103,7 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
|
|||||||
: [...(value ?? []), selectedValue];
|
: [...(value ?? []), selectedValue];
|
||||||
onChange?.(newValue);
|
onChange?.(newValue);
|
||||||
} else {
|
} else {
|
||||||
onChange?.(selectedValue);
|
onChange?.(selectedValue, regexMatches);
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -199,6 +208,7 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
|
|||||||
(opt) => opt.value === value
|
(opt) => opt.value === value
|
||||||
)?.label
|
)?.label
|
||||||
}
|
}
|
||||||
|
{valueSuffix ? valueSuffix : ''}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
@@ -239,11 +249,22 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
|
|||||||
align="center"
|
align="center"
|
||||||
>
|
>
|
||||||
<Command
|
<Command
|
||||||
filter={(value, search) =>
|
filter={(value, search, keywords) => {
|
||||||
value.toLowerCase().includes(search.toLowerCase())
|
if (
|
||||||
? 1
|
keywords?.length &&
|
||||||
: 0
|
keywords.some((keyword) =>
|
||||||
|
new RegExp(keyword).test(search)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return value
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(search.toLowerCase())
|
||||||
|
? 1
|
||||||
|
: 0;
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<CommandInput
|
<CommandInput
|
||||||
@@ -302,14 +323,36 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
|
|||||||
const isSelected =
|
const isSelected =
|
||||||
Array.isArray(value) &&
|
Array.isArray(value) &&
|
||||||
value.includes(option.value);
|
value.includes(option.value);
|
||||||
|
|
||||||
|
const isRegexMatch =
|
||||||
|
option.regex &&
|
||||||
|
new RegExp(option.regex)?.test(
|
||||||
|
searchTerm
|
||||||
|
);
|
||||||
|
|
||||||
|
const matches = option.extractRegex
|
||||||
|
? searchTerm.match(
|
||||||
|
option.extractRegex
|
||||||
|
)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CommandItem
|
<CommandItem
|
||||||
className="flex items-center"
|
className="flex items-center"
|
||||||
key={option.value}
|
key={option.value}
|
||||||
|
keywords={
|
||||||
|
option.regex
|
||||||
|
? [option.regex]
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
// value={option.value}
|
// value={option.value}
|
||||||
onSelect={() =>
|
onSelect={() =>
|
||||||
handleSelect(
|
handleSelect(
|
||||||
option.value
|
option.value,
|
||||||
|
matches?.map(
|
||||||
|
(match) =>
|
||||||
|
match.toString()
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@@ -327,7 +370,15 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
|
|||||||
)}
|
)}
|
||||||
<div className="flex items-center truncate">
|
<div className="flex items-center truncate">
|
||||||
<span>
|
<span>
|
||||||
{option.label}
|
{isRegexMatch
|
||||||
|
? searchTerm
|
||||||
|
: option.label}
|
||||||
|
{!isRegexMatch &&
|
||||||
|
optionSuffix
|
||||||
|
? optionSuffix(
|
||||||
|
option
|
||||||
|
)
|
||||||
|
: ''}
|
||||||
</span>
|
</span>
|
||||||
{option.description && (
|
{option.description && (
|
||||||
<span className="ml-1 text-xs text-muted-foreground">
|
<span className="ml-1 text-xs text-muted-foreground">
|
||||||
@@ -337,9 +388,10 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{!multiple &&
|
{((!multiple &&
|
||||||
option.value ===
|
option.value ===
|
||||||
value && (
|
value) ||
|
||||||
|
isRegexMatch) && (
|
||||||
<CheckIcon
|
<CheckIcon
|
||||||
className={cn(
|
className={cn(
|
||||||
'ml-auto',
|
'ml-auto',
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ import { defaultSchemas } from '@/lib/data/default-schemas';
|
|||||||
import { useEventEmitter } from 'ahooks';
|
import { useEventEmitter } from 'ahooks';
|
||||||
import type { DBDependency } from '@/lib/domain/db-dependency';
|
import type { DBDependency } from '@/lib/domain/db-dependency';
|
||||||
import { storageInitialValue } from '../storage-context/storage-context';
|
import { storageInitialValue } from '../storage-context/storage-context';
|
||||||
|
import { useDiff } from '../diff-context/use-diff';
|
||||||
|
import type { DiffCalculatedEvent } from '../diff-context/diff-context';
|
||||||
|
|
||||||
export interface ChartDBProviderProps {
|
export interface ChartDBProviderProps {
|
||||||
diagram?: Diagram;
|
diagram?: Diagram;
|
||||||
@@ -30,7 +32,8 @@ export interface ChartDBProviderProps {
|
|||||||
|
|
||||||
export const ChartDBProvider: React.FC<
|
export const ChartDBProvider: React.FC<
|
||||||
React.PropsWithChildren<ChartDBProviderProps>
|
React.PropsWithChildren<ChartDBProviderProps>
|
||||||
> = ({ children, diagram, readonly }) => {
|
> = ({ children, diagram, readonly: readonlyProp }) => {
|
||||||
|
const { hasDiff } = useDiff();
|
||||||
let db = useStorage();
|
let db = useStorage();
|
||||||
const events = useEventEmitter<ChartDBEvent>();
|
const events = useEventEmitter<ChartDBEvent>();
|
||||||
const { setSchemasFilter, schemasFilter } = useLocalConfig();
|
const { setSchemasFilter, schemasFilter } = useLocalConfig();
|
||||||
@@ -53,9 +56,33 @@ export const ChartDBProvider: React.FC<
|
|||||||
const [dependencies, setDependencies] = useState<DBDependency[]>(
|
const [dependencies, setDependencies] = useState<DBDependency[]>(
|
||||||
diagram?.dependencies ?? []
|
diagram?.dependencies ?? []
|
||||||
);
|
);
|
||||||
|
const { events: diffEvents } = useDiff();
|
||||||
|
|
||||||
|
const diffCalculatedHandler = useCallback((event: DiffCalculatedEvent) => {
|
||||||
|
const { tablesAdded, fieldsAdded, relationshipsAdded } = event.data;
|
||||||
|
setTables((tables) =>
|
||||||
|
[...tables, ...(tablesAdded ?? [])].map((table) => {
|
||||||
|
const fields = fieldsAdded.get(table.id);
|
||||||
|
return fields
|
||||||
|
? { ...table, fields: [...table.fields, ...fields] }
|
||||||
|
: table;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
setRelationships((relationships) => [
|
||||||
|
...relationships,
|
||||||
|
...(relationshipsAdded ?? []),
|
||||||
|
]);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
diffEvents.useSubscription(diffCalculatedHandler);
|
||||||
|
|
||||||
const defaultSchemaName = defaultSchemas[databaseType];
|
const defaultSchemaName = defaultSchemas[databaseType];
|
||||||
|
|
||||||
|
const readonly = useMemo(
|
||||||
|
() => readonlyProp ?? hasDiff ?? false,
|
||||||
|
[readonlyProp, hasDiff]
|
||||||
|
);
|
||||||
|
|
||||||
if (readonly) {
|
if (readonly) {
|
||||||
db = storageInitialValue;
|
db = storageInitialValue;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,10 +47,6 @@ export interface DialogContext {
|
|||||||
openStarUsDialog: () => void;
|
openStarUsDialog: () => void;
|
||||||
closeStarUsDialog: () => void;
|
closeStarUsDialog: () => void;
|
||||||
|
|
||||||
// Buckle dialog
|
|
||||||
openBuckleDialog: () => void;
|
|
||||||
closeBuckleDialog: () => void;
|
|
||||||
|
|
||||||
// Export image dialog
|
// Export image dialog
|
||||||
openExportImageDialog: (
|
openExportImageDialog: (
|
||||||
params: Omit<ExportImageDialogProps, 'dialog'>
|
params: Omit<ExportImageDialogProps, 'dialog'>
|
||||||
@@ -97,8 +93,6 @@ export const dialogContext = createContext<DialogContext>({
|
|||||||
closeExportDiagramDialog: emptyFn,
|
closeExportDiagramDialog: emptyFn,
|
||||||
openImportDiagramDialog: emptyFn,
|
openImportDiagramDialog: emptyFn,
|
||||||
closeImportDiagramDialog: emptyFn,
|
closeImportDiagramDialog: emptyFn,
|
||||||
openBuckleDialog: emptyFn,
|
|
||||||
closeBuckleDialog: emptyFn,
|
|
||||||
openImportDBMLDialog: emptyFn,
|
openImportDBMLDialog: emptyFn,
|
||||||
closeImportDBMLDialog: emptyFn,
|
closeImportDBMLDialog: emptyFn,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ import type { ExportImageDialogProps } from '@/dialogs/export-image-dialog/expor
|
|||||||
import { ExportImageDialog } from '@/dialogs/export-image-dialog/export-image-dialog';
|
import { ExportImageDialog } from '@/dialogs/export-image-dialog/export-image-dialog';
|
||||||
import { ExportDiagramDialog } from '@/dialogs/export-diagram-dialog/export-diagram-dialog';
|
import { ExportDiagramDialog } from '@/dialogs/export-diagram-dialog/export-diagram-dialog';
|
||||||
import { ImportDiagramDialog } from '@/dialogs/import-diagram-dialog/import-diagram-dialog';
|
import { ImportDiagramDialog } from '@/dialogs/import-diagram-dialog/import-diagram-dialog';
|
||||||
import { BuckleDialog } from '@/dialogs/buckle-dialog/buckle-dialog';
|
|
||||||
import type { ImportDBMLDialogProps } from '@/dialogs/import-dbml-dialog/import-dbml-dialog';
|
import type { ImportDBMLDialogProps } from '@/dialogs/import-dbml-dialog/import-dbml-dialog';
|
||||||
import { ImportDBMLDialog } from '@/dialogs/import-dbml-dialog/import-dbml-dialog';
|
import { ImportDBMLDialog } from '@/dialogs/import-dbml-dialog/import-dbml-dialog';
|
||||||
|
|
||||||
@@ -54,7 +53,6 @@ export const DialogProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const [openStarUsDialog, setOpenStarUsDialog] = useState(false);
|
const [openStarUsDialog, setOpenStarUsDialog] = useState(false);
|
||||||
const [openBuckleDialog, setOpenBuckleDialog] = useState(false);
|
|
||||||
|
|
||||||
// Export image dialog
|
// Export image dialog
|
||||||
const [openExportImageDialog, setOpenExportImageDialog] = useState(false);
|
const [openExportImageDialog, setOpenExportImageDialog] = useState(false);
|
||||||
@@ -147,8 +145,6 @@ export const DialogProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
closeTableSchemaDialog: () => setOpenTableSchemaDialog(false),
|
closeTableSchemaDialog: () => setOpenTableSchemaDialog(false),
|
||||||
openStarUsDialog: () => setOpenStarUsDialog(true),
|
openStarUsDialog: () => setOpenStarUsDialog(true),
|
||||||
closeStarUsDialog: () => setOpenStarUsDialog(false),
|
closeStarUsDialog: () => setOpenStarUsDialog(false),
|
||||||
closeBuckleDialog: () => setOpenBuckleDialog(false),
|
|
||||||
openBuckleDialog: () => setOpenBuckleDialog(true),
|
|
||||||
closeExportImageDialog: () => setOpenExportImageDialog(false),
|
closeExportImageDialog: () => setOpenExportImageDialog(false),
|
||||||
openExportImageDialog: openExportImageDialogHandler,
|
openExportImageDialog: openExportImageDialogHandler,
|
||||||
openExportDiagramDialog: () => setOpenExportDiagramDialog(true),
|
openExportDiagramDialog: () => setOpenExportDiagramDialog(true),
|
||||||
@@ -193,7 +189,6 @@ export const DialogProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
/>
|
/>
|
||||||
<ExportDiagramDialog dialog={{ open: openExportDiagramDialog }} />
|
<ExportDiagramDialog dialog={{ open: openExportDiagramDialog }} />
|
||||||
<ImportDiagramDialog dialog={{ open: openImportDiagramDialog }} />
|
<ImportDiagramDialog dialog={{ open: openImportDiagramDialog }} />
|
||||||
<BuckleDialog dialog={{ open: openBuckleDialog }} />
|
|
||||||
<ImportDBMLDialog
|
<ImportDBMLDialog
|
||||||
dialog={{ open: openImportDBMLDialog }}
|
dialog={{ open: openImportDBMLDialog }}
|
||||||
{...importDBMLDialogParams}
|
{...importDBMLDialogParams}
|
||||||
|
|||||||
433
src/context/diff-context/diff-check/diff-check.ts
Normal file
433
src/context/diff-context/diff-check/diff-check.ts
Normal file
@@ -0,0 +1,433 @@
|
|||||||
|
import type { Diagram } from '@/lib/domain/diagram';
|
||||||
|
import type {
|
||||||
|
ChartDBDiff,
|
||||||
|
DiffMap,
|
||||||
|
DiffObject,
|
||||||
|
FieldDiffAttribute,
|
||||||
|
} from '../types';
|
||||||
|
import type { DBField } from '@/lib/domain/db-field';
|
||||||
|
import type { DBIndex } from '@/lib/domain/db-index';
|
||||||
|
|
||||||
|
export function getDiffMapKey({
|
||||||
|
diffObject,
|
||||||
|
objectId,
|
||||||
|
attribute,
|
||||||
|
}: {
|
||||||
|
diffObject: DiffObject;
|
||||||
|
objectId: string;
|
||||||
|
attribute?: string;
|
||||||
|
}): string {
|
||||||
|
return attribute
|
||||||
|
? `${diffObject}-${attribute}-${objectId}`
|
||||||
|
: `${diffObject}-${objectId}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function generateDiff({
|
||||||
|
diagram,
|
||||||
|
newDiagram,
|
||||||
|
}: {
|
||||||
|
diagram: Diagram;
|
||||||
|
newDiagram: Diagram;
|
||||||
|
}): {
|
||||||
|
diffMap: DiffMap;
|
||||||
|
changedTables: Map<string, boolean>;
|
||||||
|
changedFields: Map<string, boolean>;
|
||||||
|
} {
|
||||||
|
const newDiffs = new Map<string, ChartDBDiff>();
|
||||||
|
const changedTables = new Map<string, boolean>();
|
||||||
|
const changedFields = new Map<string, boolean>();
|
||||||
|
|
||||||
|
// Compare tables
|
||||||
|
compareTables({ diagram, newDiagram, diffMap: newDiffs, changedTables });
|
||||||
|
|
||||||
|
// Compare fields and indexes for matching tables
|
||||||
|
compareTableContents({
|
||||||
|
diagram,
|
||||||
|
newDiagram,
|
||||||
|
diffMap: newDiffs,
|
||||||
|
changedTables,
|
||||||
|
changedFields,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Compare relationships
|
||||||
|
compareRelationships({ diagram, newDiagram, diffMap: newDiffs });
|
||||||
|
|
||||||
|
return { diffMap: newDiffs, changedTables, changedFields };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compare tables between diagrams
|
||||||
|
function compareTables({
|
||||||
|
diagram,
|
||||||
|
newDiagram,
|
||||||
|
diffMap,
|
||||||
|
changedTables,
|
||||||
|
}: {
|
||||||
|
diagram: Diagram;
|
||||||
|
newDiagram: Diagram;
|
||||||
|
diffMap: DiffMap;
|
||||||
|
changedTables: Map<string, boolean>;
|
||||||
|
}) {
|
||||||
|
const oldTables = diagram.tables || [];
|
||||||
|
const newTables = newDiagram.tables || [];
|
||||||
|
|
||||||
|
// Check for added tables
|
||||||
|
for (const newTable of newTables) {
|
||||||
|
if (!oldTables.find((t) => t.id === newTable.id)) {
|
||||||
|
diffMap.set(
|
||||||
|
getDiffMapKey({ diffObject: 'table', objectId: newTable.id }),
|
||||||
|
{
|
||||||
|
object: 'table',
|
||||||
|
type: 'added',
|
||||||
|
tableId: newTable.id,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
changedTables.set(newTable.id, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for removed tables
|
||||||
|
for (const oldTable of oldTables) {
|
||||||
|
if (!newTables.find((t) => t.id === oldTable.id)) {
|
||||||
|
diffMap.set(
|
||||||
|
getDiffMapKey({ diffObject: 'table', objectId: oldTable.id }),
|
||||||
|
{
|
||||||
|
object: 'table',
|
||||||
|
type: 'removed',
|
||||||
|
tableId: oldTable.id,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
changedTables.set(oldTable.id, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for table name and comments changes
|
||||||
|
for (const oldTable of oldTables) {
|
||||||
|
const newTable = newTables.find((t) => t.id === oldTable.id);
|
||||||
|
|
||||||
|
if (!newTable) continue;
|
||||||
|
|
||||||
|
if (oldTable.name !== newTable.name) {
|
||||||
|
diffMap.set(
|
||||||
|
getDiffMapKey({
|
||||||
|
diffObject: 'table',
|
||||||
|
objectId: oldTable.id,
|
||||||
|
attribute: 'name',
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
object: 'table',
|
||||||
|
type: 'changed',
|
||||||
|
tableId: oldTable.id,
|
||||||
|
attributes: 'name',
|
||||||
|
newValue: newTable.name,
|
||||||
|
oldValue: oldTable.name,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
changedTables.set(oldTable.id, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (oldTable.comments !== newTable.comments) {
|
||||||
|
diffMap.set(
|
||||||
|
getDiffMapKey({
|
||||||
|
diffObject: 'table',
|
||||||
|
objectId: oldTable.id,
|
||||||
|
attribute: 'comments',
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
object: 'table',
|
||||||
|
type: 'changed',
|
||||||
|
tableId: oldTable.id,
|
||||||
|
attributes: 'comments',
|
||||||
|
newValue: newTable.comments,
|
||||||
|
oldValue: oldTable.comments,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
changedTables.set(oldTable.id, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compare fields and indexes for matching tables
|
||||||
|
function compareTableContents({
|
||||||
|
diagram,
|
||||||
|
newDiagram,
|
||||||
|
diffMap,
|
||||||
|
changedTables,
|
||||||
|
changedFields,
|
||||||
|
}: {
|
||||||
|
diagram: Diagram;
|
||||||
|
newDiagram: Diagram;
|
||||||
|
diffMap: DiffMap;
|
||||||
|
changedTables: Map<string, boolean>;
|
||||||
|
changedFields: Map<string, boolean>;
|
||||||
|
}) {
|
||||||
|
const oldTables = diagram.tables || [];
|
||||||
|
const newTables = newDiagram.tables || [];
|
||||||
|
|
||||||
|
// For each table that exists in both diagrams
|
||||||
|
for (const oldTable of oldTables) {
|
||||||
|
const newTable = newTables.find((t) => t.id === oldTable.id);
|
||||||
|
if (!newTable) continue;
|
||||||
|
|
||||||
|
// Compare fields
|
||||||
|
compareFields({
|
||||||
|
tableId: oldTable.id,
|
||||||
|
oldFields: oldTable.fields,
|
||||||
|
newFields: newTable.fields,
|
||||||
|
diffMap,
|
||||||
|
changedTables,
|
||||||
|
changedFields,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Compare indexes
|
||||||
|
compareIndexes({
|
||||||
|
tableId: oldTable.id,
|
||||||
|
oldIndexes: oldTable.indexes,
|
||||||
|
newIndexes: newTable.indexes,
|
||||||
|
diffMap,
|
||||||
|
changedTables,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compare fields between tables
|
||||||
|
function compareFields({
|
||||||
|
tableId,
|
||||||
|
oldFields,
|
||||||
|
newFields,
|
||||||
|
diffMap,
|
||||||
|
changedTables,
|
||||||
|
changedFields,
|
||||||
|
}: {
|
||||||
|
tableId: string;
|
||||||
|
oldFields: DBField[];
|
||||||
|
newFields: DBField[];
|
||||||
|
diffMap: DiffMap;
|
||||||
|
changedTables: Map<string, boolean>;
|
||||||
|
changedFields: Map<string, boolean>;
|
||||||
|
}) {
|
||||||
|
// Check for added fields
|
||||||
|
for (const newField of newFields) {
|
||||||
|
if (!oldFields.find((f) => f.id === newField.id)) {
|
||||||
|
diffMap.set(
|
||||||
|
getDiffMapKey({
|
||||||
|
diffObject: 'field',
|
||||||
|
objectId: newField.id,
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
object: 'field',
|
||||||
|
type: 'added',
|
||||||
|
fieldId: newField.id,
|
||||||
|
tableId,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
changedTables.set(tableId, true);
|
||||||
|
changedFields.set(newField.id, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for removed fields
|
||||||
|
for (const oldField of oldFields) {
|
||||||
|
if (!newFields.find((f) => f.id === oldField.id)) {
|
||||||
|
diffMap.set(
|
||||||
|
getDiffMapKey({
|
||||||
|
diffObject: 'field',
|
||||||
|
objectId: oldField.id,
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
object: 'field',
|
||||||
|
type: 'removed',
|
||||||
|
fieldId: oldField.id,
|
||||||
|
tableId,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
changedTables.set(tableId, true);
|
||||||
|
changedFields.set(oldField.id, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for field changes
|
||||||
|
for (const oldField of oldFields) {
|
||||||
|
const newField = newFields.find((f) => f.id === oldField.id);
|
||||||
|
if (!newField) continue;
|
||||||
|
|
||||||
|
// Compare basic field properties
|
||||||
|
compareFieldProperties({
|
||||||
|
tableId,
|
||||||
|
oldField,
|
||||||
|
newField,
|
||||||
|
diffMap,
|
||||||
|
changedTables,
|
||||||
|
changedFields,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compare field properties
|
||||||
|
function compareFieldProperties({
|
||||||
|
tableId,
|
||||||
|
oldField,
|
||||||
|
newField,
|
||||||
|
diffMap,
|
||||||
|
changedTables,
|
||||||
|
changedFields,
|
||||||
|
}: {
|
||||||
|
tableId: string;
|
||||||
|
oldField: DBField;
|
||||||
|
newField: DBField;
|
||||||
|
diffMap: DiffMap;
|
||||||
|
changedTables: Map<string, boolean>;
|
||||||
|
changedFields: Map<string, boolean>;
|
||||||
|
}) {
|
||||||
|
const changedAttributes: FieldDiffAttribute[] = [];
|
||||||
|
|
||||||
|
if (oldField.name !== newField.name) {
|
||||||
|
changedAttributes.push('name');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (oldField.type.id !== newField.type.id) {
|
||||||
|
changedAttributes.push('type');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (oldField.primaryKey !== newField.primaryKey) {
|
||||||
|
changedAttributes.push('primaryKey');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (oldField.unique !== newField.unique) {
|
||||||
|
changedAttributes.push('unique');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (oldField.nullable !== newField.nullable) {
|
||||||
|
changedAttributes.push('nullable');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (oldField.comments !== newField.comments) {
|
||||||
|
changedAttributes.push('comments');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changedAttributes.length > 0) {
|
||||||
|
for (const attribute of changedAttributes) {
|
||||||
|
diffMap.set(
|
||||||
|
getDiffMapKey({
|
||||||
|
diffObject: 'field',
|
||||||
|
objectId: oldField.id,
|
||||||
|
attribute: attribute,
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
object: 'field',
|
||||||
|
type: 'changed',
|
||||||
|
fieldId: oldField.id,
|
||||||
|
tableId,
|
||||||
|
attributes: attribute,
|
||||||
|
oldValue: oldField[attribute],
|
||||||
|
newValue: newField[attribute],
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
changedTables.set(tableId, true);
|
||||||
|
changedFields.set(oldField.id, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compare indexes between tables
|
||||||
|
function compareIndexes({
|
||||||
|
tableId,
|
||||||
|
oldIndexes,
|
||||||
|
newIndexes,
|
||||||
|
diffMap,
|
||||||
|
changedTables,
|
||||||
|
}: {
|
||||||
|
tableId: string;
|
||||||
|
oldIndexes: DBIndex[];
|
||||||
|
newIndexes: DBIndex[];
|
||||||
|
diffMap: DiffMap;
|
||||||
|
changedTables: Map<string, boolean>;
|
||||||
|
}) {
|
||||||
|
// Check for added indexes
|
||||||
|
for (const newIndex of newIndexes) {
|
||||||
|
if (!oldIndexes.find((i) => i.id === newIndex.id)) {
|
||||||
|
diffMap.set(
|
||||||
|
getDiffMapKey({
|
||||||
|
diffObject: 'index',
|
||||||
|
objectId: newIndex.id,
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
object: 'index',
|
||||||
|
type: 'added',
|
||||||
|
indexId: newIndex.id,
|
||||||
|
tableId,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
changedTables.set(tableId, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for removed indexes
|
||||||
|
for (const oldIndex of oldIndexes) {
|
||||||
|
if (!newIndexes.find((i) => i.id === oldIndex.id)) {
|
||||||
|
diffMap.set(
|
||||||
|
getDiffMapKey({
|
||||||
|
diffObject: 'index',
|
||||||
|
objectId: oldIndex.id,
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
object: 'index',
|
||||||
|
type: 'removed',
|
||||||
|
indexId: oldIndex.id,
|
||||||
|
tableId,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
changedTables.set(tableId, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compare relationships between diagrams
|
||||||
|
function compareRelationships({
|
||||||
|
diagram,
|
||||||
|
newDiagram,
|
||||||
|
diffMap,
|
||||||
|
}: {
|
||||||
|
diagram: Diagram;
|
||||||
|
newDiagram: Diagram;
|
||||||
|
diffMap: DiffMap;
|
||||||
|
}) {
|
||||||
|
const oldRelationships = diagram.relationships || [];
|
||||||
|
const newRelationships = newDiagram.relationships || [];
|
||||||
|
|
||||||
|
// Check for added relationships
|
||||||
|
for (const newRelationship of newRelationships) {
|
||||||
|
if (!oldRelationships.find((r) => r.id === newRelationship.id)) {
|
||||||
|
diffMap.set(
|
||||||
|
getDiffMapKey({
|
||||||
|
diffObject: 'relationship',
|
||||||
|
objectId: newRelationship.id,
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
object: 'relationship',
|
||||||
|
type: 'added',
|
||||||
|
relationshipId: newRelationship.id,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for removed relationships
|
||||||
|
for (const oldRelationship of oldRelationships) {
|
||||||
|
if (!newRelationships.find((r) => r.id === oldRelationship.id)) {
|
||||||
|
diffMap.set(
|
||||||
|
getDiffMapKey({
|
||||||
|
diffObject: 'relationship',
|
||||||
|
objectId: oldRelationship.id,
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
object: 'relationship',
|
||||||
|
type: 'removed',
|
||||||
|
relationshipId: oldRelationship.id,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
75
src/context/diff-context/diff-context.tsx
Normal file
75
src/context/diff-context/diff-context.tsx
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
import { createContext } from 'react';
|
||||||
|
import type { DiffMap } from './types';
|
||||||
|
import type { Diagram } from '@/lib/domain/diagram';
|
||||||
|
import type { DBTable } from '@/lib/domain/db-table';
|
||||||
|
import type { EventEmitter } from 'ahooks/lib/useEventEmitter';
|
||||||
|
import type { DBField } from '@/lib/domain/db-field';
|
||||||
|
import type { DataType } from '@/lib/data/data-types/data-types';
|
||||||
|
import type { DBRelationship } from '@/lib/domain/db-relationship';
|
||||||
|
|
||||||
|
export type DiffEventType = 'diff_calculated';
|
||||||
|
|
||||||
|
export type DiffEventBase<T extends DiffEventType, D> = {
|
||||||
|
action: T;
|
||||||
|
data: D;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DiffCalculatedEvent = DiffEventBase<
|
||||||
|
'diff_calculated',
|
||||||
|
{
|
||||||
|
tablesAdded: DBTable[];
|
||||||
|
fieldsAdded: Map<string, DBField[]>;
|
||||||
|
relationshipsAdded: DBRelationship[];
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
|
||||||
|
export type DiffEvent = DiffCalculatedEvent;
|
||||||
|
|
||||||
|
export interface DiffContext {
|
||||||
|
newDiagram: Diagram | null;
|
||||||
|
diffMap: DiffMap;
|
||||||
|
hasDiff: boolean;
|
||||||
|
|
||||||
|
calculateDiff: ({
|
||||||
|
diagram,
|
||||||
|
newDiagram,
|
||||||
|
}: {
|
||||||
|
diagram: Diagram;
|
||||||
|
newDiagram: Diagram;
|
||||||
|
}) => void;
|
||||||
|
|
||||||
|
// table diff
|
||||||
|
checkIfTableHasChange: ({ tableId }: { tableId: string }) => boolean;
|
||||||
|
checkIfNewTable: ({ tableId }: { tableId: string }) => boolean;
|
||||||
|
checkIfTableRemoved: ({ tableId }: { tableId: string }) => boolean;
|
||||||
|
getTableNewName: ({ tableId }: { tableId: string }) => string | null;
|
||||||
|
|
||||||
|
// field diff
|
||||||
|
checkIfFieldHasChange: ({
|
||||||
|
tableId,
|
||||||
|
fieldId,
|
||||||
|
}: {
|
||||||
|
tableId: string;
|
||||||
|
fieldId: string;
|
||||||
|
}) => boolean;
|
||||||
|
checkIfFieldRemoved: ({ fieldId }: { fieldId: string }) => boolean;
|
||||||
|
checkIfNewField: ({ fieldId }: { fieldId: string }) => boolean;
|
||||||
|
getFieldNewName: ({ fieldId }: { fieldId: string }) => string | null;
|
||||||
|
getFieldNewType: ({ fieldId }: { fieldId: string }) => DataType | null;
|
||||||
|
|
||||||
|
// relationship diff
|
||||||
|
checkIfNewRelationship: ({
|
||||||
|
relationshipId,
|
||||||
|
}: {
|
||||||
|
relationshipId: string;
|
||||||
|
}) => boolean;
|
||||||
|
checkIfRelationshipRemoved: ({
|
||||||
|
relationshipId,
|
||||||
|
}: {
|
||||||
|
relationshipId: string;
|
||||||
|
}) => boolean;
|
||||||
|
|
||||||
|
events: EventEmitter<DiffEvent>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const diffContext = createContext<DiffContext | undefined>(undefined);
|
||||||
327
src/context/diff-context/diff-provider.tsx
Normal file
327
src/context/diff-context/diff-provider.tsx
Normal file
@@ -0,0 +1,327 @@
|
|||||||
|
import React, { useCallback } from 'react';
|
||||||
|
import type { DiffContext, DiffEvent } from './diff-context';
|
||||||
|
import { diffContext } from './diff-context';
|
||||||
|
import type { ChartDBDiff, DiffMap } from './types';
|
||||||
|
import { generateDiff, getDiffMapKey } from './diff-check/diff-check';
|
||||||
|
import type { Diagram } from '@/lib/domain/diagram';
|
||||||
|
import { useEventEmitter } from 'ahooks';
|
||||||
|
import type { DBField } from '@/lib/domain/db-field';
|
||||||
|
import type { DataType } from '@/lib/data/data-types/data-types';
|
||||||
|
import type { DBRelationship } from '@/lib/domain/db-relationship';
|
||||||
|
|
||||||
|
export const DiffProvider: React.FC<React.PropsWithChildren> = ({
|
||||||
|
children,
|
||||||
|
}) => {
|
||||||
|
const [newDiagram, setNewDiagram] = React.useState<Diagram | null>(null);
|
||||||
|
const [diffMap, setDiffMap] = React.useState<DiffMap>(
|
||||||
|
new Map<string, ChartDBDiff>()
|
||||||
|
);
|
||||||
|
const [tablesChanged, setTablesChanged] = React.useState<
|
||||||
|
Map<string, boolean>
|
||||||
|
>(new Map<string, boolean>());
|
||||||
|
const [fieldsChanged, setFieldsChanged] = React.useState<
|
||||||
|
Map<string, boolean>
|
||||||
|
>(new Map<string, boolean>());
|
||||||
|
|
||||||
|
const events = useEventEmitter<DiffEvent>();
|
||||||
|
|
||||||
|
const generateNewFieldsMap = useCallback(
|
||||||
|
({
|
||||||
|
diffMap,
|
||||||
|
newDiagram,
|
||||||
|
}: {
|
||||||
|
diffMap: DiffMap;
|
||||||
|
newDiagram: Diagram;
|
||||||
|
}) => {
|
||||||
|
const newFieldsMap = new Map<string, DBField[]>();
|
||||||
|
|
||||||
|
diffMap.forEach((diff) => {
|
||||||
|
if (diff.object === 'field' && diff.type === 'added') {
|
||||||
|
const field = newDiagram?.tables
|
||||||
|
?.find((table) => table.id === diff.tableId)
|
||||||
|
?.fields.find((f) => f.id === diff.fieldId);
|
||||||
|
|
||||||
|
if (field) {
|
||||||
|
newFieldsMap.set(diff.tableId, [
|
||||||
|
...(newFieldsMap.get(diff.tableId) ?? []),
|
||||||
|
field,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return newFieldsMap;
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const findNewRelationships = useCallback(
|
||||||
|
({
|
||||||
|
diffMap,
|
||||||
|
newDiagram,
|
||||||
|
}: {
|
||||||
|
diffMap: DiffMap;
|
||||||
|
newDiagram: Diagram;
|
||||||
|
}) => {
|
||||||
|
const relationships: DBRelationship[] = [];
|
||||||
|
diffMap.forEach((diff) => {
|
||||||
|
if (diff.object === 'relationship' && diff.type === 'added') {
|
||||||
|
const relationship = newDiagram?.relationships?.find(
|
||||||
|
(rel) => rel.id === diff.relationshipId
|
||||||
|
);
|
||||||
|
|
||||||
|
if (relationship) {
|
||||||
|
relationships.push(relationship);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return relationships;
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const calculateDiff: DiffContext['calculateDiff'] = useCallback(
|
||||||
|
({ diagram, newDiagram: newDiagramArg }) => {
|
||||||
|
const {
|
||||||
|
diffMap: newDiffs,
|
||||||
|
changedTables: newChangedTables,
|
||||||
|
changedFields: newChangedFields,
|
||||||
|
} = generateDiff({ diagram, newDiagram: newDiagramArg });
|
||||||
|
|
||||||
|
setDiffMap(newDiffs);
|
||||||
|
setTablesChanged(newChangedTables);
|
||||||
|
setFieldsChanged(newChangedFields);
|
||||||
|
setNewDiagram(newDiagramArg);
|
||||||
|
|
||||||
|
events.emit({
|
||||||
|
action: 'diff_calculated',
|
||||||
|
data: {
|
||||||
|
tablesAdded:
|
||||||
|
newDiagramArg?.tables?.filter((table) => {
|
||||||
|
const tableKey = getDiffMapKey({
|
||||||
|
diffObject: 'table',
|
||||||
|
objectId: table.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
newDiffs.has(tableKey) &&
|
||||||
|
newDiffs.get(tableKey)?.type === 'added'
|
||||||
|
);
|
||||||
|
}) ?? [],
|
||||||
|
|
||||||
|
fieldsAdded: generateNewFieldsMap({
|
||||||
|
diffMap: newDiffs,
|
||||||
|
newDiagram: newDiagramArg,
|
||||||
|
}),
|
||||||
|
relationshipsAdded: findNewRelationships({
|
||||||
|
diffMap: newDiffs,
|
||||||
|
newDiagram: newDiagramArg,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[setDiffMap, events, generateNewFieldsMap, findNewRelationships]
|
||||||
|
);
|
||||||
|
|
||||||
|
const getTableNewName = useCallback<DiffContext['getTableNewName']>(
|
||||||
|
({ tableId }) => {
|
||||||
|
const tableNameKey = getDiffMapKey({
|
||||||
|
diffObject: 'table',
|
||||||
|
objectId: tableId,
|
||||||
|
attribute: 'name',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (diffMap.has(tableNameKey)) {
|
||||||
|
const diff = diffMap.get(tableNameKey);
|
||||||
|
|
||||||
|
if (diff?.type === 'changed') {
|
||||||
|
return diff.newValue as string;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
[diffMap]
|
||||||
|
);
|
||||||
|
|
||||||
|
const checkIfTableHasChange = useCallback<
|
||||||
|
DiffContext['checkIfTableHasChange']
|
||||||
|
>(({ tableId }) => tablesChanged.get(tableId) ?? false, [tablesChanged]);
|
||||||
|
|
||||||
|
const checkIfNewTable = useCallback<DiffContext['checkIfNewTable']>(
|
||||||
|
({ tableId }) => {
|
||||||
|
const tableKey = getDiffMapKey({
|
||||||
|
diffObject: 'table',
|
||||||
|
objectId: tableId,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
diffMap.has(tableKey) && diffMap.get(tableKey)?.type === 'added'
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[diffMap]
|
||||||
|
);
|
||||||
|
|
||||||
|
const checkIfTableRemoved = useCallback<DiffContext['checkIfTableRemoved']>(
|
||||||
|
({ tableId }) => {
|
||||||
|
const tableKey = getDiffMapKey({
|
||||||
|
diffObject: 'table',
|
||||||
|
objectId: tableId,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
diffMap.has(tableKey) &&
|
||||||
|
diffMap.get(tableKey)?.type === 'removed'
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[diffMap]
|
||||||
|
);
|
||||||
|
|
||||||
|
const checkIfFieldHasChange = useCallback<
|
||||||
|
DiffContext['checkIfFieldHasChange']
|
||||||
|
>(
|
||||||
|
({ fieldId }) => {
|
||||||
|
return fieldsChanged.get(fieldId) ?? false;
|
||||||
|
},
|
||||||
|
[fieldsChanged]
|
||||||
|
);
|
||||||
|
|
||||||
|
const checkIfFieldRemoved = useCallback<DiffContext['checkIfFieldRemoved']>(
|
||||||
|
({ fieldId }) => {
|
||||||
|
const fieldKey = getDiffMapKey({
|
||||||
|
diffObject: 'field',
|
||||||
|
objectId: fieldId,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
diffMap.has(fieldKey) &&
|
||||||
|
diffMap.get(fieldKey)?.type === 'removed'
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[diffMap]
|
||||||
|
);
|
||||||
|
|
||||||
|
const checkIfNewField = useCallback<DiffContext['checkIfNewField']>(
|
||||||
|
({ fieldId }) => {
|
||||||
|
const fieldKey = getDiffMapKey({
|
||||||
|
diffObject: 'field',
|
||||||
|
objectId: fieldId,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
diffMap.has(fieldKey) && diffMap.get(fieldKey)?.type === 'added'
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[diffMap]
|
||||||
|
);
|
||||||
|
|
||||||
|
const getFieldNewName = useCallback<DiffContext['getFieldNewName']>(
|
||||||
|
({ fieldId }) => {
|
||||||
|
const fieldKey = getDiffMapKey({
|
||||||
|
diffObject: 'field',
|
||||||
|
objectId: fieldId,
|
||||||
|
attribute: 'name',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (diffMap.has(fieldKey)) {
|
||||||
|
const diff = diffMap.get(fieldKey);
|
||||||
|
|
||||||
|
if (diff?.type === 'changed') {
|
||||||
|
return diff.newValue as string;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
[diffMap]
|
||||||
|
);
|
||||||
|
|
||||||
|
const getFieldNewType = useCallback<DiffContext['getFieldNewType']>(
|
||||||
|
({ fieldId }) => {
|
||||||
|
const fieldKey = getDiffMapKey({
|
||||||
|
diffObject: 'field',
|
||||||
|
objectId: fieldId,
|
||||||
|
attribute: 'type',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (diffMap.has(fieldKey)) {
|
||||||
|
const diff = diffMap.get(fieldKey);
|
||||||
|
|
||||||
|
if (diff?.type === 'changed') {
|
||||||
|
return diff.newValue as DataType;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
[diffMap]
|
||||||
|
);
|
||||||
|
|
||||||
|
const checkIfNewRelationship = useCallback<
|
||||||
|
DiffContext['checkIfNewRelationship']
|
||||||
|
>(
|
||||||
|
({ relationshipId }) => {
|
||||||
|
const relationshipKey = getDiffMapKey({
|
||||||
|
diffObject: 'relationship',
|
||||||
|
objectId: relationshipId,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
diffMap.has(relationshipKey) &&
|
||||||
|
diffMap.get(relationshipKey)?.type === 'added'
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[diffMap]
|
||||||
|
);
|
||||||
|
|
||||||
|
const checkIfRelationshipRemoved = useCallback<
|
||||||
|
DiffContext['checkIfRelationshipRemoved']
|
||||||
|
>(
|
||||||
|
({ relationshipId }) => {
|
||||||
|
const relationshipKey = getDiffMapKey({
|
||||||
|
diffObject: 'relationship',
|
||||||
|
objectId: relationshipId,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
diffMap.has(relationshipKey) &&
|
||||||
|
diffMap.get(relationshipKey)?.type === 'removed'
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[diffMap]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<diffContext.Provider
|
||||||
|
value={{
|
||||||
|
newDiagram,
|
||||||
|
diffMap,
|
||||||
|
hasDiff: diffMap.size > 0,
|
||||||
|
|
||||||
|
calculateDiff,
|
||||||
|
|
||||||
|
// table diff
|
||||||
|
getTableNewName,
|
||||||
|
checkIfNewTable,
|
||||||
|
checkIfTableRemoved,
|
||||||
|
checkIfTableHasChange,
|
||||||
|
|
||||||
|
// field diff
|
||||||
|
checkIfFieldHasChange,
|
||||||
|
checkIfFieldRemoved,
|
||||||
|
checkIfNewField,
|
||||||
|
getFieldNewName,
|
||||||
|
getFieldNewType,
|
||||||
|
|
||||||
|
// relationship diff
|
||||||
|
checkIfNewRelationship,
|
||||||
|
checkIfRelationshipRemoved,
|
||||||
|
|
||||||
|
events,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</diffContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
53
src/context/diff-context/types.ts
Normal file
53
src/context/diff-context/types.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import type { DataType } from '@/lib/data/data-types/data-types';
|
||||||
|
|
||||||
|
export type TableDiffAttribute = 'name' | 'comments';
|
||||||
|
|
||||||
|
export interface TableDiff {
|
||||||
|
object: 'table';
|
||||||
|
type: 'added' | 'removed' | 'changed';
|
||||||
|
tableId: string;
|
||||||
|
attributes?: TableDiffAttribute;
|
||||||
|
oldValue?: string;
|
||||||
|
newValue?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RelationshipDiff {
|
||||||
|
object: 'relationship';
|
||||||
|
type: 'added' | 'removed';
|
||||||
|
relationshipId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type FieldDiffAttribute =
|
||||||
|
| 'name'
|
||||||
|
| 'type'
|
||||||
|
| 'primaryKey'
|
||||||
|
| 'unique'
|
||||||
|
| 'nullable'
|
||||||
|
| 'comments';
|
||||||
|
|
||||||
|
export interface FieldDiff {
|
||||||
|
object: 'field';
|
||||||
|
type: 'added' | 'removed' | 'changed';
|
||||||
|
fieldId: string;
|
||||||
|
tableId: string;
|
||||||
|
attributes?: FieldDiffAttribute;
|
||||||
|
oldValue?: string | boolean | DataType;
|
||||||
|
newValue?: string | boolean | DataType;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IndexDiff {
|
||||||
|
object: 'index';
|
||||||
|
type: 'added' | 'removed';
|
||||||
|
indexId: string;
|
||||||
|
tableId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ChartDBDiff = TableDiff | FieldDiff | IndexDiff | RelationshipDiff;
|
||||||
|
|
||||||
|
export type DiffMap = Map<string, ChartDBDiff>;
|
||||||
|
|
||||||
|
export type DiffObject =
|
||||||
|
| TableDiff['object']
|
||||||
|
| FieldDiff['object']
|
||||||
|
| IndexDiff['object']
|
||||||
|
| RelationshipDiff['object'];
|
||||||
10
src/context/diff-context/use-diff.ts
Normal file
10
src/context/diff-context/use-diff.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { useContext } from 'react';
|
||||||
|
import { diffContext } from './diff-context';
|
||||||
|
|
||||||
|
export const useDiff = () => {
|
||||||
|
const context = useContext(diffContext);
|
||||||
|
if (context === undefined) {
|
||||||
|
throw new Error('useDiff must be used within an DiffProvider');
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
};
|
||||||
@@ -7,6 +7,7 @@ export enum KeyboardShortcutAction {
|
|||||||
SAVE_DIAGRAM = 'save_diagram',
|
SAVE_DIAGRAM = 'save_diagram',
|
||||||
TOGGLE_SIDE_PANEL = 'toggle_side_panel',
|
TOGGLE_SIDE_PANEL = 'toggle_side_panel',
|
||||||
SHOW_ALL = 'show_all',
|
SHOW_ALL = 'show_all',
|
||||||
|
TOGGLE_THEME = 'toggle_theme',
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface KeyboardShortcut {
|
export interface KeyboardShortcut {
|
||||||
@@ -63,6 +64,13 @@ export const keyboardShortcuts: Record<
|
|||||||
keyCombinationMac: 'meta+0',
|
keyCombinationMac: 'meta+0',
|
||||||
keyCombinationWin: 'ctrl+0',
|
keyCombinationWin: 'ctrl+0',
|
||||||
},
|
},
|
||||||
|
[KeyboardShortcutAction.TOGGLE_THEME]: {
|
||||||
|
action: KeyboardShortcutAction.TOGGLE_THEME,
|
||||||
|
keyCombinationLabelMac: '⌘M',
|
||||||
|
keyCombinationLabelWin: 'Ctrl+M',
|
||||||
|
keyCombinationMac: 'meta+m',
|
||||||
|
keyCombinationWin: 'ctrl+m',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface KeyboardShortcutForOS {
|
export interface KeyboardShortcutForOS {
|
||||||
|
|||||||
@@ -30,12 +30,6 @@ export interface LocalConfigContext {
|
|||||||
starUsDialogLastOpen: number;
|
starUsDialogLastOpen: number;
|
||||||
setStarUsDialogLastOpen: (lastOpen: number) => void;
|
setStarUsDialogLastOpen: (lastOpen: number) => void;
|
||||||
|
|
||||||
buckleWaitlistOpened: boolean;
|
|
||||||
setBuckleWaitlistOpened: (githubRepoOpened: boolean) => void;
|
|
||||||
|
|
||||||
buckleDialogLastOpen: number;
|
|
||||||
setBuckleDialogLastOpen: (lastOpen: number) => void;
|
|
||||||
|
|
||||||
showDependenciesOnCanvas: boolean;
|
showDependenciesOnCanvas: boolean;
|
||||||
setShowDependenciesOnCanvas: (showDependenciesOnCanvas: boolean) => void;
|
setShowDependenciesOnCanvas: (showDependenciesOnCanvas: boolean) => void;
|
||||||
|
|
||||||
@@ -53,7 +47,7 @@ export const LocalConfigContext = createContext<LocalConfigContext>({
|
|||||||
schemasFilter: {},
|
schemasFilter: {},
|
||||||
setSchemasFilter: emptyFn,
|
setSchemasFilter: emptyFn,
|
||||||
|
|
||||||
showCardinality: false,
|
showCardinality: true,
|
||||||
setShowCardinality: emptyFn,
|
setShowCardinality: emptyFn,
|
||||||
|
|
||||||
hideMultiSchemaNotification: false,
|
hideMultiSchemaNotification: false,
|
||||||
@@ -65,12 +59,6 @@ export const LocalConfigContext = createContext<LocalConfigContext>({
|
|||||||
starUsDialogLastOpen: 0,
|
starUsDialogLastOpen: 0,
|
||||||
setStarUsDialogLastOpen: emptyFn,
|
setStarUsDialogLastOpen: emptyFn,
|
||||||
|
|
||||||
buckleWaitlistOpened: false,
|
|
||||||
setBuckleWaitlistOpened: emptyFn,
|
|
||||||
|
|
||||||
buckleDialogLastOpen: 0,
|
|
||||||
setBuckleDialogLastOpen: emptyFn,
|
|
||||||
|
|
||||||
showDependenciesOnCanvas: false,
|
showDependenciesOnCanvas: false,
|
||||||
setShowDependenciesOnCanvas: emptyFn,
|
setShowDependenciesOnCanvas: emptyFn,
|
||||||
|
|
||||||
|
|||||||
@@ -10,8 +10,6 @@ const showCardinalityKey = 'show_cardinality';
|
|||||||
const hideMultiSchemaNotificationKey = 'hide_multi_schema_notification';
|
const hideMultiSchemaNotificationKey = 'hide_multi_schema_notification';
|
||||||
const githubRepoOpenedKey = 'github_repo_opened';
|
const githubRepoOpenedKey = 'github_repo_opened';
|
||||||
const starUsDialogLastOpenKey = 'star_us_dialog_last_open';
|
const starUsDialogLastOpenKey = 'star_us_dialog_last_open';
|
||||||
const buckleWaitlistOpenedKey = 'buckle_waitlist_opened';
|
|
||||||
const buckleDialogLastOpenKey = 'buckle_dialog_last_open';
|
|
||||||
const showDependenciesOnCanvasKey = 'show_dependencies_on_canvas';
|
const showDependenciesOnCanvasKey = 'show_dependencies_on_canvas';
|
||||||
const showMiniMapOnCanvasKey = 'show_minimap_on_canvas';
|
const showMiniMapOnCanvasKey = 'show_minimap_on_canvas';
|
||||||
|
|
||||||
@@ -33,7 +31,7 @@ export const LocalConfigProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const [showCardinality, setShowCardinality] = React.useState<boolean>(
|
const [showCardinality, setShowCardinality] = React.useState<boolean>(
|
||||||
(localStorage.getItem(showCardinalityKey) || 'false') === 'true'
|
(localStorage.getItem(showCardinalityKey) || 'true') === 'true'
|
||||||
);
|
);
|
||||||
|
|
||||||
const [hideMultiSchemaNotification, setHideMultiSchemaNotification] =
|
const [hideMultiSchemaNotification, setHideMultiSchemaNotification] =
|
||||||
@@ -51,17 +49,6 @@ export const LocalConfigProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
parseInt(localStorage.getItem(starUsDialogLastOpenKey) || '0')
|
parseInt(localStorage.getItem(starUsDialogLastOpenKey) || '0')
|
||||||
);
|
);
|
||||||
|
|
||||||
const [buckleWaitlistOpened, setBuckleWaitlistOpened] =
|
|
||||||
React.useState<boolean>(
|
|
||||||
(localStorage.getItem(buckleWaitlistOpenedKey) || 'false') ===
|
|
||||||
'true'
|
|
||||||
);
|
|
||||||
|
|
||||||
const [buckleDialogLastOpen, setBuckleDialogLastOpen] =
|
|
||||||
React.useState<number>(
|
|
||||||
parseInt(localStorage.getItem(buckleDialogLastOpenKey) || '0')
|
|
||||||
);
|
|
||||||
|
|
||||||
const [showDependenciesOnCanvas, setShowDependenciesOnCanvas] =
|
const [showDependenciesOnCanvas, setShowDependenciesOnCanvas] =
|
||||||
React.useState<boolean>(
|
React.useState<boolean>(
|
||||||
(localStorage.getItem(showDependenciesOnCanvasKey) || 'false') ===
|
(localStorage.getItem(showDependenciesOnCanvasKey) || 'false') ===
|
||||||
@@ -84,20 +71,6 @@ export const LocalConfigProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
localStorage.setItem(githubRepoOpenedKey, githubRepoOpened.toString());
|
localStorage.setItem(githubRepoOpenedKey, githubRepoOpened.toString());
|
||||||
}, [githubRepoOpened]);
|
}, [githubRepoOpened]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
localStorage.setItem(
|
|
||||||
buckleDialogLastOpenKey,
|
|
||||||
buckleDialogLastOpen.toString()
|
|
||||||
);
|
|
||||||
}, [buckleDialogLastOpen]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
localStorage.setItem(
|
|
||||||
buckleWaitlistOpenedKey,
|
|
||||||
buckleWaitlistOpened.toString()
|
|
||||||
);
|
|
||||||
}, [buckleWaitlistOpened]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
hideMultiSchemaNotificationKey,
|
hideMultiSchemaNotificationKey,
|
||||||
@@ -154,10 +127,6 @@ export const LocalConfigProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
setStarUsDialogLastOpen,
|
setStarUsDialogLastOpen,
|
||||||
showDependenciesOnCanvas,
|
showDependenciesOnCanvas,
|
||||||
setShowDependenciesOnCanvas,
|
setShowDependenciesOnCanvas,
|
||||||
setBuckleDialogLastOpen,
|
|
||||||
buckleDialogLastOpen,
|
|
||||||
buckleWaitlistOpened,
|
|
||||||
setBuckleWaitlistOpened,
|
|
||||||
showMiniMapOnCanvas,
|
showMiniMapOnCanvas,
|
||||||
setShowMiniMapOnCanvas,
|
setShowMiniMapOnCanvas,
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState, useCallback } from 'react';
|
||||||
import type { EffectiveTheme } from './theme-context';
|
import type { EffectiveTheme } from './theme-context';
|
||||||
import { ThemeContext } from './theme-context';
|
import { ThemeContext } from './theme-context';
|
||||||
import { useMediaQuery } from 'react-responsive';
|
import { useMediaQuery } from 'react-responsive';
|
||||||
import { useLocalConfig } from '@/hooks/use-local-config';
|
import { useLocalConfig } from '@/hooks/use-local-config';
|
||||||
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
|
import {
|
||||||
|
KeyboardShortcutAction,
|
||||||
|
keyboardShortcutsForOS,
|
||||||
|
} from '../keyboard-shortcuts-context/keyboard-shortcuts';
|
||||||
|
|
||||||
export const ThemeProvider: React.FC<React.PropsWithChildren> = ({
|
export const ThemeProvider: React.FC<React.PropsWithChildren> = ({
|
||||||
children,
|
children,
|
||||||
@@ -29,6 +34,24 @@ export const ThemeProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
}
|
}
|
||||||
}, [effectiveTheme]);
|
}, [effectiveTheme]);
|
||||||
|
|
||||||
|
const handleThemeToggle = useCallback(() => {
|
||||||
|
if (theme === 'system') {
|
||||||
|
setTheme(effectiveTheme === 'dark' ? 'light' : 'dark');
|
||||||
|
} else {
|
||||||
|
setTheme(theme === 'dark' ? 'light' : 'dark');
|
||||||
|
}
|
||||||
|
}, [theme, effectiveTheme, setTheme]);
|
||||||
|
|
||||||
|
useHotkeys(
|
||||||
|
keyboardShortcutsForOS[KeyboardShortcutAction.TOGGLE_THEME]
|
||||||
|
.keyCombination,
|
||||||
|
handleThemeToggle,
|
||||||
|
{
|
||||||
|
preventDefault: true,
|
||||||
|
},
|
||||||
|
[handleThemeToggle]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ThemeContext.Provider value={{ theme, setTheme, effectiveTheme }}>
|
<ThemeContext.Provider value={{ theme, setTheme, effectiveTheme }}>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -1,80 +0,0 @@
|
|||||||
import React, { useCallback, useEffect } from 'react';
|
|
||||||
import { useDialog } from '@/hooks/use-dialog';
|
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogClose,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from '@/components/dialog/dialog';
|
|
||||||
import { Button } from '@/components/button/button';
|
|
||||||
import type { BaseDialogProps } from '../common/base-dialog-props';
|
|
||||||
import { useLocalConfig } from '@/hooks/use-local-config';
|
|
||||||
import { useTheme } from '@/hooks/use-theme';
|
|
||||||
|
|
||||||
export interface BuckleDialogProps extends BaseDialogProps {}
|
|
||||||
|
|
||||||
export const BuckleDialog: React.FC<BuckleDialogProps> = ({ dialog }) => {
|
|
||||||
const { setBuckleWaitlistOpened } = useLocalConfig();
|
|
||||||
const { effectiveTheme } = useTheme();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!dialog.open) return;
|
|
||||||
}, [dialog.open]);
|
|
||||||
const { closeBuckleDialog } = useDialog();
|
|
||||||
|
|
||||||
const handleConfirm = useCallback(() => {
|
|
||||||
setBuckleWaitlistOpened(true);
|
|
||||||
window.open('https://waitlist.buckle.dev', '_blank');
|
|
||||||
}, [setBuckleWaitlistOpened]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog
|
|
||||||
{...dialog}
|
|
||||||
onOpenChange={(open) => {
|
|
||||||
if (!open) {
|
|
||||||
closeBuckleDialog();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<DialogContent
|
|
||||||
className="flex flex-col"
|
|
||||||
showClose={false}
|
|
||||||
onInteractOutside={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle className="hidden" />
|
|
||||||
<DialogDescription className="hidden" />
|
|
||||||
</DialogHeader>
|
|
||||||
<div className="flex w-full flex-col items-center">
|
|
||||||
<img
|
|
||||||
src={
|
|
||||||
effectiveTheme === 'light'
|
|
||||||
? '/buckle-animated.gif'
|
|
||||||
: '/buckle.png'
|
|
||||||
}
|
|
||||||
className="h-16"
|
|
||||||
/>
|
|
||||||
<div className="mt-6 text-center text-base">
|
|
||||||
We've been working on something big -{' '}
|
|
||||||
<span className="font-semibold">Ready to explore?</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<DialogFooter className="flex gap-1 md:justify-between">
|
|
||||||
<DialogClose asChild>
|
|
||||||
<Button variant="secondary">Not now</Button>
|
|
||||||
</DialogClose>
|
|
||||||
<DialogClose asChild>
|
|
||||||
<Button onClick={handleConfirm}>
|
|
||||||
Try ChartDB v2.0!
|
|
||||||
</Button>
|
|
||||||
</DialogClose>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
47
src/hooks/use-debounce-v2.ts
Normal file
47
src/hooks/use-debounce-v2.ts
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import { useEffect, useRef, useCallback } from 'react';
|
||||||
|
import { debounce as utilsDebounce } from '@/lib/utils';
|
||||||
|
|
||||||
|
interface DebouncedFunction {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
(...args: any[]): void;
|
||||||
|
cancel?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A hook that returns a debounced version of the provided function.
|
||||||
|
* The debounced function will only be called after the specified delay
|
||||||
|
* has passed without the function being called again.
|
||||||
|
*
|
||||||
|
* @param callback The function to debounce
|
||||||
|
* @param delay The delay in milliseconds
|
||||||
|
* @returns A debounced version of the callback
|
||||||
|
*/
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
export function useDebounce<T extends (...args: any[]) => any>(
|
||||||
|
callback: T,
|
||||||
|
delay: number
|
||||||
|
): (...args: Parameters<T>) => void {
|
||||||
|
// Use a ref to store the debounced function
|
||||||
|
const debouncedFnRef = useRef<DebouncedFunction>();
|
||||||
|
|
||||||
|
// Update the debounced function when dependencies change
|
||||||
|
useEffect(() => {
|
||||||
|
// Create the debounced function
|
||||||
|
debouncedFnRef.current = utilsDebounce(callback, delay);
|
||||||
|
|
||||||
|
// Clean up when component unmounts or dependencies change
|
||||||
|
return () => {
|
||||||
|
if (debouncedFnRef.current?.cancel) {
|
||||||
|
debouncedFnRef.current.cancel();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [callback, delay]);
|
||||||
|
|
||||||
|
// Create a stable callback that uses the ref
|
||||||
|
const debouncedCallback = useCallback((...args: Parameters<T>) => {
|
||||||
|
debouncedFnRef.current?.(...args);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return debouncedCallback;
|
||||||
|
}
|
||||||
@@ -151,6 +151,8 @@ export const ar: LanguageTranslation = {
|
|||||||
comments: 'تعليقات',
|
comments: 'تعليقات',
|
||||||
no_comments: 'لا يوجد تعليقات',
|
no_comments: 'لا يوجد تعليقات',
|
||||||
delete_field: 'حذف الحقل',
|
delete_field: 'حذف الحقل',
|
||||||
|
// TODO: Translate
|
||||||
|
character_length: 'Max Length',
|
||||||
},
|
},
|
||||||
index_actions: {
|
index_actions: {
|
||||||
title: 'خصائص الفهرس',
|
title: 'خصائص الفهرس',
|
||||||
|
|||||||
@@ -152,6 +152,8 @@ export const bn: LanguageTranslation = {
|
|||||||
comments: 'মন্তব্য',
|
comments: 'মন্তব্য',
|
||||||
no_comments: 'কোনো মন্তব্য নেই',
|
no_comments: 'কোনো মন্তব্য নেই',
|
||||||
delete_field: 'ফিল্ড মুছুন',
|
delete_field: 'ফিল্ড মুছুন',
|
||||||
|
// TODO: Translate
|
||||||
|
character_length: 'Max Length',
|
||||||
},
|
},
|
||||||
index_actions: {
|
index_actions: {
|
||||||
title: 'ইনডেক্স কর্ম',
|
title: 'ইনডেক্স কর্ম',
|
||||||
|
|||||||
@@ -153,6 +153,8 @@ export const de: LanguageTranslation = {
|
|||||||
comments: 'Kommentare',
|
comments: 'Kommentare',
|
||||||
no_comments: 'Keine Kommentare',
|
no_comments: 'Keine Kommentare',
|
||||||
delete_field: 'Feld löschen',
|
delete_field: 'Feld löschen',
|
||||||
|
// TODO: Translate
|
||||||
|
character_length: 'Max Length',
|
||||||
},
|
},
|
||||||
index_actions: {
|
index_actions: {
|
||||||
title: 'Indexattribute',
|
title: 'Indexattribute',
|
||||||
|
|||||||
@@ -145,6 +145,7 @@ export const en = {
|
|||||||
field_actions: {
|
field_actions: {
|
||||||
title: 'Field Attributes',
|
title: 'Field Attributes',
|
||||||
unique: 'Unique',
|
unique: 'Unique',
|
||||||
|
character_length: 'Max Length',
|
||||||
comments: 'Comments',
|
comments: 'Comments',
|
||||||
no_comments: 'No comments',
|
no_comments: 'No comments',
|
||||||
delete_field: 'Delete Field',
|
delete_field: 'Delete Field',
|
||||||
|
|||||||
@@ -142,6 +142,8 @@ export const es: LanguageTranslation = {
|
|||||||
comments: 'Comentarios',
|
comments: 'Comentarios',
|
||||||
no_comments: 'Sin comentarios',
|
no_comments: 'Sin comentarios',
|
||||||
delete_field: 'Eliminar Campo',
|
delete_field: 'Eliminar Campo',
|
||||||
|
// TODO: Translate
|
||||||
|
character_length: 'Max Length',
|
||||||
},
|
},
|
||||||
index_actions: {
|
index_actions: {
|
||||||
title: 'Atributos del Índice',
|
title: 'Atributos del Índice',
|
||||||
|
|||||||
@@ -140,6 +140,8 @@ export const fr: LanguageTranslation = {
|
|||||||
comments: 'Commentaires',
|
comments: 'Commentaires',
|
||||||
no_comments: 'Pas de commentaires',
|
no_comments: 'Pas de commentaires',
|
||||||
delete_field: 'Supprimer le Champ',
|
delete_field: 'Supprimer le Champ',
|
||||||
|
// TODO: Translate
|
||||||
|
character_length: 'Max Length',
|
||||||
},
|
},
|
||||||
index_actions: {
|
index_actions: {
|
||||||
title: "Attributs de l'Index",
|
title: "Attributs de l'Index",
|
||||||
|
|||||||
@@ -153,6 +153,8 @@ export const gu: LanguageTranslation = {
|
|||||||
comments: 'ટિપ્પણીઓ',
|
comments: 'ટિપ્પણીઓ',
|
||||||
no_comments: 'કોઈ ટિપ્પણીઓ નથી',
|
no_comments: 'કોઈ ટિપ્પણીઓ નથી',
|
||||||
delete_field: 'ફીલ્ડ કાઢી નાખો',
|
delete_field: 'ફીલ્ડ કાઢી નાખો',
|
||||||
|
// TODO: Translate
|
||||||
|
character_length: 'Max Length',
|
||||||
},
|
},
|
||||||
index_actions: {
|
index_actions: {
|
||||||
title: 'ઇન્ડેક્સ લક્ષણો',
|
title: 'ઇન્ડેક્સ લક્ષણો',
|
||||||
|
|||||||
@@ -152,6 +152,8 @@ export const hi: LanguageTranslation = {
|
|||||||
comments: 'टिप्पणियाँ',
|
comments: 'टिप्पणियाँ',
|
||||||
no_comments: 'कोई टिप्पणी नहीं',
|
no_comments: 'कोई टिप्पणी नहीं',
|
||||||
delete_field: 'फ़ील्ड हटाएँ',
|
delete_field: 'फ़ील्ड हटाएँ',
|
||||||
|
// TODO: Translate
|
||||||
|
character_length: 'Max Length',
|
||||||
},
|
},
|
||||||
index_actions: {
|
index_actions: {
|
||||||
title: 'सूचकांक विशेषताएँ',
|
title: 'सूचकांक विशेषताएँ',
|
||||||
|
|||||||
@@ -151,6 +151,8 @@ export const id_ID: LanguageTranslation = {
|
|||||||
comments: 'Komentar',
|
comments: 'Komentar',
|
||||||
no_comments: 'Tidak ada komentar',
|
no_comments: 'Tidak ada komentar',
|
||||||
delete_field: 'Hapus Kolom',
|
delete_field: 'Hapus Kolom',
|
||||||
|
// TODO: Translate
|
||||||
|
character_length: 'Max Length',
|
||||||
},
|
},
|
||||||
index_actions: {
|
index_actions: {
|
||||||
title: 'Atribut Indeks',
|
title: 'Atribut Indeks',
|
||||||
|
|||||||
@@ -155,6 +155,8 @@ export const ja: LanguageTranslation = {
|
|||||||
comments: 'コメント',
|
comments: 'コメント',
|
||||||
no_comments: 'コメントがありません',
|
no_comments: 'コメントがありません',
|
||||||
delete_field: 'フィールドを削除',
|
delete_field: 'フィールドを削除',
|
||||||
|
// TODO: Translate
|
||||||
|
character_length: 'Max Length',
|
||||||
},
|
},
|
||||||
index_actions: {
|
index_actions: {
|
||||||
title: 'インデックス属性',
|
title: 'インデックス属性',
|
||||||
|
|||||||
@@ -151,6 +151,8 @@ export const ko_KR: LanguageTranslation = {
|
|||||||
comments: '주석',
|
comments: '주석',
|
||||||
no_comments: '주석 없음',
|
no_comments: '주석 없음',
|
||||||
delete_field: '필드 삭제',
|
delete_field: '필드 삭제',
|
||||||
|
// TODO: Translate
|
||||||
|
character_length: 'Max Length',
|
||||||
},
|
},
|
||||||
index_actions: {
|
index_actions: {
|
||||||
title: '인덱스 속성',
|
title: '인덱스 속성',
|
||||||
|
|||||||
@@ -154,6 +154,8 @@ export const mr: LanguageTranslation = {
|
|||||||
comments: 'टिप्पण्या',
|
comments: 'टिप्पण्या',
|
||||||
no_comments: 'कोणत्याही टिप्पणी नाहीत',
|
no_comments: 'कोणत्याही टिप्पणी नाहीत',
|
||||||
delete_field: 'फील्ड हटवा',
|
delete_field: 'फील्ड हटवा',
|
||||||
|
// TODO: Translate
|
||||||
|
character_length: 'Max Length',
|
||||||
},
|
},
|
||||||
index_actions: {
|
index_actions: {
|
||||||
title: 'इंडेक्स गुणधर्म',
|
title: 'इंडेक्स गुणधर्म',
|
||||||
|
|||||||
@@ -152,6 +152,8 @@ export const ne: LanguageTranslation = {
|
|||||||
comments: 'टिप्पणीहरू',
|
comments: 'टिप्पणीहरू',
|
||||||
no_comments: 'कुनै टिप्पणीहरू छैनन्',
|
no_comments: 'कुनै टिप्पणीहरू छैनन्',
|
||||||
delete_field: 'क्षेत्र हटाउनुहोस्',
|
delete_field: 'क्षेत्र हटाउनुहोस्',
|
||||||
|
// TODO: Translate
|
||||||
|
character_length: 'Max Length',
|
||||||
},
|
},
|
||||||
index_actions: {
|
index_actions: {
|
||||||
title: 'सूचक विशेषताहरू',
|
title: 'सूचक विशेषताहरू',
|
||||||
|
|||||||
@@ -152,6 +152,8 @@ export const pt_BR: LanguageTranslation = {
|
|||||||
comments: 'Comentários',
|
comments: 'Comentários',
|
||||||
no_comments: 'Sem comentários',
|
no_comments: 'Sem comentários',
|
||||||
delete_field: 'Excluir Campo',
|
delete_field: 'Excluir Campo',
|
||||||
|
// TODO: Translate
|
||||||
|
character_length: 'Max Length',
|
||||||
},
|
},
|
||||||
index_actions: {
|
index_actions: {
|
||||||
title: 'Atributos do Índice',
|
title: 'Atributos do Índice',
|
||||||
|
|||||||
@@ -151,6 +151,8 @@ export const ru: LanguageTranslation = {
|
|||||||
comments: 'Комментарии',
|
comments: 'Комментарии',
|
||||||
no_comments: 'Нет комментария',
|
no_comments: 'Нет комментария',
|
||||||
delete_field: 'Удалить поле',
|
delete_field: 'Удалить поле',
|
||||||
|
// TODO: Translate
|
||||||
|
character_length: 'Max Length',
|
||||||
},
|
},
|
||||||
index_actions: {
|
index_actions: {
|
||||||
title: 'Атрибуты индекса',
|
title: 'Атрибуты индекса',
|
||||||
|
|||||||
@@ -152,6 +152,8 @@ export const te: LanguageTranslation = {
|
|||||||
comments: 'వ్యాఖ్యలు',
|
comments: 'వ్యాఖ్యలు',
|
||||||
no_comments: 'వ్యాఖ్యలు లేవు',
|
no_comments: 'వ్యాఖ్యలు లేవు',
|
||||||
delete_field: 'ఫీల్డ్ తొలగించు',
|
delete_field: 'ఫీల్డ్ తొలగించు',
|
||||||
|
// TODO: Translate
|
||||||
|
character_length: 'Max Length',
|
||||||
},
|
},
|
||||||
index_actions: {
|
index_actions: {
|
||||||
title: 'ఇండెక్స్ గుణాలు',
|
title: 'ఇండెక్స్ గుణాలు',
|
||||||
|
|||||||
@@ -151,6 +151,8 @@ export const tr: LanguageTranslation = {
|
|||||||
comments: 'Yorumlar',
|
comments: 'Yorumlar',
|
||||||
no_comments: 'Yorum yok',
|
no_comments: 'Yorum yok',
|
||||||
delete_field: 'Alanı Sil',
|
delete_field: 'Alanı Sil',
|
||||||
|
// TODO: Translate
|
||||||
|
character_length: 'Max Length',
|
||||||
},
|
},
|
||||||
index_actions: {
|
index_actions: {
|
||||||
title: 'İndeks Özellikleri',
|
title: 'İndeks Özellikleri',
|
||||||
|
|||||||
@@ -150,6 +150,8 @@ export const uk: LanguageTranslation = {
|
|||||||
comments: 'Коментарі',
|
comments: 'Коментарі',
|
||||||
no_comments: 'Немає коментарів',
|
no_comments: 'Немає коментарів',
|
||||||
delete_field: 'Видалити поле',
|
delete_field: 'Видалити поле',
|
||||||
|
// TODO: Translate
|
||||||
|
character_length: 'Max Length',
|
||||||
},
|
},
|
||||||
index_actions: {
|
index_actions: {
|
||||||
title: 'Атрибути індексу',
|
title: 'Атрибути індексу',
|
||||||
|
|||||||
@@ -151,6 +151,8 @@ export const vi: LanguageTranslation = {
|
|||||||
comments: 'Bình luận',
|
comments: 'Bình luận',
|
||||||
no_comments: 'Không có bình luận',
|
no_comments: 'Không có bình luận',
|
||||||
delete_field: 'Xóa trường',
|
delete_field: 'Xóa trường',
|
||||||
|
// TODO: Translate
|
||||||
|
character_length: 'Max Length',
|
||||||
},
|
},
|
||||||
index_actions: {
|
index_actions: {
|
||||||
title: 'Thuộc tính chỉ mục',
|
title: 'Thuộc tính chỉ mục',
|
||||||
|
|||||||
@@ -148,6 +148,8 @@ export const zh_CN: LanguageTranslation = {
|
|||||||
comments: '注释',
|
comments: '注释',
|
||||||
no_comments: '空',
|
no_comments: '空',
|
||||||
delete_field: '删除字段',
|
delete_field: '删除字段',
|
||||||
|
// TODO: Translate
|
||||||
|
character_length: 'Max Length',
|
||||||
},
|
},
|
||||||
index_actions: {
|
index_actions: {
|
||||||
title: '索引属性',
|
title: '索引属性',
|
||||||
|
|||||||
@@ -148,6 +148,8 @@ export const zh_TW: LanguageTranslation = {
|
|||||||
comments: '註解',
|
comments: '註解',
|
||||||
no_comments: '無註解',
|
no_comments: '無註解',
|
||||||
delete_field: '刪除欄位',
|
delete_field: '刪除欄位',
|
||||||
|
// TODO: Translate
|
||||||
|
character_length: 'Max Length',
|
||||||
},
|
},
|
||||||
index_actions: {
|
index_actions: {
|
||||||
title: '索引屬性',
|
title: '索引屬性',
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { DataType } from './data-types';
|
import type { DataTypeData } from './data-types';
|
||||||
|
|
||||||
export const clickhouseDataTypes: readonly DataType[] = [
|
export const clickhouseDataTypes: readonly DataTypeData[] = [
|
||||||
// Numeric Types
|
// Numeric Types
|
||||||
{ name: 'uint8', id: 'uint8' },
|
{ name: 'uint8', id: 'uint8' },
|
||||||
{ name: 'uint16', id: 'uint16' },
|
{ name: 'uint16', id: 'uint16' },
|
||||||
@@ -48,25 +48,41 @@ export const clickhouseDataTypes: readonly DataType[] = [
|
|||||||
{ name: 'mediumblob', id: 'mediumblob' },
|
{ name: 'mediumblob', id: 'mediumblob' },
|
||||||
{ name: 'tinyblob', id: 'tinyblob' },
|
{ name: 'tinyblob', id: 'tinyblob' },
|
||||||
{ name: 'blob', id: 'blob' },
|
{ name: 'blob', id: 'blob' },
|
||||||
{ name: 'varchar', id: 'varchar' },
|
{ name: 'varchar', id: 'varchar', hasCharMaxLength: true },
|
||||||
{ name: 'char', id: 'char' },
|
{ name: 'char', id: 'char', hasCharMaxLength: true },
|
||||||
{ name: 'char large object', id: 'char_large_object' },
|
{ name: 'char large object', id: 'char_large_object' },
|
||||||
{ name: 'char varying', id: 'char_varying' },
|
{ name: 'char varying', id: 'char_varying', hasCharMaxLength: true },
|
||||||
{ name: 'character large object', id: 'character_large_object' },
|
{ name: 'character large object', id: 'character_large_object' },
|
||||||
{ name: 'character varying', id: 'character_varying' },
|
{
|
||||||
|
name: 'character varying',
|
||||||
|
id: 'character_varying',
|
||||||
|
hasCharMaxLength: true,
|
||||||
|
},
|
||||||
{ name: 'nchar large object', id: 'nchar_large_object' },
|
{ name: 'nchar large object', id: 'nchar_large_object' },
|
||||||
{ name: 'nchar varying', id: 'nchar_varying' },
|
{ name: 'nchar varying', id: 'nchar_varying', hasCharMaxLength: true },
|
||||||
{
|
{
|
||||||
name: 'national character large object',
|
name: 'national character large object',
|
||||||
id: 'national_character_large_object',
|
id: 'national_character_large_object',
|
||||||
},
|
},
|
||||||
{ name: 'national character varying', id: 'national_character_varying' },
|
{
|
||||||
{ name: 'national char varying', id: 'national_char_varying' },
|
name: 'national character varying',
|
||||||
{ name: 'national character', id: 'national_character' },
|
id: 'national_character_varying',
|
||||||
{ name: 'national char', id: 'national_char' },
|
hasCharMaxLength: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'national char varying',
|
||||||
|
id: 'national_char_varying',
|
||||||
|
hasCharMaxLength: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'national character',
|
||||||
|
id: 'national_character',
|
||||||
|
hasCharMaxLength: true,
|
||||||
|
},
|
||||||
|
{ name: 'national char', id: 'national_char', hasCharMaxLength: true },
|
||||||
{ name: 'binary large object', id: 'binary_large_object' },
|
{ name: 'binary large object', id: 'binary_large_object' },
|
||||||
{ name: 'binary varying', id: 'binary_varying' },
|
{ name: 'binary varying', id: 'binary_varying', hasCharMaxLength: true },
|
||||||
{ name: 'fixedstring', id: 'fixedstring' },
|
{ name: 'fixedstring', id: 'fixedstring', hasCharMaxLength: true },
|
||||||
{ name: 'string', id: 'string' },
|
{ name: 'string', id: 'string' },
|
||||||
|
|
||||||
// Date Types
|
// Date Types
|
||||||
|
|||||||
@@ -13,12 +13,16 @@ export interface DataType {
|
|||||||
name: string;
|
name: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface DataTypeData extends DataType {
|
||||||
|
hasCharMaxLength?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export const dataTypeSchema: z.ZodType<DataType> = z.object({
|
export const dataTypeSchema: z.ZodType<DataType> = z.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const dataTypeMap: Record<DatabaseType, readonly DataType[]> = {
|
export const dataTypeMap: Record<DatabaseType, readonly DataTypeData[]> = {
|
||||||
[DatabaseType.GENERIC]: genericDataTypes,
|
[DatabaseType.GENERIC]: genericDataTypes,
|
||||||
[DatabaseType.POSTGRESQL]: postgresDataTypes,
|
[DatabaseType.POSTGRESQL]: postgresDataTypes,
|
||||||
[DatabaseType.MYSQL]: mysqlDataTypes,
|
[DatabaseType.MYSQL]: mysqlDataTypes,
|
||||||
@@ -64,3 +68,21 @@ export function areFieldTypesCompatible(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const dataTypes = Object.values(dataTypeMap).flat();
|
export const dataTypes = Object.values(dataTypeMap).flat();
|
||||||
|
|
||||||
|
export const dataTypeDataToDataType = (
|
||||||
|
dataTypeData: DataTypeData
|
||||||
|
): DataType => ({
|
||||||
|
id: dataTypeData.id,
|
||||||
|
name: dataTypeData.name,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const findDataTypeDataById = (
|
||||||
|
id: string,
|
||||||
|
databaseType?: DatabaseType
|
||||||
|
): DataTypeData | undefined => {
|
||||||
|
const dataTypesOptions = databaseType
|
||||||
|
? dataTypeMap[databaseType]
|
||||||
|
: dataTypes;
|
||||||
|
|
||||||
|
return dataTypesOptions.find((dataType) => dataType.id === id);
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import type { DataType } from './data-types';
|
import type { DataTypeData } from './data-types';
|
||||||
|
|
||||||
export const genericDataTypes: readonly DataType[] = [
|
export const genericDataTypes: readonly DataTypeData[] = [
|
||||||
{ name: 'bigint', id: 'bigint' },
|
{ name: 'bigint', id: 'bigint' },
|
||||||
{ name: 'binary', id: 'binary' },
|
{ name: 'binary', id: 'binary', hasCharMaxLength: true },
|
||||||
{ name: 'blob', id: 'blob' },
|
{ name: 'blob', id: 'blob' },
|
||||||
{ name: 'boolean', id: 'boolean' },
|
{ name: 'boolean', id: 'boolean' },
|
||||||
{ name: 'char', id: 'char' },
|
{ name: 'char', id: 'char', hasCharMaxLength: true },
|
||||||
{ name: 'date', id: 'date' },
|
{ name: 'date', id: 'date' },
|
||||||
{ name: 'datetime', id: 'datetime' },
|
{ name: 'datetime', id: 'datetime' },
|
||||||
{ name: 'decimal', id: 'decimal' },
|
{ name: 'decimal', id: 'decimal' },
|
||||||
@@ -22,6 +22,6 @@ export const genericDataTypes: readonly DataType[] = [
|
|||||||
{ name: 'time', id: 'time' },
|
{ name: 'time', id: 'time' },
|
||||||
{ name: 'timestamp', id: 'timestamp' },
|
{ name: 'timestamp', id: 'timestamp' },
|
||||||
{ name: 'uuid', id: 'uuid' },
|
{ name: 'uuid', id: 'uuid' },
|
||||||
{ name: 'varbinary', id: 'varbinary' },
|
{ name: 'varbinary', id: 'varbinary', hasCharMaxLength: true },
|
||||||
{ name: 'varchar', id: 'varchar' },
|
{ name: 'varchar', id: 'varchar', hasCharMaxLength: true },
|
||||||
] as const;
|
] as const;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { DataType } from './data-types';
|
import type { DataTypeData } from './data-types';
|
||||||
|
|
||||||
export const mariadbDataTypes: readonly DataType[] = [
|
export const mariadbDataTypes: readonly DataTypeData[] = [
|
||||||
// Numeric Types
|
// Numeric Types
|
||||||
{ name: 'tinyint', id: 'tinyint' },
|
{ name: 'tinyint', id: 'tinyint' },
|
||||||
{ name: 'smallint', id: 'smallint' },
|
{ name: 'smallint', id: 'smallint' },
|
||||||
@@ -23,10 +23,10 @@ export const mariadbDataTypes: readonly DataType[] = [
|
|||||||
{ name: 'year', id: 'year' },
|
{ name: 'year', id: 'year' },
|
||||||
|
|
||||||
// String Types
|
// String Types
|
||||||
{ name: 'char', id: 'char' },
|
{ name: 'char', id: 'char', hasCharMaxLength: true },
|
||||||
{ name: 'varchar', id: 'varchar' },
|
{ name: 'varchar', id: 'varchar', hasCharMaxLength: true },
|
||||||
{ name: 'binary', id: 'binary' },
|
{ name: 'binary', id: 'binary', hasCharMaxLength: true },
|
||||||
{ name: 'varbinary', id: 'varbinary' },
|
{ name: 'varbinary', id: 'varbinary', hasCharMaxLength: true },
|
||||||
{ name: 'tinyblob', id: 'tinyblob' },
|
{ name: 'tinyblob', id: 'tinyblob' },
|
||||||
{ name: 'blob', id: 'blob' },
|
{ name: 'blob', id: 'blob' },
|
||||||
{ name: 'mediumblob', id: 'mediumblob' },
|
{ name: 'mediumblob', id: 'mediumblob' },
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { DataType } from './data-types';
|
import type { DataTypeData } from './data-types';
|
||||||
|
|
||||||
export const mysqlDataTypes: readonly DataType[] = [
|
export const mysqlDataTypes: readonly DataTypeData[] = [
|
||||||
// Numeric Types
|
// Numeric Types
|
||||||
{ name: 'tinyint', id: 'tinyint' },
|
{ name: 'tinyint', id: 'tinyint' },
|
||||||
{ name: 'smallint', id: 'smallint' },
|
{ name: 'smallint', id: 'smallint' },
|
||||||
@@ -23,10 +23,10 @@ export const mysqlDataTypes: readonly DataType[] = [
|
|||||||
{ name: 'year', id: 'year' },
|
{ name: 'year', id: 'year' },
|
||||||
|
|
||||||
// String Types
|
// String Types
|
||||||
{ name: 'char', id: 'char' },
|
{ name: 'char', id: 'char', hasCharMaxLength: true },
|
||||||
{ name: 'varchar', id: 'varchar' },
|
{ name: 'varchar', id: 'varchar', hasCharMaxLength: true },
|
||||||
{ name: 'binary', id: 'binary' },
|
{ name: 'binary', id: 'binary', hasCharMaxLength: true },
|
||||||
{ name: 'varbinary', id: 'varbinary' },
|
{ name: 'varbinary', id: 'varbinary', hasCharMaxLength: true },
|
||||||
{ name: 'tinyblob', id: 'tinyblob' },
|
{ name: 'tinyblob', id: 'tinyblob' },
|
||||||
{ name: 'blob', id: 'blob' },
|
{ name: 'blob', id: 'blob' },
|
||||||
{ name: 'mediumblob', id: 'mediumblob' },
|
{ name: 'mediumblob', id: 'mediumblob' },
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { DataType } from './data-types';
|
import type { DataTypeData } from './data-types';
|
||||||
|
|
||||||
export const postgresDataTypes: readonly DataType[] = [
|
export const postgresDataTypes: readonly DataTypeData[] = [
|
||||||
// Numeric Types
|
// Numeric Types
|
||||||
{ name: 'smallint', id: 'smallint' },
|
{ name: 'smallint', id: 'smallint' },
|
||||||
{ name: 'integer', id: 'integer' },
|
{ name: 'integer', id: 'integer' },
|
||||||
@@ -15,9 +15,13 @@ export const postgresDataTypes: readonly DataType[] = [
|
|||||||
{ name: 'money', id: 'money' },
|
{ name: 'money', id: 'money' },
|
||||||
|
|
||||||
// Character Types
|
// Character Types
|
||||||
{ name: 'char', id: 'char' },
|
{ name: 'char', id: 'char', hasCharMaxLength: true },
|
||||||
{ name: 'varchar', id: 'varchar' },
|
{ name: 'varchar', id: 'varchar', hasCharMaxLength: true },
|
||||||
{ name: 'character varying', id: 'character_varying' },
|
{
|
||||||
|
name: 'character varying',
|
||||||
|
id: 'character_varying',
|
||||||
|
hasCharMaxLength: true,
|
||||||
|
},
|
||||||
{ name: 'text', id: 'text' },
|
{ name: 'text', id: 'text' },
|
||||||
|
|
||||||
// Binary Data Types
|
// Binary Data Types
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { DataType } from './data-types';
|
import type { DataTypeData } from './data-types';
|
||||||
|
|
||||||
export const sqlServerDataTypes: readonly DataType[] = [
|
export const sqlServerDataTypes: readonly DataTypeData[] = [
|
||||||
// Exact Numerics
|
// Exact Numerics
|
||||||
{ name: 'bigint', id: 'bigint' },
|
{ name: 'bigint', id: 'bigint' },
|
||||||
{ name: 'bit', id: 'bit' },
|
{ name: 'bit', id: 'bit' },
|
||||||
@@ -25,18 +25,18 @@ export const sqlServerDataTypes: readonly DataType[] = [
|
|||||||
{ name: 'time', id: 'time' },
|
{ name: 'time', id: 'time' },
|
||||||
|
|
||||||
// Character Strings
|
// Character Strings
|
||||||
{ name: 'char', id: 'char' },
|
{ name: 'char', id: 'char', hasCharMaxLength: true },
|
||||||
{ name: 'varchar', id: 'varchar' },
|
{ name: 'varchar', id: 'varchar', hasCharMaxLength: true },
|
||||||
{ name: 'text', id: 'text' },
|
{ name: 'text', id: 'text' },
|
||||||
|
|
||||||
// Unicode Character Strings
|
// Unicode Character Strings
|
||||||
{ name: 'nchar', id: 'nchar' },
|
{ name: 'nchar', id: 'nchar', hasCharMaxLength: true },
|
||||||
{ name: 'nvarchar', id: 'nvarchar' },
|
{ name: 'nvarchar', id: 'nvarchar', hasCharMaxLength: true },
|
||||||
{ name: 'ntext', id: 'ntext' },
|
{ name: 'ntext', id: 'ntext' },
|
||||||
|
|
||||||
// Binary Strings
|
// Binary Strings
|
||||||
{ name: 'binary', id: 'binary' },
|
{ name: 'binary', id: 'binary', hasCharMaxLength: true },
|
||||||
{ name: 'varbinary', id: 'varbinary' },
|
{ name: 'varbinary', id: 'varbinary', hasCharMaxLength: true },
|
||||||
{ name: 'image', id: 'image' },
|
{ name: 'image', id: 'image' },
|
||||||
|
|
||||||
// Other Data Types
|
// Other Data Types
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { DataType } from './data-types';
|
import type { DataTypeData } from './data-types';
|
||||||
|
|
||||||
export const sqliteDataTypes: readonly DataType[] = [
|
export const sqliteDataTypes: readonly DataTypeData[] = [
|
||||||
// Numeric Types
|
// Numeric Types
|
||||||
{ name: 'integer', id: 'integer' },
|
{ name: 'integer', id: 'integer' },
|
||||||
{ name: 'real', id: 'real' },
|
{ name: 'real', id: 'real' },
|
||||||
@@ -22,6 +22,6 @@ export const sqliteDataTypes: readonly DataType[] = [
|
|||||||
{ name: 'int', id: 'int' },
|
{ name: 'int', id: 'int' },
|
||||||
{ name: 'float', id: 'float' },
|
{ name: 'float', id: 'float' },
|
||||||
{ name: 'boolean', id: 'boolean' },
|
{ name: 'boolean', id: 'boolean' },
|
||||||
{ name: 'varchar', id: 'varchar' },
|
{ name: 'varchar', id: 'varchar', hasCharMaxLength: true },
|
||||||
{ name: 'decimal', id: 'decimal' },
|
{ name: 'decimal', id: 'decimal' },
|
||||||
] as const;
|
] as const;
|
||||||
|
|||||||
@@ -115,8 +115,22 @@ export const exportBaseSQL = (diagram: Diagram): string => {
|
|||||||
|
|
||||||
// Remove the type cast part after :: if it exists
|
// Remove the type cast part after :: if it exists
|
||||||
if (fieldDefault.includes('::')) {
|
if (fieldDefault.includes('::')) {
|
||||||
|
const endedWithParentheses = fieldDefault.endsWith(')');
|
||||||
fieldDefault = fieldDefault.split('::')[0];
|
fieldDefault = fieldDefault.split('::')[0];
|
||||||
|
|
||||||
|
if (
|
||||||
|
(fieldDefault.startsWith('(') &&
|
||||||
|
!fieldDefault.endsWith(')')) ||
|
||||||
|
endedWithParentheses
|
||||||
|
) {
|
||||||
|
fieldDefault += ')';
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fieldDefault === `('now')`) {
|
||||||
|
fieldDefault = `now()`;
|
||||||
|
}
|
||||||
|
|
||||||
sqlScript += ` DEFAULT ${fieldDefault}`;
|
sqlScript += ` DEFAULT ${fieldDefault}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ export const createFieldsFromMetadata = ({
|
|||||||
nullable: col.nullable,
|
nullable: col.nullable,
|
||||||
...(col.character_maximum_length &&
|
...(col.character_maximum_length &&
|
||||||
col.character_maximum_length !== 'null'
|
col.character_maximum_length !== 'null'
|
||||||
? { character_maximum_length: col.character_maximum_length }
|
? { characterMaximumLength: col.character_maximum_length }
|
||||||
: {}),
|
: {}),
|
||||||
...(col.precision?.precision
|
...(col.precision?.precision
|
||||||
? { precision: col.precision.precision }
|
? { precision: col.precision.precision }
|
||||||
|
|||||||
@@ -103,10 +103,9 @@ const tableToTableNode = (
|
|||||||
|
|
||||||
export interface CanvasProps {
|
export interface CanvasProps {
|
||||||
initialTables: DBTable[];
|
initialTables: DBTable[];
|
||||||
readonly?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Canvas: React.FC<CanvasProps> = ({ initialTables, readonly }) => {
|
export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
|
||||||
const { getEdge, getInternalNode, getEdges, getNode } = useReactFlow();
|
const { getEdge, getInternalNode, getEdges, getNode } = useReactFlow();
|
||||||
const [selectedTableIds, setSelectedTableIds] = useState<string[]>([]);
|
const [selectedTableIds, setSelectedTableIds] = useState<string[]>([]);
|
||||||
const [selectedRelationshipIds, setSelectedRelationshipIds] = useState<
|
const [selectedRelationshipIds, setSelectedRelationshipIds] = useState<
|
||||||
@@ -127,6 +126,7 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables, readonly }) => {
|
|||||||
filteredSchemas,
|
filteredSchemas,
|
||||||
events,
|
events,
|
||||||
dependencies,
|
dependencies,
|
||||||
|
readonly,
|
||||||
} = useChartDB();
|
} = useChartDB();
|
||||||
const { showSidePanel } = useLayout();
|
const { showSidePanel } = useLayout();
|
||||||
const { effectiveTheme } = useTheme();
|
const { effectiveTheme } = useTheme();
|
||||||
@@ -682,7 +682,7 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables, readonly }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<CanvasContextMenu>
|
<CanvasContextMenu>
|
||||||
<div className="relative flex h-full">
|
<div className="relative flex h-full" id="canvas">
|
||||||
<ReactFlow
|
<ReactFlow
|
||||||
colorMode={effectiveTheme}
|
colorMode={effectiveTheme}
|
||||||
className="canvas-cursor-default nodes-animated"
|
className="canvas-cursor-default nodes-animated"
|
||||||
|
|||||||
86
src/pages/editor-page/canvas/hooks/use-is-lost-in-canvas.tsx
Normal file
86
src/pages/editor-page/canvas/hooks/use-is-lost-in-canvas.tsx
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
import { useCallback, useState } from 'react';
|
||||||
|
import { getTableDimensions } from '../canvas-utils';
|
||||||
|
import type { TableNodeType } from '../table-node/table-node';
|
||||||
|
import { useOnViewportChange, useReactFlow } from '@xyflow/react';
|
||||||
|
import { useDebounce } from '@/hooks/use-debounce-v2';
|
||||||
|
|
||||||
|
export const useIsLostInCanvas = () => {
|
||||||
|
const { getNodes, getViewport } = useReactFlow();
|
||||||
|
const [noTablesVisible, setNoTablesVisible] = useState<boolean>(false);
|
||||||
|
|
||||||
|
// Check if any tables are visible in the current viewport
|
||||||
|
const checkVisibleTables = useCallback(() => {
|
||||||
|
const nodes = getNodes();
|
||||||
|
const viewport = getViewport();
|
||||||
|
|
||||||
|
// If there are no nodes at all, don't highlight the button
|
||||||
|
if (nodes.length === 0) {
|
||||||
|
setNoTablesVisible(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Count visible (not hidden) nodes
|
||||||
|
const visibleNodes = nodes.filter((node) => !node.hidden);
|
||||||
|
|
||||||
|
// If there are no visible nodes at all, don't highlight the button
|
||||||
|
if (visibleNodes.length === 0) {
|
||||||
|
setNoTablesVisible(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate viewport boundaries
|
||||||
|
const viewportLeft = -viewport.x / viewport.zoom;
|
||||||
|
const viewportTop = -viewport.y / viewport.zoom;
|
||||||
|
|
||||||
|
const width =
|
||||||
|
document.getElementById('canvas')?.clientWidth || window.innerWidth;
|
||||||
|
const height =
|
||||||
|
document.getElementById('canvas')?.clientHeight ||
|
||||||
|
window.innerHeight;
|
||||||
|
|
||||||
|
const viewportRight = viewportLeft + width / viewport.zoom;
|
||||||
|
const viewportBottom = viewportTop + height / viewport.zoom;
|
||||||
|
|
||||||
|
// Check if any node is visible in the viewport
|
||||||
|
const anyNodeVisible = visibleNodes.some((node) => {
|
||||||
|
let nodeWidth = node.width || 0;
|
||||||
|
let nodeHeight = node.height || 0;
|
||||||
|
|
||||||
|
if (node.type === 'table' && node.data?.table) {
|
||||||
|
const tableNodeType = node as TableNodeType;
|
||||||
|
const dimensions = getTableDimensions(tableNodeType.data.table);
|
||||||
|
nodeWidth = dimensions.width;
|
||||||
|
nodeHeight = dimensions.height;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Node boundaries
|
||||||
|
const nodeLeft = node.position.x;
|
||||||
|
const nodeTop = node.position.y;
|
||||||
|
const nodeRight = nodeLeft + nodeWidth;
|
||||||
|
const nodeBottom = nodeTop + nodeHeight;
|
||||||
|
|
||||||
|
return (
|
||||||
|
nodeRight >= viewportLeft &&
|
||||||
|
nodeLeft <= viewportRight &&
|
||||||
|
nodeBottom >= viewportTop &&
|
||||||
|
nodeTop <= viewportBottom
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Only set to true if there are tables but none are visible
|
||||||
|
setNoTablesVisible(!anyNodeVisible);
|
||||||
|
}, [getNodes, getViewport]);
|
||||||
|
|
||||||
|
// Create a debounced version of checkVisibleTables
|
||||||
|
const debouncedCheckVisibleTables = useDebounce(checkVisibleTables, 1000);
|
||||||
|
|
||||||
|
useOnViewportChange({
|
||||||
|
onEnd: () => {
|
||||||
|
debouncedCheckVisibleTables();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
isLostInCanvas: noTablesVisible,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -7,6 +7,7 @@ import { useChartDB } from '@/hooks/use-chartdb';
|
|||||||
import { useLayout } from '@/hooks/use-layout';
|
import { useLayout } from '@/hooks/use-layout';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { getCardinalityMarkerId } from './canvas-utils';
|
import { getCardinalityMarkerId } from './canvas-utils';
|
||||||
|
import { useDiff } from '@/context/diff-context/use-diff';
|
||||||
|
|
||||||
export type RelationshipEdgeType = Edge<
|
export type RelationshipEdgeType = Edge<
|
||||||
{
|
{
|
||||||
@@ -29,6 +30,7 @@ export const RelationshipEdge: React.FC<EdgeProps<RelationshipEdgeType>> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const { getInternalNode, getEdge } = useReactFlow();
|
const { getInternalNode, getEdge } = useReactFlow();
|
||||||
const { openRelationshipFromSidebar, selectSidebarSection } = useLayout();
|
const { openRelationshipFromSidebar, selectSidebarSection } = useLayout();
|
||||||
|
const { checkIfRelationshipRemoved, checkIfNewRelationship } = useDiff();
|
||||||
|
|
||||||
const { relationships } = useChartDB();
|
const { relationships } = useChartDB();
|
||||||
|
|
||||||
@@ -149,6 +151,25 @@ export const RelationshipEdge: React.FC<EdgeProps<RelationshipEdgeType>> = ({
|
|||||||
}),
|
}),
|
||||||
[relationship?.targetCardinality, selected, targetSide]
|
[relationship?.targetCardinality, selected, targetSide]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const isDiffNewRelationship = useMemo(
|
||||||
|
() =>
|
||||||
|
relationship?.id
|
||||||
|
? checkIfNewRelationship({ relationshipId: relationship.id })
|
||||||
|
: false,
|
||||||
|
[checkIfNewRelationship, relationship?.id]
|
||||||
|
);
|
||||||
|
|
||||||
|
const isDiffRelationshipRemoved = useMemo(
|
||||||
|
() =>
|
||||||
|
relationship?.id
|
||||||
|
? checkIfRelationshipRemoved({
|
||||||
|
relationshipId: relationship.id,
|
||||||
|
})
|
||||||
|
: false,
|
||||||
|
[checkIfRelationshipRemoved, relationship?.id]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<path
|
<path
|
||||||
@@ -160,6 +181,10 @@ export const RelationshipEdge: React.FC<EdgeProps<RelationshipEdgeType>> = ({
|
|||||||
className={cn([
|
className={cn([
|
||||||
'react-flow__edge-path',
|
'react-flow__edge-path',
|
||||||
`!stroke-2 ${selected ? '!stroke-pink-600' : '!stroke-slate-400'}`,
|
`!stroke-2 ${selected ? '!stroke-pink-600' : '!stroke-slate-400'}`,
|
||||||
|
{
|
||||||
|
'!stroke-green-500': isDiffNewRelationship,
|
||||||
|
'!stroke-red-500': isDiffRelationshipRemoved,
|
||||||
|
},
|
||||||
])}
|
])}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
if (e.detail === 2) {
|
if (e.detail === 2) {
|
||||||
|
|||||||
@@ -12,7 +12,15 @@ import {
|
|||||||
useUpdateNodeInternals,
|
useUpdateNodeInternals,
|
||||||
} from '@xyflow/react';
|
} from '@xyflow/react';
|
||||||
import { Button } from '@/components/button/button';
|
import { Button } from '@/components/button/button';
|
||||||
import { Check, KeyRound, MessageCircleMore, Trash2 } from 'lucide-react';
|
import {
|
||||||
|
Check,
|
||||||
|
KeyRound,
|
||||||
|
MessageCircleMore,
|
||||||
|
SquareDot,
|
||||||
|
SquareMinus,
|
||||||
|
SquarePlus,
|
||||||
|
Trash2,
|
||||||
|
} from 'lucide-react';
|
||||||
import type { DBField } from '@/lib/domain/db-field';
|
import type { DBField } from '@/lib/domain/db-field';
|
||||||
import { useChartDB } from '@/hooks/use-chartdb';
|
import { useChartDB } from '@/hooks/use-chartdb';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
@@ -23,6 +31,7 @@ import {
|
|||||||
} from '@/components/tooltip/tooltip';
|
} from '@/components/tooltip/tooltip';
|
||||||
import { useClickAway, useKeyPressEvent } from 'react-use';
|
import { useClickAway, useKeyPressEvent } from 'react-use';
|
||||||
import { Input } from '@/components/input/input';
|
import { Input } from '@/components/input/input';
|
||||||
|
import { useDiff } from '@/context/diff-context/use-diff';
|
||||||
|
|
||||||
export const LEFT_HANDLE_ID_PREFIX = 'left_rel_';
|
export const LEFT_HANDLE_ID_PREFIX = 'left_rel_';
|
||||||
export const RIGHT_HANDLE_ID_PREFIX = 'right_rel_';
|
export const RIGHT_HANDLE_ID_PREFIX = 'right_rel_';
|
||||||
@@ -95,6 +104,43 @@ export const TableNodeField: React.FC<TableNodeFieldProps> = React.memo(
|
|||||||
useKeyPressEvent('Enter', editFieldName);
|
useKeyPressEvent('Enter', editFieldName);
|
||||||
useKeyPressEvent('Escape', abortEdit);
|
useKeyPressEvent('Escape', abortEdit);
|
||||||
|
|
||||||
|
const {
|
||||||
|
checkIfFieldRemoved,
|
||||||
|
checkIfNewField,
|
||||||
|
getFieldNewName,
|
||||||
|
getFieldNewType,
|
||||||
|
checkIfFieldHasChange,
|
||||||
|
} = useDiff();
|
||||||
|
|
||||||
|
const isDiffFieldRemoved = useMemo(
|
||||||
|
() => checkIfFieldRemoved({ fieldId: field.id }),
|
||||||
|
[checkIfFieldRemoved, field.id]
|
||||||
|
);
|
||||||
|
|
||||||
|
const isDiffNewField = useMemo(
|
||||||
|
() => checkIfNewField({ fieldId: field.id }),
|
||||||
|
[checkIfNewField, field.id]
|
||||||
|
);
|
||||||
|
|
||||||
|
const fieldDiffChangedName = useMemo(
|
||||||
|
() => getFieldNewName({ fieldId: field.id }),
|
||||||
|
[getFieldNewName, field.id]
|
||||||
|
);
|
||||||
|
|
||||||
|
const fieldDiffChangedType = useMemo(
|
||||||
|
() => getFieldNewType({ fieldId: field.id }),
|
||||||
|
[getFieldNewType, field.id]
|
||||||
|
);
|
||||||
|
|
||||||
|
const isDiffFieldChanged = useMemo(
|
||||||
|
() =>
|
||||||
|
checkIfFieldHasChange({
|
||||||
|
fieldId: field.id,
|
||||||
|
tableId: tableNodeId,
|
||||||
|
}),
|
||||||
|
[checkIfFieldHasChange, field.id, tableNodeId]
|
||||||
|
);
|
||||||
|
|
||||||
const enterEditMode = (e: React.MouseEvent) => {
|
const enterEditMode = (e: React.MouseEvent) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
setEditMode(true);
|
setEditMode(true);
|
||||||
@@ -102,13 +148,23 @@ export const TableNodeField: React.FC<TableNodeFieldProps> = React.memo(
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`group relative flex h-8 items-center justify-between gap-1 border-t px-3 text-sm last:rounded-b-[6px] hover:bg-slate-100 dark:hover:bg-slate-800 ${
|
className={cn(
|
||||||
highlighted ? 'bg-pink-100 dark:bg-pink-900' : ''
|
'group relative flex h-8 items-center justify-between gap-1 border-t px-3 text-sm last:rounded-b-[6px] hover:bg-slate-100 dark:hover:bg-slate-800',
|
||||||
} transition-all duration-200 ease-in-out ${
|
'transition-all duration-200 ease-in-out',
|
||||||
visible
|
{
|
||||||
? 'max-h-8 opacity-100'
|
'bg-pink-100 dark:bg-pink-900': highlighted,
|
||||||
: 'z-0 max-h-0 overflow-hidden opacity-0'
|
'max-h-8 opacity-100': visible,
|
||||||
}`}
|
'z-0 max-h-0 overflow-hidden opacity-0': !visible,
|
||||||
|
'bg-sky-200 dark:bg-sky-800 hover:bg-sky-100 dark:hover:bg-sky-900 border-sky-300 dark:border-sky-700':
|
||||||
|
isDiffFieldChanged &&
|
||||||
|
!isDiffFieldRemoved &&
|
||||||
|
!isDiffNewField,
|
||||||
|
'bg-red-200 dark:bg-red-800 hover:bg-red-100 dark:hover:bg-red-900 border-red-300 dark:border-red-700':
|
||||||
|
isDiffFieldRemoved,
|
||||||
|
'bg-green-200 dark:bg-green-800 hover:bg-green-100 dark:hover:bg-green-900 border-green-300 dark:border-green-700':
|
||||||
|
isDiffNewField,
|
||||||
|
}
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
{isConnectable ? (
|
{isConnectable ? (
|
||||||
<>
|
<>
|
||||||
@@ -161,7 +217,14 @@ export const TableNodeField: React.FC<TableNodeFieldProps> = React.memo(
|
|||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{editMode ? (
|
{isDiffFieldRemoved ? (
|
||||||
|
<SquareMinus className="size-3.5 text-red-800 dark:text-red-200" />
|
||||||
|
) : isDiffNewField ? (
|
||||||
|
<SquarePlus className="size-3.5 text-green-800 dark:text-green-200" />
|
||||||
|
) : isDiffFieldChanged ? (
|
||||||
|
<SquareDot className="size-3.5 shrink-0 text-sky-800 dark:text-sky-200" />
|
||||||
|
) : null}
|
||||||
|
{editMode && !readonly ? (
|
||||||
<>
|
<>
|
||||||
<Input
|
<Input
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
@@ -190,10 +253,27 @@ export const TableNodeField: React.FC<TableNodeFieldProps> = React.memo(
|
|||||||
// {field.name}
|
// {field.name}
|
||||||
// </span>
|
// </span>
|
||||||
<span
|
<span
|
||||||
className="truncate"
|
className={cn('truncate', {
|
||||||
|
'text-red-800 font-normal dark:text-red-200':
|
||||||
|
isDiffFieldRemoved,
|
||||||
|
'text-green-800 font-normal dark:text-green-200':
|
||||||
|
isDiffNewField,
|
||||||
|
'text-sky-800 font-normal dark:text-sky-200':
|
||||||
|
isDiffFieldChanged &&
|
||||||
|
!isDiffFieldRemoved &&
|
||||||
|
!isDiffNewField,
|
||||||
|
})}
|
||||||
onDoubleClick={enterEditMode}
|
onDoubleClick={enterEditMode}
|
||||||
>
|
>
|
||||||
{field.name}
|
{fieldDiffChangedName ? (
|
||||||
|
<>
|
||||||
|
{field.name}{' '}
|
||||||
|
<span className="font-medium">→</span>{' '}
|
||||||
|
{fieldDiffChangedName}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
field.name
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{/* <span className="truncate">{field.name}</span> */}
|
{/* <span className="truncate">{field.name}</span> */}
|
||||||
@@ -214,7 +294,18 @@ export const TableNodeField: React.FC<TableNodeFieldProps> = React.memo(
|
|||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'text-muted-foreground',
|
'text-muted-foreground',
|
||||||
!readonly ? 'group-hover:hidden' : ''
|
!readonly ? 'group-hover:hidden' : '',
|
||||||
|
isDiffFieldRemoved
|
||||||
|
? 'text-red-800 dark:text-red-200'
|
||||||
|
: '',
|
||||||
|
isDiffNewField
|
||||||
|
? 'text-green-800 dark:text-green-200'
|
||||||
|
: '',
|
||||||
|
isDiffFieldChanged &&
|
||||||
|
!isDiffFieldRemoved &&
|
||||||
|
!isDiffNewField
|
||||||
|
? 'text-sky-800 dark:text-sky-200'
|
||||||
|
: ''
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<KeyRound size={14} />
|
<KeyRound size={14} />
|
||||||
@@ -223,11 +314,31 @@ export const TableNodeField: React.FC<TableNodeFieldProps> = React.memo(
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'content-center truncate text-right text-xs text-muted-foreground shrink-0',
|
'content-center truncate text-right text-xs text-muted-foreground',
|
||||||
!readonly ? 'group-hover:hidden' : ''
|
!readonly ? 'group-hover:hidden' : '',
|
||||||
|
isDiffFieldRemoved
|
||||||
|
? 'text-red-800 dark:text-red-200'
|
||||||
|
: '',
|
||||||
|
isDiffNewField
|
||||||
|
? 'text-green-800 dark:text-green-200'
|
||||||
|
: '',
|
||||||
|
isDiffFieldChanged &&
|
||||||
|
!isDiffFieldRemoved &&
|
||||||
|
!isDiffNewField
|
||||||
|
? 'text-sky-800 dark:text-sky-200'
|
||||||
|
: ''
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{field.type.name}
|
{fieldDiffChangedType ? (
|
||||||
|
<>
|
||||||
|
<span className="line-through">
|
||||||
|
{field.type.name.split(' ')[0]}
|
||||||
|
</span>{' '}
|
||||||
|
{fieldDiffChangedType.name.split(' ')[0]}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
field.type.name.split(' ')[0]
|
||||||
|
)}
|
||||||
{field.nullable ? '?' : ''}
|
{field.nullable ? '?' : ''}
|
||||||
</div>
|
</div>
|
||||||
{readonly ? null : (
|
{readonly ? null : (
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
export interface TableNodeStatusProps {
|
||||||
|
status: 'new' | 'changed' | 'removed' | 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TableNodeStatus: React.FC<TableNodeStatusProps> = ({ status }) => {
|
||||||
|
if (status === 'none') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className="absolute left-1/2 top-0 z-10 -translate-x-1/2 -translate-y-1/2">
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
'inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium text-white',
|
||||||
|
{
|
||||||
|
'bg-green-100 text-green-800 dark:bg-green-800 dark:text-green-100':
|
||||||
|
status === 'new',
|
||||||
|
'bg-sky-100 text-sky-800 dark:bg-sky-800 dark:text-sky-100':
|
||||||
|
status === 'changed',
|
||||||
|
'bg-red-100 text-red-800 dark:bg-red-800 dark:text-red-100':
|
||||||
|
status === 'removed',
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{status === 'new'
|
||||||
|
? 'New'
|
||||||
|
: status === 'changed'
|
||||||
|
? 'Modified'
|
||||||
|
: 'Deleted'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -10,6 +10,9 @@ import {
|
|||||||
ChevronUp,
|
ChevronUp,
|
||||||
Check,
|
Check,
|
||||||
CircleDotDashed,
|
CircleDotDashed,
|
||||||
|
SquareDot,
|
||||||
|
SquarePlus,
|
||||||
|
SquareMinus,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { Label } from '@/components/label/label';
|
import { Label } from '@/components/label/label';
|
||||||
import type { DBTable } from '@/lib/domain/db-table';
|
import type { DBTable } from '@/lib/domain/db-table';
|
||||||
@@ -30,6 +33,8 @@ import {
|
|||||||
TooltipContent,
|
TooltipContent,
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from '@/components/tooltip/tooltip';
|
} from '@/components/tooltip/tooltip';
|
||||||
|
import { useDiff } from '@/context/diff-context/use-diff';
|
||||||
|
import { TableNodeStatus } from './table-node-status/table-node-status';
|
||||||
|
|
||||||
export type TableNodeType = Node<
|
export type TableNodeType = Node<
|
||||||
{
|
{
|
||||||
@@ -61,6 +66,35 @@ export const TableNode: React.FC<NodeProps<TableNodeType>> = React.memo(
|
|||||||
const [tableName, setTableName] = useState(table.name);
|
const [tableName, setTableName] = useState(table.name);
|
||||||
const inputRef = React.useRef<HTMLInputElement>(null);
|
const inputRef = React.useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
const {
|
||||||
|
getTableNewName,
|
||||||
|
checkIfTableHasChange,
|
||||||
|
checkIfNewTable,
|
||||||
|
checkIfTableRemoved,
|
||||||
|
} = useDiff();
|
||||||
|
|
||||||
|
const fields = useMemo(() => table.fields, [table.fields]);
|
||||||
|
|
||||||
|
const tableChangedName = useMemo(
|
||||||
|
() => getTableNewName({ tableId: table.id }),
|
||||||
|
[getTableNewName, table.id]
|
||||||
|
);
|
||||||
|
|
||||||
|
const isDiffTableChanged = useMemo(
|
||||||
|
() => checkIfTableHasChange({ tableId: table.id }),
|
||||||
|
[checkIfTableHasChange, table.id]
|
||||||
|
);
|
||||||
|
|
||||||
|
const isDiffNewTable = useMemo(
|
||||||
|
() => checkIfNewTable({ tableId: table.id }),
|
||||||
|
[checkIfNewTable, table.id]
|
||||||
|
);
|
||||||
|
|
||||||
|
const isDiffTableRemoved = useMemo(
|
||||||
|
() => checkIfTableRemoved({ tableId: table.id }),
|
||||||
|
[checkIfTableRemoved, table.id]
|
||||||
|
);
|
||||||
|
|
||||||
const selectedRelEdges = edges.filter(
|
const selectedRelEdges = edges.filter(
|
||||||
(edge) =>
|
(edge) =>
|
||||||
(edge.source === id || edge.target === id) &&
|
(edge.source === id || edge.target === id) &&
|
||||||
@@ -109,13 +143,13 @@ export const TableNode: React.FC<NodeProps<TableNodeType>> = React.memo(
|
|||||||
|
|
||||||
const visibleFields = useMemo(() => {
|
const visibleFields = useMemo(() => {
|
||||||
if (expanded) {
|
if (expanded) {
|
||||||
return table.fields;
|
return fields;
|
||||||
}
|
}
|
||||||
|
|
||||||
const mustDisplayedFields = table.fields.filter((field: DBField) =>
|
const mustDisplayedFields = fields.filter((field: DBField) =>
|
||||||
isMustDisplayedField(field)
|
isMustDisplayedField(field)
|
||||||
);
|
);
|
||||||
const nonMustDisplayedFields = table.fields.filter(
|
const nonMustDisplayedFields = fields.filter(
|
||||||
(field: DBField) => !isMustDisplayedField(field)
|
(field: DBField) => !isMustDisplayedField(field)
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -133,8 +167,8 @@ export const TableNode: React.FC<NodeProps<TableNodeType>> = React.memo(
|
|||||||
return [
|
return [
|
||||||
...visibleMustDisplayedFields,
|
...visibleMustDisplayedFields,
|
||||||
...visibleNonMustDisplayedFields,
|
...visibleNonMustDisplayedFields,
|
||||||
].sort((a, b) => table.fields.indexOf(a) - table.fields.indexOf(b));
|
].sort((a, b) => fields.indexOf(a) - fields.indexOf(b));
|
||||||
}, [expanded, table.fields, isMustDisplayedField]);
|
}, [expanded, fields, isMustDisplayedField]);
|
||||||
|
|
||||||
const editTableName = useCallback(() => {
|
const editTableName = useCallback(() => {
|
||||||
if (!editMode) return;
|
if (!editMode) return;
|
||||||
@@ -174,6 +208,17 @@ export const TableNode: React.FC<NodeProps<TableNodeType>> = React.memo(
|
|||||||
: '',
|
: '',
|
||||||
highlightOverlappingTables && isOverlapping
|
highlightOverlappingTables && isOverlapping
|
||||||
? 'animate-scale-2'
|
? 'animate-scale-2'
|
||||||
|
: '',
|
||||||
|
isDiffTableChanged &&
|
||||||
|
!isDiffNewTable &&
|
||||||
|
!isDiffTableRemoved
|
||||||
|
? 'outline outline-[3px] outline-sky-500 dark:outline-sky-900 outline-offset-[5px]'
|
||||||
|
: '',
|
||||||
|
isDiffNewTable
|
||||||
|
? 'outline outline-[3px] outline-green-500 dark:outline-green-900 outline-offset-[5px]'
|
||||||
|
: '',
|
||||||
|
isDiffTableRemoved
|
||||||
|
? 'outline outline-[3px] outline-red-500 dark:outline-red-900 outline-offset-[5px]'
|
||||||
: ''
|
: ''
|
||||||
)}
|
)}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
@@ -194,14 +239,87 @@ export const TableNode: React.FC<NodeProps<TableNodeType>> = React.memo(
|
|||||||
table={table}
|
table={table}
|
||||||
focused={focused}
|
focused={focused}
|
||||||
/>
|
/>
|
||||||
|
{/* Badge added here */}
|
||||||
|
<TableNodeStatus
|
||||||
|
status={
|
||||||
|
isDiffNewTable
|
||||||
|
? 'new'
|
||||||
|
: isDiffTableRemoved
|
||||||
|
? 'removed'
|
||||||
|
: isDiffTableChanged
|
||||||
|
? 'changed'
|
||||||
|
: 'none'
|
||||||
|
}
|
||||||
|
/>
|
||||||
<div
|
<div
|
||||||
className="h-2 rounded-t-[6px]"
|
className="h-2 rounded-t-[6px]"
|
||||||
style={{ backgroundColor: table.color }}
|
style={{ backgroundColor: table.color }}
|
||||||
></div>
|
></div>
|
||||||
<div className="group flex h-9 items-center justify-between bg-slate-200 px-2 dark:bg-slate-900">
|
<div className="group flex h-9 items-center justify-between bg-slate-200 px-2 dark:bg-slate-900">
|
||||||
<div className="flex min-w-0 flex-1 items-center gap-2">
|
<div className="flex min-w-0 flex-1 items-center gap-2">
|
||||||
|
{isDiffNewTable ? (
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<SquarePlus
|
||||||
|
className="size-3.5 shrink-0 text-green-600"
|
||||||
|
strokeWidth={2.5}
|
||||||
|
/>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>New Table</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
) : isDiffTableRemoved ? (
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<SquareMinus
|
||||||
|
className="size-3.5 shrink-0 text-red-600"
|
||||||
|
strokeWidth={2.5}
|
||||||
|
/>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
Table Removed
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
) : isDiffTableChanged ? (
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<SquareDot
|
||||||
|
className="size-3.5 shrink-0 text-sky-600"
|
||||||
|
strokeWidth={2.5}
|
||||||
|
/>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
Table Changed
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
) : (
|
||||||
<Table2 className="size-3.5 shrink-0 text-gray-600 dark:text-primary" />
|
<Table2 className="size-3.5 shrink-0 text-gray-600 dark:text-primary" />
|
||||||
{editMode ? (
|
)}
|
||||||
|
|
||||||
|
{tableChangedName ? (
|
||||||
|
<Label className="flex h-5 items-center justify-center truncate rounded-sm bg-sky-200 px-2 py-0.5 text-sm font-normal text-sky-900 dark:bg-sky-800 dark:text-sky-200">
|
||||||
|
<span className="truncate">
|
||||||
|
{table.name}
|
||||||
|
</span>
|
||||||
|
<span className="mx-1 font-semibold">
|
||||||
|
→
|
||||||
|
</span>
|
||||||
|
<span className="truncate">
|
||||||
|
{tableChangedName}
|
||||||
|
</span>
|
||||||
|
</Label>
|
||||||
|
) : isDiffNewTable ? (
|
||||||
|
<Label className="flex h-5 flex-col justify-center truncate rounded-sm bg-green-200 px-2 py-0.5 text-sm font-normal text-green-900 dark:bg-green-800 dark:text-green-200">
|
||||||
|
{table.name}
|
||||||
|
</Label>
|
||||||
|
) : isDiffTableRemoved ? (
|
||||||
|
<Label className="flex h-5 flex-col justify-center truncate rounded-sm bg-red-200 px-2 py-0.5 text-sm font-normal text-red-900 dark:bg-red-800 dark:text-red-200">
|
||||||
|
{table.name}
|
||||||
|
</Label>
|
||||||
|
) : isDiffTableChanged ? (
|
||||||
|
<Label className="flex h-5 flex-col justify-center truncate rounded-sm bg-sky-200 px-2 py-0.5 text-sm font-normal text-sky-900 dark:bg-sky-800 dark:text-sky-200">
|
||||||
|
{table.name}
|
||||||
|
</Label>
|
||||||
|
) : editMode && !readonly ? (
|
||||||
<>
|
<>
|
||||||
<Input
|
<Input
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
@@ -273,11 +391,11 @@ export const TableNode: React.FC<NodeProps<TableNodeType>> = React.memo(
|
|||||||
className="transition-[max-height] duration-200 ease-in-out"
|
className="transition-[max-height] duration-200 ease-in-out"
|
||||||
style={{
|
style={{
|
||||||
maxHeight: expanded
|
maxHeight: expanded
|
||||||
? `${table.fields.length * 2}rem` // h-8 per field
|
? `${fields.length * 2}rem` // h-8 per field
|
||||||
: `${TABLE_MINIMIZED_FIELDS * 2}rem`, // h-8 per field
|
: `${TABLE_MINIMIZED_FIELDS * 2}rem`, // h-8 per field
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{table.fields.map((field: DBField) => (
|
{fields.map((field: DBField) => (
|
||||||
<TableNodeField
|
<TableNodeField
|
||||||
key={field.id}
|
key={field.id}
|
||||||
focused={focused}
|
focused={focused}
|
||||||
@@ -295,7 +413,7 @@ export const TableNode: React.FC<NodeProps<TableNodeType>> = React.memo(
|
|||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
{table.fields.length > TABLE_MINIMIZED_FIELDS && (
|
{fields.length > TABLE_MINIMIZED_FIELDS && (
|
||||||
<div
|
<div
|
||||||
className="z-10 flex h-8 cursor-pointer items-center justify-center rounded-b-md border-t text-xs text-muted-foreground transition-colors duration-200 hover:bg-slate-100 dark:hover:bg-slate-800"
|
className="z-10 flex h-8 cursor-pointer items-center justify-center rounded-b-md border-t text-xs text-muted-foreground transition-colors duration-200 hover:bg-slate-100 dark:hover:bg-slate-800"
|
||||||
onClick={toggleExpand}
|
onClick={toggleExpand}
|
||||||
|
|||||||
@@ -1,17 +1,22 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import type { ButtonProps } from '@/components/button/button';
|
import type { ButtonProps } from '@/components/button/button';
|
||||||
import { Button } from '@/components/button/button';
|
import { Button } from '@/components/button/button';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
export const ToolbarButton = React.forwardRef<
|
export const ToolbarButton = React.forwardRef<
|
||||||
React.ElementRef<typeof Button>,
|
React.ElementRef<typeof Button>,
|
||||||
ButtonProps
|
ButtonProps
|
||||||
>((props, ref) => {
|
>((props, ref) => {
|
||||||
|
const { className, ...rest } = props;
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
ref={ref}
|
ref={ref}
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
className={'w-[36px] p-2 hover:bg-primary-foreground'}
|
className={cn(
|
||||||
{...props}
|
'w-[36px] p-2 hover:bg-primary-foreground',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...rest}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { useTranslation } from 'react-i18next';
|
|||||||
import { Button } from '@/components/button/button';
|
import { Button } from '@/components/button/button';
|
||||||
import { keyboardShortcutsForOS } from '@/context/keyboard-shortcuts-context/keyboard-shortcuts';
|
import { keyboardShortcutsForOS } from '@/context/keyboard-shortcuts-context/keyboard-shortcuts';
|
||||||
import { KeyboardShortcutAction } from '@/context/keyboard-shortcuts-context/keyboard-shortcuts';
|
import { KeyboardShortcutAction } from '@/context/keyboard-shortcuts-context/keyboard-shortcuts';
|
||||||
|
import { useIsLostInCanvas } from '../hooks/use-is-lost-in-canvas';
|
||||||
|
|
||||||
const convertToPercentage = (value: number) => `${Math.round(value * 100)}%`;
|
const convertToPercentage = (value: number) => `${Math.round(value * 100)}%`;
|
||||||
|
|
||||||
@@ -28,6 +29,8 @@ export const Toolbar: React.FC<ToolbarProps> = ({ readonly }) => {
|
|||||||
const { redo, undo, hasRedo, hasUndo } = useHistory();
|
const { redo, undo, hasRedo, hasUndo } = useHistory();
|
||||||
const { getZoom, zoomIn, zoomOut, fitView } = useReactFlow();
|
const { getZoom, zoomIn, zoomOut, fitView } = useReactFlow();
|
||||||
const [zoom, setZoom] = useState<string>(convertToPercentage(getZoom()));
|
const [zoom, setZoom] = useState<string>(convertToPercentage(getZoom()));
|
||||||
|
const { isLostInCanvas } = useIsLostInCanvas();
|
||||||
|
|
||||||
useOnViewportChange({
|
useOnViewportChange({
|
||||||
onChange: ({ zoom }) => {
|
onChange: ({ zoom }) => {
|
||||||
setZoom(convertToPercentage(zoom));
|
setZoom(convertToPercentage(zoom));
|
||||||
@@ -93,7 +96,14 @@ export const Toolbar: React.FC<ToolbarProps> = ({ readonly }) => {
|
|||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<span>
|
<span>
|
||||||
<ToolbarButton onClick={showAll}>
|
<ToolbarButton
|
||||||
|
onClick={showAll}
|
||||||
|
className={
|
||||||
|
isLostInCanvas
|
||||||
|
? 'bg-pink-500 text-white hover:bg-pink-600 hover:text-white'
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
>
|
||||||
<Scan />
|
<Scan />
|
||||||
</ToolbarButton>
|
</ToolbarButton>
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -29,14 +29,11 @@ import { AlertProvider } from '@/context/alert-context/alert-provider';
|
|||||||
import { CanvasProvider } from '@/context/canvas-context/canvas-provider';
|
import { CanvasProvider } from '@/context/canvas-context/canvas-provider';
|
||||||
import { HIDE_BUCKLE_DOT_DEV } from '@/lib/env';
|
import { HIDE_BUCKLE_DOT_DEV } from '@/lib/env';
|
||||||
import { useDiagramLoader } from './use-diagram-loader';
|
import { useDiagramLoader } from './use-diagram-loader';
|
||||||
|
import { DiffProvider } from '@/context/diff-context/diff-provider';
|
||||||
|
|
||||||
const OPEN_STAR_US_AFTER_SECONDS = 30;
|
const OPEN_STAR_US_AFTER_SECONDS = 30;
|
||||||
const SHOW_STAR_US_AGAIN_AFTER_DAYS = 1;
|
const SHOW_STAR_US_AGAIN_AFTER_DAYS = 1;
|
||||||
|
|
||||||
const OPEN_BUCKLE_AFTER_SECONDS = 60;
|
|
||||||
const SHOW_BUCKLE_AGAIN_AFTER_DAYS = 1;
|
|
||||||
const SHOW_BUCKLE_AGAIN_OPENED_AFTER_DAYS = 7;
|
|
||||||
|
|
||||||
export const EditorDesktopLayoutLazy = React.lazy(
|
export const EditorDesktopLayoutLazy = React.lazy(
|
||||||
() => import('./editor-desktop-layout')
|
() => import('./editor-desktop-layout')
|
||||||
);
|
);
|
||||||
@@ -49,7 +46,7 @@ const EditorPageComponent: React.FC = () => {
|
|||||||
const { diagramName, currentDiagram, schemas, filteredSchemas } =
|
const { diagramName, currentDiagram, schemas, filteredSchemas } =
|
||||||
useChartDB();
|
useChartDB();
|
||||||
const { openSelectSchema, showSidePanel } = useLayout();
|
const { openSelectSchema, showSidePanel } = useLayout();
|
||||||
const { openStarUsDialog, openBuckleDialog } = useDialog();
|
const { openStarUsDialog } = useDialog();
|
||||||
const { diagramId } = useParams<{ diagramId: string }>();
|
const { diagramId } = useParams<{ diagramId: string }>();
|
||||||
const { isMd: isDesktop } = useBreakpoint('md');
|
const { isMd: isDesktop } = useBreakpoint('md');
|
||||||
const {
|
const {
|
||||||
@@ -58,9 +55,6 @@ const EditorPageComponent: React.FC = () => {
|
|||||||
starUsDialogLastOpen,
|
starUsDialogLastOpen,
|
||||||
setStarUsDialogLastOpen,
|
setStarUsDialogLastOpen,
|
||||||
githubRepoOpened,
|
githubRepoOpened,
|
||||||
setBuckleDialogLastOpen,
|
|
||||||
buckleDialogLastOpen,
|
|
||||||
buckleWaitlistOpened,
|
|
||||||
} = useLocalConfig();
|
} = useLocalConfig();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -91,37 +85,6 @@ const EditorPageComponent: React.FC = () => {
|
|||||||
starUsDialogLastOpen,
|
starUsDialogLastOpen,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (HIDE_BUCKLE_DOT_DEV) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!currentDiagram?.id) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
new Date().getTime() - buckleDialogLastOpen >
|
|
||||||
1000 *
|
|
||||||
60 *
|
|
||||||
60 *
|
|
||||||
24 *
|
|
||||||
(buckleWaitlistOpened
|
|
||||||
? SHOW_BUCKLE_AGAIN_OPENED_AFTER_DAYS
|
|
||||||
: SHOW_BUCKLE_AGAIN_AFTER_DAYS)
|
|
||||||
) {
|
|
||||||
const lastOpen = new Date().getTime();
|
|
||||||
setBuckleDialogLastOpen(lastOpen);
|
|
||||||
setTimeout(openBuckleDialog, OPEN_BUCKLE_AFTER_SECONDS * 1000);
|
|
||||||
}
|
|
||||||
}, [
|
|
||||||
currentDiagram?.id,
|
|
||||||
buckleWaitlistOpened,
|
|
||||||
openBuckleDialog,
|
|
||||||
setBuckleDialogLastOpen,
|
|
||||||
buckleDialogLastOpen,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const lastDiagramId = useRef<string>('');
|
const lastDiagramId = useRef<string>('');
|
||||||
|
|
||||||
const handleChangeSchema = useCallback(async () => {
|
const handleChangeSchema = useCallback(async () => {
|
||||||
@@ -235,6 +198,7 @@ export const EditorPage: React.FC = () => (
|
|||||||
<StorageProvider>
|
<StorageProvider>
|
||||||
<ConfigProvider>
|
<ConfigProvider>
|
||||||
<RedoUndoStackProvider>
|
<RedoUndoStackProvider>
|
||||||
|
<DiffProvider>
|
||||||
<ChartDBProvider>
|
<ChartDBProvider>
|
||||||
<HistoryProvider>
|
<HistoryProvider>
|
||||||
<ReactFlowProvider>
|
<ReactFlowProvider>
|
||||||
@@ -252,6 +216,7 @@ export const EditorPage: React.FC = () => (
|
|||||||
</ReactFlowProvider>
|
</ReactFlowProvider>
|
||||||
</HistoryProvider>
|
</HistoryProvider>
|
||||||
</ChartDBProvider>
|
</ChartDBProvider>
|
||||||
|
</DiffProvider>
|
||||||
</RedoUndoStackProvider>
|
</RedoUndoStackProvider>
|
||||||
</ConfigProvider>
|
</ConfigProvider>
|
||||||
</StorageProvider>
|
</StorageProvider>
|
||||||
|
|||||||
@@ -0,0 +1,162 @@
|
|||||||
|
import React, { useEffect, useRef } from 'react';
|
||||||
|
import { Ellipsis, Trash2 } from 'lucide-react';
|
||||||
|
import { Input } from '@/components/input/input';
|
||||||
|
import { Button } from '@/components/button/button';
|
||||||
|
import { Separator } from '@/components/separator/separator';
|
||||||
|
import type { DBField } from '@/lib/domain/db-field';
|
||||||
|
import { findDataTypeDataById } from '@/lib/data/data-types/data-types';
|
||||||
|
import {
|
||||||
|
Popover,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverTrigger,
|
||||||
|
} from '@/components/popover/popover';
|
||||||
|
import { Label } from '@/components/label/label';
|
||||||
|
import { Checkbox } from '@/components/checkbox/checkbox';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { Textarea } from '@/components/textarea/textarea';
|
||||||
|
import { debounce } from '@/lib/utils';
|
||||||
|
|
||||||
|
export interface TableFieldPopoverProps {
|
||||||
|
field: DBField;
|
||||||
|
updateField: (attrs: Partial<DBField>) => void;
|
||||||
|
removeField: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TableFieldPopover: React.FC<TableFieldPopoverProps> = ({
|
||||||
|
field,
|
||||||
|
updateField,
|
||||||
|
removeField,
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [localField, setLocalField] = React.useState<DBField>(field);
|
||||||
|
|
||||||
|
const debouncedUpdateFieldRef = useRef<((value?: DBField) => void) | null>(
|
||||||
|
null
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
debouncedUpdateFieldRef.current = debounce((value?: DBField) => {
|
||||||
|
updateField({
|
||||||
|
comments: value?.comments,
|
||||||
|
characterMaximumLength: value?.characterMaximumLength,
|
||||||
|
unique: value?.unique,
|
||||||
|
});
|
||||||
|
}, 200);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
debouncedUpdateFieldRef.current = null;
|
||||||
|
};
|
||||||
|
}, [updateField]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (debouncedUpdateFieldRef.current) {
|
||||||
|
debouncedUpdateFieldRef.current(localField);
|
||||||
|
}
|
||||||
|
}, [localField]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popover
|
||||||
|
onOpenChange={(isOpen) => {
|
||||||
|
if (isOpen) {
|
||||||
|
setLocalField(field);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
className="h-8 w-[32px] p-2 text-slate-500 hover:bg-primary-foreground hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-200"
|
||||||
|
>
|
||||||
|
<Ellipsis className="size-3.5" />
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="w-52">
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<div className="text-sm font-semibold">
|
||||||
|
{t(
|
||||||
|
'side_panel.tables_section.table.field_actions.title'
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<Separator orientation="horizontal" />
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Label htmlFor="width" className="text-subtitle">
|
||||||
|
{t(
|
||||||
|
'side_panel.tables_section.table.field_actions.unique'
|
||||||
|
)}
|
||||||
|
</Label>
|
||||||
|
<Checkbox
|
||||||
|
checked={localField.unique}
|
||||||
|
disabled={field.primaryKey}
|
||||||
|
onCheckedChange={(value) =>
|
||||||
|
setLocalField((current) => ({
|
||||||
|
...current,
|
||||||
|
unique: !!value,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{findDataTypeDataById(field.type.id)
|
||||||
|
?.hasCharMaxLength ? (
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Label
|
||||||
|
htmlFor="width"
|
||||||
|
className="text-subtitle"
|
||||||
|
>
|
||||||
|
{t(
|
||||||
|
'side_panel.tables_section.table.field_actions.character_length'
|
||||||
|
)}
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
value={
|
||||||
|
localField.characterMaximumLength ?? ''
|
||||||
|
}
|
||||||
|
type="number"
|
||||||
|
onChange={(e) =>
|
||||||
|
setLocalField((current) => ({
|
||||||
|
...current,
|
||||||
|
characterMaximumLength:
|
||||||
|
e.target.value,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
className="w-full rounded-md bg-muted text-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Label htmlFor="width" className="text-subtitle">
|
||||||
|
{t(
|
||||||
|
'side_panel.tables_section.table.field_actions.comments'
|
||||||
|
)}
|
||||||
|
</Label>
|
||||||
|
<Textarea
|
||||||
|
value={localField.comments}
|
||||||
|
onChange={(e) =>
|
||||||
|
setLocalField((current) => ({
|
||||||
|
...current,
|
||||||
|
comments: e.target.value,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
placeholder={t(
|
||||||
|
'side_panel.tables_section.table.field_actions.no_comments'
|
||||||
|
)}
|
||||||
|
className="w-full rounded-md bg-muted text-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Separator orientation="horizontal" />
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="flex gap-2 !text-red-700"
|
||||||
|
onClick={removeField}
|
||||||
|
>
|
||||||
|
<Trash2 className="size-3.5 text-red-700" />
|
||||||
|
{t(
|
||||||
|
'side_panel.tables_section.table.field_actions.delete_field'
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,30 +1,27 @@
|
|||||||
import React from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import { Ellipsis, GripVertical, Trash2, KeyRound } from 'lucide-react';
|
import { GripVertical, KeyRound } from 'lucide-react';
|
||||||
import { Input } from '@/components/input/input';
|
import { Input } from '@/components/input/input';
|
||||||
import { Button } from '@/components/button/button';
|
|
||||||
import { Separator } from '@/components/separator/separator';
|
|
||||||
|
|
||||||
import type { DBField } from '@/lib/domain/db-field';
|
import type { DBField } from '@/lib/domain/db-field';
|
||||||
import { useChartDB } from '@/hooks/use-chartdb';
|
import { useChartDB } from '@/hooks/use-chartdb';
|
||||||
import { dataTypeMap } from '@/lib/data/data-types/data-types';
|
import {
|
||||||
|
dataTypeDataToDataType,
|
||||||
|
dataTypeMap,
|
||||||
|
} from '@/lib/data/data-types/data-types';
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
TooltipContent,
|
TooltipContent,
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from '@/components/tooltip/tooltip';
|
} from '@/components/tooltip/tooltip';
|
||||||
import {
|
|
||||||
Popover,
|
|
||||||
PopoverContent,
|
|
||||||
PopoverTrigger,
|
|
||||||
} from '@/components/popover/popover';
|
|
||||||
import { Label } from '@/components/label/label';
|
|
||||||
import { Checkbox } from '@/components/checkbox/checkbox';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Textarea } from '@/components/textarea/textarea';
|
|
||||||
import { TableFieldToggle } from './table-field-toggle';
|
import { TableFieldToggle } from './table-field-toggle';
|
||||||
import { useSortable } from '@dnd-kit/sortable';
|
import { useSortable } from '@dnd-kit/sortable';
|
||||||
import { CSS } from '@dnd-kit/utilities';
|
import { CSS } from '@dnd-kit/utilities';
|
||||||
|
import type {
|
||||||
|
SelectBoxOption,
|
||||||
|
SelectBoxProps,
|
||||||
|
} from '@/components/select-box/select-box';
|
||||||
import { SelectBox } from '@/components/select-box/select-box';
|
import { SelectBox } from '@/components/select-box/select-box';
|
||||||
|
import { TableFieldPopover } from './table-field-modal/table-field-modal';
|
||||||
|
|
||||||
export interface TableFieldProps {
|
export interface TableFieldProps {
|
||||||
field: DBField;
|
field: DBField;
|
||||||
@@ -39,13 +36,55 @@ export const TableField: React.FC<TableFieldProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const { databaseType } = useChartDB();
|
const { databaseType } = useChartDB();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const { attributes, listeners, setNodeRef, transform, transition } =
|
const { attributes, listeners, setNodeRef, transform, transition } =
|
||||||
useSortable({ id: field.id });
|
useSortable({ id: field.id });
|
||||||
|
|
||||||
const dataFieldOptions = dataTypeMap[databaseType].map((type) => ({
|
const dataFieldOptions: SelectBoxOption[] = dataTypeMap[databaseType].map(
|
||||||
|
(type) => ({
|
||||||
label: type.name,
|
label: type.name,
|
||||||
value: type.id,
|
value: type.id,
|
||||||
}));
|
regex: type.hasCharMaxLength
|
||||||
|
? `^${type.name}\\(\\d+\\)$`
|
||||||
|
: undefined,
|
||||||
|
extractRegex: type.hasCharMaxLength ? /\((\d+)\)/ : undefined,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const onChangeDataType = useCallback<
|
||||||
|
NonNullable<SelectBoxProps['onChange']>
|
||||||
|
>(
|
||||||
|
(value, regexMatches) => {
|
||||||
|
const dataType = dataTypeMap[databaseType].find(
|
||||||
|
(v) => v.id === value
|
||||||
|
) ?? {
|
||||||
|
id: value as string,
|
||||||
|
name: value as string,
|
||||||
|
};
|
||||||
|
|
||||||
|
let characterMaximumLength: string | undefined = undefined;
|
||||||
|
|
||||||
|
if (regexMatches?.length && dataType?.hasCharMaxLength) {
|
||||||
|
characterMaximumLength = regexMatches[1];
|
||||||
|
} else if (
|
||||||
|
field.characterMaximumLength &&
|
||||||
|
dataType?.hasCharMaxLength
|
||||||
|
) {
|
||||||
|
characterMaximumLength = field.characterMaximumLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateField({
|
||||||
|
characterMaximumLength,
|
||||||
|
type: dataTypeDataToDataType(
|
||||||
|
dataType ?? {
|
||||||
|
id: value as string,
|
||||||
|
name: value as string,
|
||||||
|
}
|
||||||
|
),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[updateField, databaseType, field.characterMaximumLength]
|
||||||
|
);
|
||||||
|
|
||||||
const style = {
|
const style = {
|
||||||
transform: CSS.Translate.toString(transform),
|
transform: CSS.Translate.toString(transform),
|
||||||
@@ -96,20 +135,39 @@ export const TableField: React.FC<TableFieldProps> = ({
|
|||||||
'side_panel.tables_section.table.field_type'
|
'side_panel.tables_section.table.field_type'
|
||||||
)}
|
)}
|
||||||
value={field.type.id}
|
value={field.type.id}
|
||||||
onChange={(value) =>
|
valueSuffix={
|
||||||
updateField({
|
field.characterMaximumLength
|
||||||
type: dataTypeMap[databaseType].find(
|
? `(${field.characterMaximumLength})`
|
||||||
(v) => v.id === value
|
: ''
|
||||||
),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
optionSuffix={(option) => {
|
||||||
|
const type = dataTypeMap[databaseType].find(
|
||||||
|
(v) => v.id === option.value
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!type) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type.hasCharMaxLength) {
|
||||||
|
return `(${!field.characterMaximumLength ? 'n' : field.characterMaximumLength})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}}
|
||||||
|
onChange={onChangeDataType}
|
||||||
emptyPlaceholder={t(
|
emptyPlaceholder={t(
|
||||||
'side_panel.tables_section.table.no_types_found'
|
'side_panel.tables_section.table.no_types_found'
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>{field.type.name}</TooltipContent>
|
<TooltipContent>
|
||||||
|
{field.type.name}
|
||||||
|
{field.characterMaximumLength
|
||||||
|
? `(${field.characterMaximumLength})`
|
||||||
|
: ''}
|
||||||
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex w-4/12 justify-end gap-1 overflow-hidden">
|
<div className="flex w-4/12 justify-end gap-1 overflow-hidden">
|
||||||
@@ -152,81 +210,12 @@ export const TableField: React.FC<TableFieldProps> = ({
|
|||||||
{t('side_panel.tables_section.table.primary_key')}
|
{t('side_panel.tables_section.table.primary_key')}
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Popover>
|
<TableFieldPopover
|
||||||
<PopoverTrigger asChild>
|
field={field}
|
||||||
<Button
|
updateField={updateField}
|
||||||
variant="ghost"
|
removeField={removeField}
|
||||||
className="h-8 w-[32px] p-2 text-slate-500 hover:bg-primary-foreground hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-200"
|
|
||||||
>
|
|
||||||
<Ellipsis className="size-3.5" />
|
|
||||||
</Button>
|
|
||||||
</PopoverTrigger>
|
|
||||||
<PopoverContent className="w-52">
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
<div className="text-sm font-semibold">
|
|
||||||
{t(
|
|
||||||
'side_panel.tables_section.table.field_actions.title'
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<Separator orientation="horizontal" />
|
|
||||||
<div className="flex flex-col gap-3">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<Label
|
|
||||||
htmlFor="width"
|
|
||||||
className="text-subtitle"
|
|
||||||
>
|
|
||||||
{t(
|
|
||||||
'side_panel.tables_section.table.field_actions.unique'
|
|
||||||
)}
|
|
||||||
</Label>
|
|
||||||
<Checkbox
|
|
||||||
checked={field.unique}
|
|
||||||
disabled={field.primaryKey}
|
|
||||||
onCheckedChange={(value) =>
|
|
||||||
updateField({
|
|
||||||
unique: !!value,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
<Label
|
|
||||||
htmlFor="width"
|
|
||||||
className="text-subtitle"
|
|
||||||
>
|
|
||||||
{t(
|
|
||||||
'side_panel.tables_section.table.field_actions.comments'
|
|
||||||
)}
|
|
||||||
</Label>
|
|
||||||
<Textarea
|
|
||||||
value={field.comments}
|
|
||||||
onChange={(e) =>
|
|
||||||
updateField({
|
|
||||||
comments: e.target.value,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
placeholder={t(
|
|
||||||
'side_panel.tables_section.table.field_actions.no_comments'
|
|
||||||
)}
|
|
||||||
className="w-full rounded-md bg-muted text-sm"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Separator orientation="horizontal" />
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
className="flex gap-2 !text-red-700"
|
|
||||||
onClick={removeField}
|
|
||||||
>
|
|
||||||
<Trash2 className="size-3.5 text-red-700" />
|
|
||||||
{t(
|
|
||||||
'side_panel.tables_section.table.field_actions.delete_field'
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</PopoverContent>
|
|
||||||
</Popover>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -470,8 +470,16 @@ export const Menu: React.FC<MenuProps> = () => {
|
|||||||
</MenubarSub>
|
</MenubarSub>
|
||||||
<MenubarSeparator />
|
<MenubarSeparator />
|
||||||
<MenubarSub>
|
<MenubarSub>
|
||||||
<MenubarSubTrigger>
|
<MenubarSubTrigger className="flex items-center gap-1">
|
||||||
{t('menu.view.theme')}
|
<span>{t('menu.view.theme')}</span>
|
||||||
|
<div className="flex-1" />
|
||||||
|
<MenubarShortcut>
|
||||||
|
{
|
||||||
|
keyboardShortcutsForOS[
|
||||||
|
KeyboardShortcutAction.TOGGLE_THEME
|
||||||
|
].keyCombinationLabel
|
||||||
|
}
|
||||||
|
</MenubarShortcut>
|
||||||
</MenubarSubTrigger>
|
</MenubarSubTrigger>
|
||||||
<MenubarSubContent>
|
<MenubarSubContent>
|
||||||
<MenubarCheckboxItem
|
<MenubarCheckboxItem
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import { DiagramName } from './diagram-name';
|
|||||||
import { LastSaved } from './last-saved';
|
import { LastSaved } from './last-saved';
|
||||||
import { LanguageNav } from './language-nav/language-nav';
|
import { LanguageNav } from './language-nav/language-nav';
|
||||||
import { Menu } from './menu/menu';
|
import { Menu } from './menu/menu';
|
||||||
import { HIDE_BUCKLE_DOT_DEV } from '@/lib/env';
|
|
||||||
|
|
||||||
export interface TopNavbarProps {}
|
export interface TopNavbarProps {}
|
||||||
|
|
||||||
@@ -26,27 +25,6 @@ export const TopNavbar: React.FC<TopNavbarProps> = () => {
|
|||||||
);
|
);
|
||||||
}, [isDesktop]);
|
}, [isDesktop]);
|
||||||
|
|
||||||
const openBuckleWaitlist = useCallback(() => {
|
|
||||||
window.open('https://waitlist.buckle.dev', '_blank');
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const renderGetBuckleButton = useCallback(() => {
|
|
||||||
if (HIDE_BUCKLE_DOT_DEV) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
className="gradient-background relative inline-flex items-center justify-center overflow-hidden rounded-lg p-0.5 text-base text-gray-700 focus:outline-none focus:ring-0"
|
|
||||||
onClick={openBuckleWaitlist}
|
|
||||||
>
|
|
||||||
<span className="relative inline-flex items-center justify-center whitespace-nowrap rounded-md bg-background px-2 py-0.5 font-primary text-xs font-semibold text-foreground md:text-sm">
|
|
||||||
ChartDB v2.0 🔥
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
}, [openBuckleWaitlist]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav className="flex flex-col justify-between border-b px-3 md:h-12 md:flex-row md:items-center md:px-4">
|
<nav className="flex flex-col justify-between border-b px-3 md:h-12 md:flex-row md:items-center md:px-4">
|
||||||
<div className="flex flex-1 flex-col justify-between gap-x-1 md:flex-row md:justify-normal">
|
<div className="flex flex-1 flex-col justify-between gap-x-1 md:flex-row md:justify-normal">
|
||||||
@@ -68,7 +46,6 @@ export const TopNavbar: React.FC<TopNavbarProps> = () => {
|
|||||||
</a>
|
</a>
|
||||||
{!isDesktop ? (
|
{!isDesktop ? (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{renderGetBuckleButton()}
|
|
||||||
{renderStars()}
|
{renderStars()}
|
||||||
<LanguageNav />
|
<LanguageNav />
|
||||||
</div>
|
</div>
|
||||||
@@ -80,7 +57,6 @@ export const TopNavbar: React.FC<TopNavbarProps> = () => {
|
|||||||
<>
|
<>
|
||||||
<DiagramName />
|
<DiagramName />
|
||||||
<div className="hidden flex-1 items-center justify-end gap-2 sm:flex">
|
<div className="hidden flex-1 items-center justify-end gap-2 sm:flex">
|
||||||
{renderGetBuckleButton()}
|
|
||||||
<LastSaved />
|
<LastSaved />
|
||||||
{renderStars()}
|
{renderStars()}
|
||||||
<LanguageNav />
|
<LanguageNav />
|
||||||
|
|||||||
@@ -289,7 +289,6 @@ const TemplatePageComponent: React.FC = () => {
|
|||||||
readonly
|
readonly
|
||||||
>
|
>
|
||||||
<Canvas
|
<Canvas
|
||||||
readonly
|
|
||||||
initialTables={
|
initialTables={
|
||||||
template.diagram.tables ?? []
|
template.diagram.tables ?? []
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user