Skip to content

Conversation

@giuscris
Copy link
Member

This pull request introduces several improvements and refactorings across both the backend and frontend codebases. The main themes are enhanced asset metadata handling in PHP, improved build and linting configuration for the frontend, and TypeScript optimizations including more efficient imports and dynamic module loading.

Backend (PHP) improvements:

  • Asset metadata support:
    • The Asset class constructor now accepts an optional meta array, allowing you to associate metadata with assets. (formwork/src/Assets/Asset.php)
    • Added a getMeta method to retrieve asset metadata values by key, with support for default values. (formwork/src/Assets/Asset.php)
    • The Assets collection's add method now accepts and passes asset metadata to the Asset constructor. (formwork/src/Assets/Assets.php)

Frontend (JavaScript/TypeScript) improvements:

  • Build and linting configuration:

    • Switched JS build to use esbuild with support for chunking, ESM output, and watch mode via a new build.js script; updated related package.json scripts for cleaner builds. (panel/build.js, panel/package.json) [1] [2]
    • Refactored ESLint configuration to use defineConfig, updated ECMAScript target, added TypeScript-specific rules, and improved project parsing for .ts files. (panel/eslint.config.js) [1] [2] [3]
  • TypeScript import optimizations:

    • Replaced direct imports with import type for type-only dependencies throughout many files, reducing bundle size and improving clarity. [1] [2] [3] [4] [5] [6] [7] [8] [9] [10] [11]
  • Dynamic imports and performance:

    • Refactored components like ArrayInput, EditorInput, and TagsInput to dynamically import heavy dependencies (sortablejs, editor views) only when needed, improving initial load performance. (panel/src/ts/components/inputs/array-input.ts, panel/src/ts/components/inputs/editor-input.ts, panel/src/ts/components/inputs/tags-input.ts) [1] [2] [3] [4] [5] [6]
  • Minor bug fixes and code quality:

    • Fixed a minor bug in DurationInput interval calculation. (panel/src/ts/components/inputs/duration-input.ts)
    • Minor cleanup of redundant type assertions and improved code consistency. (panel/src/ts/components/dropdowns.ts, panel/src/ts/components/inputs/array-input.ts) [1] [2]

Let me know if you have questions about any specific change or want to dig deeper into how these improvements affect the codebase!

@giuscris giuscris added this to the 2.2.0 milestone Nov 22, 2025
@giuscris giuscris requested a review from Copilot November 22, 2025 14:47
@giuscris giuscris self-assigned this Nov 22, 2025
@giuscris giuscris added the enhancement New feature or request label Nov 22, 2025
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This pull request transitions the panel application from IIFE bundling to ES modules with code splitting for improved performance. The main changes include adding asset metadata support in PHP for module script tags, switching the build system to esbuild with ESM output, and optimizing TypeScript imports using dynamic imports and import type syntax for better tree-shaking and chunk splitting.

Key Changes

  • Asset metadata system in PHP allowing scripts to specify type="module" attribute
  • Build configuration migrated from inline esbuild to a dedicated build.js script with module splitting and chunking
  • Dynamic imports for heavy dependencies (Sortable.js, Chartist, ProseMirror editors) to enable code splitting and reduce initial bundle size

Reviewed changes

Copilot reviewed 28 out of 28 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
formwork/src/Assets/Asset.php Adds metadata support to Asset constructor and getMeta() method
formwork/src/Assets/Assets.php Updates add() method to accept and pass metadata array
panel/build.js New esbuild configuration script with ESM format, code splitting, and watch mode
panel/package.json Updates build scripts and adds clean script for chunk management
panel/eslint.config.js Refactors config with defineConfig import and adds TypeScript-specific linting rules
panel/tsconfig.json Updates library target from ES2017 to ES2020
panel/views/partials/scripts.php Modifies script rendering to support module type and imports app as ES module
panel/views/layouts/*.php Adds module metadata when registering app.min.js asset
panel/src/ts/components/inputs/array-input.ts Converts to dynamic import of Sortable with async initialization
panel/src/ts/components/inputs/tags-input.ts Converts to dynamic import of Sortable with async initialization
panel/src/ts/components/inputs/editor-input.ts Converts editor views to dynamic imports with async methods
panel/src/ts/components/statistics-chart.ts Converts Chartist to dynamic import with async initialization
panel/src/ts/components/views/pages.ts Converts Sortable to dynamic import in initSortable function
panel/src/ts/components/inputs/editor/** Updates to use import type for ProseMirror types
panel/src/ts/components/**.ts Various files updated to use import type for type-only imports
Comments suppressed due to low confidence (1)

panel/src/ts/components/inputs/editor-input.ts:149

  • Making this method async introduces a breaking change. This method is called from the constructor (outside the diff) without await, which means the editor will not be initialized when subsequent code tries to access this.editor. This will cause runtime errors when code immediately after the call attempts to use the editor instance.

Consider keeping the initialization synchronous or refactoring the constructor to handle async initialization properly.

    async switchToCode() {
        if (!this.container) {
            return;
        }
        this.editor?.destroy();
        const { CodeView } = await import("./editor/code/view");
        this.editor = new CodeView(this.container, removeBaseUri(this.element.value, this.options.baseUri), this.options.inputEventHandler, {
            editable: !(this.element.disabled || this.element.readOnly),
            placeholder: this.element.placeholder,
        });
        this.editor.view.dom.style.height = `${this.options.height}px`;

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 28 out of 28 changed files in this pull request and generated 5 comments.

Comments suppressed due to low confidence (1)

panel/src/ts/components/inputs/tags-input.ts:165

  • The createField() method is now async and uses await, but it's called from the constructor (line 45) without awaiting. This means the Sortable functionality may not be initialized when the constructor completes, potentially causing race conditions. Consider using the same pattern as ArrayInput with a separate init() method that is called but not awaited from the constructor.
    private async createField() {
        if ("limit" in this.element.dataset) {
            this.options.limit = parseInt(this.element.dataset.limit as string);
        }

        if (!("orderable" in this.element.dataset)) {
            this.options.orderable = false;
        }

        this.field.className = "form-input-tags-wrap";

        this.innerInput.className = "form-input";
        this.innerInput.type = "text";
        this.innerInput.id = this.element.id;
        this.innerInput.placeholder = this.element.placeholder;

        this.element.classList.remove("form-input");
        this.element.removeAttribute("id");
        this.element.removeAttribute("placeholder");
        this.element.readOnly = true;
        this.element.hidden = true;
        this.element.tabIndex = -1;
        this.element.ariaHidden = "true";

        if (this.element.disabled) {
            this.innerInput.disabled = true;
        }

        (this.element.parentNode as ParentNode).replaceChild(this.field, this.element);
        this.field.appendChild(this.list);
        this.field.appendChild(this.innerInput);
        this.field.appendChild(this.element);

        if (this.element.value) {
            this.tags = this.element.value.split(", ");
            this.tags.forEach((value, index) => {
                value = value.trim();
                this.tags[index] = value;
                this.insertTag(value, this.list);
            });
        }

        if (this.innerInput.placeholder) {
            this.placeholder = this.innerInput.placeholder;
            this.updatePlaceholder();
        } else {
            this.placeholder = "";
        }

        this.field.addEventListener("mousedown", (event) => {
            this.innerInput.focus();
            event.preventDefault();
        });

        if (this.options.orderable) {
            const { default: Sortable } = await import("sortablejs");

            Sortable.create(this.list, {
                forceFallback: true,
                animation: 150,
                filter: ".tag-remove",

                onChoose: () => {
                    if (this.dropdown) {
                        this.dropdown.style.display = "none";
                    }
                },

                onStart: () => {
                    this.field.classList.add("is-dragging");
                },

                onFilter: (event: SortableEvent) => {
                    if (event.target.matches(".tag-remove")) {
                        this.removeTag(event.item.innerText);
                        this.list.removeChild(event.item);
                    }
                },

                onEnd: (event: SortableEvent) => {
                    this.field.classList.remove("is-dragging");
                    const newIndex = event.newIndex;
                    const oldIndex = event.oldIndex;
                    this.innerInput.blur();
                    this.innerInput.focus();
                    if (newIndex === oldIndex || newIndex === undefined || oldIndex === undefined) {
                        return;
                    }
                    this.tags.splice(newIndex, 0, this.tags.splice(oldIndex, 1)[0]);
                    this.updateTags();
                },
            });
        }
    }

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@giuscris giuscris merged commit 3558ac2 into 2.x Nov 22, 2025
8 checks passed
@giuscris giuscris deleted the panel-js-modules branch November 22, 2025 16:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

2 participants