Skip to content

Commit 9edab60

Browse files
committed
Integrated lock task and other UI improvements
1 parent b73919e commit 9edab60

11 files changed

Lines changed: 812 additions & 37 deletions

File tree

components/project-wizard/AoiGeometryMap.vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -807,6 +807,7 @@ function syncDraftPolygon() {
807807
skipNextAoiFit = true;
808808
emit('update:aoi', draftFeature);
809809
}
810+
810811
</script>
811812

812813
<style lang="scss" scoped>

components/project-wizard/steps/AreaOfInterestStep.vue

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,29 @@
2727
</button>
2828
</div>
2929

30+
<section
31+
v-if="hasAoi"
32+
class="project-wizard-area-captured"
33+
aria-live="polite"
34+
>
35+
<div class="project-wizard-area-captured-copy">
36+
<app-icon variant="check_circle" size="18" no-margin />
37+
<div>
38+
<strong>Area of interest captured</strong>
39+
<p>{{ importedFileName ? 'The uploaded GeoJSON is active on the map.' : 'The drawn polygon is active on the map.' }}</p>
40+
</div>
41+
</div>
42+
43+
<button
44+
class="btn btn-light project-wizard-area-download"
45+
type="button"
46+
@click="emit('download')"
47+
>
48+
<app-icon variant="download" size="18" no-margin />
49+
Download Area of Interest
50+
</button>
51+
</section>
52+
3053
<div class="project-wizard-area-divider" aria-hidden="true">
3154
<span>{{ step.content.dividerLabel }}</span>
3255
</div>
@@ -118,6 +141,7 @@ interface Props {
118141
119142
const props = defineProps<Props>();
120143
const emit = defineEmits<{
144+
download: [];
121145
draw: [];
122146
reset: [];
123147
upload: [file: File];
@@ -266,6 +290,53 @@ function onFileDrop(event: DragEvent) {
266290
background: #f5f5f5 0% 0% no-repeat padding-box;
267291
}
268292
293+
.project-wizard-area-captured {
294+
display: flex;
295+
align-items: center;
296+
justify-content: space-between;
297+
gap: 1rem;
298+
padding: 0.95rem 1rem;
299+
background: #f3fcf8;
300+
border: 1px solid #bcebdd;
301+
border-radius: 0.55rem;
302+
}
303+
304+
.project-wizard-area-captured-copy {
305+
min-width: 0;
306+
display: inline-flex;
307+
align-items: flex-start;
308+
gap: 0.65rem;
309+
color: #195747;
310+
}
311+
312+
.project-wizard-area-captured-copy strong,
313+
.project-wizard-area-captured-copy p {
314+
margin: 0;
315+
}
316+
317+
.project-wizard-area-captured-copy strong {
318+
display: block;
319+
font-size: 0.95rem;
320+
font-weight: 700;
321+
}
322+
323+
.project-wizard-area-captured-copy p {
324+
margin-top: 0.12rem;
325+
color: rgba($secondary, 0.92);
326+
font-size: 0.88rem;
327+
line-height: 1.45;
328+
}
329+
330+
.project-wizard-area-download {
331+
flex-shrink: 0;
332+
display: inline-flex;
333+
align-items: center;
334+
gap: 0.45rem;
335+
color: $text-navy;
336+
background: #ffffff;
337+
border: 1px solid rgba($text-navy, 0.14);
338+
}
339+
269340
.project-wizard-area-dropzone-shell {
270341
border: 1px dashed rgba($text-navy, 0.28);
271342
border-radius: 0.35rem;
@@ -383,5 +454,14 @@ function onFileDrop(event: DragEvent) {
383454
.project-wizard-area-action {
384455
width: 100%;
385456
}
457+
458+
.project-wizard-area-captured {
459+
flex-direction: column;
460+
align-items: stretch;
461+
}
462+
463+
.project-wizard-area-download {
464+
justify-content: center;
465+
}
386466
}
387467
</style>

components/workspace-project-details/ProjectMap.vue

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -465,7 +465,9 @@ function syncSources() {
465465
466466
/**
467467
* Zoom and pan the map to frame all available data.
468-
* Priority: AOI bounds > task bounds > task-grid bounds > default center.
468+
* Priority: task bounds > task-grid bounds > AOI bounds > default center.
469+
* On the tasks tab, real task geometry is the most important context — if we fit to a large AOI
470+
* first, newly created tasks can be technically present but visually imperceptible on initial load.
469471
* If nothing is available yet, we just ease back to the default Seattle center.
470472
*/
471473
function fitMapToData() {
@@ -480,7 +482,7 @@ function fitMapToData() {
480482
const taskGridBounds = props.taskGrid && props.taskGrid.features.length > 0
481483
? getGenericFeatureCollectionBounds(props.taskGrid)
482484
: null;
483-
const bounds = aoiBounds ?? taskBounds ?? taskGridBounds;
485+
const bounds = taskBounds ?? taskGridBounds ?? aoiBounds;
484486
485487
if (!bounds) {
486488
detailMap.easeTo({
@@ -752,7 +754,7 @@ function syncLockedTaskMarkers() {
752754
element.type = 'button';
753755
element.className = 'project-detail-map-lock-marker';
754756
element.setAttribute('aria-label', 'Locked task');
755-
element.innerHTML = '<span aria-hidden="true">&#128274;</span>';
757+
element.innerHTML = '<i class="material-icons md-16 md-lock" aria-hidden="true"></i>';
756758
element.addEventListener('click', () => emit('select-task', id));
757759
758760
return new maplibregl!.Marker({
@@ -859,14 +861,16 @@ function clearLockedTaskMarkers() {
859861
width: 1.85rem;
860862
height: 1.85rem;
861863
color: #d94f4f;
862-
font-size: 1rem;
863-
line-height: 1;
864864
background: rgba(255, 255, 255, 0.96);
865865
border: 1px solid rgba(217, 79, 79, 0.2);
866866
border-radius: 999px;
867867
box-shadow: 0 0.35rem 0.85rem rgba(25, 30, 61, 0.16);
868868
}
869869
870+
:deep(.project-detail-map-lock-marker .material-icons) {
871+
margin-top: 0;
872+
}
873+
870874
@include media-breakpoint-down(sm) {
871875
.project-detail-map-legend {
872876
left: 1rem;

components/workspace-project-details/RichTextContent.vue

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,6 @@ interface Props {
1919
2020
const props = defineProps<Props>();
2121
22-
/**
23-
* Sanitize the incoming HTML with DOMPurify before rendering.
24-
* This runs on every change to `html` and prevents XSS via injected scripts or event handlers.
25-
*/
2622
const safeHtml = computed(() =>
2723
// DOMPurify.sanitize is a no-op on the server (returns the string unchanged),
2824
// but on the client it strips any dangerous markup.
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
<template>
2+
<div ref="rootRef" class="project-detail-task-lock-control">
3+
<button
4+
v-if="canUnlock"
5+
class="project-detail-task-lock-button"
6+
type="button"
7+
:disabled="busy"
8+
:aria-expanded="menuOpen ? 'true' : 'false'"
9+
aria-haspopup="menu"
10+
@click="toggleMenu"
11+
>
12+
<app-icon variant="lock" size="16" no-margin />
13+
<app-icon :variant="menuOpen ? 'keyboard_arrow_up' : 'keyboard_arrow_down'" size="16" no-margin />
14+
</button>
15+
16+
<span
17+
v-else
18+
class="project-detail-task-lock-indicator"
19+
:title="lockedByTitle"
20+
>
21+
<app-icon variant="lock" size="16" no-margin />
22+
</span>
23+
24+
<div
25+
v-if="canUnlock && menuOpen"
26+
class="project-detail-task-lock-menu"
27+
role="menu"
28+
>
29+
<button
30+
class="project-detail-task-lock-menu-item"
31+
type="button"
32+
role="menuitem"
33+
:disabled="busy"
34+
@click="handleUnlock"
35+
>
36+
Unlock task
37+
</button>
38+
</div>
39+
</div>
40+
</template>
41+
42+
<script setup lang="ts">
43+
/**
44+
* Small row-level lock indicator / unlock menu.
45+
* This stays presentational: it only exposes an `unlock` event and leaves the actual API
46+
* mutation to the parent page so task state remains centralized.
47+
*/
48+
interface Props {
49+
busy?: boolean;
50+
canUnlock: boolean;
51+
lockedByName?: string | null;
52+
}
53+
54+
const props = defineProps<Props>();
55+
const emit = defineEmits<{
56+
unlock: [];
57+
}>();
58+
59+
const rootRef = useTemplateRef<HTMLDivElement>('rootRef');
60+
const menuOpen = ref(false);
61+
62+
const lockedByTitle = computed(() =>
63+
props.lockedByName ? `Locked by ${props.lockedByName}` : 'Task is locked',
64+
);
65+
66+
watch(
67+
() => props.busy,
68+
(isBusy) => {
69+
if (isBusy) {
70+
menuOpen.value = false;
71+
}
72+
},
73+
);
74+
75+
onMounted(() => {
76+
if (!import.meta.client) {
77+
return;
78+
}
79+
80+
window.addEventListener('pointerdown', handleWindowPointerDown);
81+
});
82+
83+
onBeforeUnmount(() => {
84+
if (!import.meta.client) {
85+
return;
86+
}
87+
88+
window.removeEventListener('pointerdown', handleWindowPointerDown);
89+
});
90+
91+
function toggleMenu() {
92+
if (props.busy) {
93+
return;
94+
}
95+
96+
menuOpen.value = !menuOpen.value;
97+
}
98+
99+
function handleUnlock() {
100+
menuOpen.value = false;
101+
emit('unlock');
102+
}
103+
104+
function handleWindowPointerDown(event: PointerEvent) {
105+
if (!menuOpen.value || !rootRef.value) {
106+
return;
107+
}
108+
109+
if (rootRef.value.contains(event.target as Node)) {
110+
return;
111+
}
112+
113+
menuOpen.value = false;
114+
}
115+
</script>
116+
117+
<style lang="scss" scoped>
118+
@import "~/assets/scss/theme.scss";
119+
120+
.project-detail-task-lock-control {
121+
position: relative;
122+
display: inline-flex;
123+
align-items: center;
124+
}
125+
126+
.project-detail-task-lock-button,
127+
.project-detail-task-lock-indicator {
128+
min-width: 2.15rem;
129+
height: 2rem;
130+
display: inline-flex;
131+
align-items: center;
132+
justify-content: center;
133+
gap: 0.05rem;
134+
color: #d94f4f;
135+
background: #ffffff;
136+
border: 1px solid rgba($text-navy, 0.14);
137+
border-radius: 0.45rem;
138+
}
139+
140+
.project-detail-task-lock-button:disabled {
141+
opacity: 0.55;
142+
}
143+
144+
.project-detail-task-lock-menu {
145+
position: absolute;
146+
left: 0;
147+
top: calc(100% + 0.45rem);
148+
z-index: 4;
149+
min-width: 10.25rem;
150+
padding: 0.35rem 0;
151+
background: #ffffff;
152+
border: 1px solid rgba($text-navy, 0.12);
153+
border-radius: 0.5rem;
154+
box-shadow: 0 0.75rem 1.6rem rgba($text-navy, 0.14);
155+
}
156+
157+
.project-detail-task-lock-menu-item {
158+
width: 100%;
159+
display: flex;
160+
align-items: center;
161+
padding: 0.65rem 0.9rem;
162+
color: #4a5170;
163+
font-size: 0.95rem;
164+
text-align: left;
165+
background: transparent;
166+
border: 0;
167+
}
168+
169+
.project-detail-task-lock-menu-item:hover:not(:disabled),
170+
.project-detail-task-lock-menu-item:focus-visible:not(:disabled) {
171+
background: #f7f8fc;
172+
}
173+
174+
.project-detail-task-lock-menu-item:disabled {
175+
opacity: 0.55;
176+
}
177+
</style>

0 commit comments

Comments
 (0)