mirror of
https://github.com/narrrl/narl-workspace.git
synced 2026-11-04 00:47:01 +01:00
rename and stuff, better navigation
This commit is contained in:
1 parent
49b2056215
commit
d8ebf4b850
7 files changed
+140
-17
No files matched your search
@@ -45,8 +45,8 @@
|
||||
{workspace.board?.lists.reduce((n, list) => n + list.cards.length, 0) ?? 0} cards
|
||||
</span>
|
||||
<span class="tools">
|
||||
<button title="new card (o)" onclick={() => hooks.openCommandLine(':card ')}>+</button>
|
||||
<button title="new list" onclick={() => hooks.openCommandLine(':list ')}>[+]</button>
|
||||
<button title="new card on this list (o)" onclick={() => hooks.openCommandLine(':card ')}>+ card</button>
|
||||
<button title="new list (:list <name>)" onclick={() => hooks.openCommandLine(':list ')}>+ list</button>
|
||||
<button title="back to the tree (:board)" onclick={() => runCommand('board')}>tree</button>
|
||||
<button title="command line (:)" onclick={() => hooks.openCommandLine(':')}>:</button>
|
||||
<button title="help (?)" onclick={() => runCommand('help')}>?</button>
|
||||
@@ -91,12 +91,14 @@
|
||||
</button>
|
||||
</li>
|
||||
{:else}
|
||||
<li class="empty faint">—</li>
|
||||
<li class="empty faint">no cards — o adds one</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</section>
|
||||
{:else}
|
||||
<div class="empty faint">no lists — :list <name> makes one</div>
|
||||
<div class="empty faint">
|
||||
no lists yet — [+] above, or :list <name>, makes a column. then o, or +, adds a card to it.
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
['o', 'new document here, straight into insert mode'],
|
||||
['O', 'new folder here'],
|
||||
['i · a', 'edit the open document'],
|
||||
['R', 'rename what is marked — document, folder or card'],
|
||||
['b', "open this space's board"],
|
||||
['Esc · q', 'back out to the list'],
|
||||
['/', 'full-text search, inside the current space'],
|
||||
['n', 'clear the search'],
|
||||
@@ -31,6 +33,7 @@
|
||||
['Enter', 'open the document the card links to'],
|
||||
['t · Space', 'tick the card off, or un-tick it'],
|
||||
['o', 'new card on this list'],
|
||||
['R', 'rename the card'],
|
||||
['x', 'delete the card (asks first)'],
|
||||
['q · Esc', 'back to the tree'],
|
||||
]
|
||||
|
||||
@@ -35,6 +35,13 @@
|
||||
{workspace.nodes.length} {workspace.nodes.length === 1 ? 'item' : 'items'}
|
||||
</span>
|
||||
<span class="tools">
|
||||
<!-- The board is the one view with no row in the list to click, so it
|
||||
needs a door of its own; a space without one is offered the switch. -->
|
||||
{#if workspace.space?.has_board}
|
||||
<button class="board" title="board (b)" onclick={() => runCommand('board')}>board</button>
|
||||
{:else if workspace.space}
|
||||
<button class="board faint" title="give this space a board (:set board)" onclick={() => runCommand('set board')}>+board</button>
|
||||
{/if}
|
||||
<button title="new document (o)" onclick={() => runCommand('new')}>+</button>
|
||||
<button title="new folder (O)" onclick={() => hooks.openCommandLine(':mkdir ')}>[+]</button>
|
||||
<button title="search (/)" onclick={() => hooks.openCommandLine('/')}>/</button>
|
||||
@@ -156,6 +163,8 @@
|
||||
.count { flex: 1; text-align: right; overflow: hidden; white-space: nowrap; }
|
||||
.tools { display: flex; gap: 2px; flex: none; }
|
||||
.tools button { padding: 0 6px; line-height: 18px; }
|
||||
.tools .board { color: var(--accent); }
|
||||
.tools .board.faint { color: var(--fg-faint); }
|
||||
|
||||
/* Roomier tap targets where there is no keyboard. */
|
||||
@media (pointer: coarse) {
|
||||
|
||||
+42
-6
@@ -442,19 +442,55 @@ export const commands: CommandSpec[] = [
|
||||
},
|
||||
{
|
||||
name: 'name',
|
||||
aliases: ['title'],
|
||||
aliases: ['title', 'rename'],
|
||||
args: '<text>',
|
||||
group: 'editing',
|
||||
help: 'rename the open document, or the selected card on a board',
|
||||
help: 'rename what is under the cursor — document, folder, or card on a board',
|
||||
run: async (arg) => {
|
||||
if (workspace.view === 'board') {
|
||||
if (!arg.trim()) return workspace.say('usage: :name <text>', 'error')
|
||||
return workspace.guard(() => workspace.patchCard({ title: arg.trim() }))
|
||||
}
|
||||
if (!requireOpen()) return
|
||||
workspace.draft.name = arg
|
||||
workspace.dirty = true
|
||||
await workspace.guard(() => workspace.save())
|
||||
if (!arg.trim()) return workspace.say('usage: :name <text>', 'error')
|
||||
|
||||
// The open document is renamed through the draft, so an unsaved body is
|
||||
// written with the new name instead of being left behind by it.
|
||||
if (workspace.open) {
|
||||
workspace.draft.name = arg
|
||||
workspace.dirty = true
|
||||
return workspace.guard(() => workspace.save())
|
||||
}
|
||||
|
||||
const node = workspace.selected
|
||||
if (!node) return workspace.say('nothing selected — :name! renames the space', 'error')
|
||||
await workspace.guard(() => workspace.renameNode(node.id, arg))
|
||||
},
|
||||
bang: {
|
||||
help: 'rename the space you are in',
|
||||
run: async (arg) => {
|
||||
const space = workspace.space
|
||||
if (!space) return workspace.say('no space open', 'error')
|
||||
if (!arg.trim()) return workspace.say('usage: :name! <text>', 'error')
|
||||
await workspace.guard(() => workspace.renameNode(space.id, arg))
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'rmspace',
|
||||
aliases: ['rms'],
|
||||
args: '[name]',
|
||||
group: 'tree',
|
||||
help: 'delete a space and everything in it (asks first)',
|
||||
run: async (arg) => {
|
||||
const wanted = arg.trim().toLowerCase()
|
||||
const space = wanted
|
||||
? workspace.spaces.find((s) => s.slug === wanted || s.name.toLowerCase() === wanted)
|
||||
: workspace.space
|
||||
if (!space) return workspace.say(`no such space: ${wanted || '(none open)'}`, 'error')
|
||||
if (!confirm(`delete the space "${space.name}" and everything in it? this cannot be undone.`)) {
|
||||
return
|
||||
}
|
||||
await workspace.guard(() => workspace.deleteSpace(space.id))
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -417,6 +417,55 @@ class Workspace {
|
||||
this.say(removed > 1 ? `deleted ${removed} nodes` : `deleted #${id}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Rename anything in the tree — a document, a folder or a space. The body
|
||||
* has to ride along because the update is a whole-node write; for the node
|
||||
* being edited that is the draft, which would otherwise be rolled back to
|
||||
* whatever the server still holds.
|
||||
*/
|
||||
async renameNode(id: number, name: string) {
|
||||
const current =
|
||||
this.open?.id === id ? { ...this.open, body: this.draft.body } : await api.getNode(id)
|
||||
const saved = await api.updateNode(id, {
|
||||
name: name.trim(),
|
||||
body: current.body,
|
||||
created_at: current.created_at,
|
||||
})
|
||||
if (this.open?.id === id) {
|
||||
this.open = saved
|
||||
this.draft.name = saved.name
|
||||
}
|
||||
if (this.path.some((crumb) => crumb.id === id)) this.path = saved.path
|
||||
if (saved.kind === 'space') this.spaces = await api.listSpaces()
|
||||
await this.refresh()
|
||||
this.say(`renamed to ${saved.slug}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a space and everything in it. The app has to land somewhere
|
||||
* afterwards, so it falls into the next space — or, if that was the last
|
||||
* one, into an empty workspace that `:space! <name>` fills again.
|
||||
*/
|
||||
async deleteSpace(id: number) {
|
||||
const { removed } = await api.deleteNode(id)
|
||||
this.open = null
|
||||
this.editing = false
|
||||
this.board = null
|
||||
this.view = 'tree'
|
||||
this.spaces = await api.listSpaces()
|
||||
const next = this.spaces.find((space) => space.slug === 'diary') ?? this.spaces[0]
|
||||
if (next) await this.enterSpace(next.id)
|
||||
else {
|
||||
localStorage.removeItem('diary:space')
|
||||
this.spaceId = null
|
||||
this.parentId = null
|
||||
this.path = []
|
||||
this.nodes = []
|
||||
this.cursor = 0
|
||||
}
|
||||
this.say(`deleted the space and ${removed - 1} node(s) in it`)
|
||||
}
|
||||
|
||||
/** Reparent the node under the cursor, for `:mv`. */
|
||||
async moveNode(id: number, parentId: number) {
|
||||
await api.moveNode(id, parentId)
|
||||
|
||||
Reference in new issue
Block a user