Skip to content

Commit b6da4d8

Browse files
adds icon to jump to linked shapes via "Value Shape" col, adds shape search box, adds "recent" edited shapes list
1 parent 23192f1 commit b6da4d8

2 files changed

Lines changed: 202 additions & 7 deletions

File tree

frontend/src/components/spreadsheet/SpreadsheetGrid.vue

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -135,15 +135,29 @@
135135
:class="{ disabled: isCellDisabled(row, col) }"
136136
:title="getCellError(row, col.key) ?? undefined"
137137
>
138-
<span v-for="(line, idx) in getMultilineValues(row, col.key)" :key="idx" class="multiline-value">{{ line }}</span>
138+
<span
139+
v-for="(line, idx) in getMultilineValues(row, col.key)"
140+
:key="idx"
141+
class="multiline-value"
142+
>{{ line }}<span
143+
v-if="col.key === 'valueShape' && isValidShapeRef(line)"
144+
class="shape-nav-icon"
145+
title="Go to this shape"
146+
@click.stop.prevent="promptNavigateToShape(line, $event)"
147+
>&#9998;</span></span>
139148
</span>
140149
<span
141150
v-else
142151
class="cell-content"
143152
:class="{ disabled: isCellDisabled(row, col) }"
144153
:title="getCellError(row, col.key) ?? undefined"
145154
>
146-
{{ formatCellValue(row, col, rowIndex) }}
155+
{{ formatCellValue(row, col, rowIndex) }}<span
156+
v-if="col.key === 'valueShape' && getCellValue(row, col.key, rowIndex) && isValidShapeRef(getCellValue(row, col.key, rowIndex)!)"
157+
class="shape-nav-icon"
158+
title="Go to this shape"
159+
@click.stop.prevent="promptNavigateToShape(getCellValue(row, col.key, rowIndex)!, $event)"
160+
>&#9998;</span>
147161
</span>
148162
</template>
149163
</td>
@@ -283,7 +297,7 @@ export default defineComponent({
283297
default: false
284298
}
285299
},
286-
emits: ['shape-label-change', 'description-change', 'resource-uri-change'],
300+
emits: ['shape-label-change', 'description-change', 'resource-uri-change', 'navigate-to-shape'],
287301
setup(props, { emit }) {
288302
const containerRef = ref<HTMLDivElement | null>(null);
289303
const gridWrapper = ref<HTMLDivElement | null>(null);
@@ -1294,6 +1308,18 @@ export default defineComponent({
12941308
// Handle other redo types...
12951309
}
12961310
1311+
function isValidShapeRef(shapeId: string): boolean {
1312+
return props.shapes.some(s => s.shapeId === shapeId);
1313+
}
1314+
1315+
function promptNavigateToShape(shapeId: string, event: Event) {
1316+
event.preventDefault();
1317+
event.stopPropagation();
1318+
if (confirm(`Do you want to edit the shape "${shapeId}"?`)) {
1319+
emit('navigate-to-shape', shapeId);
1320+
}
1321+
}
1322+
12971323
function startEditDescription() {
12981324
const newDescription = prompt('Enter shape description:', props.description || '');
12991325
if (newDescription !== null && newDescription !== props.description) {
@@ -1433,6 +1459,8 @@ export default defineComponent({
14331459
startEditDescription,
14341460
showErrorTooltip,
14351461
hideErrorTooltip,
1462+
isValidShapeRef,
1463+
promptNavigateToShape,
14361464
// Send to workspace
14371465
showSendToWorkspace,
14381466
otherWorkspaces,
@@ -1717,6 +1745,25 @@ tr.row-selected td {
17171745
border-bottom: 1px dotted #ddd;
17181746
}
17191747
1748+
.shape-nav-icon {
1749+
display: inline-block;
1750+
margin-left: 4px;
1751+
font-size: 0.7rem;
1752+
color: #999;
1753+
cursor: pointer;
1754+
opacity: 0;
1755+
transition: opacity 0.15s;
1756+
vertical-align: middle;
1757+
}
1758+
1759+
.cell:hover .shape-nav-icon {
1760+
opacity: 1;
1761+
}
1762+
1763+
.shape-nav-icon:hover {
1764+
color: #2196f3;
1765+
}
1766+
17201767
.cell-content.disabled {
17211768
color: #999;
17221769
background: #f9f9f9;

frontend/src/views/WorkspaceView.vue

Lines changed: 152 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,34 @@
4141
</button>
4242
</div>
4343
<div v-if="!sidebarCollapsed" class="sidebar-content">
44+
<div class="shape-search">
45+
<input
46+
type="text"
47+
v-model="shapeSearchQuery"
48+
placeholder="Filter shapes..."
49+
class="shape-search-input"
50+
/>
51+
<span
52+
v-if="shapeSearchQuery"
53+
class="shape-search-clear"
54+
@click="shapeSearchQuery = ''"
55+
>&times;</span>
56+
</div>
57+
58+
<!-- Recent edited shapes -->
59+
<div v-if="recentEditedShapes.length > 0 && !shapeSearchQuery" class="recent-shapes">
60+
<div class="recent-shapes-header">Recent</div>
61+
<div
62+
v-for="shapeId in recentEditedShapes"
63+
:key="'recent-' + shapeId"
64+
class="recent-shape-item"
65+
:class="{ active: currentShapeId === shapeId }"
66+
@click="selectShape(shapeId)"
67+
>
68+
{{ shapeId }}
69+
</div>
70+
</div>
71+
4472
<div class="sidebar-buttons">
4573
<button
4674
class="btn btn-primary btn-grow"
@@ -167,6 +195,7 @@
167195
@shape-label-change="updateShapeLabel"
168196
@description-change="updateDescription"
169197
@resource-uri-change="updateResourceURI"
198+
@navigate-to-shape="navigateToShape"
170199
/>
171200
</main>
172201
</div>
@@ -427,18 +456,47 @@ export default defineComponent({
427456
const startingPointFileInput = ref<HTMLInputElement | null>(null);
428457
const importingStartingPoints = ref(false);
429458
459+
// Shape search
460+
const shapeSearchQuery = ref('');
461+
462+
// Recent edited shapes (session only, max 5)
463+
const recentEditedShapeIds = ref<string[]>([]);
464+
const recentEditedShapes = computed(() => {
465+
// Only show shapes that still exist
466+
return recentEditedShapeIds.value.filter(id => shapes.value.some(s => s.shapeId === id));
467+
});
468+
469+
function trackRecentShape(shapeId: string) {
470+
const list = recentEditedShapeIds.value;
471+
const idx = list.indexOf(shapeId);
472+
if (idx !== -1) {
473+
list.splice(idx, 1);
474+
}
475+
list.unshift(shapeId);
476+
if (list.length > 5) {
477+
list.pop();
478+
}
479+
}
480+
481+
function shapeMatchesSearch(shape: Shape): boolean {
482+
if (!shapeSearchQuery.value) return true;
483+
const query = shapeSearchQuery.value.toLowerCase();
484+
return shape.shapeId.toLowerCase().includes(query) ||
485+
(shape.shapeLabel || '').toLowerCase().includes(query);
486+
}
487+
430488
// Shapes at root level (no folder)
431489
const rootShapes = computed(() => {
432490
return [...shapes.value]
433-
.filter(s => s.folderId === null)
491+
.filter(s => s.folderId === null && shapeMatchesSearch(s))
434492
.sort((a, b) => a.shapeId.localeCompare(b.shapeId));
435493
});
436494
437495
// Shapes grouped by folder
438496
const shapesByFolder = computed(() => {
439497
const map = new Map<number, Shape[]>();
440498
for (const shape of shapes.value) {
441-
if (shape.folderId !== null) {
499+
if (shape.folderId !== null && shapeMatchesSearch(shape)) {
442500
if (!map.has(shape.folderId)) {
443501
map.set(shape.folderId, []);
444502
}
@@ -452,9 +510,11 @@ export default defineComponent({
452510
return map;
453511
});
454512
455-
// Sorted folders
513+
// Sorted folders - hide empty folders when searching
456514
const sortedFolders = computed(() => {
457-
return [...folders.value].sort((a, b) => a.name.localeCompare(b.name));
515+
const sorted = [...folders.value].sort((a, b) => a.name.localeCompare(b.name));
516+
if (!shapeSearchQuery.value) return sorted;
517+
return sorted.filter(f => (shapesByFolder.value.get(f.id)?.length || 0) > 0);
458518
});
459519
460520
const sortedShapes = computed(() => {
@@ -534,9 +594,17 @@ export default defineComponent({
534594
535595
function selectShape(shapeId: string) {
536596
currentShapeId.value = shapeId;
597+
trackRecentShape(shapeId);
537598
router.replace({ name: 'shape', params: { id: props.id, shapeId } });
538599
}
539600
601+
function navigateToShape(shapeId: string) {
602+
const shape = shapes.value.find(s => s.shapeId === shapeId);
603+
if (shape) {
604+
selectShape(shapeId);
605+
}
606+
}
607+
540608
async function createShape() {
541609
if (!newShapeId.value.trim()) return;
542610
try {
@@ -700,6 +768,10 @@ export default defineComponent({
700768
}
701769
702770
function isFolderCollapsed(folderId: number): boolean {
771+
// Auto-expand folders with matches when searching
772+
if (shapeSearchQuery.value && (shapesByFolder.value.get(folderId)?.length || 0) > 0) {
773+
return false;
774+
}
703775
return !expandedFolders.value.has(folderId);
704776
}
705777
@@ -823,6 +895,7 @@ export default defineComponent({
823895
showNamespaceManager,
824896
goHome,
825897
selectShape,
898+
navigateToShape,
826899
createShape,
827900
editShape,
828901
doEditShape,
@@ -847,6 +920,10 @@ export default defineComponent({
847920
isFolderCollapsed,
848921
createFolder,
849922
deleteFolder,
923+
// Shape search
924+
shapeSearchQuery,
925+
// Recent edited shapes
926+
recentEditedShapes,
850927
// Drag and drop
851928
draggedShapeId,
852929
dragOverFolderId,
@@ -1171,6 +1248,77 @@ export default defineComponent({
11711248
flex: 1;
11721249
}
11731250
1251+
/* Shape search */
1252+
.shape-search {
1253+
position: relative;
1254+
margin-bottom: 0.75rem;
1255+
}
1256+
1257+
.shape-search-input {
1258+
width: 100%;
1259+
padding: 0.4rem 1.75rem 0.4rem 0.5rem;
1260+
border: 1px solid #ddd;
1261+
border-radius: 4px;
1262+
font-size: 0.85rem;
1263+
box-sizing: border-box;
1264+
}
1265+
1266+
.shape-search-input:focus {
1267+
outline: none;
1268+
border-color: #3498db;
1269+
}
1270+
1271+
.shape-search-clear {
1272+
position: absolute;
1273+
right: 6px;
1274+
top: 50%;
1275+
transform: translateY(-50%);
1276+
cursor: pointer;
1277+
color: #999;
1278+
font-size: 1.1rem;
1279+
line-height: 1;
1280+
}
1281+
1282+
.shape-search-clear:hover {
1283+
color: #333;
1284+
}
1285+
1286+
/* Recent edited shapes */
1287+
.recent-shapes {
1288+
margin-bottom: 0.75rem;
1289+
padding-bottom: 0.5rem;
1290+
border-bottom: 1px solid #e0e0e0;
1291+
}
1292+
1293+
.recent-shapes-header {
1294+
font-size: 0.75rem;
1295+
font-weight: 600;
1296+
color: #888;
1297+
text-transform: uppercase;
1298+
letter-spacing: 0.5px;
1299+
margin-bottom: 0.25rem;
1300+
}
1301+
1302+
.recent-shape-item {
1303+
padding: 0.3rem 0.5rem;
1304+
border-radius: 4px;
1305+
cursor: pointer;
1306+
font-size: 0.85rem;
1307+
color: #2c3e50;
1308+
white-space: nowrap;
1309+
overflow: hidden;
1310+
text-overflow: ellipsis;
1311+
}
1312+
1313+
.recent-shape-item:hover {
1314+
background: #f0f0f0;
1315+
}
1316+
1317+
.recent-shape-item.active {
1318+
background: #e3f2fd;
1319+
font-weight: 500;
1320+
}
1321+
11741322
/* Root drop zone */
11751323
.root-drop-zone {
11761324
min-height: 20px;

0 commit comments

Comments
 (0)