mirror of
https://github.com/narrrl/narl-workspace.git
synced 2026-11-03 16:41:27 +01:00
keys act on the focused pane, deletes ask y/N in place
dd, x, i, R, s, y and :mv used the open document whenever one was open, so walking the list and pressing dd deleted the document beside it. They now act on the row under the cursor while the list has focus. Deletes ask an in-app y/N naming the node instead of confirm(), and the board no longer lets tree keys through to the hidden list.
This commit is contained in:
1 parent
4ded89b0e7
commit
d9b78170d8
6 files changed
+149
-28
No files matched your search
@@ -49,7 +49,9 @@ works once the frontend has been built at least once.
|
|||||||
|
|
||||||
## Keys
|
## Keys
|
||||||
|
|
||||||
The desktop UI is modal. In the browse pane:
|
The desktop UI is modal, and a key acts on whatever has focus: in the list
|
||||||
|
that is the row under the cursor, even while another document is open beside
|
||||||
|
it; in the document pane it is that document. In the browse pane:
|
||||||
|
|
||||||
| key | action |
|
| key | action |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
@@ -65,7 +67,7 @@ The desktop UI is modal. In the browse pane:
|
|||||||
| `h` | up one level in the tree |
|
| `h` | up one level in the tree |
|
||||||
| `/` | full-text search (`n` clears it) |
|
| `/` | full-text search (`n` clears it) |
|
||||||
| `s` / `y` | toggle sharing / mint a new share link |
|
| `s` / `y` | toggle sharing / mint a new share link |
|
||||||
| `x`, `dd` | delete the entry |
|
| `x`, `dd` | delete the entry under the cursor (asks `y`/`N`) |
|
||||||
| `:` | command line |
|
| `:` | command line |
|
||||||
| `Ctrl-S` | save from anywhere |
|
| `Ctrl-S` | save from anywhere |
|
||||||
| `?` | help |
|
| `?` | help |
|
||||||
|
|||||||
+36
-5
@@ -9,6 +9,7 @@
|
|||||||
import Login from './lib/Login.svelte'
|
import Login from './lib/Login.svelte'
|
||||||
import Overview from './lib/Overview.svelte'
|
import Overview from './lib/Overview.svelte'
|
||||||
import MediaBrowser from './lib/MediaBrowser.svelte'
|
import MediaBrowser from './lib/MediaBrowser.svelte'
|
||||||
|
import Prompt from './lib/Prompt.svelte'
|
||||||
import Rail from './lib/Rail.svelte'
|
import Rail from './lib/Rail.svelte'
|
||||||
import Shared from './lib/Shared.svelte'
|
import Shared from './lib/Shared.svelte'
|
||||||
import Sidebar from './lib/Sidebar.svelte'
|
import Sidebar from './lib/Sidebar.svelte'
|
||||||
@@ -58,6 +59,23 @@
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/*
|
||||||
|
* A pending `[y/N]` takes the next key before anything else sees it — the
|
||||||
|
* editor included, where `y` would otherwise start a yank. Only `y` agrees;
|
||||||
|
* every other key, Escape among them, declines.
|
||||||
|
*/
|
||||||
|
$effect(() => {
|
||||||
|
const answer = (event: KeyboardEvent) => {
|
||||||
|
const prompt = workspace.prompt
|
||||||
|
if (!prompt || ['Shift', 'Control', 'Alt', 'Meta', 'CapsLock'].includes(event.key)) return
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
prompt.resolve(event.key === 'y' || event.key === 'Y')
|
||||||
|
}
|
||||||
|
window.addEventListener('keydown', answer, true)
|
||||||
|
return () => window.removeEventListener('keydown', answer, true)
|
||||||
|
})
|
||||||
|
|
||||||
// Guard against losing a half-written entry to a stray refresh.
|
// Guard against losing a half-written entry to a stray refresh.
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (!workspace.dirty) return
|
if (!workspace.dirty) return
|
||||||
@@ -178,9 +196,9 @@
|
|||||||
|
|
||||||
/*
|
/*
|
||||||
* The board reads the same keys as the list, one dimension wider: h/l walk
|
* The board reads the same keys as the list, one dimension wider: h/l walk
|
||||||
* the lists, j/k the cards, and the shifted pair takes the card along. Only
|
* the lists, j/k the cards, and the shifted pair takes the card along.
|
||||||
* `:` and `?` fall through to the keys below, because everything else would
|
* Nothing falls through to the tree keys, because they would act on a tree
|
||||||
* act on a tree that is not on screen.
|
* that is not on screen.
|
||||||
*/
|
*/
|
||||||
function boardKey(key: string): boolean {
|
function boardKey(key: string): boolean {
|
||||||
switch (key) {
|
switch (key) {
|
||||||
@@ -331,7 +349,12 @@
|
|||||||
workspace.cardCursor = 0
|
workspace.cardCursor = 0
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (key === 'g') {
|
if (previous === 'd' && key === 'd') {
|
||||||
|
event.preventDefault()
|
||||||
|
void runCommand('delete')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (key === 'g' || key === 'd') {
|
||||||
pending = key
|
pending = key
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -339,6 +362,12 @@
|
|||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (key === ':' || key === '?') {
|
||||||
|
event.preventDefault()
|
||||||
|
if (key === ':') cmdline = ':'
|
||||||
|
else void runCommand('help')
|
||||||
|
}
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -444,7 +473,7 @@
|
|||||||
break
|
break
|
||||||
case 's':
|
case 's':
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
void runCommand(workspace.selected?.shared ? 'unshare' : 'share')
|
void runCommand(workspace.target?.shared ? 'unshare' : 'share')
|
||||||
break
|
break
|
||||||
case 'y':
|
case 'y':
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
@@ -543,6 +572,8 @@
|
|||||||
<CommandLine initial={cmdline} close={() => (cmdline = null)} />
|
<CommandLine initial={cmdline} close={() => (cmdline = null)} />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
<Prompt />
|
||||||
|
|
||||||
{#if workspace.overlay !== 'none'}
|
{#if workspace.overlay !== 'none'}
|
||||||
<!-- svelte-ignore a11y_click_events_have_key_events, a11y_no_static_element_interactions, a11y_no_noninteractive_tabindex -->
|
<!-- svelte-ignore a11y_click_events_have_key_events, a11y_no_static_element_interactions, a11y_no_noninteractive_tabindex -->
|
||||||
<div class="overlay" tabindex="-1" bind:this={overlayEl} onclick={(e) => e.target === e.currentTarget && (workspace.overlay = 'none')}
|
<div class="overlay" tabindex="-1" bind:this={overlayEl} onclick={(e) => e.target === e.currentTarget && (workspace.overlay = 'none')}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
['o', 'new document here, straight into insert mode'],
|
['o', 'new document here, straight into insert mode'],
|
||||||
['O', 'new folder here'],
|
['O', 'new folder here'],
|
||||||
['i · a', 'edit the open document'],
|
['i · a', 'edit the open document'],
|
||||||
['R', 'rename what is marked — document, folder or card'],
|
['R', 'rename what is under the cursor — document, folder or card'],
|
||||||
['m / p', 'pick a node up, then drop it into the folder you walk to'],
|
['m / p', 'pick a node up, then drop it into the folder you walk to'],
|
||||||
['b', "open this space's board"],
|
['b', "open this space's board"],
|
||||||
['Esc · q', 'close the document'],
|
['Esc · q', 'close the document'],
|
||||||
@@ -25,7 +25,7 @@
|
|||||||
['n', 'clear the search'],
|
['n', 'clear the search'],
|
||||||
['s', 'toggle sharing for the document'],
|
['s', 'toggle sharing for the document'],
|
||||||
['y', 'mint a new share link and copy it'],
|
['y', 'mint a new share link and copy it'],
|
||||||
['x · dd', 'delete what is selected (asks first)'],
|
['x · dd', 'delete what has focus — the row in the list, the document in its pane (asks y/N)'],
|
||||||
[':', 'command line'],
|
[':', 'command line'],
|
||||||
['Ctrl-S', 'save, from anywhere'],
|
['Ctrl-S', 'save, from anywhere'],
|
||||||
['?', 'this screen — in the editor too'],
|
['?', 'this screen — in the editor too'],
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
/*
|
||||||
|
* The `[y/N]` question `workspace.ask` raises, drawn where the command line
|
||||||
|
* opens. The keys are read in App.svelte, ahead of everything else, so a
|
||||||
|
* stray `y` can never reach the editor or the list underneath.
|
||||||
|
*/
|
||||||
|
import { workspace } from './store.svelte'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if workspace.prompt}
|
||||||
|
<div class="prompt" role="alertdialog" aria-label={workspace.prompt.text}>
|
||||||
|
<span class="text">{workspace.prompt.text}</span>
|
||||||
|
<span class="choice">
|
||||||
|
<button class="yes" onclick={() => workspace.prompt?.resolve(true)}><span class="kbd">y</span> yes</button>
|
||||||
|
<button onclick={() => workspace.prompt?.resolve(false)}>no</button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.prompt {
|
||||||
|
position: absolute;
|
||||||
|
left: calc(var(--frame) + 12px);
|
||||||
|
right: calc(var(--rail-w) + 12px);
|
||||||
|
bottom: calc(var(--status-h) - 1px);
|
||||||
|
z-index: 40;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 10px 14px;
|
||||||
|
background: color-mix(in oklab, var(--hull) 97%, transparent);
|
||||||
|
border: 1px solid var(--alarm);
|
||||||
|
border-bottom: 0;
|
||||||
|
border-radius: var(--radius-l) var(--radius-l) 0 0;
|
||||||
|
font-size: 13px;
|
||||||
|
animation: rise var(--t-slow) var(--ease-snap) both;
|
||||||
|
}
|
||||||
|
.text { flex: 1; min-width: 0; color: var(--light); overflow-wrap: anywhere; }
|
||||||
|
.choice { flex: none; display: flex; gap: 6px; align-items: center; }
|
||||||
|
.yes:hover { background: var(--alarm); border-color: var(--alarm); color: var(--ink); }
|
||||||
|
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
.prompt { left: 8px; right: 8px; flex-wrap: wrap; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
+28
-18
@@ -234,7 +234,9 @@ export const commands: CommandSpec[] = [
|
|||||||
bang: {
|
bang: {
|
||||||
help: 'add a card linked to the open document — Enter on it opens the document',
|
help: 'add a card linked to the open document — Enter on it opens the document',
|
||||||
run: async (arg) => {
|
run: async (arg) => {
|
||||||
const node = workspace.open ?? workspace.selected
|
// On the board the tree is not on screen, so only the open document
|
||||||
|
// can be meant; elsewhere it is whatever has focus.
|
||||||
|
const node = workspace.view === 'board' ? workspace.open : workspace.target
|
||||||
if (!node || node.kind !== 'document') {
|
if (!node || node.kind !== 'document') {
|
||||||
return workspace.say('no document to link — open one first', 'error')
|
return workspace.say('no document to link — open one first', 'error')
|
||||||
}
|
}
|
||||||
@@ -287,7 +289,8 @@ export const commands: CommandSpec[] = [
|
|||||||
if (workspace.view !== 'board') return workspace.say('no board open — :board', 'error')
|
if (workspace.view !== 'board') return workspace.say('no board open — :board', 'error')
|
||||||
const list = workspace.list
|
const list = workspace.list
|
||||||
if (!list) return workspace.say('no list selected', 'error')
|
if (!list) return workspace.say('no list selected', 'error')
|
||||||
if (!confirm(`delete the list "${list.name}" and its ${list.cards.length} cards?`)) return
|
const cards = list.cards.length
|
||||||
|
if (!(await workspace.ask(`delete the list "${list.name}" and its ${cards} card${cards === 1 ? '' : 's'}?`))) return
|
||||||
await workspace.guard(() => workspace.deleteList())
|
await workspace.guard(() => workspace.deleteList())
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -299,7 +302,7 @@ export const commands: CommandSpec[] = [
|
|||||||
group: 'tree',
|
group: 'tree',
|
||||||
help: 'move the selected node into a folder here, or up one level',
|
help: 'move the selected node into a folder here, or up one level',
|
||||||
run: async (arg) => {
|
run: async (arg) => {
|
||||||
const node = workspace.open ?? workspace.selected
|
const node = workspace.target
|
||||||
if (!node) return workspace.say('nothing selected', 'error')
|
if (!node) return workspace.say('nothing selected', 'error')
|
||||||
const target = arg.trim()
|
const target = arg.trim()
|
||||||
if (!target) return workspace.say('usage: :mv <folder|..>', 'error')
|
if (!target) return workspace.say('usage: :mv <folder|..>', 'error')
|
||||||
@@ -364,7 +367,11 @@ export const commands: CommandSpec[] = [
|
|||||||
group: 'editing',
|
group: 'editing',
|
||||||
help: 'edit the open document, or open a node by id',
|
help: 'edit the open document, or open a node by id',
|
||||||
run: async (arg) => {
|
run: async (arg) => {
|
||||||
const id = arg ? Number(arg) : workspace.open?.id ?? workspace.selected?.id
|
const node = arg ? undefined : workspace.target
|
||||||
|
if (node && node.kind !== 'document') {
|
||||||
|
return workspace.say(`${node.slug} is a ${node.kind} — l opens it`, 'error')
|
||||||
|
}
|
||||||
|
const id = arg ? Number(arg) : node?.id
|
||||||
if (!id || Number.isNaN(id)) return workspace.say('usage: :e <id>', 'error')
|
if (!id || Number.isNaN(id)) return workspace.say('usage: :e <id>', 'error')
|
||||||
await workspace.guard(() => workspace.openNode(id, true))
|
await workspace.guard(() => workspace.openNode(id, true))
|
||||||
hooks.focusEditor()
|
hooks.focusEditor()
|
||||||
@@ -424,17 +431,20 @@ export const commands: CommandSpec[] = [
|
|||||||
if (workspace.view === 'board') {
|
if (workspace.view === 'board') {
|
||||||
const card = workspace.card
|
const card = workspace.card
|
||||||
if (!card) return workspace.say('no card selected', 'error')
|
if (!card) return workspace.say('no card selected', 'error')
|
||||||
if (!confirm(`delete the card "${card.title}"?`)) return
|
if (!(await workspace.ask(`delete the card "${card.title}"?`))) return
|
||||||
return workspace.guard(() => workspace.deleteCard())
|
return workspace.guard(() => workspace.deleteCard())
|
||||||
}
|
}
|
||||||
const id = arg ? Number(arg) : workspace.open?.id ?? workspace.selected?.id
|
// Whatever has focus: the row under the cursor in the list, the open
|
||||||
|
// document only while the document pane is the one being looked at.
|
||||||
|
const id = arg ? Number(arg) : workspace.target?.id
|
||||||
if (!id || Number.isNaN(id)) return workspace.say('nothing to delete', 'error')
|
if (!id || Number.isNaN(id)) return workspace.say('nothing to delete', 'error')
|
||||||
const node = workspace.open?.id === id ? workspace.open : workspace.nodes.find((n) => n.id === id)
|
const node = workspace.open?.id === id ? workspace.open : workspace.nodes.find((n) => n.id === id)
|
||||||
|
const name = node ? `"${node.name.trim() || node.slug}"` : `#${id}`
|
||||||
const what =
|
const what =
|
||||||
node && node.kind !== 'document'
|
node && node.kind !== 'document'
|
||||||
? `delete ${node.kind} "${node.name}" and everything in it?`
|
? `delete the ${node.kind} ${name} and everything in it?`
|
||||||
: `delete #${id}?`
|
: `delete ${name}?`
|
||||||
if (!confirm(`${what} this cannot be undone.`)) return
|
if (!(await workspace.ask(`${what} this cannot be undone.`))) return
|
||||||
await workspace.guard(() => workspace.deleteNode(id))
|
await workspace.guard(() => workspace.deleteNode(id))
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -453,13 +463,13 @@ export const commands: CommandSpec[] = [
|
|||||||
|
|
||||||
// The open document is renamed through the draft, so an unsaved body is
|
// 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.
|
// written with the new name instead of being left behind by it.
|
||||||
if (workspace.open) {
|
const node = workspace.target
|
||||||
|
if (node && node.id === workspace.open?.id) {
|
||||||
workspace.draft.name = arg
|
workspace.draft.name = arg
|
||||||
workspace.dirty = true
|
workspace.dirty = true
|
||||||
return workspace.guard(() => workspace.save())
|
return workspace.guard(() => workspace.save())
|
||||||
}
|
}
|
||||||
|
|
||||||
const node = workspace.selected
|
|
||||||
if (!node) return workspace.say('nothing selected — :name! renames the space', 'error')
|
if (!node) return workspace.say('nothing selected — :name! renames the space', 'error')
|
||||||
await workspace.guard(() => workspace.renameNode(node.id, arg))
|
await workspace.guard(() => workspace.renameNode(node.id, arg))
|
||||||
},
|
},
|
||||||
@@ -485,7 +495,7 @@ export const commands: CommandSpec[] = [
|
|||||||
? workspace.spaces.find((s) => s.slug === wanted || s.name.toLowerCase() === wanted)
|
? workspace.spaces.find((s) => s.slug === wanted || s.name.toLowerCase() === wanted)
|
||||||
: workspace.space
|
: workspace.space
|
||||||
if (!space) return workspace.say(`no such space: ${wanted || '(none open)'}`, 'error')
|
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.`)) {
|
if (!(await workspace.ask(`delete the space "${space.name}" and everything in it? this cannot be undone.`))) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
await workspace.guard(() => workspace.deleteSpace(space.id))
|
await workspace.guard(() => workspace.deleteSpace(space.id))
|
||||||
@@ -510,9 +520,9 @@ export const commands: CommandSpec[] = [
|
|||||||
group: 'sharing',
|
group: 'sharing',
|
||||||
help: 'publish the document behind an unguessable link and copy it',
|
help: 'publish the document behind an unguessable link and copy it',
|
||||||
run: async () => {
|
run: async () => {
|
||||||
const id = workspace.open?.id ?? workspace.selected?.id
|
const node = workspace.target
|
||||||
|
const id = node?.id
|
||||||
if (!id) return workspace.say('nothing selected', 'error')
|
if (!id) return workspace.say('nothing selected', 'error')
|
||||||
const node = workspace.open?.id === id ? workspace.open : workspace.selected
|
|
||||||
if (node?.shared) return workspace.say('already shared — :link for a new link, :unshare to revoke')
|
if (node?.shared) return workspace.say('already shared — :link for a new link, :unshare to revoke')
|
||||||
await workspace.guard(() => workspace.toggleShare(id))
|
await workspace.guard(() => workspace.toggleShare(id))
|
||||||
},
|
},
|
||||||
@@ -522,9 +532,9 @@ export const commands: CommandSpec[] = [
|
|||||||
group: 'sharing',
|
group: 'sharing',
|
||||||
help: 'revoke the share link',
|
help: 'revoke the share link',
|
||||||
run: async () => {
|
run: async () => {
|
||||||
const id = workspace.open?.id ?? workspace.selected?.id
|
const node = workspace.target
|
||||||
|
const id = node?.id
|
||||||
if (!id) return workspace.say('nothing selected', 'error')
|
if (!id) return workspace.say('nothing selected', 'error')
|
||||||
const node = workspace.open?.id === id ? workspace.open : workspace.selected
|
|
||||||
if (!node?.shared) return workspace.say('not shared')
|
if (!node?.shared) return workspace.say('not shared')
|
||||||
await workspace.guard(() => workspace.toggleShare(id))
|
await workspace.guard(() => workspace.toggleShare(id))
|
||||||
},
|
},
|
||||||
@@ -534,9 +544,9 @@ export const commands: CommandSpec[] = [
|
|||||||
group: 'sharing',
|
group: 'sharing',
|
||||||
help: 'mint a new share link for the document and copy it',
|
help: 'mint a new share link for the document and copy it',
|
||||||
run: async () => {
|
run: async () => {
|
||||||
const id = workspace.open?.id ?? workspace.selected?.id
|
const node = workspace.target
|
||||||
|
const id = node?.id
|
||||||
if (!id) return workspace.say('nothing selected', 'error')
|
if (!id) return workspace.say('nothing selected', 'error')
|
||||||
const node = workspace.open?.id === id ? workspace.open : workspace.selected
|
|
||||||
if (!node?.shared) return workspace.say('not shared — :share first', 'error')
|
if (!node?.shared) return workspace.say('not shared — :share first', 'error')
|
||||||
await workspace.guard(async () => {
|
await workspace.guard(async () => {
|
||||||
const url = await workspace.mintShareLink(id)
|
const url = await workspace.mintShareLink(id)
|
||||||
|
|||||||
@@ -65,6 +65,13 @@ class Workspace {
|
|||||||
flash = $state<Flash>(null)
|
flash = $state<Flash>(null)
|
||||||
|
|
||||||
overlay = $state<'none' | 'help' | 'media' | 'overview'>('none')
|
overlay = $state<'none' | 'help' | 'media' | 'overview'>('none')
|
||||||
|
|
||||||
|
/*
|
||||||
|
* A `[y/N]` question in the command line's place, instead of the browser's
|
||||||
|
* confirm(): it names the thing about to go, and it is answered from the
|
||||||
|
* keyboard without a modal the rest of the page cannot see past.
|
||||||
|
*/
|
||||||
|
prompt = $state<{ text: string; resolve: (yes: boolean) => void } | null>(null)
|
||||||
media = $state<MediaFile[]>([])
|
media = $state<MediaFile[]>([])
|
||||||
overview = $state<SpaceOverview[]>([])
|
overview = $state<SpaceOverview[]>([])
|
||||||
overviewCursor = $state(0)
|
overviewCursor = $state(0)
|
||||||
@@ -92,6 +99,15 @@ class Workspace {
|
|||||||
return this.nodes[this.cursor]
|
return this.nodes[this.cursor]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* What a verb acts on: the open document while the document pane has focus,
|
||||||
|
* the row under the cursor while the list does. Walking the list with a
|
||||||
|
* document still open must not make `dd` delete the document.
|
||||||
|
*/
|
||||||
|
get target(): NodeSummary | undefined {
|
||||||
|
return this.open && this.pane === 'document' ? this.open : this.selected
|
||||||
|
}
|
||||||
|
|
||||||
get space(): NodeSummary | undefined {
|
get space(): NodeSummary | undefined {
|
||||||
return this.spaces.find((s) => s.id === this.spaceId)
|
return this.spaces.find((s) => s.id === this.spaceId)
|
||||||
}
|
}
|
||||||
@@ -109,6 +125,19 @@ class Workspace {
|
|||||||
return this.list?.cards[this.cardCursor]
|
return this.list?.cards[this.cardCursor]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ask(text: string): Promise<boolean> {
|
||||||
|
this.prompt?.resolve(false)
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
this.prompt = {
|
||||||
|
text,
|
||||||
|
resolve: (yes) => {
|
||||||
|
this.prompt = null
|
||||||
|
resolve(yes)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
say(text: string, kind: 'info' | 'error' = 'info') {
|
say(text: string, kind: 'info' | 'error' = 'info') {
|
||||||
this.flash = { text, kind }
|
this.flash = { text, kind }
|
||||||
if (kind === 'info') {
|
if (kind === 'info') {
|
||||||
@@ -471,14 +500,18 @@ class Workspace {
|
|||||||
this.#autosave = null
|
this.#autosave = null
|
||||||
}
|
}
|
||||||
this.dropStash(id)
|
this.dropStash(id)
|
||||||
|
const node = this.open?.id === id ? this.open : this.nodes.find((n) => n.id === id)
|
||||||
const { removed } = await api.deleteNode(id)
|
const { removed } = await api.deleteNode(id)
|
||||||
if (this.open?.id === id) {
|
if (this.open?.id === id) {
|
||||||
this.open = null
|
this.open = null
|
||||||
this.editing = false
|
this.editing = false
|
||||||
|
this.dirty = false
|
||||||
this.pane = 'list'
|
this.pane = 'list'
|
||||||
}
|
}
|
||||||
|
if (this.marked?.id === id) this.marked = null
|
||||||
await this.refresh()
|
await this.refresh()
|
||||||
this.say(removed > 1 ? `deleted ${removed} nodes` : `deleted #${id}`)
|
const name = node ? `"${node.name.trim() || node.slug}"` : `#${id}`
|
||||||
|
this.say(removed > 1 ? `deleted ${name} and ${removed - 1} inside it` : `deleted ${name}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in new issue
Block a user