Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions ProcessMaker/Exception/BundleIntegrityException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php

namespace ProcessMaker\Exception;

use Illuminate\Http\JsonResponse;
use Illuminate\Support\Collection;
use ProcessMaker\Models\Bundle;
use ProcessMaker\Models\BundleAsset;
use RuntimeException;

class BundleIntegrityException extends RuntimeException
{
private array $invalidAssets;

public function __construct(Bundle $bundle, Collection $invalidAssets)
{
$this->invalidAssets = $invalidAssets
->map(fn (BundleAsset $asset) => $asset->integrityDetails())
->values()
->all();

parent::__construct(__(
'The bundle :bundle contains unavailable assets and cannot be exported.',
['bundle' => $bundle->name]
));
}

public function invalidAssets(): array
{
return $this->invalidAssets;
}

public function render(): JsonResponse
{
return response()->json([
'error' => [
'code' => 422,
'message' => $this->getMessage(),
],
'errors' => [
'assets' => $this->invalidAssets,
],
], 422);
}
}
25 changes: 25 additions & 0 deletions ProcessMaker/Exception/DevLinkRemoteBundleException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

namespace ProcessMaker\Exception;

use RuntimeException;
use Throwable;

class DevLinkRemoteBundleException extends RuntimeException
{
public function __construct(array $invalidAssets, ?Throwable $previous = null)
{
$assets = collect($invalidAssets)->map(function (array $asset) {
$type = class_basename($asset['asset_type'] ?? 'Asset');
$id = $asset['asset_id'] ?? '?';
$bundleAssetId = $asset['bundle_asset_id'] ?? '?';

return "$type #$id (bundle asset #$bundleAssetId)";
})->implode(', ');

parent::__construct(__(
'The remote bundle contains unavailable assets: :assets. Repair the bundle on the source instance and try again.',
['assets' => $assets]
), 0, $previous);
}
}
9 changes: 9 additions & 0 deletions ProcessMaker/Http/Controllers/Api/DevLinkController.php
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,15 @@ private function normalizeUrl(string $url): string

public function deleteBundleAsset(BundleAsset $bundleAsset)
{
if (
!$bundleAsset->bundle->editable()
&& $bundleAsset->integrity_status === BundleAsset::INTEGRITY_VALID
) {
throw ValidationException::withMessages([
'*' => __('Only unavailable assets can be removed from an installed bundle.'),
]);
}

$bundleAsset->delete();

return response()->json(['message' => 'Bundle asset association deleted.'], 200);
Expand Down
12 changes: 10 additions & 2 deletions ProcessMaker/Jobs/DevLinkInstall.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use ProcessMaker\Exception\DevLinkRemoteBundleException;
use ProcessMaker\ImportExport\Logger;
use ProcessMaker\Jobs\ImportV2;
use ProcessMaker\Models\Bundle;
Expand Down Expand Up @@ -49,7 +51,7 @@ public function __construct(
public function handle(): void
{
//log
\Log::info('DevLinkInstall job started: ' . $this->devLinkId);
Log::info('DevLinkInstall job started: ' . $this->devLinkId);
$devLink = DevLink::findOrFail($this->devLinkId);
$logger = new Logger($this->userId);

Expand Down Expand Up @@ -82,7 +84,13 @@ public function handle(): void

public function failed(Throwable $exception): void
{
(new Logger($this->userId))->exception($exception);
$logger = new Logger($this->userId);
if ($exception instanceof DevLinkRemoteBundleException) {
Log::error($exception->getMessage(), ['exception' => $exception]);
$logger->error($exception->getMessage());
} else {
$logger->exception($exception);
}

// Unlock the job
// We can't use $this->lock->release() here because this is run in a new instance
Expand Down
18 changes: 18 additions & 0 deletions ProcessMaker/Models/Bundle.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Support\Facades\Http;
use ProcessMaker\Exception\BundleIntegrityException;
use ProcessMaker\Exception\ExporterNotSupported;
use ProcessMaker\Exception\ValidationException;
use ProcessMaker\ImportExport\Importer;
Expand Down Expand Up @@ -78,6 +79,8 @@ public function getAssetCountAttribute()

public function export()
{
$this->assertIntegrity();

$exports = [];

foreach ($this->assets as $bundleAsset) {
Expand All @@ -93,6 +96,21 @@ public function export()
return $exports;
}

public function invalidAssets()
{
return $this->assets->filter(
fn (BundleAsset $asset) => $asset->integrity_status !== BundleAsset::INTEGRITY_VALID
);
}

public function assertIntegrity(): void
{
$invalidAssets = $this->invalidAssets();
if ($invalidAssets->isNotEmpty()) {
throw new BundleIntegrityException($this, $invalidAssets);
}
}

public function exportSettings()
{
$exports = [];
Expand Down
78 changes: 69 additions & 9 deletions ProcessMaker/Models/BundleAsset.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,22 @@
namespace ProcessMaker\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Support\Str;
use ProcessMaker\Enums\ExporterMap;

class BundleAsset extends ProcessMakerModel
{
use HasFactory;

public const INTEGRITY_VALID = 'valid';

public const INTEGRITY_MISSING = 'missing';

public const INTEGRITY_TYPE_UNAVAILABLE = 'type_unavailable';

protected $guarded = ['id'];

protected $appends = ['name', 'url', 'type', 'owner_name', 'categories'];
protected $appends = ['name', 'url', 'type', 'owner_name', 'categories', 'integrity_status'];

const DATA_SOURCE_CLASS = 'ProcessMaker\Packages\Connectors\DataSources\Models\DataSource';

Expand All @@ -23,9 +30,11 @@ class BundleAsset extends ProcessMakerModel

const PM_BLOCK_CLASS = 'ProcessMaker\Package\PackagePmBlocks\Models\PmBlock';

public static function canExport(ProcessMakerModel $asset)
public static function canExport(?ProcessMakerModel $asset)
{
return method_exists($asset, 'export') && ExporterMap::getExporterClassForModel($asset);
return $asset !== null
&& method_exists($asset, 'export')
&& ExporterMap::getExporterClassForModel($asset);
}

public function bundle()
Expand All @@ -50,18 +59,30 @@ public static function makeKey(ProcessMakerModel $asset)

public function getNameAttribute()
{
$asset = $this->resolvedAsset();
if ($asset === null) {
return __('Missing :type #:id', [
'type' => $this->typeLabel(),
'id' => $this->asset_id,
]);
}

if (
$this->asset_type === Screen::class ||
$this->asset_type === Script::class
) {
return $this->asset->title;
return $asset->title;
}

return $this->asset->name;
return $asset->name;
}

public function getUrlAttribute()
{
if ($this->integrity_status !== self::INTEGRITY_VALID) {
return null;
}

switch($this->asset_type) {
case Screen::class:
return "/designer/screen-builder/{$this->asset_id}/edit";
Expand Down Expand Up @@ -110,8 +131,9 @@ public function getTypeAttribute()

public function getOwnerNameAttribute()
{
if ($this->asset && method_exists($this->asset, 'user') && $this->asset->user) {
return $this->asset->user->firstname . ' ' . $this->asset->user->lastname;
$asset = $this->resolvedAsset();
if ($asset && method_exists($asset, 'user') && $asset->user) {
return $asset->user->firstname . ' ' . $asset->user->lastname;
}

return null;
Expand All @@ -123,10 +145,48 @@ public function getCategoriesAttribute()
return [];
}

if ($this->asset && method_exists($this->asset, 'categories')) {
return $this->asset->categories->pluck('name')->toArray();
$asset = $this->resolvedAsset();
if ($asset && method_exists($asset, 'categories')) {
return $asset->categories->pluck('name')->toArray();
}

return [];
}

public function getIntegrityStatusAttribute(): string
{
if (!class_exists($this->asset_type)) {
return self::INTEGRITY_TYPE_UNAVAILABLE;
}

return $this->resolvedAsset() === null
? self::INTEGRITY_MISSING
: self::INTEGRITY_VALID;
}

public function integrityDetails(): array
{
return [
'bundle_asset_id' => $this->id,
'asset_type' => $this->asset_type,
'asset_id' => $this->asset_id,
'integrity_status' => $this->integrity_status,
];
}

private function resolvedAsset(): ?ProcessMakerModel
{
if (!class_exists($this->asset_type)) {
return null;
}

return $this->asset;
}

private function typeLabel(): string
{
$type = $this->type ?? class_basename($this->asset_type);

return Str::headline($type);
}
}
31 changes: 21 additions & 10 deletions ProcessMaker/Models/DevLink.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@
namespace ProcessMaker\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Http\Client\RequestException;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Str;
use ProcessMaker\Exception\DevLinkRemoteBundleException;
use ProcessMaker\ImportExport\Importer;
use ProcessMaker\ImportExport\Logger;
use ProcessMaker\ImportExport\Options;
Expand Down Expand Up @@ -143,19 +145,28 @@ public function installRemoteBundle($remoteBundleId, $updateType)

$this->logger->status(__('Downloading bundle from remote instance'));

$bundleInfo = $this->remoteBundle($remoteBundleId)->json();
try {
$bundleInfo = $this->remoteBundle($remoteBundleId)->json();

$bundleExport = $this->client()->get(
route('api.devlink.export-local-bundle', ['bundle' => $remoteBundleId], false)
)->json();
$bundleExport = $this->client()->get(
route('api.devlink.export-local-bundle', ['bundle' => $remoteBundleId], false)
)->json();

$bundleSettingsExport = $this->client()->get(
route('api.devlink.export-local-bundle-settings', ['bundle' => $remoteBundleId], false)
)->json();
$bundleSettingsExport = $this->client()->get(
route('api.devlink.export-local-bundle-settings', ['bundle' => $remoteBundleId], false)
)->json();

$bundleSettingsPayloads = $this->client()->get(
route('api.devlink.export-local-bundle-setting-payloads', ['bundle' => $remoteBundleId], false)
)->json();
$bundleSettingsPayloads = $this->client()->get(
route('api.devlink.export-local-bundle-setting-payloads', ['bundle' => $remoteBundleId], false)
)->json();
} catch (RequestException $exception) {
$invalidAssets = $exception->response->json('errors.assets');
if (is_array($invalidAssets) && $invalidAssets !== []) {
throw new DevLinkRemoteBundleException($invalidAssets, $exception);
}

throw $exception;
}

$bundle = Bundle::updateOrCreate(
[
Expand Down
31 changes: 24 additions & 7 deletions resources/js/admin/devlink/components/BundleAssetListing.vue
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,21 @@ const loadAssets = async () => {
loading.value = true;
const response = await window.ProcessMaker.apiClient.get(`/api/1.0/devlink/local-bundles/${bundleId}`);
bundle.value = response.data;
items.value = response.data.assets.filter(asset => asset.type.toUpperCase() === route.params.type.toUpperCase());
items.value = response.data.assets.filter(
(asset) => asset.type && asset.type.toUpperCase() === route.params.type.toUpperCase(),
);
loading.value = false;
};

const remove = async (asset) => {
const confirm = await vue.$bvModal.msgBoxConfirm(vue.$t('Are you sure you want to remote this asset from the bundle?'), {
okTitle: vue.$t('Ok'),
cancelTitle: vue.$t('Cancel'),
});
const confirm = await vue.$bvModal.msgBoxConfirm(
vue.$t('Remove this association from the bundle? The underlying asset will not be deleted.'),
{
okTitle: vue.$t('Remove from bundle'),
okVariant: 'danger',
cancelTitle: vue.$t('Cancel'),
},
);
if (!confirm) {
return;
}
Expand Down Expand Up @@ -108,12 +114,23 @@ const fields = [
class="asset-listing-table"
>
<template #cell(name)="data">
<a :href="data.item.url">{{ data.item.name }}</a>
<a
v-if="data.item.integrity_status === 'valid'"
:href="data.item.url"
>
{{ data.item.name }}
</a>
<span v-else>
{{ data.item.name }}
<b-badge variant="warning">
{{ $t('Unavailable') }}
</b-badge>
</span>
</template>
<template #cell(menu)="data">
<div class="btn-menu-container">
<button
v-if="bundle.remote_id === null"
v-if="bundle.remote_id === null || data.item.integrity_status !== 'valid'"
class="btn install-asset-btn"
@click.prevent="remove(data.item)"
>
Expand Down
Loading
Loading