Skip to content
1 change: 1 addition & 0 deletions dashboard/src/assets/api.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
341 changes: 341 additions & 0 deletions dashboard/src/assets/erd.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -491,7 +491,7 @@ const DropdownContainer = ({ children, getMenuProps, isOpen, items, isFieldDisab
return (
<ul
className={`absolute w-full shadow-lg border border-gray-200 rounded-md bg-white ${heightAdjust ? 'max-h-[140px]' : 'max-h-80'
} overflow-y-auto p-1.5 mt-1 flex flex-col gap-1 z-10 ${!(isOpen && items?.length) ? 'hidden' : ''}`}
} overflow-y-auto p-1.5 mt-2 flex flex-col gap-1 z-10 ${!(isOpen && items?.length) ? 'hidden' : ''}`}
style={{
boxShadow: '0px 8px 14px rgba(25, 39, 52, 0.08), 0px 2px 6px rgba(25, 39, 52, 0.04)',
}}
Expand Down
28 changes: 28 additions & 0 deletions dashboard/src/components/common/Forms/FormControl/Control.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { FormControl } from '@/components/ui/form'
import { SlotProps } from '@radix-ui/react-slot'
import { useMemo } from 'react'
import { useFormContext } from 'react-hook-form'


export interface ControlProps extends SlotProps {
allowOnSubmit?: boolean
}
/**
* Control is a wrapper around FormControl that adds some extra functionality
**/
export const Control = ({ allowOnSubmit, ...props }: ControlProps) => {
const formContext = useFormContext()
const docstatus = formContext?.watch('docstatus') ?? 0

const isReadOnly = useMemo(() => {
if (props['aria-readonly'] !== undefined) return props['aria-readonly']
//If document is cancelled, do no edit
if (docstatus === 2) return true

// If document is submitted, only edit if allowOnSubmit is true
if (docstatus === 1) return !allowOnSubmit
return false
}, [docstatus, props['aria-readonly'], allowOnSubmit])

return <FormControl {...props} aria-readonly={isReadOnly} />
}
64 changes: 64 additions & 0 deletions dashboard/src/components/common/Forms/FormControl/FormElement.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { useFormState } from 'react-hook-form';
import { Control, ControlProps } from './Control';
import { FormDescription, FormMessage } from '@/components/ui/form';
import { Label } from '../../Label';

interface FormElementProps extends ControlProps {
name: string;
label?: string;
tooltip?: string
}

/**
* FormElement is a wrapper around FormControl that adds some extra functionality
* like showing errors and disabling the control if the docstatus is not 0
* @param name name of the field
* @param label label of the field
* @param children the input to be wrapped
* @example -
* <FormElement name="payment_term_name" label="Payment Term Name" isRequired>
* <Input {...register("payment_term_name", {
* required: 'Payment Term Name is required', maxLength: {
* value: 140,
* message: 'Payment Term Name cannot exceed 140 characters.'
* }
* })}
* isDisabled={isEdit}
* placeholder="Payment Term Name" />
* </FormElement>
**/
export const FormElement = ({ name, label, children, tooltip, ...props }: FormElementProps) => {

const { errors } = useFormState()

/**
* The name can be a path like `items.0.item_code` so we need to split it
* and then get the error message from the errors object
* */
let error: Record<string, any> = errors
const path = name.split('.')

for (let i = 0; i < path.length; i++) {
error = error?.[path[i]]
}

return (
<Control aria-invalid={!!error} {...props}>
<div className='flex flex-col gap-1'>
{label && <div className='flex flex-row gap-1'>
<Label label={label} htmlFor={name} />
{props['aria-required'] && <span className='text-red-500 text-xs' style={{
fontSize: '16px'
}}>*</span>}
</div>
}
{children}
{tooltip && <FormDescription>{tooltip}</FormDescription>}
{error && <FormMessage>
{error.message}
</FormMessage>
}
</div>
</Control>
)
};
2 changes: 2 additions & 0 deletions dashboard/src/components/common/Forms/FormControl/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './Control'
export * from './FormElement'
16 changes: 16 additions & 0 deletions dashboard/src/components/common/Label/Label.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { FormLabel } from "@/components/ui/form"
import { LabelProps } from "@radix-ui/react-label"

interface FormLabelProps extends LabelProps {
label: string
}

export const Label = ({ label, ...props }: FormLabelProps) => {

return (
<FormLabel {...props}>
{label}
</FormLabel>

)
}
1 change: 1 addition & 0 deletions dashboard/src/components/common/Label/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { Label } from './Label'
53 changes: 38 additions & 15 deletions dashboard/src/components/features/meta_apps/YourAppAPIExplorer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@ import { useNavigate } from "react-router-dom"
import { AppsData } from "./YourApps"
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"
import { Button } from "@/components/ui/button"
import { AiOutlineApi } from "react-icons/ai"
import { useCallback, useState } from "react"
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
import { Label } from "@/components/ui/label"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { Card, CardHeader, CardTitle, CardContent, CardFooter } from "@/components/ui/card"
import { MdKeyboardArrowRight } from "react-icons/md"
import api from '../../../assets/api.svg'

export const YourAppAPIExplorer = () => {

Expand All @@ -28,15 +30,36 @@ export const YourAppAPIExplorer = () => {

if (data && data.message) {
return (
<Dialog>
<DialogTrigger asChild>
<Button size='sm' disabled={data.message.length === 0} variant={'outline'}>
<AiOutlineApi className="mr-2" />
API Explorer
</Button>
</DialogTrigger>
<ViewAPIExplorerContent data={data.message} />
</Dialog>
<Card className="flex flex-col sm:flex-row items-start p-2 border rounded-lg w-full h-full shadow-sm bg-white relative">
<div className="flex-grow h-full">
<CardHeader className="pb-4">
<CardTitle>Explore your API's</CardTitle>
</CardHeader>
<CardContent>
<div className="text-sm text-gray-500 sm:mb-8">
Explore and interact with your site installed apps whitelisted API's effortlessly
using our API Explorer.
</div>
</CardContent>

<CardFooter className="absolute bottom-0">
<Dialog>
<DialogTrigger asChild>
<Button size='sm' disabled={data.message?.length === 0} className="rounded-full px-4 pr-2 py-2 shadow-md">
Get Started
<MdKeyboardArrowRight className="ml-2 mr-0 p-0" style={{
fontSize: '1rem'
}} />
</Button>
</DialogTrigger>
<ViewAPIExplorerContent data={data.message} />
</Dialog>
</CardFooter>
</div>
<div className="flex-shrink-0 sm:mb-0 mb-16 p-4 sm:p-0 items-center justify-center">
<img src={api} alt="API" height={'full'} className="w-full rounded-md sm:w-[170px] sm:mr-6 sm:rounded-md" />
</div>
</Card>
)
}
}
Expand All @@ -54,11 +77,11 @@ export const ViewAPIExplorerContent = ({ data }: { data: AppsData[] }) => {
}, [branch, navigate])

return (
<DialogContent className="p-4 px-6">
<DialogContent className="p-6">
<DialogHeader>
<DialogTitle>Select Apps</DialogTitle>
<DialogTitle>Select App</DialogTitle>
<DialogDescription>
Select the apps to view API's
Select the app to view API's
</DialogDescription>
</DialogHeader>
<RadioGroup defaultValue={branch} onValueChange={(value) => setBranch(value)} className="flex flex-col space-y-1" >
Expand Down Expand Up @@ -89,9 +112,9 @@ export const ViewAPIExplorerCard = ({ app }: ViewAPIExplorerProps) => {
<li className="w-full px-2">
<div className="flex items-center justify-between py-2 w-full">
<div className="flex space-x-3 items-center">
<RadioGroupItem value={app.app_name} key={app.app_name} id={`${app.app_name}`} />
<Label htmlFor={`${app.app_name}`} className="flex items-center space-x-3">
<h1 className="text-lg font-medium tracking-normal" >{app.app_name}</h1>
<RadioGroupItem value={app.app_name} key={app.app_name} id={`${app.app_name}`} />
<h1 className="text-[16px] font-medium tracking-normal cursor-pointer" >{app.app_name}</h1>
</Label>
</div>
<Select
Expand Down
78 changes: 33 additions & 45 deletions dashboard/src/components/features/meta_apps/YourApps.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
import { FullPageLoader } from "@/components/common/FullPageLoader/FullPageLoader"
import { Avatar, AvatarFallback } from "@/components/ui/avatar"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardDescription, CardTitle } from "@/components/ui/card"
import { AvatarImage } from "@radix-ui/react-avatar"
import { useFrappeGetCall } from "frappe-react-sdk"
import { useMemo } from "react"
import { BsDatabase } from "react-icons/bs"
import { useNavigate } from "react-router-dom"
import { YourAppAPIExplorer } from "./YourAppAPIExplorer"
import { ViewERDButtonForSiteApps } from "../projects/ViewERDButton"

export interface AppsData {
app_name: string
Expand All @@ -19,7 +16,6 @@ export interface AppsData {
}

export const YourApps = () => {
const navigate = useNavigate()

const { data, error, isLoading } = useFrappeGetCall<{ message: AppsData[] }>('commit.api.meta_data.get_installed_apps', {}, 'get_installed_apps', {
keepPreviousData: true,
Expand All @@ -37,25 +33,24 @@ export const YourApps = () => {

if (data && data.message) {
return (
<div className="mx-auto pl-2 pr-4 h-[calc(100vh-4rem)]">
<div className="flex flex-row items-center space-x-2 gap-2 justify-end">

<div className="flex items-center space-x-2">
<div className="mx-auto pl-2 pr-4 h-full overflow-y-auto pt-2">
<div className="h-full flex flex-col gap-4">
<div className="grid grid-cols-1 gap-6 justify-between sm:grid-cols-2">
<ViewERDButtonForSiteApps />
<YourAppAPIExplorer />
<Button size='sm' onClick={() => {
window.sessionStorage.removeItem('ERDMetaDoctypes')
navigate({
pathname: `/meta-erd/create`,
})
}}>
<BsDatabase className='mr-2' /> View ERD
</Button>
</div>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-5 xl:grid-cols-7 gap-4 py-4">
{data.message.map((app: AppsData) => {
return <AppsCard key={app.app_name} app={app} />
})}
<div className="flex flex-col gap-4">
<div className="flex flex-row items-end justify-between border-b pb-2">
<div className="text-xl font-semibold pt-1">Projects</div>
</div>
<div className="grid sm:grid-cols-2 gap-x-8 pb-4">
{data.message.map((org: AppsData) => {
return (
<AppsCard app={org} key={org.app_name} />
)
})}
</div>
</div>
</div>
</div>
)
Expand All @@ -64,32 +59,25 @@ export const YourApps = () => {


const AppsCard = ({ app }: { app: AppsData }) => {

const appNameInitials = useMemo(() => {
return app.app_name.split('_').map((word) => word[0]).join('').toUpperCase()
}, [app])
return app.app_name.split('_').map((word) => word[0]).join('').toUpperCase();
}, [app]);

return (
<Card>
<CardContent className="flex flex-col gap-4 items-start p-4">
<div className="w-full flex items-center justify-center">
<Avatar className="h-32 w-32 flex items-center rounded-xl border border-gray-100">
<AvatarImage src={app.app_logo_url} className="object-contain h-full w-full" />
<AvatarFallback className="rounded-xl text-4xl">{appNameInitials}</AvatarFallback>
<div className="flex items-start border-b relative hover:bg-gray-100 p-4">
<div className="flex items-start gap-4">
<Avatar className="h-10 w-10 flex items-center rounded-lg">
<AvatarImage src={app.app_logo_url} className="object-contain h-full w-full" />
<AvatarFallback className="rounded-lg text-xl">{appNameInitials}</AvatarFallback>
</Avatar>
</div>
<div className="flex flex-col gap-2 w-full">
<div className=" flex flex-col gap-1">
<CardTitle>{app.app_name}</CardTitle>
<div className="text-xs text-gray-500">
{app.app_publisher}
<div className="flex flex-col gap-1 cursor-default">
<div className="text-base font-semibold">{app.app_name}</div>
<div className="text-sm text-gray-500 pb-4">
{app.app_description}
</div>
<div className="text-xs absolute bottom-2 text-gray-400">{app.app_publisher}</div>
</div>
<CardDescription>
{app.app_description}
</CardDescription>
</div>
</CardContent>
</Card>
)
}
</div>
</div>
);
};
Loading