1
Fork 0
mirror of https://github.com/thegeneralist01/archivr synced 2026-07-21 18:55:36 +02:00

feat: tag delete, rename, and per-user humanize-tags display setting (#16)

* feat: add tag delete and rename (backend + frontend)

- DELETE /api/archives/:id/tags/:tag_uid — deletes tag subtree via
  recursive CTE; entry_tag_assignments cascade automatically
- PATCH  /api/archives/:id/tags/:tag_uid { name } — renames a tag
  segment (case-preserving slug), cascades full_path to all descendants
  in one transaction using hierarchy CTE (not LIKE), returns updated Tag
- database: rename_tag + delete_tag with 7 unit tests covering subtree
  cascade, collision detection, descendant path rewrite, slug stripping
- TagsView: inline rename (pencil icon / double-click → input), × delete
  with confirmation dialog (warns about child tags)
- App.jsx: handleTagRenamed rewrites tagFilter for exact + descendant
  paths; handleTagDeleted clears filter for deleted subtree
- api.js: renameTag (PATCH, returns Tag) + deleteTag (DELETE)
- ContextRail: tag pills show displayPath(full_path) (humanized) with
  raw full_path in title tooltip; humanize-tags setting coming next

* feat: per-user humanize-tags display setting

- auth DB: idempotent migration adds users.humanize_slugs INTEGER DEFAULT 0
- GET /api/auth/me: returns humanize_slugs bool
- PATCH /api/auth/me: accepts { humanize_slugs: bool }, persisted via
  database::update_user_humanize_slugs
- displayPath() helper moved to utils.js (was local to ContextRail)
- TagsView: node label shows tag.name vs tag.slug based on humanizeTags
- ContextRail: pill label applies displayPath conditionally
- App.jsx: derives humanizeTags from currentUser.humanize_slugs, passes
  to TagsView + ContextRail; filter badge label humanized when on
- Settings > Profile: Display Preferences section with checkbox toggle,
  updates backend and currentUser immediately via setCurrentUser
- api.js: patchMe(patch) for generic PATCH /api/auth/me
- Tests: auth_me default=false, PATCH persists=true (2 route tests)
This commit is contained in:
TheGeneralist 2026-07-03 14:26:45 +02:00 committed by GitHub
parent d6b52ba06c
commit 55f85134df
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 699 additions and 69 deletions

View file

@ -12,6 +12,7 @@ import TagsView from './components/TagsView'
import CollectionsView from './components/CollectionsView'
import SettingsView from './components/SettingsView'
import ContextRail from './components/ContextRail'
import { displayPath } from './utils'
export const AuthContext = createContext(null);
@ -81,6 +82,8 @@ export default function App() {
return saved === 'true'
})
const humanizeTags = currentUser?.humanize_slugs ?? false;
// Persist captureDialogOpen to sessionStorage
useEffect(() => {
sessionStorage.setItem('captureDialogOpen', captureDialogOpen)
@ -186,6 +189,20 @@ export default function App() {
if (archiveId) fetchTags(archiveId).then(setTagNodes)
}, [archiveId])
const handleTagRenamed = useCallback((oldFullPath, newFullPath) => {
if (tagFilter === oldFullPath) {
setTagFilter(newFullPath);
} else if (tagFilter?.startsWith(oldFullPath + '/')) {
setTagFilter(newFullPath + tagFilter.slice(oldFullPath.length));
}
}, [tagFilter]);
const handleTagDeleted = useCallback((deletedFullPath) => {
if (tagFilter === deletedFullPath || tagFilter?.startsWith(deletedFullPath + '/')) {
setTagFilter(null);
}
}, [tagFilter]);
const handleEntryTitleChange = useCallback((entryUid, newTitle) => {
setEntries(prev => prev.map(e =>
e.entry_uid === entryUid ? { ...e, title: newTitle } : e
@ -251,7 +268,7 @@ export default function App() {
{resultCount && <><b>{resultCount.split(' ')[0]}</b>{' '}{resultCount.split(' ').slice(1).join(' ')}</>}
{tagFilter && (
<button className="tag-filter-badge" onClick={handleClearTagFilter}>
× {tagFilter}
× {humanizeTags ? displayPath(tagFilter) : tagFilter}
</button>
)}
</span>
@ -275,10 +292,15 @@ export default function App() {
{view === 'admin' && <AdminView archives={archives} />}
{view === 'tags' && (
<TagsView
archiveId={archiveId}
tagNodes={tagNodes}
tagFilter={tagFilter}
onTagFilterSet={handleTagFilterSet}
onViewChange={handleViewChange}
onTagRenamed={handleTagRenamed}
onTagDeleted={handleTagDeleted}
onTagsRefresh={handleTagsRefresh}
humanizeTags={humanizeTags}
/>
)}
{view === 'collections' && (
@ -295,6 +317,7 @@ export default function App() {
tagNodes={tagNodes}
onTagsRefresh={handleTagsRefresh}
onEntryTitleChange={handleEntryTitleChange}
humanizeTags={humanizeTags}
/>
</main>
<CaptureDialog