create/update actions and redis cahce store completed tested
This commit is contained in:
parent
06ca2b0835
commit
311736ce06
|
|
@ -43,10 +43,44 @@ Network Manager NM Network Manager
|
||||||
Application Manager AM Application Manager
|
Application Manager AM Application Manager
|
||||||
Super User SUE Super User
|
Super User SUE Super User
|
||||||
|
|
||||||
|
|
||||||
Daire Sakini Vekili Daire Sakini Vekili FL-REP Daire FL
|
Daire Sakini Vekili Daire Sakini Vekili FL-REP Daire FL
|
||||||
URL Type Tested
|
URL Type Tested
|
||||||
/building/accounts/managment/accounts : flat_representative No
|
/building/accounts/managment/accounts : flat_representative No
|
||||||
|
|
||||||
/definitions/identifications/people : flat_tenant 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
|
||||||
|
|
|
||||||
|
|
@ -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" } }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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)],
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
@ -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 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -15,6 +15,12 @@ const PageToBeChildrendMulti: React.FC<ContentProps> = ({
|
||||||
updateOnline,
|
updateOnline,
|
||||||
refreshUser,
|
refreshUser,
|
||||||
updateUser,
|
updateUser,
|
||||||
|
cacheData,
|
||||||
|
cacheLoading,
|
||||||
|
cacheError,
|
||||||
|
refreshCache,
|
||||||
|
updateCache,
|
||||||
|
clearCache
|
||||||
}) => {
|
}) => {
|
||||||
const pageComponents = pageIndexMulti[activePageUrl];
|
const pageComponents = pageIndexMulti[activePageUrl];
|
||||||
if (!pageComponents) { return <ContentToRenderNoPage lang={onlineData.lang} /> }
|
if (!pageComponents) { return <ContentToRenderNoPage lang={onlineData.lang} /> }
|
||||||
|
|
@ -25,6 +31,7 @@ const PageToBeChildrendMulti: React.FC<ContentProps> = ({
|
||||||
activePageUrl={activePageUrl} searchParams={searchParams}
|
activePageUrl={activePageUrl} searchParams={searchParams}
|
||||||
userData={userData} userLoading={userLoading} userError={userError} refreshUser={refreshUser} updateUser={updateUser}
|
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}
|
||||||
/>;
|
/>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,9 @@ import {
|
||||||
TableDataItem,
|
TableDataItem,
|
||||||
} from "@/validations/mutual/table/validations";
|
} from "@/validations/mutual/table/validations";
|
||||||
import LoadingContent from "@/components/mutual/loader/component";
|
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 {
|
interface DataTableProps {
|
||||||
table: ReturnType<typeof useReactTable>;
|
table: ReturnType<typeof useReactTable>;
|
||||||
|
|
@ -42,6 +45,7 @@ interface DataTableProps {
|
||||||
getSortingIcon: (columnId: string) => React.ReactNode;
|
getSortingIcon: (columnId: string) => React.ReactNode;
|
||||||
flexRender: typeof flexRender;
|
flexRender: typeof flexRender;
|
||||||
t: Translations;
|
t: Translations;
|
||||||
|
handleEditRow?: (row: TableDataItem) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TableFormProps {
|
interface TableFormProps {
|
||||||
|
|
@ -105,7 +109,8 @@ const DataTable: React.FC<DataTableProps> = React.memo(({
|
||||||
handleSortingChange,
|
handleSortingChange,
|
||||||
getSortingIcon,
|
getSortingIcon,
|
||||||
flexRender,
|
flexRender,
|
||||||
t
|
t,
|
||||||
|
handleEditRow
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div className="overflow-x-auto relative">
|
<div className="overflow-x-auto relative">
|
||||||
|
|
@ -120,20 +125,23 @@ const DataTable: React.FC<DataTableProps> = React.memo(({
|
||||||
<thead className="bg-gray-50">
|
<thead className="bg-gray-50">
|
||||||
{table.getHeaderGroups().map((headerGroup) => (
|
{table.getHeaderGroups().map((headerGroup) => (
|
||||||
<tr key={headerGroup.id}>
|
<tr key={headerGroup.id}>
|
||||||
{headerGroup.headers.map((header) => (
|
{headerGroup.headers.map((header) => {
|
||||||
<th
|
console.log('Rendering header:', header.id);
|
||||||
key={header.id}
|
return (
|
||||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer"
|
<th
|
||||||
onClick={() => handleSortingChange(header.column.id)}
|
key={header.id}
|
||||||
aria-sort={header.column.getIsSorted() ? (header.column.getIsSorted() === 'desc' ? 'descending' : 'ascending') : undefined}
|
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer"
|
||||||
scope="col"
|
onClick={() => handleSortingChange(header.column.id)}
|
||||||
>
|
aria-sort={header.column.getIsSorted() ? (header.column.getIsSorted() === 'desc' ? 'descending' : 'ascending') : undefined}
|
||||||
<div className="flex items-center space-x-1">
|
scope="col"
|
||||||
{flexRender(header.column.columnDef.header, header.getContext())}
|
>
|
||||||
<span>{getSortingIcon(header.column.id)}</span>
|
<div className="flex items-center space-x-1">
|
||||||
</div>
|
{flexRender(header.column.columnDef.header, header.getContext())}
|
||||||
</th>
|
<span>{getSortingIcon(header.column.id)}</span>
|
||||||
))}
|
</div>
|
||||||
|
</th>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
</thead>
|
</thead>
|
||||||
|
|
@ -147,11 +155,14 @@ const DataTable: React.FC<DataTableProps> = React.memo(({
|
||||||
) : (
|
) : (
|
||||||
table.getRowModel().rows.map((row) => (
|
table.getRowModel().rows.map((row) => (
|
||||||
<tr key={row.id} className="hover:bg-gray-50">
|
<tr key={row.id} className="hover:bg-gray-50">
|
||||||
{row.getVisibleCells().map((cell) => (
|
{row.getVisibleCells().map((cell) => {
|
||||||
<td key={cell.id} className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
console.log('Rendering cell:', cell.column.id);
|
||||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
return (
|
||||||
</td>
|
<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>
|
</tr>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
|
|
@ -196,11 +207,17 @@ const TableForm: React.FC<TableFormProps> = ({
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{renderPageOptions().map((option: { key: string | number; value: string; label: string }) => (
|
{renderPageOptions().length > 0 ? (
|
||||||
<SelectItem key={option.key} value={option.value}>
|
renderPageOptions().map((option: { key: string | number; value: string; label: string }) => (
|
||||||
{option.label}
|
<SelectItem key={option.key} value={option.value}>
|
||||||
|
{option.label}
|
||||||
|
</SelectItem>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<SelectItem key="1" value="1">
|
||||||
|
1
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
)}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
|
|
@ -328,6 +345,7 @@ const TableCardComponentImproved: React.FC<DashboardPageProps> = React.memo((pro
|
||||||
// Initialize translation with English as default
|
// Initialize translation with English as default
|
||||||
const language = props.onlineData?.lang as LanguageTypes || 'en';
|
const language = props.onlineData?.lang as LanguageTypes || 'en';
|
||||||
const t = translations[language];
|
const t = translations[language];
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
form,
|
form,
|
||||||
|
|
@ -351,30 +369,90 @@ const TableCardComponentImproved: React.FC<DashboardPageProps> = React.memo((pro
|
||||||
isNextDisabled,
|
isNextDisabled,
|
||||||
pageSizeOptions,
|
pageSizeOptions,
|
||||||
renderPageOptions,
|
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>();
|
const columnHelper = createColumnHelper<TableDataItem>();
|
||||||
|
// Make sure columns are properly defined with the actions column
|
||||||
const columns = React.useMemo(() => [
|
const columns = React.useMemo(() => [
|
||||||
columnHelper.accessor('uu_id', {
|
columnHelper.display({
|
||||||
cell: info => info.getValue(),
|
id: 'actions',
|
||||||
header: () => <span>UUID</span>,
|
header: () => <span>Actions</span>,
|
||||||
footer: info => info.column.id
|
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(),
|
cell: info => info.getValue(),
|
||||||
header: () => <span>Name</span>,
|
header: () => <span>Name</span>,
|
||||||
footer: info => info.column.id
|
footer: info => info.column.id
|
||||||
}),
|
}),
|
||||||
columnHelper.accessor('bank_date', {
|
columnHelper.accessor('email', {
|
||||||
header: () => <span>Bank Date</span>,
|
cell: info => info.getValue(),
|
||||||
|
header: () => <span>Email</span>,
|
||||||
|
footer: info => info.column.id
|
||||||
|
}),
|
||||||
|
columnHelper.accessor('description', {
|
||||||
|
header: () => <span>Description</span>,
|
||||||
cell: info => info.getValue(),
|
cell: info => info.getValue(),
|
||||||
footer: info => info.column.id
|
footer: info => info.column.id
|
||||||
}),
|
}),
|
||||||
columnHelper.accessor('currency_value', {
|
columnHelper.accessor('category', {
|
||||||
header: () => <span>Currency Value</span>,
|
header: () => <span>Category</span>,
|
||||||
cell: info => String(info.getValue()),
|
cell: info => String(info.getValue()),
|
||||||
footer: info => info.column.id
|
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>[];
|
], [columnHelper]) as ColumnDef<TableDataItem>[];
|
||||||
|
|
||||||
const table = useReactTable({
|
const table = useReactTable({
|
||||||
|
|
@ -387,8 +465,12 @@ const TableCardComponentImproved: React.FC<DashboardPageProps> = React.memo((pro
|
||||||
getPaginationRowModel: getPaginationRowModel(),
|
getPaginationRowModel: getPaginationRowModel(),
|
||||||
manualPagination: true,
|
manualPagination: true,
|
||||||
pageCount: apiPagination.totalPages || 1,
|
pageCount: apiPagination.totalPages || 1,
|
||||||
|
debugTable: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Check if actions column is included
|
||||||
|
const actionColumnExists = table.getVisibleLeafColumns().some(col => col.id === 'actions');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
isLoading ? <LoadingContent height="h-48" size="w-36 h-36" plane="h-full w-full" /> :
|
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">
|
<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}
|
error={error}
|
||||||
t={t}
|
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>
|
</div>
|
||||||
|
|
||||||
<TableForm
|
<TableForm
|
||||||
|
|
@ -435,6 +524,7 @@ const TableCardComponentImproved: React.FC<DashboardPageProps> = React.memo((pro
|
||||||
getSortingIcon={getSortingIcon}
|
getSortingIcon={getSortingIcon}
|
||||||
flexRender={flexRender}
|
flexRender={flexRender}
|
||||||
t={t}
|
t={t}
|
||||||
|
handleEditRow={handleEditRow}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,7 @@ const FallbackContent: FC<{ lang: LanguageTypes; activePageUrl: string }> = memo
|
||||||
const ContentComponent: FC<ContentProps> = ({
|
const ContentComponent: FC<ContentProps> = ({
|
||||||
searchParams, activePageUrl, mode, userData, userLoading, userError, refreshUser, updateUser,
|
searchParams, activePageUrl, mode, userData, userLoading, userError, refreshUser, updateUser,
|
||||||
onlineData, onlineLoading, onlineError, refreshOnline, updateOnline,
|
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 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)]";
|
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
|
<MemoizedMultiPage
|
||||||
activePageUrl={activePageUrl || ''} searchParams={searchParams}
|
activePageUrl={activePageUrl || ''} searchParams={searchParams}
|
||||||
userData={userData} userLoading={userLoading} userError={userError} refreshUser={refreshUser} updateUser={updateUser}
|
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>
|
</div>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
@ -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);
|
||||||
|
|
@ -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 };
|
||||||
|
|
@ -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} />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
'use client';
|
'use client';
|
||||||
import React, { ReactNode } from 'react';
|
import React, { ReactNode } from 'react';
|
||||||
import { OnlineProvider } from '@/components/mutual/context/online/provider';
|
import { OnlineProvider } from '@/components/mutual/context/online/provider';
|
||||||
|
import { CacheProvider } from '@/components/mutual/context/cache/context';
|
||||||
|
|
||||||
interface ClientProvidersProps {
|
interface ClientProvidersProps {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
|
|
@ -14,7 +15,9 @@ export function ClientProviders({ children }: ClientProvidersProps) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<OnlineProvider>
|
<OnlineProvider>
|
||||||
{children}
|
<CacheProvider>
|
||||||
|
{children}
|
||||||
|
</CacheProvider>
|
||||||
</OnlineProvider>
|
</OnlineProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 };
|
||||||
|
|
@ -6,6 +6,7 @@ import { useOnline } from "@/components/mutual/context/online/context";
|
||||||
import { useSelection } from "@/components/mutual/context/selection/context";
|
import { useSelection } from "@/components/mutual/context/selection/context";
|
||||||
import { useUser } from "@/components/mutual/context/user/context";
|
import { useUser } from "@/components/mutual/context/user/context";
|
||||||
import { useConfig } from "@/components/mutual/context/config/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 { ModeTypes } from "@/validations/mutual/dashboard/props";
|
||||||
|
|
||||||
import HeaderComponent from "@/components/custom/header/component";
|
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 { availableApplications, isLoading: menuLoading, error: menuError, menuData, refreshMenu, updateMenu } = useMenu();
|
||||||
const { selectionData, isLoading: selectionLoading, error: selectionError, refreshSelection, updateSelection } = useSelection();
|
const { selectionData, isLoading: selectionLoading, error: selectionError, refreshSelection, updateSelection } = useSelection();
|
||||||
const { configData, isLoading: configLoading, error: configError, refreshConfig, updateConfig } = useConfig();
|
const { configData, isLoading: configLoading, error: configError, refreshConfig, updateConfig } = useConfig();
|
||||||
|
const { cacheData, isLoading: cacheLoading, error: cacheError, refreshCache, updateCache, clearCache } = useCache();
|
||||||
const prefix = "/panel"
|
const prefix = "/panel"
|
||||||
const mode = (searchParams?.mode as ModeTypes) || 'shortList';
|
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} />
|
menuData={menuData} menuLoading={menuLoading} menuError={menuError} refreshMenu={refreshMenu} updateMenu={updateMenu} />
|
||||||
<ContentComponent activePageUrl={activePageUrl} mode={mode} searchParams={searchParams}
|
<ContentComponent activePageUrl={activePageUrl} mode={mode} searchParams={searchParams}
|
||||||
userData={userData} userLoading={userLoading} userError={userError} refreshUser={refreshUser} updateUser={updateUser}
|
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}
|
{/* <FooterComponent activePageUrl={activePageUrl} searchParams={searchParams}
|
||||||
configData={configData} configLoading={configLoading} configError={configError} refreshConfig={refreshConfig} updateConfig={updateConfig}
|
configData={configData} configLoading={configLoading} configError={configError} refreshConfig={refreshConfig} updateConfig={updateConfig}
|
||||||
onlineData={onlineData} onlineLoading={onlineLoading} onlineError={onlineError} refreshOnline={refreshOnline} updateOnline={updateOnline} /> */}
|
onlineData={onlineData} onlineLoading={onlineLoading} onlineError={onlineError} refreshOnline={refreshOnline} updateOnline={updateOnline} /> */}
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,14 @@
|
||||||
// import { DashboardPage } from "@/components/custom/content/DashboardPage";
|
// import { DashboardPage } from "@/components/custom/content/DashboardPage";
|
||||||
import { DPage } from "@/components/custom/content/DPage";
|
import { DPage } from "@/components/custom/content/DPage";
|
||||||
import TableCardComponentImproved from "@/components/custom/content/TableCardComponentImproved";
|
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>>> = {
|
const pageIndexMulti: Record<string, Record<string, React.FC<any>>> = {
|
||||||
"/dashboard": { DashboardPage: TableCardComponentImproved },
|
"/dashboard": { DashboardPage: TableCardComponentImproved },
|
||||||
"/build": { DashboardPage: DPage },
|
"/build": { DashboardPage: TableCardComponentImproved },
|
||||||
"/build/create": { DashboardPage: DPage },
|
"/build/create": { DashboardPage: CreateFromComponent },
|
||||||
"/build/update": { DashboardPage: DPage },
|
"/build/update": { DashboardPage: UpdateFromComponent },
|
||||||
"/people": { DashboardPage: DPage },
|
"/people": { DashboardPage: DPage },
|
||||||
"/people/create": { DashboardPage: DPage },
|
"/people/create": { DashboardPage: DPage },
|
||||||
"/people/update": { DashboardPage: DPage },
|
"/people/update": { DashboardPage: DPage },
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,12 @@ interface ContentProps {
|
||||||
userError: any;
|
userError: any;
|
||||||
refreshUser: () => Promise<void>;
|
refreshUser: () => Promise<void>;
|
||||||
updateUser: (newUser: any) => Promise<boolean>;
|
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 {
|
interface MenuProps {
|
||||||
|
|
|
||||||
|
|
@ -56,10 +56,14 @@ interface DashboardPageProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TableDataItem {
|
interface TableDataItem {
|
||||||
uu_id: string;
|
name: string;
|
||||||
process_name: string;
|
email: string;
|
||||||
bank_date: string;
|
description: string;
|
||||||
currency_value: number | string;
|
category: string;
|
||||||
|
priority: string;
|
||||||
|
notifications: boolean;
|
||||||
|
terms: boolean;
|
||||||
|
attachments: string;
|
||||||
[key: string]: any; // For any additional fields
|
[key: string]: any; // For any additional fields
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue