From fe94e114ef6e36cbbbfa4c4dc8de297eb4471aec Mon Sep 17 00:00:00 2001 From: pranalidhanavade Date: Fri, 13 Feb 2026 18:45:40 +0530 Subject: [PATCH 1/3] feat: create intent and intent list Signed-off-by: pranalidhanavade --- src/app/api/Intents.ts | 86 ++++ src/app/api/Invitation.ts | 2 +- src/app/api/organization.ts | 2 +- src/app/intents/page.tsx | 10 + src/common/enums.ts | 3 + src/components/RoleViewButton.tsx | 6 + src/config/apiRoutes.ts | 4 + src/constants/data.ts | 6 + src/features/components/SessionManager.tsx | 1 + src/features/intents/IntentList.tsx | 548 +++++++++++++++++++++ 10 files changed, 666 insertions(+), 2 deletions(-) create mode 100644 src/app/api/Intents.ts create mode 100644 src/app/intents/page.tsx create mode 100644 src/features/intents/IntentList.tsx diff --git a/src/app/api/Intents.ts b/src/app/api/Intents.ts new file mode 100644 index 000000000..3dfe0b411 --- /dev/null +++ b/src/app/api/Intents.ts @@ -0,0 +1,86 @@ +import { apiRoutes } from "@/config/apiRoutes" +import { getHeaderConfigs } from "@/config/GetHeaderConfigs" +import { axiosDelete, axiosGet, axiosPost, axiosPut } from "@/services/apiRequests" +import { AxiosResponse } from "axios" + +export const getAllIntents = async ({ + page, + itemPerPage, + search, + sortBy, + sortingOrder, + ecosystemId, +}: any): Promise => { + const url = `${apiRoutes.ecosystem.root}/${apiRoutes.intents.root}/${ecosystemId}?pageSize=${itemPerPage}&pageNumber=${page}&search=${search}&sortBy=${sortingOrder}&sortField=${sortBy}` + const axiosPayload = { + url, + config: getHeaderConfigs(), + } + + try { + return await axiosGet(axiosPayload) + } catch (error) { + const err = error as Error + return err?.message + } +} + +export const createIntent = async ( + ecosystemId: string, + payload: { name: string; description: string } +): Promise => { + const url = `${apiRoutes.ecosystem.root}/${apiRoutes.intents.root}/${ecosystemId}` + + const axiosPayload = { + url, + payload, + config: getHeaderConfigs(), + } + + try { + return await axiosPost(axiosPayload) + } catch (error) { + const err = error as Error + return err?.message + } +} + +export const updateIntent = async ( + ecosystemId: string, + intentId: string, + data: { name: string; description: string }, +): Promise => { + const url = `/v1/ecosystem/intents/${ecosystemId}/${intentId}` + + const axiosPayload = { + url, + payload: data, + config: getHeaderConfigs(), + } + + try { + return await axiosPut(axiosPayload) + } catch (error) { + const err = error as Error + return err?.message + } +} + +export const deleteIntent = async ( + ecosystemId: string, + intentId: string, +): Promise => { + const url = `/v1/ecosystem/intents/${ecosystemId}/${intentId}` + + const axiosPayload = { + url, + config: getHeaderConfigs(), + } + + try { + return await axiosDelete(axiosPayload) + } catch (error) { + const err = error as Error + return err?.message + } +} diff --git a/src/app/api/Invitation.ts b/src/app/api/Invitation.ts index d7580e5dd..607248e8b 100644 --- a/src/app/api/Invitation.ts +++ b/src/app/api/Invitation.ts @@ -114,7 +114,7 @@ export const getUserEcosystemInvitations = async ( search: string, orgId: string, ): Promise => { - const url = `${apiRoutes.Ecosystem.root}/${orgId}${apiRoutes.Ecosystem.usersInvitation}?pageNumber=${pageNumber}&pageSize=${pageSize}&search=${search}` + const url = `${apiRoutes.ecosystem.root}/${orgId}${apiRoutes.ecosystem.usersInvitation}?pageNumber=${pageNumber}&pageSize=${pageSize}&search=${search}` const config = getHeaderConfigs() diff --git a/src/app/api/organization.ts b/src/app/api/organization.ts index f95c2634a..da7a2549a 100644 --- a/src/app/api/organization.ts +++ b/src/app/api/organization.ts @@ -238,7 +238,7 @@ export const createSchemaRequest = async ( // endorsementId: string, orgId: string, ): Promise => { - const url = `${apiRoutes.Ecosystem.root}/${orgId}${apiRoutes.Ecosystem.endorsements.createSchemaRequest}` + const url = `${apiRoutes.ecosystem.root}/${orgId}${apiRoutes.ecosystem.endorsements.createSchemaRequest}` const payload = data const axiosPayload = { url, diff --git a/src/app/intents/page.tsx b/src/app/intents/page.tsx new file mode 100644 index 000000000..6ebb5d2fe --- /dev/null +++ b/src/app/intents/page.tsx @@ -0,0 +1,10 @@ +import IntentList from '@/features/intents/IntentList' +import React from 'react' + +const page = (): React.JSX.Element => ( +
+ +
+) + +export default page diff --git a/src/common/enums.ts b/src/common/enums.ts index 5afa9ead4..5884ffa78 100644 --- a/src/common/enums.ts +++ b/src/common/enums.ts @@ -41,6 +41,7 @@ export enum Features { ISSUANCE = 'issuance', VERIFICATION = 'verification', CREATE_CERTIFICATE = 'create_certificate', + ECOSYSTEM = 'ecosystem' } export enum SchemaTypes { @@ -65,6 +66,8 @@ export enum Roles { ISSUER = 'issuer', VERIFIER = 'verifier', MEMBER = 'member', + ECOSYSTEM_LEAD = 'ecosystem lead', + ECOSYSTEM_MEMBER = 'ecosystem member' } export enum OrganizationRoles { diff --git a/src/components/RoleViewButton.tsx b/src/components/RoleViewButton.tsx index 8da56520b..4150fb89d 100644 --- a/src/components/RoleViewButton.tsx +++ b/src/components/RoleViewButton.tsx @@ -50,6 +50,12 @@ const RoleViewButton = ({ roles.includes(Roles.VERIFIER) ) + case Features.ECOSYSTEM: + return ( + + roles.includes(Roles.ECOSYSTEM_LEAD) + ) + default: return roles.includes(Roles.OWNER) || roles.includes(Roles.ADMIN) } diff --git a/src/config/apiRoutes.ts b/src/config/apiRoutes.ts index 22886e6a3..87a4f540d 100644 --- a/src/config/apiRoutes.ts +++ b/src/config/apiRoutes.ts @@ -24,6 +24,10 @@ export const apiRoutes = { deleteSession: '/auth/sessionId:/sessions', }, + intents: { + root:'intents' + }, + users: { userProfile: '/users/profile', checkUser: '/users/', diff --git a/src/constants/data.ts b/src/constants/data.ts index 87e515c27..27342ebd3 100644 --- a/src/constants/data.ts +++ b/src/constants/data.ts @@ -63,6 +63,12 @@ export const navItems: NavItem[] = [ url: '/x509-certificate', icon: 'login', }, + { + title: 'Intents', + shortcut: ['l', 'l'], + url: '/intents', + icon: 'login', + }, ], }, { diff --git a/src/features/components/SessionManager.tsx b/src/features/components/SessionManager.tsx index 2f91ab613..e7fb7260c 100644 --- a/src/features/components/SessionManager.tsx +++ b/src/features/components/SessionManager.tsx @@ -16,6 +16,7 @@ const preventRedirectOnPaths = [ '/wallet-setup', '/create-did', '/x509-certificate', + '/intents', '/did-details', '/organizations', '/users', diff --git a/src/features/intents/IntentList.tsx b/src/features/intents/IntentList.tsx new file mode 100644 index 000000000..eb10def2d --- /dev/null +++ b/src/features/intents/IntentList.tsx @@ -0,0 +1,548 @@ +'use client' + +import { + IColumnData, + ITableMetadata, + SortActions, + TableStyling, + getColumns, +} from '@/components/ui/generic-table-component/columns' +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog' +import { MoreVertical, Pencil, Trash2 } from 'lucide-react' +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu' +import { Button } from '@/components/ui/button' +import ConfirmationModal from '@/components/confirmation-modal' + +import { Plus, RefreshCw } from 'lucide-react' +import React, { JSX, useEffect, useState } from 'react' + +import { AlertComponent } from '@/components/AlertComponent' +import { AxiosResponse } from 'axios' +import { CellContext } from '@tanstack/react-table' +import { DataTable } from '@/components/ui/generic-table-component/data-table' +import { DateCell } from '../organization/connectionIssuance/components/CredentialTableCells' +import Loader from '@/components/Loader' +import PageContainer from '@/components/layout/page-container' +import { apiStatusCodes } from '@/config/CommonConstant' +import { pathRoutes } from '@/config/pathRoutes' +import { useAppSelector } from '@/lib/hooks' +import { useRouter } from 'next/navigation' +import { + createIntent, + deleteIntent, + getAllIntents, + updateIntent, +} from '@/app/api/Intents' + +interface Intent { + id: string + ecosystemId: string + name: string + description: string + createDateTime: string + lastChangedDateTime: string +} + +interface PaginationState { + pageIndex: number + pageSize: number + pageCount: number + searchTerm: string + sortBy: string + sortOrder: SortActions +} + +const IntentList = (): JSX.Element => { + const router = useRouter() + const orgId = useAppSelector((state) => state.organization.orgId) + const ecosystemId = 'b3fd5fc8-d503-4f8e-a55a-5f52bd619688' + + const [loading, setLoading] = useState(true) + const [error, setError] = useState(null) + const [intentList, setIntentList] = useState([]) + const [reloading, setReloading] = useState(false) + const [openCreate, setOpenCreate] = useState(false) + const [creating, setCreating] = useState(false) + const [intentName, setIntentName] = useState('') + const [intentDesc, setIntentDesc] = useState('') + const [success, setSuccess] = useState(null) + const [failure, setFailure] = useState(null) + const [showConfirmModal, setShowConfirmModal] = useState(false) + const [selectedIntent, setSelectedIntent] = useState(null) + const [actionType, setActionType] = useState<'delete' | 'update' | null>(null) + const [actionLoading, setActionLoading] = useState(false) + const [isEdit, setIsEdit] = useState(false) + const [editIntentId, setEditIntentId] = useState(null) + + const [pagination, setPagination] = useState({ + pageIndex: 0, + pageSize: 10, + pageCount: 1, + searchTerm: '', + sortBy: 'createDateTime', + sortOrder: 'desc', + }) + + const fetchIntents = async (reload = false): Promise => { + if (reload) setReloading(true) + else setLoading(true) + + try { + const response = await getAllIntents({ + itemPerPage: pagination.pageSize, + page: pagination.pageIndex + 1, + search: pagination.searchTerm, + sortBy: pagination.sortBy, + sortingOrder: pagination.sortOrder, + ecosystemId, + }) + + const { data } = response as AxiosResponse + + if (data?.statusCode === apiStatusCodes.API_STATUS_SUCCESS) { + setIntentList(data?.data?.data ?? []) + setError(null) + } else { + setIntentList([]) + } + } catch (err) { + setIntentList([]) + setError('Failed to fetch intents') + throw err + } finally { + if (reload) setReloading(false) + else setLoading(false) + } + } + + const handleReload = async (): Promise => { + await fetchIntents(true) + } + + useEffect(() => { + if (!orgId) { + setLoading(false) + return + } + fetchIntents() + }, [orgId]) + + useEffect(() => { + if (!success && !failure) return + + const timer = setTimeout(() => { + setSuccess(null) + setFailure(null) + + if (openCreate) { + setOpenCreate(false) + setIsEdit(false) + setEditIntentId(null) + setIntentName('') + setIntentDesc('') + } + + if (showConfirmModal) { + setShowConfirmModal(false) + setSelectedIntent(null) + setActionType(null) + } + }, 3000) + + return () => clearTimeout(timer) + }, [success, failure]) + + const handleCreateIntent = (): void => { + setIsEdit(false) + setEditIntentId(null) + setIntentName('') + setIntentDesc('') + setOpenCreate(true) + } + + const handleSubmitIntent = async (): Promise => { + setSuccess(null) + setFailure(null) + + if (!intentName.trim()) { + setFailure('Intent name is required') + return + } + + setCreating(true) + + try { + if (isEdit && editIntentId) { + const res = (await updateIntent(ecosystemId, editIntentId, { + name: intentName, + description: intentDesc, + })) as AxiosResponse + + if (res?.data?.statusCode === apiStatusCodes.API_STATUS_SUCCESS) { + setSuccess(res?.data?.message || 'Intent updated successfully') + + setIntentName('') + setIntentDesc('') + setIsEdit(false) + setEditIntentId(null) + + fetchIntents(true) + return + } else { + setFailure(res?.data?.message || 'Failed to update intent') + return + } + } + + const res = (await createIntent(ecosystemId, { + name: intentName, + description: intentDesc, + })) as AxiosResponse + + if (res?.data?.statusCode === apiStatusCodes.API_STATUS_CREATED) { + setSuccess(res?.data?.message || 'Intent created successfully') + + setIntentName('') + setIntentDesc('') + + fetchIntents(true) + } else { + setFailure(res?.data?.message || 'Failed to create intent') + } + } catch { + setFailure(isEdit ? 'Failed to update intent' : 'Failed to create intent') + } finally { + setCreating(false) + } + } + + const openConfirmation = ( + intent: Intent, + action: 'delete' | 'update', + ): void => { + setSuccess(null) + setFailure(null) + + if (action === 'update') { + setIsEdit(true) + setEditIntentId(intent.id) + setIntentName(intent.name) + setIntentDesc(intent.description) + setOpenCreate(true) + return + } + + setSelectedIntent(intent) + setActionType(action) + setShowConfirmModal(true) + } + + const handleDeleteIntent = async (intentId: string): Promise => { + try { + const res = await deleteIntent(ecosystemId, intentId) + + if (typeof res === 'string') { + setFailure(res) + return false + } + + if (res?.data?.statusCode === 200) { + setSuccess(res?.data?.message || 'Intent deleted successfully') + fetchIntents(true) + return true + } + + setFailure(res?.data?.message || 'Failed to delete intent') + return false + } catch { + setFailure('Something went wrong') + return false + } + } + + const handleConfirmedAction = async (): Promise => { + if (!selectedIntent || !actionType) return + + setActionLoading(true) + + let isSuccess = false + + try { + if (actionType === 'delete') { + await handleDeleteIntent(selectedIntent.id) + } + } finally { + setActionLoading(false) + } + } + + const actionCell = ({ row }: { row: { original: Intent } }): JSX.Element => { + const intent = row.original + + return ( + + + + + + + {/* UPDATE */} + openConfirmation(intent, 'update')}> + + Update + + + {/* DELETE */} + openConfirmation(intent, 'delete')}> + + Delete + + + + ) + } + + const columnData: IColumnData[] = [ + { + id: 'name', + title: 'Intent', + accessorKey: 'name', + cell: ({ row }) => ( + {row.original.name || 'NA'} + ), + columnFunction: [], + }, + { + id: 'description', + title: 'Description', + accessorKey: 'description', + cell: ({ row }) => {row.original.description ?? 'NA'}, + columnFunction: [], + }, + { + id: 'createDateTime', + title: 'Created On', + accessorKey: 'createDateTime', + cell: ({ row }) => + row.original.createDateTime ? ( + + ) : ( + NA + ), + columnFunction: [ + { + sortCallBack: async (order): Promise => { + setPagination((prev) => ({ + ...prev, + sortBy: 'connectionId', + sortOrder: order, + })) + }, + }, + ], + }, + { + id: 'actions', + title: 'Actions', + accessorKey: 'actions', + cell: actionCell, + columnFunction: [], + }, + ] + + const metadata: ITableMetadata = { + enableSelection: false, + } + + const tableStyling: TableStyling = { metadata, columnData } + const column = getColumns(tableStyling) + + return ( + + {/* HEADER */} +
+
+

Intents

+

Manage ecosystem intents here

+
+ +
+ {/* reload */} + + + {/* create intent */} + +
+
+ + {/* table */} +
+ { + setOpenCreate(open) + + if (!open) { + setSuccess(null) + setFailure(null) + setIsEdit(false) + setEditIntentId(null) + setIntentName('') + setIntentDesc('') + } + }} + > + + + {success && ( + setSuccess(null)} + /> + )} + + {failure && ( + setFailure(null)} + /> + )} + + + {isEdit ? 'Update Intent' : 'Create Intent'} + + + +
+ {/* Name */} +
+ + setIntentName(e.target.value)} + placeholder="Enter intent name" + className="focus:ring-primary w-full rounded-md border px-3 py-2 text-sm outline-none focus:ring-2" + /> +
+ +
+ +