feat: add plugin manager filters
This commit is contained in:
parent
ddcd799f16
commit
8be107013d
|
|
@ -33,6 +33,9 @@ test.describe('Desktop localization', () => {
|
||||||
await expect(pluginManager).toContainText('Зарегистрировано возможностей:');
|
await expect(pluginManager).toContainText('Зарегистрировано возможностей:');
|
||||||
await expect(pluginManager.locator('.registry-section')).toContainText('Реестр возможностей');
|
await expect(pluginManager.locator('.registry-section')).toContainText('Реестр возможностей');
|
||||||
await expect(pluginManager.locator('.registry-section')).toContainText('Возможность');
|
await expect(pluginManager.locator('.registry-section')).toContainText('Возможность');
|
||||||
|
await expect(pluginManager.locator('.plugin-filters')).toContainText('Фильтры');
|
||||||
|
await expect(pluginManager.locator('[data-plugin-filter-results]')).toContainText('Показано плагинов:');
|
||||||
|
await expect(pluginManager.locator('[data-plugin-filter="status"]')).toContainText('Выключенные');
|
||||||
await expect(pluginManager).not.toContainText('Workspace-scoped');
|
await expect(pluginManager).not.toContainText('Workspace-scoped');
|
||||||
|
|
||||||
await page.reload();
|
await page.reload();
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { test, expect } from '@playwright/test';
|
import { test, expect } from '@playwright/test';
|
||||||
import { waitForAppReady, setupConsoleCollector, resetMockState, openPluginManager } from './helpers.js';
|
import { waitForAppReady, setupConsoleCollector, resetMockState, openPluginManager, setPluginStatus } from './helpers.js';
|
||||||
|
|
||||||
test.describe('E: Plugin Manager layout', () => {
|
test.describe('E: Plugin Manager layout', () => {
|
||||||
let consoleCollector;
|
let consoleCollector;
|
||||||
|
|
@ -88,6 +88,50 @@ test.describe('E: Plugin Manager layout', () => {
|
||||||
await expect(risk).toContainText('elevated permissions');
|
await expect(risk).toContainText('elevated permissions');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('plugin manager filters by state, declared permissions, capabilities, settings and source', async ({ page }) => {
|
||||||
|
await setPluginStatus(page, 'verstak.todo', 'disabled', false);
|
||||||
|
await page.evaluate(() => window.__wailsMock.addSyntheticPlugins(1, 'third-party'));
|
||||||
|
await openPluginManager(page);
|
||||||
|
|
||||||
|
const results = page.locator('[data-plugin-filter-results]');
|
||||||
|
await expect(results).toContainText('Showing');
|
||||||
|
|
||||||
|
await page.locator('[data-plugin-filter="status"]').selectOption('disabled');
|
||||||
|
await expect(page.locator('.plugin-card')).toHaveCount(1);
|
||||||
|
await expect(page.locator('.plugin-card')).toContainText('Todos');
|
||||||
|
await page.locator('button.reload-btn').click();
|
||||||
|
await expect(page.locator('[data-plugin-filter="status"]')).toHaveValue('disabled');
|
||||||
|
await expect(page.locator('.plugin-card')).toHaveCount(1);
|
||||||
|
|
||||||
|
await page.locator('[data-plugin-filter-reset]').click();
|
||||||
|
await page.locator('[data-plugin-filter-permission="notifications.schedule"]').check();
|
||||||
|
await expect(page.locator('.plugin-card')).toHaveCount(1);
|
||||||
|
await expect(page.locator('.plugin-card')).toContainText('Todos');
|
||||||
|
await page.locator('[data-plugin-filter-permission="secrets.read"]').check();
|
||||||
|
await expect(page.locator('.plugin-card')).toHaveCount(2);
|
||||||
|
await page.locator('[data-plugin-filter-permission="secrets.read"]').uncheck();
|
||||||
|
|
||||||
|
await page.locator('[data-plugin-filter-reset]').click();
|
||||||
|
await page.locator('[data-plugin-filter-capability="verstak/core/notifications/v1"]').check();
|
||||||
|
await expect(page.locator('.plugin-card')).toHaveCount(1);
|
||||||
|
await expect(page.locator('.plugin-card')).toContainText('Todos');
|
||||||
|
await page.locator('[data-plugin-filter-capability="secret-store"]').check();
|
||||||
|
await expect(page.locator('.plugin-card')).toHaveCount(2);
|
||||||
|
await page.locator('[data-plugin-filter-capability="secret-store"]').uncheck();
|
||||||
|
|
||||||
|
await page.locator('[data-plugin-filter-reset]').click();
|
||||||
|
await page.locator('[data-plugin-filter="settings"]').selectOption('with');
|
||||||
|
await expect(page.locator('.plugin-card').filter({ hasText: 'Platform Test' })).toBeVisible();
|
||||||
|
await expect(page.locator('.plugin-card').filter({ hasText: 'Default Editor' })).toHaveCount(0);
|
||||||
|
|
||||||
|
await page.locator('[data-plugin-filter="source"]').selectOption('third-party');
|
||||||
|
await expect(page.locator('.plugin-card')).toHaveCount(0);
|
||||||
|
await expect(page.locator('[data-plugin-filter-empty]')).toBeVisible();
|
||||||
|
|
||||||
|
await page.locator('[data-plugin-filter-reset]').click();
|
||||||
|
await expect(page.locator('.plugin-card')).toHaveCount(13);
|
||||||
|
});
|
||||||
|
|
||||||
test('workspace selection keeps exactly one active node', async ({ page }) => {
|
test('workspace selection keeps exactly one active node', async ({ page }) => {
|
||||||
const selected = page.locator('.wt-node.selected .wt-label');
|
const selected = page.locator('.wt-node.selected .wt-label');
|
||||||
await expect(selected).toHaveCount(1);
|
await expect(selected).toHaveCount(1);
|
||||||
|
|
@ -118,15 +162,15 @@ test.describe('E: Plugin Manager layout', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('workspace sidebar creates renames and trashes top-level workspaces', async ({ page }) => {
|
test('workspace sidebar creates renames and trashes top-level workspaces', async ({ page }) => {
|
||||||
await page.locator('button[title="New workspace"]').click();
|
await page.locator('button[title="New Deal"]').click();
|
||||||
const modal = page.locator('[data-workspace-create-modal]');
|
const modal = page.locator('[data-workspace-create-modal]');
|
||||||
await modal.locator('[data-workspace-name]').fill('ClientA');
|
await modal.locator('[data-workspace-name]').fill('ClientA');
|
||||||
await modal.getByRole('button', { name: 'Create workspace' }).click();
|
await modal.getByRole('button', { name: 'Create Deal' }).click();
|
||||||
|
|
||||||
await expect(page.locator('.wt-label').filter({ hasText: 'ClientA' })).toBeVisible();
|
await expect(page.locator('.wt-label').filter({ hasText: 'ClientA' })).toBeVisible();
|
||||||
|
|
||||||
const client = page.locator('.wt-node').filter({ hasText: 'ClientA' });
|
const client = page.locator('.wt-node').filter({ hasText: 'ClientA' });
|
||||||
await client.locator('button[title="Rename workspace"]').click();
|
await client.locator('button[title="Rename Deal"]').click();
|
||||||
await page.locator('.wt-rename').fill('ClientB');
|
await page.locator('.wt-rename').fill('ClientB');
|
||||||
await page.locator('button[title="Save rename"]').click();
|
await page.locator('button[title="Save rename"]').click();
|
||||||
|
|
||||||
|
|
@ -134,7 +178,7 @@ test.describe('E: Plugin Manager layout', () => {
|
||||||
await expect(page.locator('.wt-label').filter({ hasText: 'ClientA' })).toHaveCount(0);
|
await expect(page.locator('.wt-label').filter({ hasText: 'ClientA' })).toHaveCount(0);
|
||||||
|
|
||||||
const renamed = page.locator('.wt-node').filter({ hasText: 'ClientB' });
|
const renamed = page.locator('.wt-node').filter({ hasText: 'ClientB' });
|
||||||
await renamed.locator('button[title="Trash workspace"]').click();
|
await renamed.locator('button[title="Move Deal to trash"]').click();
|
||||||
|
|
||||||
await expect(page.locator('.wt-label').filter({ hasText: 'ClientB' })).toHaveCount(0);
|
await expect(page.locator('.wt-label').filter({ hasText: 'ClientB' })).toHaveCount(0);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,24 @@ export default {
|
||||||
'pluginManager.health': 'Plugin Health',
|
'pluginManager.health': 'Plugin Health',
|
||||||
'pluginManager.permissionRisk': 'Permission Risk',
|
'pluginManager.permissionRisk': 'Permission Risk',
|
||||||
'pluginManager.elevatedPermissions': '{count} plugins request elevated permissions',
|
'pluginManager.elevatedPermissions': '{count} plugins request elevated permissions',
|
||||||
|
'pluginManager.filters': 'Filters',
|
||||||
|
'pluginManager.filterResults': 'Showing {visible} of {total} plugins',
|
||||||
|
'pluginManager.filterReset': 'Reset filters',
|
||||||
|
'pluginManager.filterState': 'State',
|
||||||
|
'pluginManager.filterAll': 'All',
|
||||||
|
'pluginManager.filterEnabled': 'Enabled',
|
||||||
|
'pluginManager.filterDisabled': 'Disabled',
|
||||||
|
'pluginManager.filterFailed': 'Problem',
|
||||||
|
'pluginManager.filterDegraded': 'Degraded',
|
||||||
|
'pluginManager.filterPermissions': 'Permissions',
|
||||||
|
'pluginManager.filterCapabilities': 'Capabilities',
|
||||||
|
'pluginManager.filterSettings': 'Settings',
|
||||||
|
'pluginManager.filterWithSettings': 'With settings',
|
||||||
|
'pluginManager.filterWithoutSettings': 'Without settings',
|
||||||
|
'pluginManager.filterSource': 'Source',
|
||||||
|
'pluginManager.filterOfficial': 'Official',
|
||||||
|
'pluginManager.filterThirdParty': 'Third-party or local',
|
||||||
|
'pluginManager.filterEmpty': 'No plugins match the selected filters.',
|
||||||
'pluginManager.none': 'No plugins found',
|
'pluginManager.none': 'No plugins found',
|
||||||
'pluginManager.scannedDirs': 'Plugin directories scanned:',
|
'pluginManager.scannedDirs': 'Plugin directories scanned:',
|
||||||
'pluginManager.userPlugins': 'user plugins',
|
'pluginManager.userPlugins': 'user plugins',
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,24 @@ export default {
|
||||||
'pluginManager.health': 'Состояние плагинов',
|
'pluginManager.health': 'Состояние плагинов',
|
||||||
'pluginManager.permissionRisk': 'Риск разрешений',
|
'pluginManager.permissionRisk': 'Риск разрешений',
|
||||||
'pluginManager.elevatedPermissions': 'Плагинов с повышенными разрешениями: {count}',
|
'pluginManager.elevatedPermissions': 'Плагинов с повышенными разрешениями: {count}',
|
||||||
|
'pluginManager.filters': 'Фильтры',
|
||||||
|
'pluginManager.filterResults': 'Показано плагинов: {visible} из {total}',
|
||||||
|
'pluginManager.filterReset': 'Сбросить фильтры',
|
||||||
|
'pluginManager.filterState': 'Состояние',
|
||||||
|
'pluginManager.filterAll': 'Все',
|
||||||
|
'pluginManager.filterEnabled': 'Включённые',
|
||||||
|
'pluginManager.filterDisabled': 'Выключенные',
|
||||||
|
'pluginManager.filterFailed': 'С проблемами',
|
||||||
|
'pluginManager.filterDegraded': 'Ограниченные',
|
||||||
|
'pluginManager.filterPermissions': 'Разрешения',
|
||||||
|
'pluginManager.filterCapabilities': 'Возможности',
|
||||||
|
'pluginManager.filterSettings': 'Настройки',
|
||||||
|
'pluginManager.filterWithSettings': 'С настройками',
|
||||||
|
'pluginManager.filterWithoutSettings': 'Без настроек',
|
||||||
|
'pluginManager.filterSource': 'Источник',
|
||||||
|
'pluginManager.filterOfficial': 'Официальные',
|
||||||
|
'pluginManager.filterThirdParty': 'Сторонние или локальные',
|
||||||
|
'pluginManager.filterEmpty': 'Нет плагинов, соответствующих выбранным фильтрам.',
|
||||||
'pluginManager.none': 'Плагины не найдены',
|
'pluginManager.none': 'Плагины не найдены',
|
||||||
'pluginManager.scannedDirs': 'Проверенные каталоги плагинов:',
|
'pluginManager.scannedDirs': 'Проверенные каталоги плагинов:',
|
||||||
'pluginManager.userPlugins': 'пользовательские плагины',
|
'pluginManager.userPlugins': 'пользовательские плагины',
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,11 @@
|
||||||
let reloading = false;
|
let reloading = false;
|
||||||
let toastMessage = '';
|
let toastMessage = '';
|
||||||
let toastType = 'success'; // 'success' | 'error' | 'info'
|
let toastType = 'success'; // 'success' | 'error' | 'info'
|
||||||
|
let statusFilter = 'all';
|
||||||
|
let permissionFilters = [];
|
||||||
|
let capabilityFilters = [];
|
||||||
|
let settingsFilter = 'all';
|
||||||
|
let sourceFilter = 'all';
|
||||||
|
|
||||||
export let activeSettingsPluginId = '';
|
export let activeSettingsPluginId = '';
|
||||||
export let activeSettingsPanelId = '';
|
export let activeSettingsPanelId = '';
|
||||||
|
|
@ -256,6 +261,11 @@
|
||||||
$: totalPerms = permissions.length;
|
$: totalPerms = permissions.length;
|
||||||
$: statusSummary = computeStatusSummary(plugins);
|
$: statusSummary = computeStatusSummary(plugins);
|
||||||
$: elevatedPermissionPluginCount = computeElevatedPermissionPluginCount(plugins, permissions);
|
$: elevatedPermissionPluginCount = computeElevatedPermissionPluginCount(plugins, permissions);
|
||||||
|
$: filterPermissions = collectManifestValues(plugins, 'permissions');
|
||||||
|
$: filterCapabilities = collectCapabilities(plugins);
|
||||||
|
$: hasReliableSourceMetadata = plugins.some((plugin) => sourceGroup(plugin) !== '');
|
||||||
|
$: visiblePlugins = filterPlugins(plugins, statusFilter, permissionFilters, capabilityFilters, settingsFilter, sourceFilter, contributions);
|
||||||
|
$: filtersActive = statusFilter !== 'all' || permissionFilters.length > 0 || capabilityFilters.length > 0 || settingsFilter !== 'all' || sourceFilter !== 'all';
|
||||||
|
|
||||||
function computeStatusSummary(pluginRows) {
|
function computeStatusSummary(pluginRows) {
|
||||||
return pluginRows.reduce((summary, plugin) => {
|
return pluginRows.reduce((summary, plugin) => {
|
||||||
|
|
@ -274,6 +284,69 @@
|
||||||
return (pluginRows || []).filter(plugin => (plugin?.manifest?.permissions || []).some(permission => dangerous.has(permission))).length;
|
return (pluginRows || []).filter(plugin => (plugin?.manifest?.permissions || []).some(permission => dangerous.has(permission))).length;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function collectManifestValues(pluginRows, key) {
|
||||||
|
return Array.from(new Set((pluginRows || []).flatMap((plugin) => plugin?.manifest?.[key] || [])))
|
||||||
|
.sort((left, right) => left.localeCompare(right));
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectCapabilities(pluginRows) {
|
||||||
|
return Array.from(new Set((pluginRows || []).flatMap((plugin) => [
|
||||||
|
...(plugin?.manifest?.provides || []),
|
||||||
|
...(plugin?.manifest?.requires || []),
|
||||||
|
...(plugin?.manifest?.optionalRequires || []),
|
||||||
|
]))).sort((left, right) => left.localeCompare(right));
|
||||||
|
}
|
||||||
|
|
||||||
|
function pluginStatusGroup(plugin) {
|
||||||
|
const status = plugin?.status || '';
|
||||||
|
if (status === 'failed' || status === 'incompatible' || status === 'missing-required-capability') return 'failed';
|
||||||
|
if (status === 'degraded') return 'degraded';
|
||||||
|
if (status === 'disabled' || plugin?.enabled === false) return 'disabled';
|
||||||
|
return 'enabled';
|
||||||
|
}
|
||||||
|
|
||||||
|
function sourceGroup(plugin) {
|
||||||
|
const source = plugin?.manifest?.source;
|
||||||
|
if (source === 'official') return 'official';
|
||||||
|
if (source === 'local' || source === 'third-party') return 'third-party';
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasSettings(plugin, activeContributions) {
|
||||||
|
const pluginId = plugin?.manifest?.id;
|
||||||
|
return Boolean(pluginId && (activeContributions.settingsPanels || []).some((panel) => panel.pluginId === pluginId));
|
||||||
|
}
|
||||||
|
|
||||||
|
function includesAny(values, expected) {
|
||||||
|
return expected.length === 0 || expected.some((value) => values.includes(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterPlugins(pluginRows, activeStatusFilter, activePermissionFilters, activeCapabilityFilters, activeSettingsFilter, activeSourceFilter, activeContributions) {
|
||||||
|
return pluginRows.filter((plugin) => matchesFilters(plugin, activeStatusFilter, activePermissionFilters, activeCapabilityFilters, activeSettingsFilter, activeSourceFilter, activeContributions));
|
||||||
|
}
|
||||||
|
|
||||||
|
function matchesFilters(plugin, activeStatusFilter, activePermissionFilters, activeCapabilityFilters, activeSettingsFilter, activeSourceFilter, activeContributions) {
|
||||||
|
if (activeStatusFilter !== 'all' && pluginStatusGroup(plugin) !== activeStatusFilter) return false;
|
||||||
|
if (!includesAny(plugin?.manifest?.permissions || [], activePermissionFilters)) return false;
|
||||||
|
const declaredCapabilities = [
|
||||||
|
...(plugin?.manifest?.provides || []),
|
||||||
|
...(plugin?.manifest?.requires || []),
|
||||||
|
...(plugin?.manifest?.optionalRequires || []),
|
||||||
|
];
|
||||||
|
if (!includesAny(declaredCapabilities, activeCapabilityFilters)) return false;
|
||||||
|
if (activeSettingsFilter === 'with' && !hasSettings(plugin, activeContributions)) return false;
|
||||||
|
if (activeSettingsFilter === 'without' && hasSettings(plugin, activeContributions)) return false;
|
||||||
|
return activeSourceFilter === 'all' || sourceGroup(plugin) === activeSourceFilter;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetFilters() {
|
||||||
|
statusFilter = 'all';
|
||||||
|
permissionFilters = [];
|
||||||
|
capabilityFilters = [];
|
||||||
|
settingsFilter = 'all';
|
||||||
|
sourceFilter = 'all';
|
||||||
|
}
|
||||||
|
|
||||||
function closeSettings() {
|
function closeSettings() {
|
||||||
settingsPanel = null;
|
settingsPanel = null;
|
||||||
settingsPluginId = '';
|
settingsPluginId = '';
|
||||||
|
|
@ -345,6 +418,70 @@
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<section class="plugin-filters" aria-label={tr('pluginManager.filters')}>
|
||||||
|
<div class="filter-header">
|
||||||
|
<div>
|
||||||
|
<h3>{tr('pluginManager.filters')}</h3>
|
||||||
|
<p data-plugin-filter-results>{tr('pluginManager.filterResults', { visible: visiblePlugins.length, total: totalPlugins })}</p>
|
||||||
|
</div>
|
||||||
|
<button class="filter-reset" data-plugin-filter-reset type="button" on:click={resetFilters} disabled={!filtersActive}>{tr('pluginManager.filterReset')}</button>
|
||||||
|
</div>
|
||||||
|
<div class="filter-grid">
|
||||||
|
<label class="filter-select-label">
|
||||||
|
<span>{tr('pluginManager.filterState')}</span>
|
||||||
|
<select class="filter-select" data-plugin-filter="status" bind:value={statusFilter}>
|
||||||
|
<option value="all">{tr('pluginManager.filterAll')}</option>
|
||||||
|
<option value="enabled">{tr('pluginManager.filterEnabled')}</option>
|
||||||
|
<option value="disabled">{tr('pluginManager.filterDisabled')}</option>
|
||||||
|
<option value="failed">{tr('pluginManager.filterFailed')}</option>
|
||||||
|
<option value="degraded">{tr('pluginManager.filterDegraded')}</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="filter-select-label">
|
||||||
|
<span>{tr('pluginManager.filterSettings')}</span>
|
||||||
|
<select class="filter-select" data-plugin-filter="settings" bind:value={settingsFilter}>
|
||||||
|
<option value="all">{tr('pluginManager.filterAll')}</option>
|
||||||
|
<option value="with">{tr('pluginManager.filterWithSettings')}</option>
|
||||||
|
<option value="without">{tr('pluginManager.filterWithoutSettings')}</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
{#if hasReliableSourceMetadata}
|
||||||
|
<label class="filter-select-label">
|
||||||
|
<span>{tr('pluginManager.filterSource')}</span>
|
||||||
|
<select class="filter-select" data-plugin-filter="source" bind:value={sourceFilter}>
|
||||||
|
<option value="all">{tr('pluginManager.filterAll')}</option>
|
||||||
|
<option value="official">{tr('pluginManager.filterOfficial')}</option>
|
||||||
|
<option value="third-party">{tr('pluginManager.filterThirdParty')}</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if filterPermissions.length > 0}
|
||||||
|
<fieldset class="filter-options">
|
||||||
|
<legend>{tr('pluginManager.filterPermissions')}</legend>
|
||||||
|
<div class="filter-option-list">
|
||||||
|
{#each filterPermissions as permission}
|
||||||
|
<label><input data-plugin-filter-permission={permission} type="checkbox" bind:group={permissionFilters} value={permission} /> <code>{permission}</code></label>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if filterCapabilities.length > 0}
|
||||||
|
<fieldset class="filter-options">
|
||||||
|
<legend>{tr('pluginManager.filterCapabilities')}</legend>
|
||||||
|
<div class="filter-option-list">
|
||||||
|
{#each filterCapabilities as capability}
|
||||||
|
<label><input data-plugin-filter-capability={capability} type="checkbox" bind:group={capabilityFilters} value={capability} /> <code>{capability}</code></label>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
{/if}
|
||||||
|
</section>
|
||||||
|
|
||||||
{#if plugins.length === 0 && missingInstalled.length === 0}
|
{#if plugins.length === 0 && missingInstalled.length === 0}
|
||||||
<div class="empty">
|
<div class="empty">
|
||||||
<div class="empty-icon">
|
<div class="empty-icon">
|
||||||
|
|
@ -360,9 +497,13 @@
|
||||||
</ul>
|
</ul>
|
||||||
<p class="hint">{tr('pluginManager.installHint')}</p>
|
<p class="hint">{tr('pluginManager.installHint')}</p>
|
||||||
</div>
|
</div>
|
||||||
|
{:else if visiblePlugins.length === 0}
|
||||||
|
<div class="empty filter-empty" data-plugin-filter-empty>
|
||||||
|
<p>{tr('pluginManager.filterEmpty')}</p>
|
||||||
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="plugin-list">
|
<div class="plugin-list">
|
||||||
{#each plugins as p}
|
{#each visiblePlugins as p}
|
||||||
<PluginCard {p} {capabilities} {permissions} {contributions} {vaultOpen} {actionFeedback} settingsPanels={(contributions.settingsPanels || []).filter(sp => sp.pluginId === p.manifest?.id)} onEnable={enablePlugin} onDisable={disablePlugin} />
|
<PluginCard {p} {capabilities} {permissions} {contributions} {vaultOpen} {actionFeedback} settingsPanels={(contributions.settingsPanels || []).filter(sp => sp.pluginId === p.manifest?.id)} onEnable={enablePlugin} onDisable={disablePlugin} />
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -577,6 +718,99 @@
|
||||||
color: #f4f7fb;
|
color: #f4f7fb;
|
||||||
font-size: 0.88rem;
|
font-size: 0.88rem;
|
||||||
}
|
}
|
||||||
|
.plugin-filters {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
padding: 0.75rem;
|
||||||
|
border: 1px solid #0f3460;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #121a2c;
|
||||||
|
}
|
||||||
|
.filter-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 0.75rem;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
.filter-header h3 {
|
||||||
|
margin: 0;
|
||||||
|
color: #e0e0f0;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
.filter-header p {
|
||||||
|
margin: 0.2rem 0 0;
|
||||||
|
color: #a0a0b8;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
}
|
||||||
|
.filter-reset {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
padding: 0.35rem 0.65rem;
|
||||||
|
border: 1px solid #533483;
|
||||||
|
border-radius: 5px;
|
||||||
|
background: #16213e;
|
||||||
|
color: #e0e0e0;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
}
|
||||||
|
.filter-reset:hover:not(:disabled) { background: #0f3460; }
|
||||||
|
.filter-reset:disabled { cursor: not-allowed; opacity: 0.5; }
|
||||||
|
.filter-grid {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.65rem;
|
||||||
|
}
|
||||||
|
.filter-select-label {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.28rem;
|
||||||
|
min-width: 10rem;
|
||||||
|
color: #a0a0b8;
|
||||||
|
font-size: 0.76rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.filter-select {
|
||||||
|
min-height: 2rem;
|
||||||
|
padding: 0.3rem 1.9rem 0.3rem 0.55rem;
|
||||||
|
border: 1px solid #0f3460;
|
||||||
|
border-radius: 5px;
|
||||||
|
background: #16213e;
|
||||||
|
color: #e0e0e0;
|
||||||
|
font: inherit;
|
||||||
|
}
|
||||||
|
.filter-select:focus { outline: 2px solid #533483; outline-offset: 1px; }
|
||||||
|
.filter-options {
|
||||||
|
min-width: 0;
|
||||||
|
margin: 0.75rem 0 0;
|
||||||
|
padding: 0.55rem 0.65rem 0.65rem;
|
||||||
|
border: 1px solid rgba(15, 52, 96, 0.85);
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
.filter-options legend {
|
||||||
|
padding: 0 0.25rem;
|
||||||
|
color: #a0a0b8;
|
||||||
|
font-size: 0.76rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.filter-option-list {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.4rem 0.75rem;
|
||||||
|
}
|
||||||
|
.filter-option-list label {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.28rem;
|
||||||
|
min-width: 0;
|
||||||
|
color: #c6c6d8;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
.filter-option-list input { accent-color: #6c4fa3; }
|
||||||
|
.filter-option-list code {
|
||||||
|
max-width: min(31rem, 72vw);
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.filter-empty { margin-bottom: 1.5rem; }
|
||||||
.empty {
|
.empty {
|
||||||
padding: 2rem; text-align: center; color: #a0a0b8;
|
padding: 2rem; text-align: center; color: #a0a0b8;
|
||||||
background: #16213e; border-radius: 8px; border: 1px dashed #0f3460;
|
background: #16213e; border-radius: 8px; border: 1px dashed #0f3460;
|
||||||
|
|
@ -651,6 +885,19 @@
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.filter-header {
|
||||||
|
align-items: stretch;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-reset {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-select-label {
|
||||||
|
flex: 1 1 10rem;
|
||||||
|
}
|
||||||
|
|
||||||
.modal {
|
.modal {
|
||||||
width: min(880px, calc(100vw - 2rem));
|
width: min(880px, calc(100vw - 2rem));
|
||||||
height: min(680px, calc(100vh - 2rem));
|
height: min(680px, calc(100vh - 2rem));
|
||||||
|
|
|
||||||
|
|
@ -4276,8 +4276,9 @@ import journalSource from '../../../../../verstak-official-plugins/plugins/journ
|
||||||
getPluginState: function (pluginId) {
|
getPluginState: function (pluginId) {
|
||||||
return pluginStates[pluginId] ? Object.assign({}, pluginStates[pluginId]) : null;
|
return pluginStates[pluginId] ? Object.assign({}, pluginStates[pluginId]) : null;
|
||||||
},
|
},
|
||||||
addSyntheticPlugins: function (count) {
|
addSyntheticPlugins: function (count, source) {
|
||||||
var total = Number(count || 0);
|
var total = Number(count || 0);
|
||||||
|
var pluginSource = source === 'official' || source === 'local' || source === 'third-party' ? source : 'third-party';
|
||||||
for (var i = 1; i <= total; i++) {
|
for (var i = 1; i <= total; i++) {
|
||||||
var id = 'verstak.synthetic-layout-' + String(i).padStart(2, '0');
|
var id = 'verstak.synthetic-layout-' + String(i).padStart(2, '0');
|
||||||
pluginStates[id] = {
|
pluginStates[id] = {
|
||||||
|
|
@ -4290,7 +4291,7 @@ import journalSource from '../../../../../verstak-official-plugins/plugins/journ
|
||||||
version: '0.0.' + i,
|
version: '0.0.' + i,
|
||||||
apiVersion: '0.1.0',
|
apiVersion: '0.1.0',
|
||||||
description: 'Synthetic plugin used by frontend layout tests.',
|
description: 'Synthetic plugin used by frontend layout tests.',
|
||||||
source: 'test',
|
source: pluginSource,
|
||||||
provides: ['verstak/synthetic-layout-' + i + '/v1'],
|
provides: ['verstak/synthetic-layout-' + i + '/v1'],
|
||||||
requires: [],
|
requires: [],
|
||||||
optionalRequires: [],
|
optionalRequires: [],
|
||||||
|
|
@ -4310,7 +4311,7 @@ import journalSource from '../../../../../verstak-official-plugins/plugins/journ
|
||||||
vaultPluginState.enabledPlugins.push(id);
|
vaultPluginState.enabledPlugins.push(id);
|
||||||
}
|
}
|
||||||
if (!vaultPluginState.desiredPlugins.some(function (p) { return p.id === id; })) {
|
if (!vaultPluginState.desiredPlugins.some(function (p) { return p.id === id; })) {
|
||||||
vaultPluginState.desiredPlugins.push({ id: id, version: '0.0.' + i, source: 'test' });
|
vaultPluginState.desiredPlugins.push({ id: id, version: '0.0.' + i, source: pluginSource });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue