create/update actions and redis cahce store completed tested

This commit is contained in:
Berkay 2025-06-21 23:44:52 +03:00
parent 06ca2b0835
commit 311736ce06
19 changed files with 1717 additions and 48 deletions

View File

@ -43,10 +43,44 @@ Network Manager NM Network Manager
Application Manager AM Application Manager
Super User SUE Super User
Daire Sakini Vekili Daire Sakini Vekili FL-REP Daire FL
URL Type Tested
/building/accounts/managment/accounts : flat_representative No
/definitions/identifications/people : flat_tenant No
## Table Component with Edit/Create Functionality
### Features Implemented
1. **Table Actions Column**
- Added an actions column with pencil icon edit button as the first column in the table
- Implemented proper styling and visibility for the edit button
- Ensured the button appears in each row of the table
2. **Redis Cache Integration**
- Implemented data caching between list and edit/create views
- Row data is stored in Redis when edit button is clicked
- Cached data is retrieved and populated in the update form
- Boolean values (checkboxes) are properly handled with type conversion
3. **Navigation**
- Added dynamic routing based on activePageUrl
- Edit button navigates to `/panel/${activePageUrl}/update`
- Added "Create New" button that navigates to `/panel/${activePageUrl}/create`
- Added "Back to List" buttons on both create and update forms
4. **UI Improvements**
- Fixed page dropdown to always show at least page 1 when there's only one page
- Added debugging logs for troubleshooting
- Improved form field handling with proper type conversion
### Implementation Details
- Used `@tanstack/react-table` for table rendering
- Leveraged Redis for state management between pages
- Implemented proper error handling and debugging
- Used Next.js routing for navigation

View File

@ -0,0 +1,90 @@
import {
getCacheFromRedis,
setCacheToRedis,
clearCacheFromRedis,
} from "@/fetchers/custom/context/cache/fetch";
import { AuthError } from "@/fetchers/types/context";
import { NextResponse } from "next/server";
export async function GET(request: Request) {
try {
// Get the URL from query parameters
const { searchParams } = new URL(request.url);
const url = searchParams.get("url");
if (!url) {
return new NextResponse(
JSON.stringify({ status: 400, error: "URL parameter is required" }),
{ status: 400, headers: { "Content-Type": "application/json" } }
);
}
const cacheData = await getCacheFromRedis(url);
return NextResponse.json({ status: 200, data: cacheData || null });
} catch (error) {
if (error instanceof AuthError) {
return new NextResponse(
JSON.stringify({ status: 401, error: error.message }),
{ status: 401, headers: { "Content-Type": "application/json" } }
);
}
return new NextResponse(
JSON.stringify({ status: 500, error: "Internal server error" }),
{ status: 500, headers: { "Content-Type": "application/json" } }
);
}
}
export async function POST(request: Request) {
try {
const body = await request.json();
// Check if required parameters are present
if (!body.url || !body.data) {
return new NextResponse(
JSON.stringify({
status: 400,
error: "URL and data parameters are required",
}),
{ status: 400, headers: { "Content-Type": "application/json" } }
);
}
await setCacheToRedis(body.url, body.data);
return NextResponse.json({ status: 200, success: true });
} catch (error) {
if (error instanceof AuthError) {
return new NextResponse(
JSON.stringify({ status: 401, error: error.message }),
{ status: 401, headers: { "Content-Type": "application/json" } }
);
}
return new NextResponse(
JSON.stringify({ status: 500, error: "Internal server error" }),
{ status: 500, headers: { "Content-Type": "application/json" } }
);
}
}
export async function DELETE(request: Request) {
try {
// Get the URL from query parameters
const { searchParams } = new URL(request.url);
const url = searchParams.get("url");
if (!url) {
return new NextResponse(
JSON.stringify({ status: 400, error: "URL parameter is required" }),
{ status: 400, headers: { "Content-Type": "application/json" } }
);
}
await clearCacheFromRedis(url);
return NextResponse.json({ status: 200, success: true });
} catch (error) {
return new NextResponse(
JSON.stringify({ status: 500, error: "Internal server error" }),
{ status: 500, headers: { "Content-Type": "application/json" } }
);
}
}

View File

@ -0,0 +1,41 @@
import { NextResponse } from "next/server";
import { globalData, ensureDataInitialized, TableItem } from "../data";
export async function POST(request: Request) {
try {
// Parse the request body
const body = await request.json();
// Ensure we have the required fields
if (!body.name || !body.email) {
return NextResponse.json(
{ status: 400, message: "Name and email are required" },
{ status: 400 }
);
}
// Create a new item with an ID
const newItem: TableItem = {
id: `id-${Date.now()}-${globalData.length}`,
...body
};
// Add to global data
ensureDataInitialized();
globalData.unshift(newItem); // Add to beginning of array
console.log(`POST /api/test/create - Added new item with id: ${newItem.id}`);
return NextResponse.json({
status: 201,
data: newItem,
message: "Item added successfully"
}, { status: 201 });
} catch (error) {
console.error('Error in test/create POST API:', error);
return NextResponse.json(
{ status: 500, message: "Error adding data" },
{ status: 500 }
);
}
}

View File

@ -0,0 +1,45 @@
// Define the data type
export type TableItem = {
id?: string;
name: string;
email: string;
description: string;
category: string;
priority: string;
notifications: boolean;
terms: boolean;
attachments: string;
};
// Global data store
export let globalData: TableItem[] = [];
// Initialize with mock data if empty
export function ensureDataInitialized() {
if (globalData.length === 0) {
globalData = generateMockData(20);
}
return globalData;
}
// Generate mock data for testing
export function generateMockData(count = 20) {
const categories = ["general", "billing", "technical", "other"];
const priorities = ["low", "medium", "high"];
const attachmentTypes = ["document", "image", "video", ""];
return Array.from({ length: count }, (_, i) => ({
id: `id-${Date.now()}-${i}`,
name: `User ${i + 1}`,
email: `user${i + 1}@example.com`,
description: `This is a sample description for item ${
i + 1
}. It contains enough text to demonstrate how descriptions look in the table.`,
category: categories[Math.floor(Math.random() * categories.length)],
priority: priorities[Math.floor(Math.random() * priorities.length)],
notifications: Math.random() > 0.5,
terms: Math.random() > 0.3, // Most users accept terms
attachments:
attachmentTypes[Math.floor(Math.random() * attachmentTypes.length)],
}));
}

View File

@ -0,0 +1,109 @@
import { NextResponse } from "next/server";
import { ensureDataInitialized } from "../data";
// Helper function to handle pagination logic
function getPaginatedData(page: number, size: number) {
// Get data from global store
const allData = ensureDataInitialized();
// Calculate pagination
const startIndex = (page - 1) * size;
const endIndex = startIndex + size;
const paginatedData = allData.slice(startIndex, endIndex);
return {
paginatedData,
allData,
meta: {
page,
size,
totalCount: allData.length,
totalPages: Math.ceil(allData.length / size),
pageCount: paginatedData.length,
},
};
}
// GET endpoint for listing data with pagination
export async function GET(request: Request) {
try {
// Get query parameters for pagination
const url = new URL(request.url);
const page = parseInt(url.searchParams.get("page") || "1");
const size = parseInt(url.searchParams.get("size") || "10");
const { paginatedData, meta } = getPaginatedData(page, size);
console.log(
`GET /api/test/list - Returning ${paginatedData.length} items (page ${page}/${meta.totalPages})`
);
// Return paginated response with the structure that useTableData expects
return NextResponse.json({
success: true,
data: {
data: paginatedData,
pagination: {
size: meta.size,
page: meta.page,
allCount: meta.totalCount,
totalCount: meta.totalCount,
totalPages: meta.totalPages,
pageCount: meta.pageCount,
orderField: [],
orderType: [],
next: meta.page < meta.totalPages,
back: meta.page > 1,
},
},
});
} catch (error) {
console.error("Error in test/list GET API:", error);
return NextResponse.json(
{ success: false, message: "Error fetching data" },
{ status: 500 }
);
}
}
// POST endpoint for listing data with pagination (for useTableData compatibility)
export async function POST(request: Request) {
try {
// Parse the request body for pagination parameters
const body = await request.json();
const page = parseInt(body.page?.toString() || "1");
const size = parseInt(body.size?.toString() || "10");
const { paginatedData, meta } = getPaginatedData(page, size);
console.log(
`POST /api/test/list - Returning ${paginatedData.length} items (page ${page}/${meta.totalPages})`
);
// Return paginated response with the structure that useTableData expects
return NextResponse.json({
success: true,
data: {
data: paginatedData,
pagination: {
size: meta.size,
page: meta.page,
allCount: meta.totalCount,
totalCount: meta.totalCount,
totalPages: meta.totalPages,
pageCount: meta.pageCount,
orderField: [],
orderType: [],
next: meta.page < meta.totalPages,
back: meta.page > 1,
},
},
});
} catch (error) {
console.error("Error in test/list POST API:", error);
return NextResponse.json(
{ success: false, message: "Error fetching data" },
{ status: 500 }
);
}
}

View File

@ -0,0 +1,50 @@
import { NextResponse } from "next/server";
import { globalData, ensureDataInitialized } from "../data";
export async function PATCH(request: Request) {
try {
// Parse the request body
const body = await request.json();
// Ensure we have an ID to update
if (!body.id) {
return NextResponse.json(
{ status: 400, message: "ID is required for updates" },
{ status: 400 }
);
}
// Find the item to update
ensureDataInitialized();
const itemIndex = globalData.findIndex(item => item.id === body.id);
if (itemIndex === -1) {
return NextResponse.json(
{ status: 404, message: "Item not found" },
{ status: 404 }
);
}
// Update the item
const updatedItem = {
...globalData[itemIndex],
...body
};
globalData[itemIndex] = updatedItem;
console.log(`PATCH /api/test/update - Updated item with id: ${updatedItem.id}`);
return NextResponse.json({
status: 200,
data: updatedItem,
message: "Item updated successfully"
});
} catch (error) {
console.error('Error in test/update PATCH API:', error);
return NextResponse.json(
{ status: 500, message: "Error updating data" },
{ status: 500 }
);
}
}

View File

@ -15,6 +15,12 @@ const PageToBeChildrendMulti: React.FC<ContentProps> = ({
updateOnline,
refreshUser,
updateUser,
cacheData,
cacheLoading,
cacheError,
refreshCache,
updateCache,
clearCache
}) => {
const pageComponents = pageIndexMulti[activePageUrl];
if (!pageComponents) { return <ContentToRenderNoPage lang={onlineData.lang} /> }
@ -25,6 +31,7 @@ const PageToBeChildrendMulti: React.FC<ContentProps> = ({
activePageUrl={activePageUrl} searchParams={searchParams}
userData={userData} userLoading={userLoading} userError={userError} refreshUser={refreshUser} updateUser={updateUser}
onlineData={onlineData} onlineLoading={onlineLoading} onlineError={onlineError} refreshOnline={refreshOnline} updateOnline={updateOnline}
cacheData={cacheData} cacheLoading={cacheLoading} cacheError={cacheError} refreshCache={refreshCache} updateCache={updateCache} clearCache={clearCache}
/>;
}

View File

@ -33,6 +33,9 @@ import {
TableDataItem,
} from "@/validations/mutual/table/validations";
import LoadingContent from "@/components/mutual/loader/component";
import { Pencil } from "lucide-react";
import { useRouter } from "next/navigation";
import { setCacheData } from "@/components/mutual/context/cache/context";
interface DataTableProps {
table: ReturnType<typeof useReactTable>;
@ -42,6 +45,7 @@ interface DataTableProps {
getSortingIcon: (columnId: string) => React.ReactNode;
flexRender: typeof flexRender;
t: Translations;
handleEditRow?: (row: TableDataItem) => void;
}
interface TableFormProps {
@ -105,7 +109,8 @@ const DataTable: React.FC<DataTableProps> = React.memo(({
handleSortingChange,
getSortingIcon,
flexRender,
t
t,
handleEditRow
}) => {
return (
<div className="overflow-x-auto relative">
@ -120,7 +125,9 @@ const DataTable: React.FC<DataTableProps> = React.memo(({
<thead className="bg-gray-50">
{table.getHeaderGroups().map((headerGroup) => (
<tr key={headerGroup.id}>
{headerGroup.headers.map((header) => (
{headerGroup.headers.map((header) => {
console.log('Rendering header:', header.id);
return (
<th
key={header.id}
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer"
@ -133,7 +140,8 @@ const DataTable: React.FC<DataTableProps> = React.memo(({
<span>{getSortingIcon(header.column.id)}</span>
</div>
</th>
))}
);
})}
</tr>
))}
</thead>
@ -147,11 +155,14 @@ const DataTable: React.FC<DataTableProps> = React.memo(({
) : (
table.getRowModel().rows.map((row) => (
<tr key={row.id} className="hover:bg-gray-50">
{row.getVisibleCells().map((cell) => (
{row.getVisibleCells().map((cell) => {
console.log('Rendering cell:', cell.column.id);
return (
<td key={cell.id} className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</td>
))}
);
})}
</tr>
))
)}
@ -196,11 +207,17 @@ const TableForm: React.FC<TableFormProps> = ({
</SelectTrigger>
</FormControl>
<SelectContent>
{renderPageOptions().map((option: { key: string | number; value: string; label: string }) => (
{renderPageOptions().length > 0 ? (
renderPageOptions().map((option: { key: string | number; value: string; label: string }) => (
<SelectItem key={option.key} value={option.value}>
{option.label}
</SelectItem>
))}
))
) : (
<SelectItem key="1" value="1">
1
</SelectItem>
)}
</SelectContent>
</Select>
<FormMessage />
@ -328,6 +345,7 @@ const TableCardComponentImproved: React.FC<DashboardPageProps> = React.memo((pro
// Initialize translation with English as default
const language = props.onlineData?.lang as LanguageTypes || 'en';
const t = translations[language];
const router = useRouter();
const {
form,
@ -351,30 +369,90 @@ const TableCardComponentImproved: React.FC<DashboardPageProps> = React.memo((pro
isNextDisabled,
pageSizeOptions,
renderPageOptions,
} = useTableData({ apiUrl: `${API_BASE_URL}/test` });
} = useTableData({ apiUrl: `${API_BASE_URL}/test/list` });
const activePageUrl = props.activePageUrl || '';
console.log("activePageUrl", activePageUrl);
// Function to handle editing a row
const handleEditRow = async (row: TableDataItem) => {
try {
// Store the row data in the cache
await setCacheData(`${activePageUrl}/update`, row);
console.log('Row data stored in cache:', row);
// Navigate to the update form
router.push(`/panel/${activePageUrl}/update`);
} catch (error) {
console.error('Error storing row data in cache:', error);
}
};
const columnHelper = createColumnHelper<TableDataItem>();
// Make sure columns are properly defined with the actions column
const columns = React.useMemo(() => [
columnHelper.accessor('uu_id', {
cell: info => info.getValue(),
header: () => <span>UUID</span>,
footer: info => info.column.id
columnHelper.display({
id: 'actions',
header: () => <span>Actions</span>,
cell: (info) => {
console.log('Rendering action cell for row:', info.row.id);
return (
<Button
variant="ghost"
size="icon"
onClick={(e) => {
e.preventDefault();
console.log('Edit button clicked');
handleEditRow && handleEditRow(info.row.original);
}}
className="h-8 w-8 p-0"
>
<Pencil className="h-4 w-4" />
<span className="sr-only">Edit</span>
</Button>
);
}
}),
columnHelper.accessor('process_name', {
columnHelper.accessor('name', {
cell: info => info.getValue(),
header: () => <span>Name</span>,
footer: info => info.column.id
}),
columnHelper.accessor('bank_date', {
header: () => <span>Bank Date</span>,
columnHelper.accessor('email', {
cell: info => info.getValue(),
header: () => <span>Email</span>,
footer: info => info.column.id
}),
columnHelper.accessor('description', {
header: () => <span>Description</span>,
cell: info => info.getValue(),
footer: info => info.column.id
}),
columnHelper.accessor('currency_value', {
header: () => <span>Currency Value</span>,
columnHelper.accessor('category', {
header: () => <span>Category</span>,
cell: info => String(info.getValue()),
footer: info => info.column.id
}),
columnHelper.accessor('priority', {
header: () => <span>Priority</span>,
cell: info => String(info.getValue()),
footer: info => info.column.id
}),
columnHelper.accessor('notifications', {
header: () => <span>Notifications</span>,
cell: info => info.getValue() ? 'Yes' : 'No',
footer: info => info.column.id
}),
columnHelper.accessor('terms', {
header: () => <span>Terms Accepted</span>,
cell: info => info.getValue() ? 'Yes' : 'No',
footer: info => info.column.id
}),
columnHelper.accessor('attachments', {
header: () => <span>Attachments</span>,
cell: info => String(info.getValue() || '-'),
footer: info => info.column.id
}),
], [columnHelper]) as ColumnDef<TableDataItem>[];
const table = useReactTable({
@ -387,8 +465,12 @@ const TableCardComponentImproved: React.FC<DashboardPageProps> = React.memo((pro
getPaginationRowModel: getPaginationRowModel(),
manualPagination: true,
pageCount: apiPagination.totalPages || 1,
debugTable: true,
});
// Check if actions column is included
const actionColumnExists = table.getVisibleLeafColumns().some(col => col.id === 'actions');
return (
isLoading ? <LoadingContent height="h-48" size="w-36 h-36" plane="h-full w-full" /> :
<div className="bg-white rounded-lg shadow-md overflow-hidden mb-20">
@ -401,6 +483,13 @@ const TableCardComponentImproved: React.FC<DashboardPageProps> = React.memo((pro
error={error}
t={t}
/>
<Button
onClick={() => router.push(`/panel/${activePageUrl}/create`)}
className="bg-primary text-white hover:bg-primary/90"
>
<span className="mr-2">Create New</span>
<span className="sr-only">Create new item</span>
</Button>
</div>
<TableForm
@ -435,6 +524,7 @@ const TableCardComponentImproved: React.FC<DashboardPageProps> = React.memo((pro
getSortingIcon={getSortingIcon}
flexRender={flexRender}
t={t}
handleEditRow={handleEditRow}
/>
</div>

View File

@ -40,6 +40,7 @@ const FallbackContent: FC<{ lang: LanguageTypes; activePageUrl: string }> = memo
const ContentComponent: FC<ContentProps> = ({
searchParams, activePageUrl, mode, userData, userLoading, userError, refreshUser, updateUser,
onlineData, onlineLoading, onlineError, refreshOnline, updateOnline,
cacheData, cacheLoading, cacheError, refreshCache, updateCache, clearCache
}) => {
const loadingContent = <LoadingContent height="h-16" size="w-36 h-48" plane="h-full w-full" />;
const classNameDiv = "fixed top-24 left-80 right-0 py-10 px-15 border-emerald-150 border-l-2 overflow-y-auto h-[calc(100vh-64px)]";
@ -62,7 +63,8 @@ const ContentComponent: FC<ContentProps> = ({
<MemoizedMultiPage
activePageUrl={activePageUrl || ''} searchParams={searchParams}
userData={userData} userLoading={userLoading} userError={userError} refreshUser={refreshUser} updateUser={updateUser}
onlineData={onlineData} onlineLoading={onlineLoading} onlineError={onlineError} refreshOnline={refreshOnline} updateOnline={updateOnline} />
onlineData={onlineData} onlineLoading={onlineLoading} onlineError={onlineError} refreshOnline={refreshOnline} updateOnline={updateOnline}
cacheData={cacheData} cacheLoading={cacheLoading} cacheError={cacheError} refreshCache={refreshCache} updateCache={updateCache} clearCache={clearCache} />
</div>
</Suspense>
</div>

View File

@ -0,0 +1,403 @@
import React, { useState, useEffect } from "react";
import { apiPostFetcher } from "@/lib/fetcher";
import { withCache } from "@/components/mutual/context/cache/withCache";
import { Input } from "@/components/mutual/ui/input";
import { Label } from "@/components/mutual/ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/mutual/ui/select";
import { Textarea } from "@/components/mutual/ui/textarea";
import { Checkbox } from "@/components/mutual/ui/checkbox";
import { RadioGroup, RadioGroupItem } from "@/components/mutual/ui/radio-group";
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/mutual/ui/dropdown-menu";
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/mutual/ui/form";
import { Button } from "@/components/mutual/ui/button";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import * as z from "zod";
import { getCacheData, setCacheData, clearCacheData } from "@/components/mutual/context/cache/context";
import { useRouter } from "next/navigation";
// Define form schema with zod
const zodSchema = z.object({
name: z.string().min(2, { message: "Name must be at least 2 characters." }),
email: z.string().email({ message: "Please enter a valid email address." }),
description: z.string().min(10, { message: "Description must be at least 10 characters." }),
category: z.string({ required_error: "Please select a category." }),
priority: z.string({ required_error: "Please select a priority level." }),
notifications: z.boolean(),
terms: z.boolean().refine(val => val === true, { message: "You must accept the terms." }),
attachments: z.string().optional()
});
// Define the form type
type FormValues = z.infer<typeof zodSchema>;
function CreateFromComponentBase({
cacheData,
cacheLoading,
cacheError,
refreshCache,
updateCache,
clearCache,
activePageUrl
}: {
cacheData?: { [url: string]: any } | null;
cacheLoading?: boolean;
cacheError?: string | null;
refreshCache?: (url?: string) => Promise<void>;
updateCache?: (url: string, data: any) => Promise<void>;
clearCache?: (url: string) => Promise<void>;
activePageUrl?: string;
}) {
const [open, setOpen] = useState<boolean>(false);
const [cacheLoaded, setCacheLoaded] = useState<boolean>(false);
const router = useRouter();
const listUrl = `/panel/${activePageUrl?.replace("/create", "")}`;
const form = useForm<FormValues>({
resolver: zodResolver(zodSchema),
defaultValues: {
name: "",
email: "",
description: "",
category: "",
priority: "",
notifications: false,
terms: false,
attachments: ""
}
});
// Load cached form values only once when component mounts
useEffect(() => {
const fetchCacheDirectly = async () => {
if (activePageUrl && !cacheLoaded) {
try {
console.log("Directly fetching cache for URL:", activePageUrl);
// Directly fetch cache data using the imported function
const cachedData = await getCacheData(activePageUrl);
console.log("Directly fetched cache data:", cachedData);
if (cachedData) {
const formValues = form.getValues();
// Create a merged data object with proper typing
const mergedData: FormValues = {
name: cachedData.name || formValues.name || "",
email: cachedData.email || formValues.email || "",
description: cachedData.description || formValues.description || "",
category: cachedData.category || formValues.category || "",
priority: cachedData.priority || formValues.priority || "",
notifications: cachedData.notifications ?? formValues.notifications ?? false,
terms: cachedData.terms ?? formValues.terms ?? false,
attachments: cachedData.attachments || formValues.attachments || ""
};
console.log("Setting form with direct cache data:", mergedData);
form.reset(mergedData);
}
setCacheLoaded(true);
// Also call the context refresh if available (for state consistency)
if (refreshCache) {
refreshCache(activePageUrl);
}
} catch (error) {
console.error("Error fetching cache directly:", error);
}
}
};
fetchCacheDirectly();
}, []); // Empty dependency array since we only want to run once on mount
// Function to handle input blur events
const handleFieldBlur = async (fieldName: keyof FormValues, value: any) => {
// Only update if the value is not empty
if (value && activePageUrl) {
try {
// Get current form values
const currentValues = form.getValues();
// Prepare updated data
const updatedData = {
...currentValues,
[fieldName]: value
};
console.log("Directly updating cache with:", updatedData);
// Directly update cache using imported function
await setCacheData(activePageUrl, updatedData);
// Also use the context method if available (for state consistency)
if (updateCache) {
updateCache(activePageUrl, updatedData);
}
} catch (error) {
console.error("Error updating cache:", error);
}
}
};
// Type-safe submit handler
const onSubmit = async (data: FormValues) => {
console.log("Form submitted with data:", data);
try {
// Submit form data to API
const response = await apiPostFetcher<any>({ url: "/tst/create", isNoCache: true, body: data });
console.log("API response:", response);
// Clear cache on successful submission
if (activePageUrl) {
try {
console.log("Directly clearing cache for URL:", activePageUrl);
// Directly clear cache using imported function
await clearCacheData(activePageUrl);
// Also use the context method if available (for state consistency)
if (clearCache) {
clearCache(activePageUrl);
}
} catch (error) {
console.error("Error clearing cache:", error);
}
}
// Reset form with empty values
const emptyValues: FormValues = {
name: "",
email: "",
description: "",
category: "",
priority: "",
notifications: false,
terms: false,
attachments: ""
};
form.reset(emptyValues);
} catch (error) {
console.error("Error submitting form:", error);
}
}
return (
<div className="w-full max-w-2xl mx-auto p-6 bg-white rounded-lg shadow-md">
{/* back to list button */}
<div className="flex justify-end">
<Button onClick={() => router.push(listUrl)}>Back to List</Button>
</div>
<h2 className="text-2xl font-bold mb-6">Example Form</h2>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
{/* Input example */}
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input
placeholder="Enter your name"
{...field}
onBlur={(e) => { field.onBlur(); handleFieldBlur("name", e.target.value) }}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{/* Email input example */}
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input
type="email"
placeholder="your@email.com"
{...field}
onBlur={(e) => { field.onBlur(); handleFieldBlur("email", e.target.value) }}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{/* Textarea example */}
<FormField
control={form.control}
name="description"
render={({ field }) => (
<FormItem>
<FormLabel>Description</FormLabel>
<FormControl>
<Textarea
placeholder="Enter a detailed description"
className="min-h-[100px]"
{...field}
onBlur={(e) => { field.onBlur(); handleFieldBlur("description", e.target.value) }}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{/* Select example */}
<FormField
control={form.control}
name="category"
render={({ field }) => (
<FormItem>
<FormLabel>Category</FormLabel>
<FormControl>
<Select
onValueChange={(value) => { field.onChange(value); handleFieldBlur("category", value) }}
value={field.value}
>
<SelectTrigger>
<SelectValue placeholder="Select a category" />
</SelectTrigger>
<SelectContent>
<SelectItem value="general">General</SelectItem>
<SelectItem value="billing">Billing</SelectItem>
<SelectItem value="technical">Technical</SelectItem>
<SelectItem value="other">Other</SelectItem>
</SelectContent>
</Select>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{/* RadioGroup example */}
<FormField
control={form.control}
name="priority"
render={({ field }) => (
<FormItem className="space-y-3">
<FormLabel>Priority Level</FormLabel>
<FormControl>
<RadioGroup
onValueChange={(value) => { field.onChange(value); handleFieldBlur("priority", value) }}
value={field.value}
className="flex flex-col space-y-1"
>
<div className="flex items-center space-x-2">
<RadioGroupItem value="low" id="priority-low" />
<Label htmlFor="priority-low">Low</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="medium" id="priority-medium" />
<Label htmlFor="priority-medium">Medium</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="high" id="priority-high" />
<Label htmlFor="priority-high">High</Label>
</div>
</RadioGroup>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{/* Checkbox example */}
<FormField
control={form.control}
name="notifications"
render={({ field }) => (
<FormItem className="flex flex-row items-start space-x-3 space-y-0 rounded-md p-4 border">
<FormControl>
<Checkbox
checked={field.value}
onCheckedChange={(checked) => { field.onChange(checked); handleFieldBlur("notifications", checked) }}
/>
</FormControl>
<div className="space-y-1 leading-none">
<FormLabel>Email notifications</FormLabel>
<p className="text-sm text-gray-500">Receive email notifications when updates occur</p>
</div>
<FormMessage />
</FormItem>
)}
/>
{/* Terms checkbox example */}
<FormField
control={form.control}
name="terms"
render={({ field }) => (
<FormItem className="flex flex-row items-start space-x-3 space-y-0">
<FormControl>
<Checkbox
checked={field.value}
onCheckedChange={(checked) => { field.onChange(checked); handleFieldBlur("terms", checked) }}
/>
</FormControl>
<div className="space-y-1 leading-none">
<FormLabel>I accept the terms and conditions</FormLabel>
</div>
<FormMessage />
</FormItem>
)}
/>
{/* DropdownMenu example */}
<FormField
control={form.control}
name="attachments"
render={({ field }) => (
<FormItem>
<FormLabel>Attachments</FormLabel>
<FormControl>
<div>
<DropdownMenu open={open} onOpenChange={(isOpen: boolean) => setOpen(isOpen)}>
<DropdownMenuTrigger asChild>
<Button variant="outline" className="w-full justify-start text-left font-normal">
<span>{field.value || "Select attachment type"}</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56">
<DropdownMenuItem onClick={() => { field.onChange("document"); handleFieldBlur("attachments", "document"); setOpen(false) }}>
Document
</DropdownMenuItem>
<DropdownMenuItem onClick={() => { field.onChange("image"); handleFieldBlur("attachments", "image"); setOpen(false) }}>
Image
</DropdownMenuItem>
<DropdownMenuItem onClick={() => { field.onChange("video"); handleFieldBlur("attachments", "video"); setOpen(false) }}>
Video
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit" className="w-full">Submit</Button>
</form>
</Form>
</div>
);
}
export const CreateFromComponent = withCache(CreateFromComponentBase);
// Add default export to maintain compatibility with existing imports
export default withCache(CreateFromComponentBase);

View File

@ -0,0 +1,425 @@
import React, { useState, useEffect } from "react";
import { apiPatchFetcher } from "@/lib/fetcher";
import { withCache } from "@/components/mutual/context/cache/withCache";
import { Input } from "@/components/mutual/ui/input";
import { Label } from "@/components/mutual/ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/mutual/ui/select";
import { Textarea } from "@/components/mutual/ui/textarea";
import { Checkbox } from "@/components/mutual/ui/checkbox";
import { RadioGroup, RadioGroupItem } from "@/components/mutual/ui/radio-group";
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/mutual/ui/dropdown-menu";
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/mutual/ui/form";
import { Button } from "@/components/mutual/ui/button";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import * as z from "zod";
import { getCacheData, setCacheData, clearCacheData } from "@/components/mutual/context/cache/context";
import { useRouter } from "next/navigation";
// Define form schema with zod
const zodSchema = z.object({
name: z.string().min(2, { message: "Name must be at least 2 characters." }),
email: z.string().email({ message: "Please enter a valid email address." }),
description: z.string().min(10, { message: "Description must be at least 10 characters." }),
category: z.string({ required_error: "Please select a category." }),
priority: z.string({ required_error: "Please select a priority level." }),
notifications: z.boolean(),
terms: z.boolean().refine(val => val === true, { message: "You must accept the terms." }),
attachments: z.string().optional()
});
// Define the form type
type FormValues = z.infer<typeof zodSchema>;
function UpdateFromComponentBase({
cacheData,
cacheLoading,
cacheError,
refreshCache,
updateCache,
clearCache,
activePageUrl
}: {
cacheData?: { [url: string]: any } | null;
cacheLoading?: boolean;
cacheError?: string | null;
refreshCache?: (url?: string) => Promise<void>;
updateCache?: (url: string, data: any) => Promise<void>;
clearCache?: (url: string) => Promise<void>;
activePageUrl?: string;
}) {
const [open, setOpen] = useState<boolean>(false);
const [cacheLoaded, setCacheLoaded] = useState<boolean>(false);
const router = useRouter();
const listUrl = `/panel/${activePageUrl?.replace("/update", "")}`;
const form = useForm<FormValues>({
resolver: zodResolver(zodSchema),
defaultValues: {
name: "",
email: "",
description: "",
category: "",
priority: "",
notifications: false,
terms: false,
attachments: ""
}
});
// Load cached form values only once when component mounts
useEffect(() => {
const fetchCacheDirectly = async () => {
if (activePageUrl && !cacheLoaded) {
try {
console.log("Directly fetching cache for URL:", activePageUrl);
// Directly fetch cache data using the imported function
const cachedData = await getCacheData(activePageUrl);
console.log("Directly fetched cache data:", cachedData);
if (cachedData) {
const formValues = form.getValues();
// Create a merged data object with proper typing
// Convert boolean values explicitly to handle potential string representations
const notificationsValue = typeof cachedData.notifications === 'string'
? cachedData.notifications === 'true'
: Boolean(cachedData.notifications);
const termsValue = typeof cachedData.terms === 'string'
? cachedData.terms === 'true'
: Boolean(cachedData.terms);
console.log("Raw notification value from cache:", cachedData.notifications, "type:", typeof cachedData.notifications);
console.log("Raw terms value from cache:", cachedData.terms, "type:", typeof cachedData.terms);
console.log("Converted notification value:", notificationsValue);
console.log("Converted terms value:", termsValue);
const mergedData: FormValues = {
name: cachedData.name || formValues.name || "",
email: cachedData.email || formValues.email || "",
description: cachedData.description || formValues.description || "",
category: cachedData.category || formValues.category || "",
priority: cachedData.priority || formValues.priority || "",
notifications: notificationsValue,
terms: termsValue,
attachments: cachedData.attachments || formValues.attachments || ""
};
console.log("Setting form with direct cache data:", mergedData);
form.reset(mergedData);
}
setCacheLoaded(true);
// Also call the context refresh if available (for state consistency)
if (refreshCache) {
refreshCache(activePageUrl);
}
} catch (error) {
console.error("Error fetching cache directly:", error);
}
}
};
fetchCacheDirectly();
}, []); // Empty dependency array since we only want to run once on mount
// Function to handle input blur events
const handleFieldBlur = async (fieldName: keyof FormValues, value: any) => {
// Only update if the value is not empty
if (value && activePageUrl) {
try {
// Get current form values
const currentValues = form.getValues();
// Prepare updated data
const updatedData = {
...currentValues,
[fieldName]: value
};
console.log("Directly updating cache with:", updatedData);
// Directly update cache using imported function
await setCacheData(activePageUrl, updatedData);
// Also use the context method if available (for state consistency)
if (updateCache) {
updateCache(activePageUrl, updatedData);
}
} catch (error) {
console.error("Error updating cache:", error);
}
}
};
// Type-safe submit handler
const onSubmit = async (data: FormValues) => {
console.log("Form submitted with data:", data);
try {
// Submit form data to API using PATCH for updates
const response = await apiPatchFetcher<any>({ url: "/api/test/update", isNoCache: true, body: data });
console.log("API response:", response);
// Clear cache on successful submission
if (activePageUrl) {
try {
console.log("Directly clearing cache for URL:", activePageUrl);
// Directly clear cache using imported function
await clearCacheData(activePageUrl);
// Also use the context method if available (for state consistency)
if (clearCache) {
clearCache(activePageUrl);
}
} catch (error) {
console.error("Error clearing cache:", error);
}
}
// Reset form with empty values
const emptyValues: FormValues = {
name: "",
email: "",
description: "",
category: "",
priority: "",
notifications: false,
terms: false,
attachments: ""
};
form.reset(emptyValues);
} catch (error) {
console.error("Error submitting form:", error);
}
}
return (
<div className="w-full max-w-2xl mx-auto p-6 bg-white rounded-lg shadow-md">
{/* back to list button */}
<div className="flex justify-end">
<Button onClick={() => router.push(listUrl)}>Back to List</Button>
</div>
<h2 className="text-2xl font-bold mb-6">Update Form</h2>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
{/* Input example */}
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input
placeholder="Enter your name"
{...field}
onBlur={(e) => { field.onBlur(); handleFieldBlur("name", e.target.value) }}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{/* Email input example */}
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input
type="email"
placeholder="your@email.com"
{...field}
onBlur={(e) => { field.onBlur(); handleFieldBlur("email", e.target.value) }}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{/* Textarea example */}
<FormField
control={form.control}
name="description"
render={({ field }) => (
<FormItem>
<FormLabel>Description</FormLabel>
<FormControl>
<Textarea
placeholder="Enter a detailed description"
className="min-h-[100px]"
{...field}
onBlur={(e) => { field.onBlur(); handleFieldBlur("description", e.target.value) }}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{/* Select example */}
<FormField
control={form.control}
name="category"
render={({ field }) => (
<FormItem>
<FormLabel>Category</FormLabel>
<FormControl>
<Select
onValueChange={(value) => { field.onChange(value); handleFieldBlur("category", value) }}
value={field.value}
>
<SelectTrigger>
<SelectValue placeholder="Select a category" />
</SelectTrigger>
<SelectContent>
<SelectItem value="general">General</SelectItem>
<SelectItem value="billing">Billing</SelectItem>
<SelectItem value="technical">Technical</SelectItem>
<SelectItem value="other">Other</SelectItem>
</SelectContent>
</Select>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{/* RadioGroup example */}
<FormField
control={form.control}
name="priority"
render={({ field }) => (
<FormItem className="space-y-3">
<FormLabel>Priority Level</FormLabel>
<FormControl>
<RadioGroup
onValueChange={(value) => { field.onChange(value); handleFieldBlur("priority", value) }}
value={field.value}
className="flex flex-col space-y-1"
>
<div className="flex items-center space-x-2">
<RadioGroupItem value="low" id="priority-low" />
<Label htmlFor="priority-low">Low</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="medium" id="priority-medium" />
<Label htmlFor="priority-medium">Medium</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="high" id="priority-high" />
<Label htmlFor="priority-high">High</Label>
</div>
</RadioGroup>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{/* Checkbox example */}
<FormField
control={form.control}
name="notifications"
render={({ field }) => (
<FormItem className="flex flex-row items-start space-x-3 space-y-0 rounded-md p-4 border">
<FormControl>
<Checkbox
checked={Boolean(field.value)}
onCheckedChange={(checked) => {
console.log("Notification checkbox changed to:", checked);
field.onChange(checked);
handleFieldBlur("notifications", checked);
}}
/>
</FormControl>
<div className="space-y-1 leading-none">
<FormLabel>Email notifications</FormLabel>
<p className="text-sm text-gray-500">Receive email notifications when updates occur</p>
</div>
<FormMessage />
</FormItem>
)}
/>
{/* Terms checkbox example */}
<FormField
control={form.control}
name="terms"
render={({ field }) => (
<FormItem className="flex flex-row items-start space-x-3 space-y-0">
<FormControl>
<Checkbox
checked={Boolean(field.value)}
onCheckedChange={(checked) => {
console.log("Terms checkbox changed to:", checked);
field.onChange(checked);
handleFieldBlur("terms", checked);
}}
/>
</FormControl>
<div className="space-y-1 leading-none">
<FormLabel>I accept the terms and conditions</FormLabel>
</div>
<FormMessage />
</FormItem>
)}
/>
{/* DropdownMenu example */}
<FormField
control={form.control}
name="attachments"
render={({ field }) => (
<FormItem>
<FormLabel>Attachments</FormLabel>
<FormControl>
<div>
<DropdownMenu open={open} onOpenChange={(isOpen: boolean) => setOpen(isOpen)}>
<DropdownMenuTrigger asChild>
<Button variant="outline" className="w-full justify-start text-left font-normal">
<span>{field.value || "Select attachment type"}</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56">
<DropdownMenuItem onClick={() => { field.onChange("document"); handleFieldBlur("attachments", "document"); setOpen(false) }}>
Document
</DropdownMenuItem>
<DropdownMenuItem onClick={() => { field.onChange("image"); handleFieldBlur("attachments", "image"); setOpen(false) }}>
Image
</DropdownMenuItem>
<DropdownMenuItem onClick={() => { field.onChange("video"); handleFieldBlur("attachments", "video"); setOpen(false) }}>
Video
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit" className="w-full">Update</Button>
</form>
</Form>
</div>
);
}
export const UpdateFromComponent = withCache(UpdateFromComponentBase);
// Add default export to maintain compatibility with existing imports
export default withCache(UpdateFromComponentBase);

View File

@ -0,0 +1,211 @@
"use client";
import React, { ReactNode } from 'react';
import { createContext, useContext } from 'react';
import { createContextHook } from "../hookFactory";
// Define the shape of the cache data
type CacheData = {
[url: string]: any;
};
// Original fetch functions for backward compatibility
async function getCacheData(url: string): Promise<any | null> {
try {
const response = await fetch(`/api/context/cache?url=${encodeURIComponent(url)}`, {
method: "GET",
headers: { "Content-Type": "application/json" },
});
if (!response.ok) {
return null;
}
const data = await response.json();
if (data.status === 200) return data.data;
return null;
} catch (error) {
console.error("Error fetching cache data:", error);
return null;
}
}
async function setCacheData(url: string, data: any): Promise<boolean> {
try {
const response = await fetch(`/api/context/cache`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ url, data }),
});
if (!response.ok) {
return false;
}
const result = await response.json();
return result.status === 200;
} catch (error) {
console.error("Error setting cache data:", error);
return false;
}
}
async function clearCacheData(url: string): Promise<boolean> {
try {
const response = await fetch(`/api/context/cache?url=${encodeURIComponent(url)}`, {
method: "DELETE",
headers: { "Content-Type": "application/json" },
});
if (!response.ok) {
return false;
}
const result = await response.json();
return result.status === 200;
} catch (error) {
console.error("Error clearing cache data:", error);
return false;
}
}
// Create the cache hook using the factory
const useContextCache = createContextHook<CacheData>({
endpoint: "/context/cache",
contextName: "cache",
enablePeriodicRefresh: false,
customFetch: async () => {
// For initial load, we don't have a specific URL to fetch
// Return an empty object as the initial state
console.log("Initial cache fetch");
return {};
},
customUpdate: async (newData: CacheData) => {
// This won't be used directly, as we'll provide custom methods
console.log("Cache update with:", newData);
return true;
},
defaultValue: {}
});
// Custom hook for cache data with the expected interface
interface UseCacheResult {
cacheData: CacheData | null;
isLoading: boolean;
error: string | null;
refreshCache: (url?: string) => Promise<void>;
updateCache: (url: string, data: any) => Promise<void>;
clearCache: (url: string) => Promise<void>;
}
// Wrapper hook that adapts the generic hook to the expected interface
export function useCache(): UseCacheResult {
const { data, isLoading, error, refresh, update } = useContextCache();
// Custom refresh function that fetches data for a specific URL
const refreshCache = async (url?: string): Promise<void> => {
if (!url && typeof window !== 'undefined') {
url = window.location.pathname;
}
if (!url) return;
// Normalize URL to handle encoding issues
const normalizedUrl = url.startsWith('/') ? url : `/${url}`;
console.log("Fetching cache for normalized URL:", normalizedUrl);
try {
const urlData = await getCacheData(normalizedUrl);
console.log("Received cache data:", urlData);
// Update the local state with the fetched data
const updatedData = {
...data,
[normalizedUrl]: urlData
};
console.log("Updating cache state with:", updatedData);
await update(updatedData);
// Force a refresh to ensure the component gets the updated data
await refresh();
} catch (error) {
console.error("Error refreshing cache:", error);
}
};
// Custom update function for a specific URL
const updateCache = async (url: string, urlData: any): Promise<void> => {
// Normalize URL to handle encoding issues
const normalizedUrl = url.startsWith('/') ? url : `/${url}`;
console.log("Updating cache for normalized URL:", normalizedUrl);
try {
const success = await setCacheData(normalizedUrl, urlData);
console.log("Cache update success:", success);
if (success && data) {
// Update local state
await update({
...data,
[normalizedUrl]: urlData
});
}
return;
} catch (error) {
console.error("Error updating cache:", error);
return;
}
};
// Custom clear function for a specific URL
const clearCache = async (url: string): Promise<void> => {
// Normalize URL to handle encoding issues
const normalizedUrl = url.startsWith('/') ? url : `/${url}`;
console.log("Clearing cache for normalized URL:", normalizedUrl);
try {
const success = await clearCacheData(normalizedUrl);
console.log("Cache clear success:", success);
if (success && data) {
// Update local state
const newData = { ...data };
delete newData[normalizedUrl];
await update(newData);
}
return;
} catch (error) {
console.error("Error clearing cache:", error);
return;
}
};
return {
cacheData: data,
isLoading,
error,
refreshCache,
updateCache,
clearCache
};
}
// Create a provider component that uses the hook factory
// Create a context for the cache
const CacheContext = createContext<UseCacheResult | undefined>(undefined);
// Provider component
export function CacheProvider({ children }: { children: ReactNode }) {
const cacheHook = useCache();
return (
<CacheContext.Provider value={cacheHook}>
{children}
</CacheContext.Provider>
);
}
// Export the original functions for backward compatibility
export { getCacheData, setCacheData, clearCacheData };

View File

@ -0,0 +1,21 @@
"use client";
import React from 'react';
import { useCache } from './context';
// Higher-order component to provide cache functionality
export function withCache<P extends object>(Component: React.ComponentType<P & { activePageUrl?: string }>) {
return function WithCacheComponent(props: P & { activePageUrl?: string }) {
try {
// Try to use the cache context
const cacheProps = useCache();
// Pass both the original props and the cache props to the wrapped component
return <Component {...props} {...cacheProps} />;
} catch (error) {
// If the cache context is not available, render the component with just its original props
console.warn("Cache context not available, rendering component without cache functionality");
return <Component {...props} />;
}
};
}

View File

@ -1,6 +1,7 @@
'use client';
import React, { ReactNode } from 'react';
import { OnlineProvider } from '@/components/mutual/context/online/provider';
import { CacheProvider } from '@/components/mutual/context/cache/context';
interface ClientProvidersProps {
children: ReactNode;
@ -14,7 +15,9 @@ export function ClientProviders({ children }: ClientProvidersProps) {
return (
<OnlineProvider>
<CacheProvider>
{children}
</CacheProvider>
</OnlineProvider>
);
}

View File

@ -0,0 +1,123 @@
"use server";
import { functionRetrieveUserSelection } from "@/fetchers/fecther";
import { AuthError } from "@/fetchers/types/context";
import { getCompleteFromRedis, setCompleteToRedis } from "@/fetchers/custom/context/complete/fetch";
import { safeRedisGet, safeJsonParse } from "@/utils/redisOperations";
import { REDIS_TIMEOUT } from "@/fetchers/base";
// Import or define the ClientRedisToken type
import type { ClientRedisToken } from "@/fetchers/types/context";
// Define the cache structure
type CacheData = {
[url: string]: {
[key: string]: any;
};
};
// Extend the ClientRedisToken type to include cache
type ExtendedClientRedisToken = ClientRedisToken & {
cache: CacheData;
};
/**
* Get cached form values from Redis for a specific URL
* @param url The URL key to retrieve cached form values for
* @returns The cached form values or null if not found
*/
const getCacheFromRedis = async (url: string): Promise<any> => {
try {
const decrpytUserSelection = await functionRetrieveUserSelection();
if (!decrpytUserSelection) throw new AuthError("No user selection found");
const redisKey = decrpytUserSelection?.redisKey;
if (!redisKey) throw new AuthError("No redis key found");
// Use safe Redis get operation with proper connection handling
const result = await safeRedisGet(`${redisKey}`, REDIS_TIMEOUT);
if (!result) return null;
// Use safe JSON parsing with proper default object type
const parsedResult = safeJsonParse(result, { cache: {} }) as { cache: CacheData };
// Return the cached data for the specific URL or null if not found
return parsedResult.cache && parsedResult.cache[url] ? parsedResult.cache[url] : null;
} catch (error) {
console.error("Error getting cache from Redis:", error);
return null;
}
};
/**
* Set cached form values in Redis for a specific URL
* @param url The URL key to store the form values under
* @param formValues The form values to cache
* @returns True if successful, false otherwise
*/
const setCacheToRedis = async (url: string, formValues: any): Promise<boolean> => {
try {
const decrpytUserSelection = await functionRetrieveUserSelection();
if (!decrpytUserSelection) throw new AuthError("No user selection found");
const redisKey = decrpytUserSelection?.redisKey;
if (!redisKey) throw new AuthError("No redis key found");
// Get the complete data from Redis
const completeData = await getCompleteFromRedis() as ExtendedClientRedisToken;
if (!completeData) throw new AuthError("No complete data found in Redis");
// Initialize cache object if it doesn't exist
if (!completeData.cache) {
completeData.cache = {} as CacheData;
}
// Update the cache with the new form values for the specific URL
completeData.cache[url] = formValues;
// Save the updated data back to Redis
await setCompleteToRedis(completeData);
return true;
} catch (error) {
console.error("Error setting cache to Redis:", error);
if (error instanceof AuthError) {
throw error;
} else {
throw new AuthError(error instanceof Error ? error.message : "Unknown error");
}
}
};
/**
* Clear cached form values in Redis for a specific URL
* @param url The URL key to clear cached form values for
* @returns True if successful, false otherwise
*/
const clearCacheFromRedis = async (url: string): Promise<boolean> => {
try {
const decrpytUserSelection = await functionRetrieveUserSelection();
if (!decrpytUserSelection) throw new AuthError("No user selection found");
const redisKey = decrpytUserSelection?.redisKey;
if (!redisKey) throw new AuthError("No redis key found");
// Get the complete data from Redis
const completeData = await getCompleteFromRedis() as ExtendedClientRedisToken;
if (!completeData) throw new AuthError("No complete data found in Redis");
// If cache exists and has the URL, delete it
if (completeData.cache && completeData.cache[url]) {
delete completeData.cache[url];
// Save the updated data back to Redis
await setCompleteToRedis(completeData as any);
}
return true;
} catch (error) {
console.error("Error clearing cache from Redis:", error);
return false;
}
};
export { getCacheFromRedis, setCacheToRedis, clearCacheFromRedis };

View File

@ -6,6 +6,7 @@ import { useOnline } from "@/components/mutual/context/online/context";
import { useSelection } from "@/components/mutual/context/selection/context";
import { useUser } from "@/components/mutual/context/user/context";
import { useConfig } from "@/components/mutual/context/config/context";
import { useCache } from "@/components/mutual/context/cache/context";
import { ModeTypes } from "@/validations/mutual/dashboard/props";
import HeaderComponent from "@/components/custom/header/component";
@ -22,6 +23,7 @@ const ClientLayout: FC<ClientLayoutProps> = ({ activePageUrl, searchParams }) =>
const { availableApplications, isLoading: menuLoading, error: menuError, menuData, refreshMenu, updateMenu } = useMenu();
const { selectionData, isLoading: selectionLoading, error: selectionError, refreshSelection, updateSelection } = useSelection();
const { configData, isLoading: configLoading, error: configError, refreshConfig, updateConfig } = useConfig();
const { cacheData, isLoading: cacheLoading, error: cacheError, refreshCache, updateCache, clearCache } = useCache();
const prefix = "/panel"
const mode = (searchParams?.mode as ModeTypes) || 'shortList';
@ -38,7 +40,8 @@ const ClientLayout: FC<ClientLayoutProps> = ({ activePageUrl, searchParams }) =>
menuData={menuData} menuLoading={menuLoading} menuError={menuError} refreshMenu={refreshMenu} updateMenu={updateMenu} />
<ContentComponent activePageUrl={activePageUrl} mode={mode} searchParams={searchParams}
userData={userData} userLoading={userLoading} userError={userError} refreshUser={refreshUser} updateUser={updateUser}
onlineData={onlineData} onlineLoading={onlineLoading} onlineError={onlineError} refreshOnline={refreshOnline} updateOnline={updateOnline} />
onlineData={onlineData} onlineLoading={onlineLoading} onlineError={onlineError} refreshOnline={refreshOnline} updateOnline={updateOnline}
cacheData={cacheData} cacheLoading={cacheLoading} cacheError={cacheError} refreshCache={refreshCache} updateCache={updateCache} clearCache={clearCache} />
{/* <FooterComponent activePageUrl={activePageUrl} searchParams={searchParams}
configData={configData} configLoading={configLoading} configError={configError} refreshConfig={refreshConfig} updateConfig={updateConfig}
onlineData={onlineData} onlineLoading={onlineLoading} onlineError={onlineError} refreshOnline={refreshOnline} updateOnline={updateOnline} /> */}

View File

@ -1,12 +1,14 @@
// import { DashboardPage } from "@/components/custom/content/DashboardPage";
import { DPage } from "@/components/custom/content/DPage";
import TableCardComponentImproved from "@/components/custom/content/TableCardComponentImproved";
import CreateFromComponent from "@/components/custom/content/createFromComponent";
import UpdateFromComponent from "@/components/custom/content/updateFromComponent";
const pageIndexMulti: Record<string, Record<string, React.FC<any>>> = {
"/dashboard": { DashboardPage: TableCardComponentImproved },
"/build": { DashboardPage: DPage },
"/build/create": { DashboardPage: DPage },
"/build/update": { DashboardPage: DPage },
"/build": { DashboardPage: TableCardComponentImproved },
"/build/create": { DashboardPage: CreateFromComponent },
"/build/update": { DashboardPage: UpdateFromComponent },
"/people": { DashboardPage: DPage },
"/people/create": { DashboardPage: DPage },
"/people/update": { DashboardPage: DPage },

View File

@ -29,6 +29,12 @@ interface ContentProps {
userError: any;
refreshUser: () => Promise<void>;
updateUser: (newUser: any) => Promise<boolean>;
cacheData?: { [url: string]: any } | null;
cacheLoading?: boolean;
cacheError?: string | null;
refreshCache?: (url?: string) => Promise<void>;
updateCache?: (url: string, data: any) => Promise<void>;
clearCache?: (url: string) => Promise<void>;
}
interface MenuProps {

View File

@ -56,10 +56,14 @@ interface DashboardPageProps {
}
interface TableDataItem {
uu_id: string;
process_name: string;
bank_date: string;
currency_value: number | string;
name: string;
email: string;
description: string;
category: string;
priority: string;
notifications: boolean;
terms: boolean;
attachments: string;
[key: string]: any; // For any additional fields
}