Compare commits

...

9 Commits

Author SHA1 Message Date
Guy Ben-Aharon
44eac7daff chore(main): release 1.9.0 (#607) 2025-03-13 12:20:07 +02:00
Guy Ben-Aharon
502472b083 fix: remove Buckle dialog (#617) 2025-03-13 11:32:54 +02:00
Guy Ben-Aharon
52d2ea596c add diff mode (#614)
* initial diff viewer

* test

* test

* name change

* new table show

* diff viewer
2025-03-13 11:12:11 +02:00
Guy Ben-Aharon
bd67ccfbcf feat(chart max length): enable edit length from data type select box (#616)
* feat(chart max length): enable edit length from data type select box

* fix
2025-03-11 18:49:10 +02:00
Jonathan Fishner
62beb68fa1 feat(canvas): highlight the Show-All button when No-Tables are visible in the canvas (#612)
* feat(canvas): highlight the Show-All button when No-Tables are visible in the canvas

* fix

---------

Co-authored-by: Guy Ben-Aharon <baguy3@gmail.com>
2025-03-10 15:51:33 +02:00
Guy Ben-Aharon
09b1275475 feat(chart max length): add support for edit char max length (#613)
* feat(chart max length): add support for edit char max length

* fix

* update datatypes for max chars

---------

Co-authored-by: johnnyfish <jonathanfishner11@gmail.com>
2025-03-10 13:25:55 +02:00
Guy Ben-Aharon
5dd7fe75d1 fix(performance): Optimize performance of field comments editing (#610) 2025-03-05 18:20:04 +02:00
Jonathan Fishner
2939320a15 fix(cardinality): set true as default (#583) 2025-03-04 11:07:49 +02:00
Jonathan Fishner
a643852837 fix(shorcuts): add shortcut to toggle the theme (#602) 2025-03-04 10:28:30 +02:00
63 changed files with 1948 additions and 424 deletions

View File

@@ -1,5 +1,22 @@
# 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)

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "chartdb",
"version": "1.8.1",
"version": "1.9.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "chartdb",
"version": "1.8.1",
"version": "1.9.0",
"dependencies": {
"@ai-sdk/openai": "^0.0.51",
"@dbml/core": "^3.9.5",

View File

@@ -1,7 +1,7 @@
{
"name": "chartdb",
"private": true,
"version": "1.8.1",
"version": "1.9.0",
"type": "module",
"scripts": {
"dev": "vite",

View File

@@ -24,12 +24,19 @@ export interface SelectBoxOption {
value: string;
label: string;
description?: string;
regex?: string;
extractRegex?: RegExp;
}
export interface SelectBoxProps {
options: SelectBoxOption[];
value?: string[] | string;
onChange?: (values: string[] | string) => void;
valueSuffix?: string;
optionSuffix?: (option: SelectBoxOption) => string;
onChange?: (
values: string[] | string,
regexMatches?: string[] | string
) => void;
placeholder?: string;
inputPlaceholder?: string;
emptyPlaceholder?: string;
@@ -55,10 +62,12 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
className,
options,
value,
valueSuffix,
onChange,
multiple,
oneLine,
selectAll,
optionSuffix,
deselectAll,
clearText,
showClear,
@@ -86,7 +95,7 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
);
const handleSelect = React.useCallback(
(selectedValue: string) => {
(selectedValue: string, regexMatches?: string[]) => {
if (multiple) {
const newValue =
value?.includes(selectedValue) && Array.isArray(value)
@@ -94,7 +103,7 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
: [...(value ?? []), selectedValue];
onChange?.(newValue);
} else {
onChange?.(selectedValue);
onChange?.(selectedValue, regexMatches);
setIsOpen(false);
}
},
@@ -199,6 +208,7 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
(opt) => opt.value === value
)?.label
}
{valueSuffix ? valueSuffix : ''}
</div>
)
) : (
@@ -239,11 +249,22 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
align="center"
>
<Command
filter={(value, search) =>
value.toLowerCase().includes(search.toLowerCase())
filter={(value, search, keywords) => {
if (
keywords?.length &&
keywords.some((keyword) =>
new RegExp(keyword).test(search)
)
) {
return 1;
}
return value
.toLowerCase()
.includes(search.toLowerCase())
? 1
: 0
}
: 0;
}}
>
<div className="relative">
<CommandInput
@@ -302,14 +323,36 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
const isSelected =
Array.isArray(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 (
<CommandItem
className="flex items-center"
key={option.value}
keywords={
option.regex
? [option.regex]
: undefined
}
// value={option.value}
onSelect={() =>
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">
<span>
{option.label}
{isRegexMatch
? searchTerm
: option.label}
{!isRegexMatch &&
optionSuffix
? optionSuffix(
option
)
: ''}
</span>
{option.description && (
<span className="ml-1 text-xs text-muted-foreground">
@@ -337,19 +388,20 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
</span>
)}
</div>
{!multiple &&
{((!multiple &&
option.value ===
value && (
<CheckIcon
className={cn(
'ml-auto',
option.value ===
value
? 'opacity-100'
: 'opacity-0'
)}
/>
)}
value) ||
isRegexMatch) && (
<CheckIcon
className={cn(
'ml-auto',
option.value ===
value
? 'opacity-100'
: 'opacity-0'
)}
/>
)}
</CommandItem>
);
})}

View File

@@ -22,6 +22,8 @@ import { defaultSchemas } from '@/lib/data/default-schemas';
import { useEventEmitter } from 'ahooks';
import type { DBDependency } from '@/lib/domain/db-dependency';
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 {
diagram?: Diagram;
@@ -30,7 +32,8 @@ export interface ChartDBProviderProps {
export const ChartDBProvider: React.FC<
React.PropsWithChildren<ChartDBProviderProps>
> = ({ children, diagram, readonly }) => {
> = ({ children, diagram, readonly: readonlyProp }) => {
const { hasDiff } = useDiff();
let db = useStorage();
const events = useEventEmitter<ChartDBEvent>();
const { setSchemasFilter, schemasFilter } = useLocalConfig();
@@ -53,9 +56,33 @@ export const ChartDBProvider: React.FC<
const [dependencies, setDependencies] = useState<DBDependency[]>(
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 readonly = useMemo(
() => readonlyProp ?? hasDiff ?? false,
[readonlyProp, hasDiff]
);
if (readonly) {
db = storageInitialValue;
}

View File

@@ -47,10 +47,6 @@ export interface DialogContext {
openStarUsDialog: () => void;
closeStarUsDialog: () => void;
// Buckle dialog
openBuckleDialog: () => void;
closeBuckleDialog: () => void;
// Export image dialog
openExportImageDialog: (
params: Omit<ExportImageDialogProps, 'dialog'>
@@ -97,8 +93,6 @@ export const dialogContext = createContext<DialogContext>({
closeExportDiagramDialog: emptyFn,
openImportDiagramDialog: emptyFn,
closeImportDiagramDialog: emptyFn,
openBuckleDialog: emptyFn,
closeBuckleDialog: emptyFn,
openImportDBMLDialog: emptyFn,
closeImportDBMLDialog: emptyFn,
});

View File

@@ -19,7 +19,6 @@ import type { ExportImageDialogProps } from '@/dialogs/export-image-dialog/expor
import { ExportImageDialog } from '@/dialogs/export-image-dialog/export-image-dialog';
import { ExportDiagramDialog } from '@/dialogs/export-diagram-dialog/export-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 { 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 [openBuckleDialog, setOpenBuckleDialog] = useState(false);
// Export image dialog
const [openExportImageDialog, setOpenExportImageDialog] = useState(false);
@@ -147,8 +145,6 @@ export const DialogProvider: React.FC<React.PropsWithChildren> = ({
closeTableSchemaDialog: () => setOpenTableSchemaDialog(false),
openStarUsDialog: () => setOpenStarUsDialog(true),
closeStarUsDialog: () => setOpenStarUsDialog(false),
closeBuckleDialog: () => setOpenBuckleDialog(false),
openBuckleDialog: () => setOpenBuckleDialog(true),
closeExportImageDialog: () => setOpenExportImageDialog(false),
openExportImageDialog: openExportImageDialogHandler,
openExportDiagramDialog: () => setOpenExportDiagramDialog(true),
@@ -193,7 +189,6 @@ export const DialogProvider: React.FC<React.PropsWithChildren> = ({
/>
<ExportDiagramDialog dialog={{ open: openExportDiagramDialog }} />
<ImportDiagramDialog dialog={{ open: openImportDiagramDialog }} />
<BuckleDialog dialog={{ open: openBuckleDialog }} />
<ImportDBMLDialog
dialog={{ open: openImportDBMLDialog }}
{...importDBMLDialogParams}

View 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,
}
);
}
}
}

View 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);

View 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>
);
};

View 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'];

View 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;
};

View File

@@ -7,6 +7,7 @@ export enum KeyboardShortcutAction {
SAVE_DIAGRAM = 'save_diagram',
TOGGLE_SIDE_PANEL = 'toggle_side_panel',
SHOW_ALL = 'show_all',
TOGGLE_THEME = 'toggle_theme',
}
export interface KeyboardShortcut {
@@ -63,6 +64,13 @@ export const keyboardShortcuts: Record<
keyCombinationMac: 'meta+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 {

View File

@@ -30,12 +30,6 @@ export interface LocalConfigContext {
starUsDialogLastOpen: number;
setStarUsDialogLastOpen: (lastOpen: number) => void;
buckleWaitlistOpened: boolean;
setBuckleWaitlistOpened: (githubRepoOpened: boolean) => void;
buckleDialogLastOpen: number;
setBuckleDialogLastOpen: (lastOpen: number) => void;
showDependenciesOnCanvas: boolean;
setShowDependenciesOnCanvas: (showDependenciesOnCanvas: boolean) => void;
@@ -53,7 +47,7 @@ export const LocalConfigContext = createContext<LocalConfigContext>({
schemasFilter: {},
setSchemasFilter: emptyFn,
showCardinality: false,
showCardinality: true,
setShowCardinality: emptyFn,
hideMultiSchemaNotification: false,
@@ -65,12 +59,6 @@ export const LocalConfigContext = createContext<LocalConfigContext>({
starUsDialogLastOpen: 0,
setStarUsDialogLastOpen: emptyFn,
buckleWaitlistOpened: false,
setBuckleWaitlistOpened: emptyFn,
buckleDialogLastOpen: 0,
setBuckleDialogLastOpen: emptyFn,
showDependenciesOnCanvas: false,
setShowDependenciesOnCanvas: emptyFn,

View File

@@ -10,8 +10,6 @@ const showCardinalityKey = 'show_cardinality';
const hideMultiSchemaNotificationKey = 'hide_multi_schema_notification';
const githubRepoOpenedKey = 'github_repo_opened';
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 showMiniMapOnCanvasKey = 'show_minimap_on_canvas';
@@ -33,7 +31,7 @@ export const LocalConfigProvider: React.FC<React.PropsWithChildren> = ({
);
const [showCardinality, setShowCardinality] = React.useState<boolean>(
(localStorage.getItem(showCardinalityKey) || 'false') === 'true'
(localStorage.getItem(showCardinalityKey) || 'true') === 'true'
);
const [hideMultiSchemaNotification, setHideMultiSchemaNotification] =
@@ -51,17 +49,6 @@ export const LocalConfigProvider: React.FC<React.PropsWithChildren> = ({
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] =
React.useState<boolean>(
(localStorage.getItem(showDependenciesOnCanvasKey) || 'false') ===
@@ -84,20 +71,6 @@ export const LocalConfigProvider: React.FC<React.PropsWithChildren> = ({
localStorage.setItem(githubRepoOpenedKey, githubRepoOpened.toString());
}, [githubRepoOpened]);
useEffect(() => {
localStorage.setItem(
buckleDialogLastOpenKey,
buckleDialogLastOpen.toString()
);
}, [buckleDialogLastOpen]);
useEffect(() => {
localStorage.setItem(
buckleWaitlistOpenedKey,
buckleWaitlistOpened.toString()
);
}, [buckleWaitlistOpened]);
useEffect(() => {
localStorage.setItem(
hideMultiSchemaNotificationKey,
@@ -154,10 +127,6 @@ export const LocalConfigProvider: React.FC<React.PropsWithChildren> = ({
setStarUsDialogLastOpen,
showDependenciesOnCanvas,
setShowDependenciesOnCanvas,
setBuckleDialogLastOpen,
buckleDialogLastOpen,
buckleWaitlistOpened,
setBuckleWaitlistOpened,
showMiniMapOnCanvas,
setShowMiniMapOnCanvas,
}}

View File

@@ -1,8 +1,13 @@
import React, { useEffect, useState } from 'react';
import React, { useEffect, useState, useCallback } from 'react';
import type { EffectiveTheme } from './theme-context';
import { ThemeContext } from './theme-context';
import { useMediaQuery } from 'react-responsive';
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> = ({
children,
@@ -29,6 +34,24 @@ export const ThemeProvider: React.FC<React.PropsWithChildren> = ({
}
}, [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 (
<ThemeContext.Provider value={{ theme, setTheme, effectiveTheme }}>
{children}

View File

@@ -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>
);
};

View 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;
}

View File

@@ -151,6 +151,8 @@ export const ar: LanguageTranslation = {
comments: 'تعليقات',
no_comments: 'لا يوجد تعليقات',
delete_field: 'حذف الحقل',
// TODO: Translate
character_length: 'Max Length',
},
index_actions: {
title: 'خصائص الفهرس',

View File

@@ -152,6 +152,8 @@ export const bn: LanguageTranslation = {
comments: 'মন্তব্য',
no_comments: 'কোনো মন্তব্য নেই',
delete_field: 'ফিল্ড মুছুন',
// TODO: Translate
character_length: 'Max Length',
},
index_actions: {
title: 'ইনডেক্স কর্ম',

View File

@@ -153,6 +153,8 @@ export const de: LanguageTranslation = {
comments: 'Kommentare',
no_comments: 'Keine Kommentare',
delete_field: 'Feld löschen',
// TODO: Translate
character_length: 'Max Length',
},
index_actions: {
title: 'Indexattribute',

View File

@@ -145,6 +145,7 @@ export const en = {
field_actions: {
title: 'Field Attributes',
unique: 'Unique',
character_length: 'Max Length',
comments: 'Comments',
no_comments: 'No comments',
delete_field: 'Delete Field',

View File

@@ -142,6 +142,8 @@ export const es: LanguageTranslation = {
comments: 'Comentarios',
no_comments: 'Sin comentarios',
delete_field: 'Eliminar Campo',
// TODO: Translate
character_length: 'Max Length',
},
index_actions: {
title: 'Atributos del Índice',

View File

@@ -140,6 +140,8 @@ export const fr: LanguageTranslation = {
comments: 'Commentaires',
no_comments: 'Pas de commentaires',
delete_field: 'Supprimer le Champ',
// TODO: Translate
character_length: 'Max Length',
},
index_actions: {
title: "Attributs de l'Index",

View File

@@ -153,6 +153,8 @@ export const gu: LanguageTranslation = {
comments: 'ટિપ્પણીઓ',
no_comments: 'કોઈ ટિપ્પણીઓ નથી',
delete_field: 'ફીલ્ડ કાઢી નાખો',
// TODO: Translate
character_length: 'Max Length',
},
index_actions: {
title: 'ઇન્ડેક્સ લક્ષણો',

View File

@@ -152,6 +152,8 @@ export const hi: LanguageTranslation = {
comments: 'टिप्पणियाँ',
no_comments: 'कोई टिप्पणी नहीं',
delete_field: 'फ़ील्ड हटाएँ',
// TODO: Translate
character_length: 'Max Length',
},
index_actions: {
title: 'सूचकांक विशेषताएँ',

View File

@@ -151,6 +151,8 @@ export const id_ID: LanguageTranslation = {
comments: 'Komentar',
no_comments: 'Tidak ada komentar',
delete_field: 'Hapus Kolom',
// TODO: Translate
character_length: 'Max Length',
},
index_actions: {
title: 'Atribut Indeks',

View File

@@ -155,6 +155,8 @@ export const ja: LanguageTranslation = {
comments: 'コメント',
no_comments: 'コメントがありません',
delete_field: 'フィールドを削除',
// TODO: Translate
character_length: 'Max Length',
},
index_actions: {
title: 'インデックス属性',

View File

@@ -151,6 +151,8 @@ export const ko_KR: LanguageTranslation = {
comments: '주석',
no_comments: '주석 없음',
delete_field: '필드 삭제',
// TODO: Translate
character_length: 'Max Length',
},
index_actions: {
title: '인덱스 속성',

View File

@@ -154,6 +154,8 @@ export const mr: LanguageTranslation = {
comments: 'टिप्पण्या',
no_comments: 'कोणत्याही टिप्पणी नाहीत',
delete_field: 'फील्ड हटवा',
// TODO: Translate
character_length: 'Max Length',
},
index_actions: {
title: 'इंडेक्स गुणधर्म',

View File

@@ -152,6 +152,8 @@ export const ne: LanguageTranslation = {
comments: 'टिप्पणीहरू',
no_comments: 'कुनै टिप्पणीहरू छैनन्',
delete_field: 'क्षेत्र हटाउनुहोस्',
// TODO: Translate
character_length: 'Max Length',
},
index_actions: {
title: 'सूचक विशेषताहरू',

View File

@@ -152,6 +152,8 @@ export const pt_BR: LanguageTranslation = {
comments: 'Comentários',
no_comments: 'Sem comentários',
delete_field: 'Excluir Campo',
// TODO: Translate
character_length: 'Max Length',
},
index_actions: {
title: 'Atributos do Índice',

View File

@@ -151,6 +151,8 @@ export const ru: LanguageTranslation = {
comments: 'Комментарии',
no_comments: 'Нет комментария',
delete_field: 'Удалить поле',
// TODO: Translate
character_length: 'Max Length',
},
index_actions: {
title: 'Атрибуты индекса',

View File

@@ -152,6 +152,8 @@ export const te: LanguageTranslation = {
comments: 'వ్యాఖ్యలు',
no_comments: 'వ్యాఖ్యలు లేవు',
delete_field: 'ఫీల్డ్ తొలగించు',
// TODO: Translate
character_length: 'Max Length',
},
index_actions: {
title: 'ఇండెక్స్ గుణాలు',

View File

@@ -151,6 +151,8 @@ export const tr: LanguageTranslation = {
comments: 'Yorumlar',
no_comments: 'Yorum yok',
delete_field: 'Alanı Sil',
// TODO: Translate
character_length: 'Max Length',
},
index_actions: {
title: 'İndeks Özellikleri',

View File

@@ -150,6 +150,8 @@ export const uk: LanguageTranslation = {
comments: 'Коментарі',
no_comments: 'Немає коментарів',
delete_field: 'Видалити поле',
// TODO: Translate
character_length: 'Max Length',
},
index_actions: {
title: 'Атрибути індексу',

View File

@@ -151,6 +151,8 @@ export const vi: LanguageTranslation = {
comments: 'Bình luận',
no_comments: 'Không có bình luận',
delete_field: 'Xóa trường',
// TODO: Translate
character_length: 'Max Length',
},
index_actions: {
title: 'Thuộc tính chỉ mục',

View File

@@ -148,6 +148,8 @@ export const zh_CN: LanguageTranslation = {
comments: '注释',
no_comments: '空',
delete_field: '删除字段',
// TODO: Translate
character_length: 'Max Length',
},
index_actions: {
title: '索引属性',

View File

@@ -148,6 +148,8 @@ export const zh_TW: LanguageTranslation = {
comments: '註解',
no_comments: '無註解',
delete_field: '刪除欄位',
// TODO: Translate
character_length: 'Max Length',
},
index_actions: {
title: '索引屬性',

View File

@@ -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
{ name: 'uint8', id: 'uint8' },
{ name: 'uint16', id: 'uint16' },
@@ -48,25 +48,41 @@ export const clickhouseDataTypes: readonly DataType[] = [
{ name: 'mediumblob', id: 'mediumblob' },
{ name: 'tinyblob', id: 'tinyblob' },
{ name: 'blob', id: 'blob' },
{ name: 'varchar', id: 'varchar' },
{ name: 'char', id: 'char' },
{ name: 'varchar', id: 'varchar', hasCharMaxLength: true },
{ name: 'char', id: 'char', hasCharMaxLength: true },
{ 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 varying', id: 'character_varying' },
{
name: 'character varying',
id: 'character_varying',
hasCharMaxLength: true,
},
{ 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',
id: 'national_character_large_object',
},
{ name: 'national character varying', id: 'national_character_varying' },
{ name: 'national char varying', id: 'national_char_varying' },
{ name: 'national character', id: 'national_character' },
{ name: 'national char', id: 'national_char' },
{
name: 'national character varying',
id: 'national_character_varying',
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 varying', id: 'binary_varying' },
{ name: 'fixedstring', id: 'fixedstring' },
{ name: 'binary varying', id: 'binary_varying', hasCharMaxLength: true },
{ name: 'fixedstring', id: 'fixedstring', hasCharMaxLength: true },
{ name: 'string', id: 'string' },
// Date Types

View File

@@ -13,12 +13,16 @@ export interface DataType {
name: string;
}
export interface DataTypeData extends DataType {
hasCharMaxLength?: boolean;
}
export const dataTypeSchema: z.ZodType<DataType> = z.object({
id: z.string(),
name: z.string(),
});
export const dataTypeMap: Record<DatabaseType, readonly DataType[]> = {
export const dataTypeMap: Record<DatabaseType, readonly DataTypeData[]> = {
[DatabaseType.GENERIC]: genericDataTypes,
[DatabaseType.POSTGRESQL]: postgresDataTypes,
[DatabaseType.MYSQL]: mysqlDataTypes,
@@ -64,3 +68,21 @@ export function areFieldTypesCompatible(
}
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);
};

View File

@@ -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: 'binary', id: 'binary' },
{ name: 'binary', id: 'binary', hasCharMaxLength: true },
{ name: 'blob', id: 'blob' },
{ name: 'boolean', id: 'boolean' },
{ name: 'char', id: 'char' },
{ name: 'char', id: 'char', hasCharMaxLength: true },
{ name: 'date', id: 'date' },
{ name: 'datetime', id: 'datetime' },
{ name: 'decimal', id: 'decimal' },
@@ -22,6 +22,6 @@ export const genericDataTypes: readonly DataType[] = [
{ name: 'time', id: 'time' },
{ name: 'timestamp', id: 'timestamp' },
{ name: 'uuid', id: 'uuid' },
{ name: 'varbinary', id: 'varbinary' },
{ name: 'varchar', id: 'varchar' },
{ name: 'varbinary', id: 'varbinary', hasCharMaxLength: true },
{ name: 'varchar', id: 'varchar', hasCharMaxLength: true },
] as const;

View File

@@ -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
{ name: 'tinyint', id: 'tinyint' },
{ name: 'smallint', id: 'smallint' },
@@ -23,10 +23,10 @@ export const mariadbDataTypes: readonly DataType[] = [
{ name: 'year', id: 'year' },
// String Types
{ name: 'char', id: 'char' },
{ name: 'varchar', id: 'varchar' },
{ name: 'binary', id: 'binary' },
{ name: 'varbinary', id: 'varbinary' },
{ name: 'char', id: 'char', hasCharMaxLength: true },
{ name: 'varchar', id: 'varchar', hasCharMaxLength: true },
{ name: 'binary', id: 'binary', hasCharMaxLength: true },
{ name: 'varbinary', id: 'varbinary', hasCharMaxLength: true },
{ name: 'tinyblob', id: 'tinyblob' },
{ name: 'blob', id: 'blob' },
{ name: 'mediumblob', id: 'mediumblob' },

View File

@@ -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
{ name: 'tinyint', id: 'tinyint' },
{ name: 'smallint', id: 'smallint' },
@@ -23,10 +23,10 @@ export const mysqlDataTypes: readonly DataType[] = [
{ name: 'year', id: 'year' },
// String Types
{ name: 'char', id: 'char' },
{ name: 'varchar', id: 'varchar' },
{ name: 'binary', id: 'binary' },
{ name: 'varbinary', id: 'varbinary' },
{ name: 'char', id: 'char', hasCharMaxLength: true },
{ name: 'varchar', id: 'varchar', hasCharMaxLength: true },
{ name: 'binary', id: 'binary', hasCharMaxLength: true },
{ name: 'varbinary', id: 'varbinary', hasCharMaxLength: true },
{ name: 'tinyblob', id: 'tinyblob' },
{ name: 'blob', id: 'blob' },
{ name: 'mediumblob', id: 'mediumblob' },

View File

@@ -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
{ name: 'smallint', id: 'smallint' },
{ name: 'integer', id: 'integer' },
@@ -15,9 +15,13 @@ export const postgresDataTypes: readonly DataType[] = [
{ name: 'money', id: 'money' },
// Character Types
{ name: 'char', id: 'char' },
{ name: 'varchar', id: 'varchar' },
{ name: 'character varying', id: 'character_varying' },
{ name: 'char', id: 'char', hasCharMaxLength: true },
{ name: 'varchar', id: 'varchar', hasCharMaxLength: true },
{
name: 'character varying',
id: 'character_varying',
hasCharMaxLength: true,
},
{ name: 'text', id: 'text' },
// Binary Data Types

View File

@@ -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
{ name: 'bigint', id: 'bigint' },
{ name: 'bit', id: 'bit' },
@@ -25,18 +25,18 @@ export const sqlServerDataTypes: readonly DataType[] = [
{ name: 'time', id: 'time' },
// Character Strings
{ name: 'char', id: 'char' },
{ name: 'varchar', id: 'varchar' },
{ name: 'char', id: 'char', hasCharMaxLength: true },
{ name: 'varchar', id: 'varchar', hasCharMaxLength: true },
{ name: 'text', id: 'text' },
// Unicode Character Strings
{ name: 'nchar', id: 'nchar' },
{ name: 'nvarchar', id: 'nvarchar' },
{ name: 'nchar', id: 'nchar', hasCharMaxLength: true },
{ name: 'nvarchar', id: 'nvarchar', hasCharMaxLength: true },
{ name: 'ntext', id: 'ntext' },
// Binary Strings
{ name: 'binary', id: 'binary' },
{ name: 'varbinary', id: 'varbinary' },
{ name: 'binary', id: 'binary', hasCharMaxLength: true },
{ name: 'varbinary', id: 'varbinary', hasCharMaxLength: true },
{ name: 'image', id: 'image' },
// Other Data Types

View File

@@ -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
{ name: 'integer', id: 'integer' },
{ name: 'real', id: 'real' },
@@ -22,6 +22,6 @@ export const sqliteDataTypes: readonly DataType[] = [
{ name: 'int', id: 'int' },
{ name: 'float', id: 'float' },
{ name: 'boolean', id: 'boolean' },
{ name: 'varchar', id: 'varchar' },
{ name: 'varchar', id: 'varchar', hasCharMaxLength: true },
{ name: 'decimal', id: 'decimal' },
] as const;

View File

@@ -115,8 +115,22 @@ export const exportBaseSQL = (diagram: Diagram): string => {
// Remove the type cast part after :: if it exists
if (fieldDefault.includes('::')) {
const endedWithParentheses = fieldDefault.endsWith(')');
fieldDefault = fieldDefault.split('::')[0];
if (
(fieldDefault.startsWith('(') &&
!fieldDefault.endsWith(')')) ||
endedWithParentheses
) {
fieldDefault += ')';
}
}
if (fieldDefault === `('now')`) {
fieldDefault = `now()`;
}
sqlScript += ` DEFAULT ${fieldDefault}`;
}

View File

@@ -95,7 +95,7 @@ export const createFieldsFromMetadata = ({
nullable: col.nullable,
...(col.character_maximum_length &&
col.character_maximum_length !== 'null'
? { character_maximum_length: col.character_maximum_length }
? { characterMaximumLength: col.character_maximum_length }
: {}),
...(col.precision?.precision
? { precision: col.precision.precision }

View File

@@ -103,10 +103,9 @@ const tableToTableNode = (
export interface CanvasProps {
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 [selectedTableIds, setSelectedTableIds] = useState<string[]>([]);
const [selectedRelationshipIds, setSelectedRelationshipIds] = useState<
@@ -127,6 +126,7 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables, readonly }) => {
filteredSchemas,
events,
dependencies,
readonly,
} = useChartDB();
const { showSidePanel } = useLayout();
const { effectiveTheme } = useTheme();
@@ -682,7 +682,7 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables, readonly }) => {
return (
<CanvasContextMenu>
<div className="relative flex h-full">
<div className="relative flex h-full" id="canvas">
<ReactFlow
colorMode={effectiveTheme}
className="canvas-cursor-default nodes-animated"

View 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,
};
};

View File

@@ -7,6 +7,7 @@ import { useChartDB } from '@/hooks/use-chartdb';
import { useLayout } from '@/hooks/use-layout';
import { cn } from '@/lib/utils';
import { getCardinalityMarkerId } from './canvas-utils';
import { useDiff } from '@/context/diff-context/use-diff';
export type RelationshipEdgeType = Edge<
{
@@ -29,6 +30,7 @@ export const RelationshipEdge: React.FC<EdgeProps<RelationshipEdgeType>> = ({
}) => {
const { getInternalNode, getEdge } = useReactFlow();
const { openRelationshipFromSidebar, selectSidebarSection } = useLayout();
const { checkIfRelationshipRemoved, checkIfNewRelationship } = useDiff();
const { relationships } = useChartDB();
@@ -149,6 +151,25 @@ export const RelationshipEdge: React.FC<EdgeProps<RelationshipEdgeType>> = ({
}),
[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 (
<>
<path
@@ -160,6 +181,10 @@ export const RelationshipEdge: React.FC<EdgeProps<RelationshipEdgeType>> = ({
className={cn([
'react-flow__edge-path',
`!stroke-2 ${selected ? '!stroke-pink-600' : '!stroke-slate-400'}`,
{
'!stroke-green-500': isDiffNewRelationship,
'!stroke-red-500': isDiffRelationshipRemoved,
},
])}
onClick={(e) => {
if (e.detail === 2) {

View File

@@ -12,7 +12,15 @@ import {
useUpdateNodeInternals,
} from '@xyflow/react';
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 { useChartDB } from '@/hooks/use-chartdb';
import { cn } from '@/lib/utils';
@@ -23,6 +31,7 @@ import {
} from '@/components/tooltip/tooltip';
import { useClickAway, useKeyPressEvent } from 'react-use';
import { Input } from '@/components/input/input';
import { useDiff } from '@/context/diff-context/use-diff';
export const LEFT_HANDLE_ID_PREFIX = 'left_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('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) => {
e.stopPropagation();
setEditMode(true);
@@ -102,13 +148,23 @@ export const TableNodeField: React.FC<TableNodeFieldProps> = React.memo(
return (
<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 ${
highlighted ? 'bg-pink-100 dark:bg-pink-900' : ''
} transition-all duration-200 ease-in-out ${
visible
? 'max-h-8 opacity-100'
: 'z-0 max-h-0 overflow-hidden opacity-0'
}`}
className={cn(
'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',
{
'bg-pink-100 dark:bg-pink-900': highlighted,
'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 ? (
<>
@@ -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
ref={inputRef}
@@ -190,10 +253,27 @@ export const TableNodeField: React.FC<TableNodeFieldProps> = React.memo(
// {field.name}
// </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}
>
{field.name}
{fieldDiffChangedName ? (
<>
{field.name}{' '}
<span className="font-medium"></span>{' '}
{fieldDiffChangedName}
</>
) : (
field.name
)}
</span>
)}
{/* <span className="truncate">{field.name}</span> */}
@@ -214,7 +294,18 @@ export const TableNodeField: React.FC<TableNodeFieldProps> = React.memo(
<div
className={cn(
'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} />
@@ -223,11 +314,31 @@ export const TableNodeField: React.FC<TableNodeFieldProps> = React.memo(
<div
className={cn(
'content-center truncate text-right text-xs text-muted-foreground shrink-0',
!readonly ? 'group-hover:hidden' : ''
'content-center truncate text-right text-xs text-muted-foreground',
!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 ? '?' : ''}
</div>
{readonly ? null : (

View File

@@ -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>
);
};

View File

@@ -10,6 +10,9 @@ import {
ChevronUp,
Check,
CircleDotDashed,
SquareDot,
SquarePlus,
SquareMinus,
} from 'lucide-react';
import { Label } from '@/components/label/label';
import type { DBTable } from '@/lib/domain/db-table';
@@ -30,6 +33,8 @@ import {
TooltipContent,
TooltipTrigger,
} 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<
{
@@ -61,6 +66,35 @@ export const TableNode: React.FC<NodeProps<TableNodeType>> = React.memo(
const [tableName, setTableName] = useState(table.name);
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(
(edge) =>
(edge.source === id || edge.target === id) &&
@@ -109,13 +143,13 @@ export const TableNode: React.FC<NodeProps<TableNodeType>> = React.memo(
const visibleFields = useMemo(() => {
if (expanded) {
return table.fields;
return fields;
}
const mustDisplayedFields = table.fields.filter((field: DBField) =>
const mustDisplayedFields = fields.filter((field: DBField) =>
isMustDisplayedField(field)
);
const nonMustDisplayedFields = table.fields.filter(
const nonMustDisplayedFields = fields.filter(
(field: DBField) => !isMustDisplayedField(field)
);
@@ -133,8 +167,8 @@ export const TableNode: React.FC<NodeProps<TableNodeType>> = React.memo(
return [
...visibleMustDisplayedFields,
...visibleNonMustDisplayedFields,
].sort((a, b) => table.fields.indexOf(a) - table.fields.indexOf(b));
}, [expanded, table.fields, isMustDisplayedField]);
].sort((a, b) => fields.indexOf(a) - fields.indexOf(b));
}, [expanded, fields, isMustDisplayedField]);
const editTableName = useCallback(() => {
if (!editMode) return;
@@ -174,6 +208,17 @@ export const TableNode: React.FC<NodeProps<TableNodeType>> = React.memo(
: '',
highlightOverlappingTables && isOverlapping
? '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) => {
@@ -194,14 +239,87 @@ export const TableNode: React.FC<NodeProps<TableNodeType>> = React.memo(
table={table}
focused={focused}
/>
{/* Badge added here */}
<TableNodeStatus
status={
isDiffNewTable
? 'new'
: isDiffTableRemoved
? 'removed'
: isDiffTableChanged
? 'changed'
: 'none'
}
/>
<div
className="h-2 rounded-t-[6px]"
style={{ backgroundColor: table.color }}
></div>
<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">
<Table2 className="size-3.5 shrink-0 text-gray-600 dark:text-primary" />
{editMode ? (
{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" />
)}
{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
ref={inputRef}
@@ -273,11 +391,11 @@ export const TableNode: React.FC<NodeProps<TableNodeType>> = React.memo(
className="transition-[max-height] duration-200 ease-in-out"
style={{
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.fields.map((field: DBField) => (
{fields.map((field: DBField) => (
<TableNodeField
key={field.id}
focused={focused}
@@ -295,7 +413,7 @@ export const TableNode: React.FC<NodeProps<TableNodeType>> = React.memo(
/>
))}
</div>
{table.fields.length > TABLE_MINIMIZED_FIELDS && (
{fields.length > TABLE_MINIMIZED_FIELDS && (
<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"
onClick={toggleExpand}

View File

@@ -1,17 +1,22 @@
import React from 'react';
import type { ButtonProps } from '@/components/button/button';
import { Button } from '@/components/button/button';
import { cn } from '@/lib/utils';
export const ToolbarButton = React.forwardRef<
React.ElementRef<typeof Button>,
ButtonProps
>((props, ref) => {
const { className, ...rest } = props;
return (
<Button
ref={ref}
variant="ghost"
className={'w-[36px] p-2 hover:bg-primary-foreground'}
{...props}
className={cn(
'w-[36px] p-2 hover:bg-primary-foreground',
className
)}
{...rest}
/>
);
});

View File

@@ -15,6 +15,7 @@ import { useTranslation } from 'react-i18next';
import { Button } from '@/components/button/button';
import { keyboardShortcutsForOS } 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)}%`;
@@ -28,6 +29,8 @@ export const Toolbar: React.FC<ToolbarProps> = ({ readonly }) => {
const { redo, undo, hasRedo, hasUndo } = useHistory();
const { getZoom, zoomIn, zoomOut, fitView } = useReactFlow();
const [zoom, setZoom] = useState<string>(convertToPercentage(getZoom()));
const { isLostInCanvas } = useIsLostInCanvas();
useOnViewportChange({
onChange: ({ zoom }) => {
setZoom(convertToPercentage(zoom));
@@ -93,7 +96,14 @@ export const Toolbar: React.FC<ToolbarProps> = ({ readonly }) => {
<Tooltip>
<TooltipTrigger asChild>
<span>
<ToolbarButton onClick={showAll}>
<ToolbarButton
onClick={showAll}
className={
isLostInCanvas
? 'bg-pink-500 text-white hover:bg-pink-600 hover:text-white'
: ''
}
>
<Scan />
</ToolbarButton>
</span>

View File

@@ -29,14 +29,11 @@ import { AlertProvider } from '@/context/alert-context/alert-provider';
import { CanvasProvider } from '@/context/canvas-context/canvas-provider';
import { HIDE_BUCKLE_DOT_DEV } from '@/lib/env';
import { useDiagramLoader } from './use-diagram-loader';
import { DiffProvider } from '@/context/diff-context/diff-provider';
const OPEN_STAR_US_AFTER_SECONDS = 30;
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(
() => import('./editor-desktop-layout')
);
@@ -49,7 +46,7 @@ const EditorPageComponent: React.FC = () => {
const { diagramName, currentDiagram, schemas, filteredSchemas } =
useChartDB();
const { openSelectSchema, showSidePanel } = useLayout();
const { openStarUsDialog, openBuckleDialog } = useDialog();
const { openStarUsDialog } = useDialog();
const { diagramId } = useParams<{ diagramId: string }>();
const { isMd: isDesktop } = useBreakpoint('md');
const {
@@ -58,9 +55,6 @@ const EditorPageComponent: React.FC = () => {
starUsDialogLastOpen,
setStarUsDialogLastOpen,
githubRepoOpened,
setBuckleDialogLastOpen,
buckleDialogLastOpen,
buckleWaitlistOpened,
} = useLocalConfig();
const { toast } = useToast();
const { t } = useTranslation();
@@ -91,37 +85,6 @@ const EditorPageComponent: React.FC = () => {
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 handleChangeSchema = useCallback(async () => {
@@ -235,23 +198,25 @@ export const EditorPage: React.FC = () => (
<StorageProvider>
<ConfigProvider>
<RedoUndoStackProvider>
<ChartDBProvider>
<HistoryProvider>
<ReactFlowProvider>
<CanvasProvider>
<ExportImageProvider>
<AlertProvider>
<DialogProvider>
<KeyboardShortcutsProvider>
<EditorPageComponent />
</KeyboardShortcutsProvider>
</DialogProvider>
</AlertProvider>
</ExportImageProvider>
</CanvasProvider>
</ReactFlowProvider>
</HistoryProvider>
</ChartDBProvider>
<DiffProvider>
<ChartDBProvider>
<HistoryProvider>
<ReactFlowProvider>
<CanvasProvider>
<ExportImageProvider>
<AlertProvider>
<DialogProvider>
<KeyboardShortcutsProvider>
<EditorPageComponent />
</KeyboardShortcutsProvider>
</DialogProvider>
</AlertProvider>
</ExportImageProvider>
</CanvasProvider>
</ReactFlowProvider>
</HistoryProvider>
</ChartDBProvider>
</DiffProvider>
</RedoUndoStackProvider>
</ConfigProvider>
</StorageProvider>

View File

@@ -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>
);
};

View File

@@ -1,30 +1,27 @@
import React from 'react';
import { Ellipsis, GripVertical, Trash2, KeyRound } from 'lucide-react';
import React, { useCallback } from 'react';
import { GripVertical, KeyRound } 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 { 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 {
Tooltip,
TooltipContent,
TooltipTrigger,
} 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 { Textarea } from '@/components/textarea/textarea';
import { TableFieldToggle } from './table-field-toggle';
import { useSortable } from '@dnd-kit/sortable';
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 { TableFieldPopover } from './table-field-modal/table-field-modal';
export interface TableFieldProps {
field: DBField;
@@ -39,13 +36,55 @@ export const TableField: React.FC<TableFieldProps> = ({
}) => {
const { databaseType } = useChartDB();
const { t } = useTranslation();
const { attributes, listeners, setNodeRef, transform, transition } =
useSortable({ id: field.id });
const dataFieldOptions = dataTypeMap[databaseType].map((type) => ({
label: type.name,
value: type.id,
}));
const dataFieldOptions: SelectBoxOption[] = dataTypeMap[databaseType].map(
(type) => ({
label: type.name,
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 = {
transform: CSS.Translate.toString(transform),
@@ -96,20 +135,39 @@ export const TableField: React.FC<TableFieldProps> = ({
'side_panel.tables_section.table.field_type'
)}
value={field.type.id}
onChange={(value) =>
updateField({
type: dataTypeMap[databaseType].find(
(v) => v.id === value
),
})
valueSuffix={
field.characterMaximumLength
? `(${field.characterMaximumLength})`
: ''
}
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(
'side_panel.tables_section.table.no_types_found'
)}
/>
</span>
</TooltipTrigger>
<TooltipContent>{field.type.name}</TooltipContent>
<TooltipContent>
{field.type.name}
{field.characterMaximumLength
? `(${field.characterMaximumLength})`
: ''}
</TooltipContent>
</Tooltip>
</div>
<div className="flex w-4/12 justify-end gap-1 overflow-hidden">
@@ -152,80 +210,11 @@ export const TableField: React.FC<TableFieldProps> = ({
{t('side_panel.tables_section.table.primary_key')}
</TooltipContent>
</Tooltip>
<Popover>
<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={field.unique}
disabled={field.primaryKey}
onCheckedChange={(value) =>
updateField({
unique: !!value,
})
}
/>
</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>
<TableFieldPopover
field={field}
updateField={updateField}
removeField={removeField}
/>
</div>
</div>
);

View File

@@ -470,8 +470,16 @@ export const Menu: React.FC<MenuProps> = () => {
</MenubarSub>
<MenubarSeparator />
<MenubarSub>
<MenubarSubTrigger>
{t('menu.view.theme')}
<MenubarSubTrigger className="flex items-center gap-1">
<span>{t('menu.view.theme')}</span>
<div className="flex-1" />
<MenubarShortcut>
{
keyboardShortcutsForOS[
KeyboardShortcutAction.TOGGLE_THEME
].keyCombinationLabel
}
</MenubarShortcut>
</MenubarSubTrigger>
<MenubarSubContent>
<MenubarCheckboxItem

View File

@@ -7,7 +7,6 @@ import { DiagramName } from './diagram-name';
import { LastSaved } from './last-saved';
import { LanguageNav } from './language-nav/language-nav';
import { Menu } from './menu/menu';
import { HIDE_BUCKLE_DOT_DEV } from '@/lib/env';
export interface TopNavbarProps {}
@@ -26,27 +25,6 @@ export const TopNavbar: React.FC<TopNavbarProps> = () => {
);
}, [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 (
<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">
@@ -68,7 +46,6 @@ export const TopNavbar: React.FC<TopNavbarProps> = () => {
</a>
{!isDesktop ? (
<div className="flex items-center gap-2">
{renderGetBuckleButton()}
{renderStars()}
<LanguageNav />
</div>
@@ -80,7 +57,6 @@ export const TopNavbar: React.FC<TopNavbarProps> = () => {
<>
<DiagramName />
<div className="hidden flex-1 items-center justify-end gap-2 sm:flex">
{renderGetBuckleButton()}
<LastSaved />
{renderStars()}
<LanguageNav />

View File

@@ -289,7 +289,6 @@ const TemplatePageComponent: React.FC = () => {
readonly
>
<Canvas
readonly
initialTables={
template.diagram.tables ?? []
}