From f57d550b7be2ff6546bb2abae67959453a72ef7b Mon Sep 17 00:00:00 2001 From: Michele Locati Date: Tue, 28 Oct 2025 16:59:58 +0100 Subject: [PATCH 1/9] Add ConcreteFetch that uses window.fetch() instead of jQuery.ajax() --- assets/cms/js/fetch.js | 206 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 206 insertions(+) create mode 100644 assets/cms/js/fetch.js diff --git a/assets/cms/js/fetch.js b/assets/cms/js/fetch.js new file mode 100644 index 00000000..16942568 --- /dev/null +++ b/assets/cms/js/fetch.js @@ -0,0 +1,206 @@ +;(function(global) { + 'use strict' + + /** + * Recursively add fields to URLSearchParams. + * Used by buildRequestBody(). + * + * @param {string} prefix + * @param {Record|Array|any} value + * @param {URLSearchParams} urlSearchParams + * + * @returns {void} + */ + function addToUrlSearchParams(prefix, value, urlSearchParams) { + if (value === null || value === undefined || typeof value !== 'object') { + return; + } + if (!prefix && Array.isArray(value)) { + return; + } + for (const [key, val] of Object.entries(value)) { + if (val === null || val === undefined) { + continue; + } + const fieldName = prefix ? `${prefix}[${key}]` : key; + if (typeof val === 'object') { + addToUrlSearchParams(fieldName, val, urlSearchParams); + } else { + urlSearchParams.append(fieldName, String(val)); + } + } + } + + /** + * Build the request body for an AJAX request. + * + * @param {Record|any} data The object to build the body from + * + * @returns {URLSearchParams} The request body + * + * @example + * buildRequestBody({key: 'value', arr: [1, 2, 3], nested: {a: 'b'}}) + */ + function buildRequestBody(data) { + const urlSearchParams = new URLSearchParams(); + addToUrlSearchParams('', data, urlSearchParams); + return urlSearchParams; + } + + /** + * Prepare the request options for fetch(). + * + * @param {RequestInit|Record|null|undefined} request + * @param {Record|null|undefined} headers Additional headers to add (will not override existing ones) + * + * @returns {RequestInit} + */ + function prepareRequest(request, headers) { + if (request) { + try { + request = structuredClone(request); + } catch {} + } else { + request = {}; + } + // Default to GET if no method is specified + request.method = String(request.method || 'GET').toUpperCase(); + if (request.body?.constructor === Object) { + // Convert body object to URLSearchParams + request.body = buildRequestBody(request.body); + } + if (!request.headers) { + request.headers = {}; + } + if (!request.cache && request.method !== 'GET') { + // Disable caching for non-GET requests by default + request.cache = 'no-cache'; + } + const existingHeaderKeys = Object.keys(request.headers).map((key) => key.toLowerCase()); + if (headers) { + for (const [name, value] of Object.entries(headers)) { + const lowerCaseName = name.toLowerCase(); + if (!existingHeaderKeys.includes(lowerCaseName)) { + request.headers[name] = value; + existingHeaderKeys.push(lowerCaseName); + } + } + } + if (!existingHeaderKeys.includes('x-requested-with')) { + // Just to let the server know this is an AJAX request + request.headers['X-Requested-With'] = 'XMLHttpRequest'; + } + if (request.method !== 'GET' && request.body && !existingHeaderKeys.includes('content-type')) { + // We want to use $_POST on the server side + request.headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8'; + } + return request; + } + + /** + * Fetch JSON data from a URL. + * + * @param {string} url The URL to fetch data from + * @param {RequestInit|Record|null|undefined} request The request options and body + * + * @throws {Error} If the response contains an error or is not ok + * + * @returns {Promise} The JSON response + * + * @example + * try { + * const data = await fetchJson('/api/data', { + * method: 'POST', + * body: { + * key: 'value' + * } + * }); + * } catch (error) { + * window.alert(error.message); + * } + */ + async function fetchJson(url, request) { + request = prepareRequest(request, {Accept: 'application/json'}); + const response = await fetch(url, request); + const responseText = await response.text(); + let responseData; + try { + responseData = JSON.parse(responseText); + } catch { + throw new Error(responseText); + } + if (responseData?.errors?.length) { + throw new Error(responseData.errors[0]); + } + if (responseData?.error) { + throw new Error(responseData.error); + } + if (!response.ok) { + throw new Error(responseText); + } + return responseData; + } + + /** + * Fetch an HTML chunk from a URL. + * + * @param {string} url The URL to fetch data from + * @param {RequestInit|Record|undefined} request The request options and body + * + * @throws {Error} If the response contains an error or is not ok + * + * @returns {Promise} The HTML response + * + * @example + * try { + * const data = await fetchHtml('/api/render', { + * method: 'POST', + * body: { + * key: 'value' + * } + * }); + * } catch (error) { + * window.alert(error.message); + * } + */ + async function fetchHtml(url, request) { + request = prepareRequest( + request, + { + Accept: [ + // Prefer HTML + 'text/html', + // ... but accept JSON in case of errors + 'application/json;q=0.9', + // ... or plain text as a last resort fallback + 'text/plain;q=0.8', + ].join(', ') + } + ); + const response = await fetch(url, request); + const responseText = await response.text(); + try { + // Try to see if it's JSON with errors + const responseData = JSON.parse(responseText); + if (responseData?.errors?.length) { + throw new Error(responseData.errors[0]); + } + if (responseData?.error) { + throw new Error(responseData.error); + } + } catch { + // Not JSON, that's fine + } + if (!response.ok) { + throw new Error(responseText); + } + return responseText; + } + + global.ConcreteFetch = { + buildRequestBody, + fetchJson, + fetchHtml, + }; + +})(global); From ddbae952739782ce640f195b9d8ea69514943561 Mon Sep 17 00:00:00 2001 From: Michele Locati Date: Wed, 29 Oct 2025 10:53:12 +0100 Subject: [PATCH 2/9] Rename ConcreteFetch.fetchJson as ConcreteFetch.json, ConcreteFetch.fetchHtml as ConcreteFetch.html --- assets/cms/js/fetch.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/assets/cms/js/fetch.js b/assets/cms/js/fetch.js index 16942568..b7ebbcd9 100644 --- a/assets/cms/js/fetch.js +++ b/assets/cms/js/fetch.js @@ -48,7 +48,7 @@ } /** - * Prepare the request options for fetch(). + * Prepare the request options for window.fetch(). * * @param {RequestInit|Record|null|undefined} request * @param {Record|null|undefined} headers Additional headers to add (will not override existing ones) @@ -199,8 +199,8 @@ global.ConcreteFetch = { buildRequestBody, - fetchJson, - fetchHtml, + json: fetchJson, + html: fetchHtml, }; })(global); From 5a5675fdfa3e0c3f7ea5bc61e9bfe37ae310b197 Mon Sep 17 00:00:00 2001 From: Michele Locati Date: Wed, 29 Oct 2025 11:01:15 +0100 Subject: [PATCH 3/9] Add responseData to thrown errors --- assets/cms/js/fetch.js | 41 ++++++++++++++++++++++++++++------------- 1 file changed, 28 insertions(+), 13 deletions(-) diff --git a/assets/cms/js/fetch.js b/assets/cms/js/fetch.js index b7ebbcd9..f6de0d5c 100644 --- a/assets/cms/js/fetch.js +++ b/assets/cms/js/fetch.js @@ -97,6 +97,26 @@ return request; } + /** + * Check a JSON response for errors. + * + * @param {any} responseData + * + * @throws {Error} If the response data contains errors (the thrown error will have a responseData property) + */ + function checkJsonResponse(responseData) { + if (responseData?.errors?.length) { + const error = new Error(responseData.errors[0]); + error.responseData = responseData; + throw error; + } + if (responseData?.error) { + const error = new Error(responseData.error); + error.responseData = responseData; + throw error; + } + } + /** * Fetch JSON data from a URL. * @@ -129,12 +149,7 @@ } catch { throw new Error(responseText); } - if (responseData?.errors?.length) { - throw new Error(responseData.errors[0]); - } - if (responseData?.error) { - throw new Error(responseData.error); - } + checkJsonResponse(responseData); if (!response.ok) { throw new Error(responseText); } @@ -179,18 +194,18 @@ ); const response = await fetch(url, request); const responseText = await response.text(); + let responseData try { // Try to see if it's JSON with errors - const responseData = JSON.parse(responseText); - if (responseData?.errors?.length) { - throw new Error(responseData.errors[0]); - } - if (responseData?.error) { - throw new Error(responseData.error); - } + responseData = JSON.parse(responseText); } catch { // Not JSON, that's fine + responseData = null; } + if (responseData) { + checkJsonResponse(responseData); + } + if (!response.ok) { throw new Error(responseText); } From fa5c74c715d31706c470eb1c80a07c680e814fd6 Mon Sep 17 00:00:00 2001 From: Michele Locati Date: Wed, 29 Oct 2025 11:04:26 +0100 Subject: [PATCH 4/9] npm run lint:fix --- assets/cms/js/fetch.js | 99 +++++++++++++++++++++--------------------- 1 file changed, 49 insertions(+), 50 deletions(-) diff --git a/assets/cms/js/fetch.js b/assets/cms/js/fetch.js index f6de0d5c..d7d5f112 100644 --- a/assets/cms/js/fetch.js +++ b/assets/cms/js/fetch.js @@ -13,20 +13,20 @@ */ function addToUrlSearchParams(prefix, value, urlSearchParams) { if (value === null || value === undefined || typeof value !== 'object') { - return; + return } if (!prefix && Array.isArray(value)) { - return; + return } for (const [key, val] of Object.entries(value)) { if (val === null || val === undefined) { - continue; + continue } - const fieldName = prefix ? `${prefix}[${key}]` : key; + const fieldName = prefix ? `${prefix}[${key}]` : key if (typeof val === 'object') { - addToUrlSearchParams(fieldName, val, urlSearchParams); + addToUrlSearchParams(fieldName, val, urlSearchParams) } else { - urlSearchParams.append(fieldName, String(val)); + urlSearchParams.append(fieldName, String(val)) } } } @@ -42,9 +42,9 @@ * buildRequestBody({key: 'value', arr: [1, 2, 3], nested: {a: 'b'}}) */ function buildRequestBody(data) { - const urlSearchParams = new URLSearchParams(); - addToUrlSearchParams('', data, urlSearchParams); - return urlSearchParams; + const urlSearchParams = new URLSearchParams() + addToUrlSearchParams('', data, urlSearchParams) + return urlSearchParams } /** @@ -58,43 +58,43 @@ function prepareRequest(request, headers) { if (request) { try { - request = structuredClone(request); + request = global.structuredClone(request) } catch {} } else { - request = {}; + request = {} } // Default to GET if no method is specified - request.method = String(request.method || 'GET').toUpperCase(); + request.method = String(request.method || 'GET').toUpperCase() if (request.body?.constructor === Object) { // Convert body object to URLSearchParams - request.body = buildRequestBody(request.body); + request.body = buildRequestBody(request.body) } if (!request.headers) { - request.headers = {}; + request.headers = {} } if (!request.cache && request.method !== 'GET') { // Disable caching for non-GET requests by default - request.cache = 'no-cache'; + request.cache = 'no-cache' } - const existingHeaderKeys = Object.keys(request.headers).map((key) => key.toLowerCase()); + const existingHeaderKeys = Object.keys(request.headers).map((key) => key.toLowerCase()) if (headers) { for (const [name, value] of Object.entries(headers)) { - const lowerCaseName = name.toLowerCase(); + const lowerCaseName = name.toLowerCase() if (!existingHeaderKeys.includes(lowerCaseName)) { - request.headers[name] = value; - existingHeaderKeys.push(lowerCaseName); + request.headers[name] = value + existingHeaderKeys.push(lowerCaseName) } } } if (!existingHeaderKeys.includes('x-requested-with')) { // Just to let the server know this is an AJAX request - request.headers['X-Requested-With'] = 'XMLHttpRequest'; + request.headers['X-Requested-With'] = 'XMLHttpRequest' } if (request.method !== 'GET' && request.body && !existingHeaderKeys.includes('content-type')) { // We want to use $_POST on the server side - request.headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8'; + request.headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8' } - return request; + return request } /** @@ -106,14 +106,14 @@ */ function checkJsonResponse(responseData) { if (responseData?.errors?.length) { - const error = new Error(responseData.errors[0]); - error.responseData = responseData; - throw error; + const error = new Error(responseData.errors[0]) + error.responseData = responseData + throw error } if (responseData?.error) { - const error = new Error(responseData.error); - error.responseData = responseData; - throw error; + const error = new Error(responseData.error) + error.responseData = responseData + throw error } } @@ -140,20 +140,20 @@ * } */ async function fetchJson(url, request) { - request = prepareRequest(request, {Accept: 'application/json'}); - const response = await fetch(url, request); - const responseText = await response.text(); - let responseData; + request = prepareRequest(request, { Accept: 'application/json' }) + const response = await fetch(url, request) + const responseText = await response.text() + let responseData try { - responseData = JSON.parse(responseText); + responseData = JSON.parse(responseText) } catch { - throw new Error(responseText); + throw new Error(responseText) } - checkJsonResponse(responseData); + checkJsonResponse(responseData) if (!response.ok) { - throw new Error(responseText); + throw new Error(responseText) } - return responseData; + return responseData } /** @@ -188,34 +188,33 @@ // ... but accept JSON in case of errors 'application/json;q=0.9', // ... or plain text as a last resort fallback - 'text/plain;q=0.8', + 'text/plain;q=0.8' ].join(', ') } - ); - const response = await fetch(url, request); - const responseText = await response.text(); + ) + const response = await fetch(url, request) + const responseText = await response.text() let responseData try { // Try to see if it's JSON with errors - responseData = JSON.parse(responseText); + responseData = JSON.parse(responseText) } catch { // Not JSON, that's fine - responseData = null; + responseData = null } if (responseData) { - checkJsonResponse(responseData); + checkJsonResponse(responseData) } if (!response.ok) { - throw new Error(responseText); + throw new Error(responseText) } - return responseText; + return responseText } global.ConcreteFetch = { buildRequestBody, json: fetchJson, - html: fetchHtml, - }; - -})(global); + html: fetchHtml + } +})(global) From 217a60249a69d2b33f04a8303a8a53b0d3f5ca7e Mon Sep 17 00:00:00 2001 From: Michele Locati Date: Thu, 30 Oct 2025 15:24:26 +0100 Subject: [PATCH 5/9] Add Concrete.Vue.activateContextAsync() --- assets/cms/js/vue/Manager.js | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/assets/cms/js/vue/Manager.js b/assets/cms/js/vue/Manager.js index 038e9ee9..155fd8d8 100644 --- a/assets/cms/js/vue/Manager.js +++ b/assets/cms/js/vue/Manager.js @@ -59,6 +59,24 @@ export default class Manager { }, 10) } + /** + * Activates a particular context (and its components) for a particular selector, returning a promise. + * + * @param {String} context + * + * @returns {Promise<{Vue: typeof Vue, options: Record}> + * + * @example + * const {Vue, options} = await Concrete.Vue.activateContextAsync('cms') + */ + activateContextAsync(context) { + return new Promise((resolve) => { + this.activateContext(context, (Vue, options) => { + resolve({ Vue, options }) + }) + }) + } + /** * For a given string `context`, adds the passed components to make them available within that context. * From 5bc83446d6fb3bd89a87d357d755c7bd00a7f419 Mon Sep 17 00:00:00 2001 From: Andrew Embler Date: Wed, 5 Nov 2025 16:26:53 -0800 Subject: [PATCH 6/9] Include dropzone.css in the conversations feature --- assets/conversations/scss/frontend.scss | 2 ++ package.json | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/assets/conversations/scss/frontend.scss b/assets/conversations/scss/frontend.scss index 3820d6f7..e55cc133 100644 --- a/assets/conversations/scss/frontend.scss +++ b/assets/conversations/scss/frontend.scss @@ -9,4 +9,6 @@ @import 'bootstrap/scss/mixins'; @import '../../cms/scss/variables'; +@import 'dropzone/dist/dropzone.css'; + @import 'frontend/frontend'; diff --git a/package.json b/package.json index f098a327..1289f7ce 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@concretecms/bedrock", - "version": "1.6.4", + "version": "1.6.5", "description": "The asset framework and dependencies for Concrete CMS.", "scripts": { "lint": "standardx \"**/*.{js,vue}\" && stylelint assets/**/*.{scss,vue}", From 554cb489d1103a93abf6ea778d34dca5313068ef Mon Sep 17 00:00:00 2001 From: Andrew Embler Date: Thu, 6 Nov 2025 15:44:32 -0800 Subject: [PATCH 7/9] Fix https://github.com/concretecms/concretecms/issues/12046 --- assets/cms/js/page-notification.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/cms/js/page-notification.js b/assets/cms/js/page-notification.js index ec3a3f84..10ff827b 100644 --- a/assets/cms/js/page-notification.js +++ b/assets/cms/js/page-notification.js @@ -14,7 +14,7 @@ const modules = new Map([ class PageNotification { constructor() { const notificationsBoxHTML = ` -
+
${ccmi18n.notifications}
From 362bf1984df03fe3163270f9badf90a540e61e8b Mon Sep 17 00:00:00 2001 From: Andrew Embler Date: Thu, 6 Nov 2025 15:45:13 -0800 Subject: [PATCH 8/9] updating to 1.6.6 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 508675c2..6a22470b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@concretecms/bedrock", - "version": "1.6.4", + "version": "1.6.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@concretecms/bedrock", - "version": "1.6.4", + "version": "1.6.6", "license": "MIT", "dependencies": { "@fortawesome/fontawesome-free": "^5.15.1", diff --git a/package.json b/package.json index 1289f7ce..2b30cf5b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@concretecms/bedrock", - "version": "1.6.5", + "version": "1.6.6", "description": "The asset framework and dependencies for Concrete CMS.", "scripts": { "lint": "standardx \"**/*.{js,vue}\" && stylelint assets/**/*.{scss,vue}", From 597786fe98dfece1bc93e29851942970b470fce8 Mon Sep 17 00:00:00 2001 From: Andrew Embler Date: Thu, 6 Nov 2025 16:01:29 -0800 Subject: [PATCH 9/9] 1.6.7 --- assets/conversations/js/frontend/conversations.js | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/assets/conversations/js/frontend/conversations.js b/assets/conversations/js/frontend/conversations.js index 32b037b9..08f853a4 100644 --- a/assets/conversations/js/frontend/conversations.js +++ b/assets/conversations/js/frontend/conversations.js @@ -176,7 +176,7 @@ window._ = _ updateStatus: function(data) { if (data.status == 'ready') { var $form = $('form[data-conversation-form=subscribe]') - $('button').on('click', $form, function(e) { + $('button.btn-primary').on('click', $form, function(e) { e.preventDefault() e.stopPropagation() $.ajax({ diff --git a/package-lock.json b/package-lock.json index 6a22470b..1f2e17ef 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@concretecms/bedrock", - "version": "1.6.6", + "version": "1.6.7", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@concretecms/bedrock", - "version": "1.6.6", + "version": "1.6.7", "license": "MIT", "dependencies": { "@fortawesome/fontawesome-free": "^5.15.1", diff --git a/package.json b/package.json index 2b30cf5b..24a82a3a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@concretecms/bedrock", - "version": "1.6.6", + "version": "1.6.7", "description": "The asset framework and dependencies for Concrete CMS.", "scripts": { "lint": "standardx \"**/*.{js,vue}\" && stylelint assets/**/*.{scss,vue}",