diff --git a/ProcessMaker/Http/Controllers/Api/DevLinkController.php b/ProcessMaker/Http/Controllers/Api/DevLinkController.php index 8994184d1a..01a56364fe 100644 --- a/ProcessMaker/Http/Controllers/Api/DevLinkController.php +++ b/ProcessMaker/Http/Controllers/Api/DevLinkController.php @@ -4,14 +4,12 @@ use Illuminate\Database\Eloquent\ModelNotFoundException; use Illuminate\Http\Request; -use Illuminate\Support\Facades\File; use Illuminate\Support\Facades\Notification; use Illuminate\Validation\Rule; use ProcessMaker\Events\CustomizeUiUpdated; use ProcessMaker\Exception\ValidationException; use ProcessMaker\Http\Controllers\Controller; use ProcessMaker\Http\Resources\ApiCollection; -use ProcessMaker\Jobs\CompileSass; use ProcessMaker\Jobs\CompileUI; use ProcessMaker\Jobs\DevLinkInstall; use ProcessMaker\Models\Bundle; @@ -23,6 +21,8 @@ use ProcessMaker\Models\SettingsMenus; use ProcessMaker\Models\User; use ProcessMaker\Notifications\BundleUpdatedNotification; +use ProcessMaker\Package\PackageDynamicUI\Models\Dashboard; +use ProcessMaker\Package\PackageDynamicUI\Models\Menu; class DevLinkController extends Controller { @@ -180,7 +180,7 @@ public function increaseBundleVersion(Bundle $bundle) return $bundle; } - public function bundleUpdated($bundleId, $token) + public function bundleUpdated(int $bundleId, string $token) { try { $bundle = Bundle::where('remote_id', $bundleId)->firstOrFail(); @@ -208,7 +208,7 @@ public function deleteBundle(Bundle $bundle) $bundle->delete(); } - public function installRemoteBundle(Request $request, DevLink $devLink, $remoteBundleId) + public function installRemoteBundle(Request $request, DevLink $devLink, int $remoteBundleId) { $updateType = $request->input('updateType', DevLinkInstall::MODE_UPDATE); DevLinkInstall::dispatch( @@ -342,7 +342,7 @@ public function addSharedAsset(Request $request) $sharedAsset->saveOrFail(); } - public function removeSharedAsset($id) + public function removeSharedAsset(int $id) { $deleted = Setting::destroy($id); @@ -371,7 +371,7 @@ public function installRemoteAsset(Request $request, DevLink $devLink) ]; } - public function remoteBundleVersion(DevLink $devLink, $remoteBundleId) + public function remoteBundleVersion(DevLink $devLink, int $remoteBundleId) { return $devLink->remoteBundle($remoteBundleId); } @@ -390,80 +390,70 @@ public function deleteBundleSetting(BundleSetting $bundleSetting) return response()->json(['message' => 'Bundle setting deleted.'], 200); } - public function getBundleSetting(Bundle $bundle, $settingKey) + public function getBundleSetting(Bundle $bundle, string $settingKey) { - $setting = $bundle->settings()->where('setting', $settingKey)->first(); + return $bundle->settings()->where('setting', $settingKey)->first(); + } + + public function getBundleSettingPreview(Bundle $bundle, string $settingKey): array + { + abort_unless(in_array($settingKey, ['ui_dashboards', 'ui_menus'], true), 404); - return $setting; + return $bundle->settingPreview($settingKey); + } + + public function getBundleAllSettings(string $settingKey) + { + return match ($settingKey) { + 'ui_dashboards' => $this->getAvailableDashboards(), + 'ui_menus' => $this->getAvailableMenus(), + 'ui_settings' => Setting::whereIn('key', ['css-override', 'login-footer', 'logo-alt-text'])->get(), + default => Setting::where([ + ['group_id', SettingsMenus::getId($settingKey)], + ['hidden', 0], + ])->get(), + }; } - public function getBundleAllSettings($settingKey) + private function getAvailableDashboards() { - if ($settingKey === 'ui_settings') { - return Setting::whereIn('key', ['css-override', 'login-footer', 'logo-alt-text']) - ->get(); + if (!class_exists(Dashboard::class)) { + return []; } - return Setting::where([ - ['group_id', SettingsMenus::getId($settingKey)], - ['hidden', 0], - ])->get(); + return Dashboard::query() + ->orderBy('title') + ->get(['id', 'title']) + ->map(function (Dashboard $dashboard) { + return [ + 'key' => $dashboard->id, + 'name' => $dashboard->title, + ]; + }) + ->values(); } - public function refreshUi() + private function getAvailableMenus() { - CompileUI::dispatch(auth()->user()?->id); - CustomizeUiUpdated::dispatch([], [], false); + if (!class_exists(Menu::class)) { + return []; + } + + return Menu::query() + ->orderBy('name') + ->get(['id', 'name']) + ->map(function (Menu $menu) { + return [ + 'key' => $menu->id, + 'name' => $menu->name, + ]; + }) + ->values(); } - private function writeColors($data) + public function refreshUi(Request $request) { - // Now generate the _colors.scss file - $contents = "// Changed theme colors\n"; - foreach ($data as $value) { - $contents .= $value->id . ': ' . $value->value . ";\n"; - } - File::put(app()->resourcePath('sass') . '/_colors.scss', $contents); - } - - /** - * Write variables font in file - * - * @param $sansSerif - * @param $serif - */ - private function writeFonts($sansSerif) - { - $sansSerif = $sansSerif ? $sansSerif : $this->sansSerifFontDefault(); - // Generate the _fonts.scss file - $contents = "// Changed theme fonts\n"; - $contents .= '$font-family-sans-serif: ' . $sansSerif['id'] . " !default;\n"; - File::put(app()->resourcePath('sass') . '/_fonts.scss', $contents); - } - - /** - * run jobs compile - */ - private function compileSass($userId) - { - // Compile the Sass files - $this->dispatch(new CompileSass([ - 'tag' => 'sidebar', - 'origin' => 'resources/sass/sidebar/sidebar.scss', - 'target' => 'public/css/sidebar.css', - 'user' => $userId, - ])); - $this->dispatch(new CompileSass([ - 'tag' => 'app', - 'origin' => 'resources/sass/app.scss', - 'target' => 'public/css/app.css', - 'user' => $userId, - ])); - $this->dispatch(new CompileSass([ - 'tag' => 'queues', - 'origin' => 'resources/sass/admin/queues.scss', - 'target' => 'public/css/admin/queues.css', - 'user' => $userId, - ])); + CompileUI::dispatch($request->user()?->id); + CustomizeUiUpdated::dispatch([], [], false); } } diff --git a/ProcessMaker/ImportExport/Options.php b/ProcessMaker/ImportExport/Options.php index f70dcce89c..084489c4fc 100644 --- a/ProcessMaker/ImportExport/Options.php +++ b/ProcessMaker/ImportExport/Options.php @@ -34,9 +34,12 @@ class Options public $options; - public function __construct(array $options) + public ?int $importingUserId; + + public function __construct(array $options, ?int $importingUserId = null) { $this->options = $options; + $this->importingUserId = $importingUserId; } public function get($name, $uuid) diff --git a/ProcessMaker/Models/Bundle.php b/ProcessMaker/Models/Bundle.php index a62545e499..98f6328ab5 100644 --- a/ProcessMaker/Models/Bundle.php +++ b/ProcessMaker/Models/Bundle.php @@ -20,6 +20,13 @@ class Bundle extends ProcessMakerModel implements HasMedia use HasFactory; use InteractsWithMedia; + private const SETTING_PREVIEW_PAYLOAD_TYPES = [ + 'ui_dashboards' => 'dashboard_package', + 'ui_menus' => 'menu_package', + ]; + + private const SETTINGS_PAYLOADS_COMPLETE_PROPERTY = 'settings_payloads_complete'; + protected $guarded = ['id']; protected $appends = ['asset_count']; @@ -104,6 +111,92 @@ public function exportSettingPayloads() }); } + public function settingPreview(string $settingKey): array + { + if (!array_key_exists($settingKey, self::SETTING_PREVIEW_PAYLOAD_TYPES)) { + throw new \InvalidArgumentException('Unsupported bundle setting preview.'); + } + + $bundleSetting = $this->settings()->where('setting', $settingKey)->first(); + $selection = match (true) { + $bundleSetting === null => 'none', + $bundleSetting->config === null => 'all', + default => 'partial', + }; + + $preview = [ + 'setting' => $settingKey, + 'selection' => $selection, + 'available' => true, + 'items' => [], + ]; + + if ($selection === 'none') { + return $preview; + } + + $payloads = $this->readNewestPayloads(); + if ($payloads === null) { + $preview['available'] = false; + + return $preview; + } + + $payloadType = self::SETTING_PREVIEW_PAYLOAD_TYPES[$settingKey]; + $items = []; + foreach ($payloads as $payload) { + if (!is_array($payload) || ($payload['type'] ?? null) !== $payloadType) { + continue; + } + + $key = $payload['root'] ?? null; + $name = $payload['name'] ?? null; + if (!is_string($key) || !is_string($name)) { + continue; + } + + $items[$key] = [ + 'key' => $key, + 'name' => $name, + ]; + } + + $preview['items'] = array_values($items); + usort($preview['items'], function (array $left, array $right) { + return strcasecmp($left['name'], $right['name']) + ?: strcmp($left['key'], $right['key']); + }); + + return $preview; + } + + private function readNewestPayloads(): ?array + { + $media = $this->newestVersionFile(); + if ( + $media === null + || $media->getCustomProperty(self::SETTINGS_PAYLOADS_COMPLETE_PROPERTY) !== true + || !is_readable($media->getPath()) + ) { + return null; + } + + $compressedPayloads = file_get_contents($media->getPath()); + $payloads = null; + if ($compressedPayloads !== false && str_starts_with($compressedPayloads, "\x1f\x8b")) { + $decodedPayloads = gzdecode($compressedPayloads); + if ($decodedPayloads !== false) { + try { + $payloads = json_decode($decodedPayloads, true, flags: JSON_THROW_ON_ERROR); + } catch (\JsonException) { + $payloads = null; + } + } + } + + return is_array($payloads) ? $payloads : null; + } + public function syncAssets($assets) { $assetKeys = []; @@ -322,8 +415,9 @@ public function filesSortedByVersion() $media->getCustomProperty('version'), ]; })->sort(function ($a, $b) { - // newest versions first - return version_compare($a[1], $b[1]) * -1; + $versionComparison = version_compare($a[1], $b[1]) * -1; + + return $versionComparison ?: $b[0]->getKey() <=> $a[0]->getKey(); })->map(function ($item) { return $item[0]; }); @@ -341,18 +435,10 @@ public function savePayloadsToFile(array $payloads, array $payloadsSettings, $lo } $logger->status('Saving the bundle locally'); - // Filter and extract export payloads from nested array - $exportPayloads = array_filter($payloadsSettings, function ($payload) { - return !empty($payload[0]) && isset($payload[0]['export']); - }); - - // Extract payloads from nested array structure - $flattenedExportPayloads = array_map(function ($payload) { - return $payload[0]; // Tomar solo el primer elemento del array anidado - }, array_values($exportPayloads)); + $exportPayloads = $this->extractSettingExportPayloads($payloadsSettings); - if (!empty($flattenedExportPayloads)) { - $payloads = array_merge($payloads, $flattenedExportPayloads); + if (!empty($exportPayloads)) { + $payloads = array_merge($payloads, $exportPayloads); } $this->addMediaFromString( @@ -360,10 +446,13 @@ public function savePayloadsToFile(array $payloads, array $payloadsSettings, $lo json_encode($payloads) ), )->usingFileName('payloads.json.gz') - ->withCustomProperties(['version' => $this->version]) + ->withCustomProperties([ + 'version' => $this->version, + self::SETTINGS_PAYLOADS_COMPLETE_PROPERTY => true, + ]) ->toMediaCollection(); - // Keep only the 3 most recent versions + // Keep only the 3 most recent snapshots $count = 0; foreach ($this->filesSortedByVersion() as $media) { if ($count >= 3) { @@ -388,27 +477,50 @@ public function installSettings($settings) public function installSettingsPayloads(array $payloads, $mode, $logger = null) { + if ($logger === null) { + $logger = new Logger(); + } + $options = new Options([ 'mode' => $mode, - ]); + ], $logger->userId); - $assets = []; - foreach ($payloads as $payload) { - // Verify the type of payload without considering the order - if (!empty($payload) && is_array($payload[0])) { - // If it is a settings payload - if (isset($payload[0]['setting_type'])) { - $this->processSettingsPayload($payload); - } + foreach ($payloads as $payloadGroup) { + $settingPayloads = array_filter($payloadGroup, function ($payload) { + return is_array($payload) && isset($payload['setting_type']); + }); + + if (!empty($settingPayloads)) { + $this->processSettingsPayload($settingPayloads); + } + } + + $exportPayloads = $this->extractSettingExportPayloads($payloads); + if (empty($exportPayloads)) { + return; + } + + $logger->status('Installing bundle settings on the this instance'); + $logger->setSteps($exportPayloads); - // If it is an export payload (it can be the same or another element) - if (isset($payload[0]['export'])) { - $logger->status('Installing bundle settings on the this instance'); - $logger->setSteps($payload); - $assets[] = DevLink::import($payload[0], $options, $logger); + foreach ($exportPayloads as $payload) { + DevLink::import($payload, $options, $logger); + } + } + + private function extractSettingExportPayloads(array $payloadGroups): array + { + $exportPayloads = []; + + foreach ($payloadGroups as $payloadGroup) { + foreach ($payloadGroup as $payload) { + if (is_array($payload) && isset($payload['export'])) { + $exportPayloads[] = $payload; } } } + + return $exportPayloads; } // Auxiliary method to process the settings @@ -463,7 +575,7 @@ public function install(array $payloads, $mode, $logger = null, $reinstall = fal $options = new Options([ 'mode' => $mode, - ]); + ], $logger->userId); $assets = []; foreach ($payloads as $payload) { $assets[] = DevLink::import($payload, $options, $logger); diff --git a/resources/js/admin/devlink/components/BundleConfigurations.vue b/resources/js/admin/devlink/components/BundleConfigurations.vue index 4d347a1cf5..9bb9978249 100644 --- a/resources/js/admin/devlink/components/BundleConfigurations.vue +++ b/resources/js/admin/devlink/components/BundleConfigurations.vue @@ -4,14 +4,22 @@ {{ $t(title) }}
-
+
-
{{ $t(config.name) }}
-
{{ status(config) }}
+
+ {{ $t(config.name) }} +
+
+ {{ status(config) }} +