Skip to content

Commit 1a202ba

Browse files
committed
feat: enhance contribution management with generic visibility and disabled state handling
1 parent 8b6a833 commit 1a202ba

6 files changed

Lines changed: 137 additions & 19 deletions

File tree

packages/core/src/components/command.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,17 @@ import { customElement, property, state } from 'lit/decorators.js'
33
import { DocksWidget } from '../widgets/widget'
44
import { icon } from '../core/icon-utils'
55
import { keyBindingManager } from '../core/keybindings'
6-
import { contributionRegistry, Contribution, CommandContribution, HTMLContribution, ContributionChangeEvent, TOPIC_CONTRIBUTEIONS_CHANGED } from '../core/contributionregistry'
6+
import {
7+
contributionRegistry,
8+
Contribution,
9+
CommandContribution,
10+
HTMLContribution,
11+
ContributionChangeEvent,
12+
TOPIC_CONTRIBUTEIONS_CHANGED,
13+
getContributionDisabled,
14+
getContributionVisible,
15+
} from '../core/contributionregistry'
716
import { subscribe } from '../core/events'
8-
import { Signal } from '@lit-labs/signals'
917
import { unsafeHTML } from 'lit/directives/unsafe-html.js'
1018

1119
@customElement('docks-command')
@@ -114,7 +122,10 @@ export class DocksCommand extends DocksWidget {
114122
private renderContribution(contribution: Contribution) {
115123
if ('command' in contribution) {
116124
const commandContribution = contribution as CommandContribution
117-
const disabled = (commandContribution.disabled as Signal.Computed<boolean>)?.get()
125+
if (!getContributionVisible(commandContribution)) {
126+
return nothing
127+
}
128+
const disabled = getContributionDisabled(commandContribution)
118129
return html`
119130
<docks-command
120131
cmd="${commandContribution.command}"

packages/core/src/core/commandregistry.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,7 @@ rootContext.put("commandRegistry", commandRegistry);
225225
export interface RegisterOptions {
226226
command: Command,
227227
handler?: Handler,
228-
contribution?: Contribution & Partial<Pick<CommandContribution, 'disabled'>>
228+
contribution?: Contribution
229229
}
230230

231231
export const registerAll = (options: RegisterOptions) => {

packages/core/src/core/contributionregistry.ts

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,45 @@ export interface Contribution {
1616
name?: string;
1717
target?: string;
1818
label: string;
19+
/** When true, UI may show `label` alongside the icon (e.g. toolbar). Honored where supported in UI. */
20+
showLabel?: boolean;
1921
icon?: string;
2022
slot?: string;
23+
disabled?: (() => boolean) | Signal.Computed<boolean>;
24+
/** When false, the contribution is not rendered (default: shown). Honored where supported in UI. */
25+
visible?: (() => boolean) | Signal.Computed<boolean>;
2126
}
2227

2328
export interface CommandContribution extends Contribution {
2429
command: string;
2530
params?: Record<string, any>;
26-
showLabel?: boolean;
27-
disabled?: (() => boolean) | Signal.Computed<boolean>;
31+
}
32+
33+
export function isCommandContribution(contribution: Contribution): contribution is CommandContribution {
34+
return "command" in contribution;
35+
}
36+
37+
/** Default true when `visible` is omitted. */
38+
export function getContributionVisible(contribution: Contribution): boolean {
39+
const visible = contribution.visible as Signal.Computed<boolean> | undefined;
40+
if (!visible) return true;
41+
return visible.get() !== false;
42+
}
43+
44+
/** Default false when `disabled` is omitted. */
45+
export function getContributionDisabled(contribution: Contribution): boolean {
46+
const disabled = contribution.disabled as Signal.Computed<boolean> | undefined;
47+
if (!disabled) return false;
48+
return disabled.get() === true;
49+
}
50+
51+
function wrapContributionReactiveFlags(contribution: Contribution): void {
52+
if (contribution.disabled instanceof Function) {
53+
contribution.disabled = new Signal.Computed<boolean>(contribution.disabled);
54+
}
55+
if (contribution.visible instanceof Function) {
56+
contribution.visible = new Signal.Computed<boolean>(contribution.visible);
57+
}
2858
}
2959

3060
export interface HTMLContribution extends Contribution {
@@ -71,12 +101,7 @@ class ContributionRegistry {
71101

72102
registerContribution<T extends Contribution>(target: string, contribution: T) {
73103
const targetSlot = this.getOrCreateSlot(target);
74-
if ("command" in contribution) {
75-
const cmd = contribution as unknown as CommandContribution
76-
if (cmd.disabled instanceof Function) {
77-
cmd.disabled = new Signal.Computed<boolean>(cmd.disabled)
78-
}
79-
}
104+
wrapContributionReactiveFlags(contribution);
80105
targetSlot.push(contribution);
81106
publish(TOPIC_CONTRIBUTEIONS_CHANGED, { target, contributions: targetSlot } as ContributionChangeEvent);
82107

packages/core/src/parts/contextmenu.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,11 @@ import {
66
Contribution,
77
ContributionChangeEvent,
88
contributionRegistry,
9+
getContributionDisabled,
910
HTMLContribution,
11+
getContributionVisible,
1012
TOPIC_CONTRIBUTEIONS_CHANGED
1113
} from "../core/contributionregistry";
12-
import {Signal} from '@lit-labs/signals';
1314
import {unsafeHTML} from "lit/directives/unsafe-html.js";
1415
import {subscribe} from "../core/events";
1516
import {createRef, ref} from "lit/directives/ref.js";
@@ -217,7 +218,10 @@ export class DocksContextMenu extends DocksElement {
217218
private renderContribution(contribution: Contribution) {
218219
if ("command" in contribution) {
219220
const commandContribution = contribution as CommandContribution;
220-
const disabled = (commandContribution.disabled as Signal.Computed<boolean>)?.get();
221+
if (!getContributionVisible(commandContribution)) {
222+
return nothing;
223+
}
224+
const disabled = getContributionDisabled(commandContribution);
221225
return html`
222226
<docks-command
223227
cmd="${commandContribution.command}"

packages/core/src/parts/toolbar.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,11 @@ import {
77
Contribution,
88
ContributionChangeEvent,
99
contributionRegistry,
10+
getContributionDisabled,
1011
HTMLContribution,
12+
getContributionVisible,
1113
TOPIC_CONTRIBUTEIONS_CHANGED
1214
} from "../core/contributionregistry";
13-
import {Signal} from '@lit-labs/signals';
1415
import {unsafeHTML} from "lit/directives/unsafe-html.js";
1516
import {subscribe} from "../core/events";
1617
import { icon } from "../core/icon-utils";
@@ -28,7 +29,8 @@ function renderButtonGroup(
2829
) {
2930
const slot = slotName ?? 'default';
3031
const label = `Toolbar ${slot}`;
31-
const items = contributions.filter(c => c.slot === slotName && isToolbarItem(c));
32+
const items = contributions.filter(c =>
33+
c.slot === slotName && isToolbarItem(c) && getContributionVisible(c));
3234
const slotHtml = slotName === 'start'
3335
? html`<slot name="start"></slot>`
3436
: slotName === 'end'
@@ -211,7 +213,7 @@ export class DocksToolbar extends DocksElement {
211213
return html`
212214
<wa-button @click=${() => void this.executeCommand(commandContribution.command, commandContribution.params || {})}
213215
title=${commandContribution.label}
214-
?disabled="${(commandContribution.disabled as Signal.Computed<boolean>)?.get()}"
216+
?disabled="${getContributionDisabled(commandContribution)}"
215217
appearance="plain" size=${this.size}>
216218
${icon(commandContribution.icon, { label: commandContribution.label })}
217219
${showLabel ? commandContribution.label : ''}

packages/core/test/units/contributionregistry.test.ts

Lines changed: 78 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import {
1111
TOPIC_CONTRIBUTEIONS_CHANGED,
1212
type Contribution,
1313
type CommandContribution,
14+
getContributionVisible,
15+
getContributionDisabled,
1416
} from '../../src/core/contributionregistry';
1517

1618
describe('ContributionRegistry', () => {
@@ -36,7 +38,7 @@ describe('ContributionRegistry', () => {
3638
expect(list[1].label).toBe('Second');
3739
});
3840

39-
it('converts disabled function to Signal.Computed for CommandContribution', () => {
41+
it('converts disabled function to Signal.Computed', () => {
4042
const target = 'test-disabled-' + Math.random();
4143
const contribution: CommandContribution = {
4244
label: 'With disabled',
@@ -49,7 +51,7 @@ describe('ContributionRegistry', () => {
4951
expect((contribution.disabled as { get(): boolean }).get()).toBe(true);
5052
});
5153

52-
it('leaves existing Signal.Computed disabled unchanged for CommandContribution', () => {
54+
it('leaves existing Signal.Computed disabled unchanged', () => {
5355
const target = 'test-computed-' + Math.random();
5456
const computed = new Signal.Computed(() => false);
5557
const contribution: CommandContribution = {
@@ -62,6 +64,80 @@ describe('ContributionRegistry', () => {
6264
expect((contribution.disabled as { get(): boolean }).get()).toBe(false);
6365
});
6466

67+
it('getContributionVisible returns true when visible is omitted', () => {
68+
const contribution: CommandContribution = {
69+
label: 'Always visible',
70+
command: 'test.cmd',
71+
};
72+
expect(getContributionVisible(contribution)).toBe(true);
73+
});
74+
75+
it('getContributionVisible reflects Signal.Computed visible', () => {
76+
const contribution: CommandContribution = {
77+
label: 'Conditional',
78+
command: 'test.cmd',
79+
visible: new Signal.Computed(() => false),
80+
};
81+
expect(getContributionVisible(contribution)).toBe(false);
82+
});
83+
84+
it('getContributionDisabled returns false when disabled is omitted', () => {
85+
const contribution: CommandContribution = {
86+
label: 'Enabled',
87+
command: 'test.cmd',
88+
};
89+
expect(getContributionDisabled(contribution)).toBe(false);
90+
});
91+
92+
it('getContributionDisabled reflects Signal.Computed disabled', () => {
93+
const contribution: CommandContribution = {
94+
label: 'Disabled',
95+
command: 'test.cmd',
96+
disabled: new Signal.Computed(() => true),
97+
};
98+
expect(getContributionDisabled(contribution)).toBe(true);
99+
});
100+
101+
it('converts visible/disabled functions for any contribution', () => {
102+
const target = 'test-non-command-' + Math.random();
103+
const contribution: Contribution = {
104+
label: 'Generic',
105+
visible: () => false,
106+
disabled: () => true,
107+
};
108+
contributionRegistry.registerContribution(target, contribution);
109+
expect(contribution.visible).not.toBeInstanceOf(Function);
110+
expect(contribution.disabled).not.toBeInstanceOf(Function);
111+
expect((contribution.visible as { get(): boolean }).get()).toBe(false);
112+
expect((contribution.disabled as { get(): boolean }).get()).toBe(true);
113+
});
114+
115+
it('converts visible function to Signal.Computed', () => {
116+
const target = 'test-visible-' + Math.random();
117+
const contribution: CommandContribution = {
118+
label: 'With visible',
119+
command: 'test.cmd',
120+
visible: () => false,
121+
};
122+
contributionRegistry.registerContribution(target, contribution);
123+
expect(contribution.visible).toBeDefined();
124+
expect(contribution.visible).not.toBeInstanceOf(Function);
125+
expect((contribution.visible as { get(): boolean }).get()).toBe(false);
126+
});
127+
128+
it('leaves existing Signal.Computed visible unchanged', () => {
129+
const target = 'test-visible-computed-' + Math.random();
130+
const computed = new Signal.Computed(() => true);
131+
const contribution: CommandContribution = {
132+
label: 'Already computed visible',
133+
command: 'test.cmd',
134+
visible: computed,
135+
};
136+
contributionRegistry.registerContribution(target, contribution);
137+
expect(contribution.visible).toBe(computed);
138+
expect((contribution.visible as { get(): boolean }).get()).toBe(true);
139+
});
140+
65141
describe('contribution target remapping', () => {
66142
it('getContributions(slotB) returns contribution registered to slotA when remapped to slotB', () => {
67143
const slotA = 'test-remap-a-' + Math.random();

0 commit comments

Comments
 (0)