diff --git a/ProcessMaker/Console/Commands/NormalizeScreenInlineImages.php b/ProcessMaker/Console/Commands/NormalizeScreenInlineImages.php
new file mode 100644
index 0000000000..fd8342bdca
--- /dev/null
+++ b/ProcessMaker/Console/Commands/NormalizeScreenInlineImages.php
@@ -0,0 +1,60 @@
+orderBy('id');
+ if ($this->option('screen')) {
+ $query->where('id', (int) $this->option('screen'));
+ }
+
+ $dryRun = (bool) $this->option('dry-run');
+ $scanned = 0;
+ $modified = 0;
+ $converted = 0;
+
+ $query->chunkById(50, function ($screens) use ($normalizer, $dryRun, &$scanned, &$modified, &$converted) {
+ foreach ($screens as $screen) {
+ $scanned++;
+ $config = $screen->config;
+ if (!is_array($config) || !$normalizer->configContainsInlineImages($config)) {
+ continue;
+ }
+
+ if ($dryRun) {
+ $modified++;
+ $this->line("[dry-run] Screen #{$screen->id} ({$screen->title}) contains inline images");
+ continue;
+ }
+
+ $result = $normalizer->normalize($screen, $config);
+ if (!$result->wasModified()) {
+ continue;
+ }
+
+ $screen->config = $result->config();
+ $screen->saveOrFail();
+ $modified++;
+ $converted += $result->convertedCount();
+ $this->info("Screen #{$screen->id}: converted {$result->convertedCount()} image(s)");
+ }
+ });
+
+ $this->info("Scanned {$scanned} screen(s); modified {$modified}; new media {$converted}");
+
+ return self::SUCCESS;
+ }
+}
diff --git a/ProcessMaker/Http/Controllers/Api/FileController.php b/ProcessMaker/Http/Controllers/Api/FileController.php
index 47ad5560a9..99d1864b53 100644
--- a/ProcessMaker/Http/Controllers/Api/FileController.php
+++ b/ProcessMaker/Http/Controllers/Api/FileController.php
@@ -226,6 +226,7 @@ public function store(Request $request)
'model_id' => $addedMedia->model_id,
'file_name' => $addedMedia->file_name,
'mime_type' => $addedMedia->mime_type,
+ 'url' => $addedMedia->getUrl(),
], 200);
}
diff --git a/ProcessMaker/Http/Controllers/Api/ScreenController.php b/ProcessMaker/Http/Controllers/Api/ScreenController.php
index d0545d6042..9f208d590f 100644
--- a/ProcessMaker/Http/Controllers/Api/ScreenController.php
+++ b/ProcessMaker/Http/Controllers/Api/ScreenController.php
@@ -20,6 +20,8 @@
use ProcessMaker\Models\ScreenType;
use ProcessMaker\ProcessTranslations\ScreenTranslation;
use ProcessMaker\Query\SyntaxError;
+use ProcessMaker\Screens\ScreenInlineImageNormalizationResult;
+use ProcessMaker\Screens\ScreenInlineImageNormalizer;
use ProcessMaker\Traits\ProjectAssetTrait;
class ScreenController extends Controller
@@ -301,6 +303,8 @@ public function update(Screen $screen, Request $request)
$screen->fill($request->input());
$original = $screen->getOriginal();
+ $normalization = $this->normalizeInlineImages($screen);
+
$this->updateScreenDetails($request, $screen, $original, $lastVersion);
$screen->saveOrFail();
@@ -319,7 +323,7 @@ public function update(Screen $screen, Request $request)
$screenCache = ScreenCacheFactory::getScreenCache();
$screenCache->clearCompiledAssets();
- return response([], 204);
+ return $this->screenSaveResponse($normalization);
}
public function updateScreenDetails($request, $screen, $original, $lastVersion)
@@ -392,9 +396,38 @@ public function draft(Screen $screen, Request $request)
{
$request->validate(Screen::rules($screen));
$screen->fill($request->input());
+ $normalization = $this->normalizeInlineImages($screen);
$screen->saveDraft();
- return response([], 204);
+ return $this->screenSaveResponse($normalization);
+ }
+
+ private function normalizeInlineImages(Screen $screen): ScreenInlineImageNormalizationResult
+ {
+ $config = $screen->config;
+ if (!is_array($config)) {
+ return new ScreenInlineImageNormalizationResult([], 0, 0);
+ }
+
+ $result = app(ScreenInlineImageNormalizer::class)->normalize($screen, $config);
+ if ($result->wasModified()) {
+ $screen->config = $result->config();
+ }
+
+ return $result;
+ }
+
+ private function screenSaveResponse(ScreenInlineImageNormalizationResult $normalization)
+ {
+ if (!$normalization->wasModified()) {
+ return response([], 204);
+ }
+
+ return response([
+ 'converted_images' => $normalization->convertedCount(),
+ 'replaced_images' => $normalization->replacedCount(),
+ 'config' => $normalization->config(),
+ ], 200);
}
public function close(Screen $screen)
diff --git a/ProcessMaker/Models/Screen.php b/ProcessMaker/Models/Screen.php
index 734af25e52..02239718da 100644
--- a/ProcessMaker/Models/Screen.php
+++ b/ProcessMaker/Models/Screen.php
@@ -18,6 +18,8 @@
use ProcessMaker\Traits\ProjectAssetTrait;
use ProcessMaker\Traits\SerializeToIso8601;
use ProcessMaker\Validation\CategoryRule;
+use Spatie\MediaLibrary\HasMedia;
+use Spatie\MediaLibrary\InteractsWithMedia;
/**
* Class Screen
@@ -64,7 +66,7 @@
* @OA\Property(property="url", type="string"),
* )
*/
-class Screen extends ProcessMakerModel implements ScreenInterface, PrometheusMetricInterface
+class Screen extends ProcessMakerModel implements ScreenInterface, PrometheusMetricInterface, HasMedia
{
use SerializeToIso8601;
use HideSystemResources;
@@ -74,9 +76,12 @@ class Screen extends ProcessMakerModel implements ScreenInterface, PrometheusMet
use ExtendedPMQL;
use Exportable;
use ProjectAssetTrait;
+ use InteractsWithMedia;
const categoryClass = ScreenCategory::class;
+ public const INLINE_IMAGES_COLLECTION = 'inline_images';
+
protected $connection = 'processmaker';
/**
@@ -119,6 +124,11 @@ public static function boot()
static::deleting($clearCacheCallback);
}
+ public function registerMediaCollections(): void
+ {
+ $this->addMediaCollection(self::INLINE_IMAGES_COLLECTION);
+ }
+
/**
* Validation rules
*
diff --git a/ProcessMaker/Screens/ScreenInlineImageNormalizationResult.php b/ProcessMaker/Screens/ScreenInlineImageNormalizationResult.php
new file mode 100644
index 0000000000..5adb94dc5b
--- /dev/null
+++ b/ProcessMaker/Screens/ScreenInlineImageNormalizationResult.php
@@ -0,0 +1,33 @@
+config;
+ }
+
+ public function convertedCount(): int
+ {
+ return $this->convertedCount;
+ }
+
+ public function replacedCount(): int
+ {
+ return $this->replacedCount;
+ }
+
+ public function wasModified(): bool
+ {
+ return $this->replacedCount > 0;
+ }
+}
diff --git a/ProcessMaker/Screens/ScreenInlineImageNormalizer.php b/ProcessMaker/Screens/ScreenInlineImageNormalizer.php
new file mode 100644
index 0000000000..1c9257b304
--- /dev/null
+++ b/ProcessMaker/Screens/ScreenInlineImageNormalizer.php
@@ -0,0 +1,254 @@
+convertedCount = 0;
+ $this->replacedCount = 0;
+ $normalized = $this->walkPages($screen, $config);
+
+ return new ScreenInlineImageNormalizationResult(
+ $normalized,
+ $this->convertedCount,
+ $this->replacedCount
+ );
+ }
+
+ public function configContainsInlineImages(array $config): bool
+ {
+ return $this->containsInlineImagesInPages($config);
+ }
+
+ private function containsInlineImagesInPages(array $config): bool
+ {
+ foreach ($config as $page) {
+ if (!is_array($page) || empty($page['items']) || !is_array($page['items'])) {
+ continue;
+ }
+ if ($this->containsInlineImagesInItems($page['items'])) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ private function containsInlineImagesInItems(array $items): bool
+ {
+ foreach ($items as $item) {
+ if (!is_array($item)) {
+ continue;
+ }
+
+ $component = Arr::get($item, 'component');
+ if ($component === 'FormMultiColumn') {
+ if ($this->containsInlineImagesInMultiColumn(Arr::get($item, 'items', []))) {
+ return true;
+ }
+ continue;
+ }
+
+ if (!empty($item['items']) && is_array($item['items'])) {
+ if ($this->containsInlineImagesInItems($item['items'])) {
+ return true;
+ }
+ }
+
+ if ($component === 'FormHtmlViewer' && $this->contentHasInlineImage(Arr::get($item, 'config.content'))) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ private function containsInlineImagesInMultiColumn(array $columns): bool
+ {
+ foreach ($columns as $columnItems) {
+ if (!is_array($columnItems)) {
+ continue;
+ }
+ if ($this->containsInlineImagesInItems($columnItems)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ private function contentHasInlineImage(mixed $content): bool
+ {
+ return is_string($content)
+ && $content !== ''
+ && (bool) preg_match(self::DATA_IMAGE_PATTERN, $content);
+ }
+
+ private function walkPages(Screen $screen, array $config): array
+ {
+ foreach ($config as $pageIndex => $page) {
+ if (!is_array($page)) {
+ continue;
+ }
+ if (!empty($page['items']) && is_array($page['items'])) {
+ $config[$pageIndex]['items'] = $this->walkItems($screen, $page['items']);
+ }
+ }
+
+ return $config;
+ }
+
+ private function walkItems(Screen $screen, array $items): array
+ {
+ foreach ($items as $index => $item) {
+ if (!is_array($item)) {
+ continue;
+ }
+
+ $component = Arr::get($item, 'component');
+ if ($component === 'FormMultiColumn') {
+ $items[$index]['items'] = $this->walkMultiColumn($screen, Arr::get($item, 'items', []));
+ continue;
+ }
+
+ if (!empty($item['items']) && is_array($item['items'])) {
+ $items[$index]['items'] = $this->walkItems($screen, $item['items']);
+ }
+
+ $items[$index] = $this->normalizeItem($screen, $items[$index]);
+ }
+
+ return $items;
+ }
+
+ private function walkMultiColumn(Screen $screen, array $columns): array
+ {
+ foreach ($columns as $columnIndex => $columnItems) {
+ if (!is_array($columnItems)) {
+ continue;
+ }
+ $columns[$columnIndex] = $this->walkItems($screen, $columnItems);
+ }
+
+ return $columns;
+ }
+
+ private function normalizeItem(Screen $screen, array $item): array
+ {
+ if (Arr::get($item, 'component') !== 'FormHtmlViewer') {
+ return $item;
+ }
+
+ $content = Arr::get($item, 'config.content');
+ if (is_string($content) && $content !== '') {
+ Arr::set($item, 'config.content', $this->replaceDataImagesInHtml($screen, $content));
+ }
+
+ return $item;
+ }
+
+ private function replaceDataImagesInHtml(Screen $screen, string $html): string
+ {
+ return preg_replace_callback(
+ self::DATA_IMAGE_PATTERN,
+ function (array $matches) use ($screen) {
+ $dataUri = $matches[0];
+ $url = $this->storeDataImage($screen, $dataUri);
+
+ return $url ?? $dataUri;
+ },
+ $html
+ ) ?? $html;
+ }
+
+ private function storeDataImage(Screen $screen, string $dataUri): ?string
+ {
+ if (!preg_match(self::DATA_IMAGE_PATTERN, $dataUri, $matches)) {
+ return null;
+ }
+
+ $extension = $this->extensionFromMime($matches[1]);
+ $payload = $matches[2];
+ $hash = hash('sha256', $payload);
+ $url = $this->urlForExistingHash($screen, $hash);
+
+ if ($url === null) {
+ $url = $this->createMediaFromDataUri($screen, $dataUri, $hash, $extension);
+ }
+
+ if ($url !== null) {
+ $this->replacedCount++;
+ }
+
+ return $url;
+ }
+
+ private function urlForExistingHash(Screen $screen, string $hash): ?string
+ {
+ $existing = $screen->media()
+ ->where('collection_name', self::COLLECTION)
+ ->where('custom_properties->' . self::CONTENT_HASH_PROPERTY, $hash)
+ ->first();
+
+ return $existing?->getUrl();
+ }
+
+ private function createMediaFromDataUri(Screen $screen, string $dataUri, string $hash, string $extension): ?string
+ {
+ try {
+ $media = $screen
+ ->addMediaFromBase64($dataUri)
+ ->usingFileName('inline-' . substr($hash, 0, 12) . '.' . $extension)
+ ->withCustomProperties([
+ self::CONTENT_HASH_PROPERTY => $hash,
+ 'source' => 'screen_inline_image',
+ ])
+ ->toMediaCollection(self::COLLECTION);
+ } catch (Throwable $exception) {
+ Log::warning('Failed to store screen inline image as media', [
+ 'screen_id' => $screen->id,
+ 'message' => $exception->getMessage(),
+ ]);
+
+ return null;
+ }
+
+ $this->convertedCount++;
+
+ return $media->getUrl();
+ }
+
+ private function extensionFromMime(string $mimeSubtype): string
+ {
+ $normalized = strtolower($mimeSubtype);
+ $map = [
+ 'jpeg' => 'jpg',
+ 'svg+xml' => 'svg',
+ ];
+
+ if (isset($map[$normalized])) {
+ return $map[$normalized];
+ }
+
+ $safe = preg_replace('/[^a-z0-9]/', '', $normalized);
+
+ return $safe !== '' ? $safe : 'bin';
+ }
+}
diff --git a/public/builds/login/js/app-login.js b/public/builds/login/js/app-login.js
index 3e4e5777dd..be80587302 100644
--- a/public/builds/login/js/app-login.js
+++ b/public/builds/login/js/app-login.js
@@ -2944,13 +2944,13 @@ __webpack_require__.r(__webpack_exports__);
/* harmony import */ var _modules_lang_js__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(/*! ./modules/lang.js */ "./resources/js/modules/lang.js");
/* harmony import */ var _components_common_mixins_accessibility__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(/*! ./components/common/mixins/accessibility */ "./resources/js/components/common/mixins/accessibility.js");
var _document$head$queryS;
-function cov_2337h25edu() {
- var path = "/Users/sanjacornelius/Documents/processmaker/resources/js/app-login.js";
- var hash = "195e5558a5d99dbe9490879ad07aabe07e89bf1c";
+function cov_1alrk227ck() {
+ var path = "/Users/agustin/Sites/processmaker/resources/js/app-login.js";
+ var hash = "2651c9a18250a5d9e2cfc266641c2dc42915035e";
var global = new Function("return this")();
var gcv = "__coverage__";
var coverageData = {
- path: "/Users/sanjacornelius/Documents/processmaker/resources/js/app-login.js",
+ path: "/Users/agustin/Sites/processmaker/resources/js/app-login.js",
statementMap: {
"0": {
start: {
@@ -3928,7 +3928,7 @@ function cov_2337h25edu() {
"7": [0, 0]
},
_coverageSchema: "1a1c01bbd47fc00a2c39e90264f33305004495a9",
- hash: "195e5558a5d99dbe9490879ad07aabe07e89bf1c"
+ hash: "2651c9a18250a5d9e2cfc266641c2dc42915035e"
};
var coverage = global[gcv] || (global[gcv] = {});
if (!coverage[path] || coverage[path].hash !== hash) {
@@ -3937,13 +3937,13 @@ function cov_2337h25edu() {
var actualCoverage = coverage[path];
{
// @ts-ignore
- cov_2337h25edu = function () {
+ cov_1alrk227ck = function () {
return actualCoverage;
};
}
return actualCoverage;
}
-cov_2337h25edu();
+cov_1alrk227ck();
@@ -3959,35 +3959,35 @@ cov_2337h25edu();
-cov_2337h25edu().s[0]++;
+cov_1alrk227ck().s[0]++;
window.__ = _modules_lang_js__WEBPACK_IMPORTED_MODULE_7__["default"];
-cov_2337h25edu().s[1]++;
+cov_1alrk227ck().s[1]++;
window._ = __webpack_require__(/*! lodash */ "./node_modules/lodash/lodash.js");
-cov_2337h25edu().s[2]++;
+cov_1alrk227ck().s[2]++;
window.Popper = (__webpack_require__(/*! popper.js */ "./node_modules/popper.js/dist/esm/popper.js")["default"]);
-cov_2337h25edu().s[3]++;
+cov_1alrk227ck().s[3]++;
window.$ = window.jQuery = __webpack_require__(/*! jquery */ "./node_modules/jquery/dist/jquery.js");
-cov_2337h25edu().s[4]++;
+cov_1alrk227ck().s[4]++;
window.Vue = vue__WEBPACK_IMPORTED_MODULE_9__["default"];
-cov_2337h25edu().s[5]++;
+cov_1alrk227ck().s[5]++;
window.vue = vue__WEBPACK_IMPORTED_MODULE_9__;
-cov_2337h25edu().s[6]++;
+cov_1alrk227ck().s[6]++;
window.bootstrap = bootstrap__WEBPACK_IMPORTED_MODULE_0__;
-cov_2337h25edu().s[7]++;
+cov_1alrk227ck().s[7]++;
window.Vue.use((vue_cookies__WEBPACK_IMPORTED_MODULE_6___default()));
-cov_2337h25edu().s[8]++;
+cov_1alrk227ck().s[8]++;
window.Vue.use(_panter_vue_i18next__WEBPACK_IMPORTED_MODULE_5__["default"]);
-var translationsLoaded = (cov_2337h25edu().s[9]++, false);
-var mdates = (cov_2337h25edu().s[10]++, JSON.parse((_document$head$queryS = document.head.querySelector("meta[name=\"i18n-mdate\"]")) === null || _document$head$queryS === void 0 ? void 0 : _document$head$queryS.content));
+var translationsLoaded = (cov_1alrk227ck().s[9]++, false);
+var mdates = (cov_1alrk227ck().s[10]++, JSON.parse((_document$head$queryS = document.head.querySelector("meta[name=\"i18n-mdate\"]")) === null || _document$head$queryS === void 0 ? void 0 : _document$head$queryS.content));
// Make $t available to all vue instances
-cov_2337h25edu().s[11]++;
+cov_1alrk227ck().s[11]++;
vue__WEBPACK_IMPORTED_MODULE_9__["default"].mixin({
i18n: new _panter_vue_i18next__WEBPACK_IMPORTED_MODULE_5__["default"](i18next__WEBPACK_IMPORTED_MODULE_1__["default"])
});
-cov_2337h25edu().s[12]++;
+cov_1alrk227ck().s[12]++;
vue__WEBPACK_IMPORTED_MODULE_9__["default"].mixin(_components_common_mixins_accessibility__WEBPACK_IMPORTED_MODULE_8__["default"]);
-cov_2337h25edu().s[13]++;
+cov_1alrk227ck().s[13]++;
window.ProcessMaker = {
i18n: i18next__WEBPACK_IMPORTED_MODULE_1__["default"],
/**
@@ -3997,25 +3997,25 @@ window.ProcessMaker = {
packages: [],
missingTranslations: new Set(),
missingTranslation: function missingTranslation(value) {
- cov_2337h25edu().f[0]++;
- cov_2337h25edu().s[14]++;
+ cov_1alrk227ck().f[0]++;
+ cov_1alrk227ck().s[14]++;
if (this.missingTranslations.has(value)) {
- cov_2337h25edu().b[0][0]++;
- cov_2337h25edu().s[15]++;
+ cov_1alrk227ck().b[0][0]++;
+ cov_1alrk227ck().s[15]++;
return;
} else {
- cov_2337h25edu().b[0][1]++;
+ cov_1alrk227ck().b[0][1]++;
}
- cov_2337h25edu().s[16]++;
+ cov_1alrk227ck().s[16]++;
this.missingTranslations.add(value);
- cov_2337h25edu().s[17]++;
+ cov_1alrk227ck().s[17]++;
console.warn("Missing Translation:", value);
},
$notifications: {
icons: {}
}
};
-cov_2337h25edu().s[18]++;
+cov_1alrk227ck().s[18]++;
window.ProcessMaker.i18nPromise = i18next__WEBPACK_IMPORTED_MODULE_1__["default"].use((i18next_chained_backend__WEBPACK_IMPORTED_MODULE_2___default())).init({
lng: document.documentElement.lang,
fallbackLng: "en",
@@ -4025,20 +4025,20 @@ window.ProcessMaker.i18nPromise = i18next__WEBPACK_IMPORTED_MODULE_1__["default"
nsSeparator: false,
keySeparator: false,
parseMissingKeyHandler: function parseMissingKeyHandler(value) {
- cov_2337h25edu().f[1]++;
- cov_2337h25edu().s[19]++;
+ cov_1alrk227ck().f[1]++;
+ cov_1alrk227ck().s[19]++;
if (!translationsLoaded) {
- cov_2337h25edu().b[1][0]++;
- cov_2337h25edu().s[20]++;
+ cov_1alrk227ck().b[1][0]++;
+ cov_1alrk227ck().s[20]++;
return value;
} else {
- cov_2337h25edu().b[1][1]++;
+ cov_1alrk227ck().b[1][1]++;
}
// Report that a translation is missing
- cov_2337h25edu().s[21]++;
+ cov_1alrk227ck().s[21]++;
window.ProcessMaker.missingTranslation(value);
// Fallback to showing the english version
- cov_2337h25edu().s[22]++;
+ cov_1alrk227ck().s[22]++;
return value;
},
backend: {
@@ -4052,10 +4052,10 @@ window.ProcessMaker.i18nPromise = i18next__WEBPACK_IMPORTED_MODULE_1__["default"
}]
}
});
-cov_2337h25edu().s[23]++;
+cov_1alrk227ck().s[23]++;
window.ProcessMaker.i18nPromise.then(function () {
- cov_2337h25edu().f[2]++;
- cov_2337h25edu().s[24]++;
+ cov_1alrk227ck().f[2]++;
+ cov_1alrk227ck().s[24]++;
translationsLoaded = true;
});
@@ -4064,85 +4064,85 @@ window.ProcessMaker.i18nPromise.then(function () {
* REST api endpoints through oauth authentication
*
*/
-cov_2337h25edu().s[25]++;
+cov_1alrk227ck().s[25]++;
window.ProcessMaker.apiClient = __webpack_require__(/*! axios */ "./node_modules/axios/index.js");
-cov_2337h25edu().s[26]++;
+cov_1alrk227ck().s[26]++;
window.ProcessMaker.apiClient.defaults.headers.common["X-Requested-With"] = "XMLHttpRequest";
// Setup api versions
-var apiVersionConfig = (cov_2337h25edu().s[27]++, [{
+var apiVersionConfig = (cov_1alrk227ck().s[27]++, [{
version: "1.0",
baseURL: "/api/1.0/"
}, {
version: "1.1",
baseURL: "/api/1.1/"
}]);
-cov_2337h25edu().s[28]++;
+cov_1alrk227ck().s[28]++;
window.ProcessMaker.apiClient.defaults.baseURL = apiVersionConfig[0].baseURL;
-cov_2337h25edu().s[29]++;
+cov_1alrk227ck().s[29]++;
window.ProcessMaker.apiClient.interceptors.request.use(function (config) {
- cov_2337h25edu().f[3]++;
- cov_2337h25edu().s[30]++;
- if ((cov_2337h25edu().b[3][0]++, typeof config.url !== "string") || (cov_2337h25edu().b[3][1]++, !config.url)) {
- cov_2337h25edu().b[2][0]++;
- cov_2337h25edu().s[31]++;
+ cov_1alrk227ck().f[3]++;
+ cov_1alrk227ck().s[30]++;
+ if ((cov_1alrk227ck().b[3][0]++, typeof config.url !== "string") || (cov_1alrk227ck().b[3][1]++, !config.url)) {
+ cov_1alrk227ck().b[2][0]++;
+ cov_1alrk227ck().s[31]++;
throw new Error("Invalid URL in the request configuration");
} else {
- cov_2337h25edu().b[2][1]++;
+ cov_1alrk227ck().b[2][1]++;
}
- cov_2337h25edu().s[32]++;
+ cov_1alrk227ck().s[32]++;
apiVersionConfig.forEach(function (_ref) {
var version = _ref.version,
baseURL = _ref.baseURL;
- cov_2337h25edu().f[4]++;
- var versionPrefix = (cov_2337h25edu().s[33]++, "/api/".concat(version, "/"));
- cov_2337h25edu().s[34]++;
+ cov_1alrk227ck().f[4]++;
+ var versionPrefix = (cov_1alrk227ck().s[33]++, "/api/".concat(version, "/"));
+ cov_1alrk227ck().s[34]++;
if (config.url.startsWith(versionPrefix)) {
- cov_2337h25edu().b[4][0]++;
- cov_2337h25edu().s[35]++;
+ cov_1alrk227ck().b[4][0]++;
+ cov_1alrk227ck().s[35]++;
// eslint-disable-next-line no-param-reassign
config.baseURL = baseURL;
// eslint-disable-next-line no-param-reassign
- cov_2337h25edu().s[36]++;
+ cov_1alrk227ck().s[36]++;
config.url = config.url.replace(versionPrefix, "");
} else {
- cov_2337h25edu().b[4][1]++;
+ cov_1alrk227ck().b[4][1]++;
}
});
- cov_2337h25edu().s[37]++;
+ cov_1alrk227ck().s[37]++;
return config;
});
// Set the default API timeout
-var apiTimeout = (cov_2337h25edu().s[38]++, 5000);
-cov_2337h25edu().s[39]++;
-if ((cov_2337h25edu().b[6][0]++, window.Processmaker) && (cov_2337h25edu().b[6][1]++, window.Processmaker.apiTimeout !== undefined)) {
- cov_2337h25edu().b[5][0]++;
- cov_2337h25edu().s[40]++;
+var apiTimeout = (cov_1alrk227ck().s[38]++, 5000);
+cov_1alrk227ck().s[39]++;
+if ((cov_1alrk227ck().b[6][0]++, window.Processmaker) && (cov_1alrk227ck().b[6][1]++, window.Processmaker.apiTimeout !== undefined)) {
+ cov_1alrk227ck().b[5][0]++;
+ cov_1alrk227ck().s[40]++;
apiTimeout = window.Processmaker.apiTimeout;
} else {
- cov_2337h25edu().b[5][1]++;
+ cov_1alrk227ck().b[5][1]++;
}
-cov_2337h25edu().s[41]++;
+cov_1alrk227ck().s[41]++;
window.ProcessMaker.apiClient.defaults.timeout = apiTimeout;
// click an active tab after all components have mounted
-cov_2337h25edu().s[42]++;
+cov_1alrk227ck().s[42]++;
vue__WEBPACK_IMPORTED_MODULE_9__["default"].use({
install: function install(vue) {
- cov_2337h25edu().f[5]++;
- cov_2337h25edu().s[43]++;
+ cov_1alrk227ck().f[5]++;
+ cov_1alrk227ck().s[43]++;
vue.mixin({
mounted: function mounted() {
- cov_2337h25edu().f[6]++;
- cov_2337h25edu().s[44]++;
+ cov_1alrk227ck().f[6]++;
+ cov_1alrk227ck().s[44]++;
if (this.$parent) {
- cov_2337h25edu().b[7][0]++;
- cov_2337h25edu().s[45]++;
+ cov_1alrk227ck().b[7][0]++;
+ cov_1alrk227ck().s[45]++;
// only run on root
return;
} else {
- cov_2337h25edu().b[7][1]++;
+ cov_1alrk227ck().b[7][1]++;
}
}
});
@@ -4150,7 +4150,7 @@ vue__WEBPACK_IMPORTED_MODULE_9__["default"].use({
});
// Send an event when the global Vue and ProcessMaker instance is available
-cov_2337h25edu().s[46]++;
+cov_1alrk227ck().s[46]++;
window.dispatchEvent(new Event("app-bootstrapped"));
/***/ }),
@@ -4168,13 +4168,13 @@ __webpack_require__.r(__webpack_exports__);
/* harmony export */ });
/* harmony import */ var _babel_runtime_helpers_slicedToArray__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! @babel/runtime/helpers/slicedToArray */ "./node_modules/@babel/runtime/helpers/esm/slicedToArray.js");
-function cov_20gejcf4ab() {
- var path = "/Users/sanjacornelius/Documents/processmaker/resources/js/components/common/mixins/accessibility.js";
- var hash = "32ff012b53571eb61942410b27f8a8c856256151";
+function cov_njo9r729r() {
+ var path = "/Users/agustin/Sites/processmaker/resources/js/components/common/mixins/accessibility.js";
+ var hash = "d071c26d273404aefb2d9070a6480c9d61eb4cbb";
var global = new Function("return this")();
var gcv = "__coverage__";
var coverageData = {
- path: "/Users/sanjacornelius/Documents/processmaker/resources/js/components/common/mixins/accessibility.js",
+ path: "/Users/agustin/Sites/processmaker/resources/js/components/common/mixins/accessibility.js",
statementMap: {
"0": {
start: {
@@ -5413,7 +5413,7 @@ function cov_20gejcf4ab() {
"14": [0, 0]
},
_coverageSchema: "1a1c01bbd47fc00a2c39e90264f33305004495a9",
- hash: "32ff012b53571eb61942410b27f8a8c856256151"
+ hash: "d071c26d273404aefb2d9070a6480c9d61eb4cbb"
};
var coverage = global[gcv] || (global[gcv] = {});
if (!coverage[path] || coverage[path].hash !== hash) {
@@ -5422,48 +5422,48 @@ function cov_20gejcf4ab() {
var actualCoverage = coverage[path];
{
// @ts-ignore
- cov_20gejcf4ab = function () {
+ cov_njo9r729r = function () {
return actualCoverage;
};
}
return actualCoverage;
}
-cov_20gejcf4ab();
+cov_njo9r729r();
/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = ({
data: function data() {
- cov_20gejcf4ab().f[0]++;
- cov_20gejcf4ab().s[0]++;
+ cov_njo9r729r().f[0]++;
+ cov_njo9r729r().s[0]++;
return {
focusErrors: null
};
},
mounted: function mounted() {
- cov_20gejcf4ab().f[1]++;
- cov_20gejcf4ab().s[1]++;
+ cov_njo9r729r().f[1]++;
+ cov_njo9r729r().s[1]++;
// Listen only on the root Vue instance
if (!this.$parent) {
- cov_20gejcf4ab().b[0][0]++;
- cov_20gejcf4ab().s[2]++;
+ cov_njo9r729r().b[0][0]++;
+ cov_njo9r729r().s[2]++;
// Set the focus within any modal or popover that is instantiated
this.$root.$on("bv::modal::shown", this.setFocusWithin);
- cov_20gejcf4ab().s[3]++;
+ cov_njo9r729r().s[3]++;
this.$root.$on("bv::popover::shown", this.setFocusWithin);
} else {
- cov_20gejcf4ab().b[0][1]++;
+ cov_njo9r729r().b[0][1]++;
}
- cov_20gejcf4ab().s[4]++;
+ cov_njo9r729r().s[4]++;
if (this.focusErrors) {
- cov_20gejcf4ab().b[1][0]++;
- cov_20gejcf4ab().s[5]++;
+ cov_njo9r729r().b[1][0]++;
+ cov_njo9r729r().s[5]++;
// watch an object for form errors
this.$watch(this.focusErrors, this.focusErrorsChanged, {
deep: true
});
- cov_20gejcf4ab().s[6]++;
+ cov_njo9r729r().s[6]++;
this.dontListenForApiClientError();
} else {
- cov_20gejcf4ab().b[1][1]++;
- cov_20gejcf4ab().s[7]++;
+ cov_njo9r729r().b[1][1]++;
+ cov_njo9r729r().s[7]++;
// default api error focusing
this.listenForApiClientError();
}
@@ -5476,144 +5476,144 @@ cov_20gejcf4ab();
* @param modalId
*/
setFocusWithin: function setFocusWithin(shownEvent, modalId) {
- cov_20gejcf4ab().f[2]++;
- cov_20gejcf4ab().s[8]++;
- if ((cov_20gejcf4ab().b[3][0]++, !(shownEvent.target instanceof HTMLElement)) && (cov_20gejcf4ab().b[3][1]++, !(shownEvent.relatedTarget instanceof HTMLElement))) {
- cov_20gejcf4ab().b[2][0]++;
- cov_20gejcf4ab().s[9]++;
+ cov_njo9r729r().f[2]++;
+ cov_njo9r729r().s[8]++;
+ if ((cov_njo9r729r().b[3][0]++, !(shownEvent.target instanceof HTMLElement)) && (cov_njo9r729r().b[3][1]++, !(shownEvent.relatedTarget instanceof HTMLElement))) {
+ cov_njo9r729r().b[2][0]++;
+ cov_njo9r729r().s[9]++;
return;
} else {
- cov_20gejcf4ab().b[2][1]++;
+ cov_njo9r729r().b[2][1]++;
}
// If relatedTarget is present and an HTMLElement,
// then we know we're working with a popover,
// otherwise it's a modal
- var target = (cov_20gejcf4ab().s[10]++, shownEvent.relatedTarget instanceof HTMLElement ? (cov_20gejcf4ab().b[4][0]++, shownEvent.relatedTarget) : (cov_20gejcf4ab().b[4][1]++, shownEvent.target));
+ var target = (cov_njo9r729r().s[10]++, shownEvent.relatedTarget instanceof HTMLElement ? (cov_njo9r729r().b[4][0]++, shownEvent.relatedTarget) : (cov_njo9r729r().b[4][1]++, shownEvent.target));
// Find the first focusable element that isn't the
// modal/popover close button
- var focusableElement = (cov_20gejcf4ab().s[11]++, target.querySelector("input, select, textarea, button:not(.close), a"));
+ var focusableElement = (cov_njo9r729r().s[11]++, target.querySelector("input, select, textarea, button:not(.close), a"));
// If there is an element to focus on, then do so
- cov_20gejcf4ab().s[12]++;
+ cov_njo9r729r().s[12]++;
if (focusableElement instanceof HTMLElement) {
- cov_20gejcf4ab().b[5][0]++;
+ cov_njo9r729r().b[5][0]++;
//do not set focus() if it is a vue-multiselect
- var parentVue = (cov_20gejcf4ab().s[13]++, this.findHtmlElementParentVueComponent(focusableElement));
- cov_20gejcf4ab().s[14]++;
- if ((cov_20gejcf4ab().b[7][0]++, parentVue !== null) && (cov_20gejcf4ab().b[7][1]++, parentVue.$options.name == 'vue-multiselect')) {
- cov_20gejcf4ab().b[6][0]++;
- cov_20gejcf4ab().s[15]++;
+ var parentVue = (cov_njo9r729r().s[13]++, this.findHtmlElementParentVueComponent(focusableElement));
+ cov_njo9r729r().s[14]++;
+ if ((cov_njo9r729r().b[7][0]++, parentVue !== null) && (cov_njo9r729r().b[7][1]++, parentVue.$options.name == 'vue-multiselect')) {
+ cov_njo9r729r().b[6][0]++;
+ cov_njo9r729r().s[15]++;
return;
} else {
- cov_20gejcf4ab().b[6][1]++;
+ cov_njo9r729r().b[6][1]++;
}
- cov_20gejcf4ab().s[16]++;
+ cov_njo9r729r().s[16]++;
focusableElement.focus();
} else {
- cov_20gejcf4ab().b[5][1]++;
+ cov_njo9r729r().b[5][1]++;
}
},
findHtmlElementParentVueComponent: function findHtmlElementParentVueComponent(element) {
- cov_20gejcf4ab().f[3]++;
- cov_20gejcf4ab().s[17]++;
+ cov_njo9r729r().f[3]++;
+ cov_njo9r729r().s[17]++;
if (element === undefined) {
- cov_20gejcf4ab().b[8][0]++;
- cov_20gejcf4ab().s[18]++;
+ cov_njo9r729r().b[8][0]++;
+ cov_njo9r729r().s[18]++;
return null;
} else {
- cov_20gejcf4ab().b[8][1]++;
+ cov_njo9r729r().b[8][1]++;
}
- cov_20gejcf4ab().s[19]++;
+ cov_njo9r729r().s[19]++;
if ('__vue__' in element) {
- cov_20gejcf4ab().b[9][0]++;
- cov_20gejcf4ab().s[20]++;
+ cov_njo9r729r().b[9][0]++;
+ cov_njo9r729r().s[20]++;
return element.__vue__;
} else {
- cov_20gejcf4ab().b[9][1]++;
- cov_20gejcf4ab().s[21]++;
+ cov_njo9r729r().b[9][1]++;
+ cov_njo9r729r().s[21]++;
return this.findHtmlElementParentVueComponent(element.parentNode);
}
},
hasCustomFocusErrors: function hasCustomFocusErrors() {
- cov_20gejcf4ab().f[4]++;
- cov_20gejcf4ab().s[22]++;
+ cov_njo9r729r().f[4]++;
+ cov_njo9r729r().s[22]++;
if (this.$root._hasCustomFocusErrors) {
- cov_20gejcf4ab().b[10][0]++;
- cov_20gejcf4ab().s[23]++;
+ cov_njo9r729r().b[10][0]++;
+ cov_njo9r729r().s[23]++;
this.$off;
} else {
- cov_20gejcf4ab().b[10][1]++;
+ cov_njo9r729r().b[10][1]++;
}
},
listenForApiClientError: function listenForApiClientError() {
- cov_20gejcf4ab().f[5]++;
- cov_20gejcf4ab().s[24]++;
+ cov_njo9r729r().f[5]++;
+ cov_njo9r729r().s[24]++;
if (typeof window.ProcessMaker._focusErrorsIntitalized === "undefined") {
- cov_20gejcf4ab().b[11][0]++;
- cov_20gejcf4ab().s[25]++;
+ cov_njo9r729r().b[11][0]++;
+ cov_njo9r729r().s[25]++;
window.ProcessMaker.EventBus.$on("api-client-error", this.onApiClientError);
- cov_20gejcf4ab().s[26]++;
+ cov_njo9r729r().s[26]++;
window.ProcessMaker._focusErrorsIntitalized = true;
} else {
- cov_20gejcf4ab().b[11][1]++;
+ cov_njo9r729r().b[11][1]++;
}
},
dontListenForApiClientError: function dontListenForApiClientError() {
- cov_20gejcf4ab().f[6]++;
- cov_20gejcf4ab().s[27]++;
+ cov_njo9r729r().f[6]++;
+ cov_njo9r729r().s[27]++;
window.ProcessMaker.EventBus.$off("api-client-error", this.onApiClientError);
- cov_20gejcf4ab().s[28]++;
+ cov_njo9r729r().s[28]++;
window.ProcessMaker._focusErrorsIntitalized = true;
},
onApiClientError: function onApiClientError(error) {
- cov_20gejcf4ab().f[7]++;
- var errors = (cov_20gejcf4ab().s[29]++, _.get(error, "response.data.errors", false));
- cov_20gejcf4ab().s[30]++;
+ cov_njo9r729r().f[7]++;
+ var errors = (cov_njo9r729r().s[29]++, _.get(error, "response.data.errors", false));
+ cov_njo9r729r().s[30]++;
if (errors) {
- cov_20gejcf4ab().b[12][0]++;
- cov_20gejcf4ab().s[31]++;
+ cov_njo9r729r().b[12][0]++;
+ cov_njo9r729r().s[31]++;
this.focusErrorsChanged(errors);
} else {
- cov_20gejcf4ab().b[12][1]++;
+ cov_njo9r729r().b[12][1]++;
}
},
focusErrorsChanged: function focusErrorsChanged(newValue) {
- cov_20gejcf4ab().f[8]++;
- var selector = (cov_20gejcf4ab().s[32]++, Object.entries(newValue).filter(function (_ref) {
+ cov_njo9r729r().f[8]++;
+ var selector = (cov_njo9r729r().s[32]++, Object.entries(newValue).filter(function (_ref) {
var _ref2 = (0,_babel_runtime_helpers_slicedToArray__WEBPACK_IMPORTED_MODULE_0__["default"])(_ref, 2),
_ = _ref2[0],
value = _ref2[1];
- cov_20gejcf4ab().f[9]++;
- cov_20gejcf4ab().s[33]++;
+ cov_njo9r729r().f[9]++;
+ cov_njo9r729r().s[33]++;
return value !== null;
}) // Filter out null values
.map(function (_ref3) {
var _ref4 = (0,_babel_runtime_helpers_slicedToArray__WEBPACK_IMPORTED_MODULE_0__["default"])(_ref3, 2),
field = _ref4[0],
_ = _ref4[1];
- cov_20gejcf4ab().f[10]++;
- cov_20gejcf4ab().s[34]++;
+ cov_njo9r729r().f[10]++;
+ cov_njo9r729r().s[34]++;
return "[name='".concat(field, "']");
}) // Select elements matching the name attribute
.join(", "));
- cov_20gejcf4ab().s[35]++;
+ cov_njo9r729r().s[35]++;
if (!selector) {
- cov_20gejcf4ab().b[13][0]++;
- cov_20gejcf4ab().s[36]++;
+ cov_njo9r729r().b[13][0]++;
+ cov_njo9r729r().s[36]++;
return;
} else {
- cov_20gejcf4ab().b[13][1]++;
+ cov_njo9r729r().b[13][1]++;
}
- var firstInput = (cov_20gejcf4ab().s[37]++, document.querySelector(selector)); // Find the first match
- cov_20gejcf4ab().s[38]++;
+ var firstInput = (cov_njo9r729r().s[37]++, document.querySelector(selector)); // Find the first match
+ cov_njo9r729r().s[38]++;
if (firstInput) {
- cov_20gejcf4ab().b[14][0]++;
- cov_20gejcf4ab().s[39]++;
+ cov_njo9r729r().b[14][0]++;
+ cov_njo9r729r().s[39]++;
firstInput.focus();
} else {
- cov_20gejcf4ab().b[14][1]++;
+ cov_njo9r729r().b[14][1]++;
}
}
}
@@ -5632,13 +5632,13 @@ __webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ "default": () => (/* binding */ translate)
/* harmony export */ });
-function cov_kcsn6du19() {
- var path = "/Users/sanjacornelius/Documents/processmaker/resources/js/modules/lang.js";
- var hash = "2cc2f52cbb98903a23b86bcdba9de5ccc6934daa";
+function cov_28vu7yhuo8() {
+ var path = "/Users/agustin/Sites/processmaker/resources/js/modules/lang.js";
+ var hash = "b5d117a2f761be676d1ae2d9b49aa04060168b6e";
var global = new Function("return this")();
var gcv = "__coverage__";
var coverageData = {
- path: "/Users/sanjacornelius/Documents/processmaker/resources/js/modules/lang.js",
+ path: "/Users/agustin/Sites/processmaker/resources/js/modules/lang.js",
statementMap: {
"0": {
start: {
@@ -5686,7 +5686,7 @@ function cov_kcsn6du19() {
},
b: {},
_coverageSchema: "1a1c01bbd47fc00a2c39e90264f33305004495a9",
- hash: "2cc2f52cbb98903a23b86bcdba9de5ccc6934daa"
+ hash: "b5d117a2f761be676d1ae2d9b49aa04060168b6e"
};
var coverage = global[gcv] || (global[gcv] = {});
if (!coverage[path] || coverage[path].hash !== hash) {
@@ -5695,16 +5695,16 @@ function cov_kcsn6du19() {
var actualCoverage = coverage[path];
{
// @ts-ignore
- cov_kcsn6du19 = function () {
+ cov_28vu7yhuo8 = function () {
return actualCoverage;
};
}
return actualCoverage;
}
-cov_kcsn6du19();
+cov_28vu7yhuo8();
function translate(value) {
- cov_kcsn6du19().f[0]++;
- cov_kcsn6du19().s[0]++;
+ cov_28vu7yhuo8().f[0]++;
+ cov_28vu7yhuo8().s[0]++;
return value;
}
diff --git a/resources/js/processes/screen-builder/screen.vue b/resources/js/processes/screen-builder/screen.vue
index a9912c5f7c..697768dbd6 100644
--- a/resources/js/processes/screen-builder/screen.vue
+++ b/resources/js/processes/screen-builder/screen.vue
@@ -688,9 +688,10 @@ export default {
custom_css: this.customCSS,
watchers: this.watchers,
})
- .then(() => {
+ .then((response) => {
// Set draft status.
this.setVersionIndicator(true);
+ this.applyInlineImageSaveResponse(response);
ProcessMaker.EventBus.$emit("save-changes");
})
.catch((error) => {
@@ -1067,6 +1068,33 @@ export default {
// Recount number of elements
this.countElements();
},
+ applyInlineImageSaveResponse(response) {
+ const payload = response?.data;
+ if (!payload || typeof payload !== "object") {
+ return;
+ }
+ if (Array.isArray(payload.config)) {
+ this.syncNormalizedScreenConfig(payload.config);
+ }
+ if (payload.replaced_images > 0 || payload.converted_images > 0) {
+ ProcessMaker.alert(
+ this.$t(
+ "Inline images were stored as media files to keep this screen lightweight.",
+ ),
+ "success",
+ );
+ }
+ },
+ syncNormalizedScreenConfig(normalizedConfig) {
+ const builder = this.$refs.builder;
+ if (builder && Array.isArray(builder.config)) {
+ builder.config.splice(0, builder.config.length, ...normalizedConfig);
+ this.config = builder.config;
+ } else {
+ this.config = normalizedConfig;
+ }
+ this.screen.config = this.config;
+ },
previewSubmit() {
// eslint-disable-next-line no-alert
alert("Preview Form was Submitted");
@@ -1172,6 +1200,7 @@ export default {
if (exportScreen) {
this.exportScreen();
}
+ this.applyInlineImageSaveResponse(response);
ProcessMaker.alert(this.$t("Successfully saved"), "success");
// Set published status.
this.setVersionIndicator(false);
diff --git a/tests/Feature/Api/ScreenInlineImagesTest.php b/tests/Feature/Api/ScreenInlineImagesTest.php
new file mode 100644
index 0000000000..97f47ae980
--- /dev/null
+++ b/tests/Feature/Api/ScreenInlineImagesTest.php
@@ -0,0 +1,71 @@
+create(['type' => 'FORM']);
+ $dataUri = 'data:image/png;base64,' . self::TINY_PNG;
+ $config = [[
+ 'name' => 'Default',
+ 'items' => [[
+ 'component' => 'FormHtmlViewer',
+ 'config' => [
+ 'content' => '',
+ ],
+ ]],
+ ]];
+
+ $response = $this->apiCall('PUT', "/screens/{$screen->id}/draft", [
+ 'title' => $screen->title,
+ 'description' => $screen->description,
+ 'type' => $screen->type,
+ 'screen_category_id' => $screen->screen_category_id,
+ 'config' => $config,
+ ]);
+
+ $response->assertStatus(200);
+ $response->assertJsonPath('converted_images', 1);
+ $this->assertStringNotContainsString(
+ 'base64,',
+ $response->json('config.0.items.0.config.content')
+ );
+
+ $draft = $screen->getDraftVersion();
+ $this->assertNotNull($draft);
+ $this->assertStringNotContainsString('base64,', $draft->config[0]['items'][0]['config']['content']);
+ $this->assertSame(1, $screen->fresh()->getMedia(ScreenInlineImageNormalizer::COLLECTION)->count());
+ }
+
+ public function testDraftWithoutInlineImagesKeepsNoContentResponse()
+ {
+ $screen = Screen::factory()->create(['type' => 'FORM']);
+
+ $response = $this->apiCall('PUT', "/screens/{$screen->id}/draft", [
+ 'title' => $screen->title,
+ 'description' => $screen->description,
+ 'type' => $screen->type,
+ 'screen_category_id' => $screen->screen_category_id,
+ 'config' => [['name' => 'Default', 'items' => []]],
+ ]);
+
+ $response->assertStatus(204);
+ }
+}
diff --git a/tests/unit/Screens/ScreenInlineImageNormalizerTest.php b/tests/unit/Screens/ScreenInlineImageNormalizerTest.php
new file mode 100644
index 0000000000..fd201e4e9a
--- /dev/null
+++ b/tests/unit/Screens/ScreenInlineImageNormalizerTest.php
@@ -0,0 +1,133 @@
+ 'Default',
+ 'items' => [[
+ 'component' => 'FormHtmlViewer',
+ 'config' => [
+ 'content' => '
',
+ ],
+ ]],
+ ]];
+
+ $result = app(ScreenInlineImageNormalizer::class)->configContainsInlineImages($config);
+
+ $this->assertTrue($result);
+ }
+
+ public function testConfigContainsInlineImagesIgnoresFormImage(): void
+ {
+ $dataUri = 'data:image/png;base64,' . self::TINY_PNG;
+ $config = [[
+ 'name' => 'Default',
+ 'items' => [[
+ 'component' => 'FormImage',
+ 'config' => [
+ 'image' => $dataUri,
+ 'name' => 'logo',
+ ],
+ ]],
+ ]];
+
+ $result = app(ScreenInlineImageNormalizer::class)->configContainsInlineImages($config);
+
+ $this->assertFalse($result);
+ }
+
+ public function testReplacesBase64InRichTextContent()
+ {
+ $screen = Screen::factory()->create();
+ $dataUri = 'data:image/png;base64,' . self::TINY_PNG;
+ $config = [[
+ 'name' => 'Default',
+ 'items' => [[
+ 'component' => 'FormHtmlViewer',
+ 'config' => [
+ 'content' => '
Hello