Skip to content

Commit 4f41492

Browse files
authored
feat(notes): add option to open note details on single click (SignalK#512)
Adds an off-by-default "Single Click for Note Details" display setting. When enabled, clicking a note on the chart opens its details surface (info panel or dialog, per Prefer Info-Panel) directly, skipping the interim options popover. To preserve the popover's actions on that surface, a Move action is added to both the note details dialog and the info-panel note view (for editable, positioned notes). Move drives the map's existing Modify interaction on the clicked note feature. Closes SignalK#501
1 parent 1a3dd47 commit 4f41492

12 files changed

Lines changed: 169 additions & 3 deletions

File tree

src/app/app.component.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,7 @@
308308
id: $event
309309
})
310310
"
311+
(move)="onNoteMove($event)"
311312
(panTo)="centerAndZoom($event.center, $event.zoomLevel)"
312313
/>
313314
}

src/app/app.component.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1161,6 +1161,16 @@ export class AppComponent {
11611161
}
11621162
}
11631163

1164+
/** Start moving a Note from the info panel (close the panel first so the
1165+
* map's Modify interaction is unobstructed). */
1166+
protected onNoteMove(id: string) {
1167+
if (this.infoPanel.opened()) {
1168+
this.infoPanel.close();
1169+
this.closeDrawer();
1170+
}
1171+
this.skres.startNoteModify(id);
1172+
}
1173+
11641174
// ********* MAIN MENU ACTIONS *************
11651175

11661176
// ** open about dialog **

src/app/app.config.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ export function cleanConfig(
6666
favourites: []
6767
},
6868
preferInfoPanel: true,
69+
singleClickNoteDetails: false,
6970
statusBar: {
7071
liveEta: false,
7172
referenceSpeed: 6
@@ -75,6 +76,9 @@ export function cleanConfig(
7576
if (typeof settings.display.preferInfoPanel === 'undefined') {
7677
settings.display.preferInfoPanel = true;
7778
}
79+
if (typeof settings.display.singleClickNoteDetails === 'undefined') {
80+
settings.display.singleClickNoteDetails = false;
81+
}
7882
if (typeof settings.display.statusBar === 'undefined') {
7983
settings.display.statusBar = {
8084
liveEta: false,
@@ -476,6 +480,7 @@ export function defaultConfig(): IAppConfig {
476480
favourites: []
477481
},
478482
preferInfoPanel: true,
483+
singleClickNoteDetails: false,
479484
statusBar: {
480485
liveEta: false,
481486
referenceSpeed: 6

src/app/modules/map/fb-map.component.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1551,7 +1551,13 @@ export class FBMapComponent implements OnInit, OnDestroy {
15511551
poData.title = 'Note';
15521552
poData.resource = item;
15531553
poData.show = true;
1554-
if (this.infoPanel.opened()) {
1554+
// Single-click-to-details: skip the interim options popover and open the
1555+
// Note details surface (info panel or dialog) straight away.
1556+
if (this.app.config.display.singleClickNoteDetails) {
1557+
poData.show = false;
1558+
this.overlay.set(poData);
1559+
this.popoverInfo();
1560+
} else if (this.infoPanel.opened()) {
15551561
this.overlay.set(poData);
15561562
this.popoverInfo();
15571563
}

src/app/modules/map/fbmap-interact.service.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -250,7 +250,7 @@ export class FBMapInteractService {
250250
}
251251

252252
/** Start modifying mode */
253-
startModifying(overlay: IPopover) {
253+
startModifying(overlay: Pick<IPopover, 'type'>) {
254254
this.app.debug(`startModifying()...`);
255255
if (this.draw.features.getLength() === 0) {
256256
return;

src/app/modules/settings/components/settings-dialog.html

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,16 @@
8383
Prefer Info-Panel
8484
</mat-checkbox>
8585
</div>
86+
<div class="setting-card-row-item">
87+
<mat-checkbox
88+
[(ngModel)]="facade.settings.display.singleClickNoteDetails"
89+
(change)="persistModel()"
90+
label="after"
91+
matTooltip="Open Note details with a single click instead of showing the pop-up options"
92+
>
93+
Single Click for Note Details
94+
</mat-checkbox>
95+
</div>
8696
</div>
8797
</mat-card-content>
8898
</mat-card>

src/app/modules/skresources/components/notes/note-dialog.html

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,15 @@
130130
>
131131
<mat-icon>edit</mat-icon>
132132
</button>
133-
&nbsp;
133+
&nbsp; @if(data.note.position) {
134+
<button
135+
mat-icon-button
136+
(click)="dialogRef.close({result:true, data: 'move'})"
137+
matTooltip="Move Note"
138+
>
139+
<mat-icon>touch_app</mat-icon>
140+
</button>
141+
&nbsp; }
134142
<button
135143
mat-icon-button
136144
(click)="dialogRef.close({result:true, data: 'delete'})"

src/app/modules/skresources/components/notes/note-panel.html

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,16 @@
8282
CENTER
8383
</button>
8484
&nbsp;
85+
<button
86+
mat-button
87+
[disabled]="_note().properties?.readOnly || interacting()"
88+
(click)="onMove()"
89+
matTooltip="Move Note"
90+
>
91+
<mat-icon>touch_app</mat-icon>
92+
MOVE
93+
</button>
94+
&nbsp;
8595
<button
8696
mat-button
8797
[disabled]="_note().properties?.readOnly || interacting()"

src/app/modules/skresources/components/notes/note-panel.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ export class NotePanel {
4747
protected _note = linkedSignal(() => this.note());
4848

4949
edit = output<string>();
50+
move = output<string>();
5051
panTo = output<{
5152
center: Position;
5253
zoomLevel: number;
@@ -78,6 +79,10 @@ export class NotePanel {
7879
this.edit.emit(this.id());
7980
}
8081

82+
onMove() {
83+
this.move.emit(this.id());
84+
}
85+
8186
onDelete() {
8287
this.skres.deleteNote(this.id());
8388
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import { describe, it, expect, vi } from 'vitest';
2+
import { Collection, Feature } from 'ol';
3+
import { SKResourceService } from './resources.service';
4+
5+
/**
6+
* Regression test for #501 — the "Move" action on the Note details surfaces
7+
* must start the map Modify interaction on the *actual rendered note feature*
8+
* that the opening map click loaded into the draw collection. Modifying that
9+
* feature (not a reconstructed copy) is what visually moves the icon and yields
10+
* the coordinates the save persists. When no such feature is present (details
11+
* reached without a map click), Move is a no-op.
12+
*
13+
* It only touches `this.mapInteract`, so exercise it on a bare prototype
14+
* instance — no Angular DI (same approach as note-details-sizing.spec.ts).
15+
*/
16+
type FakeMapInteract = {
17+
draw: { features: Collection<Feature> };
18+
startModifying: ReturnType<typeof vi.fn>;
19+
};
20+
21+
function noteFeature(id: string): Feature {
22+
const f = new Feature();
23+
f.setId(id);
24+
return f;
25+
}
26+
27+
function svc(mapInteract: FakeMapInteract) {
28+
const s = Object.create(SKResourceService.prototype) as SKResourceService;
29+
Object.assign(s as unknown as Record<string, unknown>, { mapInteract });
30+
return s;
31+
}
32+
33+
describe('startNoteModify (#501)', () => {
34+
it('modifies the note feature loaded by the map click', () => {
35+
const feature = noteFeature('note.note-1');
36+
const mapInteract: FakeMapInteract = {
37+
draw: { features: new Collection([feature]) },
38+
startModifying: vi.fn()
39+
};
40+
41+
svc(mapInteract).startNoteModify('note-1');
42+
43+
expect(mapInteract.startModifying).toHaveBeenCalledWith({ type: 'note' });
44+
// the same rendered feature object is what gets modified
45+
expect(mapInteract.draw.features.item(0)).toBe(feature);
46+
});
47+
48+
it('restricts the interaction to the target note feature', () => {
49+
const target = noteFeature('note.note-1');
50+
const mapInteract: FakeMapInteract = {
51+
draw: { features: new Collection([target, noteFeature('note.note-2')]) },
52+
startModifying: vi.fn()
53+
};
54+
55+
svc(mapInteract).startNoteModify('note-1');
56+
57+
expect(mapInteract.draw.features.getLength()).toBe(1);
58+
expect(mapInteract.draw.features.item(0)).toBe(target);
59+
});
60+
61+
it('does nothing when the target note feature is not loaded', () => {
62+
const mapInteract: FakeMapInteract = {
63+
draw: { features: new Collection([noteFeature('route.r-1')]) },
64+
startModifying: vi.fn()
65+
};
66+
67+
svc(mapInteract).startNoteModify('note-1');
68+
69+
expect(mapInteract.startModifying).not.toHaveBeenCalled();
70+
});
71+
72+
it('does nothing when no feature is loaded (no map click)', () => {
73+
const mapInteract: FakeMapInteract = {
74+
draw: { features: new Collection([]) },
75+
startModifying: vi.fn()
76+
};
77+
78+
svc(mapInteract).startNoteModify('note-1');
79+
80+
expect(mapInteract.startModifying).not.toHaveBeenCalled();
81+
});
82+
});

0 commit comments

Comments
 (0)