Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Grid view #5990

Open
wants to merge 44 commits into
base: 6.2
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
259cbd5
Toggle button webcomponent
BurntimeX Nov 9, 2024
dd3458c
Grid view (PoC)
BurntimeX Sep 9, 2024
fbe528b
Add sorting function
BurntimeX Sep 9, 2024
c91b5e5
Add abstract implementation for DBO lists
BurntimeX Sep 11, 2024
91283ce
Remove debug code
BurntimeX Sep 11, 2024
50063d2
Reset to page 1 when changing sort order
BurntimeX Sep 11, 2024
d06a27d
Move column rendering into templates
BurntimeX Sep 11, 2024
979b046
Apply suggestions from code review
BurntimeX Sep 19, 2024
da6156b
Fix typescript issue
BurntimeX Sep 19, 2024
e00e089
Improve creation of the dbo object list
BurntimeX Sep 19, 2024
5e022dd
Handle I18n sorting
BurntimeX Sep 20, 2024
2ad9942
Add column renderer for phrases
BurntimeX Sep 23, 2024
c39567b
Add column renderer for timestamps
BurntimeX Sep 23, 2024
f2b75f3
Increase default rows per page
BurntimeX Sep 23, 2024
70b7717
Fix initial sort order
BurntimeX Sep 23, 2024
697bfb2
Add rpc endpoint for deletion of user ranks
BurntimeX Sep 26, 2024
2bababb
Add context menu / row actions
BurntimeX Sep 26, 2024
08aed3f
Add popstate handling
BurntimeX Sep 27, 2024
f246662
Add filters
BurntimeX Sep 30, 2024
a67bcdc
Migrate cronjob log list to grid view
BurntimeX Sep 30, 2024
f5dccbd
Fix typescript issue
BurntimeX Oct 25, 2024
ba15420
Remove obsolete whitespace
BurntimeX Nov 4, 2024
8eec222
Add row links
BurntimeX Nov 4, 2024
eea6d85
Add api endpoint for exception details
BurntimeX Nov 4, 2024
a6918b6
Migrate exception log to grid view
BurntimeX Nov 4, 2024
5afdf6a
HTML / CSS overhaul
BurntimeX Nov 4, 2024
15a2b1c
Add option to hide columns
BurntimeX Nov 5, 2024
77af1ec
Use grid view filters for exception log filtering
BurntimeX Nov 5, 2024
0adf018
Improve visuals of the filter buttons
BurntimeX Nov 5, 2024
bf206f8
Add default filtering for non DBO grid views
BurntimeX Nov 6, 2024
3631f8b
Move grid view initialization to the constructor
BurntimeX Nov 6, 2024
63858c1
Add events and an option to add columns before / after existing columns
BurntimeX Nov 6, 2024
205c175
Add filter for i18n text columns
BurntimeX Nov 7, 2024
bc229c1
Allow filtering of the user rank grid view
BurntimeX Nov 7, 2024
6f873e4
Add date range form field
BurntimeX Nov 7, 2024
6290e6d
Add time filter for columns
BurntimeX Nov 7, 2024
f4d3e02
Add support for toggle actions
BurntimeX Nov 12, 2024
c9e9a97
Migration user option list to grid view
BurntimeX Nov 12, 2024
2d2eb31
Make action column sticky
BurntimeX Nov 12, 2024
e4b6a4b
Rename ArrayGridView to DataSourceGridView
BurntimeX Nov 12, 2024
a6765ba
Rename grid view namespace for consistent naming
BurntimeX Nov 12, 2024
55ca66f
Add column renderer for currencies
BurntimeX Nov 13, 2024
b1e0dd3
Add code documentation
BurntimeX Nov 13, 2024
041a71b
Improve code documentation
BurntimeX Nov 14, 2024
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
23 changes: 23 additions & 0 deletions com.woltlab.wcf/templates/shared_dateRangeFormField.tpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<input
type="{if $field->supportsTime()}datetime{else}date{/if}"
id="{$field->getPrefixedId()}_from"
name="{$field->getPrefixedId()}[from]"
value="{$field->getFromValue()}"
data-placeholder="{lang}wcf.date.period.start{/lang}"
{if !$field->getFieldClasses()|empty} class="{implode from=$field->getFieldClasses() item='class' glue=' '}{$class}{/implode}"{/if}
{if $field->isAutofocused()} autofocus{/if}
{if $field->isRequired()} required{/if}
{if $field->isImmutable()} disabled{/if}
{foreach from=$field->getFieldAttributes() key='attributeName' item='attributeValue'} {$attributeName}="{$attributeValue}"{/foreach}
>
<input
type="{if $field->supportsTime()}datetime{else}date{/if}"
id="{$field->getPrefixedId()}_to"
name="{$field->getPrefixedId()}[to]"
value="{$field->getToValue()}"
data-placeholder="{lang}wcf.date.period.end{/lang}"
{if !$field->getFieldClasses()|empty} class="{implode from=$field->getFieldClasses() item='class' glue=' '}{$class}{/implode}"{/if}
{if $field->isRequired()} required{/if}
{if $field->isImmutable()} disabled{/if}
{foreach from=$field->getFieldAttributes() key='attributeName' item='attributeValue'} {$attributeName}="{$attributeValue}"{/foreach}
>
63 changes: 63 additions & 0 deletions com.woltlab.wcf/templates/shared_exceptionLogDetails.tpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<dl>
<dt></dt>
<dd>
<button type="button" class="button jsCopyButton">{lang}wcf.acp.exceptionLog.exception.copy{/lang}</button>
<textarea rows="5" cols="40" class="jsCopyException" hidden>{$exception[0]}</textarea>
</dd>
</dl>
<dl>
<dt>{lang}wcf.acp.exceptionLog.search.exceptionID{/lang}</dt>
<dd>{$exceptionID}</dd>
</dl>
<dl>
<dt>{lang}wcf.acp.exceptionLog.exception.date{/lang}</dt>
<dd>{$exception[date]|plainTime}</dd>
</dl>
<dl>
<dt>{lang}wcf.acp.exceptionLog.exception.requestURI{/lang}</dt>
<dd>{$exception[requestURI]}</dd>
</dl>
<dl>
<dt>{lang}wcf.acp.exceptionLog.exception.referrer{/lang}</dt>
<dd>{$exception[referrer]}</dd>
</dl>
<dl>
<dt>{lang}wcf.acp.exceptionLog.exception.userAgent{/lang}</dt>
<dd>{$exception[userAgent]}</dd>
</dl>
<dl>
<dt>{lang}wcf.acp.exceptionLog.exception.memory{/lang}</dt>
<dd>{$exception[peakMemory]|filesizeBinary} / {if $exception[maxMemory] == -1}&infin;{else}{$exception[maxMemory]|filesizeBinary}{/if}</dd>
</dl>
{foreach from=$exception[chain] item=chain}
<dl>
<dt>{lang}wcf.acp.exceptionLog.exception.message{/lang}</dt>
<dd>{$chain[message]}</dd>
</dl>
<dl>
<dt>{lang}wcf.acp.exceptionLog.exception.class{/lang}</dt>
<dd>{$chain[class]}</dd>
</dl>
<dl>
<dt>{lang}wcf.acp.exceptionLog.exception.file{/lang}</dt>
<dd>{$chain[file]} ({$chain[line]})</dd>
</dl>
{if !$chain[information]|empty}
{foreach from=$chain[information] item=extraInformation}
<dl>
<dt>{$extraInformation[0]}</dt>
<dd style="white-space: pre-wrap;">{$extraInformation[1]}</dd>
</dl>
{/foreach}
{/if}
<dl>
<dt>{lang}wcf.acp.exceptionLog.exception.stacktrace{/lang}</dt>
<dd>
<ol start="0" class="nativeList">
{foreach from=$chain[stack] item=stack}
<li>{$stack[file]} ({$stack[line]}): {$stack[class]}{$stack[type]}{$stack[function]}(&hellip;)</li>
{/foreach}
</ol>
</dd>
</dl>
{/foreach}
73 changes: 73 additions & 0 deletions com.woltlab.wcf/templates/shared_gridView.tpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
<div class="gridView">
{if $view->isFilterable()}
<div class="gridView__filterBar">
<div class="gridView__filters" id="{$view->getID()}_filters">
{foreach from=$view->getActiveFilters() item='value' key='key'}
<button type="button" class="button small" data-filter="{$key}" data-filter-value="{$value}">
{icon name='circle-xmark'}
{$view->getFilterLabel($key)}
</button>
{/foreach}
</div>
<div class="gridView__filterButton">
<button type="button" class="button small" id="{$view->getID()}_filterButton" data-endpoint="{$view->getFilterActionEndpoint()}">
{icon name='gear'}
Filter
</button>
</div>
</div>
{/if}

<table class="gridView__table" id="{$view->getID()}_table"{if !$view->countRows()} hidden{/if}>
<thead>
<tr class="gridView__headerRow">
{foreach from=$view->getVisibleColumns() item='column'}
<th
class="gridView__headerColumn {$column->getClasses()}"
data-id="{$column->getID()}"
data-sortable="{$column->isSortable()}"
>
{if $column->isSortable()}
<button type="button" class="gridView__headerColumn__button">
{unsafe:$column->getLabel()}
</button>
{else}
{unsafe:$column->getLabel()}
{/if}
</th>
{/foreach}
{if $view->hasActions()}
<th class="gridView__headerColumn gridView__actionColumn"></th>
{/if}
</td>
</thead>
<tbody>
{unsafe:$view->renderRows()}
</tbody>
</table>

<div class="gridView__pagination">
<woltlab-core-pagination id="{$view->getID()}_pagination" page="{$view->getPageNo()}" count="{$view->countPages()}"></woltlab-core-pagination>
</div>

<woltlab-core-notice type="info" id="{$view->getID()}_noItemsNotice"{if $view->countRows()} hidden{/if}>{lang}wcf.global.noItems{/lang}</woltlab-core-notice>
</div>

<script data-relocate="true">
require(['WoltLabSuite/Core/Component/GridView'], ({ GridView }) => {
new GridView(
'{unsafe:$view->getID()|encodeJs}',
'{unsafe:$view->getClassName()|encodeJS}',
{$view->getPageNo()},
'{unsafe:$view->getBaseUrl()|encodeJS}',
'{unsafe:$view->getSortField()|encodeJS}',
'{unsafe:$view->getSortOrder()|encodeJS}',
new Map([
{foreach from=$view->getParameters() key='name' item='value'}
['{unsafe:$name|encodeJs}', '{unsafe:$value|encodeJs}'],
{/foreach}
])
);
});
</script>
{unsafe:$view->renderActionInitialization()}
45 changes: 45 additions & 0 deletions com.woltlab.wcf/templates/shared_gridViewRows.tpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@

{foreach from=$view->getRows() item='row'}
<tr class="gridView__row" data-object-id="{$view->getObjectID($row)}">
{foreach from=$view->getVisibleColumns() item='column'}
<td class="gridView__column {$column->getClasses()}">
{unsafe:$view->renderColumn($column, $row)}
</td>
{/foreach}
{if $view->hasActions()}
<td class="gridView__column gridView__actionColumn">
<div class="gridView__actionColumn__buttons">
{foreach from=$view->getQuickActions() item='action'}
{unsafe:$view->renderAction($action, $row)}
{/foreach}

{if $view->hasDropdownActions()}
{hascontent}
<div class="dropdown">
<button type="button" class="gridViewActions button small dropdownToggle" aria-label="{lang}wcf.global.button.more{/lang}">
{icon name='ellipsis-vertical'}
</button>

<ul class="dropdownMenu">
{content}
{foreach from=$view->getDropdownActions() item='action'}
{if $action->isAvailable($row)}
<li>
{unsafe:$view->renderAction($action, $row)}
</li>
{/if}
{/foreach}
{/content}
</ul>
</div>
{hascontentelse}
<button type="button" disabled class="button small" aria-label="{lang}wcf.global.button.more{/lang}">
{icon name='ellipsis-vertical'}
</button>
{/hascontent}
{/if}
</div>
</td>
{/if}
</tr>
{/foreach}
38 changes: 38 additions & 0 deletions ts/WoltLabSuite/Core/Acp/Controller/ExceptionLog/View.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/**
* Shows the dialog that shows exception details.
*
* @author Marcel Werk
* @copyright 2001-2024 WoltLab GmbH
* @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
* @since 6.1
*/

import { renderException } from "WoltLabSuite/Core/Api/Exceptions/RenderException";
import { copyTextToClipboard } from "WoltLabSuite/Core/Clipboard";
import { dialogFactory } from "WoltLabSuite/Core/Component/Dialog";
import { promiseMutex } from "WoltLabSuite/Core/Helper/PromiseMutex";
import { wheneverFirstSeen } from "WoltLabSuite/Core/Helper/Selector";
import { getPhrase } from "WoltLabSuite/Core/Language";

async function showDialog(button: HTMLElement): Promise<void> {
const response = await renderException(button.closest("tr")!.dataset.objectId!);
if (!response.ok) {
return;
}

const dialog = dialogFactory().fromHtml(response.value.template).withoutControls();
dialog.content.querySelector(".jsCopyButton")?.addEventListener("click", () => {
void copyTextToClipboard(dialog.content.querySelector<HTMLTextAreaElement>(".jsCopyException")!.value);
});

dialog.show(getPhrase("wcf.acp.exceptionLog.exception.message"));
}

export function setup(): void {
wheneverFirstSeen(".jsExceptionLogEntry", (button) => {
button.addEventListener(
"click",
promiseMutex(() => showDialog(button)),
);
});
}
22 changes: 22 additions & 0 deletions ts/WoltLabSuite/Core/Api/DeleteObject.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/**
* Deletes an object.
*
* @author Marcel Werk
* @copyright 2001-2024 WoltLab GmbH
* @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
* @since 6.1
* @woltlabExcludeBundle tiny
*/

import { prepareRequest } from "WoltLabSuite/Core/Ajax/Backend";
import { ApiResult, apiResultFromError, apiResultFromValue } from "./Result";

export async function deleteObject(endpoint: string): Promise<ApiResult<[]>> {
try {
await prepareRequest(endpoint).delete().fetchAsJson();
} catch (e) {
return apiResultFromError(e);
}

return apiResultFromValue([]);
}
28 changes: 28 additions & 0 deletions ts/WoltLabSuite/Core/Api/Exceptions/RenderException.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/**
* Gets the html code for the rendering of a exception log entry.
*
* @author Marcel Werk
* @copyright 2001-2024 WoltLab GmbH
* @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
* @since 6.2
*/

import { prepareRequest } from "WoltLabSuite/Core/Ajax/Backend";
import { ApiResult, apiResultFromError, apiResultFromValue } from "../Result";

type Response = {
template: string;
};

export async function renderException(exceptionId: string): Promise<ApiResult<Response>> {
const url = new URL(`${window.WSC_RPC_API_URL}core/exceptions/${exceptionId}/render`);

let response: Response;
try {
response = (await prepareRequest(url).get().fetchAsJson()) as Response;
} catch (e) {
return apiResultFromError(e);
}

return apiResultFromValue(response);
}
43 changes: 43 additions & 0 deletions ts/WoltLabSuite/Core/Api/GridViews/GetRows.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { prepareRequest } from "WoltLabSuite/Core/Ajax/Backend";
import { ApiResult, apiResultFromError, apiResultFromValue } from "../Result";

type Response = {
template: string;
pages: number;
totalRows: number;
filterLabels: ArrayLike<string>;
};

export async function getRows(
gridViewClass: string,
pageNo: number,
sortField: string = "",
sortOrder: string = "ASC",
filters?: Map<string, string>,
gridViewParameters?: Map<string, string>,
): Promise<ApiResult<Response>> {
const url = new URL(`${window.WSC_RPC_API_URL}core/gridViews/rows`);
url.searchParams.set("gridView", gridViewClass);
url.searchParams.set("pageNo", pageNo.toString());
url.searchParams.set("sortField", sortField);
url.searchParams.set("sortOrder", sortOrder);
if (filters) {
filters.forEach((value, key) => {
url.searchParams.set(`filters[${key}]`, value);
});
}
if (gridViewParameters) {
gridViewParameters.forEach((value, key) => {
url.searchParams.set(`gridViewParameters[${key}]`, value);
});
}

let response: Response;
try {
response = (await prepareRequest(url).get().allowCaching().disableLoadingIndicator().fetchAsJson()) as Response;
} catch (e) {
return apiResultFromError(e);
}

return apiResultFromValue(response);
}
22 changes: 22 additions & 0 deletions ts/WoltLabSuite/Core/Api/PostObject.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/**
* Sends a post request to the given endpoint.
*
* @author Marcel Werk
* @copyright 2001-2024 WoltLab GmbH
* @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
* @since 6.2
* @woltlabExcludeBundle tiny
*/

import { prepareRequest } from "WoltLabSuite/Core/Ajax/Backend";
import { ApiResult, apiResultFromError, apiResultFromValue } from "./Result";

export async function postObject(endpoint: string): Promise<ApiResult<[]>> {
try {
await prepareRequest(endpoint).post().fetchAsJson();
} catch (e) {
return apiResultFromError(e);
}

return apiResultFromValue([]);
}
Loading
Loading