mirror of
https://github.com/onyx-dot-app/onyx.git
synced 2026-03-27 10:32:41 +00:00
Compare commits
2 Commits
v3.1.0
...
refactor/c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e5f5f35039 | ||
|
|
ef0887e70e |
@@ -53,7 +53,7 @@ import {
|
||||
getConnectorOauthRedirectUrl,
|
||||
useOAuthDetails,
|
||||
} from "@/lib/connectors/oauth";
|
||||
import { CreateStdOAuthCredential } from "@/components/credentials/actions/CreateStdOAuthCredential";
|
||||
import CreateStdOAuthCredential from "@/components/credentials/actions/CreateStdOAuthCredential";
|
||||
import { Spinner } from "@/components/Spinner";
|
||||
import Button from "@/refresh-components/buttons/Button";
|
||||
import { deleteConnector } from "@/lib/connector";
|
||||
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
import { deleteCredential } from "@/lib/credential";
|
||||
import ModifyCredential from "@/components/credentials/actions/ModifyCredential";
|
||||
import CreateCredential from "@/components/credentials/actions/CreateCredential";
|
||||
import { CreateStdOAuthCredential } from "@/components/credentials/actions/CreateStdOAuthCredential";
|
||||
import CreateStdOAuthCredential from "@/components/credentials/actions/CreateStdOAuthCredential";
|
||||
import { GmailMain } from "@/app/admin/connectors/[connector]/pages/gmail/GmailPage";
|
||||
import CardSection from "@/components/admin/CardSection";
|
||||
import { Spinner } from "@/components/Spinner";
|
||||
|
||||
@@ -3,9 +3,7 @@
|
||||
import { AccessType, ValidSources } from "@/lib/types";
|
||||
import useSWR, { mutate } from "swr";
|
||||
import { errorHandlingFetcher } from "@/lib/fetcher";
|
||||
import { FaKey } from "react-icons/fa";
|
||||
import { useState } from "react";
|
||||
import { FiEdit2 } from "react-icons/fi";
|
||||
import {
|
||||
deleteCredential,
|
||||
swapCredential,
|
||||
@@ -16,7 +14,6 @@ import { toast } from "@/hooks/useToast";
|
||||
import CreateCredential from "./actions/CreateCredential";
|
||||
import { CCPairFullInfo } from "@/app/admin/connector/[ccPairId]/types";
|
||||
import ModifyCredential from "./actions/ModifyCredential";
|
||||
import Text from "@/components/ui/text";
|
||||
import {
|
||||
buildCCPairInfoUrl,
|
||||
buildSimilarCredentialInfoURL,
|
||||
@@ -33,10 +30,12 @@ import {
|
||||
useOAuthDetails,
|
||||
} from "@/lib/connectors/oauth";
|
||||
import { Spinner } from "@/components/Spinner";
|
||||
import { CreateStdOAuthCredential } from "@/components/credentials/actions/CreateStdOAuthCredential";
|
||||
import { Card } from "../ui/card";
|
||||
import CreateStdOAuthCredential from "@/components/credentials/actions/CreateStdOAuthCredential";
|
||||
import Card from "@/refresh-components/cards/Card";
|
||||
import { isTypedFileField, TypedFile } from "@/lib/connectors/fileTypes";
|
||||
import { SvgEdit, SvgKey } from "@opal/icons";
|
||||
import { LineItemLayout } from "@/layouts/general-layouts";
|
||||
import { Button } from "@opal/components";
|
||||
|
||||
export interface CredentialSectionProps {
|
||||
ccPair: CCPairFullInfo;
|
||||
@@ -170,88 +169,30 @@ export default function CredentialSection({
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex
|
||||
flex-col
|
||||
gap-y-4
|
||||
rounded-lg
|
||||
bg-background"
|
||||
>
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center">
|
||||
<div className="flex-shrink-0 mr-3">
|
||||
<FaKey className="h-4 w-4 text-muted-foreground" />
|
||||
</div>
|
||||
<div className="flex-grow flex flex-col justify-center">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<Text className="font-medium">
|
||||
{ccPair.credential.name ||
|
||||
`Credential #${ccPair.credential.id}`}
|
||||
</Text>
|
||||
<div className="text-xs text-muted-foreground/70">
|
||||
Created{" "}
|
||||
<i>
|
||||
{new Date(
|
||||
ccPair.credential.time_created
|
||||
).toLocaleDateString(undefined, {
|
||||
year: "numeric",
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
})}
|
||||
</i>
|
||||
{ccPair.credential.user_email && (
|
||||
<>
|
||||
{" "}
|
||||
by <i>{ccPair.credential.user_email}</i>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setShowModifyCredential(true)}
|
||||
className="inline-flex
|
||||
items-center
|
||||
justify-center
|
||||
p-2
|
||||
rounded-md
|
||||
text-muted-foreground
|
||||
hover:bg-accent
|
||||
hover:text-accent-foreground
|
||||
transition-colors"
|
||||
>
|
||||
<FiEdit2 className="h-4 w-4" />
|
||||
<span className="sr-only">Update Credentials</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<>
|
||||
{showModifyCredential && (
|
||||
<Modal open onOpenChange={closeModifyCredential}>
|
||||
<Modal.Content>
|
||||
<Modal.Header
|
||||
icon={SvgEdit}
|
||||
title="Update Credentials"
|
||||
description="Select a credential as needed! Ensure that you have selected a credential with the proper permissions for this connector!"
|
||||
onClose={closeModifyCredential}
|
||||
/>
|
||||
<Modal.Body>
|
||||
<ModifyCredential
|
||||
close={closeModifyCredential}
|
||||
accessType={ccPair.access_type}
|
||||
attachedConnector={ccPair.connector}
|
||||
defaultedCredential={defaultedCredential}
|
||||
credentials={credentials}
|
||||
editableCredentials={editableCredentials}
|
||||
onDeleteCredential={onDeleteCredential}
|
||||
onEditCredential={(credential: Credential<any>) =>
|
||||
onEditCredential(credential)
|
||||
}
|
||||
onSwap={onSwap}
|
||||
onCreateNew={() => makeShowCreateCredential()}
|
||||
/>
|
||||
</Modal.Body>
|
||||
<ModifyCredential
|
||||
close={closeModifyCredential}
|
||||
accessType={ccPair.access_type}
|
||||
attachedConnector={ccPair.connector}
|
||||
defaultedCredential={defaultedCredential}
|
||||
credentials={credentials}
|
||||
editableCredentials={editableCredentials}
|
||||
onDeleteCredential={onDeleteCredential}
|
||||
onEditCredential={(credential: Credential<any>) =>
|
||||
onEditCredential(credential)
|
||||
}
|
||||
onSwap={onSwap}
|
||||
onCreateNew={() => makeShowCreateCredential()}
|
||||
/>
|
||||
</Modal.Content>
|
||||
</Modal>
|
||||
)}
|
||||
@@ -262,15 +203,14 @@ export default function CredentialSection({
|
||||
<Modal.Header
|
||||
icon={SvgEdit}
|
||||
title="Edit Credential"
|
||||
description="Ensure that you update to a credential with the proper permissions!"
|
||||
onClose={closeEditingCredential}
|
||||
/>
|
||||
<EditCredential
|
||||
onUpdate={onUpdateCredential}
|
||||
credential={editingCredential}
|
||||
onClose={closeEditingCredential}
|
||||
/>
|
||||
<Modal.Body>
|
||||
<EditCredential
|
||||
onUpdate={onUpdateCredential}
|
||||
credential={editingCredential}
|
||||
onClose={closeEditingCredential}
|
||||
/>
|
||||
</Modal.Body>
|
||||
</Modal.Content>
|
||||
</Modal>
|
||||
)}
|
||||
@@ -308,6 +248,34 @@ export default function CredentialSection({
|
||||
</Modal.Content>
|
||||
</Modal>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Card padding={0.5}>
|
||||
<LineItemLayout
|
||||
icon={SvgKey}
|
||||
title={
|
||||
ccPair.credential.name || `Credential #${ccPair.credential.id}`
|
||||
}
|
||||
description={`Created ${new Date(
|
||||
ccPair.credential.time_created
|
||||
).toLocaleDateString(undefined, {
|
||||
year: "numeric",
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
})}${
|
||||
ccPair.credential.user_email
|
||||
? ` by ${ccPair.credential.user_email}`
|
||||
: ""
|
||||
}`}
|
||||
rightChildren={
|
||||
<Button
|
||||
icon={SvgEdit}
|
||||
prominence="tertiary"
|
||||
onClick={() => setShowModifyCredential(true)}
|
||||
/>
|
||||
}
|
||||
reducedPadding
|
||||
/>
|
||||
</Card>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,17 +1,24 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import Button from "@/refresh-components/buttons/Button";
|
||||
import { ValidSources, AccessType } from "@/lib/types";
|
||||
import { FaAccusoft } from "react-icons/fa";
|
||||
import { submitCredential } from "@/components/admin/connectors/CredentialForm";
|
||||
import { TextFormField } from "@/components/Field";
|
||||
import { Form, Formik, FormikHelpers } from "formik";
|
||||
import { toast } from "@/hooks/useToast";
|
||||
import GDriveMain from "@/app/admin/connectors/[connector]/pages/gdrive/GoogleDrivePage";
|
||||
import { Connector } from "@/lib/connectors/connectors";
|
||||
import { Credential, credentialTemplates } from "@/lib/connectors/credentials";
|
||||
import {
|
||||
Credential,
|
||||
credentialTemplates,
|
||||
getDisplayNameForCredentialKey,
|
||||
CredentialTemplateWithAuth,
|
||||
} from "@/lib/connectors/credentials";
|
||||
import { GmailMain } from "@/app/admin/connectors/[connector]/pages/gmail/GmailPage";
|
||||
import { ActionType, dictionaryType } from "../types";
|
||||
import { createValidationSchema } from "../lib";
|
||||
import { ActionType, dictionaryType } from "@/components/credentials/types";
|
||||
import {
|
||||
createValidationSchema,
|
||||
isOptionalCredentialField,
|
||||
} from "@/components/credentials/lib";
|
||||
import { usePaidEnterpriseFeaturesEnabled } from "@/components/settings/usePaidEnterpriseFeaturesEnabled";
|
||||
import { AdvancedOptionsToggle } from "@/components/AdvancedOptionsToggle";
|
||||
import {
|
||||
@@ -19,49 +26,23 @@ import {
|
||||
IsPublicGroupSelector,
|
||||
} from "@/components/IsPublicGroupSelector";
|
||||
import { useUser } from "@/providers/UserProvider";
|
||||
import CardSection from "@/components/admin/CardSection";
|
||||
import { CredentialFieldsRenderer } from "./CredentialFieldsRenderer";
|
||||
import { TypedFile } from "@/lib/connectors/fileTypes";
|
||||
import { isTypedFileField, TypedFile } from "@/lib/connectors/fileTypes";
|
||||
import { BooleanFormField, TypedFileUploadFormField } from "@/components/Field";
|
||||
import ConnectorDocsLink from "@/components/admin/connectors/ConnectorDocsLink";
|
||||
import Tabs from "@/refresh-components/Tabs";
|
||||
import Text from "@/refresh-components/texts/Text";
|
||||
import { SvgPlusCircle } from "@opal/icons";
|
||||
const CreateButton = ({
|
||||
onClick,
|
||||
isSubmitting,
|
||||
isAdmin,
|
||||
groups,
|
||||
}: {
|
||||
onClick: () => void;
|
||||
isSubmitting: boolean;
|
||||
isAdmin: boolean;
|
||||
groups: number[];
|
||||
}) => (
|
||||
<Button
|
||||
onClick={onClick}
|
||||
disabled={isSubmitting || (!isAdmin && groups.length === 0)}
|
||||
leftIcon={SvgPlusCircle}
|
||||
>
|
||||
Create
|
||||
</Button>
|
||||
);
|
||||
import { Button } from "@opal/components";
|
||||
import * as InputLayouts from "@/layouts/input-layouts";
|
||||
import InputTypeInField from "@/refresh-components/form/InputTypeInField";
|
||||
|
||||
type formType = IsPublicGroupSelectorFormType & {
|
||||
name: string;
|
||||
[key: string]: any; // For additional credential fields
|
||||
};
|
||||
|
||||
export default function CreateCredential({
|
||||
hideSource,
|
||||
sourceType,
|
||||
accessType,
|
||||
close,
|
||||
onClose = () => null,
|
||||
onSwitch,
|
||||
onSwap = async () => null,
|
||||
swapConnector,
|
||||
refresh = () => null,
|
||||
}: {
|
||||
export interface CreateCredentialProps {
|
||||
// Source information
|
||||
hideSource?: boolean; // hides docs link
|
||||
sourceType: ValidSources;
|
||||
accessType: AccessType;
|
||||
|
||||
@@ -84,7 +65,18 @@ export default function CreateCredential({
|
||||
|
||||
// Mutating parent state
|
||||
refresh?: () => void;
|
||||
}) {
|
||||
}
|
||||
|
||||
export default function CreateCredential({
|
||||
sourceType,
|
||||
accessType,
|
||||
close,
|
||||
onClose = () => null,
|
||||
onSwitch,
|
||||
onSwap = async () => null,
|
||||
swapConnector,
|
||||
refresh = () => null,
|
||||
}: CreateCredentialProps) {
|
||||
const [showAdvancedOptions, setShowAdvancedOptions] = useState(false);
|
||||
const [authMethod, setAuthMethod] = useState<string>();
|
||||
const isPaidEnterpriseFeaturesEnabled = usePaidEnterpriseFeaturesEnabled();
|
||||
@@ -208,64 +200,222 @@ export default function CreateCredential({
|
||||
formikProps.setFieldValue("authentication_method", authMethod);
|
||||
}
|
||||
|
||||
const templateWithAuth =
|
||||
credentialTemplate as CredentialTemplateWithAuth<any>;
|
||||
const hasMultipleAuthMethods =
|
||||
templateWithAuth.authMethods &&
|
||||
templateWithAuth.authMethods.length > 1;
|
||||
|
||||
const handleAuthMethodChange = (newMethod: string) => {
|
||||
const cleaned: Record<string, any> = {
|
||||
...formikProps.values,
|
||||
authentication_method: newMethod,
|
||||
};
|
||||
templateWithAuth.authMethods?.forEach((m) => {
|
||||
if (m.value !== newMethod) {
|
||||
Object.keys(m.fields).forEach((fieldKey) => {
|
||||
delete cleaned[fieldKey];
|
||||
});
|
||||
}
|
||||
});
|
||||
formikProps.setValues(cleaned as typeof formikProps.values);
|
||||
setAuthMethod(newMethod);
|
||||
};
|
||||
|
||||
const currentAuthMethod = authMethod || initialAuthMethod;
|
||||
|
||||
return (
|
||||
<Form className="w-full flex items-stretch">
|
||||
{!hideSource && <ConnectorDocsLink sourceType={sourceType} />}
|
||||
<CardSection className="w-full items-start dark:bg-neutral-900 mt-4 flex flex-col gap-y-6">
|
||||
<TextFormField
|
||||
<Form className="w-full flex flex-col gap-4">
|
||||
<ConnectorDocsLink sourceType={sourceType} />
|
||||
|
||||
<InputLayouts.Vertical name="name" title="Name" optional>
|
||||
<InputTypeInField
|
||||
name="name"
|
||||
placeholder="(Optional) credential name.."
|
||||
label="Name:"
|
||||
placeholder="Name your credential"
|
||||
/>
|
||||
</InputLayouts.Vertical>
|
||||
|
||||
<CredentialFieldsRenderer
|
||||
credentialTemplate={credentialTemplate}
|
||||
authMethod={authMethod || initialAuthMethod}
|
||||
setAuthMethod={setAuthMethod}
|
||||
/>
|
||||
{hasMultipleAuthMethods && templateWithAuth.authMethods ? (
|
||||
<div className="w-full space-y-4">
|
||||
<input
|
||||
type="hidden"
|
||||
name="authentication_method"
|
||||
value={
|
||||
currentAuthMethod ||
|
||||
(templateWithAuth.authMethods?.[0]?.value ?? "")
|
||||
}
|
||||
/>
|
||||
|
||||
{!swapConnector && (
|
||||
<div className="mt-4 flex w-full flex-col sm:flex-row justify-between items-end">
|
||||
<div className="w-full sm:w-3/4 mb-4 sm:mb-0">
|
||||
{isPaidEnterpriseFeaturesEnabled && (
|
||||
<div className="flex flex-col items-start">
|
||||
{isAdmin && (
|
||||
<AdvancedOptionsToggle
|
||||
showAdvancedOptions={showAdvancedOptions}
|
||||
setShowAdvancedOptions={setShowAdvancedOptions}
|
||||
/>
|
||||
<Tabs
|
||||
value={
|
||||
currentAuthMethod ||
|
||||
templateWithAuth.authMethods?.[0]?.value ||
|
||||
""
|
||||
}
|
||||
onValueChange={handleAuthMethodChange}
|
||||
>
|
||||
<Tabs.List>
|
||||
{templateWithAuth.authMethods.map((method) => (
|
||||
<Tabs.Trigger key={method.value} value={method.value}>
|
||||
{method.label}
|
||||
</Tabs.Trigger>
|
||||
))}
|
||||
</Tabs.List>
|
||||
|
||||
{templateWithAuth.authMethods.map((method) => (
|
||||
<Tabs.Content
|
||||
key={method.value}
|
||||
value={method.value}
|
||||
alignItems="stretch"
|
||||
>
|
||||
{Object.keys(method.fields).length === 0 &&
|
||||
method.description && (
|
||||
<div className="p-4 bg-background-tint-02 border border-border-02 rounded-md">
|
||||
<Text secondaryBody text03>
|
||||
{method.description}
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
{(showAdvancedOptions || !isAdmin) && (
|
||||
<IsPublicGroupSelector
|
||||
formikProps={formikProps}
|
||||
objectName="credential"
|
||||
publicToWhom="Curators"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<CreateButton
|
||||
onClick={() =>
|
||||
handleSubmit(formikProps.values, formikProps, "create")
|
||||
}
|
||||
isSubmitting={formikProps.isSubmitting}
|
||||
isAdmin={isAdmin}
|
||||
groups={formikProps.values.groups}
|
||||
/>
|
||||
|
||||
{Object.entries(method.fields).map(([key, val]) => {
|
||||
if (isTypedFileField(key)) {
|
||||
return (
|
||||
<TypedFileUploadFormField
|
||||
key={key}
|
||||
name={key}
|
||||
label={getDisplayNameForCredentialKey(key)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (typeof val === "boolean") {
|
||||
return (
|
||||
<BooleanFormField
|
||||
key={key}
|
||||
name={key}
|
||||
label={getDisplayNameForCredentialKey(key)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const inputType =
|
||||
key.toLowerCase().includes("token") ||
|
||||
key.toLowerCase().includes("password") ||
|
||||
key.toLowerCase().includes("secret")
|
||||
? "password"
|
||||
: "text";
|
||||
|
||||
return (
|
||||
<InputLayouts.Vertical
|
||||
key={key}
|
||||
name={key}
|
||||
title={getDisplayNameForCredentialKey(key)}
|
||||
optional={isOptionalCredentialField(val)}
|
||||
>
|
||||
<InputTypeInField
|
||||
name={key}
|
||||
placeholder={val}
|
||||
type={inputType}
|
||||
/>
|
||||
</InputLayouts.Vertical>
|
||||
);
|
||||
})}
|
||||
</Tabs.Content>
|
||||
))}
|
||||
</Tabs>
|
||||
</div>
|
||||
) : (
|
||||
Object.entries(credentialTemplate).map(([key, val]) => {
|
||||
if (key === "authentication_method" || key === "authMethods") {
|
||||
return null;
|
||||
}
|
||||
if (isTypedFileField(key)) {
|
||||
return (
|
||||
<TypedFileUploadFormField
|
||||
key={key}
|
||||
name={key}
|
||||
label={getDisplayNameForCredentialKey(key)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (typeof val === "boolean") {
|
||||
return (
|
||||
<BooleanFormField
|
||||
key={key}
|
||||
name={key}
|
||||
label={getDisplayNameForCredentialKey(key)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const inputType =
|
||||
key.toLowerCase().includes("token") ||
|
||||
key.toLowerCase().includes("password") ||
|
||||
key.toLowerCase().includes("secret")
|
||||
? "password"
|
||||
: "text";
|
||||
|
||||
return (
|
||||
<InputLayouts.Vertical
|
||||
key={key}
|
||||
name={key}
|
||||
title={getDisplayNameForCredentialKey(key)}
|
||||
optional={isOptionalCredentialField(val)}
|
||||
>
|
||||
<InputTypeInField
|
||||
name={key}
|
||||
placeholder={val as string}
|
||||
type={inputType}
|
||||
/>
|
||||
</InputLayouts.Vertical>
|
||||
);
|
||||
})
|
||||
)}
|
||||
|
||||
{!swapConnector && (
|
||||
<div className="mt-4 flex w-full flex-col sm:flex-row justify-between items-end">
|
||||
<div className="w-full sm:w-3/4 mb-4 sm:mb-0">
|
||||
{isPaidEnterpriseFeaturesEnabled && (
|
||||
<div className="flex flex-col items-start">
|
||||
{isAdmin && (
|
||||
<AdvancedOptionsToggle
|
||||
showAdvancedOptions={showAdvancedOptions}
|
||||
setShowAdvancedOptions={setShowAdvancedOptions}
|
||||
/>
|
||||
)}
|
||||
{(showAdvancedOptions || !isAdmin) && (
|
||||
<IsPublicGroupSelector
|
||||
formikProps={formikProps}
|
||||
objectName="credential"
|
||||
publicToWhom="Curators"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</CardSection>
|
||||
<Button
|
||||
onClick={() =>
|
||||
handleSubmit(formikProps.values, formikProps, "create")
|
||||
}
|
||||
disabled={
|
||||
formikProps.isSubmitting ||
|
||||
(!isAdmin && formikProps.values.groups.length === 0)
|
||||
}
|
||||
icon={SvgPlusCircle}
|
||||
>
|
||||
Create
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{swapConnector && (
|
||||
<Button
|
||||
className="bg-rose-500 hover:bg-rose-400"
|
||||
onClick={() =>
|
||||
handleSubmit(formikProps.values, formikProps, "createAndSwap")
|
||||
}
|
||||
disabled={formikProps.isSubmitting}
|
||||
leftIcon={() => (
|
||||
<FaAccusoft className="fill-text-inverted-05" />
|
||||
)}
|
||||
icon={SvgPlusCircle}
|
||||
>
|
||||
Create
|
||||
</Button>
|
||||
|
||||
@@ -1,30 +1,33 @@
|
||||
import * as Yup from "yup";
|
||||
"use client";
|
||||
|
||||
import Button from "@/refresh-components/buttons/Button";
|
||||
import * as Yup from "yup";
|
||||
import { ValidSources } from "@/lib/types";
|
||||
import { TextFormField } from "@/components/Field";
|
||||
import { Form, Formik, FormikHelpers } from "formik";
|
||||
import CardSection from "@/components/admin/CardSection";
|
||||
import { getConnectorOauthRedirectUrl } from "@/lib/connectors/oauth";
|
||||
import { OAuthAdditionalKwargDescription } from "@/lib/connectors/credentials";
|
||||
import { Button } from "@opal/components";
|
||||
import { SvgPlusCircle } from "@opal/icons";
|
||||
import * as InputLayouts from "@/layouts/input-layouts";
|
||||
import * as GeneralLayouts from "@/layouts/general-layouts";
|
||||
import InputTypeInField from "@/refresh-components/form/InputTypeInField";
|
||||
|
||||
type formType = {
|
||||
[key: string]: any; // For additional credential fields
|
||||
[key: string]: any;
|
||||
};
|
||||
|
||||
export function CreateStdOAuthCredential({
|
||||
export interface CreateStdOAuthCredentialProps {
|
||||
sourceType: ValidSources;
|
||||
additionalFields: OAuthAdditionalKwargDescription[];
|
||||
}
|
||||
|
||||
export default function CreateStdOAuthCredential({
|
||||
sourceType,
|
||||
additionalFields,
|
||||
}: {
|
||||
// Source information
|
||||
sourceType: ValidSources;
|
||||
|
||||
additionalFields: OAuthAdditionalKwargDescription[];
|
||||
}) {
|
||||
const handleSubmit = async (
|
||||
}: CreateStdOAuthCredentialProps) {
|
||||
async function handleSubmit(
|
||||
values: formType,
|
||||
formikHelpers: FormikHelpers<formType>
|
||||
) => {
|
||||
) {
|
||||
const { setSubmitting, validateForm } = formikHelpers;
|
||||
|
||||
const errors = await validateForm(values);
|
||||
@@ -34,7 +37,6 @@ export function CreateStdOAuthCredential({
|
||||
}
|
||||
|
||||
setSubmitting(true);
|
||||
formikHelpers.setSubmitting(true);
|
||||
|
||||
const redirectUrl = await getConnectorOauthRedirectUrl(sourceType, values);
|
||||
|
||||
@@ -43,7 +45,7 @@ export function CreateStdOAuthCredential({
|
||||
}
|
||||
|
||||
window.location.href = redirectUrl;
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<Formik
|
||||
@@ -57,27 +59,26 @@ export function CreateStdOAuthCredential({
|
||||
additionalFields.map((field) => [field.name, Yup.string().required()])
|
||||
),
|
||||
})}
|
||||
onSubmit={(values, formikHelpers) => {
|
||||
handleSubmit(values, formikHelpers);
|
||||
}}
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
{() => (
|
||||
<Form className="w-full flex items-stretch">
|
||||
<CardSection className="w-full !border-0 mt-4 flex flex-col gap-y-6">
|
||||
<Form>
|
||||
<GeneralLayouts.Section>
|
||||
{additionalFields.map((field) => (
|
||||
<TextFormField
|
||||
<InputLayouts.Vertical
|
||||
key={field.name}
|
||||
name={field.name}
|
||||
label={field.display_name}
|
||||
subtext={field.description}
|
||||
type="text"
|
||||
/>
|
||||
title={field.display_name}
|
||||
description={field.description}
|
||||
>
|
||||
<InputTypeInField name={field.name} />
|
||||
</InputLayouts.Vertical>
|
||||
))}
|
||||
|
||||
<div className="flex w-full">
|
||||
<Button type="submit">Create</Button>
|
||||
</div>
|
||||
</CardSection>
|
||||
<Button type="submit" icon={SvgPlusCircle}>
|
||||
Create
|
||||
</Button>
|
||||
</GeneralLayouts.Section>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
|
||||
@@ -1,178 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import Tabs from "@/refresh-components/Tabs";
|
||||
import { useFormikContext } from "formik";
|
||||
import {
|
||||
BooleanFormField,
|
||||
TextFormField,
|
||||
TypedFileUploadFormField,
|
||||
} from "@/components/Field";
|
||||
import {
|
||||
getDisplayNameForCredentialKey,
|
||||
CredentialTemplateWithAuth,
|
||||
} from "@/lib/connectors/credentials";
|
||||
import { dictionaryType } from "../types";
|
||||
import { isTypedFileField } from "@/lib/connectors/fileTypes";
|
||||
|
||||
interface CredentialFieldsRendererProps {
|
||||
credentialTemplate: dictionaryType;
|
||||
authMethod?: string;
|
||||
setAuthMethod?: (method: string) => void;
|
||||
}
|
||||
|
||||
export function CredentialFieldsRenderer({
|
||||
credentialTemplate,
|
||||
authMethod,
|
||||
setAuthMethod,
|
||||
}: CredentialFieldsRendererProps) {
|
||||
const templateWithAuth =
|
||||
credentialTemplate as CredentialTemplateWithAuth<any>;
|
||||
const { values, setValues } = useFormikContext<any>();
|
||||
|
||||
// remove other auth‐method fields when switching
|
||||
const handleAuthMethodChange = (newMethod: string) => {
|
||||
// start from current form values
|
||||
const cleaned = { ...values, authentication_method: newMethod };
|
||||
// delete every field not in the selected auth method
|
||||
templateWithAuth.authMethods?.forEach((m) => {
|
||||
if (m.value !== newMethod) {
|
||||
Object.keys(m.fields).forEach((fieldKey) => {
|
||||
delete cleaned[fieldKey];
|
||||
});
|
||||
}
|
||||
});
|
||||
setValues(cleaned);
|
||||
setAuthMethod?.(newMethod);
|
||||
};
|
||||
|
||||
// Check if this credential template has multiple auth methods
|
||||
const hasMultipleAuthMethods =
|
||||
templateWithAuth.authMethods && templateWithAuth.authMethods.length > 1;
|
||||
|
||||
if (hasMultipleAuthMethods && templateWithAuth.authMethods) {
|
||||
return (
|
||||
<div className="w-full space-y-4">
|
||||
{/* Render authentication_method as a hidden field */}
|
||||
<input
|
||||
type="hidden"
|
||||
name="authentication_method"
|
||||
value={authMethod || (templateWithAuth.authMethods?.[0]?.value ?? "")}
|
||||
/>
|
||||
|
||||
<Tabs
|
||||
value={authMethod || templateWithAuth.authMethods?.[0]?.value || ""}
|
||||
onValueChange={handleAuthMethodChange}
|
||||
>
|
||||
<Tabs.List>
|
||||
{templateWithAuth.authMethods.map((method) => (
|
||||
<Tabs.Trigger key={method.value} value={method.value}>
|
||||
{method.label}
|
||||
</Tabs.Trigger>
|
||||
))}
|
||||
</Tabs.List>
|
||||
|
||||
{templateWithAuth.authMethods.map((method) => (
|
||||
<Tabs.Content
|
||||
key={method.value}
|
||||
value={method.value}
|
||||
alignItems="stretch"
|
||||
>
|
||||
{/* Show description if method has no fields but has a description */}
|
||||
{Object.keys(method.fields).length === 0 &&
|
||||
method.description && (
|
||||
<div className="p-4 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-700 rounded-md">
|
||||
<p className="text-sm text-blue-800 dark:text-blue-200">
|
||||
{method.description}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{Object.entries(method.fields).map(([key, val]) => {
|
||||
if (isTypedFileField(key)) {
|
||||
return (
|
||||
<TypedFileUploadFormField
|
||||
key={key}
|
||||
name={key}
|
||||
label={getDisplayNameForCredentialKey(key)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (typeof val === "boolean") {
|
||||
return (
|
||||
<BooleanFormField
|
||||
key={key}
|
||||
name={key}
|
||||
label={getDisplayNameForCredentialKey(key)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<TextFormField
|
||||
key={key}
|
||||
name={key}
|
||||
placeholder={val}
|
||||
label={getDisplayNameForCredentialKey(key)}
|
||||
type={
|
||||
key.toLowerCase().includes("token") ||
|
||||
key.toLowerCase().includes("password") ||
|
||||
key.toLowerCase().includes("secret")
|
||||
? "password"
|
||||
: "text"
|
||||
}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Tabs.Content>
|
||||
))}
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Render single auth method fields (existing behavior)
|
||||
return (
|
||||
<>
|
||||
{Object.entries(credentialTemplate).map(([key, val]) => {
|
||||
// Skip auth method metadata fields
|
||||
if (key === "authentication_method" || key === "authMethods") {
|
||||
return null;
|
||||
}
|
||||
if (isTypedFileField(key)) {
|
||||
return (
|
||||
<TypedFileUploadFormField
|
||||
key={key}
|
||||
name={key}
|
||||
label={getDisplayNameForCredentialKey(key)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (typeof val === "boolean") {
|
||||
return (
|
||||
<BooleanFormField
|
||||
key={key}
|
||||
name={key}
|
||||
label={getDisplayNameForCredentialKey(key)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<TextFormField
|
||||
key={key}
|
||||
name={key}
|
||||
placeholder={val as string}
|
||||
label={getDisplayNameForCredentialKey(key)}
|
||||
type={
|
||||
key.toLowerCase().includes("token") ||
|
||||
key.toLowerCase().includes("password") ||
|
||||
key.toLowerCase().includes("secret")
|
||||
? "password"
|
||||
: "text"
|
||||
}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,18 +1,25 @@
|
||||
import Button from "@/refresh-components/buttons/Button";
|
||||
import Text from "@/components/ui/text";
|
||||
"use client";
|
||||
|
||||
import { FaNewspaper, FaTrash } from "react-icons/fa";
|
||||
import { TextFormField, TypedFileUploadFormField } from "@/components/Field";
|
||||
import Modal, { BasicModalFooter } from "@/refresh-components/Modal";
|
||||
import { TypedFileUploadFormField } from "@/components/Field";
|
||||
import { Form, Formik, FormikHelpers } from "formik";
|
||||
import { toast } from "@/hooks/useToast";
|
||||
import {
|
||||
Credential,
|
||||
getDisplayNameForCredentialKey,
|
||||
} from "@/lib/connectors/credentials";
|
||||
import { createEditingValidationSchema, createInitialValues } from "../lib";
|
||||
import { dictionaryType, formType } from "../types";
|
||||
import {
|
||||
createEditingValidationSchema,
|
||||
createInitialValues,
|
||||
} from "@/components/credentials/lib";
|
||||
import { dictionaryType, formType } from "@/components/credentials/types";
|
||||
import { isTypedFileField } from "@/lib/connectors/fileTypes";
|
||||
import { SvgTrash } from "@opal/icons";
|
||||
import { SvgCheck, SvgTrash } from "@opal/icons";
|
||||
import { Button } from "@opal/components";
|
||||
import * as InputLayouts from "@/layouts/input-layouts";
|
||||
import * as GeneralLayouts from "@/layouts/general-layouts";
|
||||
import InputTypeInField from "@/refresh-components/form/InputTypeInField";
|
||||
|
||||
export interface EditCredentialProps {
|
||||
credential: Credential<dictionaryType>;
|
||||
onClose: () => void;
|
||||
@@ -33,10 +40,10 @@ export default function EditCredential({
|
||||
);
|
||||
const initialValues = createInitialValues(credential);
|
||||
|
||||
const handleSubmit = async (
|
||||
async function handleSubmit(
|
||||
values: formType,
|
||||
formikHelpers: FormikHelpers<formType>
|
||||
) => {
|
||||
) {
|
||||
formikHelpers.setSubmitting(true);
|
||||
try {
|
||||
await onUpdate(credential, values, onClose);
|
||||
@@ -46,68 +53,88 @@ export default function EditCredential({
|
||||
} finally {
|
||||
formikHelpers.setSubmitting(false);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-y-6">
|
||||
<Text>
|
||||
Ensure that you update to a credential with the proper permissions!
|
||||
</Text>
|
||||
|
||||
<Formik
|
||||
initialValues={initialValues}
|
||||
validationSchema={validationSchema}
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
{({ isSubmitting, resetForm }) => (
|
||||
<Form>
|
||||
<TextFormField
|
||||
includeRevert
|
||||
name="name"
|
||||
placeholder={credential.name || ""}
|
||||
label="Name (optional):"
|
||||
/>
|
||||
|
||||
{Object.entries(credential.credential_json).map(([key, value]) =>
|
||||
isTypedFileField(key) ? (
|
||||
<TypedFileUploadFormField
|
||||
key={key}
|
||||
name={key}
|
||||
label={getDisplayNameForCredentialKey(key)}
|
||||
<Formik
|
||||
initialValues={initialValues}
|
||||
validationSchema={validationSchema}
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
{({ isSubmitting, resetForm }) => (
|
||||
<Form>
|
||||
<Modal.Body>
|
||||
<GeneralLayouts.Section>
|
||||
<InputLayouts.Vertical name="name" title="Name" optional>
|
||||
<InputTypeInField
|
||||
name="name"
|
||||
placeholder={initialValues.name}
|
||||
/>
|
||||
) : (
|
||||
<TextFormField
|
||||
includeRevert
|
||||
key={key}
|
||||
name={key}
|
||||
placeholder={value as string}
|
||||
label={getDisplayNameForCredentialKey(key)}
|
||||
type={
|
||||
</InputLayouts.Vertical>
|
||||
|
||||
{Object.entries(credential.credential_json).map(
|
||||
([key, value]) => {
|
||||
if (isTypedFileField(key))
|
||||
return (
|
||||
<TypedFileUploadFormField
|
||||
key={key}
|
||||
name={key}
|
||||
label={getDisplayNameForCredentialKey(key)}
|
||||
/>
|
||||
);
|
||||
|
||||
const inputType =
|
||||
key.toLowerCase().includes("token") ||
|
||||
key.toLowerCase().includes("password")
|
||||
? "password"
|
||||
: "text"
|
||||
}
|
||||
disabled={key === "authentication_method"}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
<div className="flex justify-between w-full">
|
||||
<Button onClick={() => resetForm()} leftIcon={SvgTrash}>
|
||||
Reset Changes
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
className="bg-indigo-500 hover:bg-indigo-400"
|
||||
leftIcon={FaNewspaper}
|
||||
>
|
||||
Update
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
</div>
|
||||
: "text";
|
||||
|
||||
return (
|
||||
<InputLayouts.Vertical
|
||||
key={key}
|
||||
name={key}
|
||||
title={getDisplayNameForCredentialKey(key)}
|
||||
>
|
||||
<InputTypeInField
|
||||
name={key}
|
||||
placeholder={
|
||||
// # Note (@raunakab):
|
||||
// Will always be a string since the `if (isTypedFileField)` check above will filter out `TypeFile` values.
|
||||
value as string
|
||||
}
|
||||
type={inputType}
|
||||
variant={
|
||||
key === "authentication_method"
|
||||
? "disabled"
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</InputLayouts.Vertical>
|
||||
);
|
||||
}
|
||||
)}
|
||||
</GeneralLayouts.Section>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<BasicModalFooter
|
||||
cancel={
|
||||
<Button
|
||||
onClick={() => resetForm()}
|
||||
icon={SvgTrash}
|
||||
prominence="secondary"
|
||||
>
|
||||
Reset Changes
|
||||
</Button>
|
||||
}
|
||||
submit={
|
||||
<Button type="submit" disabled={isSubmitting} icon={SvgCheck}>
|
||||
Update
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</Modal.Footer>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,17 +1,24 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState } from "react";
|
||||
import Modal from "@/refresh-components/Modal";
|
||||
import Button from "@/refresh-components/buttons/Button";
|
||||
import Modal, { BasicModalFooter } from "@/refresh-components/Modal";
|
||||
import { Button } from "@opal/components";
|
||||
import Text from "@/refresh-components/texts/Text";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { AccessType } from "@/lib/types";
|
||||
import { EditIcon, NewChatIcon, SwapIcon } from "@/components/icons/icons";
|
||||
import {
|
||||
ConfluenceCredentialJson,
|
||||
Credential,
|
||||
} from "@/lib/connectors/credentials";
|
||||
import { Connector } from "@/lib/connectors/connectors";
|
||||
import { SvgAlertTriangle, SvgTrash } from "@opal/icons";
|
||||
import { Button as OpalButton } from "@opal/components";
|
||||
import {
|
||||
SvgAlertTriangle,
|
||||
SvgBubbleText,
|
||||
SvgCheck,
|
||||
SvgEdit,
|
||||
SvgTrash,
|
||||
} from "@opal/icons";
|
||||
|
||||
interface CredentialSelectionTableProps {
|
||||
credentials: Credential<any>[];
|
||||
editableCredentials: Credential<any>[];
|
||||
@@ -115,21 +122,21 @@ function CredentialSelectionTable({
|
||||
{new Date(credential.time_updated).toLocaleString()}
|
||||
</td>
|
||||
<td className="p-2 flex gap-x-2 content-center mt-auto">
|
||||
<OpalButton
|
||||
<Button
|
||||
onClick={async () => {
|
||||
onDeleteCredential(credential);
|
||||
}}
|
||||
disabled={selected || !editable}
|
||||
icon={SvgTrash}
|
||||
prominence="tertiary"
|
||||
/>
|
||||
{onEditCredential && (
|
||||
<button
|
||||
<Button
|
||||
disabled={!editable}
|
||||
onClick={() => onEditCredential(credential)}
|
||||
className="cursor-pointer my-auto"
|
||||
>
|
||||
<EditIcon />
|
||||
</button>
|
||||
icon={SvgEdit}
|
||||
prominence="tertiary"
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
@@ -203,31 +210,32 @@ export default function ModifyCredential({
|
||||
</Text>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button
|
||||
onClick={async () => {
|
||||
onDeleteCredential(confirmDeletionCredential);
|
||||
setConfirmDeletionCredential(null);
|
||||
}}
|
||||
>
|
||||
Confirm
|
||||
</Button>
|
||||
<Button
|
||||
secondary
|
||||
onClick={() => setConfirmDeletionCredential(null)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<BasicModalFooter
|
||||
cancel={
|
||||
<Button
|
||||
prominence="secondary"
|
||||
onClick={() => setConfirmDeletionCredential(null)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
}
|
||||
submit={
|
||||
<Button
|
||||
onClick={async () => {
|
||||
onDeleteCredential(confirmDeletionCredential);
|
||||
setConfirmDeletionCredential(null);
|
||||
}}
|
||||
>
|
||||
Confirm
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</Modal.Footer>
|
||||
</Modal.Content>
|
||||
</Modal>
|
||||
)}
|
||||
|
||||
<div className="mb-0">
|
||||
<Text as="p" className="mb-4">
|
||||
Select a credential as needed! Ensure that you have selected a
|
||||
credential with the proper permissions for this connector!
|
||||
</Text>
|
||||
|
||||
<Modal.Body>
|
||||
<CredentialSelectionTable
|
||||
onDeleteCredential={async (credential: Credential<any | null>) => {
|
||||
setConfirmDeletionCredential(credential);
|
||||
@@ -251,52 +259,48 @@ export default function ModifyCredential({
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Modal.Body>
|
||||
|
||||
{!showIfEmpty && (
|
||||
<div className="flex mt-8 justify-between">
|
||||
{onCreateNew ? (
|
||||
{!showIfEmpty && (
|
||||
<Modal.Footer>
|
||||
<BasicModalFooter
|
||||
cancel={
|
||||
onCreateNew ? (
|
||||
<Button
|
||||
onClick={onCreateNew}
|
||||
icon={SvgBubbleText}
|
||||
prominence="secondary"
|
||||
>
|
||||
Create
|
||||
</Button>
|
||||
) : undefined
|
||||
}
|
||||
submit={
|
||||
<Button
|
||||
disabled={selectedCredential == null}
|
||||
onClick={() => {
|
||||
onCreateNew();
|
||||
}}
|
||||
className="bg-background-500 disabled:border-transparent
|
||||
transition-colors duration-150 ease-in disabled:bg-background-300
|
||||
disabled:hover:bg-background-300 hover:bg-background-600 cursor-pointer"
|
||||
>
|
||||
<div className="flex gap-x-2 items-center w-full border-none">
|
||||
<NewChatIcon className="text-white" />
|
||||
<p>Create</p>
|
||||
</div>
|
||||
</Button>
|
||||
) : (
|
||||
<div />
|
||||
)}
|
||||
|
||||
<Button
|
||||
disabled={selectedCredential == null}
|
||||
onClick={() => {
|
||||
if (onSwap && attachedConnector) {
|
||||
onSwap(selectedCredential!, attachedConnector.id, accessType);
|
||||
if (close) {
|
||||
close();
|
||||
if (onSwap && attachedConnector) {
|
||||
onSwap(
|
||||
selectedCredential!,
|
||||
attachedConnector.id,
|
||||
accessType
|
||||
);
|
||||
if (close) {
|
||||
close();
|
||||
}
|
||||
}
|
||||
}
|
||||
if (onSwitch) {
|
||||
onSwitch(selectedCredential!);
|
||||
}
|
||||
}}
|
||||
className="bg-indigo-500 disabled:border-transparent
|
||||
transition-colors duration-150 ease-in disabled:bg-indigo-300
|
||||
disabled:hover:bg-indigo-300 hover:bg-indigo-600 cursor-pointer"
|
||||
>
|
||||
<div className="flex gap-x-2 items-center w-full border-none">
|
||||
<SwapIcon className="text-white" />
|
||||
<p>Select</p>
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
if (onSwitch) {
|
||||
onSwitch(selectedCredential!);
|
||||
}
|
||||
}}
|
||||
icon={SvgCheck}
|
||||
>
|
||||
Select
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</Modal.Footer>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -107,6 +107,24 @@ export function createEditingValidationSchema(json_values: dictionaryType) {
|
||||
return Yup.object().shape(schemaFields);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true when the credential template marks a field as optional.
|
||||
*
|
||||
* A field is optional when:
|
||||
* - it is a boolean toggle (`typeof def === "boolean"`)
|
||||
* - its template default is `null` (nullable / not-required)
|
||||
*
|
||||
* In the editing flow every field is optional, so callers can pass
|
||||
* `allOptional = true` to short-circuit.
|
||||
*/
|
||||
export function isOptionalCredentialField(
|
||||
def: unknown,
|
||||
allOptional = false
|
||||
): boolean {
|
||||
if (allOptional) return true;
|
||||
return def === null || typeof def === "boolean";
|
||||
}
|
||||
|
||||
export function createInitialValues(credential: Credential<any>): formType {
|
||||
const initialValues: formType = {
|
||||
name: credential.name || "",
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useSyncExternalStore } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import type { Route } from "next";
|
||||
|
||||
Reference in New Issue
Block a user