feat: Support multi-select editor workflow and improve UI/UX

- Refactored state from single selectedFeatureId to selectedFeatureIds array in Editor and Viewer
- Updated Map component to support multi-select filtering for geometry binding visibility
- Made entity, wiki, and geometry side panels scrollable for better overflow handling
- Fixed viewer mode wiki link navigation for independent wikis
- Improved geometry binding UX and state synchronization
This commit is contained in:
taDuc
2026-05-11 04:49:28 +07:00
parent f2f5295218
commit fe7696b72d
14 changed files with 200 additions and 161 deletions
@@ -46,7 +46,7 @@ type Options = {
setSnapshotEntities: Dispatch<SetStateAction<EntitySnapshot[]>>;
setSnapshotWikis: Dispatch<SetStateAction<WikiSnapshot[]>>;
setSnapshotEntityWikiLinks: Dispatch<SetStateAction<EntityWikiLinkSnapshot[]>>;
setSelectedFeatureId: Dispatch<SetStateAction<FeatureId | null>>;
setSelectedFeatureIds: Dispatch<SetStateAction<FeatureId[]>>;
setEntityFormStatus: Dispatch<SetStateAction<string | null>>;
setEntityStatus: Dispatch<SetStateAction<string | null>>;
setIsSaving: Dispatch<SetStateAction<boolean>>;
@@ -76,7 +76,7 @@ export function useSectionCommands(options: Options) {
options.setSnapshotEntities(sessionSnapshot?.entities || []);
options.setSnapshotWikis(sessionSnapshot?.wikis || []);
options.setSnapshotEntityWikiLinks(sessionSnapshot?.entity_wiki || []);
options.setSelectedFeatureId(null);
options.setSelectedFeatureIds([]);
options.setEntityFormStatus(null);
}, [options]);
@@ -271,7 +271,7 @@ export function useSectionCommands(options: Options) {
options.setSnapshotEntities(sessionSnapshot?.entities || []);
options.setSnapshotWikis(sessionSnapshot?.wikis || []);
options.setSnapshotEntityWikiLinks(sessionSnapshot?.entity_wiki || []);
options.setSelectedFeatureId(null);
options.setSelectedFeatureIds([]);
options.setEntityFormStatus(null);
// Refresh commits list for UI, but keep sectionState/head as-is.
@@ -14,8 +14,8 @@ export function useEntitySessionState() {
const [snapshotEntities, setSnapshotEntities] = useState<EntitySnapshot[]>([]);
// Thông báo trạng thái/lỗi liên quan entity/session.
const [entityStatus, setEntityStatus] = useState<string | null>(null);
// Feature đang được chọn để thao tác bind entities/metadata.
const [selectedFeatureId, setSelectedFeatureId] = useState<FeatureId | null>(null);
// Features đang được chọn để thao tác bind entities/metadata.
const [selectedFeatureIds, setSelectedFeatureIds] = useState<FeatureId[]>([]);
// Form tạo entity mới (độc lập).
const [entityForm, setEntityForm] = useState<EntityFormState>({
name: "",
@@ -50,8 +50,8 @@ export function useEntitySessionState() {
setSnapshotEntities,
entityStatus,
setEntityStatus,
selectedFeatureId,
setSelectedFeatureId,
selectedFeatureIds,
setSelectedFeatureIds,
entityForm,
setEntityForm,
selectedGeometryEntityIds,
+4 -4
View File
@@ -7,7 +7,7 @@ export function initSelect(
getMode: ModeGetter,
onDelete?: (id: string | number) => void,
onEdit?: (feature: maplibregl.MapGeoJSONFeature) => void,
onSelectId?: (id: string | number | null) => void
onSelectIds?: (ids: (string | number)[]) => void
) {
const SELECTABLE_LAYERS = [
"countries-fill",
@@ -35,7 +35,7 @@ export function initSelect(
selectedIds.forEach((id) => setSelectionStateForId(id, false));
selectedIds.clear();
if (emit) {
onSelectId?.(null);
onSelectIds?.([]);
}
}
@@ -52,13 +52,13 @@ export function initSelect(
// Alt + click on an already selected feature removes it from the selection
setSelectionStateForId(id, false);
selectedIds.delete(id);
onSelectId?.(selectedIds.size === 1 ? Array.from(selectedIds)[0] : null);
onSelectIds?.(Array.from(selectedIds));
return;
}
setSelectionStateForId(id, true);
selectedIds.add(id);
onSelectId?.(selectedIds.size === 1 ? id : null);
onSelectIds?.(Array.from(selectedIds));
}
// Chọn feature theo click trái, hỗ trợ additive bằng Alt.
+2 -1
View File
@@ -61,7 +61,8 @@ export function snapToNearestGeometry(
}
const type = feature.geometry.type;
const coords = feature.geometry.coordinates as any;
if (type === "GeometryCollection") continue;
const coords = (feature.geometry as any).coordinates;
// Xử lý cả Polygon và LineString vì viền bản đồ (border) đôi khi được render dưới dạng LineString
if (type === "Polygon") {