new plugins management page

This commit is contained in:
Pig Fang 2020-01-16 12:33:14 +08:00
parent 85f9f8378a
commit 7ecea9e7e6
12 changed files with 137 additions and 294 deletions

View File

@ -21,7 +21,6 @@ class MarketController extends Controller
$plugin = $manager->get($item['name']); $plugin = $manager->get($item['name']);
if ($plugin) { if ($plugin) {
$item['enabled'] = $plugin->isEnabled();
$item['installed'] = $plugin->version; $item['installed'] = $plugin->version;
$item['can_update'] = Comparator::greaterThan($item['version'], $item['installed']); $item['can_update'] = Comparator::greaterThan($item['version'], $item['installed']);
} else { } else {

View File

@ -88,21 +88,19 @@ class PluginController extends Controller
public function getPluginData(PluginManager $plugins) public function getPluginData(PluginManager $plugins)
{ {
return $plugins->all() return $plugins->all()
->map(function ($plugin) use ($plugins) { ->map(function (Plugin $plugin) {
return [ return [
'name' => $plugin->name, 'name' => $plugin->name,
'title' => trans($plugin->title ?: 'EMPTY'), 'title' => trans($plugin->title),
'author' => $plugin->author, 'description' => trans($plugin->description ?? ''),
'description' => trans($plugin->description ?: 'EMPTY'),
'version' => $plugin->version, 'version' => $plugin->version,
'url' => $plugin->url,
'enabled' => $plugin->isEnabled(), 'enabled' => $plugin->isEnabled(),
'readme' => (bool) $plugin->getReadme(), 'readme' => (bool) $plugin->getReadme(),
'config' => $plugin->hasConfig(), 'config' => $plugin->hasConfig(),
'dependencies' => [ 'icon' => array_merge(
'all' => $plugin->require, ['fa' => 'plug', 'faType' => 'fas', 'bg' => 'navy'],
'unsatisfied' => $plugins->getUnsatisfied($plugin), $plugin->getManifestAttr('enchants.icon', [])
], ),
]; ];
}) })
->values(); ->values();

View File

@ -16,7 +16,7 @@ class Plugin
]; ];
/** /**
* The full directory of this plugin. * The full path of this plugin.
* *
* @var string * @var string
*/ */
@ -29,11 +29,6 @@ class Plugin
*/ */
protected $manifest; protected $manifest;
/**
* Whether the plugin is enabled.
*
* @var bool
*/
protected $enabled = false; protected $enabled = false;
public function __construct(string $path, array $manifest) public function __construct(string $path, array $manifest)

View File

@ -1,40 +0,0 @@
import Vue from 'vue'
import { showModal, toast } from '../../scripts/notify'
import alertUnresolvedPlugins from './alertUnresolvedPlugins'
export default Vue.extend({
data: () => ({ plugins: [] }),
methods: {
async enablePlugin({
name, dependencies: { all }, originalIndex,
}: {
name: string
dependencies: { all: Record<string, string> }
originalIndex: number
}) {
if (Object.keys(all).length === 0) {
try {
await showModal({
text: this.$t('admin.noDependenciesNotice'),
okButtonType: 'warning',
})
} catch {
return
}
}
const {
code, message, data: { reason } = { reason: [] },
} = await this.$http.post(
'/admin/plugins/manage',
{ action: 'enable', name },
) as { code: number, message: string, data: { reason: string[] } }
if (code === 0) {
toast.success(message)
this.$set(this.plugins[originalIndex], 'enabled', true)
} else {
alertUnresolvedPlugins(message, reason)
}
},
},
})

View File

@ -43,11 +43,8 @@
<i class="fas fa-sync-alt" /> {{ $t('admin.updatePlugin') }} <i class="fas fa-sync-alt" /> {{ $t('admin.updatePlugin') }}
</template> </template>
</button> </button>
<button v-else-if="props.row.enabled" class="btn btn-primary" disabled> <button v-else class="btn btn-default" disabled>
<i class="fas fa-check" /> {{ $t('admin.statusEnabled') }} <i class="fas fa-download" /> {{ $t('admin.installPlugin') }}
</button>
<button v-else class="btn btn-primary" @click="enablePlugin(props.row)">
<i class="fas fa-plug" /> {{ $t('admin.enablePlugin') }}
</button> </button>
</template> </template>
<button <button
@ -74,7 +71,6 @@
import { VueGoodTable } from 'vue-good-table' import { VueGoodTable } from 'vue-good-table'
import 'vue-good-table/dist/vue-good-table.min.css' import 'vue-good-table/dist/vue-good-table.min.css'
import alertUnresolvedPlugins from '../../components/mixins/alertUnresolvedPlugins' import alertUnresolvedPlugins from '../../components/mixins/alertUnresolvedPlugins'
import enablePlugin from '../../components/mixins/enablePlugin'
import tableOptions from '../../components/mixins/tableOptions' import tableOptions from '../../components/mixins/tableOptions'
import emitMounted from '../../components/mixins/emitMounted' import emitMounted from '../../components/mixins/emitMounted'
import { showModal, toast } from '../../scripts/notify' import { showModal, toast } from '../../scripts/notify'
@ -86,7 +82,6 @@ export default {
}, },
mixins: [ mixins: [
emitMounted, emitMounted,
enablePlugin,
tableOptions, tableOptions,
], ],
data() { data() {

View File

@ -1,104 +1,51 @@
<template> <template>
<div class="container-fluid"> <div class="container-fluid d-flex flex-wrap">
<vue-good-table <div v-for="(plugin, index) in plugins" :key="plugin.name" class="info-box mr-3">
:rows="plugins" <span class="info-box-icon" :class="`bg-${plugin.icon.bg}`">
:columns="columns" <i :class="`${plugin.icon.faType} fa-${plugin.icon.fa}`" />
:search-options="tableOptions.search" </span>
:pagination-options="tableOptions.pagination" <div class="info-box-content">
style-class="vgt-table striped" <div class="d-flex justify-content-between">
:row-style-class="rowStyleClassFn" <div>
> <input :checked="plugin.enabled" type="checkbox" @click.prevent="switchPlugin(plugin, $event)">&nbsp;
<template #table-row="props"> <strong>{{ plugin.title }}</strong>&nbsp;
<span v-if="props.column.field === 'title'"> <span class="text-gray">v{{ plugin.version }}</span>
<strong>{{ props.formattedRow[props.column.field] }}</strong>
<div class="actions">
<template v-if="props.row.readme">
<a
:href="`${baseUrl}/admin/plugins/readme/${props.row.name}`"
class="text-primary"
>{{ $t('admin.pluginReadme') }}</a> |
</template>
<template v-if="props.row.enabled" class="actions">
<template v-if="props.row.config">
<a
v-t="'admin.configurePlugin'"
class="text-primary"
:href="`${baseUrl}/admin/plugins/config/${props.row.name}`"
/> |
</template>
<a
v-t="'admin.disablePlugin'"
href="#"
class="text-primary"
@click="disablePlugin(props.row)"
/>
</template>
<template v-else class="actions">
<a
v-t="'admin.enablePlugin'"
href="#"
class="text-primary"
@click="enablePlugin(props.row)"
/> |
<a
v-t="'admin.deletePlugin'"
href="#"
class="text-danger"
@click="deletePlugin(props.row)"
/>
</template>
</div> </div>
</span> <div class="plugin-actions">
<span v-else-if="props.column.field === 'description'"> <a
<div><p>{{ props.formattedRow.description }}</p></div> v-if="plugin.readme"
<div class="plugin-version-author"> :href="`${baseUrl}/admin/plugins/readme/${plugin.name}`"
{{ $t('admin.pluginVersion') }}
<span class="text-primary">{{ props.row.version }}</span> |
{{ $t('admin.pluginAuthor') }}
<a :href="props.row.url">{{ props.row.author }}</a> |
{{ $t('admin.pluginName') }}
{{ props.row.name }}
</div>
</span>
<span v-else-if="props.column.field === 'dependencies'">
<span v-if="Object.keys(props.row.dependencies.all).length === 0">
<i v-t="'admin.noDependencies'" />
</span>
<div v-else>
<span
v-for="(constraint, name) in props.row.dependencies.all"
:key="name"
class="badge"
:class="`bg-${name in props.row.dependencies.unsatisfied ? 'red' : 'green'}`"
> >
{{ name }}: {{ constraint }} <i class="fas fa-question" />
<br> </a>
</span> <a
v-if="plugin.enabled && plugin.config"
:href="`${baseUrl}/admin/plugins/config/${plugin.name}`"
>
<i class="fas fa-cog" />
</a>
<a href="#" @click="deletePlugin(plugin, index)">
<i class="fas fa-trash" />
</a>
</div> </div>
</span> </div>
<span v-else v-text="props.formattedRow[props.column.field]" /> <div class="mt-2 plugin-desc" :title="plugin.description">
</template> {{ plugin.description }}
</vue-good-table> </div>
</div>
</div>
</div> </div>
</template> </template>
<script> <script>
import { VueGoodTable } from 'vue-good-table' import alertUnresolvedPlugins from '../../components/mixins/alertUnresolvedPlugins'
import 'vue-good-table/dist/vue-good-table.min.css'
import enablePlugin from '../../components/mixins/enablePlugin'
import tableOptions from '../../components/mixins/tableOptions'
import emitMounted from '../../components/mixins/emitMounted' import emitMounted from '../../components/mixins/emitMounted'
import { showModal, toast } from '../../scripts/notify' import { showModal, toast } from '../../scripts/notify'
export default { export default {
name: 'Plugins', name: 'Plugins',
components: {
VueGoodTable,
},
mixins: [ mixins: [
emitMounted, emitMounted,
enablePlugin,
tableOptions,
], ],
props: { props: {
baseUrl: { baseUrl: {
@ -109,23 +56,6 @@ export default {
data() { data() {
return { return {
plugins: [], plugins: [],
columns: [
{
field: 'title', label: this.$t('admin.pluginTitle'), width: '17%',
},
{
field: 'description',
label: this.$t('admin.pluginDescription'),
sortable: false,
width: '65%',
},
{
field: 'dependencies',
label: this.$t('admin.pluginDependencies'),
sortable: false,
globalSearchDisabled: true,
},
],
} }
}, },
beforeMount() { beforeMount() {
@ -135,27 +65,47 @@ export default {
async fetchData() { async fetchData() {
this.plugins = await this.$http.get('/admin/plugins/data') this.plugins = await this.$http.get('/admin/plugins/data')
}, },
rowStyleClassFn(row) { async switchPlugin(plugin, { target }) {
return row.enabled ? 'plugin-enabled' : 'plugin' if (target.checked) {
if (await this.enablePlugin(plugin.name)) {
plugin.enabled = true
}
} else if (await this.disablePlugin(plugin.name)) {
plugin.enabled = false
}
}, },
async disablePlugin({ name, originalIndex }) { async enablePlugin(name) {
const {
code, message, data: { reason } = { reason: [] },
} = await this.$http.post(
'/admin/plugins/manage',
{ action: 'enable', name },
)
if (code === 0) {
toast.success(message)
} else {
alertUnresolvedPlugins(message, reason)
}
return code === 0
},
async disablePlugin(name) {
const { code, message } = await this.$http.post( const { code, message } = await this.$http.post(
'/admin/plugins/manage', '/admin/plugins/manage',
{ action: 'disable', name }, { action: 'disable', name },
) )
if (code === 0) { if (code === 0) {
toast.success(message) toast.success(message)
this.plugins[originalIndex].enabled = false
} else { } else {
toast.error(message) toast.error(message)
} }
return code === 0
}, },
async deletePlugin({ async deletePlugin(plugin, index) {
name, title, originalIndex,
}) {
try { try {
await showModal({ await showModal({
title, title: plugin.title,
text: this.$t('admin.confirmDeletion'), text: this.$t('admin.confirmDeletion'),
okButtonType: 'danger', okButtonType: 'danger',
}) })
@ -165,10 +115,10 @@ export default {
const { code, message } = await this.$http.post( const { code, message } = await this.$http.post(
'/admin/plugins/manage', '/admin/plugins/manage',
{ action: 'delete', name }, { action: 'delete', name: plugin.name },
) )
if (code === 0) { if (code === 0) {
this.$delete(this.plugins, originalIndex) this.$delete(this.plugins, index)
toast.success(message) toast.success(message)
} else { } else {
toast.error(message) toast.error(message)
@ -179,25 +129,35 @@ export default {
</script> </script>
<style lang="stylus"> <style lang="stylus">
.actions .info-box
margin-top 5px cursor default
color #ddd transition-property box-shadow
transition-duration 0.3s
width 32%
@media (max-width: 1280px)
width 47%
@media (max-width: 768px)
width 100%
&:hover
box-shadow 0 .5rem 1rem rgba(0,0,0,.15)
.plugin-version-author .info-box-content
color #777 max-width 85%
font-size small
a
color #337ab7
.plugin:nth-of-type(2n+1) > td:first-child .plugin-actions
border-left 5px solid rgba(51, 68, 109, 0.03) margin-top -7px
a
transition-property color
transition-duration 0.3s
color #000
&:hover
color #999
&:not(:last-child)
margin-right 7px
.plugin:nth-of-type(2n) > td:first-child .plugin-desc
border-left 5px solid #fff font-size 14px
white-space nowrap
.plugin-enabled overflow hidden
background-color #f7fcfe text-overflow ellipsis
.plugin-enabled > td:first-child
border-left 5px solid #3c8dbc
</style> </style>

View File

@ -30,13 +30,10 @@ test('render operation buttons', async () => {
name: 'a', dependencies: { all: {}, unsatisfied: {} }, installed: true, can_update: true, name: 'a', dependencies: { all: {}, unsatisfied: {} }, installed: true, can_update: true,
}, },
{ {
name: 'b', dependencies: { all: {}, unsatisfied: {} }, installed: true, enabled: true, name: 'b', dependencies: { all: {}, unsatisfied: {} }, installed: true,
}, },
{ {
name: 'c', dependencies: { all: {}, unsatisfied: {} }, installed: true, name: 'c', dependencies: { all: {}, unsatisfied: {} }, installed: false,
},
{
name: 'd', dependencies: { all: {}, unsatisfied: {} }, installed: false,
}, },
]) ])
const wrapper = mount(Market) const wrapper = mount(Market)
@ -44,9 +41,9 @@ test('render operation buttons', async () => {
const tbody = wrapper.find('tbody') const tbody = wrapper.find('tbody')
expect(tbody.find('tr:nth-child(1)').text()).toContain('admin.updatePlugin') expect(tbody.find('tr:nth-child(1)').text()).toContain('admin.updatePlugin')
expect(tbody.find('tr:nth-child(2)').text()).toContain('admin.statusEnabled') expect(tbody.find('tr:nth-child(2)').text()).toContain('admin.installPlugin')
expect(tbody.find('tr:nth-child(3)').text()).toContain('admin.enablePlugin') expect(tbody.find('tr:nth-child(2) button').attributes('disabled')).toBeTruthy()
expect(tbody.find('tr:nth-child(4)').text()).toContain('admin.installPlugin') expect(tbody.find('tr:nth-child(3)').text()).toContain('admin.installPlugin')
}) })
test('install plugin', async () => { test('install plugin', async () => {
@ -80,7 +77,7 @@ test('install plugin', async () => {
button.trigger('click') button.trigger('click')
await flushPromises() await flushPromises()
expect(wrapper.text()).toContain('admin.enablePlugin') expect(wrapper.find('.btn-default').attributes('disabled')).toBeTruthy()
}) })
test('update plugin', async () => { test('update plugin', async () => {

View File

@ -6,54 +6,30 @@ import Plugins from '@/views/admin/Plugins.vue'
jest.mock('@/scripts/notify') jest.mock('@/scripts/notify')
test('render dependencies', async () => { test('render config button', async () => {
Vue.prototype.$http.get.mockResolvedValue([ Vue.prototype.$http.get.mockResolvedValue([
{ name: 'a', dependencies: { all: {}, unsatisfied: {} } },
{ {
name: 'b', name: 'a', icon: {}, enabled: true, config: true,
dependencies: { },
all: { a: '^1.0.0', c: '^2.0.0' }, unsatisfied: { c: '' }, {
}, name: 'b', icon: {}, enabled: false, config: true,
},
{
name: 'c', icon: {}, enabled: false, config: false,
}, },
]) ])
const wrapper = mount(Plugins) const wrapper = mount(Plugins)
await flushPromises() await flushPromises()
expect(wrapper.text()).toContain('admin.noDependencies') expect(wrapper.find('.info-box:nth-child(1) .fa-cog').exists()).toBeTrue()
expect(wrapper.find('span.badge.bg-green').text()).toBe('a: ^1.0.0') expect(wrapper.find('.info-box:nth-child(2) .fa-cog').exists()).toBeFalse()
expect(wrapper.find('span.badge.bg-red').text()).toBe('c: ^2.0.0') expect(wrapper.find('.info-box:nth-child(3) .fa-cog').exists()).toBeFalse()
})
test('render operation buttons', async () => {
Vue.prototype.$http.get.mockResolvedValue([
{
name: 'a', dependencies: { all: {}, unsatisfied: {} }, enabled: true, config: true,
},
{
name: 'b', dependencies: { all: {}, unsatisfied: {} }, enabled: true, config: false,
},
{
name: 'c', dependencies: { all: {}, unsatisfied: {} }, 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 () => { test('enable plugin', async () => {
Vue.prototype.$http.get.mockResolvedValue([ Vue.prototype.$http.get.mockResolvedValue([
{ {
name: 'a', dependencies: { all: {}, unsatisfied: {} }, enabled: false, name: 'a', icon: {}, enabled: false,
},
{
name: 'b', dependencies: { all: { c: '' }, unsatisfied: {} }, enabled: false,
}, },
]) ])
Vue.prototype.$http.post Vue.prototype.$http.post
@ -61,29 +37,11 @@ test('enable plugin', async () => {
code: 1, message: '1', data: { reason: ['abc'] }, code: 1, message: '1', data: { reason: ['abc'] },
}) })
.mockResolvedValue({ code: 0, message: '0' }) .mockResolvedValue({ code: 0, message: '0' })
showModal
.mockRejectedValueOnce(null)
.mockResolvedValue({ value: '' })
const wrapper = mount(Plugins) const wrapper = mount(Plugins)
await flushPromises() await flushPromises()
const checkbox = wrapper.find('input[type=checkbox]')
wrapper checkbox.trigger('click')
.findAll('.actions')
.at(0)
.find('a')
.trigger('click')
await flushPromises()
expect(showModal).toBeCalledWith({
text: 'admin.noDependenciesNotice',
okButtonType: 'warning',
})
expect(Vue.prototype.$http.post).not.toBeCalled()
wrapper
.findAll('.actions')
.at(0)
.find('a')
.trigger('click')
await flushPromises() await flushPromises()
expect(Vue.prototype.$http.post).toBeCalledWith( expect(Vue.prototype.$http.post).toBeCalledWith(
'/admin/plugins/manage', '/admin/plugins/manage',
@ -94,19 +52,15 @@ test('enable plugin', async () => {
dangerousHTML: expect.stringContaining('<li>abc</li>'), dangerousHTML: expect.stringContaining('<li>abc</li>'),
}) })
wrapper checkbox.trigger('click')
.findAll('.actions')
.at(1)
.find('a')
.trigger('click')
await flushPromises() await flushPromises()
expect(wrapper.text()).toContain('admin.disablePlugin') expect(toast.success).toBeCalled()
}) })
test('disable plugin', async () => { test('disable plugin', async () => {
Vue.prototype.$http.get.mockResolvedValue([ Vue.prototype.$http.get.mockResolvedValue([
{ {
name: 'a', dependencies: { all: {}, unsatisfied: {} }, enabled: true, config: false, name: 'a', icon: {}, enabled: true,
}, },
]) ])
Vue.prototype.$http.post Vue.prototype.$http.post
@ -114,18 +68,18 @@ test('disable plugin', async () => {
.mockResolvedValue({ code: 0, message: '0' }) .mockResolvedValue({ code: 0, message: '0' })
const wrapper = mount(Plugins) const wrapper = mount(Plugins)
await flushPromises() await flushPromises()
const button = wrapper.find('.actions').find('a') const checkbox = wrapper.find('input[type="checkbox"]')
button.trigger('click') checkbox.trigger('click')
await flushPromises() await flushPromises()
expect(Vue.prototype.$http.post).toBeCalledWith( expect(Vue.prototype.$http.post).toBeCalledWith(
'/admin/plugins/manage', '/admin/plugins/manage',
{ action: 'disable', name: 'a' }, { action: 'disable', name: 'a' },
) )
button.trigger('click') checkbox.trigger('click')
await flushPromises() await flushPromises()
expect(toast.success).toBeCalledWith('0') expect(toast.success).toBeCalledWith('0')
expect(wrapper.text()).toContain('admin.enablePlugin') expect(checkbox.attributes('checked')).toBeFalsy()
}) })
test('delete plugin', async () => { test('delete plugin', async () => {
@ -133,8 +87,7 @@ test('delete plugin', async () => {
{ {
name: 'a', name: 'a',
title: 'My Plugin', title: 'My Plugin',
dependencies: { all: {}, unsatisfied: {} }, icon: {},
enabled: false,
}, },
]) ])
Vue.prototype.$http.post Vue.prototype.$http.post
@ -145,8 +98,7 @@ test('delete plugin', async () => {
.mockResolvedValue({ value: '' }) .mockResolvedValue({ value: '' })
const wrapper = mount(Plugins) const wrapper = mount(Plugins)
await flushPromises() await flushPromises()
const button = wrapper.find('.actions').findAll('a') const button = wrapper.find('.plugin-actions a')
.at(1)
button.trigger('click') button.trigger('click')
await flushPromises() await flushPromises()
@ -167,7 +119,7 @@ test('delete plugin', async () => {
button.trigger('click') button.trigger('click')
await flushPromises() await flushPromises()
expect(wrapper.text()).toContain('No data') expect(wrapper.text()).not.toContain('My Plugin')
}) })
test('readme link', async () => { test('readme link', async () => {
@ -175,13 +127,12 @@ test('readme link', async () => {
{ {
name: 'a', name: 'a',
readme: true, readme: true,
dependencies: { all: {}, unsatisfied: {} }, icon: {},
}, },
]) ])
const wrapper = mount(Plugins) const wrapper = mount(Plugins)
await flushPromises() await flushPromises()
const link = wrapper.find('.actions > a:nth-child(1)') const link = wrapper.find('.plugin-actions > a:nth-child(1)')
expect(link.text()).toContain('admin.pluginReadme')
expect(link.attributes('href')).toBe('/admin/plugins/readme/a') expect(link.attributes('href')).toBe('/admin/plugins/readme/a')
}) })

View File

@ -36,6 +36,7 @@
- Respond with unformatted Profile JSON to reduce bytes. - Respond with unformatted Profile JSON to reduce bytes.
- Switched to a new PHP texture renderer. - Switched to a new PHP texture renderer.
- Display 3D avatar of player when applying texture to player. - Display 3D avatar of player when applying texture to player.
- New "Plugins Management" page.
## Fixed ## Fixed

View File

@ -36,6 +36,7 @@
- 响应未格式化过的 Profile JSON 以节省流量 - 响应未格式化过的 Profile JSON 以节省流量
- 使用新的 PHP 材质渲染器 - 使用新的 PHP 材质渲染器
- 将材质应用到角色时显示角色的 3D 头像 - 将材质应用到角色时显示角色的 3D 头像
- 新的「插件管理」页面
## 修复 ## 修复

View File

@ -18,8 +18,7 @@ class PluginControllerTest extends TestCase
public function testShowManage() public function testShowManage()
{ {
$this->get('/admin/plugins/manage') $this->get('/admin/plugins/manage')->assertSee(trans('general.plugin-manage'));
->assertSee(trans('general.plugin-manage'));
} }
public function testConfig() public function testConfig()
@ -250,14 +249,6 @@ class PluginControllerTest extends TestCase
'version' => '0.0.0', 'version' => '0.0.0',
'title' => '', 'title' => '',
])])); ])]));
$mock->shouldReceive('getUnsatisfied')
->withArgs(function ($plugin) {
$this->assertEquals('a', $plugin->name);
return true;
})
->once()
->andReturn(collect(['b' => null]));
}); });
$this->getJson('/admin/plugins/data') $this->getJson('/admin/plugins/data')
->assertJsonStructure([ ->assertJsonStructure([
@ -266,12 +257,9 @@ class PluginControllerTest extends TestCase
'version', 'version',
'title', 'title',
'description', 'description',
'author',
'url',
'enabled', 'enabled',
'config', 'config',
'readme', 'readme',
'dependencies',
], ],
]); ]);
} }

View File

@ -20,9 +20,7 @@ const config = {
'admin-lte/dist/css/alt/adminlte.components.min.css', 'admin-lte/dist/css/alt/adminlte.components.min.css',
'admin-lte/dist/css/alt/adminlte.extra-components.min.css', 'admin-lte/dist/css/alt/adminlte.extra-components.min.css',
'admin-lte/dist/css/alt/adminlte.pages.min.css', 'admin-lte/dist/css/alt/adminlte.pages.min.css',
'@fortawesome/fontawesome-free/css/fontawesome.min.css', '@fortawesome/fontawesome-free/css/all.min.css',
'@fortawesome/fontawesome-free/css/regular.min.css',
'@fortawesome/fontawesome-free/css/solid.min.css',
'./resources/assets/src/styles/common.styl', './resources/assets/src/styles/common.styl',
], ],
setup: './resources/assets/src/styles/setup.styl', setup: './resources/assets/src/styles/setup.styl',