Skip to content

Commit f5f5a31

Browse files
committed
feat: add missing note/arrow property controls and consolidate page-level cards in right sidebar
- Add "Create new page", "Swap head and body", "Copy link", and "Set as default" buttons to NotePropertiesCard - Add local collapsing controls (local-collapsing and locally-collapsed switches) to note properties - Add timestamps display (created/edited/moved) to note properties with formatted dates - Add "Swap arrowheads", "Copy link", and "Set as default arrow style" buttons to ArrowPropertiesCard - Add anchor
1 parent 0229ccc commit f5f5a31

4 files changed

Lines changed: 286 additions & 27 deletions

File tree

new-deepnotes/apps/web/src/features/pages/PageEditorView.vue

Lines changed: 87 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,56 @@ async function updatePageTitle(type: "relative" | "absolute", value: string) {
321321
}
322322
}
323323
324+
function handleCopyNoteLink() {
325+
if (!selectedNoteId.value) return;
326+
const url = `${window.location.origin}/pages/${pageId.value}?elem=${selectedNoteId.value}`;
327+
navigator.clipboard.writeText(url);
328+
}
329+
330+
function handleSwapHeadBody() {
331+
if (!selectedNoteModel.value) return;
332+
const headFrag = selectedNoteModel.value.head?.value;
333+
const bodyFrag = selectedNoteModel.value.body?.value;
334+
if (!headFrag || !bodyFrag) return;
335+
// Swap Yjs XmlFragments by exchanging their internal content
336+
// This is a shallow swap: exchange the `value` references in the parent map
337+
const noteMap = selectedNoteModel.value.rawMap;
338+
if (!noteMap) return;
339+
const headMap = noteMap.get('head') as any;
340+
const bodyMap = noteMap.get('body') as any;
341+
if (!headMap || !bodyMap) return;
342+
const hVal = headMap.get('value');
343+
const bVal = bodyMap.get('value');
344+
headMap.set('value', bVal);
345+
bodyMap.set('value', hVal);
346+
}
347+
348+
async function handleSetNoteAsDefault() {
349+
pageOpsMessage.value = 'Set as default note style is not yet implemented in the new UI (requires serialization + encryption).';
350+
}
351+
352+
async function handleCreateNewPage() {
353+
pageOpsMessage.value = 'Create new page is not yet implemented in the new UI (requires page-creation crypto: encrypted titles + keyring).';
354+
}
355+
356+
function handleSwapArrowheads() {
357+
if (!selectedArrowModel.value) return;
358+
const s = selectedArrowModel.value.sourceHead.value;
359+
const t = selectedArrowModel.value.targetHead.value;
360+
selectedArrowModel.value.sourceHead.value = t;
361+
selectedArrowModel.value.targetHead.value = s;
362+
}
363+
364+
function handleCopyArrowLink() {
365+
if (!selectedArrowId.value) return;
366+
const url = `${window.location.origin}/pages/${pageId.value}?elem=${selectedArrowId.value}`;
367+
navigator.clipboard.writeText(url);
368+
}
369+
370+
async function handleSetArrowAsDefault() {
371+
pageOpsMessage.value = 'Set as default arrow style is not yet implemented in the new UI (requires serialization + encryption).';
372+
}
373+
324374
onMounted(() => {
325375
if (!isAuthenticated.value) {
326376
void router.replace({
@@ -517,6 +567,12 @@ onMounted(() => {
517567
@update:container-wrap-children="selectedNoteModel.container.wrapChildren.value = $event"
518568
@update:container-stretch-children="selectedNoteModel.container.stretchChildren.value = $event"
519569
@update:container-force-color-inheritance="selectedNoteModel.container.forceColorInheritance.value = $event"
570+
@update:local-collapsing="selectedNoteModel.collapsing.localCollapsing.value = $event"
571+
@update:locally-collapsed="selectedNoteModel.collapsing.locallyCollapsed.value = $event"
572+
@create-new-page="handleCreateNewPage"
573+
@swap-head-body="handleSwapHeadBody"
574+
@copy-link="handleCopyNoteLink"
575+
@set-as-default="handleSetNoteAsDefault"
520576
/>
521577

522578
<ArrowPropertiesCard
@@ -531,32 +587,39 @@ onMounted(() => {
531587
@update:color="selectedArrowModel.color.value = $event"
532588
@update:color-inherit="selectedArrowModel.color.inherit.value = $event"
533589
@update:read-only="selectedArrowModel.readOnly.value = $event"
590+
@update:source-anchor="selectedArrowModel.sourceAnchor.value = $event === 'null' ? null : JSON.parse($event)"
591+
@update:target-anchor="selectedArrowModel.targetAnchor.value = $event === 'null' ? null : JSON.parse($event)"
592+
@swap-arrowheads="handleSwapArrowheads"
593+
@copy-link="handleCopyArrowLink"
594+
@set-as-default="handleSetArrowAsDefault"
534595
/>
535596

536-
<PageEditorSnapshotsCard
537-
:snapshot-loading="snapshotsLoading"
538-
:snapshots="snapshotList"
539-
:collab-loading="collabLoading"
540-
:load-error="loadError"
541-
:crypto-error="cryptoError"
542-
@restore="restoreFromSnapshot($event)"
543-
@remove="deleteSnapshot($event)"
544-
@save-manual="saveSnapshotManual()"
545-
/>
546-
547-
<PageEditorManagementCard
548-
v-model:move-dest-group-id="moveDestGroupId"
549-
:collab-loading="collabLoading"
550-
:load-error="loadError"
551-
:crypto-error="cryptoError"
552-
:collab-group-id="collabGroupId"
553-
@move-to-group="management.movePageToOtherGroup()"
554-
@set-as-main-page="management.setAsGroupMainPage()"
555-
@soft-delete="management.softDeleteThisPage()"
556-
@purge="management.purgeThisPagePermanently()"
557-
/>
558-
559-
<PageEditorBacklinksCard :page-id="pageId" />
597+
<template v-if="!selectedNoteId && !selectedArrowId">
598+
<PageEditorSnapshotsCard
599+
:snapshot-loading="snapshotsLoading"
600+
:snapshots="snapshotList"
601+
:collab-loading="collabLoading"
602+
:load-error="loadError"
603+
:crypto-error="cryptoError"
604+
@restore="restoreFromSnapshot($event)"
605+
@remove="deleteSnapshot($event)"
606+
@save-manual="saveSnapshotManual()"
607+
/>
608+
609+
<PageEditorManagementCard
610+
v-model:move-dest-group-id="moveDestGroupId"
611+
:collab-loading="collabLoading"
612+
:load-error="loadError"
613+
:crypto-error="cryptoError"
614+
:collab-group-id="collabGroupId"
615+
@move-to-group="management.movePageToOtherGroup()"
616+
@set-as-main-page="management.setAsGroupMainPage()"
617+
@soft-delete="management.softDeleteThisPage()"
618+
@purge="management.purgeThisPagePermanently()"
619+
/>
620+
621+
<PageEditorBacklinksCard :page-id="pageId" />
622+
</template>
560623
</div>
561624
</template>
562625

new-deepnotes/apps/web/src/features/spatial/ArrowPropertiesCard.vue

Lines changed: 103 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
44
import { Button } from '@/components/ui/button'
55
import { Label } from '@/components/ui/label'
66
import { Switch } from '@/components/ui/switch'
7-
import { Palette } from 'lucide-vue-next'
7+
import { Palette, Copy, Save, ArrowUpDown } from 'lucide-vue-next'
88
99
const props = defineProps<{
1010
arrowId: string | null
@@ -17,18 +17,48 @@ const emit = defineEmits({
1717
'update:body-style': (value: string) => true,
1818
'update:source-head': (value: string) => true,
1919
'update:target-head': (value: string) => true,
20+
'update:source-anchor': (value: string) => true,
21+
'update:target-anchor': (value: string) => true,
2022
'update:color': (value: number) => true,
2123
'update:color-inherit': (value: boolean) => true,
2224
'update:read-only': (value: boolean) => true,
25+
'swap-arrowheads': () => true,
26+
'copy-link': () => true,
27+
'set-as-default': () => true,
2328
})
2429
2530
const bodyType = computed(() => props.arrowModel?.bodyType?.value ?? 'curve')
2631
const bodyStyle = computed(() => props.arrowModel?.bodyStyle?.value ?? 'solid')
2732
const sourceHead = computed(() => props.arrowModel?.sourceHead?.value ?? 'none')
2833
const targetHead = computed(() => props.arrowModel?.targetHead?.value ?? 'open')
34+
const sourceAnchor = computed(() => {
35+
const v = props.arrowModel?.sourceAnchor?.value
36+
if (v == null) return 'null'
37+
return JSON.stringify(v)
38+
})
39+
const targetAnchor = computed(() => {
40+
const v = props.arrowModel?.targetAnchor?.value
41+
if (v == null) return 'null'
42+
return JSON.stringify(v)
43+
})
2944
const color = computed(() => props.arrowModel?.color?.value ?? 0)
3045
const colorInherit = computed(() => props.arrowModel?.color?.inherit?.value ?? false)
3146
const readOnlyArrow = computed(() => props.arrowModel?.readOnly?.value ?? false)
47+
const createdAt = computed(() => props.arrowModel?.createdAt?.value ?? null)
48+
const editedAt = computed(() => props.arrowModel?.editedAt?.value ?? null)
49+
50+
function formatTimestamp(ts: number | null): string {
51+
if (ts == null) return ''
52+
return new Intl.DateTimeFormat('en', { dateStyle: 'medium', timeStyle: 'short' }).format(ts)
53+
}
54+
55+
const anchorOptions = [
56+
{ label: 'Auto', value: 'null' },
57+
{ label: 'Left', value: JSON.stringify({ x: -1, y: 0 }) },
58+
{ label: 'Top', value: JSON.stringify({ x: 0, y: -1 }) },
59+
{ label: 'Right', value: JSON.stringify({ x: 1, y: 0 }) },
60+
{ label: 'Bottom', value: JSON.stringify({ x: 0, y: 1 }) },
61+
]
3262
3363
const colors = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
3464
@@ -100,6 +130,45 @@ function handleColorSelect(colorIndex: number) {
100130
</div>
101131
</div>
102132

133+
<!-- Swap arrowheads -->
134+
<Button
135+
variant="outline"
136+
size="sm"
137+
class="w-full"
138+
:disabled="readOnly"
139+
@click="emit('swap-arrowheads')"
140+
>
141+
<ArrowUpDown class="h-3 w-3 mr-2" />
142+
Swap arrowheads
143+
</Button>
144+
145+
<!-- Anchors -->
146+
<div class="space-y-2">
147+
<Label>Anchors</Label>
148+
<div class="flex gap-2">
149+
<div class="flex-1">
150+
<select
151+
:value="sourceAnchor"
152+
class="h-8 w-full rounded-md border border-input bg-background px-2 text-xs ring-offset-background focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2"
153+
:disabled="readOnly"
154+
@change="emit('update:source-anchor', ($event.target as HTMLSelectElement).value)"
155+
>
156+
<option v-for="opt in anchorOptions" :key="opt.value" :value="opt.value">{{ opt.label }}</option>
157+
</select>
158+
</div>
159+
<div class="flex-1">
160+
<select
161+
:value="targetAnchor"
162+
class="h-8 w-full rounded-md border border-input bg-background px-2 text-xs ring-offset-background focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2"
163+
:disabled="readOnly"
164+
@change="emit('update:target-anchor', ($event.target as HTMLSelectElement).value)"
165+
>
166+
<option v-for="opt in anchorOptions" :key="opt.value" :value="opt.value">{{ opt.label }}</option>
167+
</select>
168+
</div>
169+
</div>
170+
</div>
171+
103172
<!-- Body Style -->
104173
<div class="space-y-2">
105174
<Label>Body Style</Label>
@@ -167,6 +236,39 @@ function handleColorSelect(colorIndex: number) {
167236
</div>
168237
</div>
169238

239+
<!-- Copy link / Set as default -->
240+
<div class="space-y-2">
241+
<Button
242+
variant="outline"
243+
size="sm"
244+
class="w-full"
245+
@click="emit('copy-link')"
246+
>
247+
<Copy class="h-3 w-3 mr-2" />
248+
Copy link to this arrow
249+
</Button>
250+
<Button
251+
variant="outline"
252+
size="sm"
253+
class="w-full"
254+
:disabled="readOnly"
255+
@click="emit('set-as-default')"
256+
>
257+
<Save class="h-3 w-3 mr-2" />
258+
Set as default arrow style
259+
</Button>
260+
</div>
261+
262+
<!-- Timestamps -->
263+
<div v-if="createdAt || editedAt" class="space-y-1 text-[11px] text-muted-foreground">
264+
<div v-if="createdAt">
265+
<span class="font-medium text-foreground">Created:</span> {{ formatTimestamp(createdAt) }}
266+
</div>
267+
<div v-if="editedAt">
268+
<span class="font-medium text-foreground">Edited:</span> {{ formatTimestamp(editedAt) }}
269+
</div>
270+
</div>
271+
170272
<!-- Read-only -->
171273
<div class="flex items-center gap-2">
172274
<Switch

0 commit comments

Comments
 (0)