typo3-typescript-sources 14.3.4 → 14.3.6
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/.last-synced-tag +1 -1
- package/README.md +1 -1
- package/backend/drag-uploader.ts +21 -4
- package/backend/element/editable-page-title.ts +3 -1
- package/backend/element/status-indicator-element.ts +93 -0
- package/backend/form-engine/container/inline-control-container.ts +7 -0
- package/backend/form-engine/element/slug-element.ts +13 -5
- package/backend/form-engine/field-wizard/shortcut-validation.ts +61 -0
- package/backend/form-engine/inline-relation/ajax-dispatcher.ts +5 -0
- package/backend/image-manipulation.ts +1 -1
- package/backend/layout-module/sticky-language-header.ts +186 -0
- package/backend/live-search/element/result/item/item.ts +2 -0
- package/backend/live-search/element/result/result-detail-container.ts +14 -1
- package/backend/module-docheader.ts +58 -0
- package/backend/page-wizard/steps/position-step.ts +5 -0
- package/backend/settings/type/stringlist.ts +7 -0
- package/backend/tests/settings/type/stringlist-test.ts +90 -0
- package/backend/toolbar/system-information-menu.ts +1 -0
- package/backend/tree/page-position-select.ts +25 -0
- package/backend/wizard/finisher/event-finisher.ts +62 -0
- package/backend/wizard/wizard.ts +99 -3
- package/dashboard/dashboard-wizard.ts +86 -0
- package/dashboard/dashboard.ts +28 -76
- package/dashboard/wizard/finisher/dashboard-wizard-submission-service.ts +43 -0
- package/dashboard/wizard/steps/preset-step.ts +164 -0
- package/dashboard/wizard/steps/title-step.ts +90 -0
- package/form/backend/form-editor/core.ts +22 -9
- package/form/backend/form-editor/inspector-component.ts +61 -38
- package/form/backend/form-editor/stage-component.ts +2 -2
- package/form/backend/form-wizard/form-wizard.ts +1 -0
- package/form/backend/form-wizard/steps/settings-step.ts +0 -10
- package/install/router.ts +1 -0
- package/package.json +2 -2
- package/redirects/event-handler.ts +3 -3
- package/scheduler/scheduler.ts +2 -2
- package/workspaces/renderable/record-table.ts +1 -1
- package/workspaces/workspace-state.ts +1 -1
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* This file is part of the TYPO3 CMS project.
|
|
3
|
+
*
|
|
4
|
+
* It is free software; you can redistribute it and/or modify it under
|
|
5
|
+
* the terms of the GNU General Public License, either version 2
|
|
6
|
+
* of the License, or any later version.
|
|
7
|
+
*
|
|
8
|
+
* For the full copyright and license information, please read the
|
|
9
|
+
* LICENSE.txt file that was distributed with this source code.
|
|
10
|
+
*
|
|
11
|
+
* The TYPO3 project - inspiring people to share!
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { StringlistTypeElement } from '@typo3/backend/settings/type/stringlist.js';
|
|
15
|
+
import { expect } from '@open-wc/testing';
|
|
16
|
+
import type { } from 'mocha';
|
|
17
|
+
|
|
18
|
+
describe('@typo3/backend/settings/type/stringlist', () => {
|
|
19
|
+
let root: HTMLElement;
|
|
20
|
+
|
|
21
|
+
beforeEach((): void => {
|
|
22
|
+
root = document.createElement('div');
|
|
23
|
+
document.body.appendChild(root);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
afterEach((): void => {
|
|
27
|
+
root.remove();
|
|
28
|
+
root = null;
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('renders one button when the value list is empty', async () => {
|
|
32
|
+
const element = new StringlistTypeElement;
|
|
33
|
+
element.formid = 'setting-test';
|
|
34
|
+
element.value = [];
|
|
35
|
+
root.appendChild(element);
|
|
36
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
37
|
+
|
|
38
|
+
const inputs = element.querySelectorAll<HTMLInputElement>('input[type="text"]');
|
|
39
|
+
expect(inputs).to.have.lengthOf(0);
|
|
40
|
+
|
|
41
|
+
const buttons = element.querySelectorAll<HTMLButtonElement>('button');
|
|
42
|
+
expect(buttons).to.have.lengthOf(1);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('renders one empty input when the value list is a single empty string', async () => {
|
|
46
|
+
const element = new StringlistTypeElement;
|
|
47
|
+
element.formid = 'setting-test';
|
|
48
|
+
element.value = [''];
|
|
49
|
+
root.appendChild(element);
|
|
50
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
51
|
+
|
|
52
|
+
const inputs = element.querySelectorAll<HTMLInputElement>('input[type="text"]');
|
|
53
|
+
expect(inputs).to.have.lengthOf(1);
|
|
54
|
+
expect(inputs[0].id).to.equal('setting-test');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('hides the remove button when only one row is shown', async () => {
|
|
58
|
+
const element = new StringlistTypeElement;
|
|
59
|
+
element.value = [];
|
|
60
|
+
root.appendChild(element);
|
|
61
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
62
|
+
|
|
63
|
+
expect(element.querySelectorAll('button')).to.have.lengthOf(1);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('adds a row when the add button is clicked on an empty list', async () => {
|
|
67
|
+
const element = new StringlistTypeElement;
|
|
68
|
+
element.value = [];
|
|
69
|
+
root.appendChild(element);
|
|
70
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
71
|
+
|
|
72
|
+
const addButton = element.querySelectorAll<HTMLButtonElement>('button:not([disabled])')[0];
|
|
73
|
+
addButton.click();
|
|
74
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
75
|
+
|
|
76
|
+
expect(element.value).to.deep.equal(['']);
|
|
77
|
+
expect(element.querySelectorAll('input[type="text"]')).to.have.lengthOf(1);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('renders per-row controls for each entry when value has multiple entries', async () => {
|
|
81
|
+
const element = new StringlistTypeElement;
|
|
82
|
+
element.value = ['/typo3', '/fileadmin'];
|
|
83
|
+
root.appendChild(element);
|
|
84
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
85
|
+
|
|
86
|
+
expect(element.querySelectorAll('input[type="text"]')).to.have.lengthOf(2);
|
|
87
|
+
// Two rows, each with + and - buttons.
|
|
88
|
+
expect(element.querySelectorAll('button')).to.have.lengthOf(4);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
@@ -17,6 +17,7 @@ import Icons from '../icons';
|
|
|
17
17
|
import PersistentStorage from '../storage/persistent';
|
|
18
18
|
import Viewport from '../viewport';
|
|
19
19
|
import type { AjaxResponse } from '@typo3/core/ajax/ajax-response';
|
|
20
|
+
import '../element/status-indicator-element';
|
|
20
21
|
|
|
21
22
|
/**
|
|
22
23
|
* Explicit selectors to avoid nesting queries
|
|
@@ -56,6 +56,21 @@ export class InsertPositionChangeEvent extends CustomEvent<{pageUid: number, pos
|
|
|
56
56
|
});
|
|
57
57
|
}
|
|
58
58
|
}
|
|
59
|
+
|
|
60
|
+
export class InsertPositionConfirmEvent extends CustomEvent<{pageUid: number, position: Position}> {
|
|
61
|
+
static readonly eventName = 'typo3:page-position-select-tree:insert-position-confirm';
|
|
62
|
+
constructor(pageUid: number, position: Position) {
|
|
63
|
+
super(InsertPositionConfirmEvent.eventName, {
|
|
64
|
+
detail: {
|
|
65
|
+
pageUid: pageUid,
|
|
66
|
+
position: position,
|
|
67
|
+
},
|
|
68
|
+
bubbles: true,
|
|
69
|
+
composed: true
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
59
74
|
@customElement('typo3-backend-component-page-position-select-tree')
|
|
60
75
|
export class PagePositionSelectTree extends PageTree {
|
|
61
76
|
|
|
@@ -140,6 +155,16 @@ export class PagePositionSelect extends LitElement {
|
|
|
140
155
|
|
|
141
156
|
private async handleNodeSelected(event: CustomEvent): Promise<void> {
|
|
142
157
|
const selectedNode: TreeNodeInterface = event.detail.node;
|
|
158
|
+
|
|
159
|
+
// Re-selecting the active node confirms it and advances the wizard.
|
|
160
|
+
if (this.isActivePagePositionNode(selectedNode)) {
|
|
161
|
+
this.dispatchEvent(new InsertPositionConfirmEvent(
|
|
162
|
+
this.activePageId,
|
|
163
|
+
this.insertPosition
|
|
164
|
+
));
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
143
168
|
const [pageUid, insertPosition] = selectedNode.identifier.split('-');
|
|
144
169
|
this.insertPosition = (insertPosition ?? 'inside') as Position;
|
|
145
170
|
this.activePageId = Number(pageUid);
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* This file is part of the TYPO3 CMS project.
|
|
3
|
+
*
|
|
4
|
+
* It is free software; you can redistribute it and/or modify it under
|
|
5
|
+
* the terms of the GNU General Public License, either version 2
|
|
6
|
+
* of the License, or any later version.
|
|
7
|
+
*
|
|
8
|
+
* For the full copyright and license information, please read the
|
|
9
|
+
* LICENSE.txt file that was distributed with this source code.
|
|
10
|
+
*
|
|
11
|
+
* The TYPO3 project - inspiring people to share!
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { html, type TemplateResult } from 'lit';
|
|
15
|
+
import type { FinisherInterface } from '@typo3/backend/wizard/finisher/finisher-interface';
|
|
16
|
+
import type { FinisherConfig } from '@typo3/backend/wizard/finisher/finisher-config';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Event finisher - dispatches the event supplied via `config.data.event` when
|
|
20
|
+
* the wizard finishes.
|
|
21
|
+
*
|
|
22
|
+
* The wizard runs in the top window while backend modules live in content
|
|
23
|
+
* frames, so the event is broadcast to the top document and every child frame,
|
|
24
|
+
* where the module that opened the wizard (e.g. the dashboard) can listen for
|
|
25
|
+
* it on its own document and update in place instead of reloading.
|
|
26
|
+
*/
|
|
27
|
+
export default class EventFinisher implements FinisherInterface {
|
|
28
|
+
private config!: FinisherConfig;
|
|
29
|
+
|
|
30
|
+
public setConfig(config: FinisherConfig): void {
|
|
31
|
+
this.config = config;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async render(): Promise<TemplateResult> {
|
|
35
|
+
return html`
|
|
36
|
+
<typo3-backend-alert
|
|
37
|
+
severity="0"
|
|
38
|
+
heading="${this.config.labels.successTitle}"
|
|
39
|
+
message="${this.config.labels.successDescription}"
|
|
40
|
+
show-icon
|
|
41
|
+
></typo3-backend-alert>
|
|
42
|
+
`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async execute(): Promise<void> {
|
|
46
|
+
const event = this.config.data.event;
|
|
47
|
+
|
|
48
|
+
if (!(event instanceof Event)) {
|
|
49
|
+
console.warn('Event finisher called without an event to dispatch');
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
document.dispatchEvent(event);
|
|
54
|
+
for (let i = 0; i < window.frames.length; i++) {
|
|
55
|
+
try {
|
|
56
|
+
window.frames[i].document.dispatchEvent(event);
|
|
57
|
+
} catch {
|
|
58
|
+
// Cross-origin frame, skip
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
package/backend/wizard/wizard.ts
CHANGED
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
import { html, LitElement, nothing, type PropertyValues, type TemplateResult } from 'lit';
|
|
15
15
|
import { customElement, property, state } from 'lit/decorators.js';
|
|
16
16
|
import { classMap } from 'lit/directives/class-map.js';
|
|
17
|
+
import { createRef, ref, type Ref } from 'lit/directives/ref.js';
|
|
18
|
+
import { KeyTypesEnum } from '@typo3/backend/enum/key-types';
|
|
17
19
|
import Modal from '@typo3/backend/modal';
|
|
18
20
|
import '@typo3/backend/element/alert-element';
|
|
19
21
|
import '@typo3/backend/element/icon-element';
|
|
@@ -41,6 +43,7 @@ type DataStoreKey = keyof DataStore;
|
|
|
41
43
|
export class Wizard extends LitElement {
|
|
42
44
|
@property({ type: Array, attribute: false }) steps: WizardStepInterface[] = [];
|
|
43
45
|
@property({ type: String, attribute: 'confirm-button-label' }) confirmButtonLabel: string;
|
|
46
|
+
@property({ type: Boolean, attribute: 'skip-summary' }) skipSummary: boolean = false;
|
|
44
47
|
@property({ type: Object, attribute: false }) submissionService: SubmissionServiceInterface;
|
|
45
48
|
|
|
46
49
|
@state() protected currentStepIndex: number = 0;
|
|
@@ -49,6 +52,8 @@ export class Wizard extends LitElement {
|
|
|
49
52
|
@state() protected allSteps: WizardStepInterface[] = [];
|
|
50
53
|
|
|
51
54
|
private progressTracker: ProgressTrackerElement | null = null;
|
|
55
|
+
private readonly primaryButtonRef: Ref<HTMLButtonElement> = createRef();
|
|
56
|
+
private focusPendingStep: WizardStepInterface | null = null;
|
|
52
57
|
|
|
53
58
|
override connectedCallback(): void {
|
|
54
59
|
super.connectedCallback();
|
|
@@ -144,7 +149,7 @@ export class Wizard extends LitElement {
|
|
|
144
149
|
return html`
|
|
145
150
|
<div class="wizard">
|
|
146
151
|
<div class="wizard-progress">${this.renderProgressTracker()}</div>
|
|
147
|
-
<div class="wizard-content" aria-live="polite">${this.currentStep?.render()}</div>
|
|
152
|
+
<div class="wizard-content" aria-live="polite" @keydown=${this.handleContentKeydown}>${this.currentStep?.render()}</div>
|
|
148
153
|
<div class="wizard-actions">${this.renderWizardButtons()}</div>
|
|
149
154
|
</div>
|
|
150
155
|
`;
|
|
@@ -177,7 +182,7 @@ export class Wizard extends LitElement {
|
|
|
177
182
|
) {
|
|
178
183
|
this.allSteps = [
|
|
179
184
|
...this.steps,
|
|
180
|
-
new ConfirmStep(this),
|
|
185
|
+
...(this.skipSummary ? [] : [new ConfirmStep(this)]),
|
|
181
186
|
new FinisherStep(this, this.submissionService),
|
|
182
187
|
];
|
|
183
188
|
|
|
@@ -188,13 +193,87 @@ export class Wizard extends LitElement {
|
|
|
188
193
|
}
|
|
189
194
|
|
|
190
195
|
if (this.currentStep && _changedProperties.has('currentStep')) {
|
|
196
|
+
// Optional per-step readiness hook (e.g. reinitialising FormEngine).
|
|
191
197
|
if (this.hasAfterRender(this.currentStep)) {
|
|
192
198
|
await this.currentStep.afterRender();
|
|
193
199
|
this.requestUpdate();
|
|
194
200
|
}
|
|
201
|
+
this.focusPendingStep = this.currentStep;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
await this.moveFocusIntoStep();
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Move focus into the current step's content once present. Async steps (e.g.
|
|
209
|
+
* @lit/task) re-render the wizard when their data settles, so retry on every
|
|
210
|
+
* update until a focus target is found — no per-step focus wiring required.
|
|
211
|
+
*/
|
|
212
|
+
private async moveFocusIntoStep(): Promise<void> {
|
|
213
|
+
const step = this.focusPendingStep;
|
|
214
|
+
if (!step || step !== this.currentStep) {
|
|
215
|
+
this.focusPendingStep = null; // no request, or navigated on since
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
await this.updateComplete;
|
|
219
|
+
if (step === this.currentStep && this.focusStepContent()) {
|
|
220
|
+
this.focusPendingStep = null;
|
|
195
221
|
}
|
|
196
222
|
}
|
|
197
223
|
|
|
224
|
+
/**
|
|
225
|
+
* Focus the selected radio, otherwise the first form control. Returns whether
|
|
226
|
+
* focus settled on the step's own content; the primary-button fall back
|
|
227
|
+
* returns `false` so async content can still claim focus once it renders.
|
|
228
|
+
*/
|
|
229
|
+
private focusStepContent(): boolean {
|
|
230
|
+
const content = this.renderRoot.querySelector('.wizard-content');
|
|
231
|
+
if (content) {
|
|
232
|
+
// Don't steal focus if the step already has a focus.
|
|
233
|
+
if (content.contains(this.ownerDocument.activeElement)) {
|
|
234
|
+
return true;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const target = this.findFocusTarget(content);
|
|
238
|
+
if (target) {
|
|
239
|
+
// autofocus makes an opening modal <dialog> pick this over the close
|
|
240
|
+
// button; focus() covers later step transitions.
|
|
241
|
+
target.setAttribute('autofocus', '');
|
|
242
|
+
target.focus();
|
|
243
|
+
return true;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// No focusable content (yet): fall back to the primary button, but not if
|
|
248
|
+
// focus already rests on an action button (don't steal it on re-renders).
|
|
249
|
+
const actions = this.renderRoot.querySelector('.wizard-actions');
|
|
250
|
+
if (!actions?.contains(this.ownerDocument.activeElement)) {
|
|
251
|
+
this.primaryButtonRef.value?.focus();
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return false;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Explicit autofocus wins, then the selected radio, then the first control
|
|
259
|
+
* that is enabled and visible. Shadow roots are not pierced — custom
|
|
260
|
+
* elements take part by declaring autofocus or tabindex="0" on their host.
|
|
261
|
+
*/
|
|
262
|
+
private findFocusTarget(content: Element): HTMLElement | null {
|
|
263
|
+
const firstOperable = (selector: string): HTMLElement | null => {
|
|
264
|
+
for (const element of content.querySelectorAll<HTMLElement>(selector)) {
|
|
265
|
+
if (!element.matches(':disabled') && element.checkVisibility()) {
|
|
266
|
+
return element;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return null;
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
return firstOperable('[autofocus]')
|
|
273
|
+
?? firstOperable('input[type="radio"]:checked')
|
|
274
|
+
?? firstOperable('input:not([type="hidden"]):not([type="radio"]):not([readonly]), select, button, textarea:not([readonly]), [contenteditable]:not([contenteditable="false"]), [tabindex]:not([tabindex="-1"])');
|
|
275
|
+
}
|
|
276
|
+
|
|
198
277
|
private hasAfterRender(step: WizardStepInterface): step is WizardStepInterface & WizardStepAfterRenderInterface {
|
|
199
278
|
return step && typeof (step as any).afterRender === 'function';
|
|
200
279
|
}
|
|
@@ -211,6 +290,23 @@ export class Wizard extends LitElement {
|
|
|
211
290
|
this.tryAutoAdvance();
|
|
212
291
|
};
|
|
213
292
|
|
|
293
|
+
/**
|
|
294
|
+
* Enter inside a bare input advances the wizard, emulating a form submit.
|
|
295
|
+
* Controls with their own Enter behaviour are left alone: inputs inside a
|
|
296
|
+
* <form>, search inputs, checkboxes and non-input controls.
|
|
297
|
+
*/
|
|
298
|
+
private readonly handleContentKeydown = (event: KeyboardEvent): void => {
|
|
299
|
+
if (event.key !== KeyTypesEnum.ENTER) {
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
const target = event.target;
|
|
303
|
+
if (!(target instanceof HTMLInputElement) || target.form || target.type === 'search' || target.type === 'checkbox') {
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
event.preventDefault();
|
|
307
|
+
void this.handleNext();
|
|
308
|
+
};
|
|
309
|
+
|
|
214
310
|
private renderWizardButtons(): TemplateResult {
|
|
215
311
|
return html`
|
|
216
312
|
${this.renderPreviousButton()}
|
|
@@ -261,7 +357,7 @@ export class Wizard extends LitElement {
|
|
|
261
357
|
return html`
|
|
262
358
|
<div>
|
|
263
359
|
${resetButtonHtml}
|
|
264
|
-
<button type="button" class="${classMap({ 'btn': true, 'btn-primary': true, 'disabled': !isComplete })}"
|
|
360
|
+
<button type="button" ${ref(this.primaryButtonRef)} class="${classMap({ 'btn': true, 'btn-primary': true, 'disabled': !isComplete })}"
|
|
265
361
|
@click="${this.handleNext}"
|
|
266
362
|
>
|
|
267
363
|
${buttonLabel}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* This file is part of the TYPO3 CMS project.
|
|
3
|
+
*
|
|
4
|
+
* It is free software; you can redistribute it and/or modify it under
|
|
5
|
+
* the terms of the GNU General Public License, either version 2
|
|
6
|
+
* of the License, or any later version.
|
|
7
|
+
*
|
|
8
|
+
* For the full copyright and license information, please read the
|
|
9
|
+
* LICENSE.txt file that was distributed with this source code.
|
|
10
|
+
*
|
|
11
|
+
* The TYPO3 project - inspiring people to share!
|
|
12
|
+
*/
|
|
13
|
+
import { customElement, query, state } from 'lit/decorators.js';
|
|
14
|
+
import { html, LitElement, type PropertyValues, type TemplateResult } from 'lit';
|
|
15
|
+
import type { SubmissionServiceInterface } from '@typo3/backend/wizard/finisher/submission-service-interface';
|
|
16
|
+
import type { WizardStepInterface } from '@typo3/backend/wizard/steps/wizard-step-interface';
|
|
17
|
+
import type { DataStore, Wizard } from '@typo3/backend/wizard/wizard';
|
|
18
|
+
import { AutoAdvanceEvent } from '@typo3/backend/wizard/events/auto-advance-event';
|
|
19
|
+
import PresetStep from '@typo3/dashboard/wizard/steps/preset-step';
|
|
20
|
+
import TitleStep from '@typo3/dashboard/wizard/steps/title-step';
|
|
21
|
+
import { DashboardWizardSubmissionService } from '@typo3/dashboard/wizard/finisher/dashboard-wizard-submission-service';
|
|
22
|
+
|
|
23
|
+
export interface DashboardWizardDataStore extends DataStore {
|
|
24
|
+
preset?: string;
|
|
25
|
+
title?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
type DashboardWizardDataStoreKey = keyof DashboardWizardDataStore;
|
|
29
|
+
|
|
30
|
+
export interface DashboardWizardContext {
|
|
31
|
+
wizard: Wizard;
|
|
32
|
+
getStoreData: <T extends DashboardWizardDataStoreKey>(key: T) => NoInfer<DashboardWizardDataStore[T]>;
|
|
33
|
+
setStoreData: <T extends DashboardWizardDataStoreKey>(key: T, value: NoInfer<DashboardWizardDataStore[T]>) => void;
|
|
34
|
+
clearStoreData: <T extends DashboardWizardDataStoreKey>(key: T) => void;
|
|
35
|
+
getDataStore: () => Readonly<DashboardWizardDataStore>;
|
|
36
|
+
dispatchAutoAdvance: () => void;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
@customElement('typo3-dashboard-wizard')
|
|
40
|
+
export class DashboardWizard extends LitElement {
|
|
41
|
+
@state() steps: WizardStepInterface[] = [];
|
|
42
|
+
@state() submissionService: SubmissionServiceInterface;
|
|
43
|
+
|
|
44
|
+
@query('typo3-backend-wizard') wizard!: Wizard;
|
|
45
|
+
|
|
46
|
+
private context: DashboardWizardContext;
|
|
47
|
+
|
|
48
|
+
protected override firstUpdated(_changedProperties: PropertyValues): void {
|
|
49
|
+
super.firstUpdated(_changedProperties);
|
|
50
|
+
|
|
51
|
+
this.context = {
|
|
52
|
+
wizard: this.wizard,
|
|
53
|
+
getStoreData: this.wizard.getStoreData.bind(this.wizard),
|
|
54
|
+
setStoreData: this.wizard.setStoreData.bind(this.wizard),
|
|
55
|
+
clearStoreData: this.wizard.clearStoreData.bind(this.wizard),
|
|
56
|
+
getDataStore: this.wizard.getDataStore.bind(this.wizard),
|
|
57
|
+
dispatchAutoAdvance: () => this.wizard.dispatchEvent(new AutoAdvanceEvent())
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
this.steps = [
|
|
61
|
+
new PresetStep(this.context),
|
|
62
|
+
new TitleStep(this.context),
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
this.submissionService = new DashboardWizardSubmissionService(this.context);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
protected override createRenderRoot(): HTMLElement | DocumentFragment {
|
|
69
|
+
return this;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
protected override render(): TemplateResult {
|
|
73
|
+
return html`
|
|
74
|
+
<typo3-backend-wizard .steps="${this.steps}"
|
|
75
|
+
.submissionService="${this.submissionService}"
|
|
76
|
+
skip-summary
|
|
77
|
+
></typo3-backend-wizard>
|
|
78
|
+
`;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
declare global {
|
|
83
|
+
interface HTMLElementTagNameMap {
|
|
84
|
+
'typo3-dashboard-wizard': DashboardWizard;
|
|
85
|
+
}
|
|
86
|
+
}
|
package/dashboard/dashboard.ts
CHANGED
|
@@ -22,7 +22,7 @@ import '@typo3/backend/element/icon-element';
|
|
|
22
22
|
import AjaxRequest from '@typo3/core/ajax/ajax-request';
|
|
23
23
|
import ClientStorage from '@typo3/backend/storage/client';
|
|
24
24
|
import { delay } from '@typo3/core/lit-helper';
|
|
25
|
-
import Modal, { type ModalElement } from '@typo3/backend/modal';
|
|
25
|
+
import Modal, { type ModalElement, Size } from '@typo3/backend/modal';
|
|
26
26
|
import { SeverityEnum } from '@typo3/backend/enum/severity';
|
|
27
27
|
import { AjaxResponse } from '@typo3/core/ajax/ajax-response';
|
|
28
28
|
import { Categories, type DataCategoriesInterface, type NewRecordWizardItemSelectedEventInterface } from '@typo3/backend/new-record-wizard';
|
|
@@ -49,7 +49,7 @@ interface DashboardInterface {
|
|
|
49
49
|
widgetPositions: Record<number, DashboardWidgetPosition[]>
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
interface DashboardPresetInterface {
|
|
52
|
+
export interface DashboardPresetInterface {
|
|
53
53
|
identifier: string;
|
|
54
54
|
title: string;
|
|
55
55
|
description: string;
|
|
@@ -154,7 +154,7 @@ class DashboardWidgetRefreshEvent extends Event {
|
|
|
154
154
|
}
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
-
class DashboardAddEvent extends Event {
|
|
157
|
+
export class DashboardAddEvent extends Event {
|
|
158
158
|
static readonly eventName = 'typo3:dashboard:dashboard:add';
|
|
159
159
|
|
|
160
160
|
constructor(
|
|
@@ -446,6 +446,8 @@ export class Dashboard extends LitElement {
|
|
|
446
446
|
this.mql = window.matchMedia('(prefers-reduced-motion: reduce)');
|
|
447
447
|
this.mqListener(this.mql);
|
|
448
448
|
this.mql.addEventListener('change', this.mqListener);
|
|
449
|
+
|
|
450
|
+
document.addEventListener(DashboardAddEvent.eventName, this.relayAddDashboardEvent);
|
|
449
451
|
}
|
|
450
452
|
|
|
451
453
|
public override disconnectedCallback() {
|
|
@@ -456,6 +458,8 @@ export class Dashboard extends LitElement {
|
|
|
456
458
|
|
|
457
459
|
this.mql?.removeEventListener('change', this.mqListener);
|
|
458
460
|
this.mql = null;
|
|
461
|
+
|
|
462
|
+
document.removeEventListener(DashboardAddEvent.eventName, this.relayAddDashboardEvent);
|
|
459
463
|
}
|
|
460
464
|
|
|
461
465
|
protected readonly mqListener = (mql: MediaQueryList|MediaQueryListEvent): void => {
|
|
@@ -496,6 +500,16 @@ export class Dashboard extends LitElement {
|
|
|
496
500
|
`;
|
|
497
501
|
}
|
|
498
502
|
|
|
503
|
+
/**
|
|
504
|
+
* Relays the cross-frame add broadcast (dispatched on `document` by the
|
|
505
|
+
* wizard finisher) onto the element, so it is handled alongside the other
|
|
506
|
+
* dashboard events. A fresh event is created because an in-flight event
|
|
507
|
+
* cannot be re-dispatched.
|
|
508
|
+
*/
|
|
509
|
+
private readonly relayAddDashboardEvent = (event: DashboardAddEvent): void => {
|
|
510
|
+
this.dispatchEvent(new DashboardAddEvent(event.preset, event.title));
|
|
511
|
+
};
|
|
512
|
+
|
|
499
513
|
private async load(): Promise<void> {
|
|
500
514
|
this.loading = true;
|
|
501
515
|
this.dashboards = await this.fetchDashboards();
|
|
@@ -514,11 +528,6 @@ export class Dashboard extends LitElement {
|
|
|
514
528
|
}
|
|
515
529
|
}
|
|
516
530
|
|
|
517
|
-
private async fetchPresets(): Promise<DashboardPresetInterface[]> {
|
|
518
|
-
const data = await this.fetchData(TYPO3.settings.ajaxUrls.dashboard_presets_get);
|
|
519
|
-
return Object.values(data);
|
|
520
|
-
}
|
|
521
|
-
|
|
522
531
|
private async fetchCategories(): Promise<Categories> {
|
|
523
532
|
const data = await this.fetchData(TYPO3.settings.ajaxUrls.dashboard_categories_get);
|
|
524
533
|
return Categories.fromData(data as DataCategoriesInterface);
|
|
@@ -538,79 +547,19 @@ export class Dashboard extends LitElement {
|
|
|
538
547
|
}
|
|
539
548
|
|
|
540
549
|
private async createDashboard(): Promise<void> {
|
|
541
|
-
|
|
542
|
-
const filteredPresets = presets.filter(preset => preset.showInWizard);
|
|
543
|
-
|
|
544
|
-
const content = html`
|
|
545
|
-
<form>
|
|
546
|
-
<div class="form-group">
|
|
547
|
-
<label class="form-label" for="dashboard-form-add-title">${labels.get('dashboard.title')}</label>
|
|
548
|
-
<input class="form-control" id="dashboard-form-add-title" type="text" name="title" required="required">
|
|
549
|
-
</div>
|
|
550
|
-
<div class="dashboard-modal-items">
|
|
551
|
-
${repeat(filteredPresets, (preset: DashboardPresetInterface) => preset.identifier, (preset: DashboardPresetInterface, index: number) => html`
|
|
552
|
-
<div class="dashboard-modal-item">
|
|
553
|
-
<input
|
|
554
|
-
type="radio"
|
|
555
|
-
name="preset"
|
|
556
|
-
value=${preset.identifier}
|
|
557
|
-
class="dashboard-modal-item-checkbox"
|
|
558
|
-
id="dashboard-form-add-preset-${preset.identifier}"
|
|
559
|
-
?checked=${index === 0}
|
|
560
|
-
>
|
|
561
|
-
<label for="dashboard-form-add-preset-${preset.identifier}" class="dashboard-modal-item-block">
|
|
562
|
-
<span class="dashboard-modal-item-icon">
|
|
563
|
-
<typo3-backend-icon identifier=${preset.icon} size="medium"></typo3-backend-icon>
|
|
564
|
-
</span>
|
|
565
|
-
<span class="dashboard-modal-item-details">
|
|
566
|
-
<span class="dashboard-modal-item-title">${preset.title}</span>
|
|
567
|
-
<span class="dashboard-modal-item-description">${preset.description}</span>
|
|
568
|
-
</span>
|
|
569
|
-
</label>
|
|
570
|
-
</div>
|
|
571
|
-
`)}
|
|
572
|
-
</div>
|
|
573
|
-
</form>
|
|
574
|
-
`;
|
|
550
|
+
await topLevelModuleImport('@typo3/dashboard/dashboard-wizard.js');
|
|
575
551
|
|
|
576
552
|
Modal.advanced({
|
|
577
553
|
type: Modal.types.default,
|
|
578
554
|
title: labels.get('dashboard.add'),
|
|
579
|
-
size:
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
callback: (currentModal: ModalElement): void => {
|
|
583
|
-
|
|
584
|
-
currentModal.addEventListener('typo3-modal-shown', (): void => {
|
|
585
|
-
(currentModal.querySelector('#dashboard-form-add-title') as HTMLInputElement)?.focus();
|
|
586
|
-
});
|
|
587
|
-
|
|
588
|
-
currentModal.querySelector('form').addEventListener('submit', (e: Event): void => {
|
|
589
|
-
e.preventDefault();
|
|
590
|
-
const form = e.target as HTMLFormElement;
|
|
591
|
-
const formData = new FormData(form);
|
|
592
|
-
this.dispatchEvent(new DashboardAddEvent(
|
|
593
|
-
formData.get('preset') as string,
|
|
594
|
-
formData.get('title') as string,
|
|
595
|
-
));
|
|
596
|
-
currentModal.hideModal();
|
|
597
|
-
});
|
|
598
|
-
|
|
555
|
+
size: {
|
|
556
|
+
width: Size.default,
|
|
557
|
+
height: Size.small,
|
|
599
558
|
},
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
name: 'cancel',
|
|
605
|
-
trigger: (e, modal) => modal.hideModal(),
|
|
606
|
-
},
|
|
607
|
-
{
|
|
608
|
-
text: labels.get('dashboard.add.button.ok'),
|
|
609
|
-
btnClass: 'btn-primary',
|
|
610
|
-
name: 'save',
|
|
611
|
-
trigger: (e, modal) => modal.querySelector('form').requestSubmit(),
|
|
612
|
-
},
|
|
613
|
-
]
|
|
559
|
+
severity: SeverityEnum.notice,
|
|
560
|
+
staticBackdrop: true,
|
|
561
|
+
content: html`<typo3-dashboard-wizard></typo3-dashboard-wizard>`,
|
|
562
|
+
buttons: [],
|
|
614
563
|
});
|
|
615
564
|
}
|
|
616
565
|
|
|
@@ -1566,6 +1515,9 @@ declare global {
|
|
|
1566
1515
|
'typo3-dashboard': Dashboard;
|
|
1567
1516
|
'typo3-dashboard-widget': DashboardWidget;
|
|
1568
1517
|
}
|
|
1518
|
+
interface DocumentEventMap {
|
|
1519
|
+
[DashboardAddEvent.eventName]: DashboardAddEvent;
|
|
1520
|
+
}
|
|
1569
1521
|
interface HTMLElementEventMap {
|
|
1570
1522
|
[DashboardWidgetContentRenderedEvent.eventName]: DashboardWidgetContentRenderedEvent;
|
|
1571
1523
|
[DashboardWidgetMoveIntendEvent.eventName]: DashboardWidgetMoveIntendEvent;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* This file is part of the TYPO3 CMS project.
|
|
3
|
+
*
|
|
4
|
+
* It is free software; you can redistribute it and/or modify it under
|
|
5
|
+
* the terms of the GNU General Public License, either version 2
|
|
6
|
+
* of the License, or any later version.
|
|
7
|
+
*
|
|
8
|
+
* For the full copyright and license information, please read the
|
|
9
|
+
* LICENSE.txt file that was distributed with this source code.
|
|
10
|
+
*
|
|
11
|
+
* The TYPO3 project - inspiring people to share!
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { SubmissionServiceInterface } from '@typo3/backend/wizard/finisher/submission-service-interface';
|
|
15
|
+
import type { FinisherResult } from '@typo3/backend/wizard/finisher/finisher-result';
|
|
16
|
+
import type { DashboardWizardContext } from '@typo3/dashboard/dashboard-wizard';
|
|
17
|
+
import { DashboardAddEvent } from '@typo3/dashboard/dashboard';
|
|
18
|
+
import labels from '~labels/dashboard.messages';
|
|
19
|
+
|
|
20
|
+
export class DashboardWizardSubmissionService implements SubmissionServiceInterface {
|
|
21
|
+
|
|
22
|
+
constructor(private readonly context: DashboardWizardContext) {
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async execute(): Promise<FinisherResult> {
|
|
26
|
+
const { preset, title } = this.context.getDataStore();
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
success: true,
|
|
30
|
+
finisher: {
|
|
31
|
+
identifier: 'event',
|
|
32
|
+
module: '@typo3/backend/wizard/finisher/event-finisher.js',
|
|
33
|
+
data: {
|
|
34
|
+
event: new DashboardAddEvent(preset ?? '', title ?? ''),
|
|
35
|
+
},
|
|
36
|
+
labels: {
|
|
37
|
+
successTitle: labels.get('dashboard.wizard.success.title'),
|
|
38
|
+
successDescription: labels.get('dashboard.wizard.success.message'),
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
}
|