diff --git a/app/Http/Controllers/PluginController.php b/app/Http/Controllers/PluginController.php
index 8b3641df..3991775b 100644
--- a/app/Http/Controllers/PluginController.php
+++ b/app/Http/Controllers/PluginController.php
@@ -79,29 +79,21 @@ class PluginController extends Controller
public function getPluginData(PluginManager $plugins)
{
- $installed = $plugins->getPlugins();
-
- return Datatables::of($installed)
- ->setRowId('plugin-{{ $name }}')
- ->editColumn('title', function ($plugin) {
- return trans($plugin->title ?: 'EMPTY');
+ return $plugins->getPlugins()
+ ->map(function ($plugin) {
+ return [
+ 'name' => $plugin->name,
+ 'title' => trans($plugin->title ?: 'EMPTY'),
+ 'author' => $plugin->author,
+ 'description' => trans($plugin->description ?: 'EMPTY'),
+ 'version' => $plugin->version,
+ 'url' => $plugin->url,
+ 'enabled' => $plugin->isEnabled(),
+ 'config' => $plugin->hasConfigView(),
+ 'dependencies' => $this->getPluginDependencies($plugin),
+ ];
})
- ->editColumn('description', function ($plugin) {
- return trans($plugin->description ?: 'EMPTY');
- })
- ->editColumn('author', function ($plugin) {
- return ['author' => trans($plugin->author ?: 'EMPTY'), 'url' => $plugin->url];
- })
- ->addColumn('dependencies', function ($plugin) {
- return $this->getPluginDependencies($plugin);
- })
- ->addColumn('status', function ($plugin) {
- return trans('admin.plugins.status.'.($plugin->isEnabled() ? 'enabled' : 'disabled'));
- })
- ->addColumn('operations', function ($plugin) {
- return ['enabled' => $plugin->isEnabled(), 'hasConfigView' => $plugin->hasConfigView()];
- })
- ->make(true);
+ ->values();
}
protected function getPluginDependencies(Plugin $plugin)
diff --git a/resources/assets/src/components/admin/Plugins.vue b/resources/assets/src/components/admin/Plugins.vue
new file mode 100644
index 00000000..73cb7cae
--- /dev/null
+++ b/resources/assets/src/components/admin/Plugins.vue
@@ -0,0 +1,235 @@
+
+
+
+
+
+ {{ props.formattedRow[props.column.field] }}
+
+
+
+
+ {{ props.formattedRow.description }}
+
+ {{ $t('admin.pluginVersion') }}
+
{{ props.row.version }} |
+ {{ $t('admin.pluginAuthor') }}
+
{{ props.row.author }} |
+ {{ $t('admin.pluginName') }}
+ {{ props.row.name }}
+
+
+
+
+
+
+ {{ dep }}: {{ semver }}
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/resources/assets/src/components/route.js b/resources/assets/src/components/route.js
index 158d35cd..4636feeb 100644
--- a/resources/assets/src/components/route.js
+++ b/resources/assets/src/components/route.js
@@ -34,6 +34,11 @@ export default [
component: () => import('./admin/Customization'),
el: '#change-color'
},
+ {
+ path: 'admin/plugins/manage',
+ component: () => import('./admin/Plugins'),
+ el: '.content'
+ },
{
path: 'admin/plugins/market',
component: () => import('./admin/Market'),
diff --git a/resources/assets/tests/components/admin/Plugins.test.js b/resources/assets/tests/components/admin/Plugins.test.js
new file mode 100644
index 00000000..5100ecfe
--- /dev/null
+++ b/resources/assets/tests/components/admin/Plugins.test.js
@@ -0,0 +1,138 @@
+import Vue from 'vue';
+import { mount } from '@vue/test-utils';
+import Plugins from '@/components/admin/Plugins';
+import { flushPromises } from '../../utils';
+import { swal } from '@/js/notify';
+import toastr from 'toastr';
+
+jest.mock('@/js/notify');
+
+test('render dependencies', async () => {
+ Vue.prototype.$http.get.mockResolvedValue([
+ { name: 'a', dependencies: { requirements: [] } },
+ { name: 'b', dependencies: {
+ requirements: { 'a': '^1.0.0', 'c': '^2.0.0' }, unsatisfiedRequirements: { 'c': {} }
+ } }
+ ]);
+ const wrapper = mount(Plugins);
+ await flushPromises();
+
+ expect(wrapper.text()).toContain('admin.noDependencies');
+ expect(wrapper.find('span.label.bg-green').text()).toBe('a: ^1.0.0');
+ expect(wrapper.find('span.label.bg-red').text()).toBe('c: ^2.0.0');
+});
+
+test('render operation buttons', async () => {
+ Vue.prototype.$http.get.mockResolvedValue([
+ { name: 'a', dependencies: { requirements: [] }, enabled: true, config: true },
+ { name: 'b', dependencies: { requirements: [] }, enabled: true, config: false },
+ { name: 'c', dependencies: { requirements: [] }, enabled: false },
+ ]);
+ const wrapper = mount(Plugins);
+ await flushPromises();
+ const tbody = wrapper.find('tbody');
+
+ expect(tbody.find('tr:nth-child(1)').text()).toContain('admin.disablePlugin');
+ expect(tbody.find('tr:nth-child(1)').text()).toContain('admin.configurePlugin');
+ expect(tbody.find('tr:nth-child(2)').text()).not.toContain('admin.configurePlugin');
+ expect(tbody.find('tr:nth-child(3)').text()).toContain('admin.enablePlugin');
+ expect(tbody.find('tr:nth-child(3)').text()).toContain('admin.deletePlugin');
+});
+
+test('enable plugin', async () => {
+ Vue.prototype.$http.get.mockResolvedValue([
+ { name: 'a', dependencies: { requirements: [] }, enabled: false },
+ { name: 'b', dependencies: { requirements: {} }, enabled: false },
+ ]);
+ Vue.prototype.$http.post
+ .mockResolvedValueOnce({ errno: 1, msg: '1', reason: ['`a
`b'] })
+ .mockResolvedValue({ errno: 0, msg: '0' });
+ swal.mockResolvedValueOnce({ dismiss: 1 })
+ .mockResolvedValueOnce({});
+ const wrapper = mount(Plugins);
+ await flushPromises();
+
+ wrapper.findAll('.actions').at(0).find('a').trigger('click');
+ await flushPromises();
+ expect(swal).toBeCalledWith({
+ text: 'admin.noDependenciesNotice',
+ type: 'warning',
+ showCancelButton: true
+ });
+ expect(Vue.prototype.$http.post).not.toBeCalled();
+
+ wrapper.findAll('.actions').at(0).find('a').trigger('click');
+ await flushPromises();
+ expect(Vue.prototype.$http.post).toBeCalledWith(
+ '/admin/plugins/manage',
+ { action: 'enable', name: 'a' }
+ );
+ expect(swal).toBeCalledWith({
+ type: 'warning',
+ html: '1
'
+ });
+
+ wrapper.findAll('.actions').at(1).find('a').trigger('click');
+ await flushPromises();
+ expect(wrapper.text()).toContain('admin.disablePlugin');
+});
+
+test('disable plugin', async () => {
+ jest.spyOn(toastr, 'success');
+ Vue.prototype.$http.get.mockResolvedValue([
+ { name: 'a', dependencies: { requirements: [] }, enabled: true, config: false }
+ ]);
+ Vue.prototype.$http.post
+ .mockResolvedValueOnce({ errno: 1, msg: '1' })
+ .mockResolvedValue({ errno: 0, msg: '0' });
+ const wrapper = mount(Plugins);
+ await flushPromises();
+ const button = wrapper.find('.actions').find('a');
+
+ button.trigger('click');
+ await flushPromises();
+ expect(Vue.prototype.$http.post).toBeCalledWith(
+ '/admin/plugins/manage',
+ { action: 'disable', name: 'a' }
+ );
+
+ button.trigger('click');
+ await flushPromises();
+ expect(toastr.success).toBeCalledWith('0');
+});
+
+test('delete plugin', async () => {
+ jest.spyOn(toastr, 'success');
+ Vue.prototype.$http.get.mockResolvedValue([
+ { name: 'a', dependencies: { requirements: [] }, enabled: false },
+ ]);
+ Vue.prototype.$http.post
+ .mockResolvedValueOnce({ errno: 1, msg: '1' })
+ .mockResolvedValue({ errno: 0, msg: '0' });
+ swal.mockResolvedValueOnce({ dismiss: 1 })
+ .mockResolvedValue({});
+ const wrapper = mount(Plugins);
+ await flushPromises();
+ const button = wrapper.find('.actions').findAll('a').at(1);
+
+ button.trigger('click');
+ await flushPromises();
+ expect(swal).toBeCalledWith({
+ text: 'admin.confirmDeletion',
+ type: 'warning',
+ showCancelButton: true
+ });
+ expect(Vue.prototype.$http.post).not.toBeCalled();
+
+ button.trigger('click');
+ await flushPromises();
+ expect(Vue.prototype.$http.post).toBeCalledWith(
+ '/admin/plugins/manage',
+ { action: 'delete', name: 'a' }
+ );
+ expect(swal).toBeCalledWith({ type: 'warning', text: '1' });
+
+ button.trigger('click');
+ await flushPromises();
+ expect(toastr.success).toBeCalledWith('0');
+});
diff --git a/resources/lang/zh_CN/front-end.yml b/resources/lang/zh_CN/front-end.yml
index b89d525c..968b3591 100644
--- a/resources/lang/zh_CN/front-end.yml
+++ b/resources/lang/zh_CN/front-end.yml
@@ -260,9 +260,9 @@ admin:
noSuchUser: 没有这个用户哦~
changePlayerNameNotice: 请输入新的角色名:
emptyPlayerName: 您还没填写角色名呢
- configurePlugin: 插件配置
+ configurePlugin: 配置
noPluginConfigNotice: 插件已被禁用或无配置页
- deletePlugin: 删除插件
+ deletePlugin: 删除
noDependencies: 无要求
whyDependencies: 为什么会这样?
statusEnabled: 已启用
@@ -278,8 +278,8 @@ admin:
updatePlugin: 更新
pluginUpdating: 正在更新...
confirmUpdate: 确定将「:plugin」从 :old 升级至 :new?
- enablePlugin: 启用插件
- disablePlugin: 禁用插件
+ enablePlugin: 启用
+ disablePlugin: 禁用
confirmDeletion: 真的要删除这个插件吗?
noDependenciesNotice: >-
此插件没有声明任何依赖关系,这代表它有可能并不兼容此版本的 Blessing
diff --git a/resources/views/admin/plugins.blade.php b/resources/views/admin/plugins.blade.php
index e08bedee..7b244e3c 100644
--- a/resources/views/admin/plugins.blade.php
+++ b/resources/views/admin/plugins.blade.php
@@ -2,10 +2,6 @@
@section('title', trans('general.plugin-manage'))
-@section('style')
-
-@endsection
-
@section('content')
@@ -18,33 +14,7 @@
-
-
- @if (session()->has('message'))
-
- {{ session('message') }}
-
- @endif
-
-
-
-
-
-
- | @lang('admin.plugins.name') |
- @lang('admin.plugins.description') |
- @lang('admin.plugins.author') |
- @lang('admin.plugins.version') |
- @lang('admin.plugins.dependencies') |
- @lang('admin.plugins.status.title') |
- @lang('admin.plugins.operations.title') |
-
-
-
-
-
-
-
+
@endsection
diff --git a/tests/PluginControllerTest.php b/tests/PluginControllerTest.php
index da3507fa..f7038930 100644
--- a/tests/PluginControllerTest.php
+++ b/tests/PluginControllerTest.php
@@ -4,6 +4,8 @@ namespace Tests;
use App\Events;
use ZipArchive;
+use Illuminate\Support\Facades\File;
+use Tests\Concerns\GeneratesFakePlugins;
use Illuminate\Foundation\Testing\WithoutMiddleware;
use Illuminate\Foundation\Testing\DatabaseMigrations;
use Illuminate\Foundation\Testing\DatabaseTransactions;
@@ -11,39 +13,16 @@ use Illuminate\Foundation\Testing\DatabaseTransactions;
class PluginControllerTest extends TestCase
{
use DatabaseTransactions;
+ use GeneratesFakePlugins;
protected function setUp()
{
parent::setUp();
- $plugins = [
- 'example-plugin' => 'example-plugin_v1.0.0.zip',
- 'avatar-api' => 'avatar-api_v1.1.1.zip'
- ];
+ $this->generateFakePlugin(['name' => 'fake-plugin-for-test', 'version' => '1.1.4']);
+ $this->generateFakePlugin(['name' => 'fake-plugin-with-config-view', 'version' => '5.1.4', 'config' => 'config.blade.php']);
- foreach ($plugins as $plugin_name => $filename) {
- if (! file_exists(base_path('plugins/'.$plugin_name))) {
- $user_agent = menv('USER_AGENT', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.140 Safari/537.36');
-
- $context = stream_context_create(['http' => [
- 'method' => 'GET',
- 'header' => "User-Agent: $user_agent"
- ]]);
-
- file_put_contents(
- storage_path('testing/'.$filename),
- file_get_contents("https://coding.net/u/printempw/p/bs-plugins-archive/git/raw/master/$filename", false, $context)
- );
-
- $zip = new ZipArchive();
- $zip->open(storage_path('testing/'.$filename));
- $zip->extractTo(base_path('plugins/'));
- $zip->close();
- unlink(storage_path('testing/'.$filename));
- }
- }
-
- return $this->actAs('admin');
+ return $this->actAs('superAdmin');
}
public function testShowManage()
@@ -55,13 +34,14 @@ class PluginControllerTest extends TestCase
public function testConfig()
{
// Plugin is disabled
- $this->get('/admin/plugins/config/example-plugin')
- ->assertStatus(404);
+ $this->get('/admin/plugins/config/fake-plugin-with-config-view')
+ ->assertNotFound();
// Plugin is enabled but it doesn't have config view
- plugin('avatar-api')->setEnabled(true);
+ plugin('fake-plugin-for-test')->setEnabled(true);
$this->get('/admin/plugins/config/avatar-api')
- ->assertStatus(404);
+ ->assertSee(trans('admin.plugins.operations.no-config-notice'))
+ ->assertNotFound();
// Plugin has config view
plugin('example-plugin')->setEnabled(true);
@@ -79,38 +59,28 @@ class PluginControllerTest extends TestCase
]);
// Invalid action
- $this->postJson('/admin/plugins/manage', ['name' => 'avatar-api'])
+ $this->postJson('/admin/plugins/manage', ['name' => 'fake-plugin-for-test'])
->assertJson([
'errno' => 1,
'msg' => trans('admin.invalid-action')
]);
- // Retrieve requirements
- $this->postJson('/admin/plugins/manage', [
- 'name' => 'avatar-api',
- 'action' => 'requirements'
- ])->assertJson([
- 'isRequirementsSatisfied' => true,
- 'requirements' => [],
- 'unsatisfiedRequirements' => []
- ]);
-
// Enable a plugin with unsatisfied dependencies
- app('plugins')->getPlugin('avatar-api')->setRequirements([
+ app('plugins')->getPlugin('fake-plugin-for-test')->setRequirements([
'blessing-skin-server' => '^3.4.0',
- 'example-plugin' => '^6.6.6',
+ 'fake-plugin-with-config-view' => '^6.6.6',
'whatever' => '^1.0.0'
]);
- app('plugins')->enable('example-plugin');
+ app('plugins')->enable('fake-plugin-with-config-view');
$this->postJson('/admin/plugins/manage', [
- 'name' => 'avatar-api',
+ 'name' => 'fake-plugin-for-test',
'action' => 'enable'
])->assertJson([
'errno' => 1,
'msg' => trans('admin.plugins.operations.unsatisfied.notice'),
'reason' => [
trans('admin.plugins.operations.unsatisfied.version', [
- 'name' => 'example-plugin',
+ 'name' => 'fake-plugin-with-config-view',
'constraint' => '^6.6.6'
]),
trans('admin.plugins.operations.unsatisfied.disabled', [
@@ -120,60 +90,68 @@ class PluginControllerTest extends TestCase
]);
// Enable a plugin
- app('plugins')->getPlugin('avatar-api')->setRequirements([]);
+ app('plugins')->getPlugin('fake-plugin-for-test')->setRequirements([]);
$this->expectsEvents(Events\PluginWasEnabled::class);
$this->postJson('/admin/plugins/manage', [
- 'name' => 'avatar-api',
+ 'name' => 'fake-plugin-for-test',
'action' => 'enable'
])->assertJson([
'errno' => 0,
'msg' => trans(
'admin.plugins.operations.enabled',
- ['plugin' => plugin('avatar-api')->title]
+ ['plugin' => plugin('fake-plugin-for-test')->title]
)
]);
// Disable a plugin
$this->postJson('/admin/plugins/manage', [
- 'name' => 'avatar-api',
+ 'name' => 'fake-plugin-for-test',
'action' => 'disable'
])->assertJson([
'errno' => 0,
'msg' => trans(
'admin.plugins.operations.disabled',
- ['plugin' => plugin('avatar-api')->title]
+ ['plugin' => plugin('fake-plugin-for-test')->title]
)
]);
$this->expectsEvents(Events\PluginWasDisabled::class);
// Delete a plugin
$this->postJson('/admin/plugins/manage', [
- 'name' => 'avatar-api',
+ 'name' => 'fake-plugin-for-test',
'action' => 'delete'
])->assertJson([
'errno' => 0,
'msg' => trans('admin.plugins.operations.deleted')
]);
$this->expectsEvents(Events\PluginWasDeleted::class);
- $this->assertFalse(file_exists(base_path('plugins/avatar-api/')));
+ $this->assertFalse(file_exists(base_path('plugins/fake-plugin-for-test/')));
}
public function testGetPluginData()
{
$this->getJson('/admin/plugins/data')
->assertJsonStructure([
- 'data' => [[
+ [
'name',
'version',
- 'path',
'title',
'description',
'author',
'url',
- 'namespace',
'enabled',
+ 'config',
'dependencies'
- ]]
+ ]
]);
}
+
+ protected function tearDown()
+ {
+ // Clean fake plugins
+ File::deleteDirectory(base_path('plugins/fake-plugin-for-test'));
+ File::deleteDirectory(base_path('plugins/fake-plugin-with-config-view'));
+
+ parent::tearDown();
+ }
}