diff --git a/packages/tools/src/filterName.ts b/packages/tools/src/filterName.ts index 81c1d0b8e..504ebf08d 100644 --- a/packages/tools/src/filterName.ts +++ b/packages/tools/src/filterName.ts @@ -2,9 +2,9 @@ import _compact from 'lodash/compact'; import _isString from 'lodash/isString'; import _startCase from 'lodash/startCase'; -export interface FilterNameDefinition { - childName: string; -} +// export interface FilterNameDefinition { +// childName: string; +// } function camelMatch(filter: string, text: string): boolean { if (!text) return false; @@ -20,7 +20,7 @@ function camelMatch(filter: string, text: string): boolean { } } -export function filterName(filter: string, ...names: (string | FilterNameDefinition)[]) { +export function filterName(filter: string, ...names: string[]) { if (!filter) return true; // const camelVariants = [name.replace(/[^A-Z]/g, '')] @@ -28,22 +28,124 @@ export function filterName(filter: string, ...names: (string | FilterNameDefinit const namesCompacted = _compact(names); - // @ts-ignore - const namesOwn: string[] = namesCompacted.filter(x => _isString(x)); - // @ts-ignore - const namesChild: string[] = namesCompacted.filter(x => x.childName).map(x => x.childName); - for (const token of tokens) { - // const tokenUpper = token.toUpperCase(); - if (token.startsWith('#')) { - // const tokenUpperSub = tokenUpper.substring(1); - const found = namesChild.find(name => camelMatch(token.substring(1), name)); - if (!found) return false; - } else { - const found = namesOwn.find(name => camelMatch(token, name)); - if (!found) return false; - } + const found = namesCompacted.find(name => camelMatch(token, name)); + if (!found) return false; } return true; } + +export function filterNameCompoud( + filter: string, + namesMain: string[], + namesChild: string[] +): 'main' | 'child' | 'both' | 'none' { + if (!filter) return 'both'; + + // const camelVariants = [name.replace(/[^A-Z]/g, '')] + const tokens = filter.split(' ').map(x => x.trim()); + + const namesCompactedMain = _compact(namesMain); + const namesCompactedChild = _compact(namesChild); + + let isMainOnly = true; + let isChildOnly = true; + + for (const token of tokens) { + const foundMain = namesCompactedMain.find(name => camelMatch(token, name)); + const foundChild = namesCompactedChild.find(name => camelMatch(token, name)); + if (!foundMain && !foundChild) return 'none'; + + if (!foundMain) isMainOnly = false; + if (!foundChild) isChildOnly = false; + } + + if (isMainOnly && isChildOnly) return 'both'; + if (isMainOnly) return 'main'; + if (isChildOnly) return 'child'; + return 'none'; +} + +export function tokenizeBySearchFilter(text: string, filter: string): { text: string; isMatch: boolean }[] { + const camelTokens = []; + const stdTokens = []; + for (const token of filter.split(' ').map(x => x.trim())) { + if (token.replace(/[A-Z]/g, '').length == 0) { + camelTokens.push(token); + } else { + stdTokens.push(token.toUpperCase()); + } + } + + let res = [ + { + text, + isMatch: false, + }, + ]; + + for (const token of camelTokens) { + const nextres = []; + for (const item of res) { + const indexes = []; + for (const char of token) { + const index = item.text.indexOf(char, indexes.length > 0 ? indexes[indexes.length - 1] + 1 : 0); + if (index < 0) { + indexes.push(-1); + } else { + indexes.push(index); + } + } + if (indexes.some(x => x < 0)) { + nextres.push(item); + } else { + let lastIndex = 0; + for (let i = 0; i < indexes.length; i++) { + if (indexes[i] > lastIndex) { + nextres.push({ text: item.text.substring(lastIndex, indexes[i]), isMatch: false }); + } + nextres.push({ text: item.text.substring(indexes[i], indexes[i] + 1), isMatch: true }); + lastIndex = indexes[i] + 1; + } + nextres.push({ text: item.text.substring(lastIndex), isMatch: false }); + } + } + res = nextres; + } + + for (const token of stdTokens) { + const nextres = []; + for (const item of res) { + const index = item.text?.toUpperCase().indexOf(token); + if (index < 0) { + nextres.push(item); + } else { + nextres.push({ text: item.text.substring(0, index), isMatch: false }); + nextres.push({ text: item.text.substring(index, index + token.length), isMatch: true }); + nextres.push({ text: item.text.substring(index + token.length), isMatch: false }); + } + } + res = nextres; + } + + return res.filter(x => x.text.length > 0); + + // const result = []; + // let lastMatch = 0; + // for (const token of tokens) { + // const index = text.indexOf(token, lastMatch); + // if (index < 0) { + // result.push({ token, isMatch: false }); + // continue; + // } + + // result.push({ token: text.substring(lastMatch, index), isMatch: false }); + // result.push({ token: text.substring(index, index + token.length), isMatch: true }); + // lastMatch = index + token.length; + // } + + // result.push({ token: text.substring(lastMatch), isMatch: false }); + + // return result; +} diff --git a/packages/web/src/appobj/AppFileAppObject.svelte b/packages/web/src/appobj/AppFileAppObject.svelte index c51176f6b..f3ed7e257 100644 --- a/packages/web/src/appobj/AppFileAppObject.svelte +++ b/packages/web/src/appobj/AppFileAppObject.svelte @@ -29,8 +29,8 @@ export const extractKey = data => data.fileName; export const createMatcher = - ({ fileName }) => filter => + ({ fileName }) => filterName(filter, fileName); const APP_ICONS = { 'config.json': 'img json', diff --git a/packages/web/src/appobj/AppFolderAppObject.svelte b/packages/web/src/appobj/AppFolderAppObject.svelte index 857228e85..8cbc52a8c 100644 --- a/packages/web/src/appobj/AppFolderAppObject.svelte +++ b/packages/web/src/appobj/AppFolderAppObject.svelte @@ -1,6 +1,6 @@ {#if groupFunc} @@ -107,7 +122,7 @@ /> {/each} {:else} - {#each list as data} + {#each listLimited as data} {/each} + {#if isListLimited} +
+ { + expandLimited = true; + }}>Show next {filtered.length - listLimited.length} +
+ {/if} {/if} diff --git a/packages/web/src/appobj/AppObjectListItem.svelte b/packages/web/src/appobj/AppObjectListItem.svelte index 395127e63..0bafecdb4 100644 --- a/packages/web/src/appobj/AppObjectListItem.svelte +++ b/packages/web/src/appobj/AppObjectListItem.svelte @@ -57,15 +57,19 @@ {module} {disableContextMenu} {passProps} + {filter} /> {#if (isExpanded || isExpandedBySearch) && subItemsComponent}
diff --git a/packages/web/src/appobj/ArchiveFileAppObject.svelte b/packages/web/src/appobj/ArchiveFileAppObject.svelte index 97874510d..84891e0b4 100644 --- a/packages/web/src/appobj/ArchiveFileAppObject.svelte +++ b/packages/web/src/appobj/ArchiveFileAppObject.svelte @@ -42,8 +42,8 @@ export const extractKey = data => data.fileName; export const createMatcher = - ({ fileName }) => filter => + ({ fileName }) => filterName(filter, fileName); const ARCHIVE_ICONS = { 'table.yaml': 'img table', @@ -70,7 +70,7 @@ import { getExtensions } from '../stores'; import createQuickExportMenu from '../utility/createQuickExportMenu'; - import { exportQuickExportFile, } from '../utility/exportFileTools'; + import { exportQuickExportFile } from '../utility/exportFileTools'; import openNewTab from '../utility/openNewTab'; import AppObjectCore from './AppObjectCore.svelte'; import InputTextModal from '../modals/InputTextModal.svelte'; diff --git a/packages/web/src/appobj/ArchiveFolderAppObject.svelte b/packages/web/src/appobj/ArchiveFolderAppObject.svelte index 35cce3784..27ee13d42 100644 --- a/packages/web/src/appobj/ArchiveFolderAppObject.svelte +++ b/packages/web/src/appobj/ArchiveFolderAppObject.svelte @@ -1,6 +1,6 @@ + + + + diff --git a/packages/web/src/appobj/SavedFileAppObject.svelte b/packages/web/src/appobj/SavedFileAppObject.svelte index 23011a5b2..99ccc2c15 100644 --- a/packages/web/src/appobj/SavedFileAppObject.svelte +++ b/packages/web/src/appobj/SavedFileAppObject.svelte @@ -104,8 +104,8 @@ export const extractKey = data => data.file; export const createMatcher = - ({ file }) => filter => + ({ file }) => filterName(filter, file); diff --git a/packages/web/src/appobj/SubDatabaseList.svelte b/packages/web/src/appobj/SubDatabaseList.svelte index 6d8abaec0..27a2d6aa8 100644 --- a/packages/web/src/appobj/SubDatabaseList.svelte +++ b/packages/web/src/appobj/SubDatabaseList.svelte @@ -12,16 +12,18 @@ export let passProps; export let isExpandedOnlyBySearch; + export let isExpandedBySearch; $: databases = useDatabaseList({ conid: isExpandedOnlyBySearch ? null : data._id }); $: dbList = isExpandedOnlyBySearch ? getLocalStorage(`database_list_${data._id}`) || [] : $databases || []; + + // .filter(x => filterName(filter, x.name, data.displayName, data.server)) filterName(filter, x.name, data.displayName, data.server)), - x => x.sortOrder ?? x.name - ).map(db => ({ ...db, connection: data }))} + list={_.sortBy(dbList, x => x.sortOrder ?? x.name).map(db => ({ ...db, connection: data }))} module={databaseAppObject} {passProps} + {filter} + {isExpandedBySearch} /> diff --git a/packages/web/src/appobj/SubProcedureLineList.svelte b/packages/web/src/appobj/SubProcedureLineList.svelte new file mode 100644 index 000000000..2539c1929 --- /dev/null +++ b/packages/web/src/appobj/SubProcedureLineList.svelte @@ -0,0 +1,21 @@ + + + + + ({ + lineData, + }))} + module={procedureLineAppObject} + {filter} + {isExpandedBySearch} +/> diff --git a/packages/web/src/appobj/SubColumnParamList.svelte b/packages/web/src/appobj/SubTableColumnList.svelte similarity index 81% rename from packages/web/src/appobj/SubColumnParamList.svelte rename to packages/web/src/appobj/SubTableColumnList.svelte index f03f98822..32d06ef0b 100644 --- a/packages/web/src/appobj/SubColumnParamList.svelte +++ b/packages/web/src/appobj/SubTableColumnList.svelte @@ -5,6 +5,8 @@ import * as columnAppObject from './ColumnAppObject.svelte'; export let data; + export let filter; + export let isExpandedBySearch; diff --git a/packages/web/src/datagrid/ColumnManager.svelte b/packages/web/src/datagrid/ColumnManager.svelte index ee889e90e..76031e8ac 100644 --- a/packages/web/src/datagrid/ColumnManager.svelte +++ b/packages/web/src/datagrid/ColumnManager.svelte @@ -235,6 +235,7 @@ {columnIndex} {allowChangeChangeSetStructure} isSelected={selectedColumns.includes(column.uniqueName) || currentColumnUniqueName == column.uniqueName} + {filter} on:click={() => { if (domFocusField) domFocusField.focus(); selectedColumns = [column.uniqueName]; diff --git a/packages/web/src/datagrid/ColumnManagerRow.svelte b/packages/web/src/datagrid/ColumnManagerRow.svelte index 864f6c60d..b5340d991 100644 --- a/packages/web/src/datagrid/ColumnManagerRow.svelte +++ b/packages/web/src/datagrid/ColumnManagerRow.svelte @@ -15,6 +15,7 @@ export let conid; export let database; export let isDynamicStructure; + export let filter = undefined; export let tableInfo; export let setTableInfo; @@ -83,7 +84,7 @@ }} /> {/if} - + {#if allowChangeChangeSetStructure && !isDynamicStructure} diff --git a/packages/web/src/elements/ColumnLabel.svelte b/packages/web/src/elements/ColumnLabel.svelte index 14d74c972..9e5443ba9 100644 --- a/packages/web/src/elements/ColumnLabel.svelte +++ b/packages/web/src/elements/ColumnLabel.svelte @@ -11,6 +11,7 @@ import { openDatabaseObjectDetail } from '../appobj/DatabaseObjectAppObject.svelte'; import FontIcon from '../icons/FontIcon.svelte'; + import TokenizedFilteredText from '../widgets/TokenizedFilteredText.svelte'; import Link from './Link.svelte'; export let notNull = false; @@ -25,6 +26,7 @@ export let conid = undefined; export let database = undefined; export let iconOverride = undefined; + export let filter = undefined; $: icon = iconOverride || getColumnIcon($$props, forceIcon); @@ -33,7 +35,7 @@ {#if icon} {/if} - {headerText || columnName} + {#if extInfo} {extInfo} {/if} diff --git a/packages/web/src/icons/FontIcon.svelte b/packages/web/src/icons/FontIcon.svelte index 1f8d6930f..06d4b956f 100644 --- a/packages/web/src/icons/FontIcon.svelte +++ b/packages/web/src/icons/FontIcon.svelte @@ -143,6 +143,7 @@ 'icon parent-filter': 'mdi mdi-home-alert', 'icon parent-filter-outline': 'mdi mdi-home-alert-outline', 'icon download': 'mdi mdi-download', + 'icon text': 'mdi mdi-text', 'icon run': 'mdi mdi-play', 'icon chevron-down': 'mdi mdi-chevron-down', diff --git a/packages/web/src/modals/DropDownMenu.svelte b/packages/web/src/modals/DropDownMenu.svelte index e1969c725..6af2f2e90 100644 --- a/packages/web/src/modals/DropDownMenu.svelte +++ b/packages/web/src/modals/DropDownMenu.svelte @@ -55,6 +55,8 @@ let submenuItem; let submenuOffset; + let switchIndex = 0; + const dispatch = createEventDispatcher(); let closeHandlers = []; @@ -80,6 +82,14 @@ submenuOffset = hoverOffset; return; } + if (item.switchStore) { + item.switchStore.update(x => ({ + ...x, + [item.switchValue]: !x[item.switchValue], + })); + switchIndex++; + return; + } dispatchClose(); if (onCloseParent) onCloseParent(); if (item.onClick) item.onClick(); @@ -131,7 +141,18 @@ }} > handleClick(e, item)} class:disabled={item.disabled} class:bold={item.isBold}> - {item.text || item.label} + + {#if item.switchStoreGetter} + {#key switchIndex} + {#if item.switchStoreGetter()[item.switchValue]} + + {:else} + + {/if} + {/key} + {/if} + {item.text || item.label} + {#if item.keyText} {formatKeyText(item.keyText)} {/if} @@ -179,6 +200,7 @@ white-space: nowrap; overflow-y: auto; max-height: calc(100% - 20px); + user-select: none; } .keyText { diff --git a/packages/web/src/stores.ts b/packages/web/src/stores.ts index a48d2018b..dd60b6b53 100644 --- a/packages/web/src/stores.ts +++ b/packages/web/src/stores.ts @@ -161,6 +161,25 @@ export const lastUsedDefaultActions = writableWithStorage({}, 'lastUsedDefaultAc export const selectedDatabaseObjectAppObject = writable(null); export const focusedConnectionOrDatabase = writable<{ conid: string; database?: string; connection: any }>(null); +export const DEFAULT_SEARCH_SETTINGS = { + collectionName: true, + schemaName: false, + tableName: true, + viewName: true, + columnName: true, + columnDataType: false, + tableComment: true, + columnComment: true, + sqlObjectName: true, + sqlObjectText: true, + tableEngine: false, +}; + +export const databaseObjectAppObjectSearchSettings = writableWithStorage( + DEFAULT_SEARCH_SETTINGS, + 'databaseObjectAppObjectSearchSettings' +); + export const currentThemeDefinition = derived([currentTheme, extensions], ([$currentTheme, $extensions]) => $extensions.themes.find(x => x.themeClassName == $currentTheme) ); @@ -358,4 +377,12 @@ let lastUsedDefaultActionsValue = {}; lastUsedDefaultActions.subscribe(value => { lastUsedDefaultActionsValue = value; }); -export const getLastUsedDefaultActions = () => lastUsedDefaultActionsValue; \ No newline at end of file +export const getLastUsedDefaultActions = () => lastUsedDefaultActionsValue; + +let databaseObjectAppObjectSearchSettingsValue: typeof DEFAULT_SEARCH_SETTINGS = { + ...DEFAULT_SEARCH_SETTINGS, +}; +databaseObjectAppObjectSearchSettings.subscribe(value => { + databaseObjectAppObjectSearchSettingsValue = value; +}); +export const getDatabaseObjectAppObjectSearchSettings = () => databaseObjectAppObjectSearchSettingsValue; diff --git a/packages/web/src/widgets/SqlObjectList.svelte b/packages/web/src/widgets/SqlObjectList.svelte index 2be2c6ee1..7af64f669 100644 --- a/packages/web/src/widgets/SqlObjectList.svelte +++ b/packages/web/src/widgets/SqlObjectList.svelte @@ -27,7 +27,7 @@ import AppObjectList from '../appobj/AppObjectList.svelte'; import _ from 'lodash'; import * as databaseObjectAppObject from '../appobj/DatabaseObjectAppObject.svelte'; - import SubColumnParamList from '../appobj/SubColumnParamList.svelte'; + import SubTableColumnList from '../appobj/SubTableColumnList.svelte'; import { chevronExpandIcon } from '../icons/expandIcons'; import ErrorInfo from '../elements/ErrorInfo.svelte'; import LoadingInfo from '../elements/LoadingInfo.svelte'; @@ -38,8 +38,10 @@ import { extractDbNameFromComposite, findEngineDriver } from 'dbgate-tools'; import { currentDatabase, + databaseObjectAppObjectSearchSettings, extensions, focusedConnectionOrDatabase, + getDatabaseObjectAppObjectSearchSettings, getSelectedDatabaseObjectAppObject, selectedDatabaseObjectAppObject, } from '../stores'; @@ -53,6 +55,7 @@ import { matchDatabaseObjectAppObject } from '../appobj/appObjectTools'; import FocusedConnectionInfoWidget from './FocusedConnectionInfoWidget.svelte'; import SubProcedureParamList from '../appobj/SubProcedureParamList.svelte'; + import SubProcedureLineList from '../appobj/SubProcedureLineList.svelte'; export let conid; export let database; @@ -124,11 +127,32 @@ return res; } - $: flatFilteredList = objectList.filter(data => { - const matcher = databaseObjectAppObject.createMatcher(data); - if (matcher && !matcher(filter)) return false; - return true; - }); + function createSearchMenu() { + const res = []; + if (driver?.databaseEngineTypes?.includes('document')) { + res.push({ label: 'Collection names' }); + } + if (driver?.databaseEngineTypes?.includes('sql')) { + res.push({ label: 'Schema name', switchValue: 'schemaName' }); + res.push({ label: 'Table name', switchValue: 'tableName' }); + res.push({ label: 'View name', switchValue: 'viewName' }); + res.push({ label: 'Column name', switchValue: 'columnName' }); + res.push({ label: 'Column data type', switchValue: 'columnType' }); + res.push({ label: 'Table comment', switchValue: 'tableComment' }); + res.push({ label: 'Column comment', switchValue: 'columnComment' }); + res.push({ label: 'Procedure/function/trigger name', switchValue: 'sqlObjectName' }); + res.push({ label: 'Procedure/function/trigger text', switchValue: 'sqlObjectText' }); + res.push({ label: 'Table engine', switchValue: 'tableEngine' }); + } + return res.map(item => ({ + ...item, + switchStore: databaseObjectAppObjectSearchSettings, + switchStoreGetter: getDatabaseObjectAppObjectSearchSettings, + })); + } + + $: matcher = databaseObjectAppObject.createMatcher(filter, $databaseObjectAppObjectSearchSettings); + $: flatFilteredList = objectList.filter(data => !matcher || matcher(data)); export function focus() { domListHandler?.focusFirst(); @@ -184,7 +208,7 @@ {:else} { @@ -192,7 +216,12 @@ }} /> - + {#if filter} + + {/if} + {#if !filter} + + {/if} @@ -240,10 +269,14 @@ .map(x => ({ ...x, conid, database }))} module={databaseObjectAppObject} groupFunc={data => getObjectTypeFieldLabel(data.objectTypeField, driver)} - subItemsComponent={data => + subItemsComponent={(data, { isExpandedBySearch }) => data.objectTypeField == 'procedures' || data.objectTypeField == 'functions' - ? SubProcedureParamList - : SubColumnParamList} + ? isExpandedBySearch + ? SubProcedureLineList + : SubProcedureParamList + : isExpandedBySearch && (data.objectTypeField == 'views' || data.objectTypeField == 'matviews') + ? SubProcedureLineList + : SubTableColumnList} isExpandable={data => data.objectTypeField == 'tables' || data.objectTypeField == 'views' || @@ -256,6 +289,7 @@ showPinnedInsteadOfUnpin: true, connection: $connection, hideSchemaName: !!$appliedCurrentSchema, + searchSettings: $databaseObjectAppObjectSearchSettings, }} getIsExpanded={data => expandedObjects.includes(`${data.objectTypeField}||${data.schemaName}||${data.pureName}`)} diff --git a/packages/web/src/widgets/TokenizedFilteredText.svelte b/packages/web/src/widgets/TokenizedFilteredText.svelte new file mode 100644 index 000000000..3a7575a46 --- /dev/null +++ b/packages/web/src/widgets/TokenizedFilteredText.svelte @@ -0,0 +1,26 @@ + + +{#if tokenized} + {#each tokenized as token} + {#if token.isMatch} + {token.text} + {:else} + {token.text} + {/if} + {/each} +{:else} + {text} +{/if} + +