diff --git a/src/configuration/index.test.ts b/src/configuration/index.test.ts index 586f27c..3415b35 100644 --- a/src/configuration/index.test.ts +++ b/src/configuration/index.test.ts @@ -11,6 +11,7 @@ import { getConfiguration, setActiveDatabase, setActiveTable, + setTableFilter, testables, } from '.'; @@ -412,7 +413,7 @@ describe('set connection appState', async () => { slug: 'prod', appState: { activeDatabase: 'db', - activeTableByDatabase: {}, + configByDatabase: {}, }, }, }, @@ -445,7 +446,7 @@ describe('set connection appState', async () => { slug: 'prod', appState: { activeDatabase: 'db', - activeTableByDatabase: {}, + configByDatabase: {}, }, }, }, @@ -492,9 +493,130 @@ describe('set connection appState', async () => { slug: 'prod', appState: { activeDatabase: 'db', - activeTableByDatabase: { - db: 'table', - db2: 'table2', + configByDatabase: { + db: { + activeTable: 'table', + tables: {}, + }, + db2: { + activeTable: 'table2', + tables: {}, + }, + }, + }, + }, + }, + }, + null, + 2 + ), + 'utf-8', + expect.any(Function) + ); + }); + + test('setActiveTable with appState but no configByDatabase', async () => { + mockExistingConfig({ + version: 1, + theme: DEFAULT_THEME.name, + locale: DEFAULT_LOCALE, + + connections: { + prod: { + name: 'prod', + host: 'prod', + user: 'root', + port: 3306, + password: Buffer.from('encrypted-password').toString('base64'), + slug: 'prod', + // @ts-expect-error -- testing edge case with existing configuration + appState: { + activeDatabase: 'ticketing', + }, + }, + }, + }); + + await setActiveDatabase('prod', 'db'); + await setActiveTable('prod', 'db', 'table'); + + expect(mockWriteFile).toHaveBeenCalledWith( + 'userData/config/config.json', + JSON.stringify( + { + version: 1, + theme: DEFAULT_THEME.name, + locale: DEFAULT_LOCALE, + connections: { + prod: { + name: 'prod', + host: 'prod', + user: 'root', + port: 3306, + password: Buffer.from('encrypted-password').toString('base64'), + slug: 'prod', + appState: { + activeDatabase: 'db', + configByDatabase: { + db: { + activeTable: 'table', + tables: {}, + }, + }, + }, + }, + }, + }, + null, + 2 + ), + 'utf-8', + expect.any(Function) + ); + }); +}); + +describe('setTableFilter', () => { + test('with basic configuration', async () => { + mockExistingConfig(); + + await setTableFilter('prod', 'db', 'sometable', 'id = 1'); + + expect(mockWriteFile).toHaveBeenCalledWith( + 'userData/config/config.json', + JSON.stringify( + { + version: 1, + theme: DEFAULT_THEME.name, + locale: DEFAULT_LOCALE, + + connections: { + local: { + name: 'local', + host: 'localhost', + user: 'root', + port: 3306, + password: Buffer.from('encrypted-password').toString('base64'), + slug: 'local', + }, + prod: { + name: 'prod', + host: 'prod', + user: 'root', + port: 3306, + password: Buffer.from('encrypted-password').toString('base64'), + slug: 'prod', + appState: { + activeDatabase: '', + configByDatabase: { + db: { + activeTable: '', + tables: { + sometable: { + currentFilter: 'id = 1', + }, + }, + }, }, }, }, diff --git a/src/configuration/index.ts b/src/configuration/index.ts index fab137c..6e35106 100644 --- a/src/configuration/index.ts +++ b/src/configuration/index.ts @@ -12,6 +12,7 @@ import { DEFAULT_LOCALE } from './locale'; import { DEFAULT_THEME } from './themes'; import { Configuration, + DatabaseConfig, EncryptedConfiguration, EncryptedConnectionObject, } from './type'; @@ -180,7 +181,7 @@ export function setActiveDatabase(connectionSlug: string, database: string) { if (!connection.appState) { connection.appState = { activeDatabase: '', - activeTableByDatabase: {}, + configByDatabase: {}, }; } @@ -193,25 +194,57 @@ export function setActiveTable( connectionSlug: string, database: string, tableName: string -) { +): void { const config = getConfiguration(); - const connection = config.connections[connectionSlug]; - - if (!connection) { + if (!config.connections[connectionSlug]) { return; } - if (!connection.appState) { - connection.appState = { - activeDatabase: '', - activeTableByDatabase: {}, - }; + const connection = ensureConnectionAppStateExist( + config.connections[connectionSlug] + ); + + const newConfig = ensureConnectionAppStateIsCorrect( + connection.appState.configByDatabase[database] + ); + + connection.appState.configByDatabase[database] = { + ...newConfig, + activeTable: tableName, + }; + + writeConfiguration(config); +} + +export function setTableFilter( + connectionSlug: string, + database: string, + tableName: string, + filter: string +): void { + const config = getConfiguration(); + + if (!config.connections[connectionSlug]) { + return; } - connection.appState.activeTableByDatabase = { - ...connection.appState.activeTableByDatabase, - [database]: tableName, + const connection = ensureConnectionAppStateExist( + config.connections[connectionSlug] + ); + + console.log(connection); + + const newConfig = ensureConnectionAppStateIsCorrect( + connection.appState.configByDatabase[database] + ); + + newConfig.tables[tableName] = { + currentFilter: filter, + }; + + connection.appState.configByDatabase[database] = { + ...newConfig, }; writeConfiguration(config); @@ -225,6 +258,55 @@ export function saveWindowState(windowState: WindowState): void { writeConfiguration(config); } +function hasAppState( + connection: EncryptedConnectionObject +): connection is EncryptedConnectionObject & + Required> { + return typeof connection.appState !== 'undefined'; +} + +function ensureConnectionAppStateExist( + connection: EncryptedConnectionObject +): EncryptedConnectionObject & + Required> { + if (hasAppState(connection)) { + if (!connection.appState.configByDatabase) { + connection.appState.configByDatabase = {}; + } + + return connection; + } + + connection.appState = { + activeDatabase: '', + configByDatabase: {}, + }; + + if (!hasAppState(connection)) { + throw new Error('Could not create app state for connection'); + } + + return connection; +} + +function ensureConnectionAppStateIsCorrect( + databaseConfig: Partial +): DatabaseConfig { + if (!databaseConfig) { + databaseConfig = {}; + } + + if (!databaseConfig.activeTable) { + databaseConfig.activeTable = ''; + } + + if (!databaseConfig.tables) { + databaseConfig.tables = {}; + } + + return databaseConfig as DatabaseConfig; +} + const IPC_EVENT_BINDING = { [CONFIGURATION_CHANNEL.GET]: getConfiguration, [CONFIGURATION_CHANNEL.ADD_CONNECTION]: addConnectionToConfig, @@ -233,6 +315,7 @@ const IPC_EVENT_BINDING = { [CONFIGURATION_CHANNEL.CHANGE_LANGUAGE]: changeLanguage, [CONFIGURATION_CHANNEL.SET_ACTIVE_DATABASE]: setActiveDatabase, [CONFIGURATION_CHANNEL.SET_ACTIVE_TABLE]: setActiveTable, + [CONFIGURATION_CHANNEL.SET_TABLE_FILTER]: setTableFilter, } as const; export function bindIpcMain(ipcMain: Electron.IpcMain): void { diff --git a/src/configuration/type.ts b/src/configuration/type.ts index 23b4491..4aa8863 100644 --- a/src/configuration/type.ts +++ b/src/configuration/type.ts @@ -9,9 +9,18 @@ export type Configuration = { windowState?: WindowState; }; +type TableConfig = { + currentFilter?: string; +}; + +export type DatabaseConfig = { + activeTable: string; + tables: Record; +}; + type ConnectionAppState = { activeDatabase: string; - activeTableByDatabase: Record; + configByDatabase: Record; }; export type EncryptedConnectionObject = { diff --git a/src/preload/config.ts b/src/preload/config.ts index b02d54e..488dab9 100644 --- a/src/preload/config.ts +++ b/src/preload/config.ts @@ -22,6 +22,13 @@ interface Config { tableName: string ): Promise; + setTableFilter( + connectionSlug: string, + database: string, + tableName: string, + filter: string + ): Promise; + editConnection( connectionSlug: string, connection: ConnectionObjectWithoutSlug @@ -35,5 +42,6 @@ export const config: Config = { changeLanguage: bindChannel(CONFIGURATION_CHANNEL.CHANGE_LANGUAGE), setActiveDatabase: bindChannel(CONFIGURATION_CHANNEL.SET_ACTIVE_DATABASE), setActiveTable: bindChannel(CONFIGURATION_CHANNEL.SET_ACTIVE_TABLE), + setTableFilter: bindChannel(CONFIGURATION_CHANNEL.SET_TABLE_FILTER), editConnection: bindChannel(CONFIGURATION_CHANNEL.EDIT_CONNECTION), }; diff --git a/src/preload/configurationChannel.ts b/src/preload/configurationChannel.ts index 886c406..124bb40 100644 --- a/src/preload/configurationChannel.ts +++ b/src/preload/configurationChannel.ts @@ -6,4 +6,5 @@ export enum CONFIGURATION_CHANNEL { CHANGE_LANGUAGE = 'config:language:change', SET_ACTIVE_DATABASE = 'config:connection:setActiveDatabase', SET_ACTIVE_TABLE = 'config:connection:setActiveTable', + SET_TABLE_FILTER = 'config:connection:setTableFilter', } diff --git a/src/renderer/component/Query/WhereFilter.tsx b/src/renderer/component/Query/WhereFilter.tsx index a54fd66..f0de771 100644 --- a/src/renderer/component/Query/WhereFilter.tsx +++ b/src/renderer/component/Query/WhereFilter.tsx @@ -1,26 +1,22 @@ import { ReactElement, useRef, useState } from 'react'; import { Button, Space } from 'antd'; +import { Form } from 'react-router-dom'; import { useTranslation } from '../../../i18n'; import { RawSqlEditor } from '../MonacoEditor/RawSqlEditor'; interface Props { - onSubmit: (where: string) => void; defaultValue: string; } -function WhereFilter({ defaultValue, onSubmit }: Props): ReactElement { +function WhereFilter({ defaultValue }: Props): ReactElement { const { t } = useTranslation(); const [where, setWhere] = useState(defaultValue); const ref = useRef(null); return ( -
{ - e.preventDefault(); - onSubmit(where); - }} - > + + + -
+ ); } diff --git a/src/renderer/component/TableLayout/TableLayout.tsx b/src/renderer/component/TableLayout/TableLayout.tsx index 4398d57..3a5aa03 100644 --- a/src/renderer/component/TableLayout/TableLayout.tsx +++ b/src/renderer/component/TableLayout/TableLayout.tsx @@ -19,7 +19,7 @@ export function TableLayout({ tableName, database, primaryKeys, - where: defaultWhere, + where, }: TableNameProps): ReactElement { const { t } = useTranslation(); const { currentConnectionSlug } = useConnectionContext(); @@ -27,7 +27,6 @@ export function TableLayout({ const [fields, setFields] = useState(null); const [error, setError] = useState(null); const [currentOffset, setCurrentOffset] = useState(0); - const [where, setWhere] = useState(defaultWhere ?? ''); const fetchTableData = useCallback( (offset: number) => { @@ -55,48 +54,44 @@ export function TableLayout({ fetchTableData(currentOffset); }, [fetchTableData, currentOffset]); - if (error) { - return
{error.message}
; - } - return (

{t('table.filters.title')}

- { - setCurrentOffset(0); - setWhere(where); - }} - /> +
- ( - <> - {tableName} - - STRUCTURE - - - )} - /> + {error ? ( + error.message + ) : ( + <> + ( + <> + {tableName} + + STRUCTURE + + + )} + /> - - - + + + + + )}
); } diff --git a/src/renderer/routes/connections.$connectionSlug.$databaseName.$tableName.tsx b/src/renderer/routes/connections.$connectionSlug.$databaseName.$tableName.tsx index 675bc1d..958ebac 100644 --- a/src/renderer/routes/connections.$connectionSlug.$databaseName.$tableName.tsx +++ b/src/renderer/routes/connections.$connectionSlug.$databaseName.$tableName.tsx @@ -1,9 +1,4 @@ -import { - LoaderFunctionArgs, - Params, - useLoaderData, - useLocation, -} from 'react-router'; +import { LoaderFunctionArgs, Params, useLoaderData } from 'react-router'; import invariant from 'tiny-invariant'; import TableLayout from '../component/TableLayout'; @@ -11,7 +6,7 @@ interface RouteParams extends LoaderFunctionArgs { params: Params<'connectionSlug' | 'databaseName' | 'tableName'>; } -export async function loader({ params }: RouteParams) { +export async function loader({ params, request }: RouteParams) { const { connectionSlug, databaseName, tableName } = params; invariant(connectionSlug, 'Connection slug is required'); @@ -23,16 +18,33 @@ export async function loader({ params }: RouteParams) { window.config.setActiveTable(connectionSlug, databaseName, tableName); + const configuration = await window.config.getConfiguration(); + + const whereFilter = + configuration.connections[connectionSlug]?.appState?.configByDatabase?.[ + databaseName + ]?.tables[tableName]?.currentFilter || ''; + + const where = new URL(request.url).searchParams.get('where') || whereFilter; + + window.config.setTableFilter(connectionSlug, databaseName, tableName, where); + return { primaryKeys, + whereFilter: where, }; } export default function TableNamePage() { - const { primaryKeys } = useLoaderData() as Awaited>; - const location = useLocation(); - - const where = new URLSearchParams(location.search).get('where'); - - return ; + const { primaryKeys, whereFilter } = useLoaderData() as Awaited< + ReturnType + >; + + return ( + + ); } diff --git a/src/renderer/routes/connections.$connectionSlug.$databaseName.test.tsx b/src/renderer/routes/connections.$connectionSlug.$databaseName.test.tsx index 0c365a8..49db156 100644 --- a/src/renderer/routes/connections.$connectionSlug.$databaseName.test.tsx +++ b/src/renderer/routes/connections.$connectionSlug.$databaseName.test.tsx @@ -29,7 +29,7 @@ function setConfiguration( password: '', appState: { activeDatabase: activeDatabase, - activeTableByDatabase: {}, + configByDatabase: {}, }, }; } diff --git a/src/renderer/routes/connections.$connectionSlug.$databaseName.tsx b/src/renderer/routes/connections.$connectionSlug.$databaseName.tsx index 7a06f36..1b4decd 100644 --- a/src/renderer/routes/connections.$connectionSlug.$databaseName.tsx +++ b/src/renderer/routes/connections.$connectionSlug.$databaseName.tsx @@ -17,10 +17,10 @@ export async function loader({ params, request }: RouteParams) { window.config.setActiveDatabase(connectionSlug, databaseName); - const { activeTableByDatabase } = + const { configByDatabase } = configuration.connections[connectionSlug]?.appState || {}; - const openedTable = activeTableByDatabase?.[databaseName]; + const openedTable = configByDatabase?.[databaseName]?.activeTable; // redirect to the current database if we are not on a "database" root page if (openedTable) { diff --git a/src/renderer/routes/connections.$connectionSlug.tsx b/src/renderer/routes/connections.$connectionSlug.tsx index 5ce3ef7..9bc8044 100644 --- a/src/renderer/routes/connections.$connectionSlug.tsx +++ b/src/renderer/routes/connections.$connectionSlug.tsx @@ -50,11 +50,11 @@ export async function loader({ params, request }: RouteParams) { const configuration = await window.config.getConfiguration(); - const { activeDatabase: configDatabase, activeTableByDatabase } = + const { activeDatabase: configDatabase, configByDatabase } = configuration.connections[connectionSlug]?.appState || {}; const openedTable = configDatabase - ? activeTableByDatabase?.[configDatabase] + ? configByDatabase?.[configDatabase]?.activeTable : undefined; if (!databaseList || !databaseList[0]) {