Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -158,4 +158,22 @@ private List<com.condation.cms.api.ui.elements.MenuEntry> siteMenus() {
)
public void list_unpublished_pages() {
}

@MenuEntry(
parent = "toolMenu",
id = "search_pages",
name = "Search pages",
permissions = {Permissions.CONTENT_EDIT},
position = 5,
scriptAction = @ScriptAction(module = "/manager/actions/page/search-pages")
)
@ShortCut(
id = "search_pages",
title = "Search pages",
permissions = {Permissions.CONTENT_EDIT},
section = "tools",
scriptAction = @ScriptAction(module = "/manager/actions/page/search-pages")
)
public void search_pages() {
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import com.condation.cms.api.db.DB;
import com.condation.cms.api.db.Page;
import com.condation.cms.api.eventbus.events.ReIndexContentMetaDataEvent;
import com.condation.cms.api.feature.features.DBFeature;
import com.condation.cms.api.feature.features.EventBusFeature;
import com.condation.cms.api.feature.features.WorkflowFeature;
import com.condation.cms.api.ui.extensions.UIRemoteMethodExtensionPoint;
Expand Down Expand Up @@ -57,7 +58,42 @@
@Extension(UIRemoteMethodExtensionPoint.class)
public class RemotePageEnpoints extends AbstractRemoteMethodeExtension {

@RemoteMethod(name = "pages.filter", permissions = {Permissions.CONTENT_EDIT})
public record SearchResultDto(String uri, String title) {
}

@RemoteMethod(name = "pages.search", permissions = {Permissions.CONTENT_EDIT})
public Object searchPages (Map<String, Object> parameters) throws RPCException {
String query = "";

if (parameters.get("query") instanceof String stringValue) {
query = stringValue;
}

final DB db = getContext().get(DBFeature.class).db();
var contentBase = db.getFileSystem().contentBase();

var hits = db.getContent().searchByTitle(query).stream()
.map(node -> {
var contentFile = contentBase.resolve(node.uri());
var url = PathUtil.toURL(contentFile, contentBase);
url = HTTPUtil.modifyUrl(url, context);

String title = "";
if (node.data().get(Constants.MetaFields.TITLE) instanceof String titleValue) {
title = titleValue;
}

return new SearchResultDto(url, title);
})
.toList();

Map<String, Object> result = new HashMap<>();
result.put("result", hits);

return result;
}

@RemoteMethod(name = "pages.filter", permissions = {Permissions.CONTENT_EDIT})
public Object filterPages (Map<String, Object> parameters) throws RPCException {

final DB db = getDB(parameters);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*-
* #%L
* UI Module
* %%
* Copyright (C) 2023 - 2026 CondationCMS
* %%
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
* #L%
*/
interface SearchPagesActionOptions {
}
export declare const runAction: (options?: SearchPagesActionOptions) => Promise<void>;
export {};
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
/*-
* #%L
* UI Module
* %%
* Copyright (C) 2023 - 2026 CondationCMS
* %%
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
* #L%
*/
import { openModal } from '@cms/modules/modal.js';
import { i18n } from '@cms/modules/localization.js';
import { searchPages } from '@cms/modules/rpc/rpc-page';
import { loadPreview } from '@cms/modules/preview.utils';
const MIN_QUERY_LENGTH = 3;
const renderResultsHtml = (results, query) => {
if (query.trim().length < MIN_QUERY_LENGTH) {
return `<p>${i18n.t('page.search.minLength', 'Enter at least 3 characters and press Enter to search.')}</p>`;
}
if (results.length === 0) {
return `<p>${i18n.t('page.search.noResults', 'No pages found.')}</p>`;
}
return `
<table class="table">
<thead>
<tr>
<th>${i18n.t('page.search.columnTitle', 'Title')}</th>
<th>${i18n.t('page.search.columnUri', 'URI')}</th>
<th></th>
</tr>
</thead>
<tbody>
${results.map(result => `
<tr>
<td>${result.title}</td>
<td>${result.uri}</td>
<td>
<a data-cms-page-uri="${result.uri}" class="btn btn-sm btn-outline-primary" href="#">
${i18n.t('page.search.loadLink', 'Load')}
</a>
</td>
</tr>
`).join('')}
</tbody>
</table>
`;
};
const state = {
modal: null
};
const bindResultLinks = (resultsElement) => {
resultsElement.querySelectorAll('a[data-cms-page-uri]').forEach((link) => {
link.addEventListener('click', (e) => {
e.preventDefault();
state.modal.hide();
loadPreview(link.dataset.cmsPageUri || '');
});
});
};
const runSearch = async (query, resultsElement) => {
if (query.trim().length < MIN_QUERY_LENGTH) {
resultsElement.innerHTML = renderResultsHtml([], query);
return;
}
try {
const response = await searchPages({ query: query.trim() });
resultsElement.innerHTML = renderResultsHtml(response.result, query);
bindResultLinks(resultsElement);
}
catch (e) {
resultsElement.innerHTML = `<p>${i18n.t('page.search.loadError', 'Could not search pages.')}</p>`;
}
};
export const runAction = async (options = {}) => {
state.modal = openModal({
title: i18n.t('page.search.title', 'Search pages'),
body: `
<input type="search" class="form-control" id="cms-search-pages-input"
placeholder="${i18n.t('page.search.placeholder', 'Search by title...')}" />
<div id="cms-search-pages-results" class="mt-3"></div>
`,
fullscreen: false,
onCancel: () => { },
onOk: () => { },
onShow: (modalElement) => {
const inputElement = modalElement.querySelector('#cms-search-pages-input');
const resultsElement = modalElement.querySelector('#cms-search-pages-results');
resultsElement.innerHTML = renderResultsHtml([], '');
inputElement.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
runSearch(inputElement.value, resultsElement);
}
});
inputElement.focus();
}
});
};
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,14 @@ export namespace MODULE_LOCALIZATIONS {
"page.unpublished.title": string;
"page.unpublished.noPages": string;
"page.unpublished.editLink": string;
"page.search.title": string;
"page.search.placeholder": string;
"page.search.minLength": string;
"page.search.noResults": string;
"page.search.columnTitle": string;
"page.search.columnUri": string;
"page.search.loadLink": string;
"page.search.loadError": string;
"pagination.previous": string;
"pagination.next": string;
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,14 @@ export const MODULE_LOCALIZATIONS = {
"page.unpublished.title": "Unveröffentlichte Seiten",
"page.unpublished.noPages": "Keine unveröffentlichten Seiten gefunden.",
"page.unpublished.editLink": "Bearbeiten",
"page.search.title": "Seiten suchen",
"page.search.placeholder": "Nach Titel suchen...",
"page.search.minLength": "Mindestens 3 Zeichen eingeben und Enter drücken, um zu suchen.",
"page.search.noResults": "Keine Seiten gefunden.",
"page.search.columnTitle": "Titel",
"page.search.columnUri": "URI",
"page.search.loadLink": "Laden",
"page.search.loadError": "Seiten konnten nicht durchsucht werden.",
"pagination.previous": "Zurück",
"pagination.next": "Weiter"
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,4 +55,15 @@ export interface FilterPagesResponse {
}
declare const filterPages: (options: FilterPagesOptions) => Promise<FilterPagesResponse>;
declare const deletePage: (options: any) => Promise<any>;
export { createPage, deletePage, filterPages };
export interface SearchPagesOptions {
query: string;
}
export interface SearchResultDto {
uri: string;
title: string;
}
export interface SearchPagesResponse {
result: SearchResultDto[];
}
declare const searchPages: (options: SearchPagesOptions) => Promise<SearchPagesResponse>;
export { createPage, deletePage, filterPages, searchPages };
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,11 @@ const deletePage = async (options) => {
};
return await executeRemoteCall(data);
};
export { createPage, deletePage, filterPages };
const searchPages = async (options) => {
var data = {
method: "pages.search",
parameters: options
};
return (await executeRemoteCall(data)).result;
};
export { createPage, deletePage, filterPages, searchPages };
122 changes: 122 additions & 0 deletions modules/ui-module/src/main/ts/src/actions/page/search-pages.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
/*-
* #%L
* UI Module
* %%
* Copyright (C) 2023 - 2026 CondationCMS
* %%
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
* #L%
*/
import { openModal } from '@cms/modules/modal.js'
import { i18n } from '@cms/modules/localization.js';
import { searchPages, SearchResultDto } from '@cms/modules/rpc/rpc-page'
import { loadPreview } from '@cms/modules/preview.utils';

interface SearchPagesActionOptions {
}

const MIN_QUERY_LENGTH = 3;

const renderResultsHtml = (results: SearchResultDto[], query: string) => {
if (query.trim().length < MIN_QUERY_LENGTH) {
return `<p>${i18n.t('page.search.minLength', 'Enter at least 3 characters and press Enter to search.')}</p>`;
}

if (results.length === 0) {
return `<p>${i18n.t('page.search.noResults', 'No pages found.')}</p>`;
}

return `
<table class="table">
<thead>
<tr>
<th>${i18n.t('page.search.columnTitle', 'Title')}</th>
<th>${i18n.t('page.search.columnUri', 'URI')}</th>
<th></th>
</tr>
</thead>
<tbody>
${results.map(result => `
<tr>
<td>${result.title}</td>
<td>${result.uri}</td>
<td>
<a data-cms-page-uri="${result.uri}" class="btn btn-sm btn-outline-primary" href="#">
${i18n.t('page.search.loadLink', 'Load')}
</a>
</td>
</tr>
`).join('')}
</tbody>
</table>
`;
};

const state: any = {
modal: null
};

const bindResultLinks = (resultsElement: HTMLElement) => {
resultsElement.querySelectorAll('a[data-cms-page-uri]').forEach((link) => {
link.addEventListener('click', (e) => {
e.preventDefault();
state.modal.hide();
loadPreview((link as HTMLElement).dataset.cmsPageUri || '');
});
});
};

const runSearch = async (query: string, resultsElement: HTMLElement) => {
if (query.trim().length < MIN_QUERY_LENGTH) {
resultsElement.innerHTML = renderResultsHtml([], query);
return;
}

try {
const response = await searchPages({ query: query.trim() });
resultsElement.innerHTML = renderResultsHtml(response.result, query);
bindResultLinks(resultsElement);
} catch (e) {
resultsElement.innerHTML = `<p>${i18n.t('page.search.loadError', 'Could not search pages.')}</p>`;
}
};

export const runAction = async (options: SearchPagesActionOptions = {}) => {
state.modal = openModal({
title: i18n.t('page.search.title', 'Search pages'),
body: `
<input type="search" class="form-control" id="cms-search-pages-input"
placeholder="${i18n.t('page.search.placeholder', 'Search by title...')}" />
<div id="cms-search-pages-results" class="mt-3"></div>
`,
fullscreen: false,
onCancel: () => { /* Optional: handle cancel */ },
onOk: () => { /* Optional: handle OK */ },
onShow: (modalElement: any) => {
const inputElement = modalElement.querySelector('#cms-search-pages-input') as HTMLInputElement;
const resultsElement = modalElement.querySelector('#cms-search-pages-results') as HTMLElement;

resultsElement.innerHTML = renderResultsHtml([], '');

inputElement.addEventListener('keydown', (e: KeyboardEvent) => {
if (e.key === 'Enter') {
e.preventDefault();
runSearch(inputElement.value, resultsElement);
}
});

inputElement.focus();
}
});
};
Loading
Loading