mirror of
https://github.com/nexu-io/open-design.git
synced 2026-05-31 19:04:39 +07:00
feat(daemon, web): enhance plugin input handling and subcategory filtering
- Updated the `pickPluginFields` function to support legacy input aliases, improving compatibility with existing plugin structures. - Added tests to ensure the new input handling works correctly with legacy inputs when resolving plugin snapshots. - Enhanced CSS styles for subcategory elements in the plugin home section, improving visual clarity and user experience. - Introduced new tests for subcategory filtering within the active workflow lane, ensuring accurate plugin categorization. This update significantly improves the plugin management experience by enhancing input handling and refining the categorization system.
This commit is contained in:
parent
26d21a942e
commit
72ecc09326
5 changed files with 122 additions and 17 deletions
|
|
@ -121,9 +121,12 @@ function pickPluginFields(body: Record<string, unknown> | null | undefined) {
|
|||
&& body.appliedPluginSnapshotId.trim().length > 0
|
||||
? body.appliedPluginSnapshotId.trim()
|
||||
: undefined;
|
||||
const pluginInputs = body.pluginInputs && typeof body.pluginInputs === 'object'
|
||||
? (body.pluginInputs as Record<string, unknown>)
|
||||
: {};
|
||||
const pluginInputs =
|
||||
body.pluginInputs && typeof body.pluginInputs === 'object'
|
||||
? (body.pluginInputs as Record<string, unknown>)
|
||||
: body.inputs && typeof body.inputs === 'object'
|
||||
? (body.inputs as Record<string, unknown>)
|
||||
: {};
|
||||
const grantCaps = Array.isArray(body.grantCaps)
|
||||
? (body.grantCaps as unknown[])
|
||||
.filter((c): c is string => typeof c === 'string')
|
||||
|
|
|
|||
|
|
@ -342,6 +342,22 @@ describe('Plan §8 e2e — daemon-side anchors', () => {
|
|||
}
|
||||
});
|
||||
|
||||
it('accepts legacy inputs alias when resolving plugin snapshots', async () => {
|
||||
await installLocal(FIXTURE_DIR);
|
||||
|
||||
const result = resolvePluginSnapshot({
|
||||
db,
|
||||
body: { pluginId: 'sample-plugin', inputs: { topic: 'demo' } },
|
||||
projectId: 'project-1',
|
||||
registry: REGISTRY_VIEW,
|
||||
});
|
||||
|
||||
expect(result?.ok).toBe(true);
|
||||
if (result?.ok) {
|
||||
expect(result.snapshot.inputs).toMatchObject({ topic: 'demo' });
|
||||
}
|
||||
});
|
||||
|
||||
it('capabilitiesRequiredError envelope shape stays stable for code agents', () => {
|
||||
const err = capabilitiesRequiredError({
|
||||
pluginId: 'sample',
|
||||
|
|
|
|||
|
|
@ -251,6 +251,10 @@
|
|||
gap: 8px;
|
||||
padding: 4px 0;
|
||||
}
|
||||
.plugins-home__facet-row--sub {
|
||||
padding-top: 0;
|
||||
opacity: 0.92;
|
||||
}
|
||||
.plugins-home__facet-row--inline .plugins-home__facet-pills {
|
||||
flex: 1;
|
||||
flex-wrap: nowrap;
|
||||
|
|
@ -328,6 +332,10 @@
|
|||
color: white;
|
||||
opacity: 0.85;
|
||||
}
|
||||
.plugins-home__pill--sub-all {
|
||||
background: var(--bg-subtle);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.plugins-home__empty--filtered {
|
||||
border-style: solid;
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
// Facet derivation contract for the plugins-home filter row. The
|
||||
// home section is driven by a single curated workflow axis (Import /
|
||||
// Create / Export / Refine / Extend). These tests lock the per-record
|
||||
// category extraction, the catalog build (preserves curated order,
|
||||
// drops empty buckets), and the selection-based filtering so the
|
||||
// manifest fields the catalog depends on don't silently drift.
|
||||
// Create / Export / Refine / Extend) plus scoped subcategories inside
|
||||
// the active lane. These tests lock the per-record category extraction,
|
||||
// the catalog build (preserves curated order, drops empty buckets), and
|
||||
// the selection-based filtering so the manifest fields the catalog
|
||||
// depends on don't silently drift.
|
||||
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import type { InstalledPluginRecord } from '@open-design/contracts';
|
||||
|
|
@ -11,6 +12,7 @@ import {
|
|||
applyFacetSelection,
|
||||
buildFacetCatalog,
|
||||
extractCategories,
|
||||
extractSubcategories,
|
||||
isFeaturedPlugin,
|
||||
} from '../../src/components/plugins-home/facets';
|
||||
|
||||
|
|
@ -91,6 +93,31 @@ describe('extractCategories', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('extractSubcategories', () => {
|
||||
it('maps Create plugins to concrete accumulated buckets', () => {
|
||||
expect(extractSubcategories(fixture({ id: 'a', od: { mode: 'prototype' } }))).toEqual(['prototype']);
|
||||
expect(extractSubcategories(fixture({ id: 'b', od: { mode: 'deck' } }))).toEqual(['deck']);
|
||||
expect(extractSubcategories(fixture({ id: 'c', od: { mode: 'design-system' } }))).toEqual(['design-system']);
|
||||
expect(extractSubcategories(fixture({ id: 'd', tags: ['hyperframes'], od: { mode: 'video' } }))).toEqual(['hyperframes']);
|
||||
expect(extractSubcategories(fixture({ id: 'e', od: { mode: 'image' } }))).toEqual(['image']);
|
||||
});
|
||||
|
||||
it('maps Import and Export plugins to lane-scoped child buckets', () => {
|
||||
expect(
|
||||
extractSubcategories(fixture({ id: 'figma', od: { taskKind: 'figma-migration', mode: 'scenario' } })),
|
||||
).toEqual(['from-figma']);
|
||||
expect(
|
||||
extractSubcategories(fixture({ id: 'folder', od: { taskKind: 'code-migration', mode: 'scenario' } })),
|
||||
).toEqual(['from-code']);
|
||||
expect(
|
||||
extractSubcategories(fixture({ id: 'next-export', tags: ['export', 'nextjs', 'react'], od: { mode: 'export' } })),
|
||||
).toEqual(['nextjs']);
|
||||
expect(
|
||||
extractSubcategories(fixture({ id: 'react-export', tags: ['export', 'react'], od: { mode: 'export' } })),
|
||||
).toEqual(['react']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildFacetCatalog', () => {
|
||||
it('produces a single category axis with curated order preserved and empty buckets dropped', () => {
|
||||
const plugins = [
|
||||
|
|
@ -120,6 +147,15 @@ describe('buildFacetCatalog', () => {
|
|||
expect(catalog.category.find((o) => o.slug === 'export')?.count).toBe(1);
|
||||
expect(catalog.category.find((o) => o.slug === 'refine')?.count).toBe(1);
|
||||
expect(catalog.category.find((o) => o.slug === 'extend')?.count).toBe(1);
|
||||
expect(catalog.subcategory.create.map((o) => o.slug)).toEqual([
|
||||
'deck',
|
||||
'design-system',
|
||||
'hyperframes',
|
||||
'image',
|
||||
'video',
|
||||
]);
|
||||
expect(catalog.subcategory.import.map((o) => o.slug)).toEqual(['from-figma']);
|
||||
expect(catalog.subcategory.export.map((o) => o.slug)).toEqual(['react']);
|
||||
});
|
||||
|
||||
it('returns an empty category axis when no plugin matches a curated bucket', () => {
|
||||
|
|
@ -144,25 +180,37 @@ describe('applyFacetSelection', () => {
|
|||
|
||||
it('returns everything when no category is selected', () => {
|
||||
expect(
|
||||
applyFacetSelection(plugins, { category: null }).map((p) => p.id),
|
||||
applyFacetSelection(plugins, { category: null, subcategory: null }).map((p) => p.id),
|
||||
).toEqual(['a', 'b', 'c', 'd', 'e', 'f', 'g']);
|
||||
});
|
||||
|
||||
it('filters by the selected category slug', () => {
|
||||
expect(
|
||||
applyFacetSelection(plugins, { category: 'create' }).map((p) => p.id),
|
||||
applyFacetSelection(plugins, { category: 'create', subcategory: null }).map((p) => p.id),
|
||||
).toEqual(['a', 'b', 'c', 'd', 'e']);
|
||||
expect(
|
||||
applyFacetSelection(plugins, { category: 'export' }).map((p) => p.id),
|
||||
applyFacetSelection(plugins, { category: 'export', subcategory: null }).map((p) => p.id),
|
||||
).toEqual(['f']);
|
||||
expect(
|
||||
applyFacetSelection(plugins, { category: 'import' }).map((p) => p.id),
|
||||
applyFacetSelection(plugins, { category: 'import', subcategory: null }).map((p) => p.id),
|
||||
).toEqual(['g']);
|
||||
});
|
||||
|
||||
it('filters by the selected subcategory inside the selected category', () => {
|
||||
expect(
|
||||
applyFacetSelection(plugins, { category: 'create', subcategory: 'design-system' }).map((p) => p.id),
|
||||
).toEqual(['a']);
|
||||
expect(
|
||||
applyFacetSelection(plugins, { category: 'create', subcategory: 'hyperframes' }).map((p) => p.id),
|
||||
).toEqual(['e']);
|
||||
expect(
|
||||
applyFacetSelection(plugins, { category: 'export', subcategory: 'react' }).map((p) => p.id),
|
||||
).toEqual(['f']);
|
||||
});
|
||||
|
||||
it('returns an empty list when no plugin matches the selected category', () => {
|
||||
expect(
|
||||
applyFacetSelection(plugins, { category: 'refine' }).map((p) => p.id),
|
||||
applyFacetSelection(plugins, { category: 'refine', subcategory: null }).map((p) => p.id),
|
||||
).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2,16 +2,18 @@
|
|||
|
||||
// Plugins home section — UI contract.
|
||||
//
|
||||
// The section renders a single curated workflow bar (Import / Create /
|
||||
// Export / Refine / Extend). Picking a category filters the grid; the
|
||||
// All pill clears the category filter. A Featured chip sits orthogonal
|
||||
// to the row and overrides the category selection. This suite locks in:
|
||||
// The section renders a curated workflow bar (Import / Create / Export /
|
||||
// Refine / Extend) plus a scoped child row for the active lane. Picking
|
||||
// a category filters the grid; the All pill clears the category filter.
|
||||
// A Featured chip sits orthogonal to the row and overrides the category
|
||||
// selection. This suite locks in:
|
||||
//
|
||||
// 1. The category row renders with All + the curated buckets that
|
||||
// have at least one plugin.
|
||||
// 2. Picking a category filters the grid to plugins in that
|
||||
// bucket.
|
||||
// 3. Concrete create types do not duplicate the Create parent as tabs.
|
||||
// 3. Concrete create types appear in the scoped child row, not as
|
||||
// peers of Create.
|
||||
// 4. Featured chip overrides the category selection and only shows
|
||||
// curator-promoted plugins.
|
||||
|
||||
|
|
@ -99,6 +101,13 @@ describe('PluginsHomeSection (category bar)', () => {
|
|||
expect(screen.queryByTestId('plugins-home-pill-category-video')).toBeNull();
|
||||
expect(screen.queryByTestId('plugins-home-pill-category-image')).toBeNull();
|
||||
expect(screen.queryByTestId('plugins-home-pill-category-react')).toBeNull();
|
||||
expect(screen.getByTestId('plugins-home-row-subcategory-create')).toBeTruthy();
|
||||
expect(screen.getByTestId('plugins-home-pill-subcategory-create-prototype')).toBeTruthy();
|
||||
expect(screen.getByTestId('plugins-home-pill-subcategory-create-deck')).toBeTruthy();
|
||||
expect(screen.getByTestId('plugins-home-pill-subcategory-create-design-system')).toBeTruthy();
|
||||
expect(screen.getByTestId('plugins-home-pill-subcategory-create-hyperframes')).toBeTruthy();
|
||||
expect(screen.getByTestId('plugins-home-pill-subcategory-create-image')).toBeTruthy();
|
||||
expect(screen.getByTestId('plugins-home-pill-subcategory-create-video')).toBeTruthy();
|
||||
// Surface / Type / Scenario rows and the More disclosure are gone.
|
||||
expect(screen.queryByTestId('plugins-home-row-surface')).toBeNull();
|
||||
expect(screen.queryByTestId('plugins-home-row-type')).toBeNull();
|
||||
|
|
@ -165,6 +174,27 @@ describe('PluginsHomeSection (category bar)', () => {
|
|||
expect(items.map((i) => i.getAttribute('data-plugin-id'))).toEqual(['g']);
|
||||
});
|
||||
|
||||
it('subcategory pills filter within the active workflow lane', () => {
|
||||
render(
|
||||
<PluginsHomeSection
|
||||
plugins={sample}
|
||||
loading={false}
|
||||
activePluginId={null}
|
||||
pendingApplyId={null}
|
||||
onUse={() => {}}
|
||||
onOpenDetails={() => {}}
|
||||
/>,
|
||||
);
|
||||
fireEvent.click(screen.getByTestId('plugins-home-pill-subcategory-create-deck'));
|
||||
let items = within(screen.getByRole('list')).getAllByRole('listitem');
|
||||
expect(items.map((i) => i.getAttribute('data-plugin-id'))).toEqual(['f']);
|
||||
|
||||
fireEvent.click(screen.getByTestId('plugins-home-pill-subcategory-create-all'));
|
||||
fireEvent.click(screen.getByTestId('plugins-home-pill-subcategory-create-hyperframes'));
|
||||
items = within(screen.getByRole('list')).getAllByRole('listitem');
|
||||
expect(items.map((i) => i.getAttribute('data-plugin-id'))).toEqual(['e']);
|
||||
});
|
||||
|
||||
it('Extend separates plugin authoring from normal creation', () => {
|
||||
render(
|
||||
<PluginsHomeSection
|
||||
|
|
|
|||
Loading…
Reference in a new issue