diff --git a/src/bundle/Resources/encore/ibexa.js.config.js b/src/bundle/Resources/encore/ibexa.js.config.js index 3bb90be8d3..c04c51ec8c 100644 --- a/src/bundle/Resources/encore/ibexa.js.config.js +++ b/src/bundle/Resources/encore/ibexa.js.config.js @@ -63,6 +63,7 @@ const layout = [ path.resolve(__dirname, '../public/js/scripts/admin.back.to.top.js'), path.resolve(__dirname, '../public/js/scripts/admin.middle.ellipsis.js'), path.resolve(__dirname, '../public/js/scripts/admin.form.error.js'), + path.resolve(__dirname, '../public/js/scripts/embedded.item.actions'), path.resolve(__dirname, '../public/js/scripts/widgets/flatpickr.js'), ]; const fieldTypes = []; diff --git a/src/bundle/Resources/public/js/scripts/admin.location.view.js b/src/bundle/Resources/public/js/scripts/admin.location.view.js index bfa500217e..7a7835dff8 100644 --- a/src/bundle/Resources/public/js/scripts/admin.location.view.js +++ b/src/bundle/Resources/public/js/scripts/admin.location.view.js @@ -7,6 +7,10 @@ const sortContainer = doc.querySelector('[data-sort-field][data-sort-order]'); const sortField = sortContainer.getAttribute('data-sort-field'); const sortOrder = sortContainer.getAttribute('data-sort-order'); + const emdedItemsUpdateChannel = new BroadcastChannel('ibexa-emded-item-live-update'); + const queryString = window.location.search; + const urlParams = new URLSearchParams(queryString); + const publishedContentId = urlParams.get('publishedContentId'); const handleEditItem = (content, location) => { const contentId = content._id; const locationId = location._id; @@ -190,6 +194,10 @@ }), ); }); + + if (publishedContentId) { + emdedItemsUpdateChannel.postMessage({ contentId: publishedContentId }); + } })( window, window.document, diff --git a/src/bundle/Resources/public/js/scripts/embedded.item.actions.js b/src/bundle/Resources/public/js/scripts/embedded.item.actions.js new file mode 100644 index 0000000000..d71514fc28 --- /dev/null +++ b/src/bundle/Resources/public/js/scripts/embedded.item.actions.js @@ -0,0 +1,313 @@ +(function (global, doc, ibexa, Routing, Translator, Popper) { + const MENU_PROPS = { + placement: 'bottom-end', + fallbackPlacements: ['bottom-start', 'top-end', 'top-start'], + }; + const token = document.querySelector('meta[name="CSRF-Token"]').content; + const siteaccess = document.querySelector('meta[name="SiteAccess"]').content; + const metaLanguageCode = document.querySelector('meta[name="LanguageCode"]')?.content; + const previewLanguageCode = metaLanguageCode ?? ibexa.adminUiConfig.languages.priority[0]; + const adminUiLanguages = ibexa.adminUiConfig.languages.mappings; + const emdedItemsUpdateChannel = new BroadcastChannel('ibexa-emded-item-live-update'); + const editEmbeddedItemForm = doc.querySelector('[name="embedded_item_edit"]'); + const actionsMenuTriggerBtns = doc.querySelectorAll('.ibexa-embedded-item-actions__menu-trigger-btn'); + const updateNode = ({ node, value, isMiddleEllipsis }) => { + if (!isMiddleEllipsis) { + node.innerHTML = value; + + return; + } + + const middleEllipsisNode = node.querySelector('.ibexa-middle-ellipsis'); + const middleEllipsisNameStartNode = node.querySelector( + '.ibexa-middle-ellipsis__name--start .ibexa-middle-ellipsis__name-ellipsized', + ); + const middleEllipsisNameEndNode = node.querySelector('.ibexa-middle-ellipsis__name--end .ibexa-middle-ellipsis__name-ellipsized'); + + middleEllipsisNode.title = value; + middleEllipsisNameStartNode.innerHTML = value; + middleEllipsisNameEndNode.innerHTML = value; + + ibexa.helpers.ellipsis.middle.parse(node); + }; + const updateNodes = async (contentId) => { + const nodesToUpdate = doc.querySelectorAll(`[data-ibexa-update-content-id="${contentId}"]`); + + if (!nodesToUpdate) { + return; + } + + const contentData = await loadContentData(contentId); + + [...nodesToUpdate].forEach((nodeToUpdate) => { + let sourceValue = contentData; + const { ibexaUpdateSourceDataPath } = nodeToUpdate.dataset; + const updateSourceDataPathArray = ibexaUpdateSourceDataPath.split('.'); + + for (const pathLevelIndex in updateSourceDataPathArray) { + const pathLevel = updateSourceDataPathArray[pathLevelIndex]; + + sourceValue = sourceValue[pathLevel]; + } + + if (sourceValue) { + const { ibexaUpdateMiddleEllipsis } = nodeToUpdate.dataset; + + updateNode({ + node: nodeToUpdate, + value: sourceValue, + isMiddleEllipsis: ibexaUpdateMiddleEllipsis, + }); + } + }); + }; + const loadContentData = async (contentId) => { + try { + const loadContentRequest = new Request(`/api/ibexa/v2/content/objects/${contentId}`, { + method: 'GET', + headers: { + Accept: 'application/vnd.ibexa.api.Content+json', + 'X-Siteaccess': siteaccess, + 'X-CSRF-Token': token, + }, + mode: 'same-origin', + credentials: 'same-origin', + }); + const response = await fetch(loadContentRequest); + + return ibexa.helpers.request.getJsonFromResponse(response); + } catch (error) { + ibexa.helpers.notification.showErrorNotification(error); + } + }; + const editContent = ({ contentId, locationId, languageCode }) => { + if (!contentId || !locationId || !languageCode) { + return; + } + + const contentInfoInput = editEmbeddedItemForm.querySelector('[name="embedded_item_edit[content_info]"]'); + const locationInput = editEmbeddedItemForm.querySelector('[name="embedded_item_edit[location]"]'); + const languageInput = editEmbeddedItemForm.querySelector(`[name="embedded_item_edit[language]"][value="${languageCode}"]`); + + contentInfoInput.value = contentId; + locationInput.value = locationId; + languageInput.click(); + + editEmbeddedItemForm.submit(); + }; + const generateGoToActionItem = ({ contentId, locationId, productCode }) => { + const href = productCode + ? Routing.generate('ibexa.product_catalog.product.view', { + productCode, + languageCode: previewLanguageCode, + }) + : Routing.generate('ibexa.content.translation.view', { + contentId, + locationId, + languageCode: previewLanguageCode, + }); + + return { + label: Translator.trans(/*@Desc("Go to content")*/ 'embedded_items.action.go_to_label', {}, 'content'), + href, + }; + }; + const generateEditActionItem = ({ contentId, locationId, productCode, languages }) => { + if (languages.length > 1) { + return { + label: Translator.trans(/*@Desc("Edit")*/ 'embedded_items.action.edit', {}, 'content'), + branch: { + groups: [ + { + id: 'edit-group', + items: languages.map(({ languageCode, name }) => { + const languageEditAction = productCode + ? { + href: Routing.generate('ibexa.product_catalog.product.edit', { + productCode, + languageCode, + }), + } + : { + onClick: () => editContent({ contentId, locationId, languageCode }), + }; + + return { + label: name, + ...languageEditAction, + }; + }), + }, + ], + }, + }; + } + + const editAction = productCode + ? { + href: Routing.generate('ibexa.product_catalog.product.edit', { + productCode, + languageCode: languages[0].languageCode, + }), + } + : { onClick: () => editContent({ contentId, locationId, languageCode: languages[0].languageCode }) }; + + return { + label: Translator.trans(/*@Desc("Edit")*/ 'embedded_items.action.edit', {}, 'content'), + ...editAction, + }; + }; + const generateMenuTreeItems = ({ contentId, locationId, productCode, languages }) => { + const goToItem = generateGoToActionItem({ contentId, locationId, productCode }); + const editItem = generateEditActionItem({ contentId, locationId, productCode, languages }); + + return { + groups: [ + { + id: 'default', + items: [goToItem, editItem], + }, + ], + }; + }; + const getLanguagesData = async ({ contentId, locationId, initialFunc = () => {}, callbackFunc = () => {} }) => { + try { + initialFunc(); + + const url = window.Routing.generate('ibexa.permission.limitation.language', { + contentId, + locationId, + versionNo: 0, + }); + const request = new Request(url, { + method: 'GET', + headers: { 'X-CSRF-Token': token }, + mode: 'same-origin', + credentials: 'same-origin', + }); + const response = await fetch(request); + const data = await ibexa.helpers.request.getJsonFromResponse(response); + + callbackFunc(); + + return data.filter((language) => language.hasAccess); + } catch (error) { + ibexa.helpers.notification.showErrorNotification(error); + } + }; + const getMenuData = ({ container, event }) => { + const { contentId, locationId, productCode, languageCodes } = container ? container.dataset : event.detail; + const parsedLanguageCodes = typeof languageCodes === 'string' ? JSON.parse(languageCodes) : languageCodes; + const languages = parsedLanguageCodes + ? parsedLanguageCodes.map((languageCode) => ({ + languageCode, + name: adminUiLanguages[languageCode].name, + })) + : []; + + return { + contentId: parseInt(contentId, 10), + locationId: parseInt(locationId, 10), + productCode, + languages, + }; + }; + const createMenu = async ({ triggerElement, container, contentId, locationId, productCode, languages }) => { + triggerElement.dataset.isMenuAttached = 1; + + const mainContainer = container.closest('.ibexa-embedded-item-actions'); + const menuLoader = mainContainer.querySelector('.ibexa-embedded-item-actions__loader'); + const askForLanguagesData = Object.keys(languages).length !== 1; + const languagesData = askForLanguagesData + ? await getLanguagesData({ + contentId, + locationId, + initialFunc: showLoader.bind(null, { triggerElement, menuLoader }), + callbackFunc: hideLoader.bind(null, { menuLoader }), + }) + : languages; + const menuItems = generateMenuTreeItems({ contentId, locationId, productCode, languages: languagesData }); + const menuInstance = new ibexa.core.MultilevelPopupMenu({ + container, + triggerElement, + }); + menuInstance.init(); + menuInstance.generateMenu({ + triggerElement, + ...MENU_PROPS, + ...menuItems, + }); + + triggerElement.click(); + }; + const showLoader = ({ triggerElement, menuLoader }) => { + Popper.createPopper(triggerElement, menuLoader, { + placement: MENU_PROPS.placement, + modifiers: [ + { + name: 'flip', + enabled: true, + options: { + fallbackPlacements: MENU_PROPS.fallbackPlacements, + }, + }, + ], + }); + + menuLoader.classList.remove('ibexa-popup-menu--hidden'); + }; + const hideLoader = ({ menuLoader }) => { + menuLoader.classList.add('ibexa-popup-menu--hidden'); + }; + + actionsMenuTriggerBtns.forEach((actionsMenuTriggerBtn) => { + actionsMenuTriggerBtn.addEventListener( + 'click', + (event) => { + const isMenuAttached = !!parseInt(actionsMenuTriggerBtn.dataset.isMenuAttached, 10); + + if (!isMenuAttached) { + event.preventDefault(); + + const menuMainContainer = actionsMenuTriggerBtn.closest('.ibexa-embedded-item-actions'); + const menuContainer = menuMainContainer.querySelector('.ibexa-embedded-item-actions__menu'); + const menuData = getMenuData({ container: menuContainer }); + + createMenu({ + triggerElement: actionsMenuTriggerBtn, + container: menuContainer, + ...menuData, + }); + } + }, + false, + ); + }); + + doc.body.addEventListener('ibexa-embedded-item:create-dynamic-menu', (event) => { + const menuData = getMenuData({ event }); + const { menuTriggerElement, menuContainer } = event.detail; + + menuTriggerElement.addEventListener( + 'click', + () => { + const isMenuAttached = !!parseInt(menuTriggerElement.dataset.isMenuAttached, 10); + + if (!isMenuAttached) { + event.preventDefault(); + + createMenu({ + triggerElement: menuTriggerElement, + container: menuContainer, + ...menuData, + }); + } + }, + false, + ); + }); + + emdedItemsUpdateChannel.addEventListener('message', (event) => { + updateNodes(event.data.contentId); + }); +})(window, document, window.ibexa, window.Routing, window.Translator, window.Popper); diff --git a/src/bundle/Resources/public/js/scripts/fieldType/ezobjectrelationlist.js b/src/bundle/Resources/public/js/scripts/fieldType/ezobjectrelationlist.js index 3b51b6ce15..b69ce7c180 100644 --- a/src/bundle/Resources/public/js/scripts/fieldType/ezobjectrelationlist.js +++ b/src/bundle/Resources/public/js/scripts/fieldType/ezobjectrelationlist.js @@ -77,9 +77,34 @@ const { escapeHTML } = ibexa.helpers.text; const itemNodes = relationsContainer.querySelectorAll('.ibexa-relations__item'); const itemNode = itemNodes[itemNodes.length - 1]; - - itemNode.setAttribute('data-content-id', escapeHTML(item.ContentInfo.Content._id)); + const contentId = escapeHTML(item.ContentInfo.Content._id); + const locationId = item.id; + const currentVersionNo = item.ContentInfo.Content.CurrentVersion.Version.VersionInfo.versionNo; + const languageCodes = item.ContentInfo.Content.CurrentVersion.Version.VersionInfo.VersionTranslationInfo.Language.map( + (language) => language.languageCode, + ); + const itemActionsMenuContainer = relationsContainer.querySelector('.ibexa-embedded-item-actions__menu'); + const itemActionsTriggerElement = relationsContainer.querySelector('.ibexa-embedded-item-actions__menu-trigger-btn'); + const itemNodeNameCell = itemNode.querySelector('.ibexa-relations__item-name'); + + itemNode.dataset.contentId = contentId; itemNode.querySelector('.ibexa-relations__table-action--remove-item').addEventListener('click', removeItem, false); + + itemNodeNameCell.dataset.ibexaUpdateContentId = contentId; + itemNodeNameCell.dataset.ibexaUpdateSourceDataPath = 'Content.Name'; + + doc.body.dispatchEvent( + new CustomEvent('ibexa-embedded-item:create-dynamic-menu', { + detail: { + contentId, + locationId, + languageCodes, + versionNo: currentVersionNo, + menuTriggerElement: itemActionsTriggerElement, + menuContainer: itemActionsMenuContainer, + }, + }), + ); }); ibexa.helpers.tooltips.parse(); @@ -143,7 +168,7 @@ const { formatShortDateTime } = ibexa.helpers.timezone; const contentTypeName = ibexa.helpers.contentType.getContentTypeName(item.ContentInfo.Content.ContentTypeInfo.identifier); const contentName = escapeHTML(item.ContentInfo.Content.TranslatedName); - const contentId = item.ContentInfo.Content._id; + const contentId = escapeHTML(item.ContentInfo.Content._id); const { rowTemplate } = relationsWrapper.dataset; return rowTemplate diff --git a/src/bundle/Resources/public/scss/fieldType/edit/_ezobjectrelationlist.scss b/src/bundle/Resources/public/scss/fieldType/edit/_ezobjectrelationlist.scss index a4e077fa75..95cf838d92 100644 --- a/src/bundle/Resources/public/scss/fieldType/edit/_ezobjectrelationlist.scss +++ b/src/bundle/Resources/public/scss/fieldType/edit/_ezobjectrelationlist.scss @@ -61,6 +61,13 @@ &__table-action--remove-item { padding: calculateRem(4px); } + + &__actions-cell { + display: flex; + gap: calculateRem(4px); + justify-content: center; + align-items: center; + } } .btn { diff --git a/src/bundle/Resources/translations/content.en.xliff b/src/bundle/Resources/translations/content.en.xliff index 4c04367cf2..6ef4e0cb0b 100644 --- a/src/bundle/Resources/translations/content.en.xliff +++ b/src/bundle/Resources/translations/content.en.xliff @@ -61,6 +61,16 @@ Location: %location% key: editing_details + + Edit + Edit + key: embedded_items.action.edit + + + Go to content + Go to content + key: embedded_items.action.go_to_label + Back Back diff --git a/src/bundle/Resources/views/themes/admin/ui/component/embedded_item_actions/embedded_item_actions.html.twig b/src/bundle/Resources/views/themes/admin/ui/component/embedded_item_actions/embedded_item_actions.html.twig new file mode 100644 index 0000000000..5ff74f5585 --- /dev/null +++ b/src/bundle/Resources/views/themes/admin/ui/component/embedded_item_actions/embedded_item_actions.html.twig @@ -0,0 +1,46 @@ +
+ {% block loader %} + + {% endblock %} + + {% block embedded_item_menu_trigger %} + + {% endblock %} + + {% block embedded_item_menu %} + {% include '@ibexadesign/ui/component/multilevel_popup_menu/multilevel_popup_menu.html.twig' with { + is_custom_init: is_custom_init|default(true), + attr: { + 'data-content-id': content_id|default(''), + 'data-location-id': location_id|default(''), + 'data-version-no': version_no|default(''), + 'data-product-code': product_code|default(''), + 'data-language-codes': language_codes|default([])|json_encode, + class: attr.class|default('ibexa-embedded-item-actions__menu') + }, + items_container_attr: { + 'data-item-template-link': include('@ibexadesign/ui/component/multilevel_popup_menu/multilevel_popup_menu_item.html.twig', { + is_button: false, + label: '{{ label }}', + action_attr: { + target: '_blank' + } + }) + } + } only %} + {% endblock %} +
diff --git a/src/bundle/Resources/views/themes/admin/ui/field_type/edit/ezrichtext.html.twig b/src/bundle/Resources/views/themes/admin/ui/field_type/edit/ezrichtext.html.twig index 15d8965bd9..103cdf4361 100644 --- a/src/bundle/Resources/views/themes/admin/ui/field_type/edit/ezrichtext.html.twig +++ b/src/bundle/Resources/views/themes/admin/ui/field_type/edit/ezrichtext.html.twig @@ -16,4 +16,7 @@ {% endif %} + {% embed '@ibexadesign/ui/component/embedded_item_actions/embedded_item_actions.html.twig' only %} + {% block embedded_item_menu_trigger %}{% endblock %} + {% endembed %} {%- endblock -%} diff --git a/src/bundle/Resources/views/themes/admin/ui/field_type/edit/relation_base.html.twig b/src/bundle/Resources/views/themes/admin/ui/field_type/edit/relation_base.html.twig index 7e8525742f..bfc50e196d 100644 --- a/src/bundle/Resources/views/themes/admin/ui/field_type/edit/relation_base.html.twig +++ b/src/bundle/Resources/views/themes/admin/ui/field_type/edit/relation_base.html.twig @@ -7,7 +7,18 @@ {% set allowed_content_types = form.parent.vars.value.fieldDefinition.fieldSettings.selectionContentTypes %} {% set helper = helper|default('') %} {% set readonly = attr.readonly|default(false) %} - + {% set remove_item_btn %} + + {% endset %} {% set col_raw_checkbox_template %} {% endset %} - {% set col_raw_actions %} - + {% set col_raw_actions_template %} + {{ remove_item_btn }} + + {% include '@ibexadesign/ui/component/embedded_item_actions/embedded_item_actions.html.twig' only %} {% endset %} {% set body_row_cols_template = [] %} @@ -54,7 +58,7 @@ {% set body_row_cols_template = body_row_cols_template|merge([ { content: '{{ content_name }}', - attr: { class: 'ibexa-relations__item-name' }, + class: 'ibexa-relations__item-name', }, { content: '{{ content_type_name }}' }, { content: '{{ published_date }}' }, @@ -72,9 +76,9 @@ {% set body_row_cols_template = body_row_cols_template|merge([ { - content: col_raw_actions, + content: col_raw_actions_template, raw: true, - has_icon: true, + class: 'ibexa-relations__actions-cell' }, ]) %} @@ -84,13 +88,25 @@ class: 'ibexa-relations__item', }) }} {% endset %} +
{% set body_rows = [] %} + {% for relation in relations %} {% set body_row_cols = [] %} + {% set col_raw_actions %} + {{ remove_item_btn }} + + {% include '@ibexadesign/ui/component/embedded_item_actions/embedded_item_actions.html.twig' with { + content_id: relation.contentId, + location_id: relation.contentInfo.mainLocationId, + version_no: relation.contentInfo.currentVersionNo, + } only %} + {% endset %} + {% if relation.contentInfo is not null and relation.contentType is not null %} {% set col_raw_checkbox %} {% set attr = attr|merge({'hidden': 'hidden'}) %} {{ block('form_widget') }} - {% endblock %} diff --git a/src/bundle/Resources/views/themes/admin/ui/form_fields.html.twig b/src/bundle/Resources/views/themes/admin/ui/form_fields.html.twig index 673ef67f61..5628f23763 100644 --- a/src/bundle/Resources/views/themes/admin/ui/form_fields.html.twig +++ b/src/bundle/Resources/views/themes/admin/ui/form_fields.html.twig @@ -274,6 +274,9 @@
{% endif %} + {% embed '@ibexadesign/ui/component/embedded_item_actions/embedded_item_actions.html.twig' only %} + {% block embedded_item_menu_trigger %}{% endblock %} + {% endembed %} {%- endblock -%} diff --git a/src/bundle/Resources/views/themes/admin/ui/html_body.html.twig b/src/bundle/Resources/views/themes/admin/ui/html_body.html.twig index 4e7eec2e56..3e2b800863 100644 --- a/src/bundle/Resources/views/themes/admin/ui/html_body.html.twig +++ b/src/bundle/Resources/views/themes/admin/ui/html_body.html.twig @@ -1,7 +1,9 @@ -
+
{% set form = ibexa_render_embedded_item_edit_form() %} - {{ form_start(form) }} + {{ form_start(form, { + attr: { target: '_blank' } + }) }} {{ form_widget(form.content_info, { 'attr': { 'hidden': 'hidden', 'class': 'ibexa-embedded-item-edit__form-field ibexa-embedded-item-edit__form-field--content-info'