diff --git a/commit/api/api_explorer.py b/commit/api/api_explorer.py index ef457cb..dc4001d 100644 --- a/commit/api/api_explorer.py +++ b/commit/api/api_explorer.py @@ -18,6 +18,7 @@ def get_apis_for_project(project_branch: str): for doc in documentation: if doc.get("function_name") == api.get("name") and doc.get("path") == api.get("api_path"): api["documentation"] = doc.get("documentation") + api["last_updated"] = doc.get("last_updated") break app_name, organization, app_logo = frappe.db.get_value("Commit Project", branch_doc.project, ["app_name", "org", "image"]) diff --git a/commit/api/generate_documentation.py b/commit/api/generate_documentation.py index 43a39df..a11d08d 100644 --- a/commit/api/generate_documentation.py +++ b/commit/api/generate_documentation.py @@ -61,24 +61,22 @@ def generate_docs_for_chunk(api_chunk): "content": ( "You are an expert documentation generator. Create detailed and comprehensive documentation " "for the code provided below in Markdown format. Each function should have the following sections:\n\n" - "- **(api[Function Name])**\n" - "- **Description**: Detailed description of what the function does and what it is used for \n" - "- **Parameters**: List of parameters with their types, descriptions, and indicate which are mandatory or optional\n" - "- **Return Type**: Type and description of the return value\n" - "- **Examples**: Code examples demonstrating how to use the function (enclosed in triple backticks ``````).\n\n" + "- # [Function Name] (as heading 1)\n" + "- ## Description: Detailed description of what the function does and what it is used for \n" + "- ## Parameters: List of parameters with their types, descriptions, and indicate which are mandatory or optional\n" + "- ## Return Type: Type and description of the return value\n" + "- ## Examples: Code examples demonstrating how to use the function (enclosed using
 and  Tags`).\n\n"
                 "The response should be a valid JSON list of objects formatted as follows: "
-                "{function_name: , path: , documentation: }.\n"
+                "{function_name: , path: , last_updated:, documentation: }.\n"
                 "Ensure the response is in valid JSON format only, enclosed in triple backticks, and does not include `---`."
             )
         }
     ]
-
+    last_updated = frappe.utils.now()
     for api in api_chunk:
-        user_message = f"function name: {api['function_name']}, path: {api['path']}, code:\n{api['code']}"
+        user_message = f"function name: {api['function_name']}, path: {api['path']}, last_updated:{last_updated} ,code:\n{api['code']}"
         messages.append({"role": "user", "content": user_message})
 
-    # print("Raw Response:\n", response_text)  # Log raw response for debugging
-
     response_text = open_ai_call(messages)
 
     cleaned_response = clean_response(response_text)
@@ -103,7 +101,7 @@ def generate_docs_for_chunk(api_chunk):
             return json.loads(cleaned_response, strict=False)
         except json.JSONDecodeError as e:
             print("Second JSON Decode Error:", e)
-            return []
+            return generate_docs_for_chunk(api_chunk)
 
     # return cleaned_response
 
@@ -120,13 +118,13 @@ def generate_documentation_for_api_snippet(api_path:str,code_snippet:str):
                 "- ## Return Type\n Specify the type and description of the return value.\n"
                 "- ## Examples\n Provide code examples demonstrating how to use the function, enclosed in triple backticks (``````).\n\n"
                 "The response should be a valid JSON formatted as follows: "
-                "{function_name: , path: , documentation: }.\n"
+                "{function_name: , path: , last_updated:, documentation: }.\n"
                 "Ensure the response is in valid JSON format only, and does not include `---`."
             )
         }
     ]
     
-    user_message = f"api path: {api_path}, code:\n{code_snippet}"
+    user_message = f"api path: {api_path}, last_updated:{frappe.utils.now()}, code:\n{code_snippet}"
     if not code_snippet:
         return []
     messages.append({"role": "user", "content": user_message})
@@ -161,4 +159,84 @@ def generate_documentation_for_api_snippet(api_path:str,code_snippet:str):
 def get_documentation_for_api(project_branch: str, file_path: str,block_start: int, block_end: int,endpoint:str,viewer_type:str = 'app'):
     code_snippet = get_file_content_from_path(project_branch, file_path,block_start, block_end,viewer_type)
     api_path = endpoint
-    return generate_documentation_for_api_snippet(api_path, code_snippet)
\ No newline at end of file
+    return generate_documentation_for_api_snippet(api_path, code_snippet)
+
+@frappe.whitelist()
+def save_documentation(project_branch:str,endpoint:str,documentation:str,viewer_type:str = 'app'):
+    # Save the documentation to the project branch
+    # 1. Check for viewer_type app or project
+    # 2. If viewer_type is app, then check the document is already present in Commit Branch Documentation doctype
+    # 3. If document present then loop over documentation check if the function_name and path matches then update the documentation else create a new dict and append to the documentation
+    # 4. If document not present then create a new document and append the documentation
+    # 5. If viewer_type is project then check the document is already present in Commit Project Branch doctype
+    # 6. If document present then loop over documentation check if the function_name and path matches then update the documentation else create a new dict and append to the documentation
+    # 7. If document not present then create a new document and append the documentation
+
+    if viewer_type == "app":
+        # Check if the document is already present in Commit Branch Documentation doctype
+        save_documentation_for_site_app(project_branch, endpoint, documentation)
+    else:
+        save_documentation_for_project_branch(project_branch, endpoint, documentation)
+
+def save_documentation_for_project_branch(project_branch:str,endpoint:str,documentation:str):
+
+    doc = frappe.get_doc("Commit Project Branch", project_branch)
+    docs = json.loads(doc.documentation) if doc.documentation else {}
+    apis = docs.get("apis", [])
+
+    # apis is list of dict with keys function_name, path, last_updated, documentation
+    # loop over apis and check if function_name and path matches then update the documentation else create a new dict and append to the documentation
+    found = False
+    for api in apis:
+        if api.get("function_name") == endpoint.split(".")[-1] and api.get("path") == endpoint:
+            api["documentation"] = documentation
+            api["last_updated"] = frappe.utils.now()
+            found = True
+            break
+    if not found:
+        apis.append({
+            "function_name": endpoint.split(".")[-1],
+            "path": endpoint,
+            "last_updated": frappe.utils.now(),
+            "documentation": documentation
+        })
+    
+    doc.documentation = json.dumps({"apis": apis})
+    doc.save()
+
+def save_documentation_for_site_app(project_branch:str,endpoint:str,documentation:str):
+
+    if frappe.db.exists("Commit Branch Documentation",project_branch):
+        doc = frappe.get_doc("Commit Branch Documentation", project_branch)
+        docs = json.loads(doc.documentation) if doc.documentation else {}
+        apis = docs.get("apis", [])
+
+        # apis is list of dict with keys function_name, path, last_updated, documentation
+        # loop over apis and check if function_name and path matches then update the documentation else create a new dict and append to the documentation
+        found = False
+        for api in apis:
+            if api.get("function_name") == endpoint.split(".")[-1] and api.get("path") == endpoint:
+                api["documentation"] = documentation
+                api["last_updated"] = frappe.utils.now()
+                found = True
+                break
+        if not found:
+            apis.append({
+                "function_name": endpoint.split(".")[-1],
+                "path": endpoint,
+                "last_updated": frappe.utils.now(),
+                "documentation": documentation
+            })
+        doc.documentation = json.dumps({"apis": apis})
+        doc.save()
+    else:
+        # Create a new document and append the documentation
+        doc = frappe.new_doc("Commit Branch Documentation")
+        doc.app = project_branch
+        doc.documentation = json.dumps({"apis": [{
+            "function_name": endpoint.split(".")[-1],
+            "path": endpoint,
+            "last_updated": frappe.utils.now(),
+            "documentation": documentation
+        }]})
+        doc.save()
\ No newline at end of file
diff --git a/commit/commit/code_analysis/apis.py b/commit/commit/code_analysis/apis.py
index d8d44f5..2c1d3bd 100644
--- a/commit/commit/code_analysis/apis.py
+++ b/commit/commit/code_analysis/apis.py
@@ -1,5 +1,7 @@
 import os
 import ast
+import frappe
+import json
 
 other_decorators = [
     '@cache_source',
@@ -33,7 +35,7 @@ def find_all_occurrences_of_whitelist(path: str, app_name: str):
             # if file.endswith('party.py'):
             indexes,line_nos,no_of_occurrences = find_indexes_of_whitelist(file_content, no_of_occurrences)
             api_count += no_of_occurrences
-            apis = get_api_details(file, file_content, indexes,line_nos, path)
+            apis = get_api_details(app_name, file, file_content, indexes,line_nos, path)
             api_details.extend(apis)
     
     return api_details
@@ -109,7 +111,7 @@ def is_in_string_or_comment(file_content, index):
     
     return indexes, line_nos, actual_count
 
-def get_api_details(file, file_content: str, indexes: list,line_nos:list, path: str):
+def get_api_details(app_name, file, file_content: str, indexes: list,line_nos:list, path: str):
     '''
     Get details of the API
     '''
@@ -118,7 +120,7 @@ def get_api_details(file, file_content: str, indexes: list,line_nos:list, path:
         whitelist_details = get_whitelist_details(file_content, index)
         api_details = get_api_name(file_content, index)
         other_decorators = get_other_decorators(file_content, index, api_details.get('def_index'))
-        apis.append({
+        obj = {
             **api_details,
             **whitelist_details,
             'other_decorators': other_decorators,
@@ -127,7 +129,11 @@ def get_api_details(file, file_content: str, indexes: list,line_nos:list, path:
             'block_end': find_function_end_lines(file_content,api_details.get('name','')),
             'file': file,
             'api_path': file.replace(path, '').replace('\\', '/').replace('.py', '').replace('/', '.')[1:] + '.' + api_details.get('name')
-        })
+        }
+        documentation, last_updated = get_documentation_from_branch_documentation(app_name, obj.get('name'), obj.get('api_path'))
+        obj['documentation'] = documentation
+        obj['last_updated'] = last_updated
+        apis.append(obj)
     
     return apis
 
@@ -286,4 +292,24 @@ def get_decorators(node):
         decorator_name = get_decorator_name(decorator)
         if decorator_name is not None:
             decorators.append(decorator_name)
-    return decorators
\ No newline at end of file
+    return decorators
+
+def get_documentation_from_branch_documentation(app_name:str, name: str, api_path: str):
+    '''
+    Get documentation from the Commit Branch Documentation
+    '''
+    if frappe.db.exists('Commit Branch Documentation',app_name):
+        branch_documentation = frappe.get_doc('Commit Branch Documentation', app_name)
+        docs = json.loads(branch_documentation.documentation) if branch_documentation.documentation else {}
+        apis = docs.get("apis", [])
+        documentation = ''
+        last_updated = ''
+        for api in apis:
+            if api.get("function_name") == name and api.get("path") == api_path:
+                documentation = api.get("documentation")
+                last_updated = api.get("last_updated")
+                break
+        return documentation, last_updated
+    else:
+        return '', ''
+   
\ No newline at end of file
diff --git a/commit/commit/doctype/commit_branch_documentation/__init__.py b/commit/commit/doctype/commit_branch_documentation/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/commit/commit/doctype/commit_branch_documentation/commit_branch_documentation.js b/commit/commit/doctype/commit_branch_documentation/commit_branch_documentation.js
new file mode 100644
index 0000000..79e7e2d
--- /dev/null
+++ b/commit/commit/doctype/commit_branch_documentation/commit_branch_documentation.js
@@ -0,0 +1,8 @@
+// Copyright (c) 2024, The Commit Company and contributors
+// For license information, please see license.txt
+
+// frappe.ui.form.on("Commit Branch Documentation", {
+// 	refresh(frm) {
+
+// 	},
+// });
diff --git a/commit/commit/doctype/commit_branch_documentation/commit_branch_documentation.json b/commit/commit/doctype/commit_branch_documentation/commit_branch_documentation.json
new file mode 100644
index 0000000..a09a825
--- /dev/null
+++ b/commit/commit/doctype/commit_branch_documentation/commit_branch_documentation.json
@@ -0,0 +1,52 @@
+{
+ "actions": [],
+ "allow_rename": 1,
+ "autoname": "field:app",
+ "creation": "2024-09-29 12:24:08.997955",
+ "doctype": "DocType",
+ "engine": "InnoDB",
+ "field_order": [
+  "app",
+  "documentation"
+ ],
+ "fields": [
+  {
+   "fieldname": "app",
+   "fieldtype": "Data",
+   "in_list_view": 1,
+   "label": "App",
+   "reqd": 1,
+   "unique": 1
+  },
+  {
+   "fieldname": "documentation",
+   "fieldtype": "JSON",
+   "label": "Documentation"
+  }
+ ],
+ "index_web_pages_for_search": 1,
+ "links": [],
+ "modified": "2024-09-29 12:29:40.251915",
+ "modified_by": "Administrator",
+ "module": "commit",
+ "name": "Commit Branch Documentation",
+ "naming_rule": "By fieldname",
+ "owner": "Administrator",
+ "permissions": [
+  {
+   "create": 1,
+   "delete": 1,
+   "email": 1,
+   "export": 1,
+   "print": 1,
+   "read": 1,
+   "report": 1,
+   "role": "System Manager",
+   "share": 1,
+   "write": 1
+  }
+ ],
+ "sort_field": "creation",
+ "sort_order": "DESC",
+ "states": []
+}
\ No newline at end of file
diff --git a/commit/commit/doctype/commit_branch_documentation/commit_branch_documentation.py b/commit/commit/doctype/commit_branch_documentation/commit_branch_documentation.py
new file mode 100644
index 0000000..c84c245
--- /dev/null
+++ b/commit/commit/doctype/commit_branch_documentation/commit_branch_documentation.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2024, The Commit Company and contributors
+# For license information, please see license.txt
+
+# import frappe
+from frappe.model.document import Document
+
+
+class CommitBranchDocumentation(Document):
+	pass
diff --git a/commit/commit/doctype/commit_branch_documentation/test_commit_branch_documentation.py b/commit/commit/doctype/commit_branch_documentation/test_commit_branch_documentation.py
new file mode 100644
index 0000000..cc8937a
--- /dev/null
+++ b/commit/commit/doctype/commit_branch_documentation/test_commit_branch_documentation.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2024, The Commit Company and Contributors
+# See license.txt
+
+# import frappe
+from frappe.tests.utils import FrappeTestCase
+
+
+class TestCommitBranchDocumentation(FrappeTestCase):
+	pass
diff --git a/dashboard/src/components/common/FullPageLoader/SpinnerLoader.tsx b/dashboard/src/components/common/FullPageLoader/SpinnerLoader.tsx
index f5088f3..56f000e 100644
--- a/dashboard/src/components/common/FullPageLoader/SpinnerLoader.tsx
+++ b/dashboard/src/components/common/FullPageLoader/SpinnerLoader.tsx
@@ -45,14 +45,17 @@ export const AsyncSpinnerLoader: React.FC = ({
 
 export interface SpinnerLoaderProps {
     className?: string;
+    style?: React.CSSProperties;
 }
 
-export const SpinnerLoader = ({ className }: SpinnerLoaderProps) => {
+export const SpinnerLoader = ({ className, style }: SpinnerLoaderProps) => {
 
     return (
         
+ role="status" + style={style} + >
) } \ No newline at end of file diff --git a/dashboard/src/components/features/api_viewer/APIDetails.tsx b/dashboard/src/components/features/api_viewer/APIDetails.tsx index cf7651f..799f6cf 100644 --- a/dashboard/src/components/features/api_viewer/APIDetails.tsx +++ b/dashboard/src/components/features/api_viewer/APIDetails.tsx @@ -8,21 +8,14 @@ import { web_url } from "@/config/socket" import { APIData, Argument } from "@/types/APIData" import { XMarkIcon } from "@heroicons/react/24/outline" import { useFrappeGetCall } from "frappe-react-sdk" -import { useMemo, useState } from "react" +import { useMemo } from "react" import { MdOutlineFileDownload } from "react-icons/md" -import Markdown from "react-markdown" -import { AiOutlineThunderbolt } from "react-icons/ai" -import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip" -import { Dialog } from "@/components/ui/dialog" -import { APIClientContent } from "../APIClient/APIClientContent" -import { APIDocumentationOfSiteApp, Documentation } from "../documentation/APIDocumentation" - -export const APIDetails = ({ project_branch, endpointData, selectedEndpoint, setSelectedEndpoint, viewerType }: { project_branch: string, endpointData: APIData[], selectedEndpoint: string, setSelectedEndpoint: React.Dispatch>, viewerType: string }) => { +import { APIDocumentationOfSiteApp } from "../documentation/APIDocumentation" +export const APIDetails = ({ project_branch, endpointData, selectedEndpoint, setSelectedEndpoint, viewerType, mutate }: { project_branch: string, endpointData: APIData[], selectedEndpoint: string, setSelectedEndpoint: React.Dispatch>, viewerType: string, mutate: () => void }) => { const data = useMemo(() => { return endpointData.find((endpoint: APIData) => endpoint.name === selectedEndpoint) }, [endpointData, selectedEndpoint]) - const requestTypeBgColor = (requestType: string) => { switch (requestType) { case 'GET': @@ -37,7 +30,6 @@ export const APIDetails = ({ project_branch, endpointData, selectedEndpoint, set return 'bg-gray-100' } } - const requestTypeBorderColor = (requestType: string) => { switch (requestType) { case 'GET': @@ -52,14 +44,11 @@ export const APIDetails = ({ project_branch, endpointData, selectedEndpoint, set return 'ring-gray-600/20' } } - - const [apiOpen, setApiOpen] = useState(false) - return (
-

{data?.name}

+

API Details

{data?.allow_guest || data?.xss_safe ?
{data?.allow_guest && Allow Guest @@ -84,32 +73,15 @@ export const APIDetails = ({ project_branch, endpointData, selectedEndpoint, set
-
+
+
Name :
+
{data?.name}
+
+
Endpoint :
-
-
-
- {data?.api_path} -
- -
-
- {viewerType === 'app' && - - - - - - Click to make an API call to this endpoint - - - } -
+
+
{data?.api_path}
+
@@ -130,14 +102,12 @@ export const APIDetails = ({ project_branch, endpointData, selectedEndpoint, set
- Parameters Code Bruno - {data?.documentation && Documentation} - {viewerType === 'app' && Documentation} + Documentation @@ -148,23 +118,14 @@ export const APIDetails = ({ project_branch, endpointData, selectedEndpoint, set - {data?.documentation && - - } - {viewerType === 'app' && - - } + + + - - -
) } - - export const ParametersTable = ({ parameters }: { parameters?: Argument[] }) => { - return ( A list of parameters that can be used in the API @@ -190,10 +151,7 @@ export const ParametersTable = ({ parameters }: { parameters?: Argument[] }) =>
) } - - export const CodeSnippet = ({ apiData, project_branch, file_path, viewerType }: { apiData: APIData, project_branch: string, file_path: string, viewerType: string }) => { - const { data, error, isLoading } = useFrappeGetCall<{ message: { file_content: string } }>('commit.api.api_explorer.get_file_content_from_path', { project_branch: project_branch, file_path: file_path, @@ -207,7 +165,6 @@ export const CodeSnippet = ({ apiData, project_branch, file_path, viewerType }: const copyValue = () => { const content = JSON.parse(JSON.stringify(data?.message?.file_content ?? []) ?? '[]') return content?.join('') - } return (
@@ -215,7 +172,7 @@ export const CodeSnippet = ({ apiData, project_branch, file_path, viewerType }: {isLoading &&
} - +
@@ -227,17 +184,13 @@ export const CodeSnippet = ({ apiData, project_branch, file_path, viewerType }:
) } - - export const Bruno = ({ doc }: { doc: APIData }) => { - const rest = useMemo(() => { if (doc) { const { allow_guest, xss_safe, documentation, block_end, block_start, index, ...rest } = doc return rest } }, [doc]) - const { data, error, isLoading } = useFrappeGetCall('commit.api.bruno.generate_bruno_file', { data: JSON.stringify(rest), type: 'copy' @@ -245,20 +198,17 @@ export const Bruno = ({ doc }: { doc: APIData }) => { revalidateOnFocus: false, revalidateIfStale: false, }) - const copyValue = () => { const content = JSON.parse(JSON.stringify(data ?? '') ?? '[]') return content - } - return (
{error && } {isLoading &&
} - +
- - - - - - - - - - - - Click to view available bench commands in this app - - - + + + + + +
@@ -93,16 +95,28 @@ export const APIList = ({ apiList, app_name, branch_name, setSelectedEndpoint, s
{/* fixed height container */}
- +
) } -export const ListView = ({ list, setSelectedEndpoint, selectedEndpoint }: { list: APIData[], setSelectedEndpoint: (endpoint: string) => void, selectedEndpoint?: string }) => { +export const ListView = ({ list, setSelectedEndpoint, selectedEndpoint, searchQuery, listRef }: { list: APIData[], setSelectedEndpoint: (endpoint: string) => void, selectedEndpoint?: string, searchQuery?: string, listRef?: React.RefObject }) => { + + const itemRefs = useRef<(HTMLLIElement | null)[]>([]); + + useEffect(() => { + if (listRef?.current && selectedEndpoint) { + const selectedElement = itemRefs?.current?.find(item => item?.dataset.endpoint === selectedEndpoint); + if (selectedElement) { + selectedElement.scrollIntoView({ behavior: 'smooth', block: 'center' }); + } + } + }, []); + return ( -
-
    +
    +
      {list.length === 0 && (

      Sorry we couldn't find what you were looking for.

      @@ -110,7 +124,12 @@ export const ListView = ({ list, setSelectedEndpoint, selectedEndpoint }: { list
      )} {list.map((person: APIData, index: number) => ( -
    • setSelectedEndpoint(person.name)}> +
    • { + if (el) { + el.dataset.endpoint = person.name; + itemRefs.current[index] = el; + } + }} className={`flex justify-between gap-x-6 p-2 hover:bg-gray-100 cursor-pointer group ${selectedEndpoint === person.name ? 'bg-gray-100' : ''} `} onClick={() => setSelectedEndpoint(person.name)}>

      {person.name}

      @@ -137,9 +156,9 @@ export const ListView = ({ list, setSelectedEndpoint, selectedEndpoint }: { list ))}
    {/* create a div which is at fixed location and should be stick bottom which will show total list count at right corner of same w as above ul*/} - {list.length &&
    -

    Total {list.length} API's

    + {list.length &&
    +

    {list.length} API's {searchQuery ? "found" : ''}

    }
    ) -} +} \ No newline at end of file diff --git a/dashboard/src/components/features/documentation/APIDocumentation.tsx b/dashboard/src/components/features/documentation/APIDocumentation.tsx index 4e0bd2b..cb4ee62 100644 --- a/dashboard/src/components/features/documentation/APIDocumentation.tsx +++ b/dashboard/src/components/features/documentation/APIDocumentation.tsx @@ -4,44 +4,37 @@ import { Button } from "@/components/ui/button"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; import { APIData } from "@/types/APIData"; import { useFrappePostCall } from "frappe-react-sdk"; -import { useMemo, useState } from "react"; +import { useCallback, useMemo, useState } from "react"; import { MdOutlineRocketLaunch } from "react-icons/md"; -import Markdown from "react-markdown"; import MDEditor from '@uiw/react-md-editor'; import { FiEdit, FiSave } from "react-icons/fi"; - - -export const Documentation = ({ documentation }: { documentation: string }) => { - - const renderContent = () => { - if (typeof documentation === 'string') { - return {documentation}; - } else if (typeof documentation === 'object' && documentation !== null && !Array.isArray(documentation)) { - return
    {JSON.stringify(documentation, null, 2)}
    ; - } else { - return
    Invalid documentation format
    ; - } - }; - - return ( -
    - {renderContent()} -
    - ) -} +import { isSystemManager } from "@/utils/roles"; +import { IoMdClose } from "react-icons/io"; +import { convertFrappeTimestampToTimeAgo } from "@/components/utils/dateconversion"; export interface DocumentationResponse { function_name: string, path: string, documentation: string } -export const APIDocumentationOfSiteApp = ({ apiData, project_branch, file_path, endPoint }: { apiData: APIData, project_branch: string, file_path: string, endPoint: string }) => { +export const APIDocumentationOfSiteApp = ({ apiData, project_branch, file_path, endPoint, viewerType, mutate }: { apiData: APIData, project_branch: string, file_path: string, endPoint: string, viewerType: string, mutate: () => void }) => { + + const renderContent = () => { + // return string by type checking + if (typeof apiData?.documentation === 'string') { + return apiData.documentation + } else if (typeof apiData?.documentation === 'object' && apiData?.documentation !== null && !Array.isArray(apiData?.documentation)) { + return JSON.stringify(apiData?.documentation, null, 2) + } else { + return '' + } + } const { call, error, loading } = useFrappePostCall('commit.api.generate_documentation.get_documentation_for_api') const [edit, setEdit] = useState(false) - const [documentation, setDocumentation] = useState() + const [documentation, setDocumentation] = useState(renderContent()) const generateDocumentation = () => { call({ @@ -49,74 +42,107 @@ export const APIDocumentationOfSiteApp = ({ apiData, project_branch, file_path, file_path: file_path, block_start: apiData.block_start ?? 0, block_end: apiData.block_end ?? 0, - endpoint: endPoint + endpoint: endPoint, + viewer_type: viewerType }).then((res) => { - setDocumentation(res.message) + setDocumentation(res.message?.documentation ?? '') setEdit(true) }) } const onDocumentationChange = (value: string) => { - setDocumentation((documentation) => { - return { - function_name: documentation?.function_name ?? endPoint?.split('.').pop() ?? '', - path: documentation?.path ?? endPoint, - documentation: value - } - }) + setDocumentation(value) } const previewMode = useMemo(() => { return edit ? 'live' : 'preview' }, [edit]) - const SaveEditButton = () => { + const { call: saveCall } = useFrappePostCall('commit.api.generate_documentation.save_documentation') + + const SaveEditButton = useCallback(() => { if (edit) { // code for saving the documentation + saveCall({ + project_branch: project_branch, + endpoint: endPoint, + documentation: documentation ?? '', + viewer_type: viewerType + }).then(() => { + mutate() + setEdit(false) + }) } else { setEdit(true) } - } + }, [edit, documentation, project_branch, endPoint, viewerType]) + + const isCreateAccess = isSystemManager(); return (
    {error && }
    -
    - - - - - - - Generate Documentation for this API - - - - - - - - - - {edit ? 'Save Documentation' : 'Edit Documentation'} - - - -
    + {apiData?.last_updated ?
    +
    + Last Docs Updated - {convertFrappeTimestampToTimeAgo(apiData?.last_updated)} +
    + {isCreateAccess && } +
    : (isCreateAccess && )} onDocumentationChange(value ?? '')} - style={{ minHeight: 'calc(100vh - 24rem)', overflowY: 'auto', margin: 8, padding: 4 }} + style={{ + minHeight: (apiData?.last_updated || isCreateAccess) ? 'calc(100vh - 24rem)' : 'calc(100vh - 21rem)', + overflowY: 'auto', margin: 8, padding: 4 + }} />
    ) +} + +export const AllButton = ({ generateDocumentation, loading, edit, setEdit, SaveEditButton, renderContent, setDocumentation }: { generateDocumentation: () => void, loading: boolean, edit: boolean, setEdit: (value: boolean) => void, SaveEditButton: () => void, renderContent: () => string, setDocumentation: (value: string | undefined) => void }) => { + + return ( +
    +
    + {!edit && + + + + + + Generate Documentation for this API + + + } + {edit && } + + + + + + + {edit ? 'Save Documentation' : 'Edit Documentation'} + + + +
    +
    + ) } \ No newline at end of file diff --git a/dashboard/src/pages/features/api_viewer/APIViewer.tsx b/dashboard/src/pages/features/api_viewer/APIViewer.tsx index a7a7bd5..288c462 100644 --- a/dashboard/src/pages/features/api_viewer/APIViewer.tsx +++ b/dashboard/src/pages/features/api_viewer/APIViewer.tsx @@ -1,9 +1,9 @@ import { APIDetails } from "@/components/features/api_viewer/APIDetails" import { APIList } from "@/components/features/api_viewer/APIList" -import { useState } from "react" +import { useEffect, useRef, useState } from "react" import { useFrappeGetCall } from "frappe-react-sdk" import { APIData } from "@/types/APIData" -import { useParams } from "react-router-dom" +import { useNavigate, useParams } from "react-router-dom" import { Header } from "@/components/common/Header" import { FullPageLoader } from "@/components/common/FullPageLoader/FullPageLoader" import { ErrorBanner } from "@/components/common/ErrorBanner/ErrorBanner" @@ -33,14 +33,46 @@ export const APIViewerContainer = () => { export const APIViewer = ({ projectBranch }: { projectBranch: string }) => { const [selectedendpoint, setSelectedEndpoint] = useState('') + const navigate = useNavigate() - const { data, isLoading, error } = useFrappeGetCall<{ message: GetAPIResponse }>('commit.api.api_explorer.get_apis_for_project', { - project_branch: projectBranch - }, undefined, { - revalidateOnFocus: false, - revalidateIfStale: false, - onSuccess: (d: { message: GetAPIResponse }) => setSelectedEndpoint(d.message.apis[0].name) - }) + const listRef = useRef(null); + + // Fetch the query parameters from the URL + useEffect(() => { + const searchParams = new URLSearchParams(window.location.search) + const endpointFromURL = searchParams.get('api') + + // Set selected endpoint from URL if available + if (endpointFromURL) { + setSelectedEndpoint(endpointFromURL) + } + }, []) + + // Update the URL search params when selectedEndpoint changes + useEffect(() => { + if (selectedendpoint) { + const searchParams = new URLSearchParams(window.location.search) + searchParams.set('api', selectedendpoint) + navigate({ search: searchParams.toString() }, { replace: true }) + } + }, [selectedendpoint, navigate]) + + const { data, isLoading, error, mutate } = useFrappeGetCall<{ message: GetAPIResponse }>( + 'commit.api.api_explorer.get_apis_for_project', + { + project_branch: projectBranch + }, + undefined, + { + revalidateOnFocus: false, + revalidateIfStale: false, + onSuccess: (d: { message: GetAPIResponse }) => { + if (!selectedendpoint) { + setSelectedEndpoint(d.message.apis[0].name) + } + } + } + ) if (isLoading) { return @@ -51,7 +83,7 @@ export const APIViewer = ({ projectBranch }: { projectBranch: string }) => {
    {error && } {data &&
    -
    +
    { setSelectedEndpoint={setSelectedEndpoint} selectedEndpoint={selectedendpoint} path_to_folder={data?.message.path_to_folder} + listRef={listRef} />
    {selectedendpoint && (
    { selectedEndpoint={selectedendpoint} setSelectedEndpoint={setSelectedEndpoint} viewerType="project" + mutate={mutate} />
    )} diff --git a/dashboard/src/pages/features/api_viewer/AppAPIViewer.tsx b/dashboard/src/pages/features/api_viewer/AppAPIViewer.tsx index 98b1543..e11bd33 100644 --- a/dashboard/src/pages/features/api_viewer/AppAPIViewer.tsx +++ b/dashboard/src/pages/features/api_viewer/AppAPIViewer.tsx @@ -5,8 +5,8 @@ import { APIDetails } from "@/components/features/api_viewer/APIDetails" import { APIList } from "@/components/features/api_viewer/APIList" import { APIData } from "@/types/APIData" import { useFrappeGetCall } from "frappe-react-sdk" -import { useState } from "react" -import { useParams } from "react-router-dom" +import { useEffect, useRef, useState } from "react" +import { useNavigate, useParams } from "react-router-dom" interface GetAPIResponse { apis: APIData[] @@ -25,13 +25,40 @@ export const AppAPIViewerContainer = () => { export const AppAPIViewer = ({ appName }: { appName: string }) => { const [selectedendpoint, setSelectedEndpoint] = useState('') + const navigate = useNavigate() - const { data, isLoading, error } = useFrappeGetCall<{ message: GetAPIResponse }>('commit.api.meta_data.get_apis_for_app', { + const listRef = useRef(null); + + // Fetch the query parameters from the URL + useEffect(() => { + const searchParams = new URLSearchParams(window.location.search) + const endpointFromURL = searchParams.get('api') + + // Set selected endpoint from URL if available + if (endpointFromURL) { + setSelectedEndpoint(endpointFromURL) + } + }, []) + + // Update the URL search params when selectedEndpoint changes + useEffect(() => { + if (selectedendpoint) { + const searchParams = new URLSearchParams(window.location.search) + searchParams.set('api', selectedendpoint) + navigate({ search: searchParams.toString() }, { replace: true }) + } + }, [selectedendpoint, navigate]) + + const { data, isLoading, error, mutate } = useFrappeGetCall<{ message: GetAPIResponse }>('commit.api.meta_data.get_apis_for_app', { app_name: appName }, undefined, { revalidateIfStale: false, revalidateOnFocus: false, - onSuccess: (d: { message: GetAPIResponse }) => setSelectedEndpoint(d.message.apis?.[0]?.name) + onSuccess: (d: { message: GetAPIResponse }) => { + if (!selectedendpoint) { + setSelectedEndpoint(d.message.apis[0].name) + } + } }) if (isLoading) { @@ -44,7 +71,7 @@ export const AppAPIViewer = ({ appName }: { appName: string }) => {
    {error && } {data &&
    -
    +
    { setSelectedEndpoint={setSelectedEndpoint} selectedEndpoint={selectedendpoint} path_to_folder="" + listRef={listRef} />
    @@ -59,9 +87,9 @@ export const AppAPIViewer = ({ appName }: { appName: string }) => { {selectedendpoint && (
    - +
    )}
    } diff --git a/dashboard/src/pages/features/erd/ERDViewer.tsx b/dashboard/src/pages/features/erd/ERDViewer.tsx index 000c5b6..9f4c542 100644 --- a/dashboard/src/pages/features/erd/ERDViewer.tsx +++ b/dashboard/src/pages/features/erd/ERDViewer.tsx @@ -178,7 +178,7 @@ export const ModuleDoctypeListDrawer = ({ open, setOpen, apps, setSelectedApps,
    -
    +
    diff --git a/dashboard/src/types/APIData.ts b/dashboard/src/types/APIData.ts index e982dcb..3fac31a 100644 --- a/dashboard/src/types/APIData.ts +++ b/dashboard/src/types/APIData.ts @@ -13,6 +13,7 @@ export interface APIData { block_start: number block_end: number documentation?: string + last_updated: string } export interface Argument {