refactor: improve type safety by replacing any types with specific interfaces across API services and components.

This commit is contained in:
taDuc
2026-05-14 21:54:44 +07:00
parent dca3ca67ad
commit b220798978
20 changed files with 249 additions and 71 deletions
+4 -2
View File
@@ -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 || "" };
});
+10 -1
View File
@@ -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] || "");
+2 -1
View File
@@ -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
+2 -1
View File
@@ -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;
}
+16 -3
View File
@@ -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 ? (
+6 -2
View File
@@ -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
+6 -3
View File
@@ -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">
+7 -3
View File
@@ -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 (
+13 -2
View File
@@ -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
View File
@@ -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;
};
+16 -2
View File
@@ -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;
+10 -1
View File
@@ -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,
});
+6 -4
View File
@@ -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?: {
+6 -5
View File
@@ -4,20 +4,21 @@ 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;
is_deleted?: boolean;
created_at?: string;
updated_at?: string;
content_sample?:{
created_at?: string;
content?: string;
id?: string;
content_sample?: {
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,
};
+80 -24
View File
@@ -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"
+8 -2
View File
@@ -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 = {
+8 -1
View File
@@ -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;
};