@digital-blueprint/formalize-app
Version:
[GitHub Repository](https://github.com/digital-blueprint/formalize-app) | [npmjs package](https://www.npmjs.com/package/@digital-blueprint/formalize-app) | [Unpkg CDN](https://unpkg.com/browse/@digital-blueprint/formalize-app/) | [Formalize Bundle](https:
673 lines (607 loc) • 27.1 kB
JavaScript
// @ts-nocheck
import {html} from 'lit';
import {classMap} from 'lit/directives/class-map.js';
import {ScopedElementsMixin, Icon, MiniSpinner, DBPSelect} from '@dbp-toolkit/common';
import DBPLitElement from '@dbp-toolkit/common/dbp-lit-element';
import {setOverridesByGlobalCache} from '@dbp-toolkit/common/i18next.js';
import {createInstance} from './i18n.js';
import {CustomTabulatorTable} from './table-components.js';
import {SUBMISSION_STATES} from './utils.js';
import {MANAGE_FORMS_COMPONENT_STYLES} from './manage-forms-component-styles.js';
export class ManageFormSubmissionsPage extends ScopedElementsMixin(DBPLitElement) {
constructor() {
super();
this._i18n = createInstance();
this.lang = this._i18n.language;
this.langDir = '';
this.showFormsTable = false;
this.showSubmissionTables = false;
this.loadingSubmissionTables = false;
this.activeFormName = '';
this.columnConfigurationStorageKeys = {draft: '', submitted: ''};
this.paginationSizeStorageKey = '';
this.createSubmissionUrl = '';
this.hideCreateSubmissionButton = false;
this.enabledStates = {draft: false, submitted: false};
this.noSubmissionAvailable = {draft: true, submitted: true};
this.searchWidgetIsOpen = {draft: false, submitted: false};
this.isEditSubmissionEnabled = {draft: false, submitted: false};
this.enableSubmissionPermissionEditing = false;
this.isEditSubmissionPermissionEnabled = {draft: false, submitted: false};
this.isBatchTaggingEnabled = {draft: false, submitted: false};
this.isDeleteAllSubmissionEnabled = {draft: false, submitted: false};
this.isDeleteSelectedSubmissionEnabled = {draft: false, submitted: false};
this.optionsSubmissions = {draft: {}, submitted: {}};
this.submissions = {draft: [], submitted: []};
this.selectedRowCount = {draft: 0, submitted: 0};
this.allRowCount = {draft: 0, submitted: 0};
this.visibleRowCount = {draft: 0, submitted: 0};
this.searchIsActive = {draft: false, submitted: false};
this.submissionsHasAttachment = {draft: false, submitted: false};
}
static get scopedElements() {
return {
'dbp-icon': Icon,
'dbp-mini-spinner': MiniSpinner,
'dbp-tabulator-table': CustomTabulatorTable,
'dbp-select': DBPSelect,
};
}
static get properties() {
return {
...super.properties,
lang: {type: String},
langDir: {type: String, attribute: 'lang-dir'},
showFormsTable: {type: Boolean, attribute: false},
showSubmissionTables: {type: Boolean, attribute: false},
loadingSubmissionTables: {type: Boolean, attribute: false},
activeFormName: {type: String, attribute: false},
columnConfigurationStorageKeys: {type: Object, attribute: false},
paginationSizeStorageKey: {type: String, attribute: false},
createSubmissionUrl: {type: String, attribute: false},
hideCreateSubmissionButton: {type: Boolean, attribute: false},
enabledStates: {type: Object, attribute: false},
noSubmissionAvailable: {type: Object, attribute: false},
searchWidgetIsOpen: {type: Object, attribute: false},
isEditSubmissionEnabled: {type: Object, attribute: false},
enableSubmissionPermissionEditing: {type: Boolean, attribute: false},
isEditSubmissionPermissionEnabled: {type: Object, attribute: false},
isBatchTaggingEnabled: {type: Object, attribute: false},
isDeleteAllSubmissionEnabled: {type: Object, attribute: false},
isDeleteSelectedSubmissionEnabled: {type: Object, attribute: false},
optionsSubmissions: {type: Object, attribute: false},
submissions: {type: Object, attribute: false},
selectedRowCount: {type: Object, attribute: false},
allRowCount: {type: Object, attribute: false},
visibleRowCount: {type: Object, attribute: false},
searchIsActive: {type: Object, attribute: false},
submissionsHasAttachment: {type: Object, attribute: false},
};
}
update(changedProperties) {
changedProperties.forEach((oldValue, propName) => {
if (propName === 'lang') {
void this._i18n.changeLanguage(this.lang);
}
if ((propName === 'lang' || propName === 'langDir') && this.langDir) {
void setOverridesByGlobalCache(this._i18n, this);
}
});
super.update(changedProperties);
}
connectedCallback() {
super.connectedCallback();
if (this.langDir) {
void setOverridesByGlobalCache(this._i18n, this);
}
}
static get styles() {
return MANAGE_FORMS_COMPONENT_STYLES;
}
getSubmissionTable(state) {
return this.renderRoot?.querySelector(`#tabulator-table-submissions-${state}`) ?? null;
}
getSearchbar(state) {
return this.renderRoot?.querySelector(`#searchbar--${state}`) ?? null;
}
getSearchSelect(state) {
return this.renderRoot?.querySelector(`#search-select--${state}`) ?? null;
}
getSearchOperator(state) {
return this.renderRoot?.querySelector(`#search-operator--${state}`) ?? null;
}
getActionsContainers() {
return this.renderRoot?.querySelectorAll('.actions-container') ?? [];
}
closeAllSearchWidgets() {
this.searchWidgetIsOpen = {
draft: false,
submitted: false,
};
}
handleBackToOverview() {
this.closeAllSearchWidgets();
this.dispatchEvent(
new CustomEvent('back-to-overview', {
bubbles: true,
composed: true,
}),
);
}
handleSearch(state) {
this.dispatchEvent(
new CustomEvent('submission-search', {
detail: {state},
bubbles: true,
composed: true,
}),
);
}
handleResetSearch(state) {
this.dispatchEvent(
new CustomEvent('submission-search-reset', {
detail: {state},
bubbles: true,
composed: true,
}),
);
}
handleAction(action, state, payload = null) {
this.dispatchEvent(
new CustomEvent('submission-action', {
detail: {action, state, payload},
bubbles: true,
composed: true,
}),
);
}
toggleSearchFilters(state) {
const open = !this.searchWidgetIsOpen[state];
this.searchWidgetIsOpen = {
...this.searchWidgetIsOpen,
[state]: open,
};
this.dispatchEvent(
new CustomEvent('submission-search-toggle', {
detail: {state, open},
bubbles: true,
composed: true,
}),
);
}
getTableHeaderOptions(state) {
const i18n = this._i18n;
if (this.submissions[state].length === 0) {
return [];
}
let options = [];
options.push(html`
<option value="all">${i18n.t('manage-forms.all-columns')}</option>
`);
let cols = Object.keys(this.submissions[state][0]);
for (let col of cols) {
if (col && col !== 'htmlButtons') {
options.push(html`
<option value="${col}">${col}</option>
`);
}
}
return options;
}
getTableFilterOptions() {
const i18n = this._i18n;
return html`
<option value="like">${i18n.t('manage-forms.search-operator-like')}</option>
<option value="=">${i18n.t('manage-forms.search-operator-equal')}</option>
<option value="!=">${i18n.t('manage-forms.search-operator-notequal')}</option>
<option value="starts">${i18n.t('manage-forms.search-operator-starts')}</option>
<option value="ends">${i18n.t('manage-forms.search-operator-ends')}</option>
<option value="<">${i18n.t('manage-forms.search-operator-less')}</option>
<option value="<=">${i18n.t('manage-forms.search-operator-lessthanorequal')}</option>
<option value=">">${i18n.t('manage-forms.search-operator-greater')}</option>
<option value=">=">${i18n.t('manage-forms.search-operator-greaterorequal')}</option>
<option value="regex">${i18n.t('manage-forms.search-operator-regex')}</option>
<option value="keywords">${i18n.t('manage-forms.search-operator-keywords')}</option>
`;
}
getExportOptions(state) {
const i18n = this._i18n;
// If a filter is active only export the visible rows.
// If anything is selected export the selected rows.
// If no filter is active and nothing is selected export all rows.
const exportCount =
this.selectedRowCount[state] > 0
? this.selectedRowCount[state]
: this.searchIsActive[state]
? this.visibleRowCount[state]
: this.allRowCount[state];
const exportActions = [];
exportActions.push({
value: 'csv',
label: i18n.t('manage-forms.export-csv-label', {n: exportCount}),
iconName: 'download',
});
exportActions.push({
value: 'xlsx',
label: i18n.t('manage-forms.export-xlsx-label', {n: exportCount}),
iconName: 'download',
});
exportActions.push({
value: 'pdf',
label: i18n.t('manage-forms.export-pdf-label', {n: exportCount}),
iconName: 'download',
});
if (this.submissionsHasAttachment[state]) {
exportActions.push({
value: 'attachments',
label: i18n.t('manage-forms.export-attachments-label', {n: exportCount}),
iconName: 'folder',
});
}
return exportActions;
}
renderExportWidget(state) {
const i18n = this._i18n;
return html`
<div class="export-container">
<dbp-select
id="export-dropdown--${state}"
class="export-dropdown"
@change="${(event) => {
this.handleAction('export', state, {event: event});
}}"
label="${i18n.t('manage-forms.default-export-select')}"
align="right"
allow-expand
.options="${this.getExportOptions(state)}"></dbp-select>
</div>
`;
}
/**
* Handle change events from the actions widget dropdown and buttons
* @param {CustomEvent} event
*/
handleActionsDropdownChange(event, state) {
// Handle form-specific actions
if (event.detail && event.detail.option && event.detail.value) {
const option = event.detail.option;
// const value = event.detail.value;
if (option.value === 'edit-submission') {
this.handleAction('edit-submission', state, {event});
return;
}
if (option.value === 'batch-tagging') {
this.handleAction('batch-tagging', state);
return;
}
if (option.value === 'edit-permission') {
this.handleAction('edit-permission', state);
return;
}
if (option.value === 'delete-all') {
this.handleAction('delete-all', state);
return;
}
if (option.value === 'delete-selected') {
this.handleAction('delete-selected', state);
return;
}
}
}
renderActionsWidget(state) {
const i18n = this._i18n;
const submissionActions = [];
submissionActions.push({
value: 'edit-submission',
disabled: !this.isEditSubmissionEnabled[state],
label: i18n.t('manage-forms.edit-submission-button-text'),
iconName: 'pencil',
});
submissionActions.push({
value: 'batch-tagging',
disabled: !this.isBatchTaggingEnabled[state],
label: i18n.t('manage-forms.batch-tagging-button-text'),
iconName: 'tags',
});
if (this.enableSubmissionPermissionEditing) {
submissionActions.push({
value: 'edit-permission',
disabled: !this.isEditSubmissionPermissionEnabled[state],
label: i18n.t('manage-forms.edit-permission-button-text'),
iconName: 'edit-permission',
});
}
if (this.isDeleteAllSubmissionEnabled[state]) {
submissionActions.push({
value: 'delete-all',
label: i18n.t('manage-forms.delete-all-submissions-button-text', {
n: this.allRowCount[state],
}),
iconName: 'trash',
});
}
if (this.isDeleteSelectedSubmissionEnabled[state]) {
submissionActions.push({
value: 'delete-selected',
label: i18n.t('manage-forms.delete-selected-submissions-button-text', {
n: this.selectedRowCount[state],
}),
iconName: 'delete-selection',
});
}
return html`
<div class="actions-container" id="actions-container--${state}">
<dbp-select
id="action-dropdown--${state}"
class="action-dropdown-state"
?disabled=${!this.isActionAvailable[state]}
@change="${(event) => this.handleActionsDropdownChange(event, state)}"
label="${i18n.t('manage-forms.actions-button-text')}"
align="left"
allow-expand
.options="${submissionActions}"></dbp-select>
</div>
`;
}
renderSearchWidget(state, tableName) {
const i18n = this._i18n;
return html`
<div class="search-input">
<label for="searchbar--${state}">
${i18n.t('manage-forms.search-input-label')}:
</label>
<input
type="text"
id="searchbar--${state}"
data-state="${state}"
class="searchbar"
placeholder="${i18n.t('manage-forms.searchbar-placeholder')}"
@input=${() => this.handleSearch(state)} />
<button
class="button search-button"
id="search-button--${state}"
title="${i18n.t('manage-forms.search-button')}"
aria-label="${i18n.t('manage-forms.search-button-aria', {tableName})}"
@click="${() => {
this.handleSearch(state);
}}">
<dbp-icon name="search" aria-hidden="true"></dbp-icon>
</button>
<button
class="button search-toggle-filters-button"
id="search-toggle-filters-button--${state}"
aria-expanded="${this.searchWidgetIsOpen[state]}"
aria-controls="search-filter-columns--${state} search-filter-operator--${state}"
title="${i18n.t('manage-forms.open-search-filters')}"
@click="${() => {
this.toggleSearchFilters(state);
}}">
<dbp-icon name="chevron-down" aria-hidden="true"></dbp-icon>
<span class="button-text">
${
this.searchWidgetIsOpen[state]
? i18n.t('manage-forms.close-search-filters')
: i18n.t('manage-forms.open-search-filters')
}
</span>
</button>
</div>
<div
id="search-filter-columns--${state}"
role="region"
aria-label="${i18n.t('manage-forms.search-filters-region')}"
class="search-filter-columns ${classMap({
open: this.searchWidgetIsOpen[state],
})}">
<label for="search-select-${state}">${i18n.t('manage-forms.search-in')}:</label>
<select
id="search-select--${state}"
class="button dropdown-menu search-select"
title="${i18n.t('manage-forms.search-in-column')}"
@change="${() => {
this.handleSearch(state);
}}">
<optgroup label="${i18n.t('manage-forms.search-in-column')}">
<legend>${i18n.t('manage-forms.search-in-column')}:</legend>
${this.getTableHeaderOptions(state)}
</optgroup>
</select>
<!-- Purely decorative chevron, the adjacent select already carries the label -->
<dbp-icon name="chevron-down" aria-hidden="true"></dbp-icon>
</div>
<div
id="search-filter-operator--${state}"
role="region"
aria-label="${i18n.t('manage-forms.search-operator')}"
class="search-filter-operator ${classMap({
open: this.searchWidgetIsOpen[state],
})}">
<label for="search-operator--${state}">
${i18n.t('manage-forms.search-operator')}:
</label>
<select
id="search-operator--${state}"
title="${i18n.t('manage-forms.search-operator')}"
class="button dropdown-menu search-operator"
@change="${() => {
this.handleSearch(state);
}}">
<optgroup label="${i18n.t('manage-forms.search-operator')}">
<legend>${i18n.t('manage-forms.search-operator')}:</legend>
${this.getTableFilterOptions()}
</optgroup>
</select>
<!-- Purely decorative chevron, the adjacent select already carries the label -->
<dbp-icon name="chevron-down" aria-hidden="true"></dbp-icon>
</div>
<button
class="button search-toggle-filters-button"
id="search-toggle-filters-button--${state}"
aria-expanded="${this.searchWidgetIsOpen[state]}"
aria-controls="search-filter-columns--${state} search-filter-operator--${state}"
title="${i18n.t('manage-forms.open-search-filters')}"
@click="${() => {
this.toggleSearchFilters(state);
}}">
<dbp-icon name="chevron-down" aria-hidden="true"></dbp-icon>
<span class="button-text">
${
this.searchWidgetIsOpen[state]
? i18n.t('manage-forms.close-search-filters')
: i18n.t('manage-forms.open-search-filters')
}
</span>
</button>
`;
}
renderStatusBar(state) {
const i18n = this._i18n;
const visibleCount = this.visibleRowCount?.[state] ?? 0;
const selectedCount = this.selectedRowCount?.[state] ?? 0;
return html`
<div class="statusbar" role="status" aria-live="polite" aria-atomic="true">
<span class="selection-info">
${i18n.t('manage-forms.n-items-shown-label', {
n: visibleCount,
})}
${
selectedCount > 0
? html`
,
${i18n.t('manage-forms.n-items-selected-label', {
n: selectedCount,
})}
`
: ''
}
</span>
<button
class="reset-search"
?disabled="${this.searchIsActive[state] === false}"
@click="${() => {
this.handleResetSearch(state);
}}">
<dbp-icon name="spinner-arrow" aria-hidden="true"></dbp-icon>
${i18n.t('manage-forms.reset-search-label')}
</button>
</div>
`;
}
handleSelectionCountChanged(state, event) {
const count = event.detail?.count ?? 0;
this.selectedRowCount = {
...this.selectedRowCount,
[state]: count,
};
}
render() {
const i18n = this._i18n;
const submissionTableTitle = {
draft: i18n.t('manage-forms.submission-table-draft-title'),
submitted: i18n.t('manage-forms.submission-table-submitted-title'),
};
return html`
<div
class="table-wrapper submissions${classMap({
hideWithoutDisplay: !this.showSubmissionTables,
})}">
<div class="submissions-top-bar">
<span class="back-navigation">
<button
type="button"
@click="${() => this.handleBackToOverview()}"
title="${i18n.t('manage-forms.back-text')}">
<dbp-icon name="chevron-left"></dbp-icon>
${i18n.t('manage-forms.back-text')}
</button>
</span>
<span
class="loading submissions-spinner ${classMap({
hidden: !this.loadingSubmissionTables || this.showFormsTable,
})}">
<dbp-mini-spinner text="${i18n.t('loading-message')}"></dbp-mini-spinner>
</span>
</div>
<div class="table-header submissions">
<h2 class="form-name">${this.activeFormName}</h2>
${
this.createSubmissionUrl && !this.hideCreateSubmissionButton
? html`
<a
class="button is-primary"
href="${this.createSubmissionUrl}"
target="_blank">
<dbp-icon name="plus" aria-hidden="true"></dbp-icon>
${i18n.t('manage-forms.create-submission-button')}
</a>
`
: ''
}
</div>
</div>
<div
class="container submissions-table ${classMap({
hidden: !this.showSubmissionTables,
})}">
${Object.values(SUBMISSION_STATES).map(
(state) => html`
<div
class="${classMap({
hidden: this.enabledStates[state] ? false : true,
})}">
<h3 class="table-title">${submissionTableTitle[state]}</h3>
<div
class="${classMap({
open: this.searchWidgetIsOpen[state],
'table-action-header': true,
[`table-action-header--${state}`]: true,
})}">
${
this.noSubmissionAvailable[state] === true
? ''
: html`
${this.renderActionsWidget(state)}
${this.renderSearchWidget(
state,
submissionTableTitle[state],
)}
${this.renderExportWidget(state)}
`
}
</div>
${
this.noSubmissionAvailable[state] === true
? ''
: html`
${this.renderStatusBar(state)}
`
}
<dbp-tabulator-table
lang="${this.lang}"
class="tabulator-table tabulator-table--${state}"
id="tabulator-table-submissions-${state}"
data-state="${state}"
identifier="submissions-table-${state}"
.options=${this.optionsSubmissions[state]}
pagination-size="5"
.paginationSizeStorageKey=${this.paginationSizeStorageKey}
sticky-header
select-rows-enabled
column-configuration-enabled
column-configuration-in-header
.columnConfigurationExcludedFields=${['rowIndex', 'htmlButtons']}
.columnConfigurationStorageKey=${
this.columnConfigurationStorageKeys[state]
}
@dbp-tabulator-table-selection-count-changed=${(event) =>
this.handleSelectionCountChanged(
state,
event,
)}></dbp-tabulator-table>
</div>
`,
)}
</div>
`;
}
}