@eclipse-docks/core 0.7.108 → 0.7.109

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.109",
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,
@@ -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';