Skip to content

Commit 12ff12f

Browse files
committed
Add bundle setting preview functionality and corresponding tests
1 parent a58d644 commit 12ff12f

8 files changed

Lines changed: 306 additions & 2 deletions

File tree

ProcessMaker/Http/Controllers/Api/DevLinkController.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -395,6 +395,13 @@ public function getBundleSetting(Bundle $bundle, string $settingKey)
395395
return $bundle->settings()->where('setting', $settingKey)->first();
396396
}
397397

398+
public function getBundleSettingPreview(Bundle $bundle, string $settingKey): array
399+
{
400+
abort_unless(in_array($settingKey, ['ui_dashboards', 'ui_menus'], true), 404);
401+
402+
return $bundle->settingPreview($settingKey);
403+
}
404+
398405
public function getBundleAllSettings(string $settingKey)
399406
{
400407
return match ($settingKey) {

ProcessMaker/Models/Bundle.php

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@ class Bundle extends ProcessMakerModel implements HasMedia
2020
use HasFactory;
2121
use InteractsWithMedia;
2222

23+
private const SETTING_PREVIEW_PAYLOAD_TYPES = [
24+
'ui_dashboards' => 'dashboard_package',
25+
'ui_menus' => 'menu_package',
26+
];
27+
2328
protected $guarded = ['id'];
2429

2530
protected $appends = ['asset_count'];
@@ -104,6 +109,88 @@ public function exportSettingPayloads()
104109
});
105110
}
106111

112+
public function settingPreview(string $settingKey): array
113+
{
114+
if (!array_key_exists($settingKey, self::SETTING_PREVIEW_PAYLOAD_TYPES)) {
115+
throw new \InvalidArgumentException('Unsupported bundle setting preview.');
116+
}
117+
118+
$bundleSetting = $this->settings()->where('setting', $settingKey)->first();
119+
$selection = match (true) {
120+
$bundleSetting === null => 'none',
121+
$bundleSetting->config === null => 'all',
122+
default => 'partial',
123+
};
124+
125+
$preview = [
126+
'setting' => $settingKey,
127+
'selection' => $selection,
128+
'available' => true,
129+
'items' => [],
130+
];
131+
132+
if ($selection === 'none') {
133+
return $preview;
134+
}
135+
136+
$payloads = $this->readNewestPayloads();
137+
if ($payloads === null) {
138+
$preview['available'] = false;
139+
140+
return $preview;
141+
}
142+
143+
$payloadType = self::SETTING_PREVIEW_PAYLOAD_TYPES[$settingKey];
144+
$items = [];
145+
foreach ($payloads as $payload) {
146+
if (!is_array($payload) || ($payload['type'] ?? null) !== $payloadType) {
147+
continue;
148+
}
149+
150+
$key = $payload['root'] ?? null;
151+
$name = $payload['name'] ?? null;
152+
if (!is_string($key) || !is_string($name)) {
153+
continue;
154+
}
155+
156+
$items[$key] = [
157+
'key' => $key,
158+
'name' => $name,
159+
];
160+
}
161+
162+
$preview['items'] = array_values($items);
163+
usort($preview['items'], function (array $left, array $right) {
164+
return strcasecmp($left['name'], $right['name'])
165+
?: strcmp($left['key'], $right['key']);
166+
});
167+
168+
return $preview;
169+
}
170+
171+
private function readNewestPayloads(): ?array
172+
{
173+
$media = $this->newestVersionFile();
174+
if ($media === null || !is_readable($media->getPath())) {
175+
return null;
176+
}
177+
178+
$compressedPayloads = file_get_contents($media->getPath());
179+
$payloads = null;
180+
if ($compressedPayloads !== false && str_starts_with($compressedPayloads, "\x1f\x8b")) {
181+
$decodedPayloads = gzdecode($compressedPayloads);
182+
if ($decodedPayloads !== false) {
183+
try {
184+
$payloads = json_decode($decodedPayloads, true, flags: JSON_THROW_ON_ERROR);
185+
} catch (\JsonException) {
186+
$payloads = null;
187+
}
188+
}
189+
}
190+
191+
return is_array($payloads) ? $payloads : null;
192+
}
193+
107194
public function syncAssets($assets)
108195
{
109196
$assetKeys = [];

resources/js/admin/devlink/components/BundleConfigurations.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
</div>
2020
<div class="config-action">
2121
<button
22-
v-if="props.type || (config.configurable && !props.disabled)"
22+
v-if="props.type || config.configurable"
2323
class="config-action-button"
2424
@click="$emit('open-settings-modal', {
2525
key: config.type,

resources/js/admin/devlink/components/BundleSettingsModal.vue

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ import { useRoute } from "vue-router/composables";
6464
6565
const emit = defineEmits(["settings-saved"]);
6666
67-
defineProps({
67+
const props = defineProps({
6868
editable: {
6969
type: Boolean,
7070
default: false,
@@ -81,6 +81,7 @@ const selectedIds = ref([]);
8181
const allSelected = ref(false);
8282
const loading = ref(false);
8383
const bundleSettingExists = ref(false);
84+
const previewAvailable = ref(true);
8485
8586
const computedFields = computed(() => [
8687
{
@@ -106,6 +107,10 @@ const isPlatformConfiguration = computed(() => (
106107
));
107108
108109
const modalDescription = computed(() => {
110+
if (isPlatformConfiguration.value && !props.editable) {
111+
return `These ${modalTitle.value.toLowerCase()} are included in the installed bundle.`;
112+
}
113+
109114
if (settingKey.value === "ui_dashboards") {
110115
return [
111116
"Select the dashboards to include in this bundle.",
@@ -126,6 +131,15 @@ const modalDescription = computed(() => {
126131
});
127132
128133
const emptyText = computed(() => {
134+
if (isPlatformConfiguration.value && !props.editable && !previewAvailable.value) {
135+
return "Bundle content preview is unavailable for this version";
136+
}
137+
if (settingKey.value === "ui_dashboards" && !props.editable) {
138+
return "No dashboards included";
139+
}
140+
if (settingKey.value === "ui_menus" && !props.editable) {
141+
return "No menus included";
142+
}
129143
if (settingKey.value === "ui_dashboards") {
130144
return "No dashboards available";
131145
}
@@ -142,10 +156,31 @@ const hide = () => {
142156
}
143157
};
144158
159+
const loadSettingPreview = async () => {
160+
const response = await window.ProcessMaker.apiClient.get(
161+
`devlink/local-bundles/${bundleId}/setting-preview/${settingKey.value}`,
162+
);
163+
const preview = response.data;
164+
165+
previewAvailable.value = preview.available;
166+
bundleSettingExists.value = preview.selection !== "none";
167+
settings.value = (preview.items || []).map((setting) => ({
168+
...setting,
169+
enabled: true,
170+
}));
171+
selectedIds.value = settings.value.map((setting) => setting.key);
172+
allSelected.value = preview.selection === "all" && settings.value.length > 0;
173+
};
174+
145175
const loadSettings = async () => {
146176
loading.value = true;
147177
148178
try {
179+
if (isPlatformConfiguration.value && !props.editable) {
180+
await loadSettingPreview();
181+
return;
182+
}
183+
149184
const [response, settingsResponse] = await Promise.all([
150185
window.ProcessMaker.apiClient.get(`devlink/local-bundles/${bundleId}/setting/${settingKey.value}`),
151186
window.ProcessMaker.apiClient.get(`devlink/local-bundles/all-settings/${settingKey.value}`),
@@ -182,6 +217,11 @@ const loadSettings = async () => {
182217
};
183218
184219
const onOk = async () => {
220+
if (!props.editable) {
221+
hide();
222+
return;
223+
}
224+
185225
if (selectedIds.value.length === 0 && !bundleSettingExists.value) {
186226
hide();
187227
return;
@@ -215,6 +255,7 @@ const show = (config) => {
215255
selectedIds.value = [];
216256
allSelected.value = false;
217257
bundleSettingExists.value = false;
258+
previewAvailable.value = true;
218259
if (bundleSettingsModal.value) {
219260
bundleSettingsModal.value.show();
220261
loadSettings();

routes/api.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -423,6 +423,10 @@
423423
Route::get('devlink/local-bundles', [DevLinkController::class, 'localBundles'])->name('devlink.local-bundles');
424424
Route::get('devlink/local-bundles/{bundle}', [DevLinkController::class, 'showBundle'])->name('devlink.local-bundle');
425425
Route::get('devlink/local-bundles/{bundle}/setting/{settingKey}', [DevLinkController::class, 'getBundleSetting'])->name('devlink.local-bundle-setting');
426+
Route::get(
427+
'devlink/local-bundles/{bundle}/setting-preview/{settingKey}',
428+
[DevLinkController::class, 'getBundleSettingPreview']
429+
)->name('devlink.local-bundle-setting-preview');
426430
Route::get('devlink/local-bundles/all-settings/{settingKey}', [DevLinkController::class, 'getBundleAllSettings'])->name('devlink.local-bundle-all-settings');
427431
Route::post('devlink/local-bundles/setting/refresh-ui', [DevLinkController::class, 'refreshUi'])->name('devlink.local-bundle-setting-refresh-ui');
428432
Route::post('devlink/local-bundles', [DevLinkController::class, 'createBundle'])->name('devlink.create-bundle');

tests/Feature/Api/DevLinkTest.php

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace Tests\Feature\Api;
44

55
use Illuminate\Support\Facades\Http;
6+
use Illuminate\Support\Facades\Storage;
67
use PHPUnit\Framework\Attributes\DataProvider;
78
use ProcessMaker\Http\Controllers\Api\DevLinkController;
89
use ProcessMaker\Models\Bundle;
@@ -68,6 +69,72 @@ public function testGetBundleAllSettingsReturnsDashboardAndMenuOptions()
6869
], $menuOptions);
6970
}
7071

72+
public function testGetBundleSettingPreviewReturnsInstalledSourceMetadata()
73+
{
74+
Storage::fake(config('media-library.disk_name'));
75+
76+
$bundle = Bundle::factory()->create(['version' => 1]);
77+
$bundle->addSettings('ui_dashboards', json_encode(['id' => [9001, 9002]]));
78+
$bundle->savePayloadsToFile([], [[
79+
[
80+
'type' => 'dashboard_package',
81+
'version' => '2',
82+
'root' => 'dashboard-zulu',
83+
'name' => 'Zulu Dashboard',
84+
'export' => [],
85+
],
86+
[
87+
'type' => 'dashboard_package',
88+
'version' => '2',
89+
'root' => 'dashboard-alpha',
90+
'name' => 'Alpha Dashboard',
91+
'export' => [],
92+
],
93+
]]);
94+
95+
$response = $this->apiCall(
96+
'GET',
97+
route('api.devlink.local-bundle-setting-preview', [
98+
'bundle' => $bundle->id,
99+
'settingKey' => 'ui_dashboards',
100+
])
101+
);
102+
103+
$response->assertOk()->assertExactJson([
104+
'setting' => 'ui_dashboards',
105+
'selection' => 'partial',
106+
'available' => true,
107+
'items' => [
108+
['key' => 'dashboard-alpha', 'name' => 'Alpha Dashboard'],
109+
['key' => 'dashboard-zulu', 'name' => 'Zulu Dashboard'],
110+
],
111+
]);
112+
113+
$bundleWithoutSnapshot = Bundle::factory()->create();
114+
$bundleWithoutSnapshot->addSettings('ui_menus', null);
115+
$unavailableResponse = $this->apiCall(
116+
'GET',
117+
route('api.devlink.local-bundle-setting-preview', [
118+
'bundle' => $bundleWithoutSnapshot->id,
119+
'settingKey' => 'ui_menus',
120+
])
121+
);
122+
$unavailableResponse->assertOk()->assertJson([
123+
'selection' => 'all',
124+
'available' => false,
125+
'items' => [],
126+
]);
127+
128+
$unsupportedResponse = $this->apiCall(
129+
'GET',
130+
route('api.devlink.local-bundle-setting-preview', [
131+
'bundle' => $bundle->id,
132+
'settingKey' => 'users',
133+
])
134+
);
135+
$unsupportedResponse->assertNotFound();
136+
}
137+
71138
#[DataProvider('selectableUiSettingsProvider')]
72139
public function testAddSettingsPersistsPartialAllAndEmptySelections(string $settingKey)
73140
{

0 commit comments

Comments
 (0)