@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:
1,652 lines • 55.3 kB
JavaScript
import {css, html} from 'lit';
import {html as staticHtml, unsafeStatic} from 'lit/static-html.js';
import {ref, createRef} from 'lit/directives/ref.js';
import {
ScopedElementsMixin,
Button,
Icon,
IconButton,
MiniSpinner,
DBPSelect,
sendNotification,
} from '@dbp-toolkit/common';
import * as commonUtils from '@dbp-toolkit/common/utils';
import * as commonStyles from '@dbp-toolkit/common/styles.js';
import DBPFormalizeLitElement from './dbp-formalize-lit-element.js';
import {formatDate, SUBMISSION_STATES_BINARY} from './utils.js';
import {DeletionConfirmationModal} from './deletion-confirmation-modal.js';
import {CustomTabulatorTable, GetDetailsButton} from './table-components.js';
import {Modal} from '@dbp-toolkit/common/src/modal.js';
import {Notification} from '@dbp-toolkit/notification';
import {
gatherFormDataFromElement,
validateRequiredFields,
} from '@dbp-toolkit/form-elements/src/utils.js';
function parseSubmissionData(submission) {
if (!submission?.dataFeedElement) {
return {};
}
try {
return JSON.parse(submission.dataFeedElement);
} catch (error) {
console.error('Failed to parse item data:', error);
return {};
}
}
function parseListAttribute(value) {
if (Array.isArray(value)) {
return value.map((item) => `${item}`.trim()).filter((item) => item !== '');
}
if (typeof value !== 'string') {
return [];
}
const trimmedValue = value.trim();
if (trimmedValue === '') {
return [];
}
try {
const parsedValue = JSON.parse(trimmedValue);
if (Array.isArray(parsedValue)) {
return parsedValue.map((item) => `${item}`.trim()).filter((item) => item !== '');
}
} catch {
// Fallback to comma-separated values for HTML attributes.
}
return trimmedValue
.split(',')
.map((item) => item.trim())
.filter((item) => item !== '');
}
function getLocalizedFormName(form, lang) {
const localizedName = form?.localizedNames?.find((name) => name.languageTag === lang);
return localizedName?.name || form?.name || form?.identifier || '';
}
function getSentenceCaseItemName(name, lang) {
if (lang !== 'en' || !name) {
return name;
}
return name.charAt(0).toLowerCase() + name.slice(1);
}
const itemFormsCache = new Map();
const itemFormsInflight = new Map();
const editModalNotificationId = 'manage-fields-edit-modal-notification';
class ManageFields extends ScopedElementsMixin(DBPFormalizeLitElement) {
constructor() {
super();
this.formRef = createRef();
this.itemModules = [];
this.itemFormEntries = [];
this.activeModule = null;
this.activeForm = null;
this.items = [];
this.selectedItem = null;
this.mode = 'forms';
this.loading = true;
this.loadingItems = false;
this.saving = false;
this.deletingItems = false;
this.selectedItemCount = 0;
this.lastModifiedByName = '';
this.errorMessage = '';
this.itemFrontendKeys = '';
this._hasLoadedForms = false;
this._isLoadingForms = false;
this._lastFrontendKeys = '';
this._routeApplyPromise = Promise.resolve();
this._submissionsLoadPromise = null;
this._submissionsLoadFormIdentifier = '';
this._userNameCache = new Map();
this._failedUserNameLookups = new Set();
}
static get scopedElements() {
return {
'dbp-button': Button,
'dbp-icon': Icon,
'dbp-icon-button': IconButton,
'dbp-select': DBPSelect,
'dbp-mini-spinner': MiniSpinner,
'dbp-formalize-get-details-button': GetDetailsButton,
'dbp-tabulator-table': CustomTabulatorTable,
'dbp-formalize-deletion-confirmation-modal': DeletionConfirmationModal,
'dbp-modal': Modal,
'dbp-notification': Notification,
};
}
static get properties() {
return {
...super.properties,
itemModules: {type: Array, attribute: false},
itemFormEntries: {type: Array, attribute: false},
activeModule: {type: Object, attribute: false},
activeForm: {type: Object, attribute: false},
items: {type: Array, attribute: false},
selectedItem: {type: Object, attribute: false},
mode: {type: String, attribute: false},
loading: {type: Boolean, attribute: false},
loadingItems: {type: Boolean, attribute: false},
saving: {type: Boolean, attribute: false},
deletingItems: {type: Boolean, attribute: false},
selectedItemCount: {type: Number, attribute: false},
lastModifiedByName: {type: String, attribute: false},
errorMessage: {type: String, attribute: false},
itemFrontendKeys: {type: String, attribute: 'item-frontend-keys'},
};
}
async initialize() {
await this.loadItemForms();
}
async loadItemForms() {
if (!this.auth?.token || this._isLoadingForms) {
return;
}
const cacheKey = `${this.basePath}|${this.entryPointUrl}|${this.itemFrontendKeys}`;
const cachedResult = itemFormsCache.get(cacheKey);
if (cachedResult) {
this.itemModules = cachedResult.itemModules;
this.itemFormEntries = cachedResult.itemFormEntries;
this._hasLoadedForms = true;
this._lastFrontendKeys = this.itemFrontendKeys;
await this.applyRoute();
return;
}
this._isLoadingForms = true;
this.loading = true;
this.errorMessage = '';
try {
let inflight = itemFormsInflight.get(cacheKey);
if (!inflight) {
inflight = this.loadItemFormEntries().finally(() => {
itemFormsInflight.delete(cacheKey);
});
itemFormsInflight.set(cacheKey, inflight);
}
const result = await inflight;
itemFormsCache.set(cacheKey, result);
this.itemModules = result.itemModules;
this.itemFormEntries = result.itemFormEntries;
this._hasLoadedForms = true;
this._lastFrontendKeys = this.itemFrontendKeys;
await this.applyRoute();
} catch (error) {
console.error(error);
this.errorMessage = this._i18n.t('manage-fields.load-error');
} finally {
this._isLoadingForms = false;
this.loading = false;
}
}
async loadItemFormEntries() {
const itemModules = await this.getItemModules();
const itemFormEntries = await this.getMatchingForms(itemModules);
return {itemModules, itemFormEntries};
}
async getItemModules() {
const response = await fetch(this.basePath + 'modules.json');
const data = await response.json();
const itemEntries = Object.entries(data.items || {});
const frontendKeys = parseListAttribute(this.itemFrontendKeys);
const modules = [];
for (const [, path] of itemEntries) {
const absolutePath = new URL(path, window.location.origin + this.basePath).href;
const module = await import(absolutePath);
const object = new module.default();
const frontendKey = object.getFormFrontendKey?.();
if (
object.getFormComponent &&
object.getItemText &&
(frontendKeys.length === 0 || frontendKeys.includes(frontendKey))
) {
modules.push(object);
}
}
return modules;
}
async getMatchingForms(itemModules) {
if (itemModules.length === 0) {
return [];
}
const frontendKeys = parseListAttribute(this.itemFrontendKeys);
const formsUrl = new URL(this.entryPointUrl + '/formalize/forms');
formsUrl.searchParams.set('perPage', '9999');
frontendKeys.forEach((frontendKey) => {
formsUrl.searchParams.append('whereFrontendKeyIn[]', frontendKey);
});
const response = await fetch(formsUrl.href, {
headers: {
'Content-Type': 'application/ld+json',
Authorization: 'Bearer ' + this.getToken(),
},
});
if (!response.ok) {
this.handleErrorResponse(response);
return [];
}
const data = await response.json();
const forms = data['hydra:member'] || [];
const entries = [];
for (const form of forms) {
const module = itemModules.find((candidate) => {
const frontendKey = candidate.getFormFrontendKey?.();
const formIdentifier = candidate.getFormIdentifier?.();
return (
(frontendKey && form.frontendKey === frontendKey) ||
(formIdentifier && form.identifier === formIdentifier)
);
});
if (module) {
entries.push({form, module});
}
}
return entries;
}
async loadSubmissions() {
if (!this.activeForm) {
this.items = [];
return;
}
const formIdentifier = this.activeForm.identifier;
if (
this._submissionsLoadPromise !== null &&
this._submissionsLoadFormIdentifier === formIdentifier
) {
await this._submissionsLoadPromise;
return;
}
this.loadingItems = true;
this._submissionsLoadFormIdentifier = formIdentifier;
const loadPromise = this.fetchSubmissions(formIdentifier).finally(() => {
if (this._submissionsLoadPromise === loadPromise) {
this._submissionsLoadPromise = null;
this._submissionsLoadFormIdentifier = '';
this.loadingItems = false;
}
});
this._submissionsLoadPromise = loadPromise;
await this._submissionsLoadPromise;
}
async fetchSubmissions(formIdentifier) {
const response = await fetch(
this.entryPointUrl +
'/formalize/submissions?formIdentifier=' +
formIdentifier +
'&perPage=9999',
{
headers: {
'Content-Type': 'application/ld+json',
Authorization: 'Bearer ' + this.getToken(),
},
},
);
if (!response.ok) {
this.handleErrorResponse(response);
this.items = [];
return;
}
const data = await response.json();
if (this.activeForm?.identifier === formIdentifier) {
this.items = data['hydra:member'] || [];
}
}
getNormalizedRouteSegments() {
const segments = [...this.getRoutingData().pathSegments];
const basePathSegments = new URL(this.basePath || '/', window.location.origin).pathname
.split('/')
.filter((segment) => segment !== '');
while (basePathSegments.length > 0 && segments[0] === basePathSegments[0]) {
segments.shift();
basePathSegments.shift();
}
if (this._i18n.languages.includes(segments[0])) {
segments.shift();
}
const formSegmentIndex = segments.findIndex((segment) => {
return this.itemFormEntries.some((entry) => entry.form.identifier === segment);
});
if (formSegmentIndex > 0) {
return segments.slice(formSegmentIndex);
}
return segments;
}
async applyRoute() {
if (!this._hasLoadedForms) {
return;
}
const pathSegments = this.getNormalizedRouteSegments();
const formIdentifier = pathSegments[0] || '';
const actionOrItemIdentifier = pathSegments[1] || '';
const editMarker = pathSegments[2] || '';
if (!formIdentifier) {
this.activeForm = null;
this.activeModule = null;
this.selectedItem = null;
this.items = [];
this.selectedItemCount = 0;
this.mode = 'forms';
return;
}
const entry = this.itemFormEntries.find((itemFormEntry) => {
return itemFormEntry.form.identifier === formIdentifier;
});
if (!entry) {
this.activeForm = null;
this.activeModule = null;
this.selectedItem = null;
this.items = [];
this.selectedItemCount = 0;
this.mode = 'unknown-form';
return;
}
const activeFormChanged = this.activeForm?.identifier !== entry.form.identifier;
this.activeForm = entry.form;
this.activeModule = entry.module;
if (activeFormChanged || this.items.length === 0) {
this.selectedItemCount = 0;
await this.loadSubmissions();
}
if (actionOrItemIdentifier === 'create') {
this.selectedItem = null;
this.mode = 'edit';
return;
}
if (actionOrItemIdentifier && editMarker === 'edit') {
this.selectedItem =
this.items.find((item) => item.identifier === actionOrItemIdentifier) || null;
this.mode = this.selectedItem ? 'edit' : 'unknown-item';
return;
}
this.selectedItem = null;
this.mode = 'list';
}
setRoute(path) {
this.sendSetPropertyEvent('routing-url', path, true);
}
openForm(entry) {
this.setRoute(`/${entry.form.identifier}`);
}
/**
* @param {{form: {identifier: string}} | null} [entry]
*/
createItem(entry = null) {
const form = entry?.form || this.activeForm;
if (!form) {
return;
}
this.setRoute(`/${form.identifier}/create`);
}
editItem(item) {
if (!this.activeForm) {
return;
}
this.setRoute(`/${this.activeForm.identifier}/${item.identifier}/edit`);
}
cancelEdit() {
if (!this.activeForm) {
this.setRoute('/');
return;
}
this.setRoute(`/${this.activeForm.identifier}`);
}
async saveItem() {
const formElement = this.formRef.value?.shadowRoot?.querySelector('form');
if (!formElement) {
return;
}
const isValid = await validateRequiredFields(formElement);
if (!isValid) {
const form = /** @type {import('./form/base-object.js').BaseFormElement | null} */ (
this.formRef.value
);
form?.scrollToFirstInvalidField(formElement, true);
sendNotification({
summary: this._i18n.t('errors.warning-title'),
body: this._i18n.t('errors.form-validation-warning-notification-body'),
type: 'warning',
timeout: 5,
targetNotificationId: editModalNotificationId,
});
return;
}
this.saving = true;
const formData = new FormData();
formData.append('dataFeedElement', JSON.stringify(gatherFormDataFromElement(formElement)));
if (!this.selectedItem) {
formData.append('form', '/formalize/forms/' + this.activeForm.identifier);
formData.append('submissionState', String(SUBMISSION_STATES_BINARY.SUBMITTED));
}
try {
const response = await fetch(
this.selectedItem
? this.entryPointUrl + '/formalize/submissions/' + this.selectedItem.identifier
: this.entryPointUrl + '/formalize/submissions',
{
method: this.selectedItem ? 'PATCH' : 'POST',
headers: {
Authorization: 'Bearer ' + this.getToken(),
},
body: formData,
},
);
if (!response.ok) {
this.handleErrorResponse(response, editModalNotificationId);
return;
}
try {
await response.json();
} catch {
// The API may return an empty body for PATCH responses.
}
sendNotification({
summary: this._i18n.t('success.success-title'),
body: this._i18n.t(
this.selectedItem ? 'manage-fields.item-updated' : 'manage-fields.item-created',
),
type: 'success',
timeout: 5,
});
await this.loadSubmissions();
this.setRoute(`/${this.activeForm.identifier}`);
} finally {
this.saving = false;
}
}
async deleteItem(item) {
await this.deleteItems([item]);
}
async deleteItems(items) {
if (!items || items.length === 0) {
sendNotification({
summary: this._i18n.t('errors.warning-title'),
body: this._i18n.t('errors.no-submission-selected'),
type: 'warning',
timeout: 10,
});
return;
}
/** @type {DeletionConfirmationModal | null} */
const confirmationModal = this.renderRoot.querySelector(
'dbp-formalize-deletion-confirmation-modal',
);
const confirmed = await confirmationModal?.confirm();
if (!confirmed) {
return;
}
this.deletingItems = true;
let deletedCount = 0;
try {
for (const item of items) {
const response = await fetch(
this.entryPointUrl + '/formalize/submissions/' + item.identifier,
{
method: 'DELETE',
headers: {
Authorization: 'Bearer ' + this.getToken(),
},
},
);
if (!response.ok) {
this.handleErrorResponse(response);
continue;
}
deletedCount++;
}
} finally {
this.deletingItems = false;
}
if (deletedCount === 0) {
return;
}
sendNotification({
summary: this._i18n.t('success.success-title'),
body: this._i18n.t('manage-fields.items-deleted', {count: deletedCount}),
type: 'success',
timeout: 5,
});
await this.loadSubmissions();
this.selectedItemCount = 0;
this.setRoute(`/${this.activeForm.identifier}`);
}
async deleteSelectedItems() {
/** @type {CustomTabulatorTable | null} */
const table = this.renderRoot?.querySelector('#manage-fields-item-table');
const selectedData = table?.tabulatorTable?.getSelectedData() || [];
await this.deleteItems(selectedData.map((row) => row.item).filter(Boolean));
}
async deleteAllItems() {
await this.deleteItems([...this.items]);
}
getItemTitle(item) {
const data = parseSubmissionData(item);
return (
this.activeModule?.getItemText?.(data, this.lang) ||
this._i18n.t('manage-fields.unnamed-item')
);
}
getLastModifiedByIdentifier(item) {
const lastModifiedBy = item?.lastModifiedBy || item?.lastModifiedByPerson || null;
const identifier =
item?.lastModifiedById || lastModifiedBy?.identifier || lastModifiedBy?.['@id'] || '';
return `${identifier}`.trim();
}
async loadLastModifiedByName(item = this.selectedItem) {
const identifier = this.getLastModifiedByIdentifier(item);
this.lastModifiedByName = '';
if (!identifier || !this.auth?.token || this._failedUserNameLookups.has(identifier)) {
return;
}
if (this._userNameCache.has(identifier)) {
this.lastModifiedByName = this._userNameCache.get(identifier);
return;
}
try {
const response = await this.apiGetUserDetails(identifier);
if (!response.ok) {
this._failedUserNameLookups.add(identifier);
return;
}
const user = await response.json();
const fullName =
[user?.givenName, user?.familyName].filter(Boolean).join(' ') || user?.name || '';
this._userNameCache.set(identifier, fullName);
if (this.getLastModifiedByIdentifier(this.selectedItem) === identifier) {
this.lastModifiedByName = fullName;
}
} catch (error) {
console.error('Failed to load last modifier details:', error);
}
}
getLastModifiedByName(item) {
const lastModifiedBy = item?.lastModifiedBy || item?.lastModifiedByPerson || null;
return (
item?.lastModifiedByName ||
lastModifiedBy?.name ||
[lastModifiedBy?.givenName, lastModifiedBy?.familyName].filter(Boolean).join(' ') ||
this.lastModifiedByName
);
}
renderEditMetadata() {
const i18n = this._i18n;
const item = this.selectedItem || {};
const emptyValue = html`
—
`;
const metadata = [
{
label: i18n.t('render-form.forms.base-object.submission-creation-date-label'),
value: formatDate(item.dateCreated),
},
{
label: i18n.t('render-form.forms.base-object.last-modified-date-label'),
value: formatDate(item.dateLastModified),
},
{
label: i18n.t('render-form.forms.base-object.last-modified-by-name-label'),
value: this.getLastModifiedByName(item),
},
];
return html`
<div class="edit-metadata">
${metadata.map(
(entry) => html`
<div>
<span class="label metadata-label">${entry.label}:</span>
<span class="value">${entry.value || emptyValue}</span>
</div>
`,
)}
</div>
`;
}
getFormTableData() {
return this.itemFormEntries.map((entry) => ({
name: getLocalizedFormName(entry.form, this.lang),
entry,
}));
}
getItemTableData() {
return this.items.map((item) => ({
title: this.getItemTitle(item),
dateCreated: item.dateCreated || '',
item,
}));
}
createTableActionButton(iconName, title, onClick, ariaLabel = title) {
const button = this.createScopedElement('dbp-icon-button');
button.setAttribute('subscribe', 'lang');
button.setAttribute('icon-name', iconName);
// The title stays short as a tooltip, while the aria-label additionally names the row
// so screen reader users can tell the repeated action buttons apart.
button.title = title;
button.setAttribute('aria-label', ariaLabel);
button.addEventListener('click', (event) => {
event.stopPropagation();
onClick();
});
return button;
}
createTableActions(buttons) {
const actions = document.createElement('div');
actions.style.display = 'flex';
actions.style.flexWrap = 'wrap';
actions.style.gap = '0.5rem';
actions.style.alignItems = 'center';
actions.style.justifyContent = 'flex-end';
buttons.forEach((button) => actions.append(button));
return actions;
}
createFormActions(entry) {
const i18n = this._i18n;
const formName = getLocalizedFormName(entry.form, this.lang);
const openButton = this.createScopedElement('dbp-formalize-get-details-button');
openButton.setAttribute('subscribe', 'lang');
openButton.title = i18n.t('manage-fields.open-form');
openButton.ariaLabel = i18n.t('manage-fields.open-form-aria', {formName});
openButton.addEventListener('click', (event) => {
event.stopPropagation();
this.openForm(entry);
});
return this.createTableActions([openButton]);
}
createItemActions(item) {
const i18n = this._i18n;
const itemName = this.getItemTitle(item);
return this.createTableActions([
this.createTableActionButton(
'pencil',
i18n.t('manage-fields.edit-item'),
() => this.editItem(item),
i18n.t('manage-fields.edit-item-aria', {itemName}),
),
]);
}
getFormTableOptions() {
const i18n = this._i18n;
const langsForms = {
en: {
columns: {
name: i18n.t('manage-fields.form', {lng: 'en'}),
},
},
de: {
columns: {
name: i18n.t('manage-fields.form', {lng: 'de'}),
},
},
};
return {
langs: langsForms,
data: this.getFormTableData(),
layout: 'fitColumns',
rowHeight: 64,
columnDefaults: {
vertAlign: 'middle',
hozAlign: 'left',
resizable: false,
},
placeholder: i18n.t('manage-fields.no-form'),
columns: [
{field: 'name', sorter: 'string', minWidth: 150, widthGrow: 1},
{
field: 'actions',
headerSort: false,
hozAlign: 'right',
formatter: (cell) => this.createFormActions(cell.getRow().getData().entry),
width: 100,
widthGrow: 0,
widthShrink: 0,
},
],
};
}
getInitialItemColumns() {
const i18n = this._i18n;
return [
{
field: 'title',
title: i18n.t('manage-fields.item'),
sorter: 'string',
minWidth: 150,
widthGrow: 1,
visible: true,
frozen: false,
},
{
field: 'dateCreated',
title: i18n.t('manage-fields.date-created'),
sorter: 'string',
width: 180,
widthGrow: 0,
widthShrink: 0,
visible: true,
frozen: false,
},
];
}
getActionsColumnDefinition() {
return {
field: 'actions',
headerSort: false,
hozAlign: 'right',
headerHozAlign: 'right',
width: 100,
widthGrow: 0,
widthShrink: 0,
formatter: (cell) => this.createItemActions(cell.getRow().getData().item),
};
}
getItemTableOptions() {
const i18n = this._i18n;
const langsItems = {
en: {
columns: {
title: i18n.t('manage-fields.item', {lng: 'en'}),
dateCreated: i18n.t('manage-fields.date-created', {lng: 'en'}),
},
},
de: {
columns: {
title: i18n.t('manage-fields.item', {lng: 'de'}),
dateCreated: i18n.t('manage-fields.date-created', {lng: 'de'}),
},
},
};
return {
langs: langsItems,
data: this.getItemTableData(),
layout: 'fitColumns',
rowHeight: 64,
columnDefaults: {
vertAlign: 'middle',
hozAlign: 'left',
resizable: false,
},
placeholder: i18n.t('manage-fields.no-items'),
selectableRows: true,
rowHeader: {
formatter: 'rowSelection',
titleFormatter: 'rowSelection',
titleFormatterParams: {rowRange: 'visible'},
headerSort: false,
resizable: false,
frozen: true,
width: 40,
minWidth: 40,
widthGrow: 0,
widthShrink: 0,
headerHozAlign: 'center',
hozAlign: 'center',
},
columns: [
...this.getInitialItemColumns().map(({title, ...column}) => column),
this.getActionsColumnDefinition(),
],
};
}
getItemActionOptions() {
const i18n = this._i18n;
if (this.selectedItemCount > 0) {
return [
{
value: 'delete-selected',
label: i18n.t('manage-fields.delete-selected-items', {
n: this.selectedItemCount,
}),
iconName: 'delete-selection',
},
];
}
return [
{
value: 'delete-all',
label: i18n.t('manage-fields.delete-all-items', {n: this.items.length}),
iconName: 'trash',
},
];
}
handleItemAction(event) {
const option = event.detail?.option;
if (!option) {
return;
}
if (option.value === 'delete-selected') {
void this.deleteSelectedItems();
return;
}
if (option.value === 'delete-all') {
void this.deleteAllItems();
}
}
handleItemSelectionCountChanged(event) {
this.selectedItemCount = event.detail?.count ?? 0;
}
/** @returns {CustomTabulatorTable | null} */
getItemTable() {
return /** @type {CustomTabulatorTable | null} */ (
this.renderRoot?.querySelector('#manage-fields-item-table') ?? null
);
}
/** @returns {HTMLInputElement | null} */
getItemSearchbar() {
return /** @type {HTMLInputElement | null} */ (
this.renderRoot?.querySelector('#manage-fields-item-searchbar') ?? null
);
}
handleItemSearch(event) {
event?.preventDefault();
const searchInput = this.getItemSearchbar();
if (!searchInput) return;
this.applyItemSearch(searchInput.value.trim());
}
handleItemSearchInput(event) {
const target = /** @type {HTMLInputElement | null} */ (event.target);
if (!target) return;
this.applyItemSearch(target.value.trim());
}
applyItemSearch(filterValue) {
const table = this.getItemTable();
if (!table) return;
if (filterValue === '') {
table.clearFilter();
return;
}
const filters = this.getInitialItemColumns()
.filter((column) => column.field && column.visible !== false)
.map((column) => ({field: column.field, type: 'like', value: filterValue}));
table.setFilter([filters]);
}
handleResetItemSearch() {
const searchInput = this.getItemSearchbar();
const table = this.getItemTable();
if (!searchInput || !table) return;
searchInput.value = '';
table.clearFilter();
searchInput.focus();
}
async syncTabulatorTable(selector, options) {
const table = this.renderRoot?.querySelector(selector);
if (!table) {
return;
}
table.options = options;
table.data = options.data;
if (!table.tabulatorTable) {
await table.updateComplete;
if (!table.tabulatorTable && !table.tableBuilding) {
table.buildTable();
}
return;
}
table.tabulatorTable.setLocale(this.lang);
table.tabulatorTable.replaceData(options.data);
}
getFormHtml() {
if (!this.activeModule || !this.activeForm) {
return html``;
}
const tagName = 'dbp-formalize-item-' + this.activeModule.getUrlSlug();
this.defineScopedElement(tagName, this.activeModule.getFormComponent());
const formData = this.selectedItem ? parseSubmissionData(this.selectedItem) : {};
return staticHtml`
<${unsafeStatic(tagName)}
${ref(this.formRef)}
subscribe="auth,lang,entry-point-url"
form-identifier=${this.activeForm.identifier}
form-url-slug=${this.activeModule.getUrlSlug()}
.formProperties=${this.activeForm}
.formData=${formData}></${unsafeStatic(tagName)}>
`;
}
getColumnConfigurationStorageKey(scope) {
const userId = this.auth?.['user-id'];
return this.isLoggedIn() && userId ? `formalize-manage-fields-${scope}-${userId}` : '';
}
getPaginationSizeStorageKey() {
const userId = this.auth?.['user-id'];
return this.isLoggedIn() && userId ? `formalize-manage-fields-${userId}` : '';
}
renderFormOverview() {
const i18n = this._i18n;
if (this.itemFormEntries.length === 0) {
return html`
<div class="notification is-warning">${i18n.t('manage-fields.no-form')}</div>
`;
}
return html`
<dbp-tabulator-table
lang="${this.lang}"
class="tabulator-table"
id="manage-fields-form-table"
identifier="manage-fields-form-table"
pagination-enabled
pagination-size="5"
.paginationSizeStorageKey=${this.getPaginationSizeStorageKey()}
.options=${this.getFormTableOptions()}></dbp-tabulator-table>
`;
}
renderList() {
const i18n = this._i18n;
return html`
<div class="active-form-wrapper">
<div class="active-form-top-bar">
<span class="back-navigation">
<button
type="button"
@click=${() => this.setRoute('/')}
title="${i18n.t('manage-fields.back-to-forms')}">
<dbp-icon name="chevron-left"></dbp-icon>
${i18n.t('manage-fields.back-to-forms')}
</button>
</span>
</div>
<div class="active-form-header">
<h2 class="form-name">${getLocalizedFormName(this.activeForm, this.lang)}</h2>
<button class="button is-primary" @click=${() => this.createItem()}>
<dbp-icon name="plus" aria-hidden="true"></dbp-icon>
${i18n.t('manage-fields.create-item')}
</button>
</div>
</div>
${
this.loadingItems
? html`
<dbp-mini-spinner text="${i18n.t('loading-message')}"></dbp-mini-spinner>
`
: this.items.length === 0
? html`
<p class="empty-state">${i18n.t('manage-fields.no-items')}</p>
`
: html`
<div class="items-action-header">
<dbp-select
?disabled=${this.deletingItems}
@change=${(event) => this.handleItemAction(event)}
label="${i18n.t('manage-fields.actions-button-text')}"
align="left"
allow-expand
.options=${this.getItemActionOptions()}></dbp-select>
<form
class="search-input items-search search-field"
@submit=${this.handleItemSearch}>
<label
for="manage-fields-item-searchbar"
class="label visually-hidden">
${i18n.t('manage-fields.search-input-label')}:
</label>
<div class="control search-control">
<input
type="text"
id="manage-fields-item-searchbar"
class="input searchbar"
placeholder="${i18n.t(
'manage-fields.searchbar-placeholder',
)}"
aria-label="${i18n.t(
'manage-fields.search-input-label',
)}"
@input=${this.handleItemSearchInput} />
<span class="search-icon" aria-hidden="true">
<dbp-icon name="search"></dbp-icon>
</span>
</div>
<button
type="submit"
class="button search-button"
title="${i18n.t('manage-fields.search-button')}"
aria-label="${i18n.t('manage-fields.search-button')}"
style="position:absolute;left:-9999px;width:1px;height:1px;overflow:hidden;">
<dbp-icon name="search" aria-hidden="true"></dbp-icon>
</button>
</form>
<button
type="button"
class="reset-search"
@click=${this.handleResetItemSearch}>
<dbp-icon name="spinner-arrow" aria-hidden="true"></dbp-icon>
${i18n.t('manage-fields.reset-search-label')}
</button>
</div>
<dbp-tabulator-table
lang="${this.lang}"
class="tabulator-table"
id="manage-fields-item-table"
identifier="manage-fields-item-table"
pagination-enabled
pagination-size="5"
.paginationSizeStorageKey=${this.getPaginationSizeStorageKey()}
select-rows-enabled
column-configuration-enabled
column-configuration-in-header
.columnConfigurationExcludedFields=${['actions']}
.columnConfigurationStorageKey=${this.getColumnConfigurationStorageKey(
`items-${this.activeForm?.identifier || 'form'}`,
)}
@dbp-tabulator-table-selection-count-changed=${(event) =>
this.handleItemSelectionCountChanged(event)}
.options=${this.getItemTableOptions()}></dbp-tabulator-table>
`
}
`;
}
renderEdit() {
const i18n = this._i18n;
const formName =
this.activeModule?.getFormName?.(this.lang) ||
getLocalizedFormName(this.activeForm, this.lang) ||
i18n.t('manage-fields.item');
const titleName = getSentenceCaseItemName(formName, this.lang);
const itemName = this.selectedItem ? this.getItemTitle(this.selectedItem) : '';
const titleKey = this.selectedItem
? 'manage-fields.edit-item-title-with-name'
: 'manage-fields.create-item-title';
return html`
<dbp-modal
id="manage-fields-edit-modal"
modal-id="manage-fields-edit-modal"
subscribe="lang"
class="modal-width manage-fields-edit-modal"
sticky-footer
@dbp-modal-closed=${() => {
if (this.mode === 'edit') this.cancelEdit();
}}>
<div slot="title">
<h2 class="edit-title">${i18n.t(titleKey, {name: titleName, itemName})}</h2>
</div>
<div slot="header">
<dbp-notification
id=${editModalNotificationId}
lang=${this.lang}></dbp-notification>
${this.renderEditMetadata()}
</div>
<div slot="content">${this.mode === 'edit' ? this.getFormHtml() : ''}</div>
<div slot="footer" class="edit-actions">
<dbp-button
type="is-secondary"
no-spinner-on-click
@click=${() =>
/** @type {Modal | null} */ (
this.renderRoot?.querySelector('#manage-fields-edit-modal')
)?.close()}>
<dbp-icon name="close" aria-hidden="true"></dbp-icon>
<span class="button-label">${i18n.t('manage-fields.cancel')}</span>
</dbp-button>
<dbp-button
type="is-primary"
?disabled=${this.saving}
no-spinner-on-click
@click=${() => this.saveItem()}>
<dbp-icon name="save" aria-hidden="true"></dbp-icon>
<span class="button-label">${i18n.t('manage-fields.save-item')}</span>
</dbp-button>
</div>
</dbp-modal>
`;
}
renderContent() {
if (this.mode === 'forms') {
return this.renderFormOverview();
}
if (this.mode === 'unknown-form') {
return html`
<div class="notification is-warning">
${this._i18n.t('manage-fields.unknown-form')}
</div>
${this.renderFormOverview()}
`;
}
if (this.mode === 'unknown-item') {
return html`
<div class="notification is-warning">
${this._i18n.t('manage-fields.unknown-item')}
</div>
${this.renderList()}
`;
}
return this.renderList();
}
render() {
const i18n = this._i18n;
if (!this.isLoggedIn() && !this.isAuthPending()) {
return html`
<div class="notification is-warning">
${i18n.t('error-login-message')}
<a
href="#"
@click=${(event) => {
event.preventDefault();
this.sendSetPropertyEvent('requested-login-status', 'logged-in');
}}>
${i18n.t('error-login-link')}
</a>
</div>
`;
}
if ((this.loading && !this._hasLoadedForms) || this.isAuthPending()) {
return html`
<dbp-mini-spinner text="${i18n.t('loading-message')}"></dbp-mini-spinner>
`;
}
if (this.errorMessage) {
return html`
<div class="notification is-danger">${this.errorMessage}</div>
`;
}
if (this.itemModules.length === 0) {
return html`
<div class="notification is-warning">${i18n.t('manage-fields.no-module')}</div>
`;
}
return html`
<dbp-formalize-deletion-confirmation-modal
subscribe="lang"
message-key="manage-fields.delete-confirmation-message"
message-li2-key="manage-fields.delete-confirmation-message-li2"></dbp-formalize-deletion-confirmation-modal>
<section class="manage-fields">
${
this.mode === 'list' || this.mode === 'unknown-item'
? ''
: html`
<h2>${i18n.t('manage-fields.title')}</h2>
`
}
${this.renderContent()} ${this.activeForm ? this.renderEdit() : ''}
</section>
`;
}
update(changedProperties) {
super.update(changedProperties);
const oldAuth = changedProperties.get('auth');
const oldToken = oldAuth?.token || '';
const newToken = this.auth?.token || '';
const gainedToken = !oldToken && newToken;
const itemFrontendKeysChanged =
changedProperties.has('itemFrontendKeys') &&
this._lastFrontendKeys !== this.itemFrontendKeys;
if (((gainedToken && !this._hasLoadedForms) || itemFrontendKeysChanged) && newToken) {
void this.loadItemForms();
return;
}
if (changedProperties.has('routingUrl') && this._hasLoadedForms) {
this._routeApplyPromise = this._routeApplyPromise.then(() => this.applyRoute());
}
}
updated(changedProperties) {
super.updated?.(changedProperties);
const needFormTableSync =
changedProperties.has('itemFormEntries') ||
changedProperties.has('lang') ||
changedProperties.has('mode');
const needItemTableSync =
changedProperties.has('items') ||
changedProperties.has('lang') ||
changedProperties.has('mode');
if (needFormTableSync) {
void this.syncTabulatorTable('#manage-fields-form-table', this.getFormTableOptions());
}
if (needItemTableSync) {
void this.syncTabulatorTable('#manage-fields-item-table', this.getItemTableOptions());
}
if (changedProperties.has('selectedItem') || changedProperties.has('auth')) {
void this.loadLastModifiedByName();
}
if (changedProperties.has('mode') && this.activeForm) {
/** @type {Modal | null} */
const modal = this.renderRoot?.querySelector('#manage-fields-edit-modal');
if (this.mode === 'edit') {
void modal?.updateComplete.then(() => {
if (this.mode === 'edit' && modal.isConnected) modal.open();
});
} else if (modal?.isOpen()) {
modal.close();
}
}
}
static get styles() {
return css`
${commonStyles.getThemeCSS()}
${commonStyles.getGeneralCSS(false)}
${commonStyles.getButtonCSS()}
${commonStyles.getNotificationCSS()}
:host {
display: block;
}
.manage-fields {
display: grid;
gap: 1rem;
}
.manage-fields h2 {
font-weight: 300;
margin: 0;
}
.active-form-header {
display: block;
}
.active-form-header .button {
display: inline-flex;
width: auto;
gap: 0.25rem;
}
.active-form-top-bar {
align-items: center;
display: flex;
flex-wrap: wrap;
gap: 1rem;
justify-content: space-between;
padding-top: 1rem;
}
.back-navigation dbp-icon {
font-size: 0.8em;
padding-bottom: 2px;
padding-right: 7px;
}
.back-navigation:hover {
background-color: var(--dbp-hover-background-color);
color: var(--dbp-hover-color, var(--dbp-content));
}
.back-navigation:hover::before {
background-color: var(--dbp-hover-color, var(--dbp-content));
}
.back-navigation button {
border: 0;
padding: 0;
background: none;
color: inherit;
font: inherit;
}
.active-form-header h2,
.edit-title {
margin: 0;
}
.active-form-header .form-name {
margin-top: 0.5em;
margin-bottom: 1em;
}
.edit-metadata {
display: grid;
gap: 0.25rem;
margin: 0.5rem 0;
}
.edit-metadata > div {
align-items: baseline;
display: flex;
gap: 0.25rem;
}
.edit-metadata .label {
font-weight: bold;
}
.metadata-label {
margin: 0;
}
.edit-actions {
display: flex;
flex-wrap: wrap;
gap: 0.75rem;
justify-content: flex-end;
padding: 1em 0;
}
.tabulator-table {
width: 100%;
}
#manage-fields-form-table,
#manage-fields-item-table {
display: block;
max-width: 100%;
min-width: 0;
}
.forms-table-actions,
.items-action-header {
display: flex;
align-items: center;
gap: 1rem;
margin-bottom: 0.5rem;
}
.forms-search,
.items-search {
flex: 1;
min-width: 12rem;
}
.forms-search label,
.items-search label {
clip: rect(0 0 0 0);
clip-path: inset(50%);
height: 1px;
overflow: hidden;
position: absolute;
white-space: nowrap;
width: 1px;
}
.reset-search {
cursor: pointer;
background: none;
border: 0 none;
padding: 5px;
transform: translateX(5px);
}
.reset-search:disabled {
cursor: not-allowed;
}
.reset-search dbp-icon {
transition: transform 250ms ease-in;
}
.reset-search:hover:not(:disabled) dbp-icon {
transform: rotate(360deg);
}
@media (max-width: 530px) {
.forms-table-actions,
.items-action-header {
align-items: stretch;
flex-wrap: wrap;
justify-content: space-between;
}
.items-action-header dbp-select {
order: 1;
}
.items-action-header .reset-search {
order: 2;
}
.forms-search,
.items-search {
flex-basis: 100%;
}
}
.create-form-btn {
display: inline-flex;
align-items: center;
gap: 0.4rem;
}
.create-form-btn-icon {
flex-shrink: 0;
top: 0;
}
.hidden {
display: none;
}
/* 1:1 aus dbp-bulletin-view-job-offers.js:1522-1552 – overlay icon statt separatem button */
.search-field {
margin-bottom: 0;
}
.search-control {
position: relative;
flex: 1;
}
.search-control .input {
padding-right: 2.5rem;
width: 100%;
}
.search-control .input.searchbar {
/* überschreibt .searchbar aus styles.js (border-right:0) für job-offers look */
border: 1px solid var(--dbp-content);
border-right: 1px solid var(--dbp-content);
padding: 0 2.5rem 0 0.5em;
height: 32px;
box-sizing: border-box;
}
.search-icon {
position: absolute;
right: 0.75rem;
top: 50%;
transform: translateY(-50%);
color: var(--dbp-muted);
pointer-events: none;
}
.visually-hidden {
clip: rect(0 0 0 0);
clip-path: inset(50%);
height: 1px;
overflow: hidden;
position: absolute;
white-space: nowrap;
width: 1px;
}
.modal-width {
--dbp-modal-min-width: min(95vw, 900px);
--dbp-modal-max-width: min(95vw, 900px);
--dbp-modal-max-height: 90vh;
--dbp-modal-content-overflow-y: auto;
}
.manage-fields-edit-modal {
--dbp-label-margin-bottom: 3px;
}
}
.empty-state {
color: var(--dbp-muted);
}
@media only screen and (orientation: portrait) and (max-width: 768px) {
.back-navigation {
padding-top: 0;
}
.active-form-top-bar {
padding-top: 0;
}
.active-form-header .form-name {
margin-bottom: 0.5em;
}
}
@media (max-width: 560px) {
.modal-width {
--dbp-modal-min-width: unset;
--dbp-modal-max-width: unset;
--dbp-modal-max-height: 90vh;
--dbp-modal-content-overflow-y: auto;
}
}
`;
}
}
commonUtils.defineCustomElement('dbp-formalize-manage-fields', ManageFields);