{"record":{"id":"e3a53e6f0523df0d","repo":"Leantime/leantime","slug":"plugin-s-cannot-be-installed","errorCode":null,"errorMessage":"Plugin %s cannot be installed","messagePattern":"Plugin (.+?) cannot be installed","errorType":"console","errorClass":"RuntimeException","httpStatus":null,"severity":"warning","filePath":"app/Command/InstallPluginCommand.php","lineNumber":38,"sourceCode":"{\n    /**\n     * {@inheritdoc}\n     */\n    protected function configure(): void\n    {\n        $this->addArgument('plugin', InputArgument::REQUIRED, 'The plugin name');\n    }\n\n    /**\n     * {@inheritdoc}\n     */\n    protected function executeCommand(): int\n    {\n        $name = $this->input->getArgument('plugin');\n        $plugin = $this->getPlugin($name);\n\n        if (! isset($plugin->foldername)) {\n            throw new RuntimeException(sprintf('Plugin %s cannot be installed', $plugin->name));\n        }\n\n        if (! $this->confirm(sprintf('Install plugin %s', $plugin->name))) {\n            return Command::SUCCESS;\n        }\n\n        return $this->plugins->installPlugin($plugin->foldername) ? Command::SUCCESS : Command::FAILURE;\n    }\n}\n","sourceCodeStart":20,"sourceCodeEnd":48,"githubUrl":"https://github.com/Leantime/leantime/blob/9a9f49f1008f4782b30f6723c54228f4f992e636/app/Command/InstallPluginCommand.php#L20-L48","documentation":"This browser console.warn comes from the load-more partial of the My To-Dos widget (app/Domain/Widgets/Templates/partials/myToDosLoadMore.blade.php:88). After fetching the next page of tasks (global pagination), the handler appends each task's HTML into an existing group container located by a key derived from the task (typically `document.querySelector` on a group selector like `#task-group-${groupKey}` / existingGroup). When no matching group element is found in the DOM, the task cannot be placed and the handler logs 'Group not found for key:' and drops that card on the floor. The comment in source even admits this 'shouldn't happen with global pagination' — it is a server/client group-invariant violation.","triggerScenarios":"Clicking 'load more' in the My To-Dos widget when: (1) a task on the later page has a status/date/milestone group key that had no representatives on the first page, so its group heading was never rendered; (2) the group key format changed between the initial render (server-side Blade) and the load-more response (JS-built), e.g. key escaping, umlauts/emoji, or case differences so querySelector mismatches; (3) the widget DOM was re-rendered or partially updated (another HTMX event replaced the list) while a load-more fetch was in flight, detaching the groups; (4) duplicate/blank keys produce an id like `task-group-` that matches nothing.","commonSituations":"Large to-do lists spanning many statuses after an admin adds a new ticket status; language/date-format differences changing group labels between initial load and pagination; race between an HTMX-triggered widget refresh and the user clicking load-more; recent refactors of the widget's grouping markup that renamed the data attributes/ids used to build the selector.","solutions":["Reproduce deterministically: filter your to-dos so page 1 contains only groups A/B but page 2 contains a task of group C — if the warn fires for C, fix the server side to always render an (empty) group container for every possible group, or switch pagination to be per-group.","Inspect the network response of the load-more call and compare each task's group key with the ids actually present in the DOM (`document.querySelectorAll('[id^=task-group-]')`) to spot key-format mismatches (escaping, case, whitespace).","If the mismatch is a race with HTMX refresh, abort or ignore in-flight load-more responses when the container was swapped (check a marker/nonce set on the list root before inserting).","Implement the fallback the comment suggests: when the group is missing, create the group heading + list container (clone the template used on first render) instead of dropping the task.","If duplicate ids exist in the DOM (querySelector then returns the first wrong group), deduplicate by scoping the search to the widget root element."],"exampleFix":"// before — task is silently dropped when its group is absent\n} else {\n    // Group doesn't exist yet - this shouldn't happen with global pagination,\n    // but if it does, we could create a new group here\n    console.warn('Group not found for key:', groupKey);\n}\n\n// after — create the missing group so no task is lost\n} else {\n    const template = document.getElementById('todoGroupTemplate');\n    if (template) {\n        const group = template.content.firstElementChild.cloneNode(true);\n        group.dataset.groupKey = groupKey;\n        group.querySelector('.todo-group-title').textContent = groupLabel;\n        listRoot.appendChild(group);\n        group.insertAdjacentHTML('beforeend', taskContent);\n    } else {\n        console.warn('[Widgets] Group not found for key and no group template:', groupKey);\n    }\n}","handlingStrategy":"type-guard","validationCode":"// Before inserting a fetched task, validate its group container exists in the current DOM\nconst groupEl = document.querySelector(`#task-group-${CSS.escape(groupKey)}`);\nconst listRoot = document.getElementById('myToDosList');\nif (!listRoot) { /* widget was swapped mid-flight; discard this response instead of inserting */ }\nif (!groupEl) { /* create the group before appending, or refetch the widget root */ }","typeGuard":"function findTodoGroup(groupKey) {\n    if (typeof groupKey !== 'string' || groupKey === '') return null;\n    const el = document.getElementById('task-group-' + CSS.escape(groupKey));\n    return el instanceof HTMLElement ? el : null;\n}\n\nconst existingGroup = findTodoGroup(groupKey);\nif (existingGroup) {\n    existingGroup.insertAdjacentHTML('beforeend', taskContent);\n} else {\n    console.warn('[Widgets] Group not found for key:', groupKey);\n}","tryCatchPattern":null,"preventionTips":["Render an empty group container for every possible group on the first page so later pages can always find a home (or create groups client-side from a template when missing).","Use CSS.escape() when building selectors from server-provided keys — unescaped special characters are the classic cause of querySelector misses.","Version/nonce the widget root: ignore load-more responses whose nonce no longer matches the DOM (guards against HTMX-refresh races).","Keep server-side grouping and the JS group-key derivation in one shared helper/template so the two sides cannot drift."],"tags":["javascript","dom","widget","pagination","frontend"],"backgroundTag":"dom-element-not-found","analyzedSha":"9a9f49f1008f4782b30f6723c54228f4f992e636","analyzedAt":"2026-08-21T02:37:38.966Z","schemaVersion":2},"datasetVersion":"2026-08-21T03:17:12.404Z"}