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:
parent
d6b52ba06c
commit
55f85134df
13 changed files with 699 additions and 69 deletions
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue