@eclipse-docks/core 0.7.108 → 0.7.110

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eclipse-docks/core",
3
- "version": "0.7.108",
3
+ "version": "0.7.110",
4
4
  "description": "Eclipse Docks platform core: registries, services, parts, widgets, and API",
5
5
  "type": "module",
6
6
  "license": "EPL-2.0",
@@ -35,8 +35,14 @@ export {
35
35
  navigableInfoDialog,
36
36
  confirmDialog,
37
37
  promptDialog,
38
+ formDialog,
39
+ formDialogRequired,
40
+ validateFormDialogFields,
38
41
  filebrowserDialog,
39
- type NavigableDialogAction
42
+ type NavigableDialogAction,
43
+ type FormDialogField,
44
+ type FormDialogFieldType,
45
+ type FormDialogOptions,
40
46
  } from '../dialogs';
41
47
  export {
42
48
  toastInfo,
package/src/api/types.ts CHANGED
@@ -36,7 +36,10 @@ export type {
36
36
  export type {
37
37
  NavigableDialogAction,
38
38
  FilebrowserDialogMode,
39
- FilebrowserDialogState
39
+ FilebrowserDialogState,
40
+ FormDialogField,
41
+ FormDialogFieldType,
42
+ FormDialogOptions,
40
43
  } from '../dialogs';
41
44
  export type {
42
45
  TreeNode,
@@ -60,6 +60,16 @@ export class DocksCommand extends DocksWidget {
60
60
  @state()
61
61
  private dropdownContributions: Contribution[] = []
62
62
 
63
+ private closeDropdown(dropdown: { open?: boolean } | null | undefined) {
64
+ if (dropdown && dropdown.open !== undefined) {
65
+ dropdown.open = false
66
+ }
67
+ }
68
+
69
+ private closeParentDropdown() {
70
+ this.closeDropdown(this.closest('wa-dropdown') as { open?: boolean } | null)
71
+ }
72
+
63
73
  private handleClick(event?: Event) {
64
74
  if (this.disabled) return
65
75
 
@@ -67,27 +77,20 @@ export class DocksCommand extends DocksWidget {
67
77
  event.stopPropagation()
68
78
  }
69
79
 
80
+ this.closeParentDropdown()
81
+
70
82
  if (this.action) {
71
83
  this.action(event)
72
84
  return
73
85
  }
74
86
 
75
87
  if (this.cmd) {
76
- const dropdown = this.closest('wa-dropdown') as any;
77
- if (dropdown && dropdown.open !== undefined) {
78
- dropdown.open = false;
79
- }
80
88
  void this.executeCommand(this.cmd, this.params);
81
89
  }
82
90
  }
83
91
 
84
92
  private handleSelect(event: CustomEvent) {
85
- // Close dropdown immediately when any item is selected
86
- // This ensures the dropdown is hidden before the command executes
87
- const dropdown = event.target as any;
88
- if (dropdown && dropdown.open !== undefined) {
89
- dropdown.open = false;
90
- }
93
+ this.closeDropdown(event.target as { open?: boolean })
91
94
  }
92
95
 
93
96
  private isInDropdown(): boolean {
@@ -182,8 +182,13 @@ class DialogService {
182
182
  }
183
183
  const stateWithClose = { ...state, close: cleanup };
184
184
 
185
+ const dialogLabel =
186
+ state && typeof state === 'object' && typeof (state as { label?: unknown }).label === 'string'
187
+ ? (state as { label: string }).label
188
+ : (contribution.label || dialogId);
189
+
185
190
  const template = html`
186
- <wa-dialog label="${contribution.label || dialogId}" open @wa-request-close=${cleanup}>
191
+ <wa-dialog label="${dialogLabel}" open @wa-request-close=${cleanup}>
187
192
  <style>
188
193
  .dialog-service-content {
189
194
  display: flex;
@@ -0,0 +1,239 @@
1
+ import { html, css, PropertyValues } from "lit";
2
+ import { customElement, state, property } from "lit/decorators.js";
3
+ import { DocksDialogContent } from "../parts/dialog-content";
4
+ import { contributionRegistry } from "../core/contributionregistry";
5
+ import {
6
+ DIALOG_CONTRIBUTION_TARGET,
7
+ OK_BUTTON,
8
+ CANCEL_BUTTON,
9
+ dialogService,
10
+ } from "../core/dialogservice";
11
+
12
+ /** HTML input types supported by {@link formDialog}. */
13
+ export type FormDialogFieldType = "text" | "password" | "url" | "email" | "number";
14
+
15
+ export interface FormDialogField {
16
+ /** Key in the returned values record. */
17
+ name: string;
18
+ label: string;
19
+ type?: FormDialogFieldType;
20
+ value?: string;
21
+ placeholder?: string;
22
+ /** When false, empty values are allowed. Defaults to true. */
23
+ required?: boolean;
24
+ }
25
+
26
+ export interface FormDialogOptions {
27
+ /** Dialog window title (shown on `wa-dialog`). */
28
+ label: string;
29
+ fields: FormDialogField[];
30
+ /** Optional message above the fields (plain text or markdown). */
31
+ message?: string;
32
+ markdown?: boolean;
33
+ }
34
+
35
+ interface FormDialogState extends FormDialogOptions {
36
+ resolve: (values: Record<string, string> | null) => void;
37
+ }
38
+
39
+ @customElement("docks-form-dialog-content")
40
+ export class DocksFormDialogContent extends DocksDialogContent {
41
+ @property({ attribute: false })
42
+ fields: FormDialogField[] = [];
43
+
44
+ @property({ type: String })
45
+ message = "";
46
+
47
+ @property({ type: Boolean })
48
+ markdown = false;
49
+
50
+ @state()
51
+ private values: Record<string, string> = {};
52
+
53
+ @state()
54
+ private validationError = "";
55
+
56
+ static styles = [
57
+ ...DocksDialogContent.styles,
58
+ css`
59
+ .form-fields {
60
+ display: flex;
61
+ flex-direction: column;
62
+ gap: 0.75rem;
63
+ min-width: 22rem;
64
+ }
65
+
66
+ .field label {
67
+ display: flex;
68
+ flex-direction: column;
69
+ gap: 0.25rem;
70
+ font-size: var(--wa-font-size-s);
71
+ }
72
+
73
+ .field-label {
74
+ font-weight: 500;
75
+ color: var(--wa-color-text-normal);
76
+ }
77
+
78
+ wa-input {
79
+ width: 100%;
80
+ }
81
+
82
+ .validation-error {
83
+ color: var(--wa-color-danger-600, #dc2626);
84
+ font-size: var(--wa-font-size-s);
85
+ }
86
+ `,
87
+ ];
88
+
89
+ async firstUpdated(changedProperties: PropertyValues) {
90
+ super.firstUpdated(changedProperties);
91
+ const initial: Record<string, string> = {};
92
+ for (const field of this.fields) {
93
+ initial[field.name] = field.value ?? "";
94
+ }
95
+ this.values = initial;
96
+
97
+ await this.updateComplete;
98
+ const input = this.shadowRoot?.querySelector("wa-input");
99
+ const inputEl = (input as HTMLElement & { shadowRoot?: ShadowRoot })?.shadowRoot?.querySelector(
100
+ "input",
101
+ );
102
+ inputEl?.focus();
103
+ }
104
+
105
+ getResult(): Record<string, string> {
106
+ return { ...this.values };
107
+ }
108
+
109
+ setValidationError(message: string): void {
110
+ this.validationError = message;
111
+ }
112
+
113
+ private updateField(name: string, value: string): void {
114
+ this.values = { ...this.values, [name]: value };
115
+ if (this.validationError) this.validationError = "";
116
+ }
117
+
118
+ private handleKeyDown(event: KeyboardEvent): void {
119
+ if (event.key === "Enter") {
120
+ event.preventDefault();
121
+ this.dispatchEvent(new CustomEvent("dialog-ok", { bubbles: true, composed: true }));
122
+ } else if (event.key === "Escape") {
123
+ event.preventDefault();
124
+ this.dispatchEvent(new CustomEvent("dialog-cancel", { bubbles: true, composed: true }));
125
+ }
126
+ }
127
+
128
+ render() {
129
+ return html`
130
+ ${this.message ? this.renderMessage(this.message, this.markdown) : null}
131
+ <div class="form-fields" @keydown=${this.handleKeyDown}>
132
+ ${this.fields.map(
133
+ (field) => html`
134
+ <div class="field">
135
+ <label>
136
+ <span class="field-label">${field.label}</span>
137
+ <wa-input
138
+ type=${field.type ?? "text"}
139
+ .value=${this.values[field.name] ?? ""}
140
+ placeholder=${field.placeholder ?? ""}
141
+ @input=${(event: Event) =>
142
+ this.updateField(
143
+ field.name,
144
+ (event.target as HTMLInputElement).value,
145
+ )}
146
+ ></wa-input>
147
+ </label>
148
+ </div>
149
+ `,
150
+ )}
151
+ ${this.validationError
152
+ ? html`<div class="validation-error">${this.validationError}</div>`
153
+ : null}
154
+ </div>
155
+ `;
156
+ }
157
+ }
158
+
159
+ declare global {
160
+ interface HTMLElementTagNameMap {
161
+ "docks-form-dialog-content": DocksFormDialogContent;
162
+ }
163
+ }
164
+
165
+ function trimValues(values: Record<string, string>): Record<string, string> {
166
+ return Object.fromEntries(
167
+ Object.entries(values).map(([key, value]) => [key, value.trim()]),
168
+ );
169
+ }
170
+
171
+ function findFormDialogContent(): DocksFormDialogContent | null {
172
+ return document.querySelector("docks-form-dialog-content");
173
+ }
174
+
175
+ /** Validates required fields; returns an error message or null if valid. */
176
+ export function validateFormDialogFields(
177
+ fields: FormDialogField[],
178
+ values: Record<string, string>,
179
+ ): string | null {
180
+ for (const field of fields) {
181
+ if (field.required === false) continue;
182
+ if (!values[field.name]?.trim()) {
183
+ return `${field.label} is required.`;
184
+ }
185
+ }
186
+ return null;
187
+ }
188
+
189
+ contributionRegistry.registerContribution(DIALOG_CONTRIBUTION_TARGET, {
190
+ id: "form",
191
+ label: "Form",
192
+ buttons: [OK_BUTTON, CANCEL_BUTTON],
193
+ component: (state?: FormDialogState) => {
194
+ if (!state) {
195
+ return html`<div>Error: No form dialog state</div>`;
196
+ }
197
+
198
+ return html`
199
+ <docks-form-dialog-content
200
+ .message=${state.message ?? ""}
201
+ .markdown=${state.markdown ?? false}
202
+ .fields=${state.fields}
203
+ ></docks-form-dialog-content>
204
+ `;
205
+ },
206
+ onButton: async (id: string, result: unknown, state?: FormDialogState) => {
207
+ if (!state) {
208
+ return true;
209
+ }
210
+
211
+ if (id === "ok") {
212
+ const values = trimValues((result ?? {}) as Record<string, string>);
213
+ const error = validateFormDialogFields(state.fields, values);
214
+ if (error) {
215
+ findFormDialogContent()?.setValidationError(error);
216
+ return false;
217
+ }
218
+ state.resolve(values);
219
+ } else {
220
+ state.resolve(null);
221
+ }
222
+
223
+ return true;
224
+ },
225
+ });
226
+
227
+ export function formDialog(options: FormDialogOptions): Promise<Record<string, string> | null> {
228
+ return new Promise((resolve) => {
229
+ dialogService.open("form", { ...options, resolve });
230
+ });
231
+ }
232
+
233
+ export async function formDialogRequired(
234
+ options: FormDialogOptions,
235
+ ): Promise<Record<string, string>> {
236
+ const values = await formDialog(options);
237
+ if (!values) throw new Error("Cancelled");
238
+ return values;
239
+ }
@@ -1,4 +1,12 @@
1
1
  export { promptDialog } from './prompt-dialog';
2
+ export {
3
+ formDialog,
4
+ formDialogRequired,
5
+ validateFormDialogFields,
6
+ type FormDialogField,
7
+ type FormDialogFieldType,
8
+ type FormDialogOptions,
9
+ } from './form-dialog';
2
10
  export { infoDialog } from './info-dialog';
3
11
  export { confirmDialog } from './confirm-dialog';
4
12
  export { navigableInfoDialog, type NavigableDialogAction } from './navigable-info-dialog';
package/src/parts/tabs.ts CHANGED
@@ -469,13 +469,27 @@ export class DocksTabs extends DocksContainer {
469
469
  overflow: hidden;
470
470
  }
471
471
 
472
- :host(:is([placement="top"], [placement="bottom"])) wa-tab-group::part(base) {
472
+ /* Top: nav (order 0) → row 1 auto, body (order 2) → row 2 1fr. */
473
+ :host([placement="top"]) wa-tab-group::part(base) {
473
474
  display: grid;
474
475
  grid-template-rows: auto minmax(0, 1fr);
475
476
  height: 100%;
476
477
  width: 100%;
477
478
  }
478
479
 
480
+ /* Bottom: body (order 1) → row 1 1fr, nav (order 2) → row 2 auto. */
481
+ :host([placement="bottom"]) wa-tab-group::part(base) {
482
+ display: grid;
483
+ grid-template-rows: minmax(0, 1fr) auto;
484
+ height: 100%;
485
+ width: 100%;
486
+ }
487
+
488
+ :host([placement="bottom"]) wa-tab-group::part(body) {
489
+ min-height: 0;
490
+ overflow: hidden;
491
+ }
492
+
479
493
  :host(:is([placement="start"], [placement="end"])) wa-tab-group::part(base) {
480
494
  height: 100%;
481
495
  width: 100%;