diff --git a/src/drivers/common/MySQLCommonInterface.ts b/src/drivers/common/MySQLCommonInterface.ts
index 0cfd81f..8008f9e 100644
--- a/src/drivers/common/MySQLCommonInterface.ts
+++ b/src/drivers/common/MySQLCommonInterface.ts
@@ -292,6 +292,7 @@ export default class MySQLCommonInterface extends SQLCommonInterface {
table_schema: database,
table_name: table,
})
+ .orderBy('ORDINAL_POSITION')
.toRawSQL(),
},
],
diff --git a/src/libs/QueryBuilder.ts b/src/libs/QueryBuilder.ts
index 13fc45c..829288d 100644
--- a/src/libs/QueryBuilder.ts
+++ b/src/libs/QueryBuilder.ts
@@ -24,6 +24,7 @@ interface QueryStates {
where: QueryWhere[];
select: string[];
limit?: number;
+ orderBy?: [string, 'ASC' | 'DESC'];
}
abstract class QueryDialect {
@@ -98,6 +99,12 @@ export class QueryBuilder {
return this;
}
+ orderBy(field: string, by: 'ASC' | 'DESC' = 'ASC') {
+ if (!['ASC', 'DESC'].includes(by)) throw 'Order by must be DESC or ASC';
+ this.states.orderBy = [field, by];
+ return this;
+ }
+
select(...columns: string[]) {
this.states.select = this.states.select.concat(columns);
return this;
@@ -195,6 +202,12 @@ export class QueryBuilder {
'FROM',
this.dialect.escapeIdentifier(this.states.table),
whereSql ? 'WHERE ' + whereSql : whereSql,
+ this.states.orderBy
+ ? 'ORDER BY ' +
+ this.dialect.escapeIdentifier(this.states.orderBy[0]) +
+ ' ' +
+ this.states.orderBy[1]
+ : null,
this.states.limit ? `LIMIT ?` : null,
]
.filter(Boolean)
diff --git a/src/renderer/components/OptimizeTable/CellCheckInput.tsx b/src/renderer/components/OptimizeTable/CellCheckInput.tsx
new file mode 100644
index 0000000..e8689a8
--- /dev/null
+++ b/src/renderer/components/OptimizeTable/CellCheckInput.tsx
@@ -0,0 +1,9 @@
+import styles from './cells.module.scss';
+
+export default function CellCheckInput() {
+ return (
+
+
+
+ );
+}
diff --git a/src/renderer/components/OptimizeTable/CellGroupSelectInput.tsx b/src/renderer/components/OptimizeTable/CellGroupSelectInput.tsx
new file mode 100644
index 0000000..172e7ab
--- /dev/null
+++ b/src/renderer/components/OptimizeTable/CellGroupSelectInput.tsx
@@ -0,0 +1,81 @@
+import { useState, useEffect, useRef } from 'react';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import styles from './cells.module.scss';
+import { faChevronDown } from '@fortawesome/free-solid-svg-icons';
+import AvoidOffscreen from '../AvoidOffscreen';
+import DropContainer from '../DropContainer';
+import OptionList from '../OptionList';
+import OptionListGroup from '../OptionList/OptionListGroup';
+import OptionListItem from '../OptionList/OptionListItem';
+import TextField from '../TextField';
+
+interface CellGroupSelectInputProps {
+ items: { name: string; items: { value: string; text: string }[] }[];
+ value?: string;
+}
+
+export default function CellGroupSelectInput({
+ items,
+ value,
+}: CellGroupSelectInputProps) {
+ const [show, setShow] = useState(false);
+ const ref = useRef(null);
+
+ useEffect(() => {
+ if (ref.current && show) {
+ const onDocumentClick = (e: MouseEvent) => {
+ if (ref.current && !ref.current.contains(e.target as Node)) {
+ setShow(false);
+ }
+ };
+
+ document.addEventListener('click', onDocumentClick);
+ return () => document.removeEventListener('click', onDocumentClick);
+ }
+ }, [ref, show, setShow]);
+
+ return (
+
+
{
+ setShow(true);
+ }}
+ >
+
{value}
+
+
+
+
+ {show && (
+
+
+
+
+
+
+
+
+ {items.map((group) => {
+ return (
+
+ {group.items.map((sub) => {
+ return (
+
+ );
+ })}
+
+ );
+ })}
+
+
+
+
+
+ )}
+
+ );
+}
diff --git a/src/renderer/components/OptimizeTable/CellSingleLineInput.tsx b/src/renderer/components/OptimizeTable/CellSingleLineInput.tsx
new file mode 100644
index 0000000..4578f41
--- /dev/null
+++ b/src/renderer/components/OptimizeTable/CellSingleLineInput.tsx
@@ -0,0 +1,44 @@
+import styles from './cells.module.scss';
+
+interface CellInputProps {
+ onLostFocus?: (value: string | null | undefined) => void;
+ readOnly?: boolean;
+ value?: string | null;
+ alignRight?: boolean;
+ onChange?: (value: string) => void;
+}
+
+export default function CellSingleLineInput({
+ onLostFocus,
+ readOnly,
+ value,
+ onChange,
+ alignRight,
+}: CellInputProps) {
+ return (
+
+ {
+ if (e.key === 'Enter') {
+ if (onLostFocus) {
+ onLostFocus(value);
+ }
+ }
+ }}
+ spellCheck="false"
+ autoFocus
+ type="text"
+ readOnly={readOnly}
+ className={styles.input}
+ style={alignRight ? { textAlign: 'right' } : undefined}
+ onBlur={() => {
+ if (onLostFocus) onLostFocus(value);
+ }}
+ onChange={(e) => {
+ if (onChange) onChange(e.currentTarget.value);
+ }}
+ value={value ?? ''}
+ />
+
+ );
+}
diff --git a/src/renderer/components/OptimizeTable/cells.module.scss b/src/renderer/components/OptimizeTable/cells.module.scss
new file mode 100644
index 0000000..2bbfdc7
--- /dev/null
+++ b/src/renderer/components/OptimizeTable/cells.module.scss
@@ -0,0 +1,46 @@
+.checkContainer {
+ display: flex;
+ height: 100%;
+ width: 100%;
+ padding: 0px 10px;
+ user-select: none;
+ justify-content: center;
+
+ input {
+ transform: scale(1.2)
+ }
+}
+
+.inputContainer {
+ display: flex;
+ width: 100%;
+ height: 100%;
+}
+
+.input {
+ padding: 5px 10px;
+ border: 1px solid var(--color-surface);
+ background: var(--color-surface);
+ color: var(--color-text);
+ box-sizing: border-box;
+ width: 100%;
+ height: 100%;
+ outline: none;
+ font-size: 1rem;
+}
+
+.dropdownContainer {
+ display: flex;
+ padding: 0px 10px;
+
+ .dropdownContent, .dropdownArrow {
+ line-height: 32px;
+ }
+
+ .dropdownContent {
+ flex-grow: 1;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ }
+}
diff --git a/src/renderer/components/OptionList/OptionListGroup.tsx b/src/renderer/components/OptionList/OptionListGroup.tsx
index 3a3d262..7681e14 100644
--- a/src/renderer/components/OptionList/OptionListGroup.tsx
+++ b/src/renderer/components/OptionList/OptionListGroup.tsx
@@ -1,3 +1,14 @@
-export default function OptionListGroup() {
- return ;
+import { PropsWithChildren } from 'react';
+import styles from './styles.module.scss';
+
+export default function OptionListGroup({
+ children,
+ text,
+}: PropsWithChildren<{ text: string }>) {
+ return (
+
+ );
}
diff --git a/src/renderer/components/OptionList/styles.module.scss b/src/renderer/components/OptionList/styles.module.scss
index a4ba618..2f193cb 100644
--- a/src/renderer/components/OptionList/styles.module.scss
+++ b/src/renderer/components/OptionList/styles.module.scss
@@ -22,6 +22,12 @@
}
+.groupLabel {
+ font-weight: bold;
+ padding: 5px 10px;
+ padding-left: 15px;
+}
+
.item {
display: flex;
cursor: pointer;
diff --git a/src/renderer/components/ResizableTable/styles.module.scss b/src/renderer/components/ResizableTable/styles.module.scss
index 930ebd2..6657019 100644
--- a/src/renderer/components/ResizableTable/styles.module.scss
+++ b/src/renderer/components/ResizableTable/styles.module.scss
@@ -39,10 +39,6 @@
border-right: 1px solid var(--color-table-grid);
}
- td {
- overflow: hidden;
- }
-
th {
padding: 5px 10px;
position: sticky;
diff --git a/src/renderer/components/SchemaEditor/TableColumnsAlterDiff.ts b/src/renderer/components/SchemaEditor/TableColumnsAlterDiff.ts
new file mode 100644
index 0000000..af98b3d
--- /dev/null
+++ b/src/renderer/components/SchemaEditor/TableColumnsAlterDiff.ts
@@ -0,0 +1,69 @@
+import { TableColumnSchema } from 'types/SqlSchema';
+
+interface TableColumnsAlterDiffNode {
+ index: number;
+ original: TableColumnSchema | null;
+ changed: Partial | null;
+}
+
+interface ColumnNode extends TableColumnsAlterDiffNode {
+ index: number;
+ original: TableColumnSchema | null;
+ changed: Partial | null;
+ next: ColumnNode | null;
+ prev: ColumnNode | null;
+}
+
+export class TableColumnsAlterDiff {
+ protected root: ColumnNode | null = null;
+
+ /**
+ * Initial from original columns
+ *
+ * @param columns
+ * @returns
+ */
+ constructor(columns: TableColumnSchema[]) {
+ const root: ColumnNode = {
+ next: null,
+ prev: null,
+ index: 0,
+ original: columns[0],
+ changed: {},
+ };
+
+ let currentNode = root;
+ for (let i = 1; i < columns.length; i++) {
+ const newNode: ColumnNode = {
+ next: null,
+ prev: currentNode,
+ index: i,
+ original: columns[i],
+ changed: {},
+ };
+
+ currentNode.next = newNode;
+ currentNode = newNode;
+ }
+
+ this.root = root;
+ }
+
+ toArray() {
+ if (!this.root) return [];
+
+ const arr: TableColumnsAlterDiffNode[] = [];
+ let ptr: ColumnNode | null = this.root;
+
+ while (ptr) {
+ arr.push({
+ index: ptr.index,
+ original: ptr.original ? Object.freeze({ ...ptr.original }) : null,
+ changed: Object.freeze({ ...ptr.changed }),
+ });
+ ptr = ptr.next;
+ }
+
+ return arr;
+ }
+}
diff --git a/src/renderer/components/SchemaEditor/index.tsx b/src/renderer/components/SchemaEditor/index.tsx
new file mode 100644
index 0000000..fbcc30c
--- /dev/null
+++ b/src/renderer/components/SchemaEditor/index.tsx
@@ -0,0 +1,72 @@
+import ResizableTable from '../ResizableTable';
+import { TableColumnsAlterDiff } from './TableColumnsAlterDiff';
+import TableCellContent from '../ResizableTable/TableCellContent';
+import CellSingleLineInput from '../OptimizeTable/CellSingleLineInput';
+import CellCheckInput from '../OptimizeTable/CellCheckInput';
+import CellGroupSelectInput from '../OptimizeTable/CellGroupSelectInput';
+
+const MySQLGroupTypes = [
+ {
+ name: 'Integer',
+ items: [
+ { value: 'tinyint', text: 'TINYINT' },
+ { value: 'smallint', text: 'SMALLINT' },
+ { value: 'mediumint', text: 'MEDIUMINT' },
+ { value: 'int', text: 'INT' },
+ { value: 'bigint', text: 'BIGINT' },
+ { value: 'bit', text: 'BIT' },
+ ],
+ },
+ {
+ name: 'Real',
+ items: [
+ { value: 'float', text: 'FLOAT' },
+ { value: 'double', text: 'DOUBLE' },
+ { value: 'decimal', text: 'DECIMAL' },
+ ],
+ },
+];
+
+export default function SchemaEditor({
+ diff,
+}: {
+ diff: TableColumnsAlterDiff;
+}) {
+ const diffArray = diff.toArray();
+ return (
+
+ {diffArray.map((diff) => {
+ return (
+
+
+
+ |
+
+
+ |
+
+
+ |
+
+
+ |
+ |
+
+ );
+ })}
+
+ );
+}
diff --git a/src/stories/SchemaEditor.stories.tsx b/src/stories/SchemaEditor.stories.tsx
new file mode 100644
index 0000000..7a7f613
--- /dev/null
+++ b/src/stories/SchemaEditor.stories.tsx
@@ -0,0 +1,48 @@
+import type { Meta, StoryObj } from '@storybook/react';
+import StorybookContainer from './StoryContainer';
+import { useState } from 'react';
+import SchemaEditor from 'renderer/components/SchemaEditor';
+import { TableColumnSchema } from 'types/SqlSchema';
+import { TableColumnsAlterDiff } from 'renderer/components/SchemaEditor/TableColumnsAlterDiff';
+
+const INITIAL_COLUMN: TableColumnSchema[] = [
+ {
+ name: 'id',
+ dataType: 'int',
+ charLength: null,
+ default: '0',
+ comment: '',
+ nullable: false,
+ },
+ {
+ name: 'name',
+ dataType: 'varchar',
+ charLength: 255,
+ default: '',
+ comment: '',
+ nullable: false,
+ },
+];
+
+function StoryPage() {
+ const [diff] = useState(() => new TableColumnsAlterDiff(INITIAL_COLUMN));
+
+ return (
+
+
+
+ );
+}
+
+const meta = {
+ title: 'Components/SchemaEditor',
+ component: StoryPage,
+ tags: ['autodocs'],
+} satisfies Meta;
+
+export default meta;
+type Story = StoryObj;
+
+export const Primary: Story = {
+ args: {},
+};