chore (dashboard): turn on null checks (#3390)
This commit is contained in:
5
.changeset/unlucky-months-teach.md
Normal file
5
.changeset/unlucky-months-teach.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@nhost/dashboard': minor
|
||||
---
|
||||
|
||||
chore (dashboard): Turn on strictNullChecks config
|
||||
0
config/.husky/pre-commit
Normal file → Executable file
0
config/.husky/pre-commit
Normal file → Executable file
@@ -42,6 +42,7 @@ test('should be able to delete a user', async ({
|
||||
await expect(
|
||||
page.getByRole('button', { name: `View ${email}`, exact: true }),
|
||||
).not.toBeVisible();
|
||||
await expect(page.getByText('User deleted successfully.')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should be able to delete a user from the details page', async ({
|
||||
@@ -75,4 +76,5 @@ test('should be able to delete a user from the details page', async ({
|
||||
await expect(
|
||||
page.getByRole('button', { name: `View ${email}`, exact: true }),
|
||||
).not.toBeVisible();
|
||||
await expect(page.getByText('User deleted successfully.')).toBeVisible();
|
||||
});
|
||||
|
||||
@@ -196,6 +196,10 @@ test('should create table with foreign key constraint', async ({
|
||||
|
||||
await page.getByRole('button', { name: /add/i }).click();
|
||||
|
||||
await expect(
|
||||
page.getByTestId('foreignKeyFormSubmitButton'),
|
||||
).not.toBeVisible();
|
||||
|
||||
await expect(
|
||||
page.getByText(`public.${firstTableName}.id`, { exact: true }),
|
||||
).toBeVisible();
|
||||
|
||||
@@ -1,46 +1,46 @@
|
||||
/**
|
||||
* URL of the dashboard to test against.
|
||||
*/
|
||||
export const TEST_DASHBOARD_URL = process.env.NHOST_TEST_DASHBOARD_URL;
|
||||
export const TEST_DASHBOARD_URL = process.env.NHOST_TEST_DASHBOARD_URL!;
|
||||
|
||||
/**
|
||||
* Name of the organization to test against.
|
||||
*/
|
||||
export const TEST_ORGANIZATION_NAME = process.env.NHOST_TEST_ORGANIZATION_NAME;
|
||||
export const TEST_ORGANIZATION_NAME = process.env.NHOST_TEST_ORGANIZATION_NAME!;
|
||||
|
||||
/**
|
||||
* Slug of the organization to test against.
|
||||
*/
|
||||
export const TEST_ORGANIZATION_SLUG = process.env.NHOST_TEST_ORGANIZATION_SLUG;
|
||||
export const TEST_ORGANIZATION_SLUG = process.env.NHOST_TEST_ORGANIZATION_SLUG!;
|
||||
|
||||
/**
|
||||
* Name of the project to test against.
|
||||
*/
|
||||
export const TEST_PROJECT_NAME = process.env.NHOST_TEST_PROJECT_NAME;
|
||||
export const TEST_PROJECT_NAME = process.env.NHOST_TEST_PROJECT_NAME!;
|
||||
|
||||
/**
|
||||
* Subdomain of the project to test against.
|
||||
*/
|
||||
export const TEST_PROJECT_SUBDOMAIN = process.env.NHOST_TEST_PROJECT_SUBDOMAIN;
|
||||
export const TEST_PROJECT_SUBDOMAIN = process.env.NHOST_TEST_PROJECT_SUBDOMAIN!;
|
||||
|
||||
/**
|
||||
* Hasura admin secret of the test project to use.
|
||||
*/
|
||||
export const TEST_PROJECT_ADMIN_SECRET =
|
||||
process.env.NHOST_TEST_PROJECT_ADMIN_SECRET;
|
||||
process.env.NHOST_TEST_PROJECT_ADMIN_SECRET!;
|
||||
|
||||
/**
|
||||
* Email of the test account to use.
|
||||
*/
|
||||
export const TEST_USER_EMAIL = process.env.NHOST_TEST_USER_EMAIL;
|
||||
export const TEST_USER_EMAIL = process.env.NHOST_TEST_USER_EMAIL!;
|
||||
|
||||
/**
|
||||
* Password of the test account to use.
|
||||
*/
|
||||
export const TEST_USER_PASSWORD = process.env.NHOST_TEST_USER_PASSWORD;
|
||||
export const TEST_USER_PASSWORD = process.env.NHOST_TEST_USER_PASSWORD!;
|
||||
|
||||
export const TEST_PERSONAL_ORG_SLUG = process.env.NHOST_TEST_PERSONAL_ORG_SLUG;
|
||||
export const TEST_PERSONAL_ORG_SLUG = process.env.NHOST_TEST_PERSONAL_ORG_SLUG!;
|
||||
|
||||
const freeUserEmails = process.env.NHOST_TEST_FREE_USER_EMAILS;
|
||||
const freeUserEmails = process.env.NHOST_TEST_FREE_USER_EMAILS!;
|
||||
|
||||
export const TEST_FREE_USER_EMAILS = JSON.parse(freeUserEmails);
|
||||
export const TEST_FREE_USER_EMAILS: string[] = JSON.parse(freeUserEmails);
|
||||
|
||||
@@ -9,7 +9,7 @@ interface CookieConsentProps {
|
||||
}
|
||||
|
||||
export default function CookieConsent({ onAccept }: CookieConsentProps) {
|
||||
const [consentGiven, setConsentGiven] = useSSRLocalStorage(
|
||||
const [consentGiven, setConsentGiven] = useSSRLocalStorage<boolean | null>(
|
||||
'cookie-consent',
|
||||
null,
|
||||
);
|
||||
|
||||
@@ -51,7 +51,7 @@ export interface DialogContextProps {
|
||||
/**
|
||||
* Call this function to open an alert dialog.
|
||||
*/
|
||||
openAlertDialog: <TPayload = string>(config?: DialogConfig<TPayload>) => void;
|
||||
openAlertDialog: <TPayload = string>(config: DialogConfig<TPayload>) => void;
|
||||
/**
|
||||
* Call this function to close the active dialog.
|
||||
*/
|
||||
|
||||
@@ -23,6 +23,10 @@ import {
|
||||
drawerReducer,
|
||||
} from './dialogReducers';
|
||||
|
||||
function isBaseSyntheticEvent(event: any): event is BaseSyntheticEvent {
|
||||
return event?.type !== undefined;
|
||||
}
|
||||
|
||||
function DialogProvider({ children }: PropsWithChildren<unknown>) {
|
||||
const router = useRouter();
|
||||
|
||||
@@ -87,7 +91,7 @@ function DialogProvider({ children }: PropsWithChildren<unknown>) {
|
||||
drawerDispatch({ type: 'CLEAR_DRAWER_CONTENT' });
|
||||
}, []);
|
||||
|
||||
function openAlertDialog<TConfig = string>(config?: DialogConfig<TConfig>) {
|
||||
function openAlertDialog<TConfig = string>(config: DialogConfig<TConfig>) {
|
||||
alertDialogDispatch({ type: 'OPEN_ALERT', payload: config });
|
||||
}
|
||||
|
||||
@@ -122,8 +126,12 @@ function DialogProvider({ children }: PropsWithChildren<unknown>) {
|
||||
);
|
||||
|
||||
const closeDrawerWithDirtyGuard = useCallback(
|
||||
(event?: BaseSyntheticEvent) => {
|
||||
if (isDrawerDirty.current && event?.type !== 'submit') {
|
||||
(event?: any) => {
|
||||
if (
|
||||
isDrawerDirty.current &&
|
||||
isBaseSyntheticEvent(event) &&
|
||||
event.type !== 'submit'
|
||||
) {
|
||||
setShowDirtyConfirmation(true);
|
||||
openDirtyConfirmation({ props: { onPrimaryAction: closeDrawer } });
|
||||
return;
|
||||
@@ -135,8 +143,12 @@ function DialogProvider({ children }: PropsWithChildren<unknown>) {
|
||||
);
|
||||
|
||||
const closeDialogWithDirtyGuard = useCallback(
|
||||
(event?: BaseSyntheticEvent) => {
|
||||
if (isDialogDirty.current && event?.type !== 'submit') {
|
||||
(event?: any) => {
|
||||
if (
|
||||
isDialogDirty.current &&
|
||||
isBaseSyntheticEvent(event) &&
|
||||
event.type !== 'submit'
|
||||
) {
|
||||
setShowDirtyConfirmation(true);
|
||||
openDirtyConfirmation({ props: { onPrimaryAction: closeDialog } });
|
||||
return;
|
||||
@@ -250,7 +262,7 @@ function DialogProvider({ children }: PropsWithChildren<unknown>) {
|
||||
<BaseDialog
|
||||
{...dialogProps}
|
||||
title={dialogTitle}
|
||||
open={dialogOpen}
|
||||
open={!!dialogOpen}
|
||||
onClose={closeDialogWithDirtyGuard}
|
||||
TransitionProps={{ onExited: clearDialogContent, unmountOnExit: false }}
|
||||
PaperProps={{
|
||||
|
||||
@@ -115,7 +115,7 @@ export function drawerReducer(
|
||||
}
|
||||
|
||||
export type AlertDialogAction =
|
||||
| { type: 'OPEN_ALERT'; payload?: DialogConfig }
|
||||
| { type: 'OPEN_ALERT'; payload: DialogConfig }
|
||||
| { type: 'HIDE_ALERT' }
|
||||
| { type: 'CLEAR_ALERT_CONTENT' };
|
||||
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import type { LinkProps } from '@/components/ui/v2/Link';
|
||||
import { Link } from '@/components/ui/v2/Link';
|
||||
import type { MakeRequired } from '@/types/common';
|
||||
import NextLink from 'next/link';
|
||||
import type { ForwardedRef, PropsWithoutRef } from 'react';
|
||||
import { forwardRef } from 'react';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
export interface NavLinkProps extends PropsWithoutRef<LinkProps> {
|
||||
export interface NavLinkProps
|
||||
extends MakeRequired<PropsWithoutRef<LinkProps>, 'href'> {
|
||||
/**
|
||||
* Determines whether or not the link should be disabled.
|
||||
*/
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useIsCurrentUserOwner } from '@/features/orgs/projects/common/hooks/use
|
||||
|
||||
interface Props {
|
||||
buttonText?: string;
|
||||
onClick?: () => void;
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
function OpenTransferDialogButton({ buttonText, onClick }: Props) {
|
||||
|
||||
@@ -19,7 +19,7 @@ export type PaginationProps = DetailedHTMLProps<
|
||||
/**
|
||||
* Number of total elements per page.
|
||||
*/
|
||||
elementsPerPage?: number;
|
||||
elementsPerPage: number;
|
||||
/**
|
||||
* Total number of elements.
|
||||
*/
|
||||
|
||||
@@ -7,7 +7,7 @@ import { TimePickerInput } from './TimePickerInput';
|
||||
|
||||
interface TimePickerProps {
|
||||
date: Date | undefined;
|
||||
setDate: (date: Date | undefined) => void;
|
||||
setDate: (date: Date) => void;
|
||||
}
|
||||
|
||||
function TimePicker({ date, setDate }: TimePickerProps) {
|
||||
|
||||
@@ -15,7 +15,7 @@ export interface TimePickerInputProps
|
||||
extends React.InputHTMLAttributes<HTMLInputElement> {
|
||||
picker: TimePickerType;
|
||||
date: Date | undefined;
|
||||
setDate: (date: Date | undefined) => void;
|
||||
setDate: (date: Date) => void;
|
||||
period?: Period;
|
||||
onRightFocus?: () => void;
|
||||
onLeftFocus?: () => void;
|
||||
|
||||
@@ -19,7 +19,7 @@ function getOrderedTimezones(dateTime: string, selectedTimezone: string) {
|
||||
) {
|
||||
const selectedTimezoneOption = timezones.find(
|
||||
(tz) => tz.value === selectedTimezone,
|
||||
);
|
||||
)!;
|
||||
orderedTimezones = [
|
||||
selectedTimezoneOption,
|
||||
...timezones.filter((tz) => tz.value !== selectedTimezone),
|
||||
|
||||
@@ -8,7 +8,7 @@ export interface UIContextProps {
|
||||
/**
|
||||
* The date and time when maintenance mode will end.
|
||||
*/
|
||||
maintenanceEndDate: Date;
|
||||
maintenanceEndDate: Date | null;
|
||||
}
|
||||
|
||||
const UIContext = createContext<UIContextProps>({
|
||||
|
||||
@@ -24,7 +24,7 @@ type Option = {
|
||||
};
|
||||
|
||||
interface VirtualizedCommandProps<O extends Option> {
|
||||
height: string;
|
||||
height?: string;
|
||||
options: O[];
|
||||
placeholder: string;
|
||||
selectedOption: string;
|
||||
@@ -181,7 +181,7 @@ interface VirtualizedComboboxProps<O extends Option> {
|
||||
width?: string;
|
||||
height?: string;
|
||||
button?: React.JSX.Element;
|
||||
onSelectOption?: (option: O) => void;
|
||||
onSelectOption: (option: O) => void;
|
||||
selectedOption: string;
|
||||
align?: 'start' | 'center' | 'end';
|
||||
side?: 'right' | 'top' | 'bottom' | 'left';
|
||||
@@ -210,7 +210,7 @@ function VirtualizedCombobox<O extends Option>({
|
||||
}}
|
||||
>
|
||||
{selectedOption
|
||||
? options.find((option) => option.value === selectedOption).value
|
||||
? options.find((option) => option.value === selectedOption)!.value
|
||||
: searchPlaceholder}
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
|
||||
@@ -3,7 +3,9 @@ import type {
|
||||
AutocompleteProps,
|
||||
} from '@/components/ui/v2/Autocomplete';
|
||||
import { Autocomplete } from '@/components/ui/v2/Autocomplete';
|
||||
import { isNotEmptyValue } from '@/lib/utils';
|
||||
import { callAll } from '@/utils/callAll';
|
||||
import type { FilterOptionsState } from '@mui/material';
|
||||
import type { ForwardedRef } from 'react';
|
||||
import { forwardRef } from 'react';
|
||||
import type { FieldValues, UseControllerProps } from 'react-hook-form';
|
||||
@@ -28,6 +30,29 @@ export interface ControlledAutocompleteProps<
|
||||
control?: UseControllerProps<TFieldValues>['control'];
|
||||
}
|
||||
|
||||
export function defaultFilterOptions(
|
||||
options: AutocompleteOption<string>[],
|
||||
{ inputValue }: FilterOptionsState<AutocompleteOption<string>>,
|
||||
) {
|
||||
const inputValueLower = inputValue.toLowerCase();
|
||||
const matched: AutocompleteOption<string>[] = [];
|
||||
const otherOptions: AutocompleteOption<string>[] = [];
|
||||
|
||||
options.forEach((option) => {
|
||||
const optionLabelLower = option.label.toLowerCase();
|
||||
|
||||
if (optionLabelLower.startsWith(inputValueLower)) {
|
||||
matched.push(option);
|
||||
} else {
|
||||
otherOptions.push(option);
|
||||
}
|
||||
});
|
||||
|
||||
const result = [...matched, ...otherOptions];
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function ControlledAutocomplete(
|
||||
{
|
||||
controllerProps,
|
||||
@@ -38,9 +63,10 @@ function ControlledAutocomplete(
|
||||
ref: ForwardedRef<HTMLInputElement>,
|
||||
) {
|
||||
const form = useFormContext();
|
||||
const nameAttr = controllerProps?.name || name || '';
|
||||
const { field } = useController({
|
||||
...(controllerProps || {}),
|
||||
name: controllerProps?.name || name || '',
|
||||
name: nameAttr,
|
||||
control: controllerProps?.control || control,
|
||||
});
|
||||
|
||||
@@ -53,13 +79,15 @@ function ControlledAutocomplete(
|
||||
return (
|
||||
<Autocomplete
|
||||
inputValue={
|
||||
typeof field.value !== 'object' ? field.value.toString() : undefined
|
||||
typeof field.value !== 'object' && isNotEmptyValue(field.value)
|
||||
? field.value.toString()
|
||||
: undefined
|
||||
}
|
||||
{...props}
|
||||
{...field}
|
||||
ref={mergeRefs([field.ref, ref])}
|
||||
onChange={(event, options, reason, details) => {
|
||||
setValue?.(controllerProps?.name || name, options, {
|
||||
setValue?.(nameAttr, options, {
|
||||
shouldDirty: true,
|
||||
});
|
||||
|
||||
@@ -67,7 +95,10 @@ function ControlledAutocomplete(
|
||||
props.onChange(event, options, reason, details);
|
||||
}
|
||||
}}
|
||||
onBlur={callAll(field.onBlur, props.onBlur)}
|
||||
onBlur={callAll<[React.FocusEvent<HTMLDivElement>]>(
|
||||
field.onBlur,
|
||||
props.onBlur,
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -41,9 +41,10 @@ function ControlledCheckbox(
|
||||
ref: ForwardedRef<HTMLButtonElement>,
|
||||
) {
|
||||
const { setValue } = useFormContext();
|
||||
const nameAttr = controllerProps?.name || name || '';
|
||||
const { field } = useController({
|
||||
...controllerProps,
|
||||
name: controllerProps?.name || name || '',
|
||||
name: nameAttr,
|
||||
control: controllerProps?.control || control,
|
||||
});
|
||||
|
||||
@@ -53,13 +54,16 @@ function ControlledCheckbox(
|
||||
name={field.name}
|
||||
ref={mergeRefs([field.ref, ref])}
|
||||
onChange={(event, checked) => {
|
||||
setValue(controllerProps?.name || name, checked, { shouldDirty: true });
|
||||
setValue(nameAttr, checked, { shouldDirty: true });
|
||||
|
||||
if (props.onChange) {
|
||||
props.onChange(event, checked);
|
||||
}
|
||||
}}
|
||||
onBlur={callAll(field.onBlur, props.onBlur)}
|
||||
onBlur={callAll<[React.FocusEvent<HTMLButtonElement>]>(
|
||||
field.onBlur,
|
||||
props.onBlur,
|
||||
)}
|
||||
checked={uncheckWhenDisabled && props.disabled ? false : field.value}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -27,9 +27,10 @@ function ControlledSelect(
|
||||
ref: ForwardedRef<HTMLButtonElement>,
|
||||
) {
|
||||
const { setValue } = useFormContext();
|
||||
const nameAttr = controllerProps?.name || name || '';
|
||||
const { field } = useController({
|
||||
...controllerProps,
|
||||
name: controllerProps?.name || name || '',
|
||||
name: nameAttr,
|
||||
control: controllerProps?.control || control,
|
||||
});
|
||||
|
||||
@@ -39,7 +40,7 @@ function ControlledSelect(
|
||||
{...field}
|
||||
ref={mergeRefs([field.ref, ref])}
|
||||
onChange={(event, value) => {
|
||||
setValue(controllerProps?.name || name, value, { shouldDirty: true });
|
||||
setValue(nameAttr, value, { shouldDirty: true });
|
||||
|
||||
if (props.onChange) {
|
||||
props.onChange(event, value);
|
||||
|
||||
@@ -31,9 +31,10 @@ function ControlledSwitch(
|
||||
ref: ForwardedRef<HTMLSpanElement>,
|
||||
) {
|
||||
const { setValue } = useFormContext();
|
||||
const nameAttr = controllerProps?.name || name || '';
|
||||
const { field } = useController({
|
||||
...controllerProps,
|
||||
name: controllerProps?.name || name || '',
|
||||
name: nameAttr,
|
||||
control: controllerProps?.control || control,
|
||||
});
|
||||
|
||||
@@ -43,7 +44,7 @@ function ControlledSwitch(
|
||||
{...field}
|
||||
ref={mergeRefs([field.ref, ref])}
|
||||
onChange={(event) => {
|
||||
setValue(controllerProps?.name || name, event.target.checked, {
|
||||
setValue(nameAttr, event.target.checked, {
|
||||
shouldDirty: true,
|
||||
});
|
||||
|
||||
|
||||
@@ -8,11 +8,11 @@ export interface FormProps extends BoxProps {
|
||||
/**
|
||||
* Function to be called when the form is submitted.
|
||||
*/
|
||||
onSubmit?: (...args: any[]) => any;
|
||||
onSubmit: (...args: any[]) => any;
|
||||
}
|
||||
|
||||
export default function Form({ onSubmit, onKeyDown, ...props }: FormProps) {
|
||||
const formRef = useRef<HTMLDivElement>();
|
||||
const formRef = useRef<HTMLDivElement | null>(null);
|
||||
const {
|
||||
handleSubmit,
|
||||
formState: { isSubmitting },
|
||||
@@ -28,7 +28,7 @@ export default function Form({ onSubmit, onKeyDown, ...props }: FormProps) {
|
||||
}
|
||||
|
||||
const submitButton = Array.from(
|
||||
formRef.current.getElementsByTagName('button'),
|
||||
formRef.current!.getElementsByTagName('button'),
|
||||
).find((item) => item.type === 'submit');
|
||||
|
||||
// Disabling submit if the submit button is disabled
|
||||
|
||||
@@ -37,7 +37,9 @@ export default function AuthenticatedLayout({
|
||||
|
||||
const { isAuthenticated, isLoading, isSigningOut } = useAuth();
|
||||
const { isHealthy, isLoading: isHealthyLoading } = useIsHealthy();
|
||||
const [mainNavContainer, setMainNavContainer] = useState(null);
|
||||
const [mainNavContainer, setMainNavContainer] = useState<HTMLElement | null>(
|
||||
null,
|
||||
);
|
||||
const { mainNavPinned } = useTreeNavState();
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -30,7 +30,7 @@ type Option = {
|
||||
export default function OrgsComboBox() {
|
||||
const { orgs } = useOrgs();
|
||||
const isPlatform = useIsPlatform();
|
||||
const [, setLastSlug] = useSSRLocalStorage('slug', null);
|
||||
const [, setLastSlug] = useSSRLocalStorage<string | null>('slug', null);
|
||||
|
||||
const {
|
||||
query: { orgSlug },
|
||||
|
||||
@@ -14,7 +14,7 @@ import NavTree from './NavTree';
|
||||
import { useTreeNavState } from './TreeNavStateContext';
|
||||
|
||||
interface MainNavProps {
|
||||
container: HTMLElement;
|
||||
container: HTMLElement | null;
|
||||
}
|
||||
|
||||
export default function MainNav({ container }: MainNavProps) {
|
||||
|
||||
@@ -13,7 +13,7 @@ import { UserIcon } from '@/components/ui/v2/icons/UserIcon';
|
||||
import { Badge } from '@/components/ui/v3/badge';
|
||||
import { Button } from '@/components/ui/v3/button';
|
||||
import { useOrgs, type Org } from '@/features/orgs/projects/hooks/useOrgs';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { cn, isNotEmptyValue } from '@/lib/utils';
|
||||
import { getConfigServerUrl, isPlatform as getIsPlatform } from '@/utils/env';
|
||||
import { Box, ChevronDown, ChevronRight, Plus } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
@@ -341,7 +341,7 @@ type NavItem = {
|
||||
};
|
||||
|
||||
const buildNavTreeData = (
|
||||
org: Org,
|
||||
org?: Org,
|
||||
): { items: Record<TreeItemIndex, TreeItem<NavItem>> } => {
|
||||
if (!org) {
|
||||
return {
|
||||
@@ -382,8 +382,11 @@ const buildNavTreeData = (
|
||||
|
||||
export default function NavTree() {
|
||||
const { currentOrg: org } = useOrgs();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
const navTree = useMemo(() => buildNavTreeData(org), [org?.slug]);
|
||||
const navTree = useMemo(
|
||||
() => buildNavTreeData(org),
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[org?.slug],
|
||||
);
|
||||
const { orgsTreeViewState, setOrgsTreeViewState, setOpen } =
|
||||
useTreeNavState();
|
||||
|
||||
@@ -426,7 +429,7 @@ export default function NavTree() {
|
||||
asChild
|
||||
onClick={() => {
|
||||
// do not focus an item if we already there
|
||||
// this will prevent the case where clikcing on the project name
|
||||
// this will prevent the case where clicking on the project name
|
||||
// would focus on the project name instead of the overview page
|
||||
if (
|
||||
navTree.items[item.index].data.targetUrl ===
|
||||
@@ -515,13 +518,19 @@ export default function NavTree() {
|
||||
canSearch={false}
|
||||
onExpandItem={(item) => {
|
||||
setOrgsTreeViewState(
|
||||
({ expandedItems: prevExpandedItems, ...rest }) => ({
|
||||
...rest,
|
||||
// Add item index to expandedItems only if it's not already present
|
||||
expandedItems: prevExpandedItems.includes(item.index)
|
||||
? prevExpandedItems
|
||||
: [...prevExpandedItems, item.index],
|
||||
}),
|
||||
({ expandedItems: prevExpandedItems, ...rest }) => {
|
||||
const newExpandedItems = isNotEmptyValue(prevExpandedItems)
|
||||
? [...prevExpandedItems]
|
||||
: [];
|
||||
|
||||
return {
|
||||
...rest,
|
||||
// Add item index to expandedItems only if it's not already present
|
||||
expandedItems: newExpandedItems?.includes(item.index)
|
||||
? prevExpandedItems
|
||||
: [...newExpandedItems, item.index],
|
||||
};
|
||||
},
|
||||
);
|
||||
}}
|
||||
onCollapseItem={(item) => {
|
||||
@@ -529,7 +538,7 @@ export default function NavTree() {
|
||||
({ expandedItems: prevExpandedItems, ...rest }) => ({
|
||||
...rest,
|
||||
// Remove the item index from expandedItems
|
||||
expandedItems: prevExpandedItems.filter(
|
||||
expandedItems: (prevExpandedItems ?? []).filter(
|
||||
(index) => index !== item.index,
|
||||
),
|
||||
}),
|
||||
|
||||
@@ -12,7 +12,7 @@ export default function PinnedMainNav() {
|
||||
query: { orgSlug },
|
||||
} = useRouter();
|
||||
|
||||
const scrollContainerRef = useRef();
|
||||
const scrollContainerRef = useRef<HTMLDivElement | null>(null);
|
||||
const { mainNavPinned, setMainNavPinned } = useTreeNavState();
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -31,25 +31,19 @@ interface TreeNavProviderProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
function useSyncedTreeViewState(
|
||||
useTreeStateFromURL: () => {
|
||||
expandedItems: string[];
|
||||
focusedItem: string | null;
|
||||
},
|
||||
) {
|
||||
const { expandedItems, focusedItem } = useTreeStateFromURL();
|
||||
function useSyncedTreeViewState() {
|
||||
const { expandedItems, focusedItem } = useNavTreeStateFromURL();
|
||||
|
||||
const [state, setState] = useState<IndividualTreeViewState<never>>({
|
||||
const [state, setState] = useState<IndividualTreeViewState>({
|
||||
expandedItems,
|
||||
focusedItem,
|
||||
selectedItems: null,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setState((prevState) => ({
|
||||
...prevState,
|
||||
expandedItems: [
|
||||
...new Set([...prevState.expandedItems, ...expandedItems]),
|
||||
...new Set([...(prevState.expandedItems ?? []), ...expandedItems]),
|
||||
],
|
||||
focusedItem,
|
||||
}));
|
||||
@@ -64,7 +58,7 @@ function TreeNavStateProvider({ children }: TreeNavProviderProps) {
|
||||
'pin-nav-tree',
|
||||
true,
|
||||
);
|
||||
const orgsTreeViewState = useSyncedTreeViewState(useNavTreeStateFromURL);
|
||||
const orgsTreeViewState = useSyncedTreeViewState();
|
||||
|
||||
const value = useMemo(
|
||||
() => ({
|
||||
|
||||
@@ -146,7 +146,7 @@ export default function SettingsContainer({
|
||||
{!switchId && showSwitch && (
|
||||
<Switch
|
||||
checked={enabled}
|
||||
onChange={(e) => onEnabledChange(e.target.checked)}
|
||||
onChange={(e) => onEnabledChange?.(e.target.checked)}
|
||||
className="self-center"
|
||||
{...switchSlot}
|
||||
/>
|
||||
|
||||
@@ -19,7 +19,7 @@ export interface CodeBlockPropsBase {
|
||||
/**
|
||||
* Text of the toast that appears when the code is copied to the clipboard.
|
||||
*/
|
||||
copyToClipboardToastTitle?: string;
|
||||
copyToClipboardToastTitle: string;
|
||||
}
|
||||
|
||||
export type CodeBlockProps = CodeBlockPropsBase &
|
||||
@@ -63,7 +63,7 @@ interface CopyToClipboardButtonProps
|
||||
> {
|
||||
filenameColor?: string;
|
||||
tooltipColor?: string;
|
||||
toastTitle?: string;
|
||||
toastTitle: string;
|
||||
}
|
||||
|
||||
function CopyToClipboardButton({
|
||||
|
||||
@@ -11,7 +11,7 @@ function CopyToClipboardButton({
|
||||
title,
|
||||
...props
|
||||
}: {
|
||||
textToCopy: string;
|
||||
textToCopy?: string | null;
|
||||
title: string;
|
||||
} & ButtonProps) {
|
||||
const [disabled, setDisabled] = useState(true);
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import { useUI } from '@/components/common/UIProvider';
|
||||
import { Alert } from '@/components/ui/v2/Alert';
|
||||
|
||||
export default function MaintenanceAlert() {
|
||||
const { maintenanceActive, maintenanceEndDate } = useUI();
|
||||
|
||||
if (!maintenanceActive) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function MaintenceEndDate({
|
||||
maintenanceEndDate,
|
||||
}: {
|
||||
maintenanceEndDate: Date;
|
||||
}) {
|
||||
const dateTimeFormat = Intl.DateTimeFormat(undefined, {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
@@ -27,6 +25,21 @@ export default function MaintenanceAlert() {
|
||||
const minute = parts.find((part) => part.type === 'minute')?.value;
|
||||
const timeZone = parts.find((part) => part.type === 'timeZoneName')?.value;
|
||||
|
||||
return (
|
||||
<p>
|
||||
Maintenance is expected to be completed at {year}-{month}-{day} {hour}:
|
||||
{minute} {timeZone}.
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
export default function MaintenanceAlert() {
|
||||
const { maintenanceActive, maintenanceEndDate } = useUI();
|
||||
|
||||
if (!maintenanceActive) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Alert severity="warning" className="mt-4">
|
||||
<p>
|
||||
@@ -36,10 +49,7 @@ export default function MaintenanceAlert() {
|
||||
</p>
|
||||
|
||||
{maintenanceEndDate && (
|
||||
<p>
|
||||
Maintenance is expected to be completed at {year}-{month}-{day} {hour}
|
||||
:{minute} {timeZone}.
|
||||
</p>
|
||||
<MaintenceEndDate maintenanceEndDate={maintenanceEndDate} />
|
||||
)}
|
||||
</Alert>
|
||||
);
|
||||
|
||||
@@ -5,8 +5,10 @@ import type { DetailedHTMLProps, ForwardedRef, HTMLProps } from 'react';
|
||||
import { forwardRef } from 'react';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
export interface ReadOnlyToggleProps
|
||||
extends DetailedHTMLProps<HTMLProps<HTMLSpanElement>, HTMLSpanElement> {
|
||||
export type ReadOnlyToggleProps = Omit<
|
||||
DetailedHTMLProps<HTMLProps<HTMLSpanElement>, HTMLSpanElement>,
|
||||
'checked'
|
||||
> & {
|
||||
/**
|
||||
* Determines whether the toggle is checked or not.
|
||||
*/
|
||||
@@ -24,7 +26,7 @@ export interface ReadOnlyToggleProps
|
||||
*/
|
||||
label?: TextProps;
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
function ReadOnlyToggle(
|
||||
{ checked, className, slotProps = {}, ...props }: ReadOnlyToggleProps,
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
import type { BoxProps } from '@/components/ui/v2/Box';
|
||||
import { Box } from '@/components/ui/v2/Box';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
export type AvatarProps = Pick<BoxProps, 'component'> & {
|
||||
style?: {
|
||||
[key: string]: string;
|
||||
};
|
||||
className?: string;
|
||||
avatarUrl?: string | null;
|
||||
name?: string | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* @deprecated Use `v2/Avatar` instead.
|
||||
*/
|
||||
export default function Avatar({
|
||||
style = {},
|
||||
className = '',
|
||||
avatarUrl,
|
||||
name = '',
|
||||
...rest
|
||||
}: AvatarProps) {
|
||||
const noAvatar = !avatarUrl || avatarUrl.includes('blank');
|
||||
|
||||
const classes = twMerge(
|
||||
'border rounded-full bg-cover bg-center',
|
||||
className,
|
||||
noAvatar && 'border-0 text-white flex items-center justify-center',
|
||||
);
|
||||
|
||||
if (noAvatar) {
|
||||
const initials = name
|
||||
.split(' ')
|
||||
.slice(0, 2)
|
||||
.map((currentNamePart) => `${currentNamePart.charAt(0).toUpperCase()}`)
|
||||
.join('');
|
||||
|
||||
return (
|
||||
<Box
|
||||
className={classes}
|
||||
style={style}
|
||||
sx={{
|
||||
backgroundColor: (theme) =>
|
||||
theme.palette.mode === 'dark' ? `grey.400` : `grey.500`,
|
||||
color: (theme) => `${theme.palette.common.white} !important`,
|
||||
}}
|
||||
{...rest}
|
||||
>
|
||||
{initials}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
style={Object.assign(style, { backgroundImage: `url(${avatarUrl})` })}
|
||||
className={classes}
|
||||
aria-label={name ? `Avatar of ${name}` : 'Avatar'}
|
||||
role="img"
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
export * from './Avatar';
|
||||
export { default as Avatar } from './Avatar';
|
||||
@@ -11,7 +11,7 @@ export default function ClientOnlyPortal({
|
||||
children,
|
||||
selector,
|
||||
}: ClientOnlyPortalProps) {
|
||||
const ref = useRef();
|
||||
const ref = useRef<Element | DocumentFragment>();
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -19,5 +19,5 @@ export default function ClientOnlyPortal({
|
||||
setMounted(true);
|
||||
}, [selector]);
|
||||
|
||||
return mounted ? createPortal(children, ref.current) : null;
|
||||
return mounted ? createPortal(children, ref.current!) : null;
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ import MaterialAutocomplete, {
|
||||
} from '@mui/material/Autocomplete';
|
||||
import clsx from 'clsx';
|
||||
import type { ForwardedRef } from 'react';
|
||||
import { forwardRef, useEffect, useRef, useState } from 'react';
|
||||
import { forwardRef, useEffect, useState } from 'react';
|
||||
|
||||
export interface AutocompleteOption<TValue = string> {
|
||||
/**
|
||||
@@ -217,7 +217,6 @@ function Autocomplete(
|
||||
}: AutocompleteProps<AutocompleteOption>,
|
||||
ref: ForwardedRef<HTMLInputElement>,
|
||||
) {
|
||||
const inputRef = useRef<HTMLInputElement>();
|
||||
const { formControl: formControlSlotProps, ...defaultComponentsProps } =
|
||||
slotProps || {};
|
||||
|
||||
@@ -228,7 +227,7 @@ function Autocomplete(
|
||||
// TODO: Revisit this implementation. We should probably have a better way to
|
||||
// make this component controlled.
|
||||
useEffect(() => {
|
||||
setInputValue(externalInputValue);
|
||||
setInputValue(externalInputValue ?? '');
|
||||
}, [externalInputValue]);
|
||||
|
||||
const filterOptionsFn = externalFilterOptions || filterOptions;
|
||||
@@ -431,7 +430,6 @@ function Autocomplete(
|
||||
...params
|
||||
}) => (
|
||||
<Input
|
||||
ref={inputRef}
|
||||
slotProps={{
|
||||
input: {
|
||||
className: slotProps?.input?.className,
|
||||
|
||||
@@ -31,7 +31,9 @@ function ColorPreferenceProvider({
|
||||
colorPreferenceStorageKey,
|
||||
);
|
||||
|
||||
if (!['light', 'dark', 'system'].includes(storedColorPreference)) {
|
||||
if (
|
||||
!['light', 'dark', 'system'].includes(storedColorPreference as string)
|
||||
) {
|
||||
setColorPreference('system');
|
||||
|
||||
return;
|
||||
|
||||
@@ -9,16 +9,15 @@ import { pickersDayClasses } from '@mui/x-date-pickers/PickersDay';
|
||||
import type { StaticDatePickerProps } from '@mui/x-date-pickers/StaticDatePicker';
|
||||
import { StaticDatePicker } from '@mui/x-date-pickers/StaticDatePicker';
|
||||
|
||||
export interface DatePickerProps
|
||||
extends Omit<
|
||||
StaticDatePickerProps<any, Date>,
|
||||
'renderInput' | 'componentsProps' | 'renderDay'
|
||||
> {
|
||||
export type DatePickerProps = Omit<
|
||||
StaticDatePickerProps<any, Date>,
|
||||
'renderInput' | 'componentsProps' | 'renderDay' | 'onChange' | 'value'
|
||||
> & {
|
||||
/**
|
||||
* Date value to be displayed in the datepicker.
|
||||
* It should be of "yyyy-MM-dd'T'HH:mm" format.
|
||||
*/
|
||||
value: Date;
|
||||
value: Date | null;
|
||||
/**
|
||||
* If true, it will not allow the user to select any date.
|
||||
*/
|
||||
@@ -35,7 +34,7 @@ export interface DatePickerProps
|
||||
* Function to be called when the user selects a date.
|
||||
*/
|
||||
onChange: (value: Date, keyboardInputValue?: string) => void;
|
||||
}
|
||||
};
|
||||
|
||||
const CustomStaticDatePicker = styled(StaticDatePicker)(({ theme }) => ({
|
||||
borderRadius: '10px',
|
||||
@@ -177,7 +176,12 @@ function DatePicker({
|
||||
minDate={minDate}
|
||||
maxDate={maxDate}
|
||||
onChange={(newValue) => onChange(newValue as Date)}
|
||||
renderInput={() => null}
|
||||
renderInput={
|
||||
(() => null) as unknown as StaticDatePickerProps<
|
||||
any,
|
||||
Date
|
||||
>['renderInput']
|
||||
}
|
||||
/>
|
||||
</LocalizationProvider>
|
||||
);
|
||||
|
||||
@@ -15,7 +15,9 @@ const LinearProgress = styled(MaterialLinearProgress)(({ theme, value }) => ({
|
||||
},
|
||||
[`& .${linearProgressClasses.bar}`]: {
|
||||
backgroundColor:
|
||||
value >= 100 ? theme.palette.error.dark : theme.palette.primary.main,
|
||||
value && value >= 100
|
||||
? theme.palette.error.dark
|
||||
: theme.palette.primary.main,
|
||||
},
|
||||
}));
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ const StyledOption = styled(BaseOption)(({ theme }) => ({
|
||||
},
|
||||
}));
|
||||
|
||||
function Option<TValue>(
|
||||
function Option<TValue extends {}>(
|
||||
{ children, ...props }: OptionProps<TValue>,
|
||||
ref: ForwardedRef<HTMLLIElement>,
|
||||
) {
|
||||
|
||||
@@ -63,7 +63,7 @@ const StyledPopper = styled(BasePopper)`
|
||||
z-index: 9999;
|
||||
`;
|
||||
|
||||
function Select<TValue>(
|
||||
function Select<TValue extends {}>(
|
||||
{
|
||||
className,
|
||||
slotProps,
|
||||
|
||||
@@ -60,7 +60,7 @@ function Slider(
|
||||
ref={ref}
|
||||
components={{
|
||||
Rail: SliderRail({
|
||||
value: allowed,
|
||||
value: allowed ?? 0,
|
||||
max: props.max,
|
||||
marks: props.marks,
|
||||
step: props.step,
|
||||
@@ -69,7 +69,7 @@ function Slider(
|
||||
}}
|
||||
color="primary"
|
||||
{...props}
|
||||
marks={allowed > 0 ? false : props.marks}
|
||||
marks={allowed && allowed > 0 ? false : props.marks}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
type={type}
|
||||
className={cn(
|
||||
'flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-0 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-accent',
|
||||
prefix && 'pl-6',
|
||||
{ 'pl-6': prefix },
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
|
||||
@@ -52,7 +52,7 @@ const sheetVariants = cva(
|
||||
interface SheetContentProps
|
||||
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
|
||||
VariantProps<typeof sheetVariants> {
|
||||
container?: HTMLElement;
|
||||
container?: HTMLElement | null;
|
||||
hideCloseButton?: boolean;
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ const SheetContent = React.forwardRef<
|
||||
{
|
||||
side = 'right',
|
||||
className,
|
||||
container,
|
||||
container = null,
|
||||
hideCloseButton,
|
||||
children,
|
||||
...props
|
||||
|
||||
@@ -64,7 +64,7 @@ function MfaQRCodeAndTOTPSecret({ onSuccess }: Props) {
|
||||
</p>
|
||||
<img alt="qrcode" src={qrCodeDataUrl} className="mx-auto w-64" />
|
||||
</div>
|
||||
<CopyMfaTOTPSecret totpSecret={totpSecret} />
|
||||
{totpSecret && <CopyMfaTOTPSecret totpSecret={totpSecret} />}
|
||||
<MfaOtpForm loading={isActivating} sendMfaOtp={onSendMfaOtp} />
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -23,8 +23,8 @@ import { FormProvider, useForm } from 'react-hook-form';
|
||||
import * as Yup from 'yup';
|
||||
|
||||
export const createPATFormValidationSchema = Yup.object({
|
||||
name: Yup.string().label('Name').nullable().required(),
|
||||
expiresAt: Yup.string().label('Expiration date').nullable().required(),
|
||||
name: Yup.string().label('Name').required(),
|
||||
expiresAt: Yup.string().label('Expiration date').required(),
|
||||
});
|
||||
|
||||
export type CreatePATFormValues = Yup.InferType<
|
||||
@@ -70,15 +70,17 @@ export default function CreatePATForm({
|
||||
onCancel,
|
||||
location,
|
||||
}: CreatePATFormProps) {
|
||||
const [personalAccessToken, setPersonalAccessToken] = useState<string>();
|
||||
const [personalAccessToken, setPersonalAccessToken] = useState<
|
||||
string | null
|
||||
>();
|
||||
const { onDirtyStateChange } = useDialog();
|
||||
const nhostClient = useNhostClient();
|
||||
const apolloClient = useApolloClient();
|
||||
const form = useForm<CreatePATFormValues>({
|
||||
reValidateMode: 'onSubmit',
|
||||
defaultValues: {
|
||||
name: null,
|
||||
expiresAt: null,
|
||||
name: '',
|
||||
expiresAt: '',
|
||||
},
|
||||
resolver: yupResolver(createPATFormValidationSchema),
|
||||
});
|
||||
@@ -150,7 +152,7 @@ export default function CreatePATForm({
|
||||
color="secondary"
|
||||
onClick={() => {
|
||||
onDirtyStateChange(false, location);
|
||||
onCancel();
|
||||
onCancel?.();
|
||||
}}
|
||||
>
|
||||
Close
|
||||
|
||||
@@ -7,7 +7,7 @@ import RemoveSecurityKeyButton from './RemoveSecurityKeyButton';
|
||||
|
||||
type SecurityKeyProps = {
|
||||
id: string;
|
||||
nickname?: string;
|
||||
nickname?: string | null;
|
||||
};
|
||||
|
||||
function SecurityKey({ id, nickname }: SecurityKeyProps) {
|
||||
|
||||
@@ -37,7 +37,10 @@ function useGithubAuthentication({
|
||||
},
|
||||
{
|
||||
onError: () => {
|
||||
toast.error(errorText, getToastStyleProps());
|
||||
toast.error(
|
||||
errorText || 'Something went wrong. Please try again later.',
|
||||
getToastStyleProps(),
|
||||
);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import useOnSignInWithEmailAndPasswordHandler from '@/features/auth/SignIn/SignInWithEmailAndPassword/hooks/useOnSignInWithEmailAndPasswordHandler';
|
||||
import useRequestNewMfaTicket from '@/features/auth/SignIn/SignInWithEmailAndPassword/hooks/useRequestNewMfaTicket';
|
||||
import { isNotEmptyValue } from '@/lib/utils';
|
||||
import { useNhostClient } from '@/providers/nhost';
|
||||
import { getToastStyleProps } from '@/utils/constants/settings';
|
||||
import { useRef, useState } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
import MfaSignInOtpForm from './MfaSignInOtpForm';
|
||||
import SignInWithEmailAndPasswordForm from './SignInWithEmailAndPasswordForm';
|
||||
|
||||
@@ -18,18 +20,31 @@ function SignInWithEmailAndPassword() {
|
||||
|
||||
const { onSignInWithEmailAndPassword, isLoading, emailAndPasswordRef } =
|
||||
useOnSignInWithEmailAndPasswordHandler({ onNeedsMfa });
|
||||
const requestNewMfaTicketFn = useRequestNewMfaTicket();
|
||||
|
||||
async function requestNewMfaTicket() {
|
||||
const { email, password } = emailAndPasswordRef.current;
|
||||
mfaTicket.current = await requestNewMfaTicketFn(email, password);
|
||||
if (isNotEmptyValue(emailAndPasswordRef.current)) {
|
||||
try {
|
||||
const { email, password } = emailAndPasswordRef.current;
|
||||
const response = await nhost.auth.signInEmailPassword({
|
||||
email,
|
||||
password,
|
||||
});
|
||||
mfaTicket.current = response.body?.mfa!.ticket;
|
||||
} catch (error) {
|
||||
toast.error(
|
||||
error?.message ||
|
||||
'An error occurred while verifying TOTP. Please try again.',
|
||||
getToastStyleProps(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function onHandleSendMfaOtp(otp: string) {
|
||||
try {
|
||||
setIsMfaLoading(true);
|
||||
await nhost.auth.verifySignInMfaTotp({
|
||||
ticket: mfaTicket.current,
|
||||
ticket: mfaTicket.current!,
|
||||
otp,
|
||||
});
|
||||
} finally {
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
import { useNhostClient } from '@/providers/nhost';
|
||||
import { getToastStyleProps } from '@/utils/constants/settings';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
function useRequestNewMfaTicket() {
|
||||
const nhost = useNhostClient();
|
||||
async function requestNewMfaTicket(email: string, password: string) {
|
||||
let mfaTicket: string;
|
||||
try {
|
||||
const response = await nhost.auth.signInEmailPassword({
|
||||
email,
|
||||
password,
|
||||
});
|
||||
mfaTicket = response.body?.mfa.ticket;
|
||||
} catch (error) {
|
||||
toast.error(
|
||||
error?.message ||
|
||||
'An error occurred while verifying TOTP. Please try again.',
|
||||
getToastStyleProps(),
|
||||
);
|
||||
}
|
||||
|
||||
return mfaTicket;
|
||||
}
|
||||
|
||||
return requestNewMfaTicket;
|
||||
}
|
||||
|
||||
export default useRequestNewMfaTicket;
|
||||
@@ -43,7 +43,7 @@ function SignUpWithEmailAndPasswordForm() {
|
||||
<FormLabel>Verification</FormLabel>
|
||||
<FormControl>
|
||||
<Turnstile
|
||||
siteKey={process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY}
|
||||
siteKey={process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY!}
|
||||
options={{ theme: 'dark', size: 'flexible' }}
|
||||
onSuccess={(token) => {
|
||||
form.setValue('turnstileToken', token, {
|
||||
|
||||
@@ -37,7 +37,7 @@ function SignUpWithSecurityKeyForm() {
|
||||
<FormLabel>Verification</FormLabel>
|
||||
<FormControl>
|
||||
<Turnstile
|
||||
siteKey={process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY}
|
||||
siteKey={process.env.NEXT_PUBLIC_TURNSTILE_SITE_KEY!}
|
||||
options={{ theme: 'dark', size: 'flexible' }}
|
||||
onSuccess={(token) => {
|
||||
form.setValue('turnstileToken', token, {
|
||||
|
||||
@@ -61,12 +61,12 @@ import { z } from 'zod';
|
||||
const createOrgFormSchema = z.object({
|
||||
name: z.string().min(2),
|
||||
organizationType: z.string().min(1, 'Please select an organization type'),
|
||||
plan: z.optional(z.string()),
|
||||
plan: z.string(),
|
||||
});
|
||||
|
||||
interface CreateOrgFormProps {
|
||||
plans: PrefetchNewAppPlansFragment[];
|
||||
onSubmit?: ({
|
||||
onSubmit: ({
|
||||
name,
|
||||
plan,
|
||||
}: z.infer<typeof createOrgFormSchema>) => Promise<void>;
|
||||
@@ -276,7 +276,7 @@ interface CreateOrgDialogProps {
|
||||
isStarterDisabled?: boolean;
|
||||
}
|
||||
|
||||
function isPropSet(prop: any) {
|
||||
function isPropSet<T>(prop: T): prop is Exclude<T, undefined | null> {
|
||||
return prop !== undefined;
|
||||
}
|
||||
|
||||
@@ -315,17 +315,15 @@ export default function CreateOrgDialog({
|
||||
organizationType,
|
||||
plan,
|
||||
}: {
|
||||
name?: string;
|
||||
organizationType?: string;
|
||||
plan?: string;
|
||||
name: string;
|
||||
organizationType: string;
|
||||
plan: string;
|
||||
}) => {
|
||||
await execPromiseWithErrorToast(
|
||||
async () => {
|
||||
const defaultRedirectUrl = `${window.location.origin}/orgs/verify`;
|
||||
|
||||
const {
|
||||
data: { billingCreateOrganizationRequest: clientSecret },
|
||||
} = await createOrganizationRequest({
|
||||
const response = await createOrganizationRequest({
|
||||
variables: {
|
||||
organizationName: name,
|
||||
planID: plan,
|
||||
@@ -333,8 +331,10 @@ export default function CreateOrgDialog({
|
||||
},
|
||||
});
|
||||
|
||||
if (clientSecret) {
|
||||
setStripeClientSecret(clientSecret);
|
||||
if (response.data?.billingCreateOrganizationRequest) {
|
||||
setStripeClientSecret(
|
||||
response.data?.billingCreateOrganizationRequest,
|
||||
);
|
||||
} else {
|
||||
const {
|
||||
data: { organizations },
|
||||
@@ -343,10 +343,10 @@ export default function CreateOrgDialog({
|
||||
const newOrg = organizations.find((org) => org.plan.isFree);
|
||||
|
||||
analytics.track('Organization Created', {
|
||||
organizationId: newOrg.id,
|
||||
organizationSlug: newOrg.slug,
|
||||
organizationId: newOrg?.id,
|
||||
organizationSlug: newOrg?.slug,
|
||||
organizationName: name,
|
||||
organizationPlan: newOrg.plan.name,
|
||||
organizationPlan: newOrg?.plan.name,
|
||||
organizationOwnerId: currentUser?.id,
|
||||
organizationOwnerEmail: currentUser?.email,
|
||||
organizationMetadata: {
|
||||
@@ -355,7 +355,7 @@ export default function CreateOrgDialog({
|
||||
isOnboarding: false,
|
||||
});
|
||||
|
||||
router.push(`/orgs/${newOrg.slug}/projects`);
|
||||
router.push(`/orgs/${newOrg?.slug}/projects`);
|
||||
handleOpenChange(false);
|
||||
}
|
||||
},
|
||||
@@ -414,9 +414,9 @@ export default function CreateOrgDialog({
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{!loading && !stripeClientSecret && (
|
||||
{data && !loading && !stripeClientSecret && (
|
||||
<CreateOrgForm
|
||||
plans={data?.plans}
|
||||
plans={data.plans}
|
||||
onSubmit={createOrg}
|
||||
onCancel={() => handleOpenChange(false)}
|
||||
isStarterDisabled={isStarterDisabled}
|
||||
|
||||
@@ -35,7 +35,7 @@ const validationSchema = Yup.object({
|
||||
threshold: Yup.number().test(
|
||||
'is-valid-threshold',
|
||||
`Threshold must be greater than 110% of your plan's price`,
|
||||
(value: number, { options }) => {
|
||||
(value, { options }) => {
|
||||
const planPrice = options?.context?.planPrice || 0;
|
||||
if (value === 0) {
|
||||
return true;
|
||||
@@ -105,7 +105,7 @@ export default function SpendingNotifications() {
|
||||
const enabled = watch('enabled');
|
||||
|
||||
const progress = useMemo(() => {
|
||||
if (!enabled || threshold <= 0 || !amountDue) {
|
||||
if (!enabled || !threshold || threshold <= 0 || !amountDue) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -134,7 +134,7 @@ export default function SpendingNotifications() {
|
||||
const updateConfigPromise = updateConfig({
|
||||
variables: {
|
||||
id: org?.id,
|
||||
threshold: values.threshold,
|
||||
threshold: values.threshold!,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -103,7 +103,7 @@ export default function SubscriptionPlan() {
|
||||
redirectURL,
|
||||
},
|
||||
});
|
||||
const clientSecret = result?.data?.billingUpgradeFreeOrganization;
|
||||
const clientSecret = result?.data?.billingUpgradeFreeOrganization!;
|
||||
setStripeClientSecret(clientSecret);
|
||||
} else {
|
||||
await execPromiseWithErrorToast(
|
||||
|
||||
@@ -50,7 +50,7 @@ function TransferProjectDialogContent({
|
||||
data: { organizations },
|
||||
} = await refetchOrgs();
|
||||
|
||||
const newOrg = organizations.find((org) => org.slug === Slug);
|
||||
const newOrg = organizations.find((org) => org.slug === Slug)!;
|
||||
|
||||
setShowContent(true);
|
||||
onOrganizationChange(newOrg.id);
|
||||
|
||||
@@ -71,7 +71,7 @@ function TransferProjectForm({
|
||||
}
|
||||
}, [selectedOrganizationId, form]);
|
||||
|
||||
const isUserAdminOfOrg = (org: Org, userId: string) =>
|
||||
const isUserAdminOfOrg = (org: Org, userId?: string) =>
|
||||
org.members.some(
|
||||
(member) =>
|
||||
member.role === Organization_Members_Role_Enum.Admin &&
|
||||
@@ -99,7 +99,9 @@ function TransferProjectForm({
|
||||
});
|
||||
|
||||
const targetOrg = orgs.find((o) => o.id === organization);
|
||||
await push(`/orgs/${targetOrg.slug}/projects`);
|
||||
if (targetOrg) {
|
||||
await push(`/orgs/${targetOrg.slug}/projects`);
|
||||
}
|
||||
},
|
||||
{
|
||||
loadingMessage: 'Transferring project...',
|
||||
@@ -132,7 +134,7 @@ function TransferProjectForm({
|
||||
value={org.id}
|
||||
disabled={
|
||||
org.plan.isFree || // disable the personal org
|
||||
org.id === currentOrg.id || // disable the current org as it can't be a destination org
|
||||
org.id === currentOrg?.id || // disable the current org as it can't be a destination org
|
||||
!isUserAdminOfOrg(org, user?.id) // disable orgs that the current user is not admin of
|
||||
}
|
||||
>
|
||||
|
||||
@@ -165,7 +165,7 @@ export default function AnnouncementsTray() {
|
||||
<DropdownMenuItem
|
||||
disabled={announcement.read.length === 0}
|
||||
onClick={() =>
|
||||
handleSetUnread(announcement.read.at(0).id)
|
||||
handleSetUnread(announcement?.read?.at(0)?.id)
|
||||
}
|
||||
>
|
||||
Mark as unread
|
||||
|
||||
@@ -20,7 +20,7 @@ import { useIsPlatform } from '@/features/orgs/projects/common/hooks/useIsPlatfo
|
||||
import { useOrgs } from '@/features/orgs/projects/hooks/useOrgs';
|
||||
import { execPromiseWithErrorToast } from '@/features/orgs/utils/execPromiseWithErrorToast';
|
||||
import { useUserData } from '@/hooks/useUserData';
|
||||
import { isEmptyValue } from '@/lib/utils';
|
||||
import { isEmptyValue, isNotEmptyValue } from '@/lib/utils';
|
||||
import {
|
||||
CheckoutStatus,
|
||||
useDeleteOrganizationMemberInviteMutation,
|
||||
@@ -80,32 +80,23 @@ export default function NotificationsTray() {
|
||||
userID: userData?.id,
|
||||
},
|
||||
});
|
||||
if (organizationNewRequests.length > 0) {
|
||||
const { sessionID } = organizationNewRequests.at(0);
|
||||
const firstOrganizationNewRequests = organizationNewRequests.at(0);
|
||||
if (isNotEmptyValue(firstOrganizationNewRequests)) {
|
||||
const { sessionID } = firstOrganizationNewRequests;
|
||||
|
||||
const {
|
||||
data: { billingPostOrganizationRequest },
|
||||
} = await postOrganizationRequest({
|
||||
const response = await postOrganizationRequest({
|
||||
variables: {
|
||||
sessionID,
|
||||
},
|
||||
});
|
||||
|
||||
const { Status } = billingPostOrganizationRequest;
|
||||
if (response.data?.billingPostOrganizationRequest) {
|
||||
const { billingPostOrganizationRequest } = response.data;
|
||||
const { Status } = billingPostOrganizationRequest;
|
||||
|
||||
switch (Status) {
|
||||
case CheckoutStatus.Open:
|
||||
if (Status === CheckoutStatus.Open) {
|
||||
setPendingOrgRequest(billingPostOrganizationRequest);
|
||||
break;
|
||||
|
||||
case CheckoutStatus.Completed:
|
||||
break;
|
||||
|
||||
case CheckoutStatus.Expired:
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -303,7 +294,9 @@ export default function NotificationsTray() {
|
||||
</DialogHeader>
|
||||
|
||||
{pendingOrgRequest && (
|
||||
<StripeEmbeddedForm clientSecret={pendingOrgRequest.ClientSecret} />
|
||||
<StripeEmbeddedForm
|
||||
clientSecret={pendingOrgRequest.ClientSecret!}
|
||||
/>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
@@ -54,7 +54,7 @@ export default function PendingInvites() {
|
||||
const { org } = useCurrentOrg();
|
||||
const isAdmin = useIsOrgAdmin();
|
||||
const [inviteDialogOpen, setInviteDialogOpen] = useState(false);
|
||||
const [orgInviteError, setOrgInviteError] = useState(null);
|
||||
const [orgInviteError, setOrgInviteError] = useState<string | null>(null);
|
||||
|
||||
const {
|
||||
data: { organizationMemberInvites = [] } = {},
|
||||
@@ -110,7 +110,7 @@ export default function PendingInvites() {
|
||||
{
|
||||
loadingMessage: 'Sending invite...',
|
||||
successMessage: `Invite to join Organization ${org?.name} sent to ${email}.`,
|
||||
errorMessage: null,
|
||||
errorMessage: '',
|
||||
onError: async (error) => {
|
||||
await discordAnnounce(
|
||||
`Error trying to invite to ${email} to Organization ${org?.name} ${error.message}`,
|
||||
|
||||
@@ -28,7 +28,7 @@ function useFinishOrgCreation({
|
||||
pendingMessage,
|
||||
onCompleted,
|
||||
onError,
|
||||
}: UseFinishOrgCreationProps): [boolean, CheckoutStatus] {
|
||||
}: UseFinishOrgCreationProps) {
|
||||
const router = useRouter();
|
||||
const { session_id } = router.query;
|
||||
|
||||
@@ -44,35 +44,37 @@ function useFinishOrgCreation({
|
||||
setLoading(true);
|
||||
execPromiseWithErrorToast(
|
||||
async () => {
|
||||
const {
|
||||
data: { billingPostOrganizationRequest },
|
||||
} = await postOrganizationRequest({
|
||||
const response = await postOrganizationRequest({
|
||||
variables: {
|
||||
sessionID: session_id as string,
|
||||
},
|
||||
});
|
||||
if (response.data?.billingPostOrganizationRequest) {
|
||||
const { billingPostOrganizationRequest } = response.data;
|
||||
const { Status } = billingPostOrganizationRequest;
|
||||
|
||||
const { Status } = billingPostOrganizationRequest;
|
||||
setLoading(false);
|
||||
setPostOrganizationRequestStatus(Status);
|
||||
|
||||
setLoading(false);
|
||||
setPostOrganizationRequestStatus(Status);
|
||||
switch (Status) {
|
||||
case CheckoutStatus.Completed:
|
||||
onCompleted(billingPostOrganizationRequest);
|
||||
break;
|
||||
|
||||
switch (Status) {
|
||||
case CheckoutStatus.Completed:
|
||||
onCompleted(billingPostOrganizationRequest);
|
||||
break;
|
||||
case CheckoutStatus.Expired:
|
||||
onError?.();
|
||||
throw new Error('Request to create organization has expired');
|
||||
|
||||
case CheckoutStatus.Expired:
|
||||
onError();
|
||||
throw new Error('Organization request has expired');
|
||||
|
||||
case CheckoutStatus.Open:
|
||||
// TODO discuss what to do in this case
|
||||
onError();
|
||||
throw new Error(pendingMessage);
|
||||
|
||||
default:
|
||||
break;
|
||||
case CheckoutStatus.Open:
|
||||
// TODO discuss what to do in this case
|
||||
onError?.();
|
||||
throw new Error(
|
||||
pendingMessage ||
|
||||
'Request to create organization with status "Open"',
|
||||
);
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -10,8 +10,8 @@ function useIsPiTREnabled() {
|
||||
});
|
||||
|
||||
const isPiTREnabled = useMemo(
|
||||
() => isNotNull(data?.config.postgres.pitr?.retention),
|
||||
[data?.config.postgres.pitr?.retention],
|
||||
() => isNotNull(data?.config?.postgres.pitr),
|
||||
[data?.config?.postgres.pitr],
|
||||
);
|
||||
|
||||
return { isPiTREnabled, loading };
|
||||
|
||||
@@ -20,8 +20,8 @@ function useIsPiTREnabledLazy(appId?: string) {
|
||||
}, [appId, getPostgresSettings]);
|
||||
|
||||
const isPiTREnabled = useMemo(
|
||||
() => isNotEmptyValue(data?.config.postgres.pitr?.retention),
|
||||
[data?.config.postgres.pitr?.retention],
|
||||
() => isNotEmptyValue(data?.config?.postgres.pitr),
|
||||
[data?.config?.postgres.pitr],
|
||||
);
|
||||
|
||||
return { isPiTREnabled, loading };
|
||||
|
||||
@@ -18,8 +18,8 @@ function usePiTRBaseBackups(appId: string) {
|
||||
'An error occurred while fetching the Point-in-Time backup data. Please try again later.',
|
||||
);
|
||||
}
|
||||
if (isNotEmptyValue(data.getPiTRBaseBackups)) {
|
||||
const earliestBackup = data.getPiTRBaseBackups.slice(-1).pop();
|
||||
if (isNotEmptyValue(data?.getPiTRBaseBackups)) {
|
||||
const earliestBackup = data.getPiTRBaseBackups.slice(-1).pop()!;
|
||||
setEarliestBackup(earliestBackup.date);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,17 +10,18 @@ import { useMemo } from 'react';
|
||||
*/
|
||||
export default function useRemoteApplicationGQLClient() {
|
||||
const { project, loading } = useProject();
|
||||
const serviceUrl = generateAppServiceUrl(
|
||||
project?.subdomain,
|
||||
project?.region,
|
||||
'graphql',
|
||||
);
|
||||
|
||||
const userApplicationClient = useMemo(() => {
|
||||
if (loading || !serviceUrl) {
|
||||
if (loading || !project?.subdomain) {
|
||||
return new ApolloClient({ cache: new InMemoryCache() });
|
||||
}
|
||||
|
||||
const serviceUrl = generateAppServiceUrl(
|
||||
project.subdomain,
|
||||
project.region,
|
||||
'graphql',
|
||||
);
|
||||
|
||||
return new ApolloClient({
|
||||
cache: new InMemoryCache(),
|
||||
link: new HttpLink({
|
||||
@@ -29,11 +30,16 @@ export default function useRemoteApplicationGQLClient() {
|
||||
'x-hasura-admin-secret':
|
||||
process.env.NEXT_PUBLIC_ENV === 'dev'
|
||||
? getHasuraAdminSecret()
|
||||
: project?.config?.hasura.adminSecret,
|
||||
: project?.config?.hasura.adminSecret!,
|
||||
},
|
||||
}),
|
||||
});
|
||||
}, [loading, serviceUrl, project?.config?.hasura.adminSecret]);
|
||||
}, [
|
||||
loading,
|
||||
project?.config?.hasura.adminSecret,
|
||||
project?.subdomain,
|
||||
project?.region,
|
||||
]);
|
||||
|
||||
return userApplicationClient;
|
||||
}
|
||||
|
||||
@@ -6,7 +6,11 @@ function useRestoreApplicationDatabasePiTR() {
|
||||
useRestoreApplicationDatabasePiTrMutation();
|
||||
|
||||
async function restoreApplicationDatabase(
|
||||
variables: { appId: string; recoveryTarget: string; fromAppId?: string },
|
||||
variables: {
|
||||
appId: string;
|
||||
recoveryTarget: string;
|
||||
fromAppId: string | null;
|
||||
},
|
||||
onCompleted?: () => void,
|
||||
) {
|
||||
await execPromiseWithErrorToast(
|
||||
|
||||
@@ -68,6 +68,9 @@ export const validationSchema = Yup.object({
|
||||
});
|
||||
|
||||
export type AssistantFormValues = Yup.InferType<typeof validationSchema>;
|
||||
export type AssistantFormInitialData = AssistantFormValues & {
|
||||
fileStores?: string[];
|
||||
};
|
||||
|
||||
export interface AssistantFormProps extends DialogFormProps {
|
||||
/**
|
||||
@@ -78,9 +81,7 @@ export interface AssistantFormProps extends DialogFormProps {
|
||||
/**
|
||||
* if there is initialData then it's an update operation
|
||||
*/
|
||||
initialData?: AssistantFormValues & {
|
||||
fileStores?: string[];
|
||||
};
|
||||
initialData?: AssistantFormInitialData;
|
||||
fileStores?: GraphiteFileStore[];
|
||||
|
||||
/**
|
||||
@@ -126,7 +127,7 @@ export default function AssistantForm({
|
||||
const assistantFileStore = initialData?.fileStores
|
||||
? fileStores?.find(
|
||||
(fileStore: GraphiteFileStore) =>
|
||||
fileStore.id === initialData?.fileStores[0],
|
||||
fileStore.id === initialData?.fileStores?.[0],
|
||||
)
|
||||
: null;
|
||||
|
||||
|
||||
@@ -10,10 +10,12 @@ import { Option } from '@/components/ui/v2/Option';
|
||||
import { Text } from '@/components/ui/v2/Text';
|
||||
import { Tooltip } from '@/components/ui/v2/Tooltip';
|
||||
import { type AssistantFormValues } from '@/features/orgs/projects/ai/AssistantForm/AssistantForm';
|
||||
import { useFieldArray, useFormContext } from 'react-hook-form';
|
||||
import { useFieldArray, useFormContext, type Path } from 'react-hook-form';
|
||||
|
||||
type AssistantFormPath = Path<AssistantFormValues>;
|
||||
|
||||
interface ArgumentsFormSectionProps {
|
||||
nestedField: string;
|
||||
nestedField: 'graphql' | 'webhooks';
|
||||
nestIndex: number;
|
||||
}
|
||||
|
||||
@@ -32,6 +34,11 @@ export default function ArgumentsFormSection({
|
||||
name: `${nestedField}.${nestIndex}.arguments`,
|
||||
});
|
||||
|
||||
function removeOnChangeHandler(name: AssistantFormPath) {
|
||||
const { onChange: defaultOnChange, ...props } = register(name);
|
||||
return props;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box className="space-y-4">
|
||||
<div className="flex flex-row items-center justify-between">
|
||||
@@ -60,7 +67,6 @@ export default function ArgumentsFormSection({
|
||||
// We're putting ts-ignore here so we could use the same components for both graphql and webhooks
|
||||
// by passing the nestedField = 'graphql' or nestedField = 'webhooks'
|
||||
{...register(
|
||||
// @ts-ignore
|
||||
`${nestedField}.${nestIndex}.arguments.${index}.name`,
|
||||
)}
|
||||
id={`${field.id}-name`}
|
||||
@@ -68,10 +74,10 @@ export default function ArgumentsFormSection({
|
||||
className="w-full"
|
||||
hideEmptyHelperText
|
||||
error={
|
||||
!!errors?.[nestedField]?.[nestIndex]?.arguments[index].name
|
||||
!!errors?.[nestedField]?.[nestIndex]?.arguments?.[index]?.name
|
||||
}
|
||||
helperText={
|
||||
errors?.[nestedField]?.[nestIndex]?.arguments[index]?.name
|
||||
errors?.[nestedField]?.[nestIndex]?.arguments?.[index]?.name
|
||||
?.message
|
||||
}
|
||||
fullWidth
|
||||
@@ -80,7 +86,6 @@ export default function ArgumentsFormSection({
|
||||
|
||||
<Input
|
||||
{...register(
|
||||
// @ts-ignore
|
||||
`${nestedField}.${nestIndex}.arguments.${index}.description`,
|
||||
)}
|
||||
id={`${field.id}-description`}
|
||||
@@ -88,11 +93,11 @@ export default function ArgumentsFormSection({
|
||||
className="w-full"
|
||||
hideEmptyHelperText
|
||||
error={
|
||||
!!errors?.[nestedField]?.[nestIndex]?.arguments[index]
|
||||
.description
|
||||
!!errors?.[nestedField]?.[nestIndex]?.arguments?.[index]
|
||||
?.description
|
||||
}
|
||||
helperText={
|
||||
errors?.[nestedField]?.[nestIndex]?.arguments[index]
|
||||
errors?.[nestedField]?.[nestIndex]?.arguments?.[index]
|
||||
?.description?.message
|
||||
}
|
||||
fullWidth
|
||||
@@ -107,8 +112,7 @@ export default function ArgumentsFormSection({
|
||||
<Box className="w-full">
|
||||
<ControlledSelect
|
||||
fullWidth
|
||||
{...register(
|
||||
// @ts-ignore
|
||||
{...removeOnChangeHandler(
|
||||
`${nestedField}.${nestIndex}.arguments.${index}.type`,
|
||||
)}
|
||||
id={`${field.id}-type`}
|
||||
|
||||
@@ -60,8 +60,8 @@ export default function GraphqlDataSourcesFormSection() {
|
||||
placeholder="Name"
|
||||
className="w-full"
|
||||
hideEmptyHelperText
|
||||
error={!!errors?.graphql?.at(index)?.name}
|
||||
helperText={errors?.graphql?.at(index)?.message}
|
||||
error={!!errors?.graphql?.at?.(index)?.name}
|
||||
helperText={errors?.graphql?.at?.(index)?.message}
|
||||
fullWidth
|
||||
autoComplete="off"
|
||||
/>
|
||||
@@ -73,8 +73,8 @@ export default function GraphqlDataSourcesFormSection() {
|
||||
placeholder="Description"
|
||||
className="w-full"
|
||||
hideEmptyHelperText
|
||||
error={!!errors?.graphql?.at(index)?.description}
|
||||
helperText={errors?.graphql?.at(index)?.description?.message}
|
||||
error={!!errors?.graphql?.at?.(index)?.description}
|
||||
helperText={errors?.graphql?.at?.(index)?.description?.message}
|
||||
fullWidth
|
||||
autoComplete="off"
|
||||
multiline
|
||||
@@ -90,8 +90,8 @@ export default function GraphqlDataSourcesFormSection() {
|
||||
placeholder="Query"
|
||||
className="w-full"
|
||||
hideEmptyHelperText
|
||||
error={!!errors?.graphql?.at(index)?.query}
|
||||
helperText={errors?.graphql?.at(index)?.query?.message}
|
||||
error={!!errors?.graphql?.at?.(index)?.query}
|
||||
helperText={errors?.graphql?.at?.(index)?.query?.message}
|
||||
fullWidth
|
||||
autoComplete="off"
|
||||
multiline
|
||||
|
||||
@@ -60,8 +60,8 @@ export default function WebhooksDataSourcesFormSection() {
|
||||
placeholder="Name"
|
||||
className="w-full"
|
||||
hideEmptyHelperText
|
||||
error={!!errors?.webhooks?.at(index)?.name}
|
||||
helperText={errors?.webhooks?.at(index)?.message}
|
||||
error={!!errors?.webhooks?.at?.(index)?.name}
|
||||
helperText={errors?.webhooks?.at?.(index)?.message}
|
||||
fullWidth
|
||||
autoComplete="off"
|
||||
/>
|
||||
@@ -73,8 +73,8 @@ export default function WebhooksDataSourcesFormSection() {
|
||||
placeholder="Description"
|
||||
className="w-full"
|
||||
hideEmptyHelperText
|
||||
error={!!errors?.webhooks?.at(index)?.description}
|
||||
helperText={errors?.webhooks?.at(index)?.description?.message}
|
||||
error={!!errors?.webhooks?.at?.(index)?.description}
|
||||
helperText={errors?.webhooks?.at?.(index)?.description?.message}
|
||||
fullWidth
|
||||
autoComplete="off"
|
||||
multiline
|
||||
@@ -90,8 +90,8 @@ export default function WebhooksDataSourcesFormSection() {
|
||||
placeholder="URL"
|
||||
className="w-full"
|
||||
hideEmptyHelperText
|
||||
error={!!errors?.webhooks?.at(index)?.URL}
|
||||
helperText={errors?.webhooks?.at(index)?.URL?.message}
|
||||
error={!!errors?.webhooks?.at?.(index)?.URL}
|
||||
helperText={errors?.webhooks?.at?.(index)?.URL?.message}
|
||||
fullWidth
|
||||
autoComplete="off"
|
||||
multiline
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export * from './AssistantForm';
|
||||
export { default as AssistantForm } from './AssistantForm';
|
||||
|
||||
@@ -8,7 +8,10 @@ import { DotsHorizontalIcon } from '@/components/ui/v2/icons/DotsHorizontalIcon'
|
||||
import { TrashIcon } from '@/components/ui/v2/icons/TrashIcon';
|
||||
import { UserIcon } from '@/components/ui/v2/icons/UserIcon';
|
||||
import { Text } from '@/components/ui/v2/Text';
|
||||
import { AssistantForm } from '@/features/orgs/projects/ai/AssistantForm';
|
||||
import {
|
||||
AssistantForm,
|
||||
type AssistantFormInitialData,
|
||||
} from '@/features/orgs/projects/ai/AssistantForm';
|
||||
import { DeleteAssistantModal } from '@/features/orgs/projects/ai/DeleteAssistantModal';
|
||||
import { type Assistant } from '@/pages/orgs/[orgSlug]/projects/[appSubdomain]/ai/assistants';
|
||||
import { type GraphiteFileStore } from '@/pages/orgs/[orgSlug]/projects/[appSubdomain]/ai/file-stores';
|
||||
@@ -23,14 +26,14 @@ interface AssistantsListProps {
|
||||
/**
|
||||
* The list of file stores
|
||||
*/
|
||||
fileStores: GraphiteFileStore[];
|
||||
fileStores?: GraphiteFileStore[];
|
||||
|
||||
/**
|
||||
* Function to be called after a submitting the form for either creating or updating an assistant.
|
||||
*
|
||||
* @example onDelete={() => refetch()}
|
||||
*/
|
||||
onCreateOrUpdate?: () => Promise<any>;
|
||||
onCreateOrUpdate: () => Promise<any>;
|
||||
|
||||
/**
|
||||
* Function to be called after a successful delete action.
|
||||
@@ -53,11 +56,9 @@ export default function AssistantsList({
|
||||
component: (
|
||||
<AssistantForm
|
||||
assistantId={assistant.assistantID}
|
||||
initialData={{
|
||||
...assistant,
|
||||
}}
|
||||
initialData={assistant as AssistantFormInitialData}
|
||||
fileStores={fileStores}
|
||||
onSubmit={() => onCreateOrUpdate()}
|
||||
onSubmit={onCreateOrUpdate}
|
||||
/>
|
||||
),
|
||||
});
|
||||
|
||||
@@ -41,6 +41,10 @@ export const validationSchema = Yup.object({
|
||||
|
||||
export type AutoEmbeddingsFormValues = Yup.InferType<typeof validationSchema>;
|
||||
|
||||
export type AutoEmbeddingsInitialData = AutoEmbeddingsFormValues & {
|
||||
model: string;
|
||||
};
|
||||
|
||||
export interface AutoEmbeddingsFormProps extends DialogFormProps {
|
||||
/**
|
||||
* To use in conjunction with initialData to allow for updating the autoEmbeddingsConfiguration
|
||||
@@ -50,7 +54,7 @@ export interface AutoEmbeddingsFormProps extends DialogFormProps {
|
||||
/**
|
||||
* if there is initialData then it's an update operation
|
||||
*/
|
||||
initialData?: AutoEmbeddingsFormValues & { model: string };
|
||||
initialData?: AutoEmbeddingsInitialData;
|
||||
|
||||
/**
|
||||
* Function to be called when the operation is cancelled.
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export * from './AutoEmbeddingsForm';
|
||||
export { default as AutoEmbeddingsForm } from './AutoEmbeddingsForm';
|
||||
|
||||
@@ -10,7 +10,10 @@ import { TrashIcon } from '@/components/ui/v2/icons/TrashIcon';
|
||||
import { UserIcon } from '@/components/ui/v2/icons/UserIcon';
|
||||
import { Text } from '@/components/ui/v2/Text';
|
||||
import { Tooltip } from '@/components/ui/v2/Tooltip';
|
||||
import { AutoEmbeddingsForm } from '@/features/orgs/projects/ai/AutoEmbeddingsForm';
|
||||
import {
|
||||
AutoEmbeddingsForm,
|
||||
type AutoEmbeddingsInitialData,
|
||||
} from '@/features/orgs/projects/ai/AutoEmbeddingsForm';
|
||||
import { DeleteAutoEmbeddingsModal } from '@/features/orgs/projects/ai/DeleteAutoEmbeddingsModal';
|
||||
import type { AutoEmbeddingsConfiguration } from '@/pages/orgs/[orgSlug]/projects/[appSubdomain]/ai/auto-embeddings';
|
||||
import { formatDistanceToNow } from 'date-fns';
|
||||
@@ -26,7 +29,7 @@ interface AutoEmbeddingsConfigurationsListProps {
|
||||
*
|
||||
* @example onDelete={() => refetch()}
|
||||
*/
|
||||
onCreateOrUpdate?: () => Promise<any>;
|
||||
onCreateOrUpdate: () => Promise<any>;
|
||||
|
||||
/**
|
||||
* Function to be called after a successful delete action.
|
||||
@@ -55,9 +58,7 @@ export default function AutoEmbeddingsList({
|
||||
component: (
|
||||
<AutoEmbeddingsForm
|
||||
autoEmbeddingsId={autoEmbeddingsConfiguration.id}
|
||||
initialData={{
|
||||
...autoEmbeddingsConfiguration,
|
||||
}}
|
||||
initialData={autoEmbeddingsConfiguration as AutoEmbeddingsInitialData}
|
||||
onSubmit={() => onCreateOrUpdate()}
|
||||
/>
|
||||
),
|
||||
|
||||
@@ -6,11 +6,12 @@ import { Text } from '@/components/ui/v2/Text';
|
||||
import { generateAppServiceUrl } from '@/features/orgs/projects/common/utils/generateAppServiceUrl';
|
||||
import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
||||
import { execPromiseWithErrorToast } from '@/features/orgs/utils/execPromiseWithErrorToast';
|
||||
import { isNotEmptyValue } from '@/lib/utils';
|
||||
import { type AutoEmbeddingsConfiguration } from '@/pages/orgs/[orgSlug]/projects/[appSubdomain]/ai/auto-embeddings';
|
||||
import { useDeleteGraphiteAutoEmbeddingsConfigurationMutation } from '@/utils/__generated__/graphite.graphql';
|
||||
import { getHasuraAdminSecret } from '@/utils/env';
|
||||
import { ApolloClient, HttpLink, InMemoryCache } from '@apollo/client';
|
||||
import { useState } from 'react';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
export interface DeleteAutoEmbeddingsModalProps {
|
||||
@@ -29,24 +30,38 @@ export default function DeleteAutoEmbeddingsModal({
|
||||
|
||||
const { project } = useProject();
|
||||
|
||||
const serviceUrl = generateAppServiceUrl(
|
||||
const client = useMemo(() => {
|
||||
if (
|
||||
isNotEmptyValue(project?.subdomain) &&
|
||||
isNotEmptyValue(project?.region) &&
|
||||
isNotEmptyValue(project?.config)
|
||||
) {
|
||||
const serviceUrl = generateAppServiceUrl(
|
||||
project.subdomain,
|
||||
project.region,
|
||||
'graphql',
|
||||
);
|
||||
return new ApolloClient({
|
||||
cache: new InMemoryCache(),
|
||||
link: new HttpLink({
|
||||
uri: serviceUrl,
|
||||
headers: {
|
||||
'x-hasura-admin-secret':
|
||||
process.env.NEXT_PUBLIC_ENV === 'dev'
|
||||
? getHasuraAdminSecret()
|
||||
: project.config.hasura.adminSecret,
|
||||
},
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
return new ApolloClient({ cache: new InMemoryCache() });
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [
|
||||
project?.config?.hasura.adminSecret,
|
||||
project?.subdomain,
|
||||
project?.region,
|
||||
'graphql',
|
||||
);
|
||||
|
||||
const client = new ApolloClient({
|
||||
cache: new InMemoryCache(),
|
||||
link: new HttpLink({
|
||||
uri: serviceUrl,
|
||||
headers: {
|
||||
'x-hasura-admin-secret':
|
||||
process.env.NEXT_PUBLIC_ENV === 'dev'
|
||||
? getHasuraAdminSecret()
|
||||
: project?.config?.hasura.adminSecret,
|
||||
},
|
||||
}),
|
||||
});
|
||||
]);
|
||||
|
||||
const [deleteAutoEmbeddingsConfiguration] =
|
||||
useDeleteGraphiteAutoEmbeddingsConfigurationMutation({
|
||||
|
||||
@@ -19,6 +19,7 @@ import { useIsPlatform } from '@/features/orgs/projects/common/hooks/useIsPlatfo
|
||||
import { useAdminApolloClient } from '@/features/orgs/projects/hooks/useAdminApolloClient';
|
||||
import { useOrgs } from '@/features/orgs/projects/hooks/useOrgs';
|
||||
import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
||||
import { isNotEmptyValue } from '@/lib/utils';
|
||||
import {
|
||||
useSendDevMessageMutation,
|
||||
useStartDevSessionMutation,
|
||||
@@ -31,7 +32,9 @@ import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil';
|
||||
const MAX_THREAD_LENGTH = 50;
|
||||
|
||||
export type Message = Omit<
|
||||
SendDevMessageMutation['graphite']['sendDevMessage']['messages'][0],
|
||||
NonNullable<
|
||||
SendDevMessageMutation['graphite']
|
||||
>['sendDevMessage']['messages'][number],
|
||||
'__typename'
|
||||
>;
|
||||
|
||||
@@ -84,7 +87,8 @@ export default function DevAssistant() {
|
||||
|
||||
if (!sessionID || hasBeenAnHourSinceLastMessage) {
|
||||
const sessionRes = await startDevSession({ client: adminClient });
|
||||
sessionID = sessionRes?.data?.graphite?.startDevSession?.sessionID;
|
||||
sessionID =
|
||||
sessionRes?.data?.graphite?.startDevSession?.sessionID || '';
|
||||
setStoredSessionID(sessionID);
|
||||
}
|
||||
|
||||
@@ -92,37 +96,33 @@ export default function DevAssistant() {
|
||||
throw new Error('Failed to start a new session');
|
||||
}
|
||||
|
||||
const {
|
||||
data: {
|
||||
graphite: { sendDevMessage: { messages: newMessages } = {} } = {},
|
||||
} = {},
|
||||
} = await sendDevMessage({
|
||||
const result = await sendDevMessage({
|
||||
variables: {
|
||||
message: userInput,
|
||||
sessionId: sessionID || '',
|
||||
sessionId: sessionID,
|
||||
prevMessageID: !hasBeenAnHourSinceLastMessage
|
||||
? lastMessage?.id || ''
|
||||
: '',
|
||||
},
|
||||
});
|
||||
const newMessages = result.data?.graphite?.sendDevMessage.messages;
|
||||
if (isNotEmptyValue(newMessages)) {
|
||||
let thread = [
|
||||
// remove the temp messages of the user input while we wait for the dev assistant to respond
|
||||
...$messages.filter((item) => item.createdAt),
|
||||
...newMessages
|
||||
// remove empty messages
|
||||
.filter((item) => item.message)
|
||||
|
||||
let thread = [
|
||||
// remove the temp messages of the user input while we wait for the dev assistant to respond
|
||||
...$messages.filter((item) => item.createdAt),
|
||||
...newMessages
|
||||
// add the currentProject.id to the new messages
|
||||
.map((item) => ({ ...item, projectId: project?.id })),
|
||||
];
|
||||
|
||||
// remove empty messages
|
||||
.filter((item) => item.message)
|
||||
|
||||
// add the currentProject.id to the new messages
|
||||
.map((item) => ({ ...item, projectId: project?.id })),
|
||||
];
|
||||
|
||||
if (thread.length > MAX_THREAD_LENGTH) {
|
||||
thread = thread.slice(thread.length - MAX_THREAD_LENGTH); // keep the thread at a max length of MAX_THREAD_LENGTH
|
||||
if (thread.length > MAX_THREAD_LENGTH) {
|
||||
thread = thread.slice(thread.length - MAX_THREAD_LENGTH); // keep the thread at a max length of MAX_THREAD_LENGTH
|
||||
}
|
||||
setMessages(thread);
|
||||
}
|
||||
|
||||
setMessages(thread);
|
||||
} catch (error) {
|
||||
toast.custom(
|
||||
(t) => (
|
||||
|
||||
@@ -55,7 +55,7 @@ export default function MessageBox({ message }: { message: Message }) {
|
||||
<Box
|
||||
className="flex flex-col space-y-4 border-t p-4 first:border-t-0"
|
||||
sx={{
|
||||
backgroundColor: isUserMessage && 'background.default',
|
||||
backgroundColor: isUserMessage ? 'background.default' : undefined,
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
|
||||
@@ -12,7 +12,7 @@ interface MessagesListProps {
|
||||
|
||||
function MessagesList({ loading }: MessagesListProps) {
|
||||
const { project } = useProject();
|
||||
const bottomElement = useRef(null);
|
||||
const bottomElement = useRef<HTMLDivElement | null>(null);
|
||||
const messages = useRecoilValue(projectMessagesState(project?.id));
|
||||
|
||||
const scrollToBottom = () =>
|
||||
|
||||
@@ -77,7 +77,10 @@ export default function FileStoreForm({
|
||||
}))
|
||||
: [];
|
||||
|
||||
const formDefaultValues = { ...initialData, buckets: [] };
|
||||
const formDefaultValues = {
|
||||
...initialData,
|
||||
buckets: [] as { label: string; value: string }[],
|
||||
};
|
||||
formDefaultValues.buckets = initialData?.buckets
|
||||
? initialData.buckets.map((bucket) => ({
|
||||
label: bucket,
|
||||
|
||||
@@ -25,7 +25,7 @@ interface FileStoresListProps {
|
||||
*
|
||||
* @example onDelete={() => refetch()}
|
||||
*/
|
||||
onCreateOrUpdate?: () => Promise<any>;
|
||||
onCreateOrUpdate: () => Promise<any>;
|
||||
|
||||
/**
|
||||
* Function to be called after a successful delete action.
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { ApplyLocalSettingsDialog } from '@/components/common/ApplyLocalSettingsDialog';
|
||||
import { useDialog } from '@/components/common/DialogProvider';
|
||||
import { useUI } from '@/components/common/UIProvider';
|
||||
import { ControlledAutocomplete } from '@/components/form/ControlledAutocomplete';
|
||||
import {
|
||||
ControlledAutocomplete,
|
||||
defaultFilterOptions,
|
||||
} from '@/components/form/ControlledAutocomplete';
|
||||
import { Form } from '@/components/form/Form';
|
||||
import { SettingsContainer } from '@/components/layout/SettingsContainer';
|
||||
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
|
||||
@@ -35,6 +38,7 @@ import { useIsPlatform } from '@/features/orgs/projects/common/hooks/useIsPlatfo
|
||||
import { useLocalMimirClient } from '@/features/orgs/projects/hooks/useLocalMimirClient';
|
||||
import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
||||
import { execPromiseWithErrorToast } from '@/features/orgs/utils/execPromiseWithErrorToast';
|
||||
import { isNotEmptyValue } from '@/lib/utils';
|
||||
|
||||
const validationSchema = Yup.object({
|
||||
version: Yup.object({
|
||||
@@ -66,9 +70,7 @@ export default function AISettings() {
|
||||
const [aiServiceEnabled, setAIServiceEnabled] = useState(true);
|
||||
|
||||
const {
|
||||
data: {
|
||||
config: { ai, postgres: { version: postgresVersion } = {} } = {},
|
||||
} = {},
|
||||
data,
|
||||
loading: loadingAiSettings,
|
||||
error: errorGettingAiSettings,
|
||||
} = useGetAiSettingsQuery({
|
||||
@@ -79,6 +81,9 @@ export default function AISettings() {
|
||||
skip: !project?.id,
|
||||
});
|
||||
|
||||
const ai = data?.config?.ai;
|
||||
const postgresVersion = data?.config?.postgres?.version;
|
||||
|
||||
const { data: graphiteVersionsData, loading: loadingGraphiteVersionsData } =
|
||||
useGetSoftwareVersionsQuery({
|
||||
variables: {
|
||||
@@ -132,13 +137,13 @@ export default function AISettings() {
|
||||
if (ai) {
|
||||
reset({
|
||||
version: {
|
||||
label: ai?.version,
|
||||
value: ai?.version,
|
||||
label: ai?.version!,
|
||||
value: ai?.version!,
|
||||
},
|
||||
webhookSecret: ai?.webhookSecret,
|
||||
synchPeriodMinutes: ai?.autoEmbeddings?.synchPeriodMinutes,
|
||||
apiKey: ai?.openai?.apiKey,
|
||||
organization: ai?.openai?.organization,
|
||||
organization: ai?.openai?.organization!,
|
||||
compute: {
|
||||
cpu: ai?.resources?.compute?.cpu ?? 62,
|
||||
memory: ai?.resources?.compute?.memory ?? 128,
|
||||
@@ -156,7 +161,7 @@ export default function AISettings() {
|
||||
!ai &&
|
||||
!aiSettingsFormValues.version.value
|
||||
) {
|
||||
setValue('version', availableVersions?.at(0));
|
||||
setValue('version', availableVersions.at(0)!);
|
||||
}
|
||||
}, [
|
||||
ai,
|
||||
@@ -167,7 +172,10 @@ export default function AISettings() {
|
||||
]);
|
||||
|
||||
const toggleAIService = async (enabled: boolean) => {
|
||||
if (!isPostgresVersionValidForAI(postgresVersion)) {
|
||||
if (
|
||||
isNotEmptyValue(postgresVersion) &&
|
||||
!isPostgresVersionValidForAI(postgresVersion)
|
||||
) {
|
||||
toast.error(
|
||||
'In order to enable the AI service you need to update your database version to 14.6-20231018-1 or newer.',
|
||||
{
|
||||
@@ -212,7 +220,7 @@ export default function AISettings() {
|
||||
async () => {
|
||||
await updateConfig({
|
||||
variables: {
|
||||
appId: project.id,
|
||||
appId: project?.id,
|
||||
config: {
|
||||
ai: {
|
||||
version: formValues.version.value,
|
||||
@@ -309,25 +317,7 @@ export default function AISettings() {
|
||||
name="version"
|
||||
autoHighlight
|
||||
isOptionEqualToValue={() => false}
|
||||
filterOptions={(options, { inputValue }) => {
|
||||
const inputValueLower = inputValue.toLowerCase();
|
||||
const matched = [];
|
||||
const otherOptions = [];
|
||||
|
||||
options.forEach((option) => {
|
||||
const optionLabelLower = option.label.toLowerCase();
|
||||
|
||||
if (optionLabelLower.startsWith(inputValueLower)) {
|
||||
matched.push(option);
|
||||
} else {
|
||||
otherOptions.push(option);
|
||||
}
|
||||
});
|
||||
|
||||
const result = [...matched, ...otherOptions];
|
||||
|
||||
return result;
|
||||
}}
|
||||
filterOptions={defaultFilterOptions}
|
||||
fullWidth
|
||||
className="col-span-4"
|
||||
options={availableVersions}
|
||||
|
||||
@@ -39,7 +39,7 @@ export default function OTPEmailSettings() {
|
||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||
});
|
||||
|
||||
const { enabled } = data?.config?.auth?.method?.otp?.email || {};
|
||||
const enabled = !!data?.config?.auth?.method?.otp?.email?.enabled;
|
||||
|
||||
const form = useForm<OTPEmailSettingsFormValues>({
|
||||
reValidateMode: 'onSubmit',
|
||||
@@ -74,7 +74,7 @@ export default function OTPEmailSettings() {
|
||||
) => {
|
||||
const updateConfigPromise = updateConfig({
|
||||
variables: {
|
||||
appId: project.id,
|
||||
appId: project?.id,
|
||||
config: {
|
||||
auth: {
|
||||
method: {
|
||||
|
||||
@@ -51,7 +51,9 @@ export default function AllowedEmailDomainsSettings() {
|
||||
const form = useForm<AllowedEmailSettingsFormValues>({
|
||||
reValidateMode: 'onSubmit',
|
||||
defaultValues: {
|
||||
enabled: email?.allowed?.length > 0 || emailDomains?.allowed?.length > 0,
|
||||
enabled:
|
||||
(email?.allowed?.length ?? 0) > 0 ||
|
||||
(emailDomains?.allowed?.length ?? 0) > 0,
|
||||
allowedEmails: email?.allowed?.join(', ') || '',
|
||||
allowedEmailDomains: emailDomains?.allowed?.join(', ') || '',
|
||||
},
|
||||
@@ -67,7 +69,8 @@ export default function AllowedEmailDomainsSettings() {
|
||||
if (!loading && email && emailDomains) {
|
||||
form.reset({
|
||||
enabled:
|
||||
email?.allowed?.length > 0 || emailDomains?.allowed?.length > 0,
|
||||
(email?.allowed?.length ?? 0) > 0 ||
|
||||
(emailDomains?.allowed?.length ?? 0) > 0,
|
||||
allowedEmails: email?.allowed?.join(', ') || '',
|
||||
allowedEmailDomains: emailDomains?.allowed?.join(', ') || '',
|
||||
});
|
||||
@@ -93,12 +96,12 @@ export default function AllowedEmailDomainsSettings() {
|
||||
) => {
|
||||
const updateConfigPromise = updateConfig({
|
||||
variables: {
|
||||
appId: project.id,
|
||||
appId: project?.id,
|
||||
config: {
|
||||
auth: {
|
||||
user: {
|
||||
email: {
|
||||
blocked: email.blocked,
|
||||
blocked: email?.blocked,
|
||||
allowed:
|
||||
values.enabled && values.allowedEmails
|
||||
? values.allowedEmails
|
||||
@@ -107,7 +110,7 @@ export default function AllowedEmailDomainsSettings() {
|
||||
: [],
|
||||
},
|
||||
emailDomains: {
|
||||
blocked: emailDomains.blocked,
|
||||
blocked: emailDomains?.blocked,
|
||||
allowed:
|
||||
values.enabled && values.allowedEmailDomains
|
||||
? values.allowedEmailDomains
|
||||
|
||||
@@ -82,7 +82,7 @@ export default function AllowedRedirectURLsSettings() {
|
||||
) => {
|
||||
const updateConfigPromise = updateConfig({
|
||||
variables: {
|
||||
appId: project.id,
|
||||
appId: project?.id,
|
||||
config: {
|
||||
auth: {
|
||||
redirections: {
|
||||
|
||||
@@ -24,7 +24,7 @@ const validationSchema = Yup.object({
|
||||
export type AnonymousSignInFormValues = Yup.InferType<typeof validationSchema>;
|
||||
|
||||
export default function AnonymousSignInSettings() {
|
||||
const { project } = useProject();
|
||||
const { project, loading: isProjectLoading } = useProject();
|
||||
const { openDialog } = useDialog();
|
||||
const isPlatform = useIsPlatform();
|
||||
const { maintenanceActive } = useUI();
|
||||
@@ -39,7 +39,7 @@ export default function AnonymousSignInSettings() {
|
||||
...(!isPlatform ? { client: localMimirClient } : {}),
|
||||
});
|
||||
|
||||
const { enabled } = data?.config?.auth?.method?.anonymous || {};
|
||||
const enabled = !!data?.config?.auth?.method?.anonymous?.enabled;
|
||||
|
||||
const form = useForm<AnonymousSignInFormValues>({
|
||||
reValidateMode: 'onSubmit',
|
||||
@@ -55,7 +55,7 @@ export default function AnonymousSignInSettings() {
|
||||
}
|
||||
}, [loading, enabled, form]);
|
||||
|
||||
if (loading) {
|
||||
if (loading || isProjectLoading) {
|
||||
return (
|
||||
<ActivityIndicator
|
||||
delay={1000}
|
||||
@@ -74,7 +74,7 @@ export default function AnonymousSignInSettings() {
|
||||
) => {
|
||||
const updateConfigPromise = updateConfig({
|
||||
variables: {
|
||||
appId: project.id,
|
||||
appId: project?.id,
|
||||
config: {
|
||||
auth: {
|
||||
method: {
|
||||
|
||||
@@ -58,7 +58,7 @@ export type AppleProviderFormValues = Yup.InferType<typeof validationSchema>;
|
||||
|
||||
export default function AppleProviderSettings() {
|
||||
const theme = useTheme();
|
||||
const { project } = useProject();
|
||||
const { project, loading: isProjectLoading } = useProject();
|
||||
const { openDialog } = useDialog();
|
||||
const isPlatform = useIsPlatform();
|
||||
const { maintenanceActive } = useUI();
|
||||
@@ -102,7 +102,7 @@ export default function AppleProviderSettings() {
|
||||
}
|
||||
}, [loading, teamId, keyId, clientId, privateKey, audience, enabled, form]);
|
||||
|
||||
if (loading) {
|
||||
if (loading || isProjectLoading) {
|
||||
return (
|
||||
<ActivityIndicator
|
||||
delay={1000}
|
||||
@@ -122,7 +122,7 @@ export default function AppleProviderSettings() {
|
||||
async function handleSubmit(formValues: AppleProviderFormValues) {
|
||||
const updateConfigPromise = updateConfig({
|
||||
variables: {
|
||||
appId: project.id,
|
||||
appId: project?.id,
|
||||
config: {
|
||||
auth: {
|
||||
method: {
|
||||
@@ -256,8 +256,8 @@ export default function AppleProviderSettings() {
|
||||
name="redirectUrl"
|
||||
id="apple-redirectUrl"
|
||||
defaultValue={`${generateAppServiceUrl(
|
||||
project.subdomain,
|
||||
project.region,
|
||||
project!.subdomain,
|
||||
project!.region,
|
||||
'auth',
|
||||
)}/signin/provider/apple/callback`}
|
||||
className="col-span-2"
|
||||
@@ -275,8 +275,8 @@ export default function AppleProviderSettings() {
|
||||
e.stopPropagation();
|
||||
copy(
|
||||
`${generateAppServiceUrl(
|
||||
project.subdomain,
|
||||
project.region,
|
||||
project!.subdomain,
|
||||
project!.region,
|
||||
'auth',
|
||||
)}/signin/provider/apple/callback`,
|
||||
'Redirect URL',
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { ApplyLocalSettingsDialog } from '@/components/common/ApplyLocalSettingsDialog';
|
||||
import { useDialog } from '@/components/common/DialogProvider';
|
||||
import { useUI } from '@/components/common/UIProvider';
|
||||
import { ControlledAutocomplete } from '@/components/form/ControlledAutocomplete';
|
||||
import {
|
||||
ControlledAutocomplete,
|
||||
defaultFilterOptions,
|
||||
} from '@/components/form/ControlledAutocomplete';
|
||||
import { Form } from '@/components/form/Form';
|
||||
import { SettingsContainer } from '@/components/layout/SettingsContainer';
|
||||
import { ActivityIndicator } from '@/components/ui/v2/ActivityIndicator';
|
||||
@@ -12,7 +15,7 @@ import {
|
||||
useUpdateConfigMutation,
|
||||
} from '@/generated/graphql';
|
||||
import { yupResolver } from '@hookform/resolvers/yup';
|
||||
import { useEffect } from 'react';
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
import * as Yup from 'yup';
|
||||
|
||||
@@ -20,6 +23,7 @@ import { useIsPlatform } from '@/features/orgs/projects/common/hooks/useIsPlatfo
|
||||
import { useLocalMimirClient } from '@/features/orgs/projects/hooks/useLocalMimirClient';
|
||||
import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
||||
import { execPromiseWithErrorToast } from '@/features/orgs/utils/execPromiseWithErrorToast';
|
||||
import { isEmptyValue } from '@/lib/utils';
|
||||
|
||||
const validationSchema = Yup.object({
|
||||
version: Yup.object({
|
||||
@@ -56,16 +60,25 @@ export default function AuthServiceVersionSettings() {
|
||||
});
|
||||
|
||||
const { version } = data?.config?.auth || {};
|
||||
const versions = authVersionsData?.softwareVersions || [];
|
||||
const availableVersions = Array.from(
|
||||
new Set(versions.map((el) => el.version)).add(version),
|
||||
)
|
||||
.sort()
|
||||
.reverse()
|
||||
.map((availableVersion) => ({
|
||||
label: availableVersion,
|
||||
value: availableVersion,
|
||||
}));
|
||||
|
||||
const availableVersions = useMemo(() => {
|
||||
const versions = authVersionsData?.softwareVersions || [];
|
||||
|
||||
if (isEmptyValue(versions)) {
|
||||
return [];
|
||||
}
|
||||
const versionSet = new Set(versions.map((el) => el.version));
|
||||
if (version) {
|
||||
versionSet.add(version);
|
||||
}
|
||||
return Array.from(versionSet)
|
||||
.sort()
|
||||
.reverse()
|
||||
.map((availableVersion) => ({
|
||||
label: availableVersion,
|
||||
value: availableVersion,
|
||||
}));
|
||||
}, [authVersionsData?.softwareVersions, version]);
|
||||
|
||||
const form = useForm<AuthServiceVersionFormValues>({
|
||||
reValidateMode: 'onSubmit',
|
||||
@@ -105,7 +118,7 @@ export default function AuthServiceVersionSettings() {
|
||||
) => {
|
||||
const updateConfigPromise = updateConfig({
|
||||
variables: {
|
||||
appId: project.id,
|
||||
appId: project?.id,
|
||||
config: {
|
||||
auth: {
|
||||
version: formValues.version.value,
|
||||
@@ -168,25 +181,7 @@ export default function AuthServiceVersionSettings() {
|
||||
return option.value;
|
||||
}}
|
||||
isOptionEqualToValue={() => false}
|
||||
filterOptions={(options, { inputValue }) => {
|
||||
const inputValueLower = inputValue.toLowerCase();
|
||||
const matched = [];
|
||||
const otherOptions = [];
|
||||
|
||||
options.forEach((option) => {
|
||||
const optionLabelLower = option.label.toLowerCase();
|
||||
|
||||
if (optionLabelLower.startsWith(inputValueLower)) {
|
||||
matched.push(option);
|
||||
} else {
|
||||
otherOptions.push(option);
|
||||
}
|
||||
});
|
||||
|
||||
const result = [...matched, ...otherOptions];
|
||||
|
||||
return result;
|
||||
}}
|
||||
filterOptions={defaultFilterOptions}
|
||||
fullWidth
|
||||
className="lg:col-span-2"
|
||||
options={availableVersions}
|
||||
|
||||
@@ -50,7 +50,7 @@ const validationSchema = Yup.object({
|
||||
export type AzureADProviderFormValues = Yup.InferType<typeof validationSchema>;
|
||||
|
||||
export default function AzureADProviderSettings() {
|
||||
const { project } = useProject();
|
||||
const { project, loading: isProjectLoading } = useProject();
|
||||
const { openDialog } = useDialog();
|
||||
const isPlatform = useIsPlatform();
|
||||
const { maintenanceActive } = useUI();
|
||||
@@ -90,7 +90,7 @@ export default function AzureADProviderSettings() {
|
||||
}
|
||||
}, [loading, clientId, clientSecret, tenant, enabled, form]);
|
||||
|
||||
if (loading) {
|
||||
if (loading || isProjectLoading) {
|
||||
return (
|
||||
<ActivityIndicator
|
||||
delay={1000}
|
||||
@@ -110,7 +110,7 @@ export default function AzureADProviderSettings() {
|
||||
async function handleSubmit(formValues: AzureADProviderFormValues) {
|
||||
const updateConfigPromise = updateConfig({
|
||||
variables: {
|
||||
appId: project.id,
|
||||
appId: project!.id,
|
||||
config: {
|
||||
auth: {
|
||||
method: {
|
||||
@@ -186,8 +186,8 @@ export default function AzureADProviderSettings() {
|
||||
name="redirectUrl"
|
||||
id="azuerad-redirectUrl"
|
||||
defaultValue={`${generateAppServiceUrl(
|
||||
project.subdomain,
|
||||
project.region,
|
||||
project!.subdomain,
|
||||
project!.region,
|
||||
'auth',
|
||||
)}/signin/provider/azuread/callback`}
|
||||
className="col-span-2"
|
||||
@@ -205,8 +205,8 @@ export default function AzureADProviderSettings() {
|
||||
e.stopPropagation();
|
||||
copy(
|
||||
`${generateAppServiceUrl(
|
||||
project.subdomain,
|
||||
project.region,
|
||||
project!.subdomain,
|
||||
project!.region,
|
||||
'auth',
|
||||
)}/signin/provider/azuread/callback`,
|
||||
'Redirect URL',
|
||||
|
||||
@@ -26,7 +26,7 @@ import { twMerge } from 'tailwind-merge';
|
||||
|
||||
export default function BitbucketProviderSettings() {
|
||||
const { maintenanceActive } = useUI();
|
||||
const { project } = useProject();
|
||||
const { project, loading: isProjectLoading } = useProject();
|
||||
const [updateConfig] = useUpdateConfigMutation({
|
||||
refetchQueries: [GetSignInMethodsDocument],
|
||||
});
|
||||
@@ -49,7 +49,7 @@ export default function BitbucketProviderSettings() {
|
||||
resolver: yupResolver(baseProviderValidationSchema),
|
||||
});
|
||||
|
||||
if (loading) {
|
||||
if (loading || isProjectLoading) {
|
||||
return (
|
||||
<ActivityIndicator
|
||||
delay={1000}
|
||||
@@ -69,7 +69,7 @@ export default function BitbucketProviderSettings() {
|
||||
async function handleSubmit(formValues: BaseProviderSettingsFormValues) {
|
||||
const updateConfigPromise = updateConfig({
|
||||
variables: {
|
||||
appId: project.id,
|
||||
appId: project!.id,
|
||||
config: {
|
||||
auth: {
|
||||
method: {
|
||||
@@ -125,8 +125,8 @@ export default function BitbucketProviderSettings() {
|
||||
hideEmptyHelperText
|
||||
label="Redirect URL"
|
||||
defaultValue={`${generateAppServiceUrl(
|
||||
project.subdomain,
|
||||
project.region,
|
||||
project!.subdomain,
|
||||
project!.region,
|
||||
'auth',
|
||||
)}/signin/provider/bitbucket/callback`}
|
||||
disabled
|
||||
@@ -140,8 +140,8 @@ export default function BitbucketProviderSettings() {
|
||||
e.stopPropagation();
|
||||
copy(
|
||||
`${generateAppServiceUrl(
|
||||
project.subdomain,
|
||||
project.region,
|
||||
project!.subdomain,
|
||||
project!.region,
|
||||
'auth',
|
||||
)}/signin/provider/bitbucket/callback`,
|
||||
'Redirect URL',
|
||||
|
||||
@@ -49,7 +49,9 @@ export default function BlockedEmailSettings() {
|
||||
const form = useForm<BlockedEmailFormValues>({
|
||||
reValidateMode: 'onSubmit',
|
||||
defaultValues: {
|
||||
enabled: email?.blocked?.length > 0 || emailDomains?.blocked?.length > 0,
|
||||
enabled:
|
||||
(email?.blocked?.length ?? 0) > 0 ||
|
||||
(emailDomains?.blocked?.length ?? 0) > 0,
|
||||
blockedEmails: email?.blocked?.join(', ') || '',
|
||||
blockedEmailDomains: emailDomains?.blocked?.join(', ') || '',
|
||||
},
|
||||
@@ -64,7 +66,8 @@ export default function BlockedEmailSettings() {
|
||||
if (!loading && email && emailDomains) {
|
||||
form.reset({
|
||||
enabled:
|
||||
email?.blocked?.length > 0 || emailDomains?.blocked?.length > 0,
|
||||
(email?.blocked?.length ?? 0) > 0 ||
|
||||
(emailDomains?.blocked?.length ?? 0) > 0,
|
||||
blockedEmails: email?.blocked?.join(', ') || '',
|
||||
blockedEmailDomains: emailDomains?.blocked?.join(', ') || '',
|
||||
});
|
||||
@@ -90,12 +93,12 @@ export default function BlockedEmailSettings() {
|
||||
) => {
|
||||
const updateConfigPromise = updateConfig({
|
||||
variables: {
|
||||
appId: project.id,
|
||||
appId: project!.id,
|
||||
config: {
|
||||
auth: {
|
||||
user: {
|
||||
email: {
|
||||
allowed: email.allowed,
|
||||
allowed: email?.allowed,
|
||||
blocked:
|
||||
values.enabled && values.blockedEmails
|
||||
? [
|
||||
@@ -108,7 +111,7 @@ export default function BlockedEmailSettings() {
|
||||
: [],
|
||||
},
|
||||
emailDomains: {
|
||||
allowed: emailDomains.allowed,
|
||||
allowed: emailDomains?.allowed,
|
||||
blocked:
|
||||
values.enabled && values.blockedEmailDomains
|
||||
? [
|
||||
|
||||
@@ -76,7 +76,7 @@ export default function ClientURLSettings() {
|
||||
const handleClientURLChange = async (values: ClientURLFormValues) => {
|
||||
const updateConfigPromise = updateConfig({
|
||||
variables: {
|
||||
appId: project.id,
|
||||
appId: project!.id,
|
||||
config: {
|
||||
auth: {
|
||||
redirections: {
|
||||
|
||||
@@ -44,14 +44,14 @@ export default function ConcealErrorsSettings() {
|
||||
const form = useForm<ToggleConcealErrorsFormValues>({
|
||||
reValidateMode: 'onSubmit',
|
||||
defaultValues: {
|
||||
enabled: data?.config?.auth?.misc?.concealErrors,
|
||||
enabled: !!data?.config?.auth?.misc?.concealErrors,
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!loading) {
|
||||
form.reset({
|
||||
enabled: data?.config?.auth?.misc?.concealErrors,
|
||||
enabled: data?.config?.auth?.misc?.concealErrors!,
|
||||
});
|
||||
}
|
||||
}, [loading, data, form]);
|
||||
@@ -77,7 +77,7 @@ export default function ConcealErrorsSettings() {
|
||||
) => {
|
||||
const updateConfigPromise = updateConfig({
|
||||
variables: {
|
||||
appId: project.id,
|
||||
appId: project!.id,
|
||||
config: {
|
||||
auth: {
|
||||
misc: {
|
||||
|
||||
@@ -20,7 +20,7 @@ function ConfirmDeleteSMTPSettingsModal({
|
||||
close,
|
||||
onDelete,
|
||||
}: {
|
||||
onDelete?: () => Promise<any>;
|
||||
onDelete: () => Promise<any>;
|
||||
close: () => void;
|
||||
}) {
|
||||
const onClickDelete = async () => {
|
||||
@@ -76,7 +76,7 @@ export default function DeleteSMTPSettings() {
|
||||
const deleteSMTPSettings = async () => {
|
||||
const updateConfigPromise = updateConfig({
|
||||
variables: {
|
||||
appId: project.id,
|
||||
appId: project!.id,
|
||||
config: {
|
||||
provider: {
|
||||
smtp: null,
|
||||
|
||||
@@ -42,14 +42,14 @@ export default function DisableNewUsersSettings() {
|
||||
const form = useForm<DisableNewUsersFormValues>({
|
||||
reValidateMode: 'onSubmit',
|
||||
defaultValues: {
|
||||
disabled: data?.config?.auth?.signUp?.disableNewUsers,
|
||||
disabled: !!data?.config?.auth?.signUp?.disableNewUsers,
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!loading) {
|
||||
form.reset({
|
||||
disabled: data?.config?.auth?.signUp?.disableNewUsers,
|
||||
disabled: !!data?.config?.auth?.signUp?.disableNewUsers,
|
||||
});
|
||||
}
|
||||
}, [loading, data, form]);
|
||||
@@ -75,7 +75,7 @@ export default function DisableNewUsersSettings() {
|
||||
) => {
|
||||
const updateConfigPromise = updateConfig({
|
||||
variables: {
|
||||
appId: project.id,
|
||||
appId: project!.id,
|
||||
config: {
|
||||
auth: {
|
||||
signUp: {
|
||||
|
||||
@@ -74,7 +74,7 @@ export default function DisableSignUpsSettings() {
|
||||
) => {
|
||||
const updateConfigPromise = updateConfig({
|
||||
variables: {
|
||||
appId: project.id,
|
||||
appId: project!.id,
|
||||
config: {
|
||||
auth: {
|
||||
signUp: {
|
||||
|
||||
@@ -29,7 +29,7 @@ import { FormProvider, useForm } from 'react-hook-form';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
export default function DiscordProviderSettings() {
|
||||
const { project } = useProject();
|
||||
const { project, loading: isProjectLoading } = useProject();
|
||||
const { openDialog } = useDialog();
|
||||
const isPlatform = useIsPlatform();
|
||||
const { maintenanceActive } = useUI();
|
||||
@@ -67,7 +67,7 @@ export default function DiscordProviderSettings() {
|
||||
}
|
||||
}, [loading, clientId, clientSecret, enabled, form]);
|
||||
|
||||
if (loading) {
|
||||
if (loading || isProjectLoading) {
|
||||
return (
|
||||
<ActivityIndicator
|
||||
delay={1000}
|
||||
@@ -160,8 +160,8 @@ export default function DiscordProviderSettings() {
|
||||
hideEmptyHelperText
|
||||
label="Redirect URL"
|
||||
defaultValue={`${generateAppServiceUrl(
|
||||
project.subdomain,
|
||||
project.region,
|
||||
project!.subdomain,
|
||||
project!.region,
|
||||
'auth',
|
||||
)}/signin/provider/discord/callback`}
|
||||
disabled
|
||||
@@ -175,8 +175,8 @@ export default function DiscordProviderSettings() {
|
||||
e.stopPropagation();
|
||||
copy(
|
||||
`${generateAppServiceUrl(
|
||||
project.subdomain,
|
||||
project.region,
|
||||
project!.subdomain,
|
||||
project!.region,
|
||||
'auth',
|
||||
)}/signin/provider/discord/callback`,
|
||||
'Redirect URL',
|
||||
|
||||
@@ -65,8 +65,8 @@ export default function EmailAndPasswordSettings() {
|
||||
useEffect(() => {
|
||||
if (!loading) {
|
||||
form.reset({
|
||||
hibpEnabled,
|
||||
emailVerificationRequired,
|
||||
hibpEnabled: !!hibpEnabled,
|
||||
emailVerificationRequired: !!emailVerificationRequired,
|
||||
passwordMinLength,
|
||||
});
|
||||
}
|
||||
@@ -97,7 +97,7 @@ export default function EmailAndPasswordSettings() {
|
||||
async function handleSubmit(formValues: EmailAndPasswordFormValues) {
|
||||
const updateConfigPromise = updateConfig({
|
||||
variables: {
|
||||
appId: project.id,
|
||||
appId: project!.id,
|
||||
config: {
|
||||
auth: {
|
||||
method: {
|
||||
|
||||
@@ -30,7 +30,7 @@ import { useProject } from '@/features/orgs/projects/hooks/useProject';
|
||||
import { execPromiseWithErrorToast } from '@/features/orgs/utils/execPromiseWithErrorToast';
|
||||
|
||||
export default function FacebookProviderSettings() {
|
||||
const { project } = useProject();
|
||||
const { project, loading: isProjectLoading } = useProject();
|
||||
const { openDialog } = useDialog();
|
||||
const isPlatform = useIsPlatform();
|
||||
const { maintenanceActive } = useUI();
|
||||
@@ -68,7 +68,7 @@ export default function FacebookProviderSettings() {
|
||||
}
|
||||
}, [loading, clientId, clientSecret, enabled, form]);
|
||||
|
||||
if (loading) {
|
||||
if (loading || isProjectLoading) {
|
||||
return (
|
||||
<ActivityIndicator
|
||||
delay={1000}
|
||||
@@ -88,7 +88,7 @@ export default function FacebookProviderSettings() {
|
||||
async function handleSubmit(formValues: BaseProviderSettingsFormValues) {
|
||||
const updateConfigPromise = updateConfig({
|
||||
variables: {
|
||||
appId: project.id,
|
||||
appId: project!.id,
|
||||
config: {
|
||||
auth: {
|
||||
method: {
|
||||
@@ -161,8 +161,8 @@ export default function FacebookProviderSettings() {
|
||||
hideEmptyHelperText
|
||||
label="Redirect URL"
|
||||
defaultValue={`${generateAppServiceUrl(
|
||||
project.subdomain,
|
||||
project.region,
|
||||
project!.subdomain,
|
||||
project!.region,
|
||||
'auth',
|
||||
)}/signin/provider/facebook/callback`}
|
||||
disabled
|
||||
@@ -176,8 +176,8 @@ export default function FacebookProviderSettings() {
|
||||
e.stopPropagation();
|
||||
copy(
|
||||
`${generateAppServiceUrl(
|
||||
project.subdomain,
|
||||
project.region,
|
||||
project!.subdomain,
|
||||
project!.region,
|
||||
'auth',
|
||||
)}/signin/provider/facebook/callback`,
|
||||
'Redirect URL',
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user