Skip to content

Commit 1db1d78

Browse files
WIP: first draft of actual global search
1 parent 111e19d commit 1db1d78

File tree

5 files changed

+196
-41
lines changed

5 files changed

+196
-41
lines changed

structr-app/src/main/resources/structr/icon/sprites.svg

Lines changed: 2 additions & 2 deletions
Loading

structr-app/src/main/resources/structr/js/command.js

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -317,15 +317,12 @@ let Command = {
317317
/**
318318
* Send SEARCH_NODES command to the server.
319319
*
320-
* The server will search for nodes with the given searchString in ANY of their keys
320+
* The server will search for nodes with the given queryString in ANY of their keys
321321
*/
322-
searchNodes: async (searchString, config) => {
322+
searchNodes: async (config) => {
323323
let obj = {
324324
command: 'SEARCH_NODES',
325-
data: {
326-
searchString: searchString,
327-
...config
328-
}
325+
data: config
329326
};
330327
return new Promise((resolve, reject) => {
331328
StructrWS.sendObj(obj, resolve);

structr-app/src/main/resources/structr/js/init.js

Lines changed: 166 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,8 @@ let Structr = {
276276
},
277277
init: () => {
278278
_Helpers.fastRemoveAllChildren(document.querySelector('#errorText'));
279+
280+
Structr.globalSearch.init();
279281
},
280282
clearMain: () => {
281283

@@ -1917,6 +1919,151 @@ let Structr = {
19171919
});
19181920
},
19191921

1922+
globalSearch: {
1923+
init: () => {
1924+
let form = document.querySelector('#global-search-node-form');
1925+
let searchField = form.querySelector('[name="queryString"]');
1926+
1927+
form.addEventListener('submit', e => {
1928+
e.preventDefault();
1929+
1930+
let data = Structr.globalSearch.getBasicFormData(form);
1931+
1932+
if (data.queryString.length > 0) {
1933+
Structr.globalSearch.doSearch(data);
1934+
} else {
1935+
form.reportValidity();
1936+
}
1937+
});
1938+
1939+
searchField.addEventListener('search', () => {
1940+
if (searchField.value === '') {
1941+
Structr.globalSearch.clear();
1942+
}
1943+
});
1944+
},
1945+
clear: () => {
1946+
let resultsElement = document.querySelector('#global-search-results');
1947+
for (let oldResult of resultsElement.querySelectorAll('[data-id]')) {
1948+
_Helpers.fastRemoveElement(oldResult);
1949+
}
1950+
},
1951+
doSearch: async (data) => {
1952+
1953+
let results = await Command.searchNodes(data);
1954+
1955+
Structr.globalSearch.clear();
1956+
1957+
let resultsElement = document.querySelector('#global-search-results');
1958+
1959+
for (let result of results) {
1960+
1961+
for (let key of result.keys) {
1962+
1963+
let el = _Helpers.createSingleDOMElementFromHTML(Structr.globalSearch.templates.result(result, key));
1964+
1965+
resultsElement.appendChild(el);
1966+
1967+
el.querySelector('button').addEventListener('click', Structr.globalSearch.goToResultButtonClicked);
1968+
}
1969+
}
1970+
},
1971+
goToResultButtonClicked: e => {
1972+
1973+
let id = e.target.closest('[data-id]').dataset.id;
1974+
let key = e.target.closest('[data-id]').dataset.key;
1975+
1976+
let matchingTabUrlHash = '#pages:' + _Pages.search.getTabForKey(key);
1977+
let link = document.querySelector(`[href="${matchingTabUrlHash}"]`)
1978+
1979+
if (link && id === _Pages.centerPane.dataset['elementId']) {
1980+
1981+
_Pages.activateCenterPane(link);
1982+
1983+
} else {
1984+
1985+
// preselect tab for that element
1986+
_Pages.saveActiveCenterTab(id, matchingTabUrlHash);
1987+
1988+
_Pages.selectAndShowArbitraryDOMElement(id);
1989+
}
1990+
},
1991+
getBasicFormData: (form) => {
1992+
let data = {};
1993+
1994+
for (let el of form.elements) {
1995+
switch(el.type) {
1996+
case 'checkbox':
1997+
data[el.name] = el.checked;
1998+
break;
1999+
default:
2000+
data[el.name] = el.value;
2001+
break;
2002+
}
2003+
}
2004+
2005+
return data;
2006+
},
2007+
templates: {
2008+
popover: config => `
2009+
<div popover id="global-search-popover" class="absolute" style="
2010+
position-anchor: --global-search;
2011+
position-area: x-start y-end;
2012+
width: 500px;
2013+
height: 700px;
2014+
overflow: auto;
2015+
border: 1px solid var(--input-field-border);
2016+
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2);
2017+
border-radius: .25rem;
2018+
font-size: 1rem;
2019+
resize: both;
2020+
">
2021+
2022+
<div class="overflow-y-auto max-h-full h-full">
2023+
<div class="mx-4 my-4">
2024+
<form id="global-search-node-form" class="flex flex-col gap-2">
2025+
<div class="flex gap-2">
2026+
<input type="search" name="queryString" required placeholder="Search term...">
2027+
<button type="submit" class="action button btn focus:border-gray-666 active:border-green">Search</button>
2028+
</div>
2029+
<div>
2030+
<label class="flex items-center"><input type="checkbox" checked name="searchDOM">Search Page Elements</label>
2031+
<label class="flex items-center"><input type="checkbox" checked name="searchSchema">Search Schema Code</label>
2032+
<label class="flex items-center"><input type="checkbox" checked name="searchFlow">Search Flow Nodes</label>
2033+
</div>
2034+
2035+
<div>
2036+
<label class="flex items-center"><input type="checkbox" name="caseInsensitive">Case Insensitive</label>
2037+
</div>
2038+
</form>
2039+
2040+
<div id="global-search-results" class="grid items-center gap-x-2 gap-y-3 mt-6" style="grid-template-columns: [ name ] minmax(0, 1fr) [ keys ] minmax(10%, max-content) [ id ] 4rem [ actions ] minmax(2rem, max-content)">
2041+
<div class="contents font-bold">
2042+
<div>Name/Type</div>
2043+
<div>Key</div>
2044+
<div>ID</div>
2045+
<div></div>
2046+
</div>
2047+
</div>
2048+
</div>
2049+
</div>
2050+
</div>
2051+
`,
2052+
result: (result, key) => `
2053+
<div class="contents" data-id="${result.id}" data-key="${key}">
2054+
<div>${result.name ? `${result.name} [${result.type}]` : result.type}</div>
2055+
<div>${key}</div>
2056+
<div class="truncate">${result.id}</div>
2057+
<div>
2058+
<button class="flex items-center hover:bg-gray-100 focus:border-gray-666 active:border-green p-2 mr-0" title="Go to element">
2059+
${_Icons.getSvgIcon(_Icons.iconOpenInNewPage, 16, 16, [..._Icons.getSvgIconClassesNonColorIcon(), 'pointer-events-none'])}
2060+
</button>
2061+
</div>
2062+
</div>
2063+
`
2064+
}
2065+
},
2066+
19202067
/* basically only exists to get rid of repeating strings. is also used to filter out internal keys from dialogs */
19212068
internalKeys: {
19222069
name: 'name',
@@ -1978,14 +2125,29 @@ let Structr = {
19782125
</div>
19792126
</div>
19802127
1981-
<div class="flex gap-4 ml-2 mr-6">
2128+
<div class="flex gap-4 items-center mr-6">
2129+
2130+
<div style="target-name: --global-search;">
2131+
2132+
<button class="m-0 p-0 border-0" popovertarget="global-search-popover" style="anchor-name: --global-search;">
2133+
${_Icons.getSvgIcon(_Icons.iconSearch, 24, 24, _Icons.getSvgIconClassesForColoredIcon(['text-white', 'mt-1']), 'Global Search')}
2134+
</button>
19822135
1983-
<a target="_blank" href="${_Helpers.getPrefixedRootUrl('/structr/config')}">${_Icons.getSvgIconWithID('settings-icon', _Icons.iconSettingsWrench, 20,20,_Icons.getSvgIconClassesForColoredIcon(['text-white', 'mt-1.5']), 'System Settings')}</a>
2136+
${Structr.globalSearch.templates.popover(config)}
2137+
</div>
19842138
1985-
${_Icons.getSvgIconWithID('terminal-icon', _Icons.iconTerminal, 26,26,_Icons.getSvgIconClassesForColoredIcon(['text-white']), 'Toggle Console')}
2139+
<div>
2140+
<a target="_blank" href="${_Helpers.getPrefixedRootUrl('/structr/config')}">
2141+
${_Icons.getSvgIconWithID('settings-icon', _Icons.iconSettingsWrench, 20,20, _Icons.getSvgIconClassesForColoredIcon(['text-white', 'mt-1']), 'System Settings')}
2142+
</a>
2143+
</div>
2144+
2145+
<div>
2146+
${_Icons.getSvgIconWithID('terminal-icon', _Icons.iconTerminal, 26,26, _Icons.getSvgIconClassesForColoredIcon(['text-white']), 'Toggle Console')}
2147+
</div>
19862148
19872149
<div id="${Structr.notificationIconId}" class="relative">
1988-
${_Icons.getSvgIcon(_Icons.iconNotificationBell, 20,20,_Icons.getSvgIconClassesForColoredIcon(['text-white', 'mt-1']), 'Show notifications')}
2150+
${_Icons.getSvgIcon(_Icons.iconNotificationBell, 20,20, _Icons.getSvgIconClassesForColoredIcon(['text-white', 'mt-1']), 'Show notifications')}
19892151
<div class="absolute flex items-center rounded-full h-4 -top-1 -right-3 text-white bg-red">
19902152
<div data-notification-count class="px-2 text-xs empty:hidden"></div>
19912153
</div>

structr-app/src/main/resources/structr/js/pages.js

Lines changed: 16 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3933,18 +3933,19 @@ let _Pages = {
39333933

39343934
search: {
39353935
init: () => {
3936-
let form = document.querySelector('#search-node-form');
3937-
let searchField = form.querySelector('[name="searchString"]');
3936+
let form = document.querySelector('#pages-search-node-form');
3937+
let searchField = form.querySelector('[name="queryString"]');
39383938

39393939
form.addEventListener('submit', e => {
39403940
e.preventDefault();
39413941

3942-
let searchString = searchField.value.trim();
3942+
let data = Structr.globalSearch.getBasicFormData(form);
3943+
data.searchDOM = true;
39433944

3944-
if (searchString.length > 0) {
3945-
_Pages.search.doSearch(searchString, form.querySelector('[name="caseInsensitive"]').checked);
3945+
if (data.queryString.length > 0) {
3946+
_Pages.search.doSearch(data);
39463947
} else {
3947-
_Helpers.blinkRed(searchField);
3948+
form.reportValidity();
39483949
}
39493950
});
39503951

@@ -3955,21 +3956,18 @@ let _Pages = {
39553956
});
39563957
},
39573958
clear: () => {
3958-
let resultsElement = document.querySelector('#dom-search-results');
3959+
let resultsElement = document.querySelector('#pages-search-results');
39593960
for (let oldResult of resultsElement.querySelectorAll('[data-id]')) {
39603961
_Helpers.fastRemoveElement(oldResult);
39613962
}
39623963
},
3963-
doSearch: async (searchString, caseInsensitive) => {
3964+
doSearch: async (data) => {
39643965

3965-
let results = await Command.searchNodes(searchString, {
3966-
searchDOM: true,
3967-
caseInsensitive: caseInsensitive
3968-
});
3966+
let results = await Command.searchNodes(data);
39693967

39703968
_Pages.search.clear();
39713969

3972-
let resultsElement = document.querySelector('#dom-search-results');
3970+
let resultsElement = document.querySelector('#pages-search-results');
39733971

39743972
for (let result of results) {
39753973

@@ -4027,17 +4025,17 @@ let _Pages = {
40274025
slideoutContent: config => `
40284026
<div class="overflow-y-auto max-h-full h-full">
40294027
<div class="mx-4 my-4">
4030-
<form id="search-node-form" class="flex flex-col gap-2">
4028+
<form id="pages-search-node-form" class="flex flex-col gap-2">
40314029
<div class="flex gap-2">
4032-
<input type="search" name="searchString" placeholder="Search">
4030+
<input type="search" name="queryString" required placeholder="Search term...">
40334031
<button type="submit" class="action button btn focus:border-gray-666 active:border-green">Search</button>
40344032
</div>
4035-
<div class="flex gap-2">
4036-
<label><input type="checkbox" name="caseInsensitive">Case Insensitive</label>
4033+
<div>
4034+
<label class="flex items-center"><input type="checkbox" name="caseInsensitive" value="true">Case Insensitive</label>
40374035
</div>
40384036
</form>
40394037
4040-
<div id="dom-search-results" class="grid items-center gap-x-2 gap-y-3 mt-6" style="grid-template-columns: [ name ] minmax(0, 1fr) [ keys ] minmax(10%, max-content) [ id ] 4rem [ actions ] minmax(2rem, max-content)">
4038+
<div id="pages-search-results" class="grid items-center gap-x-2 gap-y-3 mt-6" style="grid-template-columns: [ name ] minmax(0, 1fr) [ keys ] minmax(10%, max-content) [ id ] 4rem [ actions ] minmax(2rem, max-content)">
40414039
<div class="contents font-bold">
40424040
<div>Name/Type</div>
40434041
<div>Key</div>

structr-base/src/main/java/org/structr/websocket/command/SearchNodesCommand.java

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,20 +18,13 @@
1818
*/
1919
package org.structr.websocket.command;
2020

21-
import com.google.gson.Gson;
22-
import com.google.gson.JsonSyntaxException;
23-
import org.apache.commons.lang3.StringUtils;
2421
import org.slf4j.Logger;
2522
import org.slf4j.LoggerFactory;
2623
import org.structr.api.util.Iterables;
2724
import org.structr.common.SecurityContext;
2825
import org.structr.common.error.FrameworkException;
2926
import org.structr.core.GraphObject;
30-
import org.structr.core.app.QueryGroup;
3127
import org.structr.core.app.StructrApp;
32-
import org.structr.core.property.PropertyKey;
33-
import org.structr.core.traits.Traits;
34-
import org.structr.core.traits.definitions.NodeInterfaceTraitDefinition;
3528
import org.structr.websocket.StructrWebSocket;
3629
import org.structr.websocket.message.MessageBuilder;
3730
import org.structr.websocket.message.WebSocketMessage;
@@ -47,7 +40,7 @@ public class SearchNodesCommand extends AbstractCommand {
4740

4841
private static final Logger logger = LoggerFactory.getLogger(SearchNodesCommand.class.getName());
4942

50-
private static final String SEARCH_STRING_KEY = "searchString";
43+
private static final String QUERY_STRING_KEY = "queryString";
5144
private static final String SEARCH_DOM_BOOL_KEY = "searchDOM";
5245
private static final String SEARCH_FLOW_BOOL_KEY = "searchFlow";
5346
private static final String SEARCH_SCHEMA_BOOL_KEY = "searchSchema";
@@ -63,8 +56,8 @@ public void processMessage(final WebSocketMessage webSocketData) {
6356
setDoTransactionNotifications(false);
6457

6558
final SecurityContext securityContext = getWebSocket().getSecurityContext();
66-
final String searchString = webSocketData.getNodeDataStringValue(SEARCH_STRING_KEY);
6759

60+
final String queryString = webSocketData.getNodeDataStringValue(QUERY_STRING_KEY);
6861
final boolean searchDOM = webSocketData.getNodeDataBooleanValue(SEARCH_DOM_BOOL_KEY);
6962
final boolean searchFlow = webSocketData.getNodeDataBooleanValue(SEARCH_FLOW_BOOL_KEY);
7063
final boolean searchSchema = webSocketData.getNodeDataBooleanValue(SEARCH_SCHEMA_BOOL_KEY);
@@ -73,7 +66,7 @@ public void processMessage(final WebSocketMessage webSocketData) {
7366

7467
try {
7568

76-
final Map<String, Object> obj = Map.of("searchString", searchString);
69+
final Map<String, Object> obj = Map.of("queryString", queryString);
7770

7871
final ArrayList<String> labels = new ArrayList<>();
7972
if (searchDOM) { labels.add("n:DOMNode"); }
@@ -82,7 +75,7 @@ public void processMessage(final WebSocketMessage webSocketData) {
8275

8376
if (!labels.isEmpty()) {
8477

85-
final String containsClause = caseInsensitive ? "toLower(toString(n[prop])) CONTAINS toLower($searchString)" : "n[prop] CONTAINS $searchString";
78+
final String containsClause = caseInsensitive ? "toLower(toString(n[prop])) CONTAINS toLower($queryString)" : "n[prop] CONTAINS $queryString";
8679

8780
final String cypherQuery = """
8881
MATCH (n)
@@ -105,6 +98,11 @@ public void processMessage(final WebSocketMessage webSocketData) {
10598
webSocketData.setRawResultCount(resultCountBeforePaging);
10699

107100
webSocketData.setResult(result);
101+
102+
} else {
103+
104+
webSocketData.setRawResultCount(0);
105+
webSocketData.setResult(List.of());
108106
}
109107

110108
getWebSocket().send(webSocketData, true);

0 commit comments

Comments
 (0)