Skip to content

Commit 35204ab

Browse files
authored
Merge pull request #8903 from ProcessMaker/epic/FOUR-32196
FOUR-32196: Support custom dashboard and menus imports through DevLink
2 parents ae4a6d3 + f3c3af7 commit 35204ab

21 files changed

Lines changed: 1647 additions & 315 deletions

ProcessMaker/Http/Controllers/Api/DevLinkController.php

Lines changed: 117 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,16 @@
33
namespace ProcessMaker\Http\Controllers\Api;
44

55
use Illuminate\Database\Eloquent\ModelNotFoundException;
6+
use Illuminate\Http\Client\ConnectionException;
7+
use Illuminate\Http\Client\RequestException;
68
use Illuminate\Http\Request;
7-
use Illuminate\Support\Facades\File;
89
use Illuminate\Support\Facades\Notification;
910
use Illuminate\Validation\Rule;
1011
use ProcessMaker\Events\CustomizeUiUpdated;
1112
use ProcessMaker\Exception\ValidationException;
1213
use ProcessMaker\Http\Controllers\Controller;
14+
use ProcessMaker\Http\Requests\DevLinkStoreRequest;
1315
use ProcessMaker\Http\Resources\ApiCollection;
14-
use ProcessMaker\Jobs\CompileSass;
1516
use ProcessMaker\Jobs\CompileUI;
1617
use ProcessMaker\Jobs\DevLinkInstall;
1718
use ProcessMaker\Models\Bundle;
@@ -23,6 +24,8 @@
2324
use ProcessMaker\Models\SettingsMenus;
2425
use ProcessMaker\Models\User;
2526
use ProcessMaker\Notifications\BundleUpdatedNotification;
27+
use ProcessMaker\Package\PackageDynamicUI\Models\Dashboard;
28+
use ProcessMaker\Package\PackageDynamicUI\Models\Menu;
2629

2730
class DevLinkController extends Controller
2831
{
@@ -56,27 +59,38 @@ public function show(DevLink $devLink)
5659
return $devLink;
5760
}
5861

59-
public function store(Request $request)
62+
public function store(DevLinkStoreRequest $request)
6063
{
61-
$request->validate([
62-
'name' => ['required'],
63-
'url' => ['required', 'url'],
64-
]);
65-
$devLink = DevLink::where('name', $request->input('name'))->first();
66-
if ($devLink) {
67-
$devLink->url = $request->input('url');
68-
} else {
69-
$devLink = new DevLink();
70-
$devLink->name = $request->input('name');
71-
$devLink->url = $request->input('url');
64+
$normalizedUrl = $this->normalizeUrl($request->input('url'));
65+
$existingDevLink = DevLink::query()
66+
->get(['id', 'name', 'url'])
67+
->first(fn (DevLink $devLink) => $this->normalizeUrl($devLink->url) === $normalizedUrl);
68+
if ($existingDevLink) {
69+
throw ValidationException::withMessages([
70+
'url' => __(
71+
'This instance is already linked as :name. Open or reconnect the existing connection.',
72+
['name' => $existingDevLink->name]
73+
),
74+
]);
7275
}
76+
77+
$devLink = new DevLink();
78+
$devLink->name = $request->input('name');
79+
$devLink->url = $normalizedUrl;
7380
$devLink->saveOrFail();
7481

7582
return $devLink;
7683
}
7784

7885
public function update(Request $request, DevLink $devLink)
7986
{
87+
$request->merge([
88+
'name' => trim((string) $request->input('name')),
89+
]);
90+
$request->validate([
91+
'name' => ['required', 'string', Rule::unique('dev_links', 'name')->ignore($devLink->id)],
92+
]);
93+
8094
$devLink->name = $request->input('name');
8195
$devLink->saveOrFail();
8296

@@ -180,7 +194,7 @@ public function increaseBundleVersion(Bundle $bundle)
180194
return $bundle;
181195
}
182196

183-
public function bundleUpdated($bundleId, $token)
197+
public function bundleUpdated(int $bundleId, string $token)
184198
{
185199
try {
186200
$bundle = Bundle::where('remote_id', $bundleId)->firstOrFail();
@@ -208,7 +222,7 @@ public function deleteBundle(Bundle $bundle)
208222
$bundle->delete();
209223
}
210224

211-
public function installRemoteBundle(Request $request, DevLink $devLink, $remoteBundleId)
225+
public function installRemoteBundle(Request $request, DevLink $devLink, int $remoteBundleId)
212226
{
213227
$updateType = $request->input('updateType', DevLinkInstall::MODE_UPDATE);
214228
DevLinkInstall::dispatch(
@@ -342,7 +356,7 @@ public function addSharedAsset(Request $request)
342356
$sharedAsset->saveOrFail();
343357
}
344358

345-
public function removeSharedAsset($id)
359+
public function removeSharedAsset(int $id)
346360
{
347361
$deleted = Setting::destroy($id);
348362

@@ -371,9 +385,41 @@ public function installRemoteAsset(Request $request, DevLink $devLink)
371385
];
372386
}
373387

374-
public function remoteBundleVersion(DevLink $devLink, $remoteBundleId)
388+
public function remoteBundleVersion(DevLink $devLink, int $remoteBundleId)
389+
{
390+
try {
391+
$payload = $devLink->remoteBundle($remoteBundleId)->json();
392+
} catch (RequestException|ConnectionException $e) {
393+
return [
394+
'available' => false,
395+
'version' => null,
396+
];
397+
}
398+
399+
if (!is_array($payload)) {
400+
return [
401+
'available' => false,
402+
'version' => null,
403+
];
404+
}
405+
406+
$payload['available'] = true;
407+
408+
return $payload;
409+
}
410+
411+
private function normalizeUrl(string $url): string
375412
{
376-
return $devLink->remoteBundle($remoteBundleId);
413+
$parts = parse_url(trim($url));
414+
$scheme = strtolower($parts['scheme']);
415+
$host = strtolower($parts['host']);
416+
$port = $parts['port'] ?? null;
417+
418+
if (($scheme === 'http' && $port === 80) || ($scheme === 'https' && $port === 443)) {
419+
$port = null;
420+
}
421+
422+
return $scheme . '://' . $host . ($port === null ? '' : ':' . $port);
377423
}
378424

379425
public function deleteBundleAsset(BundleAsset $bundleAsset)
@@ -390,80 +436,70 @@ public function deleteBundleSetting(BundleSetting $bundleSetting)
390436
return response()->json(['message' => 'Bundle setting deleted.'], 200);
391437
}
392438

393-
public function getBundleSetting(Bundle $bundle, $settingKey)
439+
public function getBundleSetting(Bundle $bundle, string $settingKey)
440+
{
441+
return $bundle->settings()->where('setting', $settingKey)->first();
442+
}
443+
444+
public function getBundleSettingPreview(Bundle $bundle, string $settingKey): array
394445
{
395-
$setting = $bundle->settings()->where('setting', $settingKey)->first();
446+
abort_unless(in_array($settingKey, ['ui_dashboards', 'ui_menus'], true), 404);
396447

397-
return $setting;
448+
return $bundle->settingPreview($settingKey);
398449
}
399450

400-
public function getBundleAllSettings($settingKey)
451+
public function getBundleAllSettings(string $settingKey)
401452
{
402-
if ($settingKey === 'ui_settings') {
403-
return Setting::whereIn('key', ['css-override', 'login-footer', 'logo-alt-text'])
404-
->get();
453+
return match ($settingKey) {
454+
'ui_dashboards' => $this->getAvailableDashboards(),
455+
'ui_menus' => $this->getAvailableMenus(),
456+
'ui_settings' => Setting::whereIn('key', ['css-override', 'login-footer', 'logo-alt-text'])->get(),
457+
default => Setting::where([
458+
['group_id', SettingsMenus::getId($settingKey)],
459+
['hidden', 0],
460+
])->get(),
461+
};
462+
}
463+
464+
private function getAvailableDashboards()
465+
{
466+
if (!class_exists(Dashboard::class)) {
467+
return [];
405468
}
406469

407-
return Setting::where([
408-
['group_id', SettingsMenus::getId($settingKey)],
409-
['hidden', 0],
410-
])->get();
470+
return Dashboard::query()
471+
->orderBy('title')
472+
->get(['id', 'title'])
473+
->map(function (Dashboard $dashboard) {
474+
return [
475+
'key' => $dashboard->id,
476+
'name' => $dashboard->title,
477+
];
478+
})
479+
->values();
411480
}
412481

413-
public function refreshUi()
482+
private function getAvailableMenus()
414483
{
415-
CompileUI::dispatch(auth()->user()?->id);
416-
CustomizeUiUpdated::dispatch([], [], false);
484+
if (!class_exists(Menu::class)) {
485+
return [];
486+
}
487+
488+
return Menu::query()
489+
->orderBy('name')
490+
->get(['id', 'name'])
491+
->map(function (Menu $menu) {
492+
return [
493+
'key' => $menu->id,
494+
'name' => $menu->name,
495+
];
496+
})
497+
->values();
417498
}
418499

419-
private function writeColors($data)
500+
public function refreshUi(Request $request)
420501
{
421-
// Now generate the _colors.scss file
422-
$contents = "// Changed theme colors\n";
423-
foreach ($data as $value) {
424-
$contents .= $value->id . ': ' . $value->value . ";\n";
425-
}
426-
File::put(app()->resourcePath('sass') . '/_colors.scss', $contents);
427-
}
428-
429-
/**
430-
* Write variables font in file
431-
*
432-
* @param $sansSerif
433-
* @param $serif
434-
*/
435-
private function writeFonts($sansSerif)
436-
{
437-
$sansSerif = $sansSerif ? $sansSerif : $this->sansSerifFontDefault();
438-
// Generate the _fonts.scss file
439-
$contents = "// Changed theme fonts\n";
440-
$contents .= '$font-family-sans-serif: ' . $sansSerif['id'] . " !default;\n";
441-
File::put(app()->resourcePath('sass') . '/_fonts.scss', $contents);
442-
}
443-
444-
/**
445-
* run jobs compile
446-
*/
447-
private function compileSass($userId)
448-
{
449-
// Compile the Sass files
450-
$this->dispatch(new CompileSass([
451-
'tag' => 'sidebar',
452-
'origin' => 'resources/sass/sidebar/sidebar.scss',
453-
'target' => 'public/css/sidebar.css',
454-
'user' => $userId,
455-
]));
456-
$this->dispatch(new CompileSass([
457-
'tag' => 'app',
458-
'origin' => 'resources/sass/app.scss',
459-
'target' => 'public/css/app.css',
460-
'user' => $userId,
461-
]));
462-
$this->dispatch(new CompileSass([
463-
'tag' => 'queues',
464-
'origin' => 'resources/sass/admin/queues.scss',
465-
'target' => 'public/css/admin/queues.css',
466-
'user' => $userId,
467-
]));
502+
CompileUI::dispatch($request->user()?->id);
503+
CustomizeUiUpdated::dispatch([], [], false);
468504
}
469505
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
3+
namespace ProcessMaker\Http\Requests;
4+
5+
use Illuminate\Foundation\Http\FormRequest;
6+
use Illuminate\Validation\Rule;
7+
use ProcessMaker\Rules\HttpOrigin;
8+
9+
class DevLinkStoreRequest extends FormRequest
10+
{
11+
public function authorize(): bool
12+
{
13+
return true;
14+
}
15+
16+
protected function prepareForValidation(): void
17+
{
18+
$this->merge([
19+
'name' => trim((string) $this->input('name')),
20+
'url' => trim((string) $this->input('url')),
21+
]);
22+
}
23+
24+
public function rules(): array
25+
{
26+
return [
27+
'name' => ['required', 'string', Rule::unique('dev_links', 'name')],
28+
'url' => ['bail', 'required', 'url', new HttpOrigin()],
29+
];
30+
}
31+
}

ProcessMaker/ImportExport/Options.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,12 @@ class Options
3434

3535
public $options;
3636

37-
public function __construct(array $options)
37+
public ?int $importingUserId;
38+
39+
public function __construct(array $options, ?int $importingUserId = null)
3840
{
3941
$this->options = $options;
42+
$this->importingUserId = $importingUserId;
4043
}
4144

4245
public function get($name, $uuid)

0 commit comments

Comments
 (0)