Skip to content

feat(card-grid): Keep-style grid view with list/grid toggle (E04) - #1

Merged
zentala merged 20 commits into
mainfrom
feat/E04-card-grid
Jul 29, 2026
Merged

zentala merged 20 commits into
mainfrom
feat/E04-card-grid

Conversation

@zentala

@zentala zentala commented Jul 29, 2026

Copy link
Copy Markdown
Owner

Keep-style masonry card grid as an alternative to the list view, with a persisted list/grid toggle.

What

  • Card grid (NotesView switches list ⇄ grid on store.app.viewMode): NotesGrid.vue CSS multi-column masonry rendering NoteCard.vue (title, content excerpt, color tint, pinned/error/share state). Pinned notes sit above others.
  • Shared action menu: extracted NoteActionsMenu.vue (favorite/archive/share/color/category/rename/delete) via a vue-frag <Fragment> so the buttons hoist into the host NcActions/NcListItem. One menu backs both the list row and the card — no drift. NoteItem.vue refactored to consume it (behavior-neutral).
  • View mode persisted in localStorage via src/view-mode.js (guarded against disabled storage).
  • Backend: read-only excerpt in the list payload (Note::getData) so cards render a preview without per-note fetches — the list still excludes content. Derived from content, so not in generateEtag() (contentEtag covers it); no setter, no 4-place rule.

Tests

  • New vitest unit harness: view-mode persistence + grouping (Pinned/Others, category) — asserts counts so a dropped note fails red.
  • PHP API testExcerpt: list carries excerpt without content; excerpt tracks content edits.
  • Playwright view-toggle.spec.ts: toggle renders cards, persists across reload.

Deferred (logged in the epic IMPRO)

  • List/grid virtualization: masonry and vue-virtual-scroller conflict (scroller needs fixed-size items); grid uses zero-JS CSS masonry now, windowed virtualization lands when Keep import (E08) justifies the volume.

Deploy-verify

The excerpt field needs a deploy-verify on the NC server (no local PHP), batched with the pending E01b/E02/E03.

Summary by CodeRabbit

  • New Features

    • Added grid and list views, with the selected layout remembered between visits.
    • Added note colors with a color picker and visual indicators.
    • Added archived notes navigation, filtering, and archive actions.
    • Added note excerpts and support for preserving note metadata while editing.
    • Expanded the API to support colors and archived status.
  • Bug Fixes

    • Invalid note property requests now return a clear “Bad Request” response instead of a server error.

zentala added 14 commits July 28, 2026 23:36
Rename notes_meta -> notesplus_meta in MetaMapper and base migrations, add
idempotent Version6002 create-migration for instances enabled under the old
name, and rename built asset tokens (notes-main -> notesplus-main).
Add a minimal FrontMatter parse/serialize layer (ADR-007); Note.getContent()
now returns the body without the fence and setContent() preserves owned attrs.
Wire color through getData, generateEtag, the updateProperty/{color} route, and
the API create/update controllers. Untested locally (no PHP); deploy-verify.
Add a Keep-style 12-color palette (notes-colors.js), a NoteColorPicker swatch
grid, and wire setColor through NotesService + the note action menu. Colored
notes get a left accent bar in the list. Build verified locally.
…aping

Add a single validation choke point in Note::normalizeColorValue (empty->null,
else lowercase #rrggbb, else InvalidArgumentException -> HTTP 400 via
handleErrorResponse) and drop the duplicated '' ? null handling in the
controllers. Memoize getRawContent so getData() reads the file once. Make
FrontMatter::unquote undo YAML single-quote doubling so the helper is safe for
E05 tags.
normalizeColor() now returns a value only for a valid #rrggbb, so a malformed
stored value can never reach the --np-note-color custom property. The swatch
picker gains a Back action.
Stand up tests/unit (phpunit.xml + composer autoload-dev + Make target + CI
test-unit job). FrontMatterTest covers round-trip, bail cases, fence-in-body,
apostrophe/hex quoting and CRLF. APIv1Test::testColor covers the end-to-end
round-trip, 400-on-invalid, and clear.
@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

NotesPlus is rebranded across runtime paths, assets, and metadata tables. Notes gain front-matter-backed color, archive, and excerpt support. The frontend adds archive filtering and persisted list/grid views, while PHPUnit, Vitest, API, and Playwright coverage are expanded.

Changes

NotesPlus rebranding

Layer / File(s) Summary
Application paths, assets, and storage
., lib/..., src/..., templates/main.php, playwright/...
NotesPlus asset URLs, package identity, dashboard assets, router base, attachment paths, metadata tables, migrations, and test-server configuration are updated.

Note metadata and archive controls

Layer / File(s) Summary
Front matter and note persistence
lib/Service/FrontMatter.php, lib/Service/Note.php, lib/Service/MetaService.php, tests/unit/Service/...
Notes parse and serialize front matter, preserve raw content, expose color/archive/excerpt data, and include new attributes in ETags.
API and archive filtering
lib/Controller/..., appinfo/routes.php, src/stores/notes.js, src/components/CategoriesList.vue, tests/api/APIv1Test.php
API endpoints accept color and archived values, invalid arguments return 400, and archived notes receive dedicated filtering and navigation.
Color and shared note actions
src/notes-colors.js, src/NotesService.js, src/components/noteActions.js, src/components/NoteColorPicker.vue, src/components/NoteItem.vue
Color normalization, picker interactions, shared note actions, archive toggles, and client update requests are added.

Grid/list view

Layer / File(s) Summary
View persistence and grouping
src/view-mode.js, src/stores/app.js, src/grouping.js, src/*.test.js
List/grid mode is validated and persisted in local storage, and notes are grouped for grid rendering.
Card rendering and view integration
src/components/NoteCard.vue, src/components/NotesGrid.vue, src/components/NotesView.vue, playwright/e2e/view-toggle.spec.ts
The UI renders note cards in a masonry grid, toggles between list and grid modes, and verifies persistence across reloads.

Test infrastructure

Layer / File(s) Summary
PHP and JavaScript test execution
Makefile, composer.json, package.json, tests/unit/phpunit.xml, vitest.config.js, .github/workflows/test.yml
Unit-test commands, autoloading, test configuration, dependencies, and CI jobs are added or updated.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant NotesView
  participant AppStore
  participant NotesGrid
  User->>NotesView: Select grid view
  NotesView->>AppStore: Persist view mode
  AppStore-->>NotesView: Return grid mode
  NotesView->>NotesGrid: Render grouped notes
  NotesGrid-->>User: Display note cards
Loading

Suggested reviewers: enjeck

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 37.05% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly captures the main change: a Keep-style card grid with a persisted list/grid toggle.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/E04-card-grid

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 13

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
lib/Service/MetaService.php (1)

205-218: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

generateEtag() now has an unguarded exception path via getColor()/getArchived().

Both new getters read raw file content (via Note::getRawContent()), which can throw. Unlike generateContentEtag() just above (wrapped in try/catch specifically because content reads can fail), generateEtag() calls getColor()/getArchived() with no protection. Since this runs inside getAll()'s per-note loop, one unreadable note now aborts meta-sync for all notes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/Service/MetaService.php` around lines 205 - 218, Update
MetaService::generateEtag() to guard the getColor() and getArchived() calls
against raw-content read failures, following the existing exception-handling
approach in generateContentEtag(). Ensure an unreadable note does not abort
getAll() processing for the remaining notes, while preserving normal ETag
generation for readable notes.
lib/Controller/NotesApiController.php (1)

161-207: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Partial update risk: color validation can fail after other fields already persisted.

update() applies content/modified/title/category/favorite (185-198) before color/archived (199-204). If setColor() throws \InvalidArgumentException (invalid hex), the request returns 400, but any earlier mutations in this same call (e.g. content, favorite) have already been written to the file/tags — the client sees "rejected" while the note was actually partially modified. Consider validating color up front (before any mutation) or reordering so validating setters run first.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/Controller/NotesApiController.php` around lines 161 - 207, Validate the
optional color before applying any note mutations in update(), using setColor()
or the existing color-validation behavior to reject invalid hex values upfront.
Ensure an InvalidArgumentException from color validation occurs before content,
modified, title/category, favorite, or archived changes are persisted, while
preserving normal updates for valid colors.
🧹 Nitpick comments (2)
src/components/NoteColorPicker.vue (1)

7-18: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Minor a11y semantics nit: this is a mutually-exclusive selection, not independent toggles.

role="group" + aria-pressed on each swatch is valid for independent toggle buttons, but here only one color can be active at a time. role="radiogroup" with aria-checked on each swatch would better convey the single-select semantics to screen readers.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/NoteColorPicker.vue` around lines 7 - 18, Update the
NoteColorPicker container from group semantics to radiogroup, and replace each
swatch button’s aria-pressed state with aria-checked while preserving the
existing isActive(color.value) selection logic.
lib/Service/Note.php (1)

105-183: 🚀 Performance & Scalability | 🔵 Trivial | 🏗️ Heavy lift

Repeated, uncached front-matter parsing on every getter call.

getContent(), getColor(), getArchived() each independently call $this->frontMatter->parse($this->getRawContent()). A single getData() call parses the same raw content up to 4 times (color, archived, excerpt→getContent, content). More importantly, since color/archived/excerpt are not excludable by NotesController::index() (which hardcodes exclude=['etag','content'] specifically to avoid the "expensive" content read noted on generateContentEtag), every note's raw file content is now read on every listing/poll request regardless of whether it changed — defeating that optimization.

Consider caching the parsed {attrs, body} result alongside $rawContent (invalidated on write), and/or caching the excerpt server-side (e.g. in Meta, recomputed only when contentEtag changes, mirroring the existing generateContentEtag caching pattern) so the frequently-polled listing endpoint doesn't re-read every note's file on each poll.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/Service/Note.php` around lines 105 - 183, The Note getters repeatedly
parse and reread raw content, defeating listing optimizations. Update Note’s
content-loading flow around getContent(), getColor(), getArchived(), and
getExcerpt() to cache the parsed {attrs, body} result alongside the raw content,
reuse it across getters, and invalidate the cache whenever the note is written;
preserve existing getter values and getData() behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/test.yml:
- Around line 30-40: Update the “Unit tests” step to invoke the PHPUnit binary
installed by setup-php through the job PATH instead of ./vendor/bin/phpunit,
unless PHPUnit is intentionally added to composer.json with the lockfile
updated.

In `@lib/Controller/NotesController.php`:
- Around line 293-306: Wrap the color and archived mutation/read flows in the
same $this->inLockScope(...) mechanism used by the title and category cases.
Ensure Note::setColor and Note::setArchived, along with their corresponding
getters, execute within the lock while preserving the existing null-checks and
result assignment behavior.

In `@lib/Db/MetaMapper.php`:
- Line 18: Update the migration represented by Version6002Date20260725120000 to
preserve existing metadata from notes_meta before MetaMapper switches to
notesplus_meta. Rename or copy the existing table and its rows into
notesplus_meta, handling the case where notes_meta is absent, so existing
metadata remains available after the migration.

In `@lib/Migration/Version3005Date20200528204430.php`:
- Around line 36-37: Keep Version3005Date20200528204430 unchanged; do not alter
its notesplus_meta table operation to perform a rename. Add a new forward
migration that explicitly detects the existing old table and migrates or renames
it to the intended table for installations where the historical migration
already ran.

In `@lib/Migration/Version3005Date20200528204431.php`:
- Line 36: Leave the historical table targets unchanged in
lib/Migration/Version3005Date20200528204431.php lines 36-36 and
lib/Migration/Version3005Date20200528204430.php lines 36-37. Add one new forward
migration that renames or copies notes_meta to notesplus_meta while preserving
rows and indexes, then update MetaMapper to use notesplus_meta only after that
migration; apply the MetaMapper change at lib/Db/MetaMapper.php line 18.

In `@lib/Migration/Version6002Date20260725120000.php`:
- Around line 25-26: Align the migration identifier in
Version6002Date20260725120000 with the version declared in appinfo/info.xml by
renaming it to the appropriate Version6000... prefix, including the class and
migration filename references. Only retain the 6002 prefix if you intentionally
update appinfo/info.xml to the corresponding 6.2.x version.

In `@lib/Service/Note.php`:
- Around line 255-284: Update setColor and setArchived to perform their
read-modify-write operations through the same locking and stale-content
protection used by setTitle/setCategory and full content updates. Ensure
getRawContent and writeRawContent execute within the appropriate inLockScope
context and use the existing ETag/check mechanism, preserving the attribute
mutation behavior while preventing concurrent edits from being overwritten.
- Around line 171-176: Update getData() around getColor() and getArchived() to
catch \Throwable from each raw-content read, matching the existing graceful
handling used for excerpt and content. When either read fails, preserve the
response shape, mark the note data with error=true, and continue processing so
one unreadable note cannot abort the listing.

In `@Makefile`:
- Around line 90-93: Add a .PHONY declaration for the test targets, including
test and test-unit, so matching files or directories cannot cause Make to skip
these recipes.

In `@src/components/NoteActionsMenu.vue`:
- Around line 213-239: Update onColorSelected, onToggleFavorite, and
onToggleArchived to catch failures from setColor, setFavorite, and setArchived
and report them through the existing showError mechanism, matching onRename and
onDeleteNote. Preserve the loading-state cleanup, including resetting
loading.favorite on both success and failure, while ensuring failed toggle
requests provide user feedback.
- Around line 59-76: Update onCategoryChange in NoteActionsMenu.vue to handle
string tag results before changing showCategorySelect: keep the selector open
while a new category is being created so the tag-apply flow can complete. Only
reset/hide the selector and move focus after a category is successfully applied
and differs from the current category, including the corresponding logic
referenced around lines 251–259.

In `@src/grouping.js`:
- Around line 39-49: Update the category grouping logic to mirror
NotesView.vue’s categoryToLabel behavior: derive each group label relative to
the selected category by removing its prefix, and suppress the label when the
group category equals the selected category. Use the existing selected-category
context and preserve grouping order and note membership in the surrounding
grouping function.

In `@vitest.config.js`:
- Around line 6-8: Reorder the imports in vitest.config.js so the
`@vitejs/plugin-vue2` import appears before the node:url import, satisfying the
configured perfectionist/sort-imports rule while leaving defineConfig unchanged.

---

Outside diff comments:
In `@lib/Controller/NotesApiController.php`:
- Around line 161-207: Validate the optional color before applying any note
mutations in update(), using setColor() or the existing color-validation
behavior to reject invalid hex values upfront. Ensure an
InvalidArgumentException from color validation occurs before content, modified,
title/category, favorite, or archived changes are persisted, while preserving
normal updates for valid colors.

In `@lib/Service/MetaService.php`:
- Around line 205-218: Update MetaService::generateEtag() to guard the
getColor() and getArchived() calls against raw-content read failures, following
the existing exception-handling approach in generateContentEtag(). Ensure an
unreadable note does not abort getAll() processing for the remaining notes,
while preserving normal ETag generation for readable notes.

---

Nitpick comments:
In `@lib/Service/Note.php`:
- Around line 105-183: The Note getters repeatedly parse and reread raw content,
defeating listing optimizations. Update Note’s content-loading flow around
getContent(), getColor(), getArchived(), and getExcerpt() to cache the parsed
{attrs, body} result alongside the raw content, reuse it across getters, and
invalidate the cache whenever the note is written; preserve existing getter
values and getData() behavior.

In `@src/components/NoteColorPicker.vue`:
- Around line 7-18: Update the NoteColorPicker container from group semantics to
radiogroup, and replace each swatch button’s aria-pressed state with
aria-checked while preserving the existing isActive(color.value) selection
logic.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: ea7e9749-dcf9-4643-85b1-3280473559b0

📥 Commits

Reviewing files that changed from the base of the PR and between 0ff6975 and dc9eb13.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (40)
  • .github/workflows/test.yml
  • Makefile
  • appinfo/routes.php
  • composer.json
  • lib/AppInfo/DashboardWidget.php
  • lib/Controller/Helper.php
  • lib/Controller/NotesApiController.php
  • lib/Controller/NotesController.php
  • lib/Controller/PageController.php
  • lib/Db/MetaMapper.php
  • lib/Migration/Version3005Date20200528204430.php
  • lib/Migration/Version3005Date20200528204431.php
  • lib/Migration/Version6002Date20260725120000.php
  • lib/Service/FrontMatter.php
  • lib/Service/MetaService.php
  • lib/Service/Note.php
  • package.json
  • playwright/e2e/view-toggle.spec.ts
  • src/NotesService.js
  • src/Util.js
  • src/components/CategoriesList.vue
  • src/components/NoteActionsMenu.vue
  • src/components/NoteCard.vue
  • src/components/NoteColorPicker.vue
  • src/components/NoteItem.vue
  • src/components/NotesGrid.vue
  • src/components/NotesView.vue
  • src/grouping.js
  • src/grouping.test.js
  • src/notes-colors.js
  • src/stores/app.js
  • src/stores/notes.js
  • src/test-setup.js
  • src/view-mode.js
  • src/view-mode.test.js
  • templates/main.php
  • tests/api/APIv1Test.php
  • tests/unit/Service/FrontMatterTest.php
  • tests/unit/phpunit.xml
  • vitest.config.js

Comment thread .github/workflows/test.yml
Comment thread lib/Controller/NotesController.php Outdated
Comment on lines +293 to +306
case 'color':
if ($color !== null) {
$note->setColor($color);
}
$result = $note->getColor();
break;

case 'archived':
if ($archived !== null) {
$note->setArchived($archived);
}
$result = $note->getArchived();
break;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

color/archived cases skip the lock scope used for title/category.

title and category mutations run inside $this->inLockScope(...) because they touch the file. color/archived also mutate the file's raw content (via Note::setColor/setArchived's read-modify-write of the whole file), but are called directly here with no lock, unlike its siblings.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/Controller/NotesController.php` around lines 293 - 306, Wrap the color
and archived mutation/read flows in the same $this->inLockScope(...) mechanism
used by the title and category cases. Ensure Note::setColor and
Note::setArchived, along with their corresponding getters, execute within the
lock while preserving the existing null-checks and result assignment behavior.

Comment thread lib/Db/MetaMapper.php Outdated
class MetaMapper extends QBMapper {
public function __construct(IDBConnection $db) {
parent::__construct($db, 'notes_meta');
parent::__construct($db, 'notesplus_meta');

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Migrate existing metadata before switching the mapper table.

Existing installations have metadata in notes_meta; changing MetaMapper to notesplus_meta makes those rows invisible unless a new migration renames/copies the table and preserves its data. The supplied Version6002Date20260725120000.php context creates a new table, but does not show a transfer from notes_meta.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/Db/MetaMapper.php` at line 18, Update the migration represented by
Version6002Date20260725120000 to preserve existing metadata from notes_meta
before MetaMapper switches to notesplus_meta. Rename or copy the existing table
and its rows into notesplus_meta, handling the case where notes_meta is absent,
so existing metadata remains available after the migration.

Comment on lines +36 to +37
if ($schema->hasTable('notesplus_meta')) {
$schema->dropTable('notesplus_meta');

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Do not modify this historical migration to perform the table rename.

Existing installations that already ran Version3005Date20200528204430 will not rerun it, so changing the target from notes_meta does not migrate their data. Keep historical migrations immutable and add a new forward migration that handles the old table explicitly.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/Migration/Version3005Date20200528204430.php` around lines 36 - 37, Keep
Version3005Date20200528204430 unchanged; do not alter its notesplus_meta table
operation to perform a rename. Add a new forward migration that explicitly
detects the existing old table and migrates or renames it to the intended table
for installations where the historical migration already ran.

$schema = $schemaClosure();

$table = $schema->createTable('notes_meta');
$table = $schema->createTable('notesplus_meta');

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🔴 Critical | 🏗️ Heavy lift

Implement the metadata-table rename in one new forward migration. Editing historical migrations can cause fresh-install duplicate-table failures, while existing installations will retain rows in notes_meta and lose mapper visibility after the switch.

  • lib/Migration/Version3005Date20200528204431.php#L36-L36: leave the historical creation target unchanged; avoid creating notesplus_meta twice.
  • lib/Migration/Version3005Date20200528204430.php#L36-L37: leave the historical drop target unchanged; do not rely on an already-applied migration for upgrades.
  • lib/Db/MetaMapper.php#L18-L18: switch to notesplus_meta only after a forward migration renames/copies the existing table and preserves its rows and indexes.
📍 Affects 3 files
  • lib/Migration/Version3005Date20200528204431.php#L36-L36 (this comment)
  • lib/Migration/Version3005Date20200528204430.php#L36-L37
  • lib/Db/MetaMapper.php#L18-L18
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/Migration/Version3005Date20200528204431.php` at line 36, Leave the
historical table targets unchanged in
lib/Migration/Version3005Date20200528204431.php lines 36-36 and
lib/Migration/Version3005Date20200528204430.php lines 36-37. Add one new forward
migration that renames or copies notes_meta to notesplus_meta while preserving
rows and indexes, then update MetaMapper to use notesplus_meta only after that
migration; apply the MetaMapper change at lib/Db/MetaMapper.php line 18.

Comment thread Makefile
Comment on lines +90 to +93
test: test-unit test-api

test-unit:
phpunit -c tests/unit/phpunit.xml --testdox

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Declare the test targets as phony.

Without a .PHONY declaration, a file or directory named test can cause Make to skip the test suite.

Proposed fix
+.PHONY: test test-unit test-api
+
 test: test-unit test-api
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
test: test-unit test-api
test-unit:
phpunit -c tests/unit/phpunit.xml --testdox
.PHONY: test test-unit test-api
test: test-unit test-api
test-unit:
phpunit -c tests/unit/phpunit.xml --testdox
🧰 Tools
🪛 checkmake (0.3.2)

[warning] 90-90: Target "test" should be declared PHONY.

(phonydeclared)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Makefile` around lines 90 - 93, Add a .PHONY declaration for the test
targets, including test and test-unit, so matching files or directories cannot
cause Make to skip these recipes.

Source: Linters/SAST tools

Comment thread src/components/NoteActionsMenu.vue Outdated
Comment on lines +59 to +76
<NcActionInput
v-else
:model-value="note.category"
type="multiselect"
label="label"
track-by="id"
:multiple="false"
:options="categories"
:disabled="loading.category"
:taggable="true"
@input="onCategoryChange"
@search-change="onCategoryChange"
>
<template #icon>
<FolderOutlineIcon :size="20" />
</template>
{{ t('notesplus', 'Change category') }}
</NcActionInput>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== files =="
git ls-files | rg '(^|/)NoteActionsMenu\.vue$|(^|/)package\.json$|(^|/)pnpm-lock\.yaml$|(^|/)package-lock\.json$|(^|/)yarn\.lock$' || true

echo "== target outline =="
ast-grep outline src/components/NoteActionsMenu.vue --view compact || true

echo "== target relevant sections =="
sed -n '1,120p' src/components/NoteActionsMenu.vue
sed -n '120,280p' src/components/NoteActionsMenu.vue

echo "== package versions =="
if [ -f package.json ]; then jq -r '.dependencies["`@nextcloud/vue`"],.devDependencies["`@nextcloud/vue`"]' package.json 2>/dev/null || grep -n '`@nextcloud/vue`' package.json; fi
if [ -f pnpm-lock.yaml ]; then grep -n '`@nextcloud/vue`:' pnpm-lock.yaml | head -20 || true; fi

Repository: zentala/notes-plus

Length of output: 7886


🌐 Web query:

@nextcloud/vue 8.39.0 NcActionInput multiselect taggable search-change event tag event

💡 Result:

In @nextcloud/vue version 8.39.0, the NcActionInput component uses the NcSelect component internally when configured with type="multiselect" [1][2][3]. Regarding the specific events you mentioned: 1. Search-change event: NcActionInput itself does not explicitly expose a search-change event. Because it wraps NcSelect for multiselect functionality, it primarily emits standard Vue events such as input, change, and update:value [2][3]. If you require search functionality (e.g., filtering options based on user input), you should typically interact with the underlying NcSelect or use a dedicated search component, as NcActionInput is designed as a higher-level wrapper for simple actions. 2. Tag event: There is no native tag event emitted by NcActionInput. When using type="multiselect", the component relies on the v-model binding (via the update:value event) to manage the array of selected items. Interactions related to adding or removing tags are handled through this v-model update cycle. If you are currently using NcActionInput for complex multiselect requirements, it is recommended to use the NcSelect component directly [4][5]. NcSelect provides richer support for advanced features like taggable inputs, custom search filtering, and specific events (such as search-change or tag-related events) that are not exposed by the restricted API of NcActionInput [4]. You can find the official documentation for NcSelect and other components in the Nextcloud Vue Style Guide [6]. Since you are on the v8.x stable branch, refer to the stable8 documentation link provided in the library's repository README [7].

Citations:


🌐 Web query:

@nextcloud/vue 8.39.0 NcActionInput source NcSelect multiselect taggable

💡 Result:

In @nextcloud/vue v8.39.0, the NcActionInput component is a wrapper intended to provide a unified interface for various input types, including multiselect fields [1]. As part of the library's modernization efforts, deprecated components like NcMultiselect have been removed [2], and developers are encouraged to use NcSelect instead [2][3]. NcActionInput supports a multiselect type through its type prop, which internally utilizes NcSelect [1]. To implement a multiselect or taggable interface, you should rely on NcSelect, which is based on the underlying vue-select library [4][5]. Key implementation details: 1. Multiselect and Taggable Functionality: Since NcMultiselect is deprecated, NcSelect is the standard component for handling both single and multiple selections [2][3]. For multiselect behavior, you typically configure NcSelect with the multiple prop [6]. When taggable behavior is required (allowing users to create new options), this is usually handled by the underlying vue-select capabilities exposed via NcSelect [4][5]. 2. NcActionInput Integration: You can set the type prop of NcActionInput to "multiselect" [1]. While NcActionInput simplifies certain UI patterns, if your use case requires complex taggable or multiselect behaviors, it is often more flexible to use NcSelect directly, as seen in many Nextcloud internal applications [4][5]. 3. Documentation: For specific props and event usage, refer to the Nextcloud Vue Style Guide [3]. Because v8.x targets Nextcloud 28+ (Vue 2), ensure you are referencing the stable8 documentation site [7][8]. For the most robust implementation, verify the current properties passed to NcSelect in your specific environment, as it serves as the primary component for selection inputs [3][9]. If you require taggable inputs, ensure your NcSelect instance is configured to allow new option creation, which may require handling custom events or props provided by the underlying select implementation [4][5].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== lockfile `@nextcloud/vue` entries =="
python3 - <<'PY'
from pathlib import Path
p=Path('package-lock.json')
if p.exists():
    import json
    data=json.loads(p.read_text())
    for key, val in data.get('packages',{}).items():
        if key.endswith('node_modules/@nextcloud/vue') or key == 'node_modules/@nextcloud/vue':
            print(key, val.get('version'), val.get('resolved', ''))
PY

echo "== installed component sources if available =="
for d in node_modules/@nextcloud/vue/dist/chunks node_modules/@nextcloud/vue/node_modules/@nextcloud/vue/dist/chunks; do
  if [ -d "$d" ]; then
    echo "--- $d"
    find "$d" -maxdepth 3 \( -name '*NcActionInput*' -o -name '*NcSelect*' -o -name '*Multiselect*' \) | sed -n '1,80p'
  fi
done

echo "== package source inside tgz if `@nextcloud/vue` resolved to tarball =="
node - <<'JS'
const fs = require('fs')
const path = require('path')
const lock = JSON.parse(fs.readFileSync('package-lock.json','utf8'))
let pkg
for (const [key, val] of Object.entries(lock.packages || {})) {
  if (key === 'node_modules/@nextcloud/vue') pkg = val; break
  if (key.endsWith('node_modules/@nextcloud/vue')) pkg = val;
}
console.log(JSON.stringify(pkg, null, 2))
JS

Repository: zentala/notes-plus

Length of output: 539


🌐 Web query:

@nextcloud/vue 8.39.0 source NcSelect tags taggable @tag @update:value

💡 Result:

In @nextcloud/vue version 8.39.0, NcSelectTags is a wrapper component designed to handle tagging logic, which internally utilizes the NcSelect component [1][2]. Regarding your query about taggable behavior and value updates: 1. Handling Value Updates: NcSelectTags emits an input event (or update:modelValue/update:model-value events depending on internal configuration) to communicate changes to the parent component [3][1][2]. The component handles the mapping between tag objects and IDs based on the multiple property [1][2]. If you need to intercept value updates, you should listen for these events, typically using v-model or @input in your parent template [3][4]. 2. Taggable Behavior: The NcSelectTags component is specifically purpose-built for tagging interfaces within Nextcloud [1][2]. It manages options filtering and tag selection internally [1][2]. If you are experiencing issues with tag search or selection, ensure that your props (such as fetchTags or options) are correctly passed to the component, as it automatically configures the underlying NcSelect instance [1][2]. 3. Component Evolution: Note that NcSelectTags is the recommended component for tag-related selections, whereas NcSelect is a general-purpose super-component [5][6]. If you are working on a migration or a new implementation, rely on the official Nextcloud Vue Style Guide for the most up-to-date usage patterns for these components [7]. If you are looking for specific event signatures, NcSelectTags in version 8.x typically maps selection changes to the model through its internal handleInput method, which then triggers the standard Vue event emission for v-model compatibility [1][2].

Citations:


🌐 Web query:

@nextcloud/vue 8.39.0 source NcActionInput multiselect NcSelect search-change

💡 Result:

In @nextcloud/vue v8.39.0, the NcActionInput component serves as a wrapper for various input types, including NcSelect [1]. When using NcActionInput with the type set to multiselect or select, the underlying NcSelect component typically handles events by passing them through [2]. Regarding search functionality and events: 1. Event Name: The event emitted by NcSelect when the search query changes is named 'search' [3]. 2. Usage: This 'search' event is designed to allow developers to implement server-side searching or custom filtering by listening to the query string emitted during typing [3]. 3. Implementation: If you are using NcActionInput and need to capture search changes from an embedded NcSelect, you should be able to listen for the 'search' event directly on the NcActionInput component, as it is documented to pass through all events for the NcSelect component [2]. For authoritative guidance on available props and events for your specific version, refer to the stable8 documentation portal, which matches the v8.x release line [4][5]: https://stable8--nextcloud-vue-components.netlify.app/ [4].

Citations:


🌐 Web query:

@nextcloud/vue stable8 NcSelect source search-change tag taggable event search

💡 Result:

In the @nextcloud/vue library, specifically within the stable8 branch (which supports Vue 2), a significant update was made regarding search functionality for selection components [1][2]. A key development for the NcSelectUsers component was the addition of a missing search event, which was backported to stable8 (referenced in PR #6838 and PR #6811) [1][3]. This event is specifically intended to be used for triggering custom search logic (e.g., fetching more users) rather than relying on preloaded data [1]. Regarding your query on NcSelect, tag, and taggable functionality: 1. NcSelect: In recent versions, NcSelectUsers has been introduced as a recommended component for handling user selection, often serving as a replacement for older user-select property approaches within NcSelect [3][4]. 2. Search-change event: While the search event is explicitly documented as being added to NcSelectUsers to handle search-driven data fetching [1], standard NcSelect components typically inherit event patterns from the underlying Vue libraries (like vue-multiselect) they wrap [5]. 3. Tagging: If you are looking for "taggable" functionality, it is often implemented via component props or slots that manage arrays of options or custom input handling, as seen in various Nextcloud application implementations (e.g., tagging in tasks) [5]. For the most accurate implementation details tailored to your version, please consult the official stable8 documentation site, as the stable8 branch maintains its own documentation separate from the main (Vue 3) branch [2][6]. Documentation for stable8: https://stable8--nextcloud-vue-components.netlify.app [2]

Citations:


🌐 Web query:

@nextcloud/vue 8.39.0 NcSelect source searchChangeEvent inputEvent taggable

💡 Result:

In @nextcloud/vue v8.39.0, NcSelect is a wrapper component that leverages vue-select (specifically compatible with the library's internal implementation) [1][2]. Key details regarding your requested features: searchChangeEvent: The NcSelect component generally exposes events from the underlying select library [3]. In the context of search functionality, events such as search (often mapped to the vue-select 'search' event) are used to trigger external search requests rather than preloading all options [4]. inputEvent: Similar to other form components in the library, input events are handled via standard Vue model binding (v-model) and event forwarding [3][5]. Recent updates to NcSelect have focused on forwarding standard Vue events (such as blur, focus, and keydown) to ensure consistent behavior with other input components like NcTextField [3]. taggable: The taggable functionality (allowing users to create new tags) is typically handled via the underlying vue-select properties that NcSelect wraps. If you encounter issues where search or tagging does not behave as expected, it is often due to the interaction between custom filters (e.g., options-filter) and the search input state [6]. For implementation, check the official stable8 documentation for the most accurate prop and event signatures for v8.39.0 [7][8]. Recent maintenance has improved the integration of NcTextField within the search slot to provide a consistent floating label design, which also ensures that event forwarding for inputs is standardized [3]. If you are experiencing specific bugs with tag search or input events, ensure that any custom filters are applied correctly, as they may override search behavior depending on the component version [6].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

if [ -f package-lock.json ]; then
  echo "== package-lock relevant NcActionInput snippets =="
  rg -n '"NcActionInput"' -C 3 package-lock.json || true
fi

echo "== lockfile all `@nextcloud/vue` 8.39.0 entries =="
python3 - <<'PY'
import json
from pathlib import Path
data=json.loads(Path('package-lock.json').read_text())
for k,v in data.get('packages',{}).items():
    if v.get('version') == '8.39.0' and '`@nextcloud/vue`' in k:
        print(k, v.get('resolved'))
PY

Repository: zentala/notes-plus

Length of output: 343


Handle created categories before hiding the selector.

onCategoryChange() resets showCategorySelect unconditionally before inspecting result, so creating a new category text can close the category selector before a tag-apply path can complete. Detect string tag results before resetting the panel, and only reset/move focus when the category is actually applied and changed. Also applies to 251-259.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/NoteActionsMenu.vue` around lines 59 - 76, Update
onCategoryChange in NoteActionsMenu.vue to handle string tag results before
changing showCategorySelect: keep the selector open while a new category is
being created so the tag-apply flow can complete. Only reset/hide the selector
and move focus after a category is successfully applied and differs from the
current category, including the corresponding logic referenced around lines
251–259.

Comment thread src/components/NoteActionsMenu.vue Outdated
Comment on lines +213 to +239
async onColorSelected(color) {
this.showColorSelect = false
if ((this.note.color ?? null) === (color ?? null)) {
return
}
this.loading.color = true
try {
await setColor(this.note.id, color)
} finally {
this.loading.color = false
}
},

onToggleFavorite() {
this.loading.favorite = true
setFavorite(this.note.id, !this.note.favorite)
.catch(() => {
})
.then(() => {
this.loading.favorite = false
})
},

onToggleArchived() {
setArchived(this.note.id, !this.note.archived)
.catch(() => {})
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Favorite/archive/color toggles fail silently.

onColorSelected, onToggleFavorite, and onToggleArchived swallow request failures with empty .catch(() => {})/no catch at all, unlike onRename/onDeleteNote which call showError. If setColor/setFavorite/setArchived fail (network/ETag conflict), the user gets no indication the action didn't persist.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/NoteActionsMenu.vue` around lines 213 - 239, Update
onColorSelected, onToggleFavorite, and onToggleArchived to catch failures from
setColor, setFavorite, and setArchived and report them through the existing
showError mechanism, matching onRename and onDeleteNote. Preserve the
loading-state cleanup, including resetting loading.favorite on both success and
failure, while ensuring failed toggle requests provide user feedback.

Comment thread src/grouping.js
Comment on lines +39 to +49
// category selected: one group per distinct category, preserving order
const groups = []
for (const note of notes) {
const last = groups[groups.length - 1]
if (!last || last.key !== note.category) {
groups.push({ key: note.category, label: note.category, notes: [] })
}
groups[groups.length - 1].notes.push(note)
}
return groups
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Category-group labels use the full path and never suppress the selected category.

In list view, categoryToLabel() strips the selected-category prefix and the caption is hidden entirely when the group is the selected category (category !== group.category). Here, label is always the raw note.category, so in grid mode every subcategory caption shows the absolute path instead of the relative name, and the selected category itself gets a redundant caption. This is a real, user-visible divergence between the two view modes for the same underlying data.

🐛 Proposed fix (mirrors NotesView.vue's `categoryToLabel`)
+import { categoryLabel } from './Util.js'
+
 export function groupNotesForGrid(notes, selectedCategory) {
 	if (selectedCategory === null) {
 		...
 	}

 	// category selected: one group per distinct category, preserving order
 	const groups = []
 	for (const note of notes) {
 		const last = groups[groups.length - 1]
 		if (!last || last.key !== note.category) {
-			groups.push({ key: note.category, label: note.category, notes: [] })
+			const label = note.category === selectedCategory
+				? null
+				: categoryLabel(note.category.substring(selectedCategory.length + 1))
+			groups.push({ key: note.category, label, notes: [] })
 		}
 		groups[groups.length - 1].notes.push(note)
 	}
 	return groups
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// category selected: one group per distinct category, preserving order
const groups = []
for (const note of notes) {
const last = groups[groups.length - 1]
if (!last || last.key !== note.category) {
groups.push({ key: note.category, label: note.category, notes: [] })
}
groups[groups.length - 1].notes.push(note)
}
return groups
}
import { categoryLabel } from './Util.js'
// category selected: one group per distinct category, preserving order
const groups = []
for (const note of notes) {
const last = groups[groups.length - 1]
if (!last || last.key !== note.category) {
const label = note.category === selectedCategory
? null
: categoryLabel(note.category.substring(selectedCategory.length + 1))
groups.push({ key: note.category, label, notes: [] })
}
groups[groups.length - 1].notes.push(note)
}
return groups
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/grouping.js` around lines 39 - 49, Update the category grouping logic to
mirror NotesView.vue’s categoryToLabel behavior: derive each group label
relative to the selected category by removing its prefix, and suppress the label
when the group category equals the selected category. Use the existing
selected-category context and preserve grouping order and note membership in the
surrounding grouping function.

Comment thread vitest.config.js
Comment on lines +6 to +8
import { fileURLToPath } from 'node:url'
import vue from '@vitejs/plugin-vue2'
import { defineConfig } from 'vitest/config'

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Fix the import-order lint error.

The configured perfectionist/sort-imports rule requires @vitejs/plugin-vue2 to appear before node:url.

Proposed fix
-import { fileURLToPath } from 'node:url'
 import vue from '`@vitejs/plugin-vue2`'
+import { fileURLToPath } from 'node:url'
 import { defineConfig } from 'vitest/config'
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import { fileURLToPath } from 'node:url'
import vue from '@vitejs/plugin-vue2'
import { defineConfig } from 'vitest/config'
import vue from '`@vitejs/plugin-vue2`'
import { fileURLToPath } from 'node:url'
import { defineConfig } from 'vitest/config'
🧰 Tools
🪛 ESLint

[error] 7-7: Expected "@vitejs/plugin-vue2" to come before "node:url".

(perfectionist/sort-imports)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@vitest.config.js` around lines 6 - 8, Reorder the imports in vitest.config.js
so the `@vitejs/plugin-vue2` import appears before the node:url import, satisfying
the configured perfectionist/sort-imports rule while leaving defineConfig
unchanged.

Source: Linters/SAST tools

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@lib/Db/MetaMapper.php`:
- Line 18: Update Version6002Date20260725120000 to migrate rows from the legacy
notes_meta table before MetaMapper uses notesplus_meta, preserving metadata and
ETag values. Make the migration tolerate an absent legacy table and remain safe
to rerun without duplicating or overwriting existing data.

In `@lib/Service/SettingsService.php`:
- Around line 153-159: Update the setting-processing flow in setPublic() to
validate that $name exists in $this->attrs before reading its default or other
nested values. Drop or reject unknown setting names first, then retain the
existing default-removal behavior for recognized settings.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: eab338a6-f97e-49d8-b9b7-c13804788c58

📥 Commits

Reviewing files that changed from the base of the PR and between dc9eb13 and 10d3b82.

📒 Files selected for processing (46)
  • .github/workflows/test.yml
  • composer.json
  • lib/AppInfo/Application.php
  • lib/AppInfo/BeforeShareCreatedListener.php
  • lib/AppInfo/BeforeTemplateRenderedListener.php
  • lib/AppInfo/Capabilities.php
  • lib/AppInfo/DashboardWidget.php
  • lib/AppInfo/SearchProvider.php
  • lib/Controller/ChunkCursor.php
  • lib/Controller/ETagDoesNotMatchException.php
  • lib/Controller/Helper.php
  • lib/Controller/NotesApiController.php
  • lib/Controller/NotesController.php
  • lib/Controller/PageController.php
  • lib/Controller/SettingsController.php
  • lib/Db/Meta.php
  • lib/Db/MetaMapper.php
  • lib/Listener/NoteFileEventsListener.php
  • lib/Migration/Cleanup.php
  • lib/Migration/EditorHint.php
  • lib/Migration/Version3005Date20200528204430.php
  • lib/Migration/Version3005Date20200528204431.php
  • lib/Reference/NoteReferenceProvider.php
  • lib/Service/ImageNotWritableException.php
  • lib/Service/InsufficientStorageException.php
  • lib/Service/MetaNote.php
  • lib/Service/MetaService.php
  • lib/Service/Note.php
  • lib/Service/NoteDoesNotExistException.php
  • lib/Service/NoteNotWritableException.php
  • lib/Service/NoteUtil.php
  • lib/Service/NotesFolderException.php
  • lib/Service/NotesService.php
  • lib/Service/SettingsService.php
  • lib/Service/TagService.php
  • lib/Service/Util.php
  • playwright/start-nextcloud-server.mjs
  • src/NotesService.js
  • src/components/EditorEasyMDE.vue
  • src/components/EditorMarkdownIt.vue
  • src/router.js
  • tests/api/APIv02Test.php
  • tests/api/APIv1Test.php
  • tests/api/AbstractAPITest.php
  • tests/api/CapabilitiesTest.php
  • tests/api/CommonAPITest.php
🚧 Files skipped from review as they are similar to previous changes (4)
  • composer.json
  • src/NotesService.js
  • lib/Controller/PageController.php
  • lib/Service/Note.php

Comment thread lib/Db/MetaMapper.php

class MetaMapper extends QBMapper {
public function __construct(IDBConnection $db) {
parent::__construct($db, 'notesplus_meta');

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Preserve legacy metadata before switching the mapper table.

MetaMapper now reads and writes notesplus_meta, while existing installations may still have rows in notes_meta. Ensure Version6002Date20260725120000 renames or copies the legacy rows before this mapper is used, handles an absent legacy table, and is safe to rerun; otherwise existing metadata and ETag state become invisible.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/Db/MetaMapper.php` at line 18, Update Version6002Date20260725120000 to
migrate rows from the legacy notes_meta table before MetaMapper uses
notesplus_meta, preserving metadata and ETag values. Make the migration tolerate
an absent legacy table and remain safe to rerun without duplicating or
overwriting existing data.

Comment on lines +153 to +159
$default = is_callable($this->attrs[$name]['default']) ? $this->attrs[$name]['default']($uid) : $this->attrs[$name]['default'];
if (!$writeDefaults && (!array_key_exists($name, $this->attrs)
|| $value === null
|| $value === $default
)) {
unset($settings[$name]);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Guard unknown setting names before dereferencing $this->attrs[$name].

NotesApiController::setSettings() passes request parameters directly to setPublic(). An unrecognized key reaches Line 153 before the unknown-key check on Lines 154-157, producing undefined-key/offset warnings and potentially failing the request. Drop or reject unknown keys first.

Proposed fix
 		foreach ($settings as $name => $value) {
+			if (!array_key_exists($name, $this->attrs)) {
+				unset($settings[$name]);
+				continue;
+			}
 			if ($value !== null && array_key_exists($name, $this->attrs)) {
 				$settings[$name] = $value = $this->attrs[$name]['validate']($value);
 			}
 			if ($name === 'notesPath' && $value !== null) {
 				continue;
 			}
 			$default = is_callable($this->attrs[$name]['default']) ? $this->attrs[$name]['default']($uid) : $this->attrs[$name]['default'];
-			if (!$writeDefaults && (!array_key_exists($name, $this->attrs)
+			if (!$writeDefaults && (
 				|| $value === null
 				|| $value === $default
 			)) {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/Service/SettingsService.php` around lines 153 - 159, Update the
setting-processing flow in setPublic() to validate that $name exists in
$this->attrs before reading its default or other nested values. Drop or reject
unknown setting names first, then retain the existing default-removal behavior
for recognized settings.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/components/NoteItem.vue (1)

82-99: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Use the Vue 2 value binding for the category multiselect.

This app uses vue 2.7, while @nextcloud/vue 8.x multiselect input bindings use value/@update:value, not Vue 3 model-value; as written, the existing category is not passed back into the component and can appear unselected.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/NoteItem.vue` around lines 82 - 99, Update the category
NcActionInput binding in NoteItem.vue to use Vue 2’s value prop instead of
model-value, and change the corresponding event handling to the `@update`:value
contract expected by `@nextcloud/vue` 8.x. Preserve the existing note.category
value and onCategoryChange behavior.
🧹 Nitpick comments (1)
src/components/noteActions.js (1)

167-170: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Archive toggle has no loading state or error surface.

Unlike the favorite/color handlers, this swallows the rejection and never sets a loading flag, so a failed archive silently leaves the row unchanged. Consider reusing the loading pattern for consistent feedback.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/noteActions.js` around lines 167 - 170, Update
onToggleArchived in the note action handlers to reuse the existing loading-state
pattern used by the favorite/color handlers: set loading before calling
setArchived, surface failures instead of swallowing the rejection, and clear
loading when the operation settles so users receive consistent feedback.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/components/noteActions.js`:
- Around line 182-190: Update onCategoryChange to wrap the awaited setCategory
call in a try/finally, matching onColorSelected, so loading.category is always
reset when setCategory succeeds or rejects; preserve the existing category
validation and error propagation behavior.

In `@src/components/NoteCard.vue`:
- Around line 94-96: Update NoteCard’s onCategoryChange handler to accept both
typed string values and { id, label } option payloads from the category
selector. Normalize either input into the existing category-selection flow so
newly created typed tags are persisted instead of ignored.

---

Outside diff comments:
In `@src/components/NoteItem.vue`:
- Around line 82-99: Update the category NcActionInput binding in NoteItem.vue
to use Vue 2’s value prop instead of model-value, and change the corresponding
event handling to the `@update`:value contract expected by `@nextcloud/vue` 8.x.
Preserve the existing note.category value and onCategoryChange behavior.

---

Nitpick comments:
In `@src/components/noteActions.js`:
- Around line 167-170: Update onToggleArchived in the note action handlers to
reuse the existing loading-state pattern used by the favorite/color handlers:
set loading before calling setArchived, surface failures instead of swallowing
the rejection, and clear loading when the operation settles so users receive
consistent feedback.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: e21b6a2b-212b-41ba-970b-76ea5a4e05bd

📥 Commits

Reviewing files that changed from the base of the PR and between 10d3b82 and bfcc5af.

📒 Files selected for processing (6)
  • playwright/e2e/note-actions.spec.ts
  • src/components/NoteCard.vue
  • src/components/NoteItem.vue
  • src/components/noteActions.js
  • tests/api/APIv1Test.php
  • tests/api/AbstractAPITest.php
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/api/APIv1Test.php

Comment on lines +182 to +190
async onCategoryChange(result) {
this.showCategorySelect = false
const category = result?.id ?? result?.label ?? null
if (category !== null && this.note.category !== category) {
this.loading.category = true
await setCategory(this.note.id, category)
this.loading.category = false
}
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

loading.category sticks on failure.

setCategory rejects on sync errors (it rethrows after handleSyncError), so the reset never runs and the category input stays permanently disabled, plus an unhandled rejection. Mirror the try/finally used in onColorSelected.

🛡️ Proposed fix
 			if (category !== null && this.note.category !== category) {
 				this.loading.category = true
-				await setCategory(this.note.id, category)
-				this.loading.category = false
+				try {
+					await setCategory(this.note.id, category)
+				} finally {
+					this.loading.category = false
+				}
 			}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async onCategoryChange(result) {
this.showCategorySelect = false
const category = result?.id ?? result?.label ?? null
if (category !== null && this.note.category !== category) {
this.loading.category = true
await setCategory(this.note.id, category)
this.loading.category = false
}
},
async onCategoryChange(result) {
this.showCategorySelect = false
const category = result?.id ?? result?.label ?? null
if (category !== null && this.note.category !== category) {
this.loading.category = true
try {
await setCategory(this.note.id, category)
} finally {
this.loading.category = false
}
}
},
🧰 Tools
🪛 ast-grep (0.45.0)

[warning] 186-186: Avoid using the initial state variable in setState
Context: setCategory(this.note.id, category)
Note: [CWE-710] Improper Adherence to Coding Standards. Security best practice.

(setstate-same-var)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/noteActions.js` around lines 182 - 190, Update
onCategoryChange to wrap the awaited setCategory call in a try/finally, matching
onColorSelected, so loading.category is always reset when setCategory succeeds
or rejects; preserve the existing category validation and error propagation
behavior.

Comment on lines +94 to +96
:taggable="true"
@input="onCategoryChange"
@search-change="onCategoryChange"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

For @nextcloud/vue version 8.39.0, what event and payload does NcActionInput with type="multiselect" emit when taggable creates a new tag?

💡 Result:

In @nextcloud/vue version 8.39.0, the NcActionInput component with type="multiselect" does not natively expose a taggable property or emit a specific event for the creation of new tags. When NcActionInput is used with type="multiselect", it internally delegates to the NcSelect component [1][2]. The NcActionInput component itself is configured to emit standard events such as input, submit, change, and update:value (or update:modelValue) to handle changes to its state [1][3][2]. If functionality for creating tags is required, developers typically use the NcSelectTags component, which is designed for tag management and handles tag selection and input [4][3]. NcSelectTags emits update:modelValue (and its legacy equivalents) when tags are added or removed [4]. Please note that earlier versions of the library contained a deprecated NcMultiselect component, which has been removed in favor of NcSelect and its specialized variants [5][6].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Package versions:"
if [ -f package.json ]; then
  node -e "const p=require('./package.json'); console.log(JSON.stringify({dependencies:p.dependencies?.['`@nextcloud/vue`'], devDependencies:p.devDependencies?.['`@nextcloud/vue`']}, null, 2))"
fi

echo
echo "Locate related files:"
fd -a 'NoteCard\.vue|noteActions\.js|package-lock\.json|pnpm-lock\.yaml|yarn\.lock' . | sed 's#^\./##'

echo
echo "Inspect NoteCard around multiselect/taggable:"
if [ -f src/components/NoteCard.vue ]; then
  nl -ba src/components/NoteCard.vue | sed -n '70,120p'
fi

echo
echo "Inspect noteActions.js:"
if [ -f src/components/noteActions.js ]; then
  nl -ba src/components/noteActions.js | sed -n '1,220p'
fi

echo
echo "Search onCategoryChange usages and taggable multiselect events:"
rg -n "onCategoryChange|taggable|`@search-change`|`@tag`|`@create`|type=.*multiselect|NcActionInput" src package.json

Repository: zentala/notes-plus

Length of output: 450


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Inspect NoteCard.vue around multiselect/taggable:"
awk '{printf "%5d  %s\n", NR, $0}' src/components/NoteCard.vue | sed -n '70,120p'

echo
echo "Inspect noteActions.js:"
awk '{printf "%5d  %s\n", NR, $0}' src/components/noteActions.js | sed -n '1,220p'

echo
echo "Search relevant usages/events:"
rg -n "onCategoryChange|taggable|search-change|tag|create|type=.*multiselect|NcActionInput" src package.json

echo
echo "Fetch `@nextcloud/vue` 8.39.0 NcActionInput / NcSelect dist docs/events source references:"
python3 - <<'PY'
import json, urllib.request
urls = [
    "https://unpkg.com/@nextcloud/vue@8.39.0/lib/components/NcActionInput.d.ts",
    "https://unpkg.com/@nextcloud/vue@8.39.0/lib/components/NcInputField.d.ts",
    "https://unpkg.com/@nextcloud/vue@8.39.0/lib/components/NcSelect.d.ts",
]
for url in urls:
    print(f"\n--- {url}")
    try:
        with urllib.request.urlopen(url, timeout=20) as r:
            data = r.read().decode("utf-8", errors="replace")
        lines = data.splitlines()
        for i,line in enumerate(lines,1):
            if any(x in line for x in ["component:", "defineEmits", "NcMultiselect", "NcSelect", "create", "tag", "search-change", "update:modelValue"]):
                print(f"{i:5d} {line}")
    except Exception as e:
        print(f"ERROR: {type(e).__name__}: {e}")
PY

Repository: zentala/notes-plus

Length of output: 14957


🌐 Web query:

@nextcloud/vue 8.39.0 NcSelect taggable create event payload emit update:modelValue

💡 Result:

In @nextcloud/vue (specifically version 8.x and later), NcSelect and its variants like NcSelectTags use standard Vue 3-style v-model binding [1][2]. When using the taggable prop, the component emits an update:modelValue event to signal changes to the selected value [1][2]. Key details regarding the event payload and usage: 1. v-model Compatibility: The component uses modelValue as its primary prop for the selected value and emits update:modelValue for two-way data binding [1]. In older versions or when using the passthru prop, it may still support legacy input event patterns, but the modern implementation relies on update:modelValue [3][1]. 2. Event Payload: The payload emitted with the update:modelValue event is the updated value of the selection [1]. When taggable is true, and a user creates a new tag, the payload will include that new value (typically as a string, or an object if the internal structure requires it) alongside any previously selected items [1][2]. 3. Implementation Note: If you are encountering issues where new tags are not being captured or the selection appears null, ensure you are correctly binding v-model (or:modelValue and @update:modelValue) and that your logic handles the potential null or empty states, especially when clearing the selection [4]. For the most accurate implementation details for your specific version, please consult the documentation hosted at https://nextcloud-vue-components.netlify.app/ for v9 (Vue 3) or the stable8 documentation for v8 (Vue 2), as the underlying implementation of NcSelect heavily leverages vue-select and these mechanisms are standard for that library within the Nextcloud ecosystem [3][1][5].

Citations:


🌐 Web query:

@nextcloud/vue 8.39.0 NcActionInput type multiselect taggable search-change input event

💡 Result:

In @nextcloud/vue version 8.39.0, the NcActionInput component does not natively support a multiselect type with taggable functionality [1][2]. Historically, NcActionInput included a multiselect option [2][3], but the underlying NcMultiselect component has been deprecated and removed from the library [4]. The library has migrated to using NcSelect for selection-based inputs [4], and NcActionInput has been updated to use NcSelect instead of the older NcMultiselect component [4]. Regarding the search-change event: 1. NcActionInput itself does not expose a native search-change event [2][3]. It primarily emits standard input events such as input, change, and submit [3][5]. 2. If you are implementing a searchable or taggable multiselect interface, the recommended approach is to use the NcSelect component directly [4]. NcSelect provides the necessary functionality for multiselect, taggable interfaces, and search-related events [4]. If you need custom search behavior, you should handle this via the events provided by the underlying component (e.g., NcSelect) rather than expecting NcActionInput to bridge a search-change event [4][5].

Citations:


🌐 Web query:

nextcloud-vue 8.39.0 source NcActionInput multiselect NcSelect taggable

💡 Result:

In nextcloud-vue v8.39.0 [1], NcMultiselect is deprecated [2][3], and its functionality has been superseded by NcSelect [2]. NcActionInput includes support for a multiselect type [4][5]. When using NcActionInput with type="multiselect", the component internally utilizes NcSelect to provide the selection interface [5]. For tag-based selection, the recommended component is NcSelectTags [6][7]. This component wraps NcSelect and is specifically designed to handle tag lists, including features like automatic fetching of system tags or accepting custom options, as well as configuration for multiple selections [6][8][7]. Key Implementation Details: - Migration: All components that previously used NcMultiselect have been migrated to NcSelect [2][3]. - NcActionInput: Its type prop includes multiselect as a valid value [5]. It is not designed to accept a fully custom NcSelect instance directly as an input type, but rather switches internal behavior when type="multiselect" is set [5]. - NcSelectTags: This is the dedicated component for taggable interfaces [6][7]. It supports the multiple prop (defaulting to true) and allows for filtering or custom tagging behavior via its props, which are largely inherited from or modeled after NcSelect [6][7]. For current usage and API definitions, refer to the Nextcloud Vue Style Guide documentation site, which provides the authoritative reference for available props and component patterns [9][10].

Citations:


🌐 Web query:

nextcloud-vue 8.39.0 changelog NcMultiselect removed NcSelect NcActionInput

💡 Result:

The Nextcloud Vue library version 8.39.0, released on May 7, 2026, does not contain any breaking changes such as the removal of NcMultiselect, NcSelect, or NcActionInput [1][2]. The changelog for version 8.39.0 focuses on minor enhancements, specifically: - Changes to the active state design for NcAppNavigationItem and NcListItem [1][3]. - Adjustments to the NcAppContent edge style for Nextcloud 34+ [1][3]. - A redesign of the active tab in NcAppSidebarTab (as a rounded filled pill) [1][3]. - A new navigation design for NcAppSettingsDialog [1][3]. The components mentioned (NcMultiselect, NcSelect, NcActionInput) remain part of the library, though it is important to note that major breaking changes, including various component refactors and property renames, were introduced in version 9.0.0 (released in September 2025) [4][5]. For instance, NcSelect underwent significant updates in that transition, such as the removal of the userSelect property [4]. Version 8.x serves as a maintenance branch, and no such removal of these specific core components occurred in the 8.39.0 release [1][6].

Citations:


🌐 Web query:

nextcloud-vue 8.39.0 NcActionInput search-change event handler emits search string

💡 Result:

In nextcloud-vue version 8.39.0, the NcActionInput component does not natively expose a search-change event [1][2]. Historically, NcActionInput has emitted standard events such as input, submit, change, and update:value (which has since been migrated to update:modelValue) [1][3][2][4]. There is no documented search-change event handler for this component [1][2]. It is possible that such an event was confused with search functionality implemented in other components (such as NcSelectUsers, which does feature a search event) [5][6] or that a custom event handler was being manually implemented in a parent component. To handle input changes, you should rely on the standard Vue 3 v-model binding, which uses the update:modelValue event [3][4]. If you require specific search-as-you-type behavior, you may need to listen to the input event on the underlying input element or implement a debounced handler within the component wrapping the NcActionInput [1][2].

Citations:


🌐 Web query:

nextcloud-vue 8.39.0 NcActionInput search-change NcInputField emit

💡 Result:

In nextcloud-vue v8.39.0, both NcActionInput and NcInputField have undergone significant changes regarding event handling as part of the library's migration to Vue 3 standards and improved event naming conventions [1][2]. Key points regarding the components and their events: 1. Model Binding and Event Renaming: The value prop has been renamed to modelValue, and the update:value or input events have been replaced by the standard update:modelValue event [1]. This change applies to nearly all input-related components in the library, including NcActionInput and NcInputField [1]. 2. Event Naming: As part of a refactoring effort to standardize event names, custom events in nextcloud-vue have been updated to use camelCase [2]. When using these components in Vue 3, you should use the camelCase event names (e.g., @update:modelValue) to match the emitted events [2]. 3. Regarding "search-change": There is no standard event named search-change emitted by NcActionInput or NcInputField in the core library [3][1][4][5]. If you are experiencing a behavior related to a search input, it is likely that you are listening for a standard input, change, or update:modelValue event and reacting to the input value within your application logic [3][6]. If you are migrating or developing with version 8.39.0, ensure your code: - Uses v-model (which automatically binds to modelValue and listens for update:modelValue) instead of manual value binding and input events [1]. - Uses camelCase for custom event listeners [2]. - Consults the official documentation for the stable8 branch for component-specific API details, as the library maintains separate documentation for different major versions [7][8].

Citations:


🌐 Web query:

"search-change" "@nextcloud/vue" "NcSelect"

💡 Result:

The @nextcloud/vue library's NcSelect component uses the 'search' event to handle search queries [1][2]. When a user types in the input, the component emits the 'search' event with the current query string as the payload [1][2]. Developers typically use this event to trigger asynchronous data loading (e.g., API calls) to populate the select component's options [1][3][2]. It is standard practice to debounce this search handler to reduce the number of API requests while the user is typing [1][2][4]. Note that there is no 'search-change' event; the event used for tracking changes to the search input text within NcSelect is simply 'search' [1][2]. If you are migrating or working with newer versions of @nextcloud/vue: - The component has undergone significant changes in recent versions, including the migration to Vue 3 where the 'value' prop has been renamed to 'modelValue' and 'input' events have become 'update:modelValue' [5]. - For user selection specifically, the library provides a dedicated NcSelectUsers component, which is recommended over using the older 'user-select' property on NcSelect [6][5]. - Always consult the official documentation at https://nextcloud-vue-components.netlify.app/ for the latest API specifications relevant to your version [7][8].

Citations:


🌐 Web query:

"search-change" "@nextcloud/vue" "NcActionInput"

💡 Result:

NcActionInput is a UI component provided by the @nextcloud/vue library, designed to provide an input field within an action context [1][2]. It acts as a wrapper that allows developers to use various types of input fields (such as text, date, password, select, etc.) while maintaining a consistent Nextcloud-styled interface [1][2]. Key aspects of NcActionInput include: Functionality: It supports multiple input types via its type prop, including "text", "date", "datetime-local", "password", "number", "email", and others [1]. It integrates with the ActionGlobalMixin, which is standard for action-related components in the library [1]. Props: Key props include: - modelValue: The value of the input (notably, the component was updated to use modelValue instead of the deprecated value prop) [3][4]. - type: Specifies the type of input field to render [1]. - id and inputId: Used for accessibility and element identification [1]. - isNativePicker: A boolean flag to toggle the use of a native date-time picker [1]. Performance Considerations: As of mid-2025, the component has been identified as potentially heavy because it statically imports several large sub-components [2]. Efforts have been made to improve this, such as implementing lazy loading for its larger dependencies [2]. Usage: Like other components in @nextcloud/vue, it can be imported directly from the library [5]. Documentation: Developers can refer to the Nextcloud Vue Style Guide for live examples and specific API documentation regarding props, events, and slots [6][7]. Further technical details are tracked in the main repository's issues and changelogs [3][2].

Citations:


🌐 Web query:

@nextcloud/vue 8.39.0 dist NcSelect taggable $emit create taggableSelect

💡 Result:

In @nextcloud/vue v8.39.0, NcSelect is a wrapper component that leverages vue-select internally [1][2][3]. To enable the "taggable" functionality (where users can create new options on the fly), you pass the taggable prop directly to the NcSelect component [4]. When using taggable mode, NcSelect relies on vue-select's event handling. While standard value updates are emitted via the input event (or update:modelValue in newer patterns), custom creation of tags typically involves the taggable behavior managed by vue-select's internal logic. If you need to intercept or handle the creation of a new tag specifically, you should use the taggable prop alongside the create-option prop (if customization is needed) [4]. To emit custom events to a parent component when a user creates a tag, the common pattern is: 1. Define a wrapper method in your component that listens for the selection change or a specific creation event [4][5]. 2. Use the $emit function to pass the data to the parent [4][5]. Example implementation pattern: <NcSelect taggable:options="options" v-model="selected" @input="handleInput" /> <script> export default { methods: { handleInput(value) { // Custom logic for when a tag is selected or created this.$emit('create', value); } } } </script> Please note that for specific tag-related features (like system tags), Nextcloud provides specialized components such as NcSelectTags, which handle the API communication and tag creation logic automatically [1][3]. If you are working with standard tags, using NcSelectTags is recommended over raw NcSelect to ensure consistent behavior with the Nextcloud backend [1][3].

Citations:


🌐 Web query:

@nextcloud/vue 8.39.0 dist NcSelect defineEmits create

💡 Result:

In @nextcloud/vue version 8.39.0, the NcSelect component is primarily designed for use in Vue 2 environments [1]. As a Vue 2 component, it does not use the Vue 3 script setup syntax (which relies on defineEmits) internally [1]. Regarding your query about using create and defineEmits with NcSelect: 1. defineEmits Context: The defineEmits function is a Vue 3 compiler macro [2]. Since @nextcloud/vue v8.x targets Vue 2 [1], you should not expect to use defineEmits inside your own components when importing or wrapping NcSelect. Instead, in Vue 2, you would define emitted events via the emits option in your component definition. 2. Component Usage: NcSelect is a standard Vue component [3]. You generally do not need to "create" it manually via functions; you import and use it in your template as a declarative component [4][1]: import { NcSelect } from '@nextcloud/vue' // In your Vue 2 component export default { components: { NcSelect }, //... } 3. Interfacing with NcSelect: If you are trying to handle events from NcSelect, you listen to them directly in your template using the standard v-on syntax (e.g., @input or @update), rather than relying on defineEmits [5][6]. If you are transitioning your application to Vue 3, please note that @nextcloud/vue v9.x is the version target for Vue 3 [1]. You should migrate to v9.x to gain support for Vue 3 features like script setup and defineEmits [1].

Citations:


🌐 Web query:

github nextcloud-libraries nextcloud-vue v8.39.0 src components NcSelect.js create-option taggable

💡 Result:

In @nextcloud/vue v8.39.0, the NcSelect component is a wrapper around the third-party library vue-select [1][2][3]. Because it acts as a wrapper, it inherits many of the underlying features of vue-select, including taggable support [2][3]. To use taggable functionality in NcSelect: 1. Taggable Prop: You can enable tagging by passing the taggable prop (e.g.,:taggable="true") to the NcSelect component [2]. 2. Create-Option Prop: When using taggable, you may also need to define the create-option prop if you need to customize how new tags are generated from user input [2]. The create-option function should return an object that matches the structure of your existing options [2]. Example usage pattern: <NcSelect taggable:options="options":create-option="option => ({ id: option, label: option })" v-model="selection" /> Note that @nextcloud/vue components often undergo refactoring. For instance, specific specialized select components like NcSelectTags or NcSelectUsers have been introduced to replace or supplement general NcSelect usage [4][3]. Always check the official Nextcloud Vue Style Guide for the most up-to-date implementation recommendations for your specific Nextcloud version, as documentation for stable8 (the branch containing v8.39.0) is available at https://stable8--nextcloud-vue-components.netlify.app [5][1].

Citations:


🌐 Web query:

npm nextcloud-vue@8.39.0 source NcSelect create-option vue-select

💡 Result:

In the nextcloud-vue library (version 8.39.0), the NcSelect component is a wrapper around the third-party vue-select component [1]. Regarding your specific interest in "create-option" functionality: 1. Relationship to vue-select: NcSelect internally utilizes vue-select [1]. While it abstracts many configurations, it passes properties and slots down to the underlying vue-select instance. 2. Handling Custom Options: If you are attempting to enable the creation of new options (a feature native to vue-select via the taggable and create-option props), you should pass these props directly to the NcSelect component. 3. Implementation Strategy: You can pass the taggable="true" prop to NcSelect to enable tag creation. If you need custom logic for creating options, you can use the create-option prop (or equivalent vue-select API) via the NcSelect component interface. 4. Architectural Note: Please be aware that in recent versions of nextcloud-vue, there has been a push to decompose "super-components" [2]. For specific use cases involving users, the library now provides a dedicated NcSelectUsers component, which is the recommended replacement for the older user-select functionality previously embedded within NcSelect [3][2]. For the most accurate implementation details regarding props allowed by your specific version, consult the documentation for vue-select, as NcSelect propagates these standard vue-select properties [1]. You can explore the component's structure in your node_modules/@nextcloud/vue/dist/components/NcSelect directory or via the official style guide [4].

Citations:


Persist typed category tags through value updates.

onCategoryChange() needs to handle both string inputs from these typed tags and { id, label } option payloads; otherwise newly created categories are ignored by the existing selection code.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/NoteCard.vue` around lines 94 - 96, Update NoteCard’s
onCategoryChange handler to accept both typed string values and { id, label }
option payloads from the category selector. Normalize either input into the
existing category-selection flow so newly created typed tags are persisted
instead of ignored.

@zentala
zentala merged commit b8b4b60 into main Jul 29, 2026
8 of 12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant