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 ( +
+
{text}
+
{children}
+
+ ); } 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: {}, +};