Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
34bbaf7
feat(CrsSelector): v-model API, authority:code selection key, non-blo…
MaxNumerique Oct 22, 2026
b9fea1a
refacto from import_workflow [skip ci]
MaxNumerique Oct 22, 2026
4556cc2
fix(import_workflow): reloadObject rebuilds the hybrid viewer actor
MaxNumerique Oct 22, 2026
79c762e
Apply prepare changes
MaxNumerique Oct 23, 2026
9db9e20
feat(crs): store data CRS and cache the available CRS list
MaxNumerique Oct 23, 2026
57e155c
feat(crs): show and edit the CRS from the object tree and the info card
MaxNumerique Oct 23, 2026
7d63fb7
feat(feedback): optional action button on snackbars
MaxNumerique Oct 23, 2026
24f6f18
compact
MaxNumerique Oct 23, 2026
70b69af
Apply prepare changes
MaxNumerique Oct 23, 2026
075af2d
fix(viewer): rebuild the actor only when the geometry moved, keep cam…
MaxNumerique Oct 23, 2026
9dfbfd7
read the data only while the tooltip is open [skip ci]
MaxNumerique Oct 23, 2026
94b9c46
fix(feedback): close the snackbar when its action fails [skip ci]
MaxNumerique Oct 23, 2026
223c6db
fix(InfoCard): Vuetify classes and keyboard-reachable CRS edit button…
MaxNumerique Oct 23, 2026
e8a84b2
perf(CrsSelector): shallow CRS list, no "All" page size
MaxNumerique Oct 23, 2026
40c51ba
Apply prepare changes
MaxNumerique Oct 23, 2026
a76c7a3
oxlint
MaxNumerique Oct 23, 2026
facde66
trigger
MaxNumerique Oct 27, 2026
0432c8f
trigger
MaxNumerique Oct 27, 2026
e4668b4
perf(crs): load the geographic CRS list once per session
MaxNumerique Oct 27, 2026
f71396b
feat(crs): store the project crs
MaxNumerique Nov 2, 2026
d7494e5
feat(crs): warn about data in another crs in the object tree
MaxNumerique Nov 2, 2026
27795cd
feat(crs): show the project crs under the axes
MaxNumerique Nov 2, 2026
c92cbc3
fix(crs): old project takes the crs of its oldest saved data
MaxNumerique Nov 2, 2026
a1b4bb9
feat(crs): the project crs follows its data, light shared read
MaxNumerique Nov 2, 2026
fa63f5e
feat(crs): warn about data without crs, larger project crs widget
MaxNumerique Nov 2, 2026
5503a0f
fix(crs): restore the project crs once every data is imported
MaxNumerique Nov 2, 2026
d0fc3c4
feat(crs): open the crs dialog from the tree warning
MaxNumerique Nov 2, 2026
1d150eb
Merge remote-tracking branch 'origin/next' into feat/crs_assign
MaxNumerique Nov 3, 2026
4ea3d96
refactor(CrsSelector): type the selected rows as CRS keys
MaxNumerique Nov 3, 2026
1dc594c
trigger
MaxNumerique Nov 3, 2026
4da7dd5
Merge branch 'next' of https://github.com/Geode-solutions/OpenGeodeWe…
MaxNumerique Nov 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 53 additions & 46 deletions app/components/CrsSelector.vue
Original file line number Diff line number Diff line change
@@ -1,63 +1,45 @@
<script setup lang="ts">
import schemas, { type CRSList } from "@geode/opengeodeweb-back/opengeodeweb_back_typed_schemas.js";
import { useBackStore } from "@ogw_front/stores/back";

const schema = schemas.opengeodeweb_back.geographic_coordinate_systems;

interface Emits {
update_values: [values: Record<string, unknown>];
increment_step: [];
decrement_step: [];
}

const emit = defineEmits<Emits>();
import type { CRSList } from "@geode/opengeodeweb-back/opengeodeweb_back_typed_schemas.js";
import { useCrsStore } from "@ogw_front/stores/crs";

interface Props {
geodeObjectType: string;
keyToUpdate: string;
// Narrow containers: one "AUTHORITY:CODE" column and a truncated name
compact?: boolean;
}

const { geodeObjectType, keyToUpdate } = defineProps<Props>();
const { geodeObjectType, compact = false } = defineProps<Props>();
const selectedCrs = defineModel<CRSList | undefined>();

const search = ref<string>("");
const data_table_loading = ref<boolean>(false);
const crs_list = ref<CRSList[]>([]);
const selected_crs = ref<unknown[]>([]);
// Thousands of rows that are never mutated: no deep reactivity
const crs_list = shallowRef<CRSList[]>([]);
// Keys of the selected rows: the item-value of the table, "AUTHORITY:CODE"
const selected_crs = ref<string[]>([]);
const toggle_loading = useToggle(data_table_loading);
const backStore = useBackStore();
const crsStore = useCrsStore();

// Codes are only unique within an authority (e.g. EPSG:4812 and ESRI:4812)
function crs_key(crs: CRSList): string {
Comment thread
MaxNumerique marked this conversation as resolved.
Resolved
return `${crs.authority}:${crs.code}`;
}

function get_selected_crs(crs_code: unknown): unknown {
for (const crs of crs_list.value) {
if (crs.code === crs_code) {
return crs;
}
}
function get_selected_crs(key: string | undefined): CRSList | undefined {
return crs_list.value.find((crs) => crs_key(crs) === key);
}

watch(selected_crs, (new_value) => {
const crs = get_selected_crs(new_value[0]);
const keys_values_object = {
[keyToUpdate]: crs,
};
emit("update_values", keys_values_object);
emit("increment_step");
selectedCrs.value = get_selected_crs(new_value[0]);
});

async function get_crs_table(): void {
const params = { geode_object_type: geodeObjectType };
async function get_crs_table(): Promise<void> {
toggle_loading();
await backStore.request(
{ schema, params },
{
response_function: (response) => {
crs_list.value = response.crs_list;
},
},
);
crs_list.value = await crsStore.geographicList(geodeObjectType);
toggle_loading();
}

const headers = [
const full_headers = [
{
title: "Authority",
align: "start",
Expand All @@ -68,24 +50,41 @@ const headers = [
{ title: "Name", align: "end", key: "name" },
] as const;

// oxlint-disable-next-line no-top-level-await
await get_crs_table();
// A max width of 0 lets the name take the remaining space and truncate instead of wrapping
const compact_headers = [
{ title: "Code", align: "start", key: "code", value: crs_key, width: 110, nowrap: true },
{ title: "Name", align: "start", key: "name", maxWidth: 0, nowrap: true },
] as const;

// "All" would render the whole list (thousands of rows) at once
// oxlint-disable-next-line no-magic-numbers -- the page sizes Vuetify offers by default, "All" aside
const items_per_page_options = [10, 25, 50, 100];

const headers = computed(() => (compact ? compact_headers : full_headers));

// Not awaited in setup: the table renders right away with its loader
onMounted(async () => {
await get_crs_table();
});
</script>

<template>
<v-text-field
v-model="search"
append-icon="mdi-magnify"
data-testid="crsSearchInput"
:append-icon="compact ? undefined : 'mdi-magnify'"
:prepend-inner-icon="compact ? 'mdi-magnify' : undefined"
label="Search"
single-line
hide-details
></v-text-field>
<v-data-table
v-model="selected_crs"
:v-model:items-per-page="10"
:items-per-page="10"
:items-per-page-options="items_per_page_options"
:headers="headers"
:items="crs_list"
item-value="code"
:item-value="crs_key"
class="elevation-1"
density="compact"
fixed-header
Expand All @@ -94,5 +93,13 @@ await get_crs_table();
:search="search"
:loading="data_table_loading"
loading-text="Loading... Please wait"
></v-data-table>
>
<template v-if="compact" #[`item.name`]="{ value }">
<v-tooltip :text="value" location="top">
<template #activator="{ props: tooltipProps }">
<span v-bind="tooltipProps">{{ value }}</span>
</template>
</v-tooltip>
</template>
</v-data-table>
</template>
27 changes: 27 additions & 0 deletions app/components/FeedBack/Snackers.vue
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,15 @@ const show = ref<boolean>(true);
function calc_margin(index: number): string {
return `${index * MARGIN_BETWEEN_SNACKERS + FIRST_SNACKER_MARGIN}px`;
}

function runAction(feedback: (typeof feedbackStore.feedbacks)[number]): void {
try {
feedback.action?.onClick();
} finally {
// The snackbar closes even when the action fails
feedbackStore.delete_feedback(feedback.id);
}
}
</script>

<template>
Expand Down Expand Up @@ -62,9 +71,27 @@ function calc_margin(index: number): string {
</v-tooltip>
</v-col>
<v-spacer />
<v-col v-if="feedback.action" cols="auto">
<v-tooltip location="top" :text="feedback.action.label">
<template #activator="{ props }">
<v-btn
v-bind="props"
icon
variant="flat"
size="20"
:color="feedback.type"
data-testid="feedbackActionBtn"
@click.stop="runAction(feedback)"
>
<v-icon icon="mdi-pencil" size="20" color="white" />
</v-btn>
</template>
</v-tooltip>
</v-col>
<v-col cols="auto">
<v-btn
icon
data-testid="feedbackCloseBtn"
variant="flat"
size="20"
:color="feedback.type"
Expand Down
37 changes: 37 additions & 0 deletions app/components/ProjectCrs.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<script setup lang="ts">
import { formatCrs, formatCrsCode } from "@ogw_front/utils/crs";
import { useCrsStore } from "@ogw_front/stores/crs";

const crsStore = useCrsStore();
const label = computed<string>(() =>
crsStore.projectCrs
? `CRS: ${formatCrsCode(crsStore.projectCrs)} · ${crsStore.projectCrs.name}`
: "No CRS",
);
</script>

<template>
<!-- Under the XYZ axes, drawn by the viewer in the bottom right corner -->
<v-btn
data-testid="projectCrs"
class="project-crs text-none"
size="small"
variant="tonal"
prepend-icon="mdi-earth"
:disabled="!crsStore.crsEditEnabled"
v-tooltip="formatCrs(crsStore.projectCrs)"
@click="crsStore.requestProjectCrsEdit()"
>
<span class="text-truncate">{{ label }}</span>
</v-btn>
</template>

<style scoped>
.project-crs {
position: absolute;
right: 16px;
bottom: 12px;
z-index: 1;
max-width: 360px;
}
</style>
30 changes: 30 additions & 0 deletions app/components/Viewer/ContextMenu/InfoCard.vue
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
<script setup lang="ts">
import { formatCrs, formatCrsCode } from "@ogw_front/utils/crs";
import GlassCard from "@ogw_front/components/GlassCard.vue";
import { consola } from "consola";
import { middleTruncate } from "@ogw_front/utils/string";
import { useCopyToClipboard } from "@ogw_front/composables/copy_to_clipboard";
import { useCrsStore } from "@ogw_front/stores/crs";
import { useDataStore } from "@ogw_front/stores/data";
import { useLiveDataItem } from "@ogw_front/composables/live_data_item";
import { useMenuStore } from "@ogw_front/stores/menu";

interface Props {
Expand Down Expand Up @@ -126,6 +129,13 @@ function formatId(id: string | undefined): string {
}

const formattedId = computed<string>(() => formatId(metaData.id));

const crsStore = useCrsStore();
// A model component shares the model CRS: the CRS row is only shown for a data
const isData = computed<boolean>(() => metaData.id !== undefined && metaData.modelId === undefined);
// The menu metadata is a snapshot taken when it opened: read the item live so a CRS edit shows at once
const liveItem = useLiveDataItem(() => (isData.value ? metaData.id : undefined));
const liveCrs = computed(() => liveItem.value?.crs ?? metaData.crs);
</script>

<template>
Expand Down Expand Up @@ -168,6 +178,26 @@ const formattedId = computed<string>(() => formatId(metaData.id));
class="ml-1"
/>
</v-col>
<v-col
v-if="isData"
class="mt-1 d-inline-flex align-center px-2 py-0.5 text-caption"
style="font-size: 0.6rem"
>
<span class="font-weight-black text-secondary mr-1">CRS</span>
<span class="id-text" data-testid="infoCrsValue">{{ formatCrsCode(liveCrs) }}</span>
<v-tooltip activator="parent" location="top">{{ formatCrs(liveCrs) }}</v-tooltip>
<v-btn
v-if="crsStore.crsEditEnabled"
data-testid="infoEditCrsBtn"
icon="mdi-pencil"
variant="text"
density="compact"
size="x-small"
color="white"
class="ml-1"
@click.stop="crsStore.requestCrsEdit(metaData.id)"
/>
</v-col>

<template v-if="componentItem">
<v-divider class="w-100 my-2" opacity="0.2" />
Expand Down
Loading
Loading