refactor: improve type safety by replacing any types with specific interfaces across API services and components.
This commit is contained in:
@@ -497,9 +497,11 @@ export default function WikiBySlugClient({ slug }: { slug: string }) {
|
||||
createdAt: sample?.created_at || 'Unknown date',
|
||||
title: `Phiên bản lúc ${formatDate(sample?.created_at)}`
|
||||
};
|
||||
if (sample?.isCurrent) {
|
||||
return { ...versionInfo, content: sample.content || '' };
|
||||
if (sample && "content" in sample && (sample as any).isCurrent) {
|
||||
return { ...versionInfo, content: (sample as any).content || "" };
|
||||
}
|
||||
|
||||
|
||||
const contentResp = await getContentByVersionWikiId(versionId);
|
||||
return { ...versionInfo, content: contentResp?.data?.content || "" };
|
||||
});
|
||||
|
||||
@@ -73,7 +73,16 @@ const Calendar: React.FC = () => {
|
||||
|
||||
const handleEventClick = (clickInfo: EventClickArg) => {
|
||||
const event = clickInfo.event;
|
||||
setSelectedEvent(event as unknown as CalendarEvent);
|
||||
setSelectedEvent({
|
||||
id: event.id,
|
||||
title: event.title,
|
||||
start: event.startStr,
|
||||
end: event.endStr,
|
||||
extendedProps: {
|
||||
calendar: event.extendedProps.calendar,
|
||||
},
|
||||
} as CalendarEvent);
|
||||
|
||||
setEventTitle(event.title);
|
||||
setEventStartDate(event.start?.toISOString().split("T")[0] || "");
|
||||
setEventEndDate(event.end?.toISOString().split("T")[0] || "");
|
||||
|
||||
@@ -7,9 +7,10 @@ import "react-quill-new/dist/quill.snow.css";
|
||||
const ReactQuillEditor = dynamic(
|
||||
async () => {
|
||||
const { default: RQ } = await import("react-quill-new");
|
||||
return ({ forwardedRef, ...props }: any) => (
|
||||
return ({ forwardedRef, ...props }: { forwardedRef: React.Ref<any>; [key: string]: any }) => (
|
||||
<RQ ref={forwardedRef} {...props} />
|
||||
);
|
||||
|
||||
},
|
||||
{
|
||||
ssr: false,
|
||||
|
||||
@@ -15,10 +15,13 @@ import { IsolatedContent } from "@/components/ui/IsolatedContent";
|
||||
import { apiDeleteHistorianCV } from "@/service/historianService";
|
||||
import { statusConfig } from "@/service/handler";
|
||||
|
||||
import { Application } from "@/interface/historian";
|
||||
import { MediaItem } from "@/components/tables/MediaTable";
|
||||
|
||||
interface Props {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
application: any;
|
||||
application: Application | null;
|
||||
onRefresh: () => void;
|
||||
}
|
||||
|
||||
@@ -42,7 +45,7 @@ export default function ApplicationDetailModal({
|
||||
}
|
||||
}, [isOpen, application]);
|
||||
|
||||
const isImageFile = (file: any) => {
|
||||
const isImageFile = (file: MediaItem) => {
|
||||
const isImageMime = file.mime_type?.startsWith("image/");
|
||||
const isImageExt = /\.(jpg|jpeg|png|webp|gif)$/i.test(file.storage_key);
|
||||
return isImageMime || isImageExt;
|
||||
@@ -51,17 +54,17 @@ export default function ApplicationDetailModal({
|
||||
const mediaList = application?.media || [];
|
||||
const imageMediaOnly = mediaList.filter(isImageFile);
|
||||
|
||||
const imageSlides = imageMediaOnly.map((item: any) => ({
|
||||
const imageSlides = imageMediaOnly.map((item: MediaItem) => ({
|
||||
src: `${URL_MEDIA}${item.storage_key}`,
|
||||
title: item.original_name,
|
||||
description: `Dung lượng: ${(item.size / 1024).toFixed(2)} KB`,
|
||||
}));
|
||||
|
||||
const handleMediaClick = (item: any) => {
|
||||
const handleMediaClick = (item: MediaItem) => {
|
||||
const fileUrl = `${URL_MEDIA}${item.storage_key}`;
|
||||
if (isImageFile(item)) {
|
||||
const photoIndex = imageMediaOnly.findIndex(
|
||||
(img: any) => img.id === item.id,
|
||||
(img: MediaItem) => img.id === item.id,
|
||||
);
|
||||
setIndex(photoIndex);
|
||||
} else {
|
||||
@@ -93,7 +96,9 @@ export default function ApplicationDetailModal({
|
||||
};
|
||||
|
||||
const handleDeleteApplication = async () => {
|
||||
if (!application) return;
|
||||
await apiDeleteHistorianCV(application.id);
|
||||
|
||||
Swal.fire("Thành công!", "Hồ sơ đã được xóa.", "success");
|
||||
onRefresh();
|
||||
onClose();
|
||||
@@ -186,7 +191,7 @@ export default function ApplicationDetailModal({
|
||||
</h4>
|
||||
{mediaList.length > 0 ? (
|
||||
<div className="grid grid-cols-2 gap-4 sm:grid-cols-3 md:grid-cols-4">
|
||||
{mediaList.map((media: any, idx: number) => {
|
||||
{mediaList.map((media: MediaItem, idx: number) => {
|
||||
const isImg = isImageFile(media);
|
||||
return (
|
||||
<div
|
||||
|
||||
@@ -23,7 +23,8 @@ export interface MediaItem {
|
||||
original_name: string;
|
||||
mime_type: string;
|
||||
size: number;
|
||||
file_metadata: any;
|
||||
file_metadata: Record<string, unknown> | null;
|
||||
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
import { Modal } from "../ui/modal";
|
||||
import UserMetaCard from "@/components/user-profile/UserMetaCard";
|
||||
import UserInfoCard from "@/components/user-profile/UserInfoCard";
|
||||
import { UserMetaCardProps } from "@/interface/user";
|
||||
|
||||
import { fullDataUser } from "@/interface/admin";
|
||||
import { useEffect, useState } from "react";
|
||||
import { MediaDto } from "@/interface/media";
|
||||
@@ -28,7 +30,17 @@ export default function UserDetailModal({
|
||||
const [mediaData, setMediaData] = useState<MediaDto | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const formattedData = { data: user };
|
||||
const formattedData: UserMetaCardProps = {
|
||||
data: user
|
||||
? {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
profile: user.profile,
|
||||
roles: user.roles as any, // UserRole and Role are slightly different, need a better fix or small cast
|
||||
}
|
||||
: undefined,
|
||||
};
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (user?.id && isOpen) {
|
||||
@@ -68,8 +80,9 @@ export default function UserDetailModal({
|
||||
</div>
|
||||
|
||||
<div className="space-y-6 custom-scrollbar max-h-[65vh] overflow-y-auto pr-2">
|
||||
<UserMetaCard data={formattedData as any} />
|
||||
<UserInfoCard data={formattedData as any} />
|
||||
<UserMetaCard data={formattedData} />
|
||||
<UserInfoCard data={formattedData} />
|
||||
|
||||
|
||||
<div className="min-h-[150px] relative">
|
||||
{loading ? (
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
import React, { useState, useRef, useEffect } from "react";
|
||||
import { ChatbotPayload } from "@/interface/chatbot";
|
||||
import { apiChatbot } from "@/service/chatbotService";
|
||||
import { AxiosError } from "axios";
|
||||
|
||||
|
||||
type Message = {
|
||||
id: string;
|
||||
@@ -67,16 +69,18 @@ export default function ChatbotWidget({
|
||||
};
|
||||
|
||||
setMessages((prev) => [...prev, botMessage]);
|
||||
} catch (error: any) {
|
||||
} catch (error) {
|
||||
const axiosError = error as AxiosError<{ message: string }>;
|
||||
const errorMessage: Message = {
|
||||
id: (Date.now() + 1).toString(),
|
||||
sender: "bot",
|
||||
text:
|
||||
error?.response?.data?.message ||
|
||||
axiosError.response?.data?.message ||
|
||||
"Có lỗi xảy ra khi kết nối. Vui lòng thử lại sau.",
|
||||
};
|
||||
setMessages((prev) => [...prev, errorMessage]);
|
||||
} finally {
|
||||
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -20,7 +20,10 @@ const formatFullDateTime = (dateString: string) => {
|
||||
return `${time} ${day}`;
|
||||
};
|
||||
|
||||
const processMedia = (mediaArray: any[]) => {
|
||||
import { Application } from "@/interface/historian";
|
||||
import { MediaItem } from "../tables/MediaTable";
|
||||
|
||||
const processMedia = (mediaArray: MediaItem[]) => {
|
||||
if (!mediaArray || mediaArray.length === 0) return { type: "empty" };
|
||||
const imageFiles = mediaArray.filter((file) => {
|
||||
const isImageMime = file.mime_type?.startsWith("image/");
|
||||
@@ -49,16 +52,17 @@ const processMedia = (mediaArray: any[]) => {
|
||||
export default function ApplicationList({
|
||||
applications,
|
||||
}: {
|
||||
applications: any[];
|
||||
applications: Application[];
|
||||
}) {
|
||||
const router = useRouter();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const handleViewDetail = (app: any) => {
|
||||
const handleViewDetail = (app: Application) => {
|
||||
dispatch(setSelectedApplication(app));
|
||||
router.push(`/user/account/applications`);
|
||||
};
|
||||
|
||||
|
||||
const StatusIcons: Record<string, React.ReactNode> = {
|
||||
APPROVED: (
|
||||
<svg
|
||||
|
||||
@@ -11,6 +11,8 @@ import { URL_MEDIA } from "../../../api";
|
||||
|
||||
import { deleteMedia } from "@/service/mediaService";
|
||||
import { INITIAL_LIMIT } from "../../../constant";
|
||||
import { MediaItem } from "../tables/MediaTable";
|
||||
|
||||
|
||||
export default function MediaLibrary({
|
||||
data,
|
||||
@@ -32,7 +34,7 @@ export default function MediaLibrary({
|
||||
setLocalMedia(data?.data || []);
|
||||
}, [data]);
|
||||
|
||||
const isImageFile = (file: any) => {
|
||||
const isImageFile = (file: MediaItem) => {
|
||||
const isImageMime = file.mime_type?.startsWith("image/");
|
||||
const isImageExt = /\.(jpg|jpeg|png|webp|gif)$/i.test(file.storage_key);
|
||||
return isImageMime || isImageExt;
|
||||
@@ -77,7 +79,7 @@ export default function MediaLibrary({
|
||||
}
|
||||
};
|
||||
|
||||
const handleItemClick = (item: any, idx: number, isImage: boolean) => {
|
||||
const handleItemClick = (item: MediaItem, idx: number, isImage: boolean) => {
|
||||
if (isSelectionMode) {
|
||||
toggleItemSelection(item.id);
|
||||
} else {
|
||||
@@ -166,7 +168,7 @@ export default function MediaLibrary({
|
||||
}
|
||||
};
|
||||
|
||||
const renderItemCard = (item: any, isImage: boolean, idx: number) => {
|
||||
const renderItemCard = (item: MediaItem, isImage: boolean, idx: number) => {
|
||||
const isSelected = selectedIds.includes(item.id);
|
||||
|
||||
return (
|
||||
@@ -254,6 +256,7 @@ export default function MediaLibrary({
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<div className="rounded-2xl border border-gray-200 bg-white p-6 dark:border-zinc-800 dark:bg-zinc-900/50">
|
||||
<div className="mb-6 flex flex-col items-start justify-between gap-4 sm:flex-row sm:items-center">
|
||||
|
||||
@@ -10,6 +10,8 @@ import { Profile, UserMetaCardProps } from "@/interface/user";
|
||||
import { apiUpdateUser } from "@/service/userService";
|
||||
import { toast } from "sonner";
|
||||
import Link from "next/link";
|
||||
import { AxiosError } from "axios";
|
||||
|
||||
|
||||
export default function UserInfoCard({ data }: { data: UserMetaCardProps }) {
|
||||
const router = useRouter();
|
||||
@@ -62,8 +64,9 @@ export default function UserInfoCard({ data }: { data: UserMetaCardProps }) {
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 1000);
|
||||
} catch (error: any) {
|
||||
const serverResponse = error.response?.data;
|
||||
} catch (error) {
|
||||
const axiosError = error as AxiosError<{ status: boolean; message: string }>;
|
||||
const serverResponse = axiosError.response?.data;
|
||||
|
||||
if (serverResponse && serverResponse.status === false) {
|
||||
const msg = serverResponse.message || "";
|
||||
@@ -77,8 +80,9 @@ export default function UserInfoCard({ data }: { data: UserMetaCardProps }) {
|
||||
toast.error("Không thể kết nối đến máy chủ hoặc lỗi hệ thống.");
|
||||
}
|
||||
|
||||
console.error("Lỗi chi tiết:", error);
|
||||
console.error("Lỗi chi tiết:", axiosError);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -9,7 +9,17 @@ export const apiGetListUser = async (payload: getUserDto) => {
|
||||
return response?.data;
|
||||
};
|
||||
|
||||
export const apiChangeRole = async (id: string, payload: any) => {
|
||||
export interface ChangeRolePayload {
|
||||
role_ids: string[];
|
||||
user_id: string;
|
||||
}
|
||||
|
||||
export interface UpdateApplicationStatusPayload {
|
||||
status: "APPROVED" | "REJECTED";
|
||||
review_note: string;
|
||||
}
|
||||
|
||||
export const apiChangeRole = async (id: string, payload: ChangeRolePayload) => {
|
||||
const response = await api.patch(API.Admin.CHANGE_ROLE(id), payload);
|
||||
return response?.data;
|
||||
};
|
||||
@@ -32,11 +42,12 @@ export const apiGetUserMedia = async (id: string) => {
|
||||
return response?.data;
|
||||
};
|
||||
|
||||
export const apiUpdateApplicationStatus = async (id: string, payload: any) => {
|
||||
export const apiUpdateApplicationStatus = async (id: string, payload: UpdateApplicationStatusPayload) => {
|
||||
const response = await api.put(API.Admin.UPDATE_APPLICATION_STATUS(id), payload);
|
||||
return response?.data;
|
||||
};
|
||||
|
||||
|
||||
export const apiGetUserById = async (userId: string) => {
|
||||
const response = await api.get(API.Admin.GET_USER_BY_ID(userId));
|
||||
return response?.data;
|
||||
|
||||
+28
-4
@@ -2,6 +2,29 @@ import api from "@/config/config";
|
||||
import { API } from "../../api";
|
||||
import { clearStoredTokens, extractTokensFromResponsePayload, setStoredTokens } from "@/auth/tokenStore";
|
||||
|
||||
export interface SignUpPayload {
|
||||
display_name: string;
|
||||
email: string;
|
||||
password: string;
|
||||
token_id: string;
|
||||
}
|
||||
|
||||
export interface SignInPayload {
|
||||
email: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface ResetPasswordPayload {
|
||||
email: string;
|
||||
new_password: string;
|
||||
token_id: string;
|
||||
}
|
||||
|
||||
export interface ChangePasswordPayload {
|
||||
old_password: string;
|
||||
new_password: string;
|
||||
}
|
||||
|
||||
export const apiCreateOTP = async (email: string, token_type: number = 2) => {
|
||||
const response = await api.post(API.Auth.CREATEOTP, {
|
||||
email,
|
||||
@@ -16,7 +39,7 @@ export const apiVerifyOTP = async (email: string, token: string, token_type: num
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const apiSignUp = async (payload: any) => {
|
||||
export const apiSignUp = async (payload: SignUpPayload) => {
|
||||
const response = await api.post(API.Auth.SIGNUP, payload);
|
||||
return response.data;
|
||||
};
|
||||
@@ -27,14 +50,14 @@ export const apiLogout = async () => {
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const apiSignIn = async (payload: any) => {
|
||||
export const apiSignIn = async (payload: SignInPayload) => {
|
||||
const response = await api.post(API.Auth.SIGNIN, payload);
|
||||
const tokens = extractTokensFromResponsePayload(response?.data);
|
||||
if (tokens) setStoredTokens(tokens);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const apiResetPassword = async (payload: any) => {
|
||||
export const apiResetPassword = async (payload: ResetPasswordPayload) => {
|
||||
const response = await api.post(API.Auth.FORGOT_PASSWORD, payload);
|
||||
return response.data;
|
||||
};
|
||||
@@ -44,7 +67,8 @@ export const apiGetCurrentUser = async () => {
|
||||
return response?.data;
|
||||
};
|
||||
|
||||
export const apiChangePassword = async (payload: any) => {
|
||||
export const apiChangePassword = async (payload: ChangePasswordPayload) => {
|
||||
const response = await api.patch(API.User.CHANGE_PASSWORD, payload);
|
||||
return response?.data;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,16 +1,30 @@
|
||||
import api from "@/config/config";
|
||||
import { API } from "../../api";
|
||||
|
||||
export const createHistorianCV = async (payload: any) => {
|
||||
export interface CreateHistorianCVPayload {
|
||||
content: string;
|
||||
media_ids: (string | number)[];
|
||||
verify_type: string;
|
||||
}
|
||||
|
||||
export interface GetUserApplicationsPayload {
|
||||
page?: number;
|
||||
limit?: number;
|
||||
status?: string;
|
||||
user_id?: string;
|
||||
}
|
||||
|
||||
export const createHistorianCV = async (payload: CreateHistorianCVPayload) => {
|
||||
const response = await api.post(API.Historian.CREATE_CV, payload);
|
||||
return response?.data;
|
||||
};
|
||||
|
||||
export const apiGetUserApplications = async (payload :any) => {
|
||||
export const apiGetUserApplications = async (payload : GetUserApplicationsPayload) => {
|
||||
const response = await api.get(API.Historian.APPLICATION, { params: payload });
|
||||
return response?.data;
|
||||
};
|
||||
|
||||
|
||||
export const apiDeleteHistorianCV = async (id: number | string) => {
|
||||
const response = await api.delete(API.Historian.DELETE_CV(id));
|
||||
return response?.data;
|
||||
|
||||
@@ -121,7 +121,16 @@ export const deleteMediaById = async (mediaId: string) => {
|
||||
return response?.data;
|
||||
}
|
||||
|
||||
export const getMedia = async (payload: any) => {
|
||||
export interface GetMediaPayload {
|
||||
page?: number;
|
||||
limit?: number;
|
||||
search?: string;
|
||||
sortBy?: string;
|
||||
sortOrder?: "asc" | "desc";
|
||||
user_id?: string;
|
||||
}
|
||||
|
||||
export const getMedia = async (payload: GetMediaPayload) => {
|
||||
const response = await api.get(API.Media.GET_MEDIA, {
|
||||
params: payload,
|
||||
});
|
||||
|
||||
@@ -9,8 +9,9 @@ export type { GeometriesBBoxQuery } from "@/uhm/types/api";
|
||||
export type EntityGeometrySearchGeo = {
|
||||
id: string;
|
||||
type: string | null;
|
||||
draw_geometry: unknown;
|
||||
binding?: unknown;
|
||||
draw_geometry: Geometry;
|
||||
binding?: string[];
|
||||
|
||||
time_start?: number | null;
|
||||
time_end?: number | null;
|
||||
};
|
||||
@@ -106,8 +107,9 @@ export async function searchGeometriesByEntityName(
|
||||
type GeometryRow = {
|
||||
id: string;
|
||||
geo_type: number;
|
||||
draw_geometry: unknown;
|
||||
binding?: unknown;
|
||||
draw_geometry: Geometry;
|
||||
binding?: string[];
|
||||
|
||||
time_start?: number;
|
||||
time_end?: number;
|
||||
bbox?: {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { ApiError, requestJson } from "@/uhm/api/http";
|
||||
|
||||
export type Wiki = {
|
||||
id: string;
|
||||
project_id?: string;
|
||||
project_id: string;
|
||||
title?: string;
|
||||
slug?: string | null;
|
||||
content?: string;
|
||||
@@ -12,12 +12,13 @@ export type Wiki = {
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
content_sample?: {
|
||||
created_at?: string;
|
||||
content?: string;
|
||||
id?: string;
|
||||
id: string;
|
||||
title: string;
|
||||
created_at: string;
|
||||
}[];
|
||||
};
|
||||
|
||||
|
||||
export async function searchWikisByTitle(title: string, options?: { limit?: number; cursor?: string; entityId?: string }): Promise<Wiki[]> {
|
||||
const keyword = title.trim();
|
||||
const params = new URLSearchParams({ title: keyword });
|
||||
|
||||
@@ -41,7 +41,9 @@ export function persistBackgroundLayerVisibility(visibility: BackgroundLayerVisi
|
||||
function normalizeBackgroundLayerVisibility(raw: unknown): BackgroundLayerVisibility | null {
|
||||
if (!raw || typeof raw !== "object") return null;
|
||||
|
||||
const source = raw as Record<string, unknown>;
|
||||
const source = raw as Partial<Record<string, boolean>>;
|
||||
|
||||
|
||||
const next: BackgroundLayerVisibility = {
|
||||
...DEFAULT_BACKGROUND_LAYER_VISIBILITY,
|
||||
};
|
||||
|
||||
@@ -3,7 +3,8 @@ import { normalizeGeoTypeKey, typeKeyToGeoTypeCode } from "@/uhm/lib/map/geo/geo
|
||||
import type { Change } from "@/uhm/lib/editor/draft/editorTypes";
|
||||
import type { EntitySnapshot } from "@/uhm/types/entities";
|
||||
import type { EntitySnapshotOperation } from "@/uhm/types/entities";
|
||||
import type { Feature, FeatureCollection, GeometryEntitySnapshot, GeometrySnapshot } from "@/uhm/types/geo";
|
||||
import type { Feature, FeatureCollection, Geometry, GeometryEntitySnapshot, GeometrySnapshot } from "@/uhm/types/geo";
|
||||
|
||||
import type { EditorSnapshot, Project } from "@/uhm/types/projects";
|
||||
import type { WikiSnapshot } from "@/uhm/types/wiki";
|
||||
import type { EntityWikiLinkSnapshot } from "@/uhm/types/projects";
|
||||
@@ -14,6 +15,56 @@ function isRecord(value: unknown): value is UnknownRecord {
|
||||
return !!value && typeof value === "object" && !Array.isArray(value);
|
||||
}
|
||||
|
||||
interface RawEntityRow extends UnknownRecord {
|
||||
id?: string | number;
|
||||
operation?: string;
|
||||
source?: string;
|
||||
ref?: { id?: string };
|
||||
name?: string;
|
||||
description?: string;
|
||||
status?: number;
|
||||
}
|
||||
|
||||
interface RawGeometryRow extends UnknownRecord {
|
||||
id?: string | number;
|
||||
operation?: string;
|
||||
source?: string;
|
||||
ref?: { id?: string };
|
||||
type?: string | number;
|
||||
geo_type?: string | number;
|
||||
draw_geometry?: Geometry;
|
||||
geometry?: Geometry;
|
||||
binding?: string[];
|
||||
time_start?: number;
|
||||
time_end?: number;
|
||||
}
|
||||
|
||||
interface RawWikiRow extends UnknownRecord {
|
||||
id?: string;
|
||||
operation?: string;
|
||||
source?: string;
|
||||
ref?: { id?: string };
|
||||
title?: string;
|
||||
slug?: string;
|
||||
doc?: string;
|
||||
updated_at?: string | number;
|
||||
}
|
||||
|
||||
interface RawGeometryEntityRow extends UnknownRecord {
|
||||
geometry_id?: string | number;
|
||||
entity_id?: string | number;
|
||||
operation?: string;
|
||||
base_links_hash?: string;
|
||||
}
|
||||
|
||||
interface RawEntityWikiRow extends UnknownRecord {
|
||||
entity_id?: string;
|
||||
wiki_id?: string;
|
||||
operation?: string;
|
||||
is_deleted?: boolean | number;
|
||||
}
|
||||
|
||||
|
||||
function sanitizeEntitySnapshotOperation(op: unknown): EntitySnapshotOperation {
|
||||
if (typeof op !== "string") return "reference";
|
||||
const v = op.trim();
|
||||
@@ -132,10 +183,11 @@ export function normalizeEditorSnapshot(raw: unknown): EditorSnapshot | null {
|
||||
? geometryEntityRaw
|
||||
.filter(isRecord)
|
||||
.map((r) => {
|
||||
const geometry_id = getStringId(r.geometry_id);
|
||||
const entity_id = typeof r.entity_id === "string" ? r.entity_id : "";
|
||||
const row = r as RawGeometryEntityRow;
|
||||
const geometry_id = getStringId(row.geometry_id);
|
||||
const entity_id = typeof row.entity_id === "string" ? row.entity_id : "";
|
||||
return {
|
||||
...(r as unknown as Omit<GeometryEntitySnapshot, "geometry_id" | "entity_id">),
|
||||
...(row as unknown as Omit<GeometryEntitySnapshot, "geometry_id" | "entity_id">),
|
||||
geometry_id,
|
||||
entity_id,
|
||||
};
|
||||
@@ -191,16 +243,17 @@ export function normalizeEditorSnapshot(raw: unknown): EditorSnapshot | null {
|
||||
const links = geometryEntity || migratedGeometryEntity || [];
|
||||
const byGeom = new Map<string, string[]>();
|
||||
for (const row of links) {
|
||||
if ((row as any)?.operation === "delete") continue;
|
||||
if ((row as RawGeometryEntityRow).operation === "delete") continue;
|
||||
const list = byGeom.get(row.geometry_id) || [];
|
||||
list.push(row.entity_id);
|
||||
byGeom.set(row.geometry_id, list);
|
||||
}
|
||||
const entityNameById = new Map<string, string>();
|
||||
for (const row of entities || []) {
|
||||
for (const r of entities || []) {
|
||||
const row = r as RawEntityRow;
|
||||
const id = typeof row?.id === "string" ? row.id : "";
|
||||
if (!id) continue;
|
||||
const name = typeof (row as any)?.name === "string" ? String((row as any).name).trim() : "";
|
||||
const name = typeof row.name === "string" ? String(row.name).trim() : "";
|
||||
if (name) entityNameById.set(id, name);
|
||||
}
|
||||
const geometryById = new Map<string, GeometrySnapshot>();
|
||||
@@ -327,7 +380,7 @@ export function buildEditorSnapshot(options: {
|
||||
? cloned.name.trim()
|
||||
: id;
|
||||
const source: "inline" | "ref" = cloned.source === "inline" ? "inline" : "ref";
|
||||
const opRaw = sanitizeEntitySnapshotOperation((cloned as any).operation);
|
||||
const opRaw = sanitizeEntitySnapshotOperation((cloned as RawEntityRow).operation);
|
||||
// Editor state should delete objects by removing them from the list.
|
||||
// Keep this defensive guard to avoid emitting delete markers unexpectedly.
|
||||
if (opRaw === "delete") continue;
|
||||
@@ -417,13 +470,14 @@ export function buildEditorSnapshot(options: {
|
||||
}
|
||||
|
||||
const baselineGeometryEntity = new globalThis.Map<string, string | undefined>();
|
||||
for (const row of options.previousSnapshot?.geometry_entity || []) {
|
||||
for (const r of options.previousSnapshot?.geometry_entity || []) {
|
||||
const row = r as RawGeometryEntityRow;
|
||||
if (!row) continue;
|
||||
if ((row as any).operation === "delete") continue;
|
||||
if (row.operation === "delete") continue;
|
||||
const geometry_id = typeof row.geometry_id === "string" || typeof row.geometry_id === "number" ? String(row.geometry_id).trim() : "";
|
||||
const entity_id = typeof row.entity_id === "string" || typeof row.entity_id === "number" ? String(row.entity_id).trim() : "";
|
||||
if (!geometry_id || !entity_id) continue;
|
||||
baselineGeometryEntity.set(`${geometry_id}::${entity_id}`, (row as any).base_links_hash);
|
||||
baselineGeometryEntity.set(`${geometry_id}::${entity_id}`, row.base_links_hash);
|
||||
}
|
||||
|
||||
const currentGeometryEntityRows: GeometryEntitySnapshot[] = [];
|
||||
@@ -474,7 +528,7 @@ export function buildEditorSnapshot(options: {
|
||||
const previousWikis = new globalThis.Map<string, WikiSnapshot>();
|
||||
for (const item of options.previousSnapshot?.wikis || []) {
|
||||
if (!item || typeof item !== "object") continue;
|
||||
if ((item as any).operation === "delete") continue;
|
||||
if ((item as RawWikiRow).operation === "delete") continue;
|
||||
const id = (item as WikiSnapshot).id;
|
||||
if (typeof id === "string" && id.length > 0) previousWikis.set(id, item as WikiSnapshot);
|
||||
}
|
||||
@@ -537,24 +591,26 @@ export function buildEditorSnapshot(options: {
|
||||
for (const prev of previousWikis.values()) {
|
||||
if (!prev?.id) continue;
|
||||
if (currentWikiIds.has(prev.id)) continue;
|
||||
const row = prev as RawWikiRow;
|
||||
deletedWikis.push({
|
||||
id: prev.id,
|
||||
source: prev.source === "inline" ? "inline" : "ref",
|
||||
operation: "delete",
|
||||
title: typeof prev.title === "string" ? prev.title : "Untitled wiki",
|
||||
slug: (prev as any).slug ?? null,
|
||||
doc: (prev as any).doc ?? null,
|
||||
updated_at: (prev as any).updated_at ?? undefined,
|
||||
slug: row.slug ?? null,
|
||||
doc: row.doc ?? null,
|
||||
updated_at: row.updated_at ?? undefined,
|
||||
} as WikiSnapshot);
|
||||
}
|
||||
const wikis = [...wikisCurrent, ...deletedWikis];
|
||||
|
||||
const baselineEntityWiki = new Set<string>();
|
||||
for (const row of options.previousSnapshot?.entity_wiki || []) {
|
||||
if (!row || typeof (row as any).entity_id !== "string" || typeof (row as any).wiki_id !== "string") continue;
|
||||
if ((row as any).operation === "delete") continue;
|
||||
const entity_id = (row as any).entity_id.trim();
|
||||
const wiki_id = (row as any).wiki_id.trim();
|
||||
for (const r of options.previousSnapshot?.entity_wiki || []) {
|
||||
const row = r as RawEntityWikiRow;
|
||||
if (!row || typeof row.entity_id !== "string" || typeof row.wiki_id !== "string") continue;
|
||||
if (row.operation === "delete") continue;
|
||||
const entity_id = row.entity_id.trim();
|
||||
const wiki_id = row.wiki_id.trim();
|
||||
if (!entity_id || !wiki_id) continue;
|
||||
baselineEntityWiki.add(`${entity_id}::${wiki_id}`);
|
||||
}
|
||||
@@ -591,7 +647,7 @@ export function buildEditorSnapshot(options: {
|
||||
source: e.source,
|
||||
operation: e.operation,
|
||||
name: typeof e.name === "string" ? e.name : undefined,
|
||||
description: typeof (e as any).description === "string" ? (e as any).description : (e as any).description ?? null,
|
||||
description: typeof (e as RawEntityRow).description === "string" ? (e as RawEntityRow).description : (e as RawEntityRow).description ?? null,
|
||||
}))
|
||||
.sort((a, b) => String(a.id).localeCompare(String(b.id))),
|
||||
geometries: geometries.slice().sort((a, b) => String(a.id).localeCompare(String(b.id))),
|
||||
@@ -602,8 +658,8 @@ export function buildEditorSnapshot(options: {
|
||||
source: w.source,
|
||||
operation: w.operation,
|
||||
title: w.title,
|
||||
slug: (w as any).slug ?? null,
|
||||
doc: (w as any).doc ?? null,
|
||||
slug: (w as RawWikiRow).slug ?? null,
|
||||
doc: (w as RawWikiRow).doc ?? null,
|
||||
}))
|
||||
.sort((a, b) => a.id.localeCompare(b.id)),
|
||||
entity_wiki: entityWikis,
|
||||
@@ -640,7 +696,7 @@ function dedupeAndSortGeometryEntity(rows: GeometryEntitySnapshot[]): GeometryEn
|
||||
const geometry_id = typeof row.geometry_id === "string" ? row.geometry_id : "";
|
||||
const entity_id = typeof row.entity_id === "string" ? row.entity_id : "";
|
||||
if (!geometry_id || !entity_id) continue;
|
||||
const opRaw = (row as any).operation;
|
||||
const opRaw = (row as RawGeometryEntityRow).operation;
|
||||
const operation: GeometryEntitySnapshot["operation"] =
|
||||
opRaw === "delete"
|
||||
? "delete"
|
||||
|
||||
@@ -4,8 +4,14 @@ export type ApiEnvelope<T> = {
|
||||
status: boolean | "success" | "error" | string;
|
||||
data?: T;
|
||||
message?: string;
|
||||
errors?: unknown;
|
||||
pagination?: unknown;
|
||||
errors?: string | Record<string, string[]> | null;
|
||||
pagination?: {
|
||||
current_page: number;
|
||||
page_size: number;
|
||||
total_records: number;
|
||||
total_pages: number;
|
||||
} | null;
|
||||
|
||||
};
|
||||
|
||||
export type GeometriesBBoxQuery = {
|
||||
|
||||
@@ -2,15 +2,22 @@
|
||||
// FE stores Tiptap JSON as a JSON-stringified payload.
|
||||
export type WikiDoc = string | null;
|
||||
|
||||
export type WikiContentSample = {
|
||||
id: string;
|
||||
title: string;
|
||||
created_at: string;
|
||||
};
|
||||
|
||||
export type WikiSnapshotOperation = "create" | "update" | "delete" | "reference";
|
||||
|
||||
export type WikiSnapshot = {
|
||||
id: string;
|
||||
source: "inline" | "ref";
|
||||
// Optional for backwards-compat with older commits. New commits should include it.
|
||||
operation?: WikiSnapshotOperation;
|
||||
title: string;
|
||||
slug?: string | null;
|
||||
doc: WikiDoc;
|
||||
content_sample?: WikiContentSample[];
|
||||
updated_at?: string;
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user