Compare commits

..

56 Commits

Author SHA1 Message Date
Szilárd Dóró
decb0b057c Merge pull request #1677 from nhost/changeset-release/main
chore: update versions
2023-02-28 11:39:26 +01:00
github-actions[bot]
fc79b890df chore: update versions 2023-02-28 10:06:25 +00:00
Szilárd Dóró
211eb42af5 Merge pull request #1622 from nhost/chore/improved-dialogs
chore(dashboard): improve Dialog and Drawer API
2023-02-28 11:05:03 +01:00
Szilárd Dóró
a7398451e3 fix(dashboard): add dirty state checking to user form 2023-02-28 10:47:42 +01:00
Szilárd Dóró
4b4f0d0150 chore(dashboard): add changeset 2023-02-28 10:31:47 +01:00
Szilárd Dóró
f37e2a23e2 Merge remote-tracking branch 'origin/main' into chore/improved-dialogs 2023-02-28 10:31:10 +01:00
Johan Eliasson
1a4a061284 Merge pull request #1674 from nhost/changeset-release/main
chore: update versions
2023-02-27 11:55:28 +01:00
github-actions[bot]
78555c7e85 chore: update versions 2023-02-27 08:22:19 +00:00
Johan Eliasson
01ded8ffff Merge pull request #1670 from nhost/functions-tests
Functions fix + tests
2023-02-27 09:21:05 +01:00
Johan Eliasson
3c7cf92edf Create .changeset/eighty-mugs-flash.md 2023-02-27 09:20:49 +01:00
Johan Eliasson
bb4301fd34 more tests 2023-02-26 17:49:19 +01:00
Szilárd Dóró
c8c8948755 Merge pull request #1667 from nhost/changeset-release/main
chore: update versions
2023-02-24 12:45:06 +01:00
github-actions[bot]
17e9e5899e chore: update versions 2023-02-24 11:44:31 +00:00
Szilárd Dóró
bd22c48131 Merge pull request #1666 from nhost/fix/workspace-invitation
fix(nhost-js): use correct URL for functions requests
2023-02-24 12:43:02 +01:00
Szilárd Dóró
89a239ff3a fix(nhost-js): improve code readability 2023-02-24 12:22:54 +01:00
Szilárd Dóró
0131886011 fix(nhost-js): use correct URL for functions requests 2023-02-24 12:06:30 +01:00
Szilárd Dóró
340c014fe8 Merge pull request #1664 from nhost/update-codeowners
chore: update codeowners
2023-02-24 11:06:07 +01:00
Szilárd Dóró
bc9c8b9456 chore: update codeowners 2023-02-24 11:05:35 +01:00
Nuno Pato
c22b2621ba Merge pull request #1661 from nhost/changeset-release/main
chore: update versions
2023-02-23 20:33:34 -01:00
github-actions[bot]
726746c4d3 chore: update versions 2023-02-23 19:19:03 +00:00
Nuno Pato
c431570783 Merge pull request #1662 from nhost/fix/file-upload
fix(hasura-storage-js): fix forbidden error
2023-02-23 18:17:47 -01:00
Szilárd Dóró
445d8ef449 fix(hasura-storage-js): fix forbidden error 2023-02-23 18:16:22 +01:00
Szilárd Dóró
0f4ea18e42 Merge pull request #1655 from nhost/feat/auth-storage-permissions
fix(dashboard): allow permission editing for auth and storage schemas
2023-02-23 15:22:05 +01:00
Szilárd Dóró
dae7c5d517 Merge pull request #1660 from nhost/fix/user-creation-content-type
fix(dashboard): set correct Content-Type for user creation
2023-02-23 14:38:16 +01:00
Szilárd Dóró
f673adea00 fix(dashboard): set correct Content-Type for user creation 2023-02-23 12:50:00 +01:00
Szilárd Dóró
1c6f1e3b33 Merge pull request #1656 from nhost/changeset-release/main
chore: update versions
2023-02-23 11:56:07 +01:00
github-actions[bot]
d1365ea516 chore: update versions 2023-02-23 10:20:53 +00:00
Szilárd Dóró
72dbba7881 Merge pull request #1659 from nhost/chore/revert-graphql-client
chore(graphql-js): revert GraphQL Client
2023-02-23 11:19:26 +01:00
Szilárd Dóró
a3f3991d5a Merge pull request #1658 from nhost/fix/user-creation
fix(dashboard): false positive message on user creation
2023-02-23 11:01:44 +01:00
Szilárd Dóró
c71fe2cf72 Revert "chore(graphql-js): revert GraphQL client for now"
This reverts commit 9a0ab5b887.
2023-02-23 10:15:02 +01:00
Szilárd Dóró
24c5ed3ea4 chore(graphql-js): cleanup 2023-02-23 10:14:43 +01:00
Szilárd Dóró
2d9145f918 chore(graphql-js): revert GraphQL client for now 2023-02-23 10:10:14 +01:00
Szilárd Dóró
9a0ab5b887 chore(graphql-js): revert GraphQL client for now 2023-02-23 10:06:58 +01:00
Szilárd Dóró
1ddf704c5b fix(dashboard): false positive message on user creation 2023-02-23 09:39:32 +01:00
Szilárd Dóró
6f4ee845c6 Merge pull request #1643 from nhost/fix/auth-last-seen
fix(dashboard): use correct date for last seen
2023-02-22 20:01:18 +01:00
Szilárd Dóró
0368663dea fix(dashboard): allow permission editing for auth and storage schemas
fixes #1555
2023-02-22 19:59:20 +01:00
Pilou
76ce7d7b6e Merge pull request #1653 from nhost/changeset-release/main
chore: update versions
2023-02-22 15:43:41 +01:00
github-actions[bot]
538bfbcb3e chore: update versions 2023-02-22 14:23:55 +00:00
Pilou
07b35d1e5f Merge pull request #1650 from nhost/docs/graphql-client-readme
GraphQL client: fix snake_case bug and improve readme
2023-02-22 15:22:34 +01:00
Pierre-Louis Mercereau
2200a0ed07 fix: correct types on snake case operations 2023-02-22 15:13:43 +01:00
Szilárd Dóró
df23d97126 Merge pull request #1652 from nhost/changeset-release/main
chore: update versions
2023-02-22 14:44:46 +01:00
github-actions[bot]
104f149369 chore: update versions 2023-02-22 13:26:08 +00:00
Szilárd Dóró
01228583a0 Merge pull request #1651 from nhost/fix/permission-editor-dropdown
fix(dashboard): prevent permission editor dropdown from being always open
2023-02-22 14:24:51 +01:00
Pierre-Louis Mercereau
93309dd851 docs: strictNullChecks 2023-02-22 14:06:10 +01:00
Szilárd Dóró
2cc18dcb51 fix(dashboard): prevent permission editor dropdown from being always open 2023-02-22 13:54:31 +01:00
Pierre-Louis Mercereau
3b48a62790 docs: ✏️ improve readme instructions 2023-02-22 13:44:15 +01:00
Pierre-Louis Mercereau
8897dec056 docs: add package.json script instructions 2023-02-22 12:23:54 +01:00
Pierre-Louis Mercereau
324dda8309 docs: streamline readme instructions 2023-02-22 11:58:51 +01:00
Szilárd Dóró
b755e9086c fix(dashboard): use correct date for last seen 2023-02-20 14:19:56 +01:00
Szilárd Dóró
962563d6a0 chore(dashboard): cleanup 2023-02-16 16:51:40 +01:00
Szilárd Dóró
8bf58ba26b chore(dashboard): migrate remaining dialogs 2023-02-16 16:37:44 +01:00
Szilárd Dóró
0c175e7a11 chore(dashboard): migrate additional dialogs to the new API 2023-02-16 16:13:47 +01:00
Szilárd Dóró
70f2fbcfc2 chore(dashboard): partially migrate dialogs to new API 2023-02-16 15:59:35 +01:00
Szilárd Dóró
d2c4ad3260 chore(dashboard): cleanup dialog provider 2023-02-16 13:32:21 +01:00
Szilárd Dóró
a9ca2c2946 chore(dashboard): migrate drawers to use new API 2023-02-16 13:25:23 +01:00
Szilárd Dóró
d854dd74b1 chore(dashboard): improve dialog management 2023-02-16 12:54:21 +01:00
197 changed files with 1499 additions and 17108 deletions

18
.github/CODEOWNERS vendored
View File

@@ -1,14 +1,14 @@
# Documentation # Documentation
# https://help.github.com/en/articles/about-code-owners # https://help.github.com/en/articles/about-code-owners
/packages @plmercereau @szilarddoro /packages @szilarddoro
/packages/docgen @szilarddoro /packages/docgen @szilarddoro
/integrations/stripe-graphql-js @elitan /integrations/stripe-graphql-js @elitan
/.github @plmercereau /.github @szilarddoro
/dashboard/ @szilarddoro @guicurcio /dashboard/ @szilarddoro
/docs/ @guicurcio @elitan /docs/ @elitan
/config/ @plmercereau @szilarddoro /config/ @szilarddoro
/examples/ @plmercereau /examples/ @szilarddoro
/examples/codegen-react-apollo @elitan @plmercereau /examples/codegen-react-apollo @elitan @szilarddoro
/examples/codegen-react-query @elitan @plmercereau /examples/codegen-react-query @elitan @szilarddoro
/examples/react-apollo-crm @elitan @plmercereau /examples/react-apollo-crm @elitan @szilarddoro

View File

@@ -1,5 +1,63 @@
# @nhost/dashboard # @nhost/dashboard
## 0.11.20
### Patch Changes
- 4b4f0d01: chore(dashboard): improve dialog management
## 0.11.19
### Patch Changes
- @nhost/react-apollo@5.0.6
- @nhost/nextjs@1.13.11
## 0.11.18
### Patch Changes
- 01318860: fix(nhost-js): use correct URL for functions requests
- Updated dependencies [01318860]
- @nhost/react-apollo@5.0.5
- @nhost/nextjs@1.13.10
## 0.11.17
### Patch Changes
- f673adea: fix(dashboard): set correct Content-Type for user creation
- 445d8ef4: chore(deps): bump `@nhost/react-apollo` to 5.0.4
- 445d8ef4: chore(deps): bump `@nhost/nextjs` to 1.13.9
- 0368663d: fix(dashboard): allow permission editing for auth and storage schemas
- Updated dependencies [445d8ef4]
- Updated dependencies [445d8ef4]
- @nhost/react-apollo@5.0.4
- @nhost/nextjs@1.13.9
## 0.11.16
### Patch Changes
- b755e908: fix(dashboard): use correct date for last seen
- 2d9145f9: chore(deps): revert GraphQL client
- 1ddf704c: fix(dashboard): don't show false positive message for failed user creation
- @nhost/react-apollo@5.0.3
- @nhost/nextjs@1.13.8
## 0.11.15
### Patch Changes
- @nhost/react-apollo@5.0.2
- @nhost/nextjs@1.13.7
## 0.11.14
### Patch Changes
- 2cc18dcb: fix(dashboard): prevent permission editor dropdown from being always open
## 0.11.13 ## 0.11.13
### Patch Changes ### Patch Changes

View File

@@ -1,6 +1,6 @@
{ {
"name": "@nhost/dashboard", "name": "@nhost/dashboard",
"version": "0.11.13", "version": "0.11.20",
"private": true, "private": true,
"scripts": { "scripts": {
"preinstall": "npx only-allow pnpm", "preinstall": "npx only-allow pnpm",

View File

@@ -52,7 +52,9 @@ function ControlledAutocomplete(
return ( return (
<Autocomplete <Autocomplete
inputValue={typeof field.value === 'string' ? field.value : undefined} inputValue={
typeof field.value !== 'object' ? field.value.toString() : undefined
}
{...props} {...props}
{...field} {...field}
ref={mergeRefs([field.ref, ref])} ref={mergeRefs([field.ref, ref])}

View File

@@ -1,31 +1,8 @@
import type { DialogFormProps } from '@/types/common';
import type { CommonDialogProps } from '@/ui/v2/Dialog'; import type { CommonDialogProps } from '@/ui/v2/Dialog';
import type { ReactNode } from 'react'; import type { ReactElement, ReactNode } from 'react';
import { createContext } from 'react'; import { createContext } from 'react';
/**
* Available dialog types.
*/
export type DialogType =
| 'EDIT_WORKSPACE_NAME'
| 'CREATE_RECORD'
| 'CREATE_COLUMN'
| 'EDIT_COLUMN'
| 'CREATE_TABLE'
| 'EDIT_TABLE'
| 'EDIT_PERMISSIONS'
| 'CREATE_FOREIGN_KEY'
| 'EDIT_FOREIGN_KEY'
| 'CREATE_ROLE'
| 'EDIT_ROLE'
| 'CREATE_USER'
| 'CREATE_PERMISSION_VARIABLE'
| 'EDIT_PERMISSION_VARIABLE'
| 'CREATE_ENVIRONMENT_VARIABLE'
| 'EDIT_ENVIRONMENT_VARIABLE'
| 'EDIT_USER'
| 'EDIT_USER_PASSWORD'
| 'EDIT_JWT_SECRET';
export interface DialogConfig<TPayload = unknown> { export interface DialogConfig<TPayload = unknown> {
/** /**
* Title of the dialog. * Title of the dialog.
@@ -41,21 +18,36 @@ export interface DialogConfig<TPayload = unknown> {
payload?: TPayload; payload?: TPayload;
} }
export interface OpenDialogOptions {
/**
* Title of the dialog.
*/
title: ReactNode;
/**
* Component to render inside the dialog skeleton.
*/
component: ReactElement<{
location?: 'drawer' | 'dialog';
onCancel?: () => void;
onSubmit?: (args?: any) => Promise<any> | void;
}>;
/**
* Props to pass to the root dialog component.
*/
props?: Partial<CommonDialogProps>;
}
export interface DialogContextProps { export interface DialogContextProps {
/** /**
* Call this function to open a dialog. * Call this function to open a dialog. It will automatically apply the
* necessary functionality to the dialog.
*/ */
openDialog: <TPayload = unknown>( openDialog: (options: OpenDialogOptions) => void;
type: DialogType,
config?: DialogConfig<TPayload>,
) => void;
/** /**
* Call this function to open a drawer. * Call this function to open a drawer. It will automatically apply the
* necessary functionality to the drawer.
*/ */
openDrawer: <TPayload = unknown>( openDrawer: (options: OpenDialogOptions) => void;
type: DialogType,
config?: DialogConfig<TPayload>,
) => void;
/** /**
* Call this function to open an alert dialog. * Call this function to open an alert dialog.
*/ */
@@ -87,7 +79,7 @@ export interface DialogContextProps {
*/ */
onDirtyStateChange: ( onDirtyStateChange: (
isDirty: boolean, isDirty: boolean,
location?: 'drawer' | 'dialog', location?: DialogFormProps['location'],
) => void; ) => void;
/** /**
* Call this function to open a dirty confirmation dialog. * Call this function to open a dirty confirmation dialog.

View File

@@ -1,30 +1,12 @@
import RetryableErrorBoundary from '@/components/common/RetryableErrorBoundary'; import RetryableErrorBoundary from '@/components/common/RetryableErrorBoundary';
import CreateForeignKeyForm from '@/components/dataBrowser/CreateForeignKeyForm';
import EditForeignKeyForm from '@/components/dataBrowser/EditForeignKeyForm';
import EditWorkspaceNameForm from '@/components/home/EditWorkspaceNameForm';
import CreateEnvironmentVariableForm from '@/components/settings/environmentVariables/CreateEnvironmentVariableForm';
import EditEnvironmentVariableForm from '@/components/settings/environmentVariables/EditEnvironmentVariableForm';
import EditJwtSecretForm from '@/components/settings/environmentVariables/EditJwtSecretForm';
import CreatePermissionVariableForm from '@/components/settings/permissions/CreatePermissionVariableForm';
import EditPermissionVariableForm from '@/components/settings/permissions/EditPermissionVariableForm';
import CreateRoleForm from '@/components/settings/roles/CreateRoleForm';
import EditRoleForm from '@/components/settings/roles/EditRoleForm';
import CreateUserForm from '@/components/users/CreateUserForm';
import EditUserForm from '@/components/users/EditUserForm';
import EditUserPasswordForm from '@/components/users/EditUserPasswordForm';
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
import AlertDialog from '@/ui/v2/AlertDialog'; import AlertDialog from '@/ui/v2/AlertDialog';
import { BaseDialog } from '@/ui/v2/Dialog'; import { BaseDialog } from '@/ui/v2/Dialog';
import Drawer from '@/ui/v2/Drawer'; import Drawer from '@/ui/v2/Drawer';
import dynamic from 'next/dynamic';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import type { import type { BaseSyntheticEvent, PropsWithChildren } from 'react';
BaseSyntheticEvent,
DetailedHTMLProps,
HTMLProps,
PropsWithChildren,
} from 'react';
import { import {
cloneElement,
isValidElement,
useCallback, useCallback,
useEffect, useEffect,
useMemo, useMemo,
@@ -33,7 +15,7 @@ import {
useState, useState,
} from 'react'; } from 'react';
import { twMerge } from 'tailwind-merge'; import { twMerge } from 'tailwind-merge';
import type { DialogConfig, DialogType } from './DialogContext'; import type { DialogConfig, OpenDialogOptions } from './DialogContext';
import DialogContext from './DialogContext'; import DialogContext from './DialogContext';
import { import {
alertDialogReducer, alertDialogReducer,
@@ -41,67 +23,11 @@ import {
drawerReducer, drawerReducer,
} from './dialogReducers'; } from './dialogReducers';
function LoadingComponent({
className,
...props
}: DetailedHTMLProps<HTMLProps<HTMLDivElement>, HTMLDivElement> = {}) {
return (
<div
{...props}
className={twMerge(
'grid items-center justify-center px-6 py-4',
className,
)}
>
<ActivityIndicator
circularProgressProps={{ className: 'w-5 h-5' }}
label="Loading form..."
/>
</div>
);
}
const CreateRecordForm = dynamic(
() => import('@/components/dataBrowser/CreateRecordForm'),
{ ssr: false, loading: () => LoadingComponent() },
);
const CreateColumnForm = dynamic(
() => import('@/components/dataBrowser/CreateColumnForm'),
{ ssr: false, loading: () => LoadingComponent() },
);
const EditColumnForm = dynamic(
() => import('@/components/dataBrowser/EditColumnForm'),
{ ssr: false, loading: () => LoadingComponent() },
);
const CreateTableForm = dynamic(
() => import('@/components/dataBrowser/CreateTableForm'),
{ ssr: false, loading: () => LoadingComponent() },
);
const EditTableForm = dynamic(
() => import('@/components/dataBrowser/EditTableForm'),
{ ssr: false, loading: () => LoadingComponent() },
);
const EditPermissionsForm = dynamic(
() => import('@/components/dataBrowser/EditPermissionsForm'),
{ ssr: false, loading: () => LoadingComponent() },
);
function DialogProvider({ children }: PropsWithChildren<unknown>) { function DialogProvider({ children }: PropsWithChildren<unknown>) {
const router = useRouter(); const router = useRouter();
const [ const [
{ { open: dialogOpen, title: dialogTitle, activeDialog, dialogProps },
open: dialogOpen,
activeDialogType,
dialogProps,
title: dialogTitle,
payload: dialogPayload,
},
dialogDispatch, dialogDispatch,
] = useReducer(dialogReducer, { ] = useReducer(dialogReducer, {
open: false, open: false,
@@ -110,10 +36,9 @@ function DialogProvider({ children }: PropsWithChildren<unknown>) {
const [ const [
{ {
open: drawerOpen, open: drawerOpen,
activeDialogType: activeDrawerType,
dialogProps: drawerProps,
title: drawerTitle, title: drawerTitle,
payload: drawerPayload, activeDialog: activeDrawer,
dialogProps: drawerProps,
}, },
drawerDispatch, drawerDispatch,
] = useReducer(drawerReducer, { ] = useReducer(drawerReducer, {
@@ -136,12 +61,9 @@ function DialogProvider({ children }: PropsWithChildren<unknown>) {
const isDialogDirty = useRef(false); const isDialogDirty = useRef(false);
const [showDirtyConfirmation, setShowDirtyConfirmation] = useState(false); const [showDirtyConfirmation, setShowDirtyConfirmation] = useState(false);
const openDialog = useCallback( const openDialog = useCallback((options: OpenDialogOptions) => {
<TConfig,>(type: DialogType, config?: DialogConfig<TConfig>) => { dialogDispatch({ type: 'OPEN_DIALOG', payload: options });
dialogDispatch({ type: 'OPEN_DIALOG', payload: { type, config } }); }, []);
},
[],
);
const closeDialog = useCallback(() => { const closeDialog = useCallback(() => {
dialogDispatch({ type: 'HIDE_DIALOG' }); dialogDispatch({ type: 'HIDE_DIALOG' });
@@ -152,12 +74,9 @@ function DialogProvider({ children }: PropsWithChildren<unknown>) {
dialogDispatch({ type: 'CLEAR_DIALOG_CONTENT' }); dialogDispatch({ type: 'CLEAR_DIALOG_CONTENT' });
}, []); }, []);
const openDrawer = useCallback( const openDrawer = useCallback((options: OpenDialogOptions) => {
<TConfig,>(type: DialogType, config?: DialogConfig<TConfig>) => { drawerDispatch({ type: 'OPEN_DRAWER', payload: options });
drawerDispatch({ type: 'OPEN_DRAWER', payload: { type, config } }); }, []);
},
[],
);
const closeDrawer = useCallback(() => { const closeDrawer = useCallback(() => {
drawerDispatch({ type: 'HIDE_DRAWER' }); drawerDispatch({ type: 'HIDE_DRAWER' });
@@ -228,9 +147,6 @@ function DialogProvider({ children }: PropsWithChildren<unknown>) {
[closeDialog, openDirtyConfirmation], [closeDialog, openDirtyConfirmation],
); );
// We are coupling this logic with the location of the dialog content which is
// not ideal. We shoule figure out a better logic for tracking the dirty
// state in the future.
const onDirtyStateChange = useCallback( const onDirtyStateChange = useCallback(
(dirty: boolean, location: 'drawer' | 'dialog' = 'drawer') => { (dirty: boolean, location: 'drawer' | 'dialog' = 'drawer') => {
if (location === 'dialog') { if (location === 'dialog') {
@@ -271,25 +187,6 @@ function DialogProvider({ children }: PropsWithChildren<unknown>) {
], ],
); );
const sharedDialogProps = {
...dialogPayload,
onSubmit: async (values: any) => {
await dialogPayload?.onSubmit?.(values);
closeDialog();
},
onCancel: closeDialogWithDirtyGuard,
};
const sharedDrawerProps = {
onSubmit: async () => {
await drawerPayload?.onSubmit();
closeDrawer();
},
onCancel: closeDrawerWithDirtyGuard,
};
useEffect(() => { useEffect(() => {
function handleCloseDrawerAndDialog() { function handleCloseDrawerAndDialog() {
if (isDrawerDirty.current || isDialogDirty.current) { if (isDrawerDirty.current || isDialogDirty.current) {
@@ -367,56 +264,20 @@ function DialogProvider({ children }: PropsWithChildren<unknown>) {
<RetryableErrorBoundary <RetryableErrorBoundary
errorMessageProps={{ className: 'pt-0 pb-5 px-6' }} errorMessageProps={{ className: 'pt-0 pb-5 px-6' }}
> >
{activeDialogType === 'EDIT_WORKSPACE_NAME' && ( {isValidElement(activeDialog)
<EditWorkspaceNameForm {...sharedDialogProps} /> ? cloneElement(activeDialog, {
)} ...activeDialog.props,
location: 'dialog',
{activeDialogType === 'CREATE_FOREIGN_KEY' && ( onSubmit: async (values?: any) => {
<CreateForeignKeyForm {...sharedDialogProps} /> await activeDialog?.props?.onSubmit?.(values);
)} closeDialog();
},
{activeDialogType === 'EDIT_FOREIGN_KEY' && ( onCancel: () => {
<EditForeignKeyForm {...sharedDialogProps} /> activeDialog?.props?.onCancel?.();
)} closeDialogWithDirtyGuard();
},
{activeDialogType === 'CREATE_ROLE' && ( })
<CreateRoleForm {...sharedDialogProps} /> : null}
)}
{activeDialogType === 'EDIT_ROLE' && (
<EditRoleForm {...sharedDialogProps} />
)}
{activeDialogType === 'CREATE_USER' && (
<CreateUserForm {...sharedDialogProps} />
)}
{activeDialogType === 'CREATE_PERMISSION_VARIABLE' && (
<CreatePermissionVariableForm {...sharedDialogProps} />
)}
{activeDialogType === 'EDIT_PERMISSION_VARIABLE' && (
<EditPermissionVariableForm {...sharedDialogProps} />
)}
{activeDialogType === 'CREATE_ENVIRONMENT_VARIABLE' && (
<CreateEnvironmentVariableForm {...sharedDialogProps} />
)}
{activeDialogType === 'EDIT_ENVIRONMENT_VARIABLE' && (
<EditEnvironmentVariableForm {...sharedDialogProps} />
)}
{activeDialogType === 'EDIT_USER_PASSWORD' && (
<EditUserPasswordForm
{...sharedDialogProps}
user={sharedDialogProps?.user}
/>
)}
{activeDialogType === 'EDIT_JWT_SECRET' && (
<EditJwtSecretForm {...sharedDialogProps} />
)}
</RetryableErrorBoundary> </RetryableErrorBoundary>
</BaseDialog> </BaseDialog>
@@ -436,51 +297,20 @@ function DialogProvider({ children }: PropsWithChildren<unknown>) {
}} }}
> >
<RetryableErrorBoundary> <RetryableErrorBoundary>
{activeDrawerType === 'CREATE_RECORD' && ( {isValidElement(activeDrawer)
<CreateRecordForm ? cloneElement(activeDrawer, {
{...sharedDrawerProps} ...activeDrawer.props,
columns={drawerPayload?.columns} location: 'drawer',
/> onSubmit: async (values?: any) => {
)} await activeDrawer?.props?.onSubmit?.(values);
closeDrawer();
{activeDrawerType === 'CREATE_COLUMN' && ( },
<CreateColumnForm {...sharedDrawerProps} /> onCancel: () => {
)} activeDrawer?.props?.onCancel?.();
closeDrawerWithDirtyGuard();
{activeDrawerType === 'EDIT_COLUMN' && ( },
<EditColumnForm })
{...sharedDrawerProps} : null}
column={drawerPayload?.column}
/>
)}
{activeDrawerType === 'CREATE_TABLE' && (
<CreateTableForm
{...sharedDrawerProps}
schema={drawerPayload?.schema}
/>
)}
{activeDrawerType === 'EDIT_TABLE' && (
<EditTableForm
{...sharedDrawerProps}
table={drawerPayload?.table}
schema={drawerPayload?.schema}
/>
)}
{activeDrawerType === 'EDIT_PERMISSIONS' && (
<EditPermissionsForm
{...sharedDrawerProps}
disabled={drawerPayload?.disabled}
schema={drawerPayload?.schema}
table={drawerPayload?.table}
/>
)}
{activeDrawerType === 'EDIT_USER' && (
<EditUserForm {...sharedDrawerProps} {...drawerPayload} />
)}
</RetryableErrorBoundary> </RetryableErrorBoundary>
</Drawer> </Drawer>

View File

@@ -1,6 +1,6 @@
import type { CommonDialogProps } from '@/ui/v2/Dialog'; import type { CommonDialogProps } from '@/ui/v2/Dialog';
import type { ReactNode } from 'react'; import type { ReactElement, ReactNode } from 'react';
import type { DialogConfig, DialogType } from './DialogContext'; import type { DialogConfig, OpenDialogOptions } from './DialogContext';
export interface DialogState { export interface DialogState {
/** /**
@@ -12,9 +12,13 @@ export interface DialogState {
*/ */
open?: boolean; open?: boolean;
/** /**
* Type of the currently active dialog. * Component to render inside the dialog skeleton.
*/ */
activeDialogType?: DialogType; activeDialog?: ReactElement<{
location?: 'drawer' | 'dialog';
onCancel?: () => void;
onSubmit?: (args?: any) => Promise<any> | void;
}>;
/** /**
* Props passed to the currently active dialog. * Props passed to the currently active dialog.
*/ */
@@ -27,10 +31,7 @@ export interface DialogState {
} }
export type DialogAction = export type DialogAction =
| { | { type: 'OPEN_DIALOG'; payload: OpenDialogOptions }
type: 'OPEN_DIALOG';
payload: { type: DialogType; config?: DialogConfig };
}
| { type: 'HIDE_DIALOG' } | { type: 'HIDE_DIALOG' }
| { type: 'CLEAR_DIALOG_CONTENT' }; | { type: 'CLEAR_DIALOG_CONTENT' };
@@ -50,10 +51,9 @@ export function dialogReducer(
return { return {
...state, ...state,
open: true, open: true,
activeDialogType: action.payload?.type, title: action.payload.title,
dialogProps: action.payload.config?.props, activeDialog: action.payload.component,
title: action.payload.config?.title, dialogProps: action.payload.props,
payload: action.payload.config?.payload,
}; };
case 'HIDE_DIALOG': case 'HIDE_DIALOG':
return { return {
@@ -64,8 +64,7 @@ export function dialogReducer(
return { return {
...state, ...state,
title: undefined, title: undefined,
payload: undefined, activeDialog: undefined,
activeDialogType: undefined,
dialogProps: undefined, dialogProps: undefined,
}; };
default: default:
@@ -74,10 +73,7 @@ export function dialogReducer(
} }
export type DrawerAction = export type DrawerAction =
| { | { type: 'OPEN_DRAWER'; payload: OpenDialogOptions }
type: 'OPEN_DRAWER';
payload: { type: DialogType; config?: DialogConfig };
}
| { type: 'HIDE_DRAWER' } | { type: 'HIDE_DRAWER' }
| { type: 'CLEAR_DRAWER_CONTENT' }; | { type: 'CLEAR_DRAWER_CONTENT' };
@@ -97,10 +93,9 @@ export function drawerReducer(
return { return {
...state, ...state,
open: true, open: true,
activeDialogType: action.payload?.type, title: action.payload.title,
dialogProps: action.payload.config?.props, activeDialog: action.payload.component,
title: action.payload.config?.title, dialogProps: action.payload.props,
payload: action.payload.config?.payload,
}; };
case 'HIDE_DRAWER': case 'HIDE_DRAWER':
return { return {
@@ -111,8 +106,7 @@ export function drawerReducer(
return { return {
...state, ...state,
title: undefined, title: undefined,
payload: undefined, activeDialog: undefined,
activeDialogType: undefined,
dialogProps: undefined, dialogProps: undefined,
}; };
default: default:

View File

@@ -0,0 +1,26 @@
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
import type { BoxProps } from '@/ui/v2/Box';
import Box from '@/ui/v2/Box';
import { twMerge } from 'tailwind-merge';
export interface FormActivityIndicatorProps extends BoxProps {}
export default function FormActivityIndicator({
className,
...props
}: FormActivityIndicatorProps) {
return (
<Box
{...props}
className={twMerge(
'grid items-center justify-center px-6 py-4',
className,
)}
>
<ActivityIndicator
circularProgressProps={{ className: 'w-5 h-5' }}
label="Loading form..."
/>
</Box>
);
}

View File

@@ -0,0 +1,2 @@
export * from './FormActivityIndicator';
export { default } from './FormActivityIndicator';

View File

@@ -3,6 +3,7 @@ import ControlledCheckbox from '@/components/common/ControlledCheckbox';
import { useDialog } from '@/components/common/DialogProvider'; import { useDialog } from '@/components/common/DialogProvider';
import Form from '@/components/common/Form'; import Form from '@/components/common/Form';
import InlineCode from '@/components/common/InlineCode'; import InlineCode from '@/components/common/InlineCode';
import type { DialogFormProps } from '@/types/common';
import type { ColumnType, DatabaseColumn } from '@/types/dataBrowser'; import type { ColumnType, DatabaseColumn } from '@/types/dataBrowser';
import Box from '@/ui/v2/Box'; import Box from '@/ui/v2/Box';
import Button from '@/ui/v2/Button'; import Button from '@/ui/v2/Button';
@@ -22,7 +23,7 @@ import ForeignKeyEditor from './ForeignKeyEditor';
export type BaseColumnFormValues = DatabaseColumn; export type BaseColumnFormValues = DatabaseColumn;
export interface BaseColumnFormProps { export interface BaseColumnFormProps extends DialogFormProps {
/** /**
* Function to be called when the form is submitted. * Function to be called when the form is submitted.
*/ */
@@ -60,6 +61,7 @@ export default function BaseColumnForm({
onSubmit: handleExternalSubmit, onSubmit: handleExternalSubmit,
onCancel, onCancel,
submitButtonText = 'Save', submitButtonText = 'Save',
location,
}: BaseColumnFormProps) { }: BaseColumnFormProps) {
const { onDirtyStateChange } = useDialog(); const { onDirtyStateChange } = useDialog();
@@ -91,8 +93,8 @@ export default function BaseColumnForm({
const isDirty = Object.keys(dirtyFields).length > 0; const isDirty = Object.keys(dirtyFields).length > 0;
useEffect(() => { useEffect(() => {
onDirtyStateChange(isDirty, 'drawer'); onDirtyStateChange(isDirty, location);
}, [isDirty, onDirtyStateChange]); }, [isDirty, location, onDirtyStateChange]);
return ( return (
<Form <Form

View File

@@ -1,5 +1,6 @@
import { useDialog } from '@/components/common/DialogProvider'; import { useDialog } from '@/components/common/DialogProvider';
import type { BaseForeignKeyFormValues } from '@/components/dataBrowser/BaseForeignKeyForm'; import CreateForeignKeyForm from '@/components/dataBrowser/CreateForeignKeyForm';
import EditForeignKeyForm from '@/components/dataBrowser/EditForeignKeyForm';
import type { DatabaseColumn } from '@/types/dataBrowser'; import type { DatabaseColumn } from '@/types/dataBrowser';
import Box from '@/ui/v2/Box'; import Box from '@/ui/v2/Box';
import Button from '@/ui/v2/Button'; import Button from '@/ui/v2/Button';
@@ -29,7 +30,7 @@ const ForeignKeyEditorInput = forwardRef(
) => { ) => {
const { openDialog } = useDialog(); const { openDialog } = useDialog();
const { setValue } = useFormContext(); const { setValue } = useFormContext();
const column = useWatch<Partial<DatabaseColumn>>(); const column = useWatch() as DatabaseColumn;
const { foreignKeyRelation } = column; const { foreignKeyRelation } = column;
if (!column.foreignKeyRelation) { if (!column.foreignKeyRelation) {
@@ -39,8 +40,8 @@ const ForeignKeyEditorInput = forwardRef(
className="py-1" className="py-1"
disabled={!column.name || !column.type} disabled={!column.name || !column.type}
ref={ref} ref={ref}
onClick={() => onClick={() => {
openDialog('CREATE_FOREIGN_KEY', { openDialog({
title: ( title: (
<span className="grid grid-flow-row"> <span className="grid grid-flow-row">
<span>Add a Foreign Key Relation</span> <span>Add a Foreign Key Relation</span>
@@ -51,16 +52,18 @@ const ForeignKeyEditorInput = forwardRef(
</Text> </Text>
</span> </span>
), ),
payload: { component: (
selectedColumn: column.name, <CreateForeignKeyForm
availableColumns: [column], selectedColumn={column.name}
onSubmit: (values: BaseForeignKeyFormValues) => { availableColumns={[column]}
setValue('foreignKeyRelation', values); onSubmit={(values) => {
onCreateSubmit(); setValue('foreignKeyRelation', values);
}, onCreateSubmit();
}, }}
}) />
} ),
});
}}
> >
Add Foreign Key Add Foreign Key
</Button> </Button>
@@ -86,20 +89,22 @@ const ForeignKeyEditorInput = forwardRef(
<div className="grid grid-flow-col"> <div className="grid grid-flow-col">
<Button <Button
ref={ref} ref={ref}
onClick={() => onClick={() => {
openDialog('EDIT_FOREIGN_KEY', { openDialog({
title: 'Edit Foreign Key Relation', title: 'Edit Foreign Key Relation',
payload: { component: (
foreignKeyRelation, <EditForeignKeyForm
availableColumns: [column], foreignKeyRelation={foreignKeyRelation}
selectedColumn: column.name, selectedColumn={column.name}
onSubmit: (values: BaseForeignKeyFormValues) => { availableColumns={[column]}
setValue('foreignKeyRelation', values); onSubmit={(values) => {
onEditSubmit(); setValue('foreignKeyRelation', values);
}, onEditSubmit();
}, }}
}) />
} ),
});
}}
variant="borderless" variant="borderless"
className="min-w-[initial] py-1 px-2" className="min-w-[initial] py-1 px-2"
> >

View File

@@ -2,6 +2,7 @@ import ControlledSelect from '@/components/common/ControlledSelect';
import { useDialog } from '@/components/common/DialogProvider'; import { useDialog } from '@/components/common/DialogProvider';
import Form from '@/components/common/Form'; import Form from '@/components/common/Form';
import useDatabaseQuery from '@/hooks/dataBrowser/useDatabaseQuery'; import useDatabaseQuery from '@/hooks/dataBrowser/useDatabaseQuery';
import type { DialogFormProps } from '@/types/common';
import type { DatabaseColumn, ForeignKeyRelation } from '@/types/dataBrowser'; import type { DatabaseColumn, ForeignKeyRelation } from '@/types/dataBrowser';
import Box from '@/ui/v2/Box'; import Box from '@/ui/v2/Box';
import Button from '@/ui/v2/Button'; import Button from '@/ui/v2/Button';
@@ -23,7 +24,7 @@ export interface BaseForeignKeyFormValues extends ForeignKeyRelation {
disableOriginColumn?: boolean; disableOriginColumn?: boolean;
} }
export interface BaseForeignKeyFormProps { export interface BaseForeignKeyFormProps extends DialogFormProps {
/** /**
* Available columns in the table. * Available columns in the table.
*/ */
@@ -64,6 +65,7 @@ export function BaseForeignKeyForm({
onSubmit: handleExternalSubmit, onSubmit: handleExternalSubmit,
onCancel, onCancel,
submitButtonText = 'Save', submitButtonText = 'Save',
location,
}: BaseForeignKeyFormProps) { }: BaseForeignKeyFormProps) {
const { onDirtyStateChange } = useDialog(); const { onDirtyStateChange } = useDialog();
@@ -86,8 +88,8 @@ export function BaseForeignKeyForm({
const isDirty = Object.keys(dirtyFields).length > 0; const isDirty = Object.keys(dirtyFields).length > 0;
useEffect(() => { useEffect(() => {
onDirtyStateChange(isDirty, 'dialog'); onDirtyStateChange(isDirty, location);
}, [isDirty, onDirtyStateChange]); }, [isDirty, location, onDirtyStateChange]);
return ( return (
<Form <Form

View File

@@ -1,6 +1,7 @@
import { useDialog } from '@/components/common/DialogProvider'; import { useDialog } from '@/components/common/DialogProvider';
import Form from '@/components/common/Form'; import Form from '@/components/common/Form';
import DatabaseRecordInputGroup from '@/components/dataBrowser/DatabaseRecordInputGroup'; import DatabaseRecordInputGroup from '@/components/dataBrowser/DatabaseRecordInputGroup';
import type { DialogFormProps } from '@/types/common';
import type { import type {
ColumnInsertOptions, ColumnInsertOptions,
DataBrowserGridColumn, DataBrowserGridColumn,
@@ -10,7 +11,7 @@ import Button from '@/ui/v2/Button';
import { useEffect } from 'react'; import { useEffect } from 'react';
import { useFormContext } from 'react-hook-form'; import { useFormContext } from 'react-hook-form';
export interface BaseRecordFormProps { export interface BaseRecordFormProps extends DialogFormProps {
/** /**
* The columns of the table. * The columns of the table.
*/ */
@@ -36,6 +37,7 @@ export default function BaseRecordForm({
onSubmit: handleExternalSubmit, onSubmit: handleExternalSubmit,
onCancel, onCancel,
submitButtonText = 'Save', submitButtonText = 'Save',
location,
}: BaseRecordFormProps) { }: BaseRecordFormProps) {
const { onDirtyStateChange } = useDialog(); const { onDirtyStateChange } = useDialog();
const { requiredColumns, optionalColumns } = columns.reduce( const { requiredColumns, optionalColumns } = columns.reduce(
@@ -70,8 +72,8 @@ export default function BaseRecordForm({
const isDirty = Object.keys(dirtyFields).length > 0; const isDirty = Object.keys(dirtyFields).length > 0;
useEffect(() => { useEffect(() => {
onDirtyStateChange(isDirty, 'drawer'); onDirtyStateChange(isDirty, location);
}, [isDirty, onDirtyStateChange]); }, [isDirty, location, onDirtyStateChange]);
// Stores columns in a map to have constant time lookup. This is necessary // Stores columns in a map to have constant time lookup. This is necessary
// for tables with many columns. // for tables with many columns.

View File

@@ -1,6 +1,7 @@
import { useDialog } from '@/components/common/DialogProvider'; import { useDialog } from '@/components/common/DialogProvider';
import Form from '@/components/common/Form'; import Form from '@/components/common/Form';
import { baseColumnValidationSchema } from '@/components/dataBrowser/BaseColumnForm'; import { baseColumnValidationSchema } from '@/components/dataBrowser/BaseColumnForm';
import type { DialogFormProps } from '@/types/common';
import type { DatabaseTable, ForeignKeyRelation } from '@/types/dataBrowser'; import type { DatabaseTable, ForeignKeyRelation } from '@/types/dataBrowser';
import Box from '@/ui/v2/Box'; import Box from '@/ui/v2/Box';
import Button from '@/ui/v2/Button'; import Button from '@/ui/v2/Button';
@@ -30,7 +31,7 @@ export interface BaseTableFormValues
foreignKeyRelations?: ForeignKeyRelation[]; foreignKeyRelations?: ForeignKeyRelation[];
} }
export interface BaseTableFormProps { export interface BaseTableFormProps extends DialogFormProps {
/** /**
* Function to be called when the form is submitted. * Function to be called when the form is submitted.
*/ */
@@ -99,7 +100,9 @@ function NameInput() {
function FormFooter({ function FormFooter({
onCancel, onCancel,
submitButtonText, submitButtonText,
}: Pick<BaseTableFormProps, 'onCancel' | 'submitButtonText'>) { location,
}: Pick<BaseTableFormProps, 'onCancel' | 'submitButtonText'> &
Pick<DialogFormProps, 'location'>) {
const { onDirtyStateChange } = useDialog(); const { onDirtyStateChange } = useDialog();
const { isSubmitting, dirtyFields } = useFormState(); const { isSubmitting, dirtyFields } = useFormState();
@@ -108,8 +111,8 @@ function FormFooter({
const isDirty = Object.keys(dirtyFields).length > 0; const isDirty = Object.keys(dirtyFields).length > 0;
useEffect(() => { useEffect(() => {
onDirtyStateChange(isDirty, 'drawer'); onDirtyStateChange(isDirty, location);
}, [isDirty, onDirtyStateChange]); }, [isDirty, location, onDirtyStateChange]);
return ( return (
<Box className="grid flex-shrink-0 grid-flow-col justify-between gap-3 border-t-1 p-2"> <Box className="grid flex-shrink-0 grid-flow-col justify-between gap-3 border-t-1 p-2">
@@ -135,6 +138,7 @@ function FormFooter({
} }
export default function BaseTableForm({ export default function BaseTableForm({
location,
onSubmit: handleExternalSubmit, onSubmit: handleExternalSubmit,
onCancel, onCancel,
submitButtonText = 'Save', submitButtonText = 'Save',
@@ -168,7 +172,11 @@ export default function BaseTableForm({
<ForeignKeyEditorSection /> <ForeignKeyEditorSection />
</div> </div>
<FormFooter onCancel={onCancel} submitButtonText={submitButtonText} /> <FormFooter
onCancel={onCancel}
submitButtonText={submitButtonText}
location={location}
/>
</Form> </Form>
); );
} }

View File

@@ -1,5 +1,7 @@
import { useDialog } from '@/components/common/DialogProvider'; import { useDialog } from '@/components/common/DialogProvider';
import type { BaseForeignKeyFormValues } from '@/components/dataBrowser/BaseForeignKeyForm'; import type { BaseForeignKeyFormValues } from '@/components/dataBrowser/BaseForeignKeyForm';
import CreateForeignKeyForm from '@/components/dataBrowser/CreateForeignKeyForm';
import EditForeignKeyForm from '@/components/dataBrowser/EditForeignKeyForm';
import type { DatabaseColumn, ForeignKeyRelation } from '@/types/dataBrowser'; import type { DatabaseColumn, ForeignKeyRelation } from '@/types/dataBrowser';
import Button from '@/ui/v2/Button'; import Button from '@/ui/v2/Button';
import PlusIcon from '@/ui/v2/icons/PlusIcon'; import PlusIcon from '@/ui/v2/icons/PlusIcon';
@@ -68,18 +70,19 @@ export default function ForeignKeyEditorSection() {
onEdit={() => { onEdit={() => {
const primaryKeyIndex = getValues('primaryKeyIndex'); const primaryKeyIndex = getValues('primaryKeyIndex');
openDialog('EDIT_FOREIGN_KEY', { openDialog({
title: 'Edit Foreign Key Relation', title: 'Edit Foreign Key Relation',
payload: { component: (
foreignKeyRelation: fields[index], <EditForeignKeyForm
availableColumns: columns.map((column, columnIndex) => foreignKeyRelation={fields[index] as ForeignKeyRelation}
columnIndex === primaryKeyIndex availableColumns={columns.map((column, columnIndex) =>
? { ...column, isPrimary: true } columnIndex === primaryKeyIndex
: column, ? { ...column, isPrimary: true }
), : column,
onSubmit: (values: BaseForeignKeyFormValues) => )}
handleEdit(values, index), onSubmit={(values) => handleEdit(values, index)}
}, />
),
}); });
}} }}
onDelete={() => remove(index)} onDelete={() => remove(index)}
@@ -105,7 +108,7 @@ export default function ForeignKeyEditorSection() {
onClick={() => { onClick={() => {
const primaryKeyIndex = getValues('primaryKeyIndex'); const primaryKeyIndex = getValues('primaryKeyIndex');
openDialog('CREATE_FOREIGN_KEY', { openDialog({
title: ( title: (
<span className="grid grid-flow-row"> <span className="grid grid-flow-row">
<span>Add a Foreign Key Relation</span> <span>Add a Foreign Key Relation</span>
@@ -116,14 +119,16 @@ export default function ForeignKeyEditorSection() {
</Text> </Text>
</span> </span>
), ),
payload: { component: (
availableColumns: columns.map((column, index) => <CreateForeignKeyForm
index === primaryKeyIndex availableColumns={columns.map((column, index) =>
? { ...column, isPrimary: true } index === primaryKeyIndex
: column, ? { ...column, isPrimary: true }
), : column,
onSubmit: handleCreate, )}
}, onSubmit={handleCreate}
/>
),
}); });
}} }}
> >

View File

@@ -15,11 +15,11 @@ import { useRouter } from 'next/router';
import { FormProvider, useForm } from 'react-hook-form'; import { FormProvider, useForm } from 'react-hook-form';
export interface CreateColumnFormProps export interface CreateColumnFormProps
extends Pick<BaseColumnFormProps, 'onCancel'> { extends Pick<BaseColumnFormProps, 'onCancel' | 'location'> {
/** /**
* Function to be called when the form is submitted. * Function to be called when the form is submitted.
*/ */
onSubmit?: () => Promise<void>; onSubmit?: (args?: any) => Promise<any>;
} }
export default function CreateColumnForm({ export default function CreateColumnForm({

View File

@@ -13,7 +13,10 @@ import { useState } from 'react';
import { FormProvider, useForm } from 'react-hook-form'; import { FormProvider, useForm } from 'react-hook-form';
export interface CreateForeignKeyFormProps export interface CreateForeignKeyFormProps
extends Pick<BaseForeignKeyFormProps, 'onCancel' | 'availableColumns'> { extends Pick<
BaseForeignKeyFormProps,
'onCancel' | 'availableColumns' | 'location'
> {
/** /**
* Column selected by default. * Column selected by default.
*/ */
@@ -21,7 +24,7 @@ export interface CreateForeignKeyFormProps
/** /**
* Function to be called when the form is submitted. * Function to be called when the form is submitted.
*/ */
onSubmit?: (values: BaseForeignKeyFormValues) => Promise<void>; onSubmit?: (values: BaseForeignKeyFormValues) => Promise<void> | void;
} }
export default function CreateForeignKeyForm({ export default function CreateForeignKeyForm({
@@ -51,9 +54,7 @@ export default function CreateForeignKeyForm({
setError(undefined); setError(undefined);
try { try {
if (onSubmit) { await onSubmit?.(values);
await onSubmit(values);
}
} catch (submitError) { } catch (submitError) {
if (submitError && submitError instanceof Error) { if (submitError && submitError instanceof Error) {
setError(submitError); setError(submitError);

View File

@@ -10,11 +10,11 @@ import { yupResolver } from '@hookform/resolvers/yup';
import { FormProvider, useForm } from 'react-hook-form'; import { FormProvider, useForm } from 'react-hook-form';
export interface CreateRecordFormProps export interface CreateRecordFormProps
extends Pick<BaseRecordFormProps, 'columns' | 'onCancel'> { extends Pick<BaseRecordFormProps, 'columns' | 'onCancel' | 'location'> {
/** /**
* Function to be called when the form is submitted. * Function to be called when the form is submitted.
*/ */
onSubmit?: () => Promise<void>; onSubmit?: (args?: any) => Promise<any>;
} }
export default function CreateRecordForm({ export default function CreateRecordForm({

View File

@@ -17,7 +17,7 @@ import { useRouter } from 'next/router';
import { FormProvider, useForm } from 'react-hook-form'; import { FormProvider, useForm } from 'react-hook-form';
export interface CreateTableFormProps export interface CreateTableFormProps
extends Pick<BaseTableFormProps, 'onCancel'> { extends Pick<BaseTableFormProps, 'onCancel' | 'location'> {
/** /**
* Schema where the table should be created. * Schema where the table should be created.
*/ */
@@ -25,7 +25,7 @@ export interface CreateTableFormProps
/** /**
* Function to be called when the form is submitted. * Function to be called when the form is submitted.
*/ */
onSubmit?: () => Promise<void>; onSubmit?: (args?: any) => Promise<any>;
} }
export default function CreateTableForm({ export default function CreateTableForm({

View File

@@ -5,6 +5,7 @@ import DataGridDateCell from '@/components/common/DataGridDateCell';
import DataGridNumericCell from '@/components/common/DataGridNumericCell'; import DataGridNumericCell from '@/components/common/DataGridNumericCell';
import DataGridTextCell from '@/components/common/DataGridTextCell'; import DataGridTextCell from '@/components/common/DataGridTextCell';
import { useDialog } from '@/components/common/DialogProvider'; import { useDialog } from '@/components/common/DialogProvider';
import FormActivityIndicator from '@/components/common/FormActivityIndicator';
import InlineCode from '@/components/common/InlineCode'; import InlineCode from '@/components/common/InlineCode';
import DataBrowserEmptyState from '@/components/dataBrowser/DataBrowserEmptyState'; import DataBrowserEmptyState from '@/components/dataBrowser/DataBrowserEmptyState';
import DataBrowserGridControls from '@/components/dataBrowser/DataBrowserGridControls'; import DataBrowserGridControls from '@/components/dataBrowser/DataBrowserGridControls';
@@ -28,9 +29,25 @@ import {
} from '@/utils/dataBrowser/postgresqlConstants'; } from '@/utils/dataBrowser/postgresqlConstants';
import { isSchemaLocked } from '@/utils/dataBrowser/schemaHelpers'; import { isSchemaLocked } from '@/utils/dataBrowser/schemaHelpers';
import { useQueryClient } from '@tanstack/react-query'; import { useQueryClient } from '@tanstack/react-query';
import dynamic from 'next/dynamic';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { useEffect, useMemo, useRef, useState } from 'react'; import { useEffect, useMemo, useRef, useState } from 'react';
const CreateColumnForm = dynamic(
() => import('@/components/dataBrowser/CreateColumnForm'),
{ ssr: false, loading: () => <FormActivityIndicator /> },
);
const EditColumnForm = dynamic(
() => import('@/components/dataBrowser/EditColumnForm'),
{ ssr: false, loading: () => <FormActivityIndicator /> },
);
const CreateRecordForm = dynamic(
() => import('@/components/dataBrowser/CreateRecordForm'),
{ ssr: false, loading: () => <FormActivityIndicator /> },
);
export interface DataBrowserGridProps extends Partial<DataGridProps<any>> {} export interface DataBrowserGridProps extends Partial<DataGridProps<any>> {}
export function createDataGridColumn( export function createDataGridColumn(
@@ -273,33 +290,36 @@ export default function DataBrowserGrid({
const memoizedData = useMemo(() => rows, [rows]); const memoizedData = useMemo(() => rows, [rows]);
async function handleInsertRowClick() { async function handleInsertRowClick() {
openDrawer('CREATE_RECORD', { openDrawer({
title: 'Insert a New Row', title: 'Insert a New Row',
payload: { component: (
columns: memoizedColumns, <CreateRecordForm
onSubmit: refetch, // TODO: Create proper typings for data browser columns
}, columns={memoizedColumns as unknown as DataBrowserGridColumn[]}
onSubmit={refetch}
/>
),
}); });
} }
async function handleInsertColumnClick() { async function handleInsertColumnClick() {
openDrawer('CREATE_COLUMN', { openDrawer({
title: 'Insert a New Column', title: 'Insert a New Column',
payload: { component: <CreateColumnForm onSubmit={refetch} />,
onSubmit: refetch,
},
}); });
} }
async function handleEditColumnClick( async function handleEditColumnClick(
column: DataBrowserGridColumn<NormalizedQueryDataRow>, column: DataBrowserGridColumn<NormalizedQueryDataRow>,
) { ) {
openDrawer('EDIT_COLUMN', { openDrawer({
title: 'Edit Column', title: 'Edit Column',
payload: { component: (
column, <EditColumnForm
onSubmit: () => queryClient.refetchQueries([currentTablePath]), column={column}
}, onSubmit={() => queryClient.refetchQueries([currentTablePath])}
/>
),
}); });
} }

View File

@@ -1,4 +1,5 @@
import { useDialog } from '@/components/common/DialogProvider'; import { useDialog } from '@/components/common/DialogProvider';
import FormActivityIndicator from '@/components/common/FormActivityIndicator';
import InlineCode from '@/components/common/InlineCode'; import InlineCode from '@/components/common/InlineCode';
import NavLink from '@/components/common/NavLink'; import NavLink from '@/components/common/NavLink';
import RetryableErrorBoundary from '@/components/common/RetryableErrorBoundary'; import RetryableErrorBoundary from '@/components/common/RetryableErrorBoundary';
@@ -31,11 +32,36 @@ import Select from '@/ui/v2/Select';
import Text from '@/ui/v2/Text'; import Text from '@/ui/v2/Text';
import { isSchemaLocked } from '@/utils/dataBrowser/schemaHelpers'; import { isSchemaLocked } from '@/utils/dataBrowser/schemaHelpers';
import { useQueryClient } from '@tanstack/react-query'; import { useQueryClient } from '@tanstack/react-query';
import dynamic from 'next/dynamic';
import Image from 'next/image'; import Image from 'next/image';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { twMerge } from 'tailwind-merge'; import { twMerge } from 'tailwind-merge';
const CreateTableForm = dynamic(
() => import('@/components/dataBrowser/CreateTableForm'),
{
ssr: false,
loading: () => <FormActivityIndicator />,
},
);
const EditTableForm = dynamic(
() => import('@/components/dataBrowser/EditTableForm'),
{
ssr: false,
loading: () => <FormActivityIndicator />,
},
);
const EditPermissionsForm = dynamic(
() => import('@/components/dataBrowser/EditPermissionsForm'),
{
ssr: false,
loading: () => <FormActivityIndicator />,
},
);
export interface DataBrowserSidebarProps extends Omit<BoxProps, 'children'> { export interface DataBrowserSidebarProps extends Omit<BoxProps, 'children'> {
/** /**
* Function to be called when a sidebar item is clicked. * Function to be called when a sidebar item is clicked.
@@ -200,7 +226,7 @@ function DataBrowserSidebarContent({
table: string, table: string,
disabled?: boolean, disabled?: boolean,
) { ) {
openDrawer('EDIT_PERMISSIONS', { openDrawer({
title: ( title: (
<span className="inline-grid grid-flow-col items-center gap-2"> <span className="inline-grid grid-flow-col items-center gap-2">
Permissions Permissions
@@ -208,22 +234,18 @@ function DataBrowserSidebarContent({
<Chip label="Preview" size="small" color="info" component="span" /> <Chip label="Preview" size="small" color="info" component="span" />
</span> </span>
), ),
component: (
<EditPermissionsForm
disabled={disabled}
schema={schema}
table={table}
/>
),
props: { props: {
PaperProps: { PaperProps: {
className: 'lg:w-[65%] lg:max-w-7xl', className: 'lg:w-[65%] lg:max-w-7xl',
}, },
}, },
payload: {
onSubmit: async () => {
await queryClient.refetchQueries([
`${dataSourceSlug}.${schema}.${table}`,
]);
await refetch();
},
disabled,
schema,
table,
},
}); });
} }
@@ -296,9 +318,11 @@ function DataBrowserSidebarContent({
endIcon={<PlusIcon />} endIcon={<PlusIcon />}
className="mt-1 w-full justify-between px-2" className="mt-1 w-full justify-between px-2"
onClick={() => { onClick={() => {
openDrawer('CREATE_TABLE', { openDrawer({
title: 'Create a New Table', title: 'Create a New Table',
payload: { onSubmit: refetch, schema: selectedSchema }, component: (
<CreateTableForm onSubmit={refetch} schema={selectedSchema} />
),
}); });
onSidebarItemClick(); onSidebarItemClick();
@@ -328,69 +352,68 @@ function DataBrowserSidebarContent({
className="group" className="group"
key={tablePath} key={tablePath}
secondaryAction={ secondaryAction={
!isSelectedSchemaLocked && ( <Dropdown.Root
<Dropdown.Root id="table-management-menu"
id="table-management-menu" onOpen={() => setSidebarMenuTable(tablePath)}
onOpen={() => setSidebarMenuTable(tablePath)} onClose={() => setSidebarMenuTable(undefined)}
onClose={() => setSidebarMenuTable(undefined)} >
<Dropdown.Trigger
asChild
hideChevron
disabled={tablePath === removableTable}
> >
<Dropdown.Trigger <IconButton
asChild variant="borderless"
hideChevron color={isSelected ? 'primary' : 'secondary'}
disabled={tablePath === removableTable} className={twMerge(
!isSelected &&
'opacity-0 group-focus-within:opacity-100 group-hover:opacity-100 group-active:opacity-100',
)}
> >
<IconButton <DotsHorizontalIcon />
variant="borderless" </IconButton>
color={isSelected ? 'primary' : 'secondary'} </Dropdown.Trigger>
className={twMerge(
!isSelected && <Dropdown.Content menu PaperProps={{ className: 'w-52' }}>
'opacity-0 group-focus-within:opacity-100 group-hover:opacity-100 group-active:opacity-100', {isGitHubConnected ? (
)} <Dropdown.Item
className="grid grid-flow-col items-center gap-2 p-2 text-sm+ font-medium"
onClick={() =>
handleEditPermissionClick(
table.table_schema,
table.table_name,
true,
)
}
> >
<DotsHorizontalIcon /> <UsersIcon
</IconButton> className="h-4 w-4"
</Dropdown.Trigger> sx={{ color: 'text.secondary' }}
/>
<Dropdown.Content <span>View Permissions</span>
menu </Dropdown.Item>
PaperProps={{ className: 'w-52' }} ) : (
> [
{isGitHubConnected ? ( !isSelectedSchemaLocked && (
<Dropdown.Item
className="grid grid-flow-col items-center gap-2 p-2 text-sm+ font-medium"
onClick={() =>
handleEditPermissionClick(
table.table_schema,
table.table_name,
true,
)
}
>
<UsersIcon
className="h-4 w-4"
sx={{ color: 'text.secondary' }}
/>
<span>View Permissions</span>
</Dropdown.Item>
) : (
[
<Dropdown.Item <Dropdown.Item
key="edit-table" key="edit-table"
className="grid grid-flow-col items-center gap-2 p-2 text-sm+ font-medium" className="grid grid-flow-col items-center gap-2 p-2 text-sm+ font-medium"
onClick={() => onClick={() =>
openDrawer('EDIT_TABLE', { openDrawer({
title: 'Edit Table', title: 'Edit Table',
payload: { component: (
onSubmit: async () => { <EditTableForm
await queryClient.refetchQueries([ onSubmit={async () => {
`${dataSourceSlug}.${table.table_schema}.${table.table_name}`, await queryClient.refetchQueries([
]); `${dataSourceSlug}.${table.table_schema}.${table.table_name}`,
await refetch(); ]);
}, await refetch();
schema: table.table_schema, }}
table, schema={table.table_schema}
}, table={table}
/>
),
}) })
} }
> >
@@ -400,32 +423,38 @@ function DataBrowserSidebarContent({
/> />
<span>Edit Table</span> <span>Edit Table</span>
</Dropdown.Item>, </Dropdown.Item>
),
!isSelectedSchemaLocked && (
<Divider <Divider
key="edit-table-separator" key="edit-table-separator"
component="li" component="li"
/>, />
<Dropdown.Item ),
key="edit-permissions" <Dropdown.Item
className="grid grid-flow-col items-center gap-2 p-2 text-sm+ font-medium" key="edit-permissions"
onClick={() => className="grid grid-flow-col items-center gap-2 p-2 text-sm+ font-medium"
handleEditPermissionClick( onClick={() =>
table.table_schema, handleEditPermissionClick(
table.table_name, table.table_schema,
) table.table_name,
} )
> }
<UsersIcon >
className="h-4 w-4" <UsersIcon
sx={{ color: 'text.secondary' }} className="h-4 w-4"
/> sx={{ color: 'text.secondary' }}
/>
<span>Edit Permissions</span> <span>Edit Permissions</span>
</Dropdown.Item>, </Dropdown.Item>,
!isSelectedSchemaLocked && (
<Divider <Divider
key="edit-permissions-separator" key="edit-permissions-separator"
component="li" component="li"
/>, />
),
!isSelectedSchemaLocked && (
<Dropdown.Item <Dropdown.Item
key="delete-table" key="delete-table"
className="grid grid-flow-col items-center gap-2 p-2 text-sm+ font-medium" className="grid grid-flow-col items-center gap-2 p-2 text-sm+ font-medium"
@@ -443,12 +472,12 @@ function DataBrowserSidebarContent({
/> />
<span>Delete Table</span> <span>Delete Table</span>
</Dropdown.Item>, </Dropdown.Item>
] ),
)} ]
</Dropdown.Content> )}
</Dropdown.Root> </Dropdown.Content>
) </Dropdown.Root>
} }
> >
<ListItem.Button <ListItem.Button

View File

@@ -18,7 +18,7 @@ import { useRouter } from 'next/router';
import { FormProvider, useForm } from 'react-hook-form'; import { FormProvider, useForm } from 'react-hook-form';
export interface EditColumnFormProps export interface EditColumnFormProps
extends Pick<BaseColumnFormProps, 'onCancel'> { extends Pick<BaseColumnFormProps, 'onCancel' | 'location'> {
/** /**
* Column to be edited. * Column to be edited.
*/ */

View File

@@ -14,7 +14,10 @@ import { useState } from 'react';
import { FormProvider, useForm } from 'react-hook-form'; import { FormProvider, useForm } from 'react-hook-form';
export interface EditForeignKeyFormProps export interface EditForeignKeyFormProps
extends Pick<BaseForeignKeyFormProps, 'onCancel' | 'availableColumns'> { extends Pick<
BaseForeignKeyFormProps,
'onCancel' | 'availableColumns' | 'location'
> {
/** /**
* Foreign key relation to be edited. * Foreign key relation to be edited.
*/ */
@@ -26,7 +29,7 @@ export interface EditForeignKeyFormProps
/** /**
* Function to be called when the form is submitted. * Function to be called when the form is submitted.
*/ */
onSubmit?: (values: BaseForeignKeyFormValues) => Promise<void>; onSubmit?: (values: BaseForeignKeyFormValues) => Promise<void> | void;
} }
export default function EditForeignKeyForm({ export default function EditForeignKeyForm({
@@ -57,9 +60,7 @@ export default function EditForeignKeyForm({
setError(undefined); setError(undefined);
try { try {
if (onSubmit) { await onSubmit?.(values);
await onSubmit(values);
}
} catch (submitError) { } catch (submitError) {
if (submitError && submitError instanceof Error) { if (submitError && submitError instanceof Error) {
setError(submitError); setError(submitError);

View File

@@ -3,6 +3,7 @@ import useMetadataQuery from '@/hooks/dataBrowser/useMetadataQuery';
import useTableQuery from '@/hooks/dataBrowser/useTableQuery'; import useTableQuery from '@/hooks/dataBrowser/useTableQuery';
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication'; import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
import { useRemoteApplicationGQLClient } from '@/hooks/useRemoteApplicationGQLClient'; import { useRemoteApplicationGQLClient } from '@/hooks/useRemoteApplicationGQLClient';
import type { DialogFormProps } from '@/types/common';
import type { import type {
DatabaseAccessLevel, DatabaseAccessLevel,
DatabaseAction, DatabaseAction,
@@ -30,7 +31,7 @@ import { twMerge } from 'tailwind-merge';
import RolePermissionEditorForm from './RolePermissionEditorForm'; import RolePermissionEditorForm from './RolePermissionEditorForm';
import RolePermissionsRow from './RolePermissionsRow'; import RolePermissionsRow from './RolePermissionsRow';
export interface EditPermissionsFormProps { export interface EditPermissionsFormProps extends DialogFormProps {
/** /**
* Determines whether the form is disabled or not. * Determines whether the form is disabled or not.
*/ */
@@ -54,6 +55,7 @@ export default function EditPermissionsForm({
schema, schema,
table, table,
onCancel, onCancel,
location,
}: EditPermissionsFormProps) { }: EditPermissionsFormProps) {
const [role, setRole] = useState<string>(); const [role, setRole] = useState<string>();
const [action, setAction] = useState<DatabaseAction>(); const [action, setAction] = useState<DatabaseAction>();
@@ -181,6 +183,7 @@ export default function EditPermissionsForm({
return ( return (
<RolePermissionEditorForm <RolePermissionEditorForm
location={location}
resourceVersion={metadata?.resourceVersion} resourceVersion={metadata?.resourceVersion}
disabled={disabled} disabled={disabled}
schema={schema} schema={schema}

View File

@@ -2,6 +2,7 @@ import { useDialog } from '@/components/common/DialogProvider';
import Form from '@/components/common/Form'; import Form from '@/components/common/Form';
import HighlightedText from '@/components/common/HighlightedText'; import HighlightedText from '@/components/common/HighlightedText';
import useManagePermissionMutation from '@/hooks/dataBrowser/useManagePermissionMutation'; import useManagePermissionMutation from '@/hooks/dataBrowser/useManagePermissionMutation';
import type { DialogFormProps } from '@/types/common';
import type { import type {
DatabaseAction, DatabaseAction,
HasuraMetadataPermission, HasuraMetadataPermission,
@@ -72,7 +73,7 @@ export interface RolePermissionEditorFormValues {
computedFields?: string[]; computedFields?: string[];
} }
export interface RolePermissionEditorFormProps { export interface RolePermissionEditorFormProps extends DialogFormProps {
/** /**
* Determines whether or not the form is disabled. * Determines whether or not the form is disabled.
*/ */
@@ -169,6 +170,7 @@ export default function RolePermissionEditorForm({
onCancel, onCancel,
permission, permission,
disabled, disabled,
location,
}: RolePermissionEditorFormProps) { }: RolePermissionEditorFormProps) {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { const {
@@ -214,8 +216,8 @@ export default function RolePermissionEditorForm({
const isDirty = Object.keys(dirtyFields).length > 0; const isDirty = Object.keys(dirtyFields).length > 0;
useEffect(() => { useEffect(() => {
onDirtyStateChange(isDirty, 'drawer'); onDirtyStateChange(isDirty, location);
}, [isDirty, onDirtyStateChange]); }, [isDirty, location, onDirtyStateChange]);
async function handleSubmit(values: RolePermissionEditorFormValues) { async function handleSubmit(values: RolePermissionEditorFormValues) {
const managePermissionPromise = managePermission({ const managePermissionPromise = managePermission({
@@ -245,7 +247,7 @@ export default function RolePermissionEditorForm({
: permission?.check, : permission?.check,
backend_only: values.backendOnly, backend_only: values.backendOnly,
computed_fields: computed_fields:
permission?.computed_fields.length > 0 permission?.computed_fields?.length > 0
? permission?.computed_fields ? permission?.computed_fields
: null, : null,
}, },
@@ -261,7 +263,7 @@ export default function RolePermissionEditorForm({
getToastStyleProps(), getToastStyleProps(),
); );
onDirtyStateChange(false, 'drawer'); onDirtyStateChange(false, location);
onSubmit?.(); onSubmit?.();
} }
@@ -270,7 +272,7 @@ export default function RolePermissionEditorForm({
openDirtyConfirmation({ openDirtyConfirmation({
props: { props: {
onPrimaryAction: () => { onPrimaryAction: () => {
onDirtyStateChange(false, 'drawer'); onDirtyStateChange(false, location);
onCancel?.(); onCancel?.();
}, },
}, },
@@ -300,7 +302,7 @@ export default function RolePermissionEditorForm({
getToastStyleProps(), getToastStyleProps(),
); );
onDirtyStateChange(false, 'drawer'); onDirtyStateChange(false, location);
onSubmit?.(); onSubmit?.();
} }

View File

@@ -6,6 +6,7 @@ import Input from '@/ui/v2/Input';
import Radio from '@/ui/v2/Radio'; import Radio from '@/ui/v2/Radio';
import RadioGroup from '@/ui/v2/RadioGroup'; import RadioGroup from '@/ui/v2/RadioGroup';
import Text from '@/ui/v2/Text'; import Text from '@/ui/v2/Text';
import type { FocusEvent } from 'react';
import { useState } from 'react'; import { useState } from 'react';
import { useFormContext } from 'react-hook-form'; import { useFormContext } from 'react-hook-form';
import PermissionSettingsSection from './PermissionSettingsSection'; import PermissionSettingsSection from './PermissionSettingsSection';
@@ -130,7 +131,13 @@ export default function RowPermissionsSection({
{action === 'select' && ( {action === 'select' && (
<Input <Input
{...register('limit')} {...register('limit', {
onBlur: (event: FocusEvent<HTMLInputElement>) => {
if (!event.target.value) {
setValue('limit', null);
}
},
})}
disabled={disabled} disabled={disabled}
id="limit" id="limit"
type="number" type="number"

View File

@@ -43,7 +43,10 @@ const baseValidationSchema = Yup.object().shape({
}); });
const selectValidationSchema = baseValidationSchema.shape({ const selectValidationSchema = baseValidationSchema.shape({
limit: Yup.number().min(0, 'Limit must not be negative.').nullable(true), limit: Yup.number()
.label('Limit')
.min(0, 'Limit must not be negative.')
.nullable(true),
allowAggregations: Yup.boolean().nullable(true), allowAggregations: Yup.boolean().nullable(true),
queryRootFields: Yup.array().of(Yup.string()).nullable(true), queryRootFields: Yup.array().of(Yup.string()).nullable(true),
subscriptionRootFields: Yup.array().of(Yup.string()).nullable(true), subscriptionRootFields: Yup.array().of(Yup.string()).nullable(true),

View File

@@ -23,7 +23,7 @@ import { useEffect, useState } from 'react';
import { FormProvider, useForm } from 'react-hook-form'; import { FormProvider, useForm } from 'react-hook-form';
export interface EditTableFormProps export interface EditTableFormProps
extends Pick<BaseTableFormProps, 'onCancel'> { extends Pick<BaseTableFormProps, 'onCancel' | 'location'> {
/** /**
* Schema where the table is located. * Schema where the table is located.
*/ */

View File

@@ -6,6 +6,7 @@ import ColumnAutocomplete from '@/components/dataBrowser/ColumnAutocomplete';
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication'; import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
import type { HasuraOperator } from '@/types/dataBrowser'; import type { HasuraOperator } from '@/types/dataBrowser';
import ActivityIndicator from '@/ui/v2/ActivityIndicator'; import ActivityIndicator from '@/ui/v2/ActivityIndicator';
import type { AutocompleteOption } from '@/ui/v2/Autocomplete';
import type { InputProps } from '@/ui/v2/Input'; import type { InputProps } from '@/ui/v2/Input';
import { inputClasses } from '@/ui/v2/Input'; import { inputClasses } from '@/ui/v2/Input';
import Option from '@/ui/v2/Option'; import Option from '@/ui/v2/Option';
@@ -211,12 +212,13 @@ export default function RuleValueInput({
<ControlledAutocomplete <ControlledAutocomplete
disabled={disabled} disabled={disabled}
freeSolo={!isHasuraInput} freeSolo={!isHasuraInput}
autoSelect={!isHasuraInput}
autoHighlight={isHasuraInput} autoHighlight={isHasuraInput}
open isOptionEqualToValue={(
isOptionEqualToValue={(option, value) => { option,
if (typeof value === 'string') { value: string | number | AutocompleteOption<string>,
return option.value.toLowerCase() === (value as string).toLowerCase(); ) => {
if (typeof value !== 'object') {
return option.value.toLowerCase() === value?.toString().toLowerCase();
} }
return option.value.toLowerCase() === value.value.toLowerCase(); return option.value.toLowerCase() === value.value.toLowerCase();

View File

@@ -1,4 +1,6 @@
import { useDialog } from '@/components/common/DialogProvider';
import Form from '@/components/common/Form'; import Form from '@/components/common/Form';
import type { DialogFormProps } from '@/types/common';
import Button from '@/ui/v2/Button'; import Button from '@/ui/v2/Button';
import Input from '@/ui/v2/Input'; import Input from '@/ui/v2/Input';
import { slugifyString } from '@/utils/helpers'; import { slugifyString } from '@/utils/helpers';
@@ -11,11 +13,12 @@ import {
import { yupResolver } from '@hookform/resolvers/yup'; import { yupResolver } from '@hookform/resolvers/yup';
import { useUserData } from '@nhost/nextjs'; import { useUserData } from '@nhost/nextjs';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { useEffect } from 'react';
import { FormProvider, useForm } from 'react-hook-form'; import { FormProvider, useForm } from 'react-hook-form';
import { toast } from 'react-hot-toast'; import { toast } from 'react-hot-toast';
import * as Yup from 'yup'; import * as Yup from 'yup';
export interface EditWorkspaceNameFormProps { export interface EditWorkspaceNameFormProps extends DialogFormProps {
/** /**
* The current workspace name if this is an edit operation. * The current workspace name if this is an edit operation.
*/ */
@@ -44,14 +47,7 @@ export interface EditWorkspaceNameFormProps {
onCancel?: VoidFunction; onCancel?: VoidFunction;
} }
export interface EditWorkspaceNameFormValues { const validationSchema = Yup.object({
/**
* New workspace name.
*/
newWorkspaceName: string;
}
const validationSchema = Yup.object().shape({
newWorkspaceName: Yup.string() newWorkspaceName: Yup.string()
.required('Workspace name is required.') .required('Workspace name is required.')
.min(4, 'The new Workspace name must be at least 4 characters.') .min(4, 'The new Workspace name must be at least 4 characters.')
@@ -71,14 +67,20 @@ const validationSchema = Yup.object().shape({
), ),
}); });
export default function EditWorkspaceName({ export type EditWorkspaceNameFormValues = Yup.InferType<
typeof validationSchema
>;
export default function EditWorkspaceNameForm({
disabled, disabled,
onSubmit, onSubmit,
onCancel, onCancel,
currentWorkspaceName, currentWorkspaceName,
currentWorkspaceId, currentWorkspaceId,
submitButtonText = 'Create', submitButtonText = 'Create',
location,
}: EditWorkspaceNameFormProps) { }: EditWorkspaceNameFormProps) {
const { onDirtyStateChange } = useDialog();
const currentUser = useUserData(); const currentUser = useUserData();
const [insertWorkspace, { client }] = useInsertWorkspaceMutation(); const [insertWorkspace, { client }] = useInsertWorkspaceMutation();
const [updateWorkspaceName] = useUpdateWorkspaceMutation({ const [updateWorkspaceName] = useUpdateWorkspaceMutation({
@@ -105,6 +107,10 @@ export default function EditWorkspaceName({
} = form; } = form;
const isDirty = Object.keys(dirtyFields).length > 0; const isDirty = Object.keys(dirtyFields).length > 0;
useEffect(() => {
onDirtyStateChange(isDirty, location);
}, [isDirty, location, onDirtyStateChange]);
async function handleSubmit({ async function handleSubmit({
newWorkspaceName, newWorkspaceName,
}: EditWorkspaceNameFormValues) { }: EditWorkspaceNameFormValues) {
@@ -112,6 +118,8 @@ export default function EditWorkspaceName({
try { try {
if (currentWorkspaceId) { if (currentWorkspaceId) {
onDirtyStateChange(false, location);
// In this bit of code we spread the props of the current path (e.g. /workspace/...) and add one key-value pair: `mutating: true`. // In this bit of code we spread the props of the current path (e.g. /workspace/...) and add one key-value pair: `mutating: true`.
// We want to indicate that the currently we're in the process of running a mutation state that will affect the routing behaviour of the website // We want to indicate that the currently we're in the process of running a mutation state that will affect the routing behaviour of the website
// i.e. redirecting to 404 if there's no workspace/project with that slug. // i.e. redirecting to 404 if there's no workspace/project with that slug.
@@ -186,6 +194,9 @@ export default function EditWorkspaceName({
include: ['getOneUser'], include: ['getOneUser'],
}); });
// The form has been submitted, it's not dirty anymore
onDirtyStateChange(false, location);
await router.push(slug); await router.push(slug);
onSubmit?.(); onSubmit?.();
} }
@@ -194,9 +205,9 @@ export default function EditWorkspaceName({
<FormProvider {...form}> <FormProvider {...form}>
<Form <Form
onSubmit={handleSubmit} onSubmit={handleSubmit}
className="flex flex-col content-between flex-auto pt-2 pb-6 overflow-hidden" className="flex flex-auto flex-col content-between overflow-hidden pt-2 pb-6"
> >
<div className="flex-auto px-6 overflow-y-auto"> <div className="flex-auto overflow-y-auto px-6">
<Input <Input
{...register('newWorkspaceName')} {...register('newWorkspaceName')}
error={Boolean(errors.newWorkspaceName?.message)} error={Boolean(errors.newWorkspaceName?.message)}

View File

@@ -58,16 +58,19 @@ export function InviteAnnounce() {
error: null, error: null,
loading: true, loading: true,
}); });
const res = await nhost.functions.call('/accept-workspace-invite', { const { res, error: acceptError } = await nhost.functions.call(
workspaceMemberInviteId: invite.id, '/accept-workspace-invite',
isAccepted: true, {
}); workspaceMemberInviteId: invite.id,
isAccepted: true,
},
);
if (res?.res?.status !== 200) { if (res?.status !== 200) {
triggerToast('An error occurred when trying to accept the invitation.'); triggerToast('An error occurred when trying to accept the invitation.');
return setSubmitState({ return setSubmitState({
error: new Error(res.error.message), error: new Error(acceptError.message),
loading: false, loading: false,
}); });
} }
@@ -90,7 +93,7 @@ export function InviteAnnounce() {
error: null, error: null,
}); });
const res = await nhost.functions.call( const { error: ignoreError } = await nhost.functions.call(
'/accept-workspace-invite', '/accept-workspace-invite',
{ {
workspaceMemberInviteId: inviteId, workspaceMemberInviteId: inviteId,
@@ -99,12 +102,12 @@ export function InviteAnnounce() {
{ useAxios: false }, { useAxios: false },
); );
if (res?.error) { if (ignoreError) {
triggerToast('An error occurred when trying to ignore the invitation.'); triggerToast('An error occurred when trying to ignore the invitation.');
setIgnoreState({ setIgnoreState({
loading: false, loading: false,
error: new Error(res.error.message), error: new Error(ignoreError.message),
}); });
return; return;

View File

@@ -1,5 +1,6 @@
import { useDialog } from '@/components/common/DialogProvider'; import { useDialog } from '@/components/common/DialogProvider';
import Form from '@/components/common/Form'; import Form from '@/components/common/Form';
import type { DialogFormProps } from '@/types/common';
import Button from '@/ui/v2/Button'; import Button from '@/ui/v2/Button';
import Input from '@/ui/v2/Input'; import Input from '@/ui/v2/Input';
import Text from '@/ui/v2/Text'; import Text from '@/ui/v2/Text';
@@ -26,7 +27,7 @@ export interface BaseEnvironmentVariableFormValues {
prodValue: string; prodValue: string;
} }
export interface BaseEnvironmentVariableFormProps { export interface BaseEnvironmentVariableFormProps extends DialogFormProps {
/** /**
* Determines the mode of the form. * Determines the mode of the form.
* *
@@ -89,6 +90,7 @@ export default function BaseEnvironmentVariableForm({
onSubmit, onSubmit,
onCancel, onCancel,
submitButtonText = 'Save', submitButtonText = 'Save',
location,
}: BaseEnvironmentVariableFormProps) { }: BaseEnvironmentVariableFormProps) {
const { onDirtyStateChange } = useDialog(); const { onDirtyStateChange } = useDialog();
const form = useFormContext<BaseEnvironmentVariableFormValues>(); const form = useFormContext<BaseEnvironmentVariableFormValues>();
@@ -103,8 +105,8 @@ export default function BaseEnvironmentVariableForm({
const isDirty = Object.keys(dirtyFields).length > 0; const isDirty = Object.keys(dirtyFields).length > 0;
useEffect(() => { useEffect(() => {
onDirtyStateChange(isDirty, 'dialog'); onDirtyStateChange(isDirty, location);
}, [isDirty, onDirtyStateChange]); }, [isDirty, location, onDirtyStateChange]);
return ( return (
<div className="grid grid-flow-row gap-6 px-6 pb-6"> <div className="grid grid-flow-row gap-6 px-6 pb-6">

View File

@@ -17,7 +17,7 @@ import { FormProvider, useForm } from 'react-hook-form';
import toast from 'react-hot-toast'; import toast from 'react-hot-toast';
export interface CreateEnvironmentVariableFormProps export interface CreateEnvironmentVariableFormProps
extends Pick<BaseEnvironmentVariableFormProps, 'onCancel'> { extends Pick<BaseEnvironmentVariableFormProps, 'onCancel' | 'location'> {
/** /**
* Function to be called when the form is submitted. * Function to be called when the form is submitted.
*/ */

View File

@@ -18,7 +18,7 @@ import { FormProvider, useForm } from 'react-hook-form';
import toast from 'react-hot-toast'; import toast from 'react-hot-toast';
export interface EditEnvironmentVariableFormProps export interface EditEnvironmentVariableFormProps
extends Pick<BaseEnvironmentVariableFormProps, 'onCancel'> { extends Pick<BaseEnvironmentVariableFormProps, 'onCancel' | 'location'> {
/** /**
* The environment variable to edit. * The environment variable to edit.
*/ */

View File

@@ -1,6 +1,7 @@
import { useDialog } from '@/components/common/DialogProvider'; import { useDialog } from '@/components/common/DialogProvider';
import Form from '@/components/common/Form'; import Form from '@/components/common/Form';
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication'; import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
import type { DialogFormProps } from '@/types/common';
import Button from '@/ui/v2/Button'; import Button from '@/ui/v2/Button';
import Input from '@/ui/v2/Input'; import Input from '@/ui/v2/Input';
import { getToastStyleProps } from '@/utils/settings/settingsConstants'; import { getToastStyleProps } from '@/utils/settings/settingsConstants';
@@ -14,7 +15,7 @@ import { FormProvider, useForm } from 'react-hook-form';
import { toast } from 'react-hot-toast'; import { toast } from 'react-hot-toast';
import * as Yup from 'yup'; import * as Yup from 'yup';
export interface EditJwtSecretFormProps { export interface EditJwtSecretFormProps extends DialogFormProps {
/** /**
* Initial JWT secret. * Initial JWT secret.
*/ */
@@ -39,14 +40,7 @@ export interface EditJwtSecretFormProps {
onCancel?: VoidFunction; onCancel?: VoidFunction;
} }
export interface EditJwtSecretFormValues { const validationSchema = Yup.object({
/**
* JWT secret.
*/
jwtSecret: string;
}
const validationSchema = Yup.object().shape({
jwtSecret: Yup.string() jwtSecret: Yup.string()
.nullable() .nullable()
.required('This field is required.') .required('This field is required.')
@@ -60,12 +54,15 @@ const validationSchema = Yup.object().shape({
}), }),
}); });
export type EditJwtSecretFormValues = Yup.InferType<typeof validationSchema>;
export default function EditJwtSecretForm({ export default function EditJwtSecretForm({
disabled, disabled,
jwtSecret, jwtSecret,
onSubmit, onSubmit,
onCancel, onCancel,
submitButtonText = 'Save', submitButtonText = 'Save',
location,
}: EditJwtSecretFormProps) { }: EditJwtSecretFormProps) {
const { currentApplication } = useCurrentWorkspaceAndApplication(); const { currentApplication } = useCurrentWorkspaceAndApplication();
const [updateApplication] = useUpdateApplicationMutation({ const [updateApplication] = useUpdateApplicationMutation({
@@ -89,8 +86,8 @@ export default function EditJwtSecretForm({
const isDirty = Object.keys(dirtyFields).length > 0; const isDirty = Object.keys(dirtyFields).length > 0;
useEffect(() => { useEffect(() => {
onDirtyStateChange(isDirty, 'dialog'); onDirtyStateChange(isDirty, location);
}, [isDirty, onDirtyStateChange]); }, [isDirty, location, onDirtyStateChange]);
async function handleSubmit(values: EditJwtSecretFormValues) { async function handleSubmit(values: EditJwtSecretFormValues) {
const updateAppPromise = updateApplication({ const updateAppPromise = updateApplication({
@@ -121,7 +118,7 @@ export default function EditJwtSecretForm({
onSubmit={handleSubmit} onSubmit={handleSubmit}
className="flex flex-auto flex-col content-between overflow-hidden pb-4" className="flex flex-auto flex-col content-between overflow-hidden pb-4"
> >
<div className="px-6 overflow-y-auto flex-auto"> <div className="flex-auto overflow-y-auto px-6">
<Input <Input
{...register('jwtSecret')} {...register('jwtSecret')}
error={Boolean(errors.jwtSecret?.message)} error={Boolean(errors.jwtSecret?.message)}

View File

@@ -1,4 +1,6 @@
import { useDialog } from '@/components/common/DialogProvider'; import { useDialog } from '@/components/common/DialogProvider';
import CreateEnvironmentVariableForm from '@/components/settings/environmentVariables/CreateEnvironmentVariableForm';
import EditEnvironmentVariableForm from '@/components/settings/environmentVariables/EditEnvironmentVariableForm';
import SettingsContainer from '@/components/settings/SettingsContainer'; import SettingsContainer from '@/components/settings/SettingsContainer';
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication'; import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
import type { EnvironmentVariable } from '@/types/application'; import type { EnvironmentVariable } from '@/types/application';
@@ -23,9 +25,9 @@ import { Fragment } from 'react';
import toast from 'react-hot-toast'; import toast from 'react-hot-toast';
import { twMerge } from 'tailwind-merge'; import { twMerge } from 'tailwind-merge';
export interface PermissionVariableSettingsFormValues { export interface EnvironmentVariableSettingsFormValues {
/** /**
* Permission variables. * Environment variables.
*/ */
environmentVariables: EnvironmentVariable[]; environmentVariables: EnvironmentVariable[];
} }
@@ -75,8 +77,9 @@ export default function EnvironmentVariableSettings() {
} }
function handleOpenCreator() { function handleOpenCreator() {
openDialog('CREATE_ENVIRONMENT_VARIABLE', { openDialog({
title: 'Create Environment Variable', title: 'Create Environment Variable',
component: <CreateEnvironmentVariableForm />,
props: { props: {
titleProps: { className: '!pb-0' }, titleProps: { className: '!pb-0' },
PaperProps: { className: 'gap-2 max-w-sm' }, PaperProps: { className: 'gap-2 max-w-sm' },
@@ -85,9 +88,13 @@ export default function EnvironmentVariableSettings() {
} }
function handleOpenEditor(originalVariable: EnvironmentVariable) { function handleOpenEditor(originalVariable: EnvironmentVariable) {
openDialog('EDIT_ENVIRONMENT_VARIABLE', { openDialog({
title: 'Edit Environment Variables', title: 'Edit Environment Variable',
payload: { originalEnvironmentVariable: originalVariable }, component: (
<EditEnvironmentVariableForm
originalEnvironmentVariable={originalVariable}
/>
),
props: { props: {
titleProps: { className: '!pb-0' }, titleProps: { className: '!pb-0' },
PaperProps: { className: 'gap-2 max-w-sm' }, PaperProps: { className: 'gap-2 max-w-sm' },

View File

@@ -1,5 +1,6 @@
import { useDialog } from '@/components/common/DialogProvider'; import { useDialog } from '@/components/common/DialogProvider';
import InlineCode from '@/components/common/InlineCode'; import InlineCode from '@/components/common/InlineCode';
import EditJwtSecretForm from '@/components/settings/environmentVariables/EditJwtSecretForm';
import SettingsContainer from '@/components/settings/SettingsContainer'; import SettingsContainer from '@/components/settings/SettingsContainer';
import useIsPlatform from '@/hooks/common/useIsPlatform'; import useIsPlatform from '@/hooks/common/useIsPlatform';
import { useAppClient } from '@/hooks/useAppClient'; import { useAppClient } from '@/hooks/useAppClient';
@@ -50,7 +51,7 @@ export default function SystemEnvironmentVariableSettings() {
} }
function showViewJwtSecretModal() { function showViewJwtSecretModal() {
openDialog('EDIT_JWT_SECRET', { openDialog({
title: ( title: (
<span className="grid grid-flow-row"> <span className="grid grid-flow-row">
<span>Auth JWT Secret</span> <span>Auth JWT Secret</span>
@@ -61,15 +62,17 @@ export default function SystemEnvironmentVariableSettings() {
</Text> </Text>
</span> </span>
), ),
payload: { component: (
disabled: true, <EditJwtSecretForm
jwtSecret: data?.app?.hasuraGraphqlJwtSecret, disabled
}, jwtSecret={data?.app?.hasuraGraphqlJwtSecret}
/>
),
}); });
} }
function showEditJwtSecretModal() { function showEditJwtSecretModal() {
openDialog('EDIT_JWT_SECRET', { openDialog({
title: ( title: (
<span className="grid grid-flow-row"> <span className="grid grid-flow-row">
<span>Edit JWT Secret</span> <span>Edit JWT Secret</span>
@@ -80,9 +83,9 @@ export default function SystemEnvironmentVariableSettings() {
</Text> </Text>
</span> </span>
), ),
payload: { component: (
jwtSecret: data?.app?.hasuraGraphqlJwtSecret, <EditJwtSecretForm jwtSecret={data?.app?.hasuraGraphqlJwtSecret} />
}, ),
}); });
} }

View File

@@ -1,5 +1,6 @@
import { useDialog } from '@/components/common/DialogProvider'; import { useDialog } from '@/components/common/DialogProvider';
import Form from '@/components/common/Form'; import Form from '@/components/common/Form';
import type { DialogFormProps } from '@/types/common';
import Button from '@/ui/v2/Button'; import Button from '@/ui/v2/Button';
import Input from '@/ui/v2/Input'; import Input from '@/ui/v2/Input';
import Text from '@/ui/v2/Text'; import Text from '@/ui/v2/Text';
@@ -7,18 +8,7 @@ import { useEffect } from 'react';
import { useFormContext } from 'react-hook-form'; import { useFormContext } from 'react-hook-form';
import * as Yup from 'yup'; import * as Yup from 'yup';
export interface BasePermissionVariableFormValues { export interface BasePermissionVariableFormProps extends DialogFormProps {
/**
* Permission variable key.
*/
key: string;
/**
* Permission variable value.
*/
value: string;
}
export interface BasePermissionVariableFormProps {
/** /**
* Function to be called when the form is submitted. * Function to be called when the form is submitted.
*/ */
@@ -40,10 +30,15 @@ export const basePermissionVariableValidationSchema = Yup.object({
value: Yup.string().required('This field is required.'), value: Yup.string().required('This field is required.'),
}); });
export type BasePermissionVariableFormValues = Yup.InferType<
typeof basePermissionVariableValidationSchema
>;
export default function BasePermissionVariableForm({ export default function BasePermissionVariableForm({
onSubmit, onSubmit,
onCancel, onCancel,
submitButtonText = 'Save', submitButtonText = 'Save',
location,
}: BasePermissionVariableFormProps) { }: BasePermissionVariableFormProps) {
const { onDirtyStateChange } = useDialog(); const { onDirtyStateChange } = useDialog();
const form = useFormContext<BasePermissionVariableFormValues>(); const form = useFormContext<BasePermissionVariableFormValues>();
@@ -56,8 +51,8 @@ export default function BasePermissionVariableForm({
const isDirty = Object.keys(dirtyFields).length > 0; const isDirty = Object.keys(dirtyFields).length > 0;
useEffect(() => { useEffect(() => {
onDirtyStateChange(isDirty, 'dialog'); onDirtyStateChange(isDirty, location);
}, [isDirty, onDirtyStateChange]); }, [isDirty, location, onDirtyStateChange]);
return ( return (
<div className="grid grid-flow-row gap-2 px-6 pb-6"> <div className="grid grid-flow-row gap-2 px-6 pb-6">

View File

@@ -19,7 +19,7 @@ import { FormProvider, useForm } from 'react-hook-form';
import toast from 'react-hot-toast'; import toast from 'react-hot-toast';
export interface CreatePermissionVariableFormProps export interface CreatePermissionVariableFormProps
extends Pick<BasePermissionVariableFormProps, 'onCancel'> { extends Pick<BasePermissionVariableFormProps, 'onCancel' | 'location'> {
/** /**
* Function to be called when the form is submitted. * Function to be called when the form is submitted.
*/ */

View File

@@ -20,7 +20,7 @@ import { FormProvider, useForm } from 'react-hook-form';
import toast from 'react-hot-toast'; import toast from 'react-hot-toast';
export interface EditPermissionVariableFormProps export interface EditPermissionVariableFormProps
extends Pick<BasePermissionVariableFormProps, 'onCancel'> { extends Pick<BasePermissionVariableFormProps, 'onCancel' | 'location'> {
/** /**
* The permission variable to be edited. * The permission variable to be edited.
*/ */

View File

@@ -1,4 +1,6 @@
import { useDialog } from '@/components/common/DialogProvider'; import { useDialog } from '@/components/common/DialogProvider';
import CreatePermissionVariableForm from '@/components/settings/permissions/CreatePermissionVariableForm';
import EditPermissionVariableForm from '@/components/settings/permissions/EditPermissionVariableForm';
import SettingsContainer from '@/components/settings/SettingsContainer'; import SettingsContainer from '@/components/settings/SettingsContainer';
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication'; import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
import type { CustomClaim } from '@/types/application'; import type { CustomClaim } from '@/types/application';
@@ -88,8 +90,9 @@ export default function PermissionVariableSettings() {
} }
function handleOpenCreator() { function handleOpenCreator() {
openDialog('CREATE_PERMISSION_VARIABLE', { openDialog({
title: 'Create Permission Variable', title: 'Create Permission Variable',
component: <CreatePermissionVariableForm />,
props: { props: {
titleProps: { className: '!pb-0' }, titleProps: { className: '!pb-0' },
PaperProps: { className: 'max-w-sm' }, PaperProps: { className: 'max-w-sm' },
@@ -98,9 +101,11 @@ export default function PermissionVariableSettings() {
} }
function handleOpenEditor(originalVariable: CustomClaim) { function handleOpenEditor(originalVariable: CustomClaim) {
openDialog('EDIT_PERMISSION_VARIABLE', { openDialog({
title: 'Edit Permission Variable', title: 'Edit Permission Variable',
payload: { originalVariable }, component: (
<EditPermissionVariableForm originalVariable={originalVariable} />
),
props: { props: {
titleProps: { className: '!pb-0' }, titleProps: { className: '!pb-0' },
PaperProps: { className: 'max-w-sm' }, PaperProps: { className: 'max-w-sm' },
@@ -136,7 +141,7 @@ export default function PermissionVariableSettings() {
description="Permission variables are used to define permission rules in the GraphQL API." description="Permission variables are used to define permission rules in the GraphQL API."
docsLink="https://docs.nhost.io/graphql/permissions" docsLink="https://docs.nhost.io/graphql/permissions"
rootClassName="gap-0" rootClassName="gap-0"
className="px-0 my-2" className="my-2 px-0"
slotProps={{ submitButton: { className: 'invisible' } }} slotProps={{ submitButton: { className: 'invisible' } }}
> >
<Box className="grid grid-cols-2 border-b-1 px-4 py-3"> <Box className="grid grid-cols-2 border-b-1 px-4 py-3">
@@ -149,7 +154,7 @@ export default function PermissionVariableSettings() {
{availablePermissionVariables.map((customClaim, index) => ( {availablePermissionVariables.map((customClaim, index) => (
<Fragment key={customClaim.key}> <Fragment key={customClaim.key}>
<ListItem.Root <ListItem.Root
className="px-4 grid grid-cols-2" className="grid grid-cols-2 px-4"
secondaryAction={ secondaryAction={
<Dropdown.Root> <Dropdown.Root>
<Tooltip <Tooltip
@@ -215,7 +220,7 @@ export default function PermissionVariableSettings() {
<> <>
X-Hasura-{customClaim.key}{' '} X-Hasura-{customClaim.key}{' '}
{customClaim.isSystemClaim && ( {customClaim.isSystemClaim && (
<LockIcon className="w-4 h-4" /> <LockIcon className="h-4 w-4" />
)} )}
</> </>
} }
@@ -237,7 +242,7 @@ export default function PermissionVariableSettings() {
</List> </List>
<Button <Button
className="justify-self-start mx-4" className="mx-4 justify-self-start"
variant="borderless" variant="borderless"
startIcon={<PlusIcon />} startIcon={<PlusIcon />}
onClick={handleOpenCreator} onClick={handleOpenCreator}

View File

@@ -1,5 +1,6 @@
import { useDialog } from '@/components/common/DialogProvider'; import { useDialog } from '@/components/common/DialogProvider';
import Form from '@/components/common/Form'; import Form from '@/components/common/Form';
import type { DialogFormProps } from '@/types/common';
import { Alert } from '@/ui/Alert'; import { Alert } from '@/ui/Alert';
import Button from '@/ui/v2/Button'; import Button from '@/ui/v2/Button';
import Input from '@/ui/v2/Input'; import Input from '@/ui/v2/Input';
@@ -8,14 +9,7 @@ import { useEffect } from 'react';
import { useFormContext } from 'react-hook-form'; import { useFormContext } from 'react-hook-form';
import * as Yup from 'yup'; import * as Yup from 'yup';
export interface BaseRoleFormValues { export interface BaseRoleFormProps extends DialogFormProps {
/**
* The name of the role.
*/
name: string;
}
export interface BaseRoleFormProps {
/** /**
* Function to be called when the form is submitted. * Function to be called when the form is submitted.
*/ */
@@ -36,10 +30,15 @@ export const baseRoleFormValidationSchema = Yup.object({
name: Yup.string().required('This field is required.'), name: Yup.string().required('This field is required.'),
}); });
export type BaseRoleFormValues = Yup.InferType<
typeof baseRoleFormValidationSchema
>;
export default function BaseRoleForm({ export default function BaseRoleForm({
onSubmit, onSubmit,
onCancel, onCancel,
submitButtonText = 'Save', submitButtonText = 'Save',
location,
}: BaseRoleFormProps) { }: BaseRoleFormProps) {
const { onDirtyStateChange } = useDialog(); const { onDirtyStateChange } = useDialog();
const form = useFormContext<BaseRoleFormValues>(); const form = useFormContext<BaseRoleFormValues>();
@@ -52,8 +51,8 @@ export default function BaseRoleForm({
const isDirty = Object.keys(dirtyFields).length > 0; const isDirty = Object.keys(dirtyFields).length > 0;
useEffect(() => { useEffect(() => {
onDirtyStateChange(isDirty, 'dialog'); onDirtyStateChange(isDirty, location);
}, [isDirty, onDirtyStateChange]); }, [isDirty, location, onDirtyStateChange]);
return ( return (
<div className="grid grid-flow-row gap-3 px-6 pb-6"> <div className="grid grid-flow-row gap-3 px-6 pb-6">

View File

@@ -18,7 +18,7 @@ import { FormProvider, useForm } from 'react-hook-form';
import toast from 'react-hot-toast'; import toast from 'react-hot-toast';
export interface CreateRoleFormProps export interface CreateRoleFormProps
extends Pick<BaseRoleFormProps, 'onCancel'> { extends Pick<BaseRoleFormProps, 'onCancel' | 'location'> {
/** /**
* Function to be called when the form is submitted. * Function to be called when the form is submitted.
*/ */

View File

@@ -18,7 +18,8 @@ import { yupResolver } from '@hookform/resolvers/yup';
import { FormProvider, useForm } from 'react-hook-form'; import { FormProvider, useForm } from 'react-hook-form';
import toast from 'react-hot-toast'; import toast from 'react-hot-toast';
export interface EditRoleFormProps extends Pick<BaseRoleFormProps, 'onCancel'> { export interface EditRoleFormProps
extends Pick<BaseRoleFormProps, 'onCancel' | 'location'> {
/** /**
* The role to be edited. * The role to be edited.
*/ */

View File

@@ -1,4 +1,6 @@
import { useDialog } from '@/components/common/DialogProvider'; import { useDialog } from '@/components/common/DialogProvider';
import CreateRoleForm from '@/components/settings/roles/CreateRoleForm';
import EditRoleForm from '@/components/settings/roles/EditRoleForm';
import SettingsContainer from '@/components/settings/SettingsContainer'; import SettingsContainer from '@/components/settings/SettingsContainer';
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication'; import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
import type { Role } from '@/types/application'; import type { Role } from '@/types/application';
@@ -108,8 +110,9 @@ export default function RoleSettings() {
} }
function handleOpenCreator() { function handleOpenCreator() {
openDialog('CREATE_ROLE', { openDialog({
title: 'Create Allowed Role', title: 'Create Allowed Role',
component: <CreateRoleForm />,
props: { props: {
titleProps: { className: '!pb-0' }, titleProps: { className: '!pb-0' },
PaperProps: { className: 'max-w-sm' }, PaperProps: { className: 'max-w-sm' },
@@ -118,9 +121,9 @@ export default function RoleSettings() {
} }
function handleOpenEditor(originalRole: Role) { function handleOpenEditor(originalRole: Role) {
openDialog('EDIT_ROLE', { openDialog({
title: 'Edit Allowed Role', title: 'Edit Allowed Role',
payload: { originalRole }, component: <EditRoleForm originalRole={originalRole} />,
props: { props: {
titleProps: { className: '!pb-0' }, titleProps: { className: '!pb-0' },
PaperProps: { className: 'max-w-sm' }, PaperProps: { className: 'max-w-sm' },

View File

@@ -222,9 +222,9 @@ function Autocomplete(
inputValue: inputValue || '', inputValue: inputValue || '',
getOptionLabel: props.getOptionLabel getOptionLabel: props.getOptionLabel
? props.getOptionLabel ? props.getOptionLabel
: (option) => { : (option: string | number | AutocompleteOption<string>) => {
if (typeof option === 'string') { if (typeof option !== 'object') {
return option; return option.toString();
} }
return option.label ?? option.dropdownLabel; return option.label ?? option.dropdownLabel;
@@ -284,33 +284,46 @@ function Autocomplete(
}} }}
PopperComponent={AutocompletePopper} PopperComponent={AutocompletePopper}
popupIcon={<ChevronDownIcon sx={{ width: 12, height: 12 }} />} popupIcon={<ChevronDownIcon sx={{ width: 12, height: 12 }} />}
getOptionLabel={(option) => { getOptionLabel={(
if (typeof option === 'string') { option: string | number | AutocompleteOption<string>,
return option; ) => {
if (!option) {
return '';
}
if (typeof option !== 'object') {
return option.toString();
} }
return option.label ?? option.dropdownLabel; return option.label ?? option.dropdownLabel;
}} }}
isOptionEqualToValue={(option, value) => { isOptionEqualToValue={(
option,
value: string | number | AutocompleteOption<string>,
) => {
if (!value) { if (!value) {
return false; return false;
} }
if (typeof value === 'string') { if (typeof value !== 'object') {
return option.value === value; return option.value.toString() === value.toString();
} }
return option.value === value.value && option.custom === value.custom; return option.value === value.value && option.custom === value.custom;
}} }}
renderTags={(value, getTagProps) => renderTags={(value, getTagProps) =>
value.map((option, index) => ( value.map(
<StyledTag (option: string | number | AutocompleteOption<string>, index) => (
deleteIcon={<XIcon />} <StyledTag
size="small" deleteIcon={<XIcon />}
label={typeof option === 'string' ? option : option.value} size="small"
{...getTagProps({ index })} label={
/> typeof option !== 'object' ? option.toString() : option.value
)) }
{...getTagProps({ index })}
/>
),
)
} }
renderGroup={({ group, key, children }) => renderGroup={({ group, key, children }) =>
group ? ( group ? (
@@ -323,9 +336,12 @@ function Autocomplete(
<div key={key}>{children}</div> <div key={key}>{children}</div>
) )
} }
renderOption={(optionProps, option) => { renderOption={(
if (typeof option === 'string') { optionProps,
return <OptionBase {...optionProps}>{option}</OptionBase>; option: string | number | AutocompleteOption<string>,
) => {
if (typeof option !== 'object') {
return <OptionBase {...optionProps}>{option.toString()}</OptionBase>;
} }
return ( return (

View File

@@ -1,4 +1,5 @@
import Backdrop from '@/ui/v2/Backdrop'; import Backdrop from '@/ui/v2/Backdrop';
import type { DialogTitleProps } from '@/ui/v2/Dialog';
import { DialogTitle } from '@/ui/v2/Dialog'; import { DialogTitle } from '@/ui/v2/Dialog';
import { styled } from '@mui/material'; import { styled } from '@mui/material';
import type { DrawerProps as MaterialDrawerProps } from '@mui/material/Drawer'; import type { DrawerProps as MaterialDrawerProps } from '@mui/material/Drawer';
@@ -10,6 +11,10 @@ export interface DrawerProps extends Omit<MaterialDrawerProps, 'title'> {
* Title of the drawer. * Title of the drawer.
*/ */
title?: ReactNode; title?: ReactNode;
/**
* Props to pass to the title component.
*/
titleProps?: DialogTitleProps;
/** /**
* Determines whether or not a close button is hidden in the drawer. * Determines whether or not a close button is hidden in the drawer.
* *
@@ -33,13 +38,18 @@ function Drawer({
children, children,
onClose, onClose,
title, title,
titleProps: { sx: titleSx, ...titleProps } = {},
...props ...props
}: DrawerProps) { }: DrawerProps) {
return ( return (
<StyledDrawer components={{ Backdrop }} onClose={onClose} {...props}> <StyledDrawer components={{ Backdrop }} onClose={onClose} {...props}>
{onClose && !hideCloseButton && ( {onClose && !hideCloseButton && (
<DialogTitle <DialogTitle
sx={{ padding: (theme) => theme.spacing(2.5, 3) }} {...titleProps}
sx={[
...(Array.isArray(titleSx) ? titleSx : [titleSx]),
{ padding: (theme) => theme.spacing(2.5, 3) },
]}
onClose={(event) => onClose(event, 'escapeKeyDown')} onClose={(event) => onClose(event, 'escapeKeyDown')}
> >
{title} {title}

View File

@@ -1,5 +1,7 @@
import { useDialog } from '@/components/common/DialogProvider';
import Form from '@/components/common/Form'; import Form from '@/components/common/Form';
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication'; import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
import type { DialogFormProps } from '@/types/common';
import { Alert } from '@/ui/Alert'; import { Alert } from '@/ui/Alert';
import Button from '@/ui/v2/Button'; import Button from '@/ui/v2/Button';
import Input from '@/ui/v2/Input'; import Input from '@/ui/v2/Input';
@@ -7,23 +9,12 @@ import generateAppServiceUrl from '@/utils/common/generateAppServiceUrl';
import { getToastStyleProps } from '@/utils/settings/settingsConstants'; import { getToastStyleProps } from '@/utils/settings/settingsConstants';
import { yupResolver } from '@hookform/resolvers/yup'; import { yupResolver } from '@hookform/resolvers/yup';
import fetch from 'cross-fetch'; import fetch from 'cross-fetch';
import { useState } from 'react'; import { useEffect, useState } from 'react';
import { FormProvider, useForm } from 'react-hook-form'; import { FormProvider, useForm } from 'react-hook-form';
import { toast } from 'react-hot-toast'; import { toast } from 'react-hot-toast';
import * as Yup from 'yup'; import * as Yup from 'yup';
export interface CreateUserFormValues { export interface CreateUserFormProps extends DialogFormProps {
/**
* Email of the user to add to this project.
*/
email: string;
/**
* Password for the user.
*/
password: string;
}
export interface CreateUserFormProps {
/** /**
* Function to be called when the operation is cancelled. * Function to be called when the operation is cancelled.
*/ */
@@ -31,10 +22,10 @@ export interface CreateUserFormProps {
/** /**
* Function to be called when the submit is successful. * Function to be called when the submit is successful.
*/ */
onSuccess?: VoidFunction; onSubmit?: VoidFunction | ((args?: any) => Promise<any>);
} }
export const CreateUserFormValidationSchema = Yup.object({ export const validationSchema = Yup.object({
email: Yup.string() email: Yup.string()
.min(5, 'Email must be at least 5 characters long.') .min(5, 'Email must be at least 5 characters long.')
.email('Invalid email address') .email('Invalid email address')
@@ -45,10 +36,14 @@ export const CreateUserFormValidationSchema = Yup.object({
.required('This field is required.'), .required('This field is required.'),
}); });
export type CreateUserFormValues = Yup.InferType<typeof validationSchema>;
export default function CreateUserForm({ export default function CreateUserForm({
onSuccess, onSubmit,
onCancel, onCancel,
location,
}: CreateUserFormProps) { }: CreateUserFormProps) {
const { onDirtyStateChange } = useDialog();
const { currentApplication } = useCurrentWorkspaceAndApplication(); const { currentApplication } = useCurrentWorkspaceAndApplication();
const [createUserFormError, setCreateUserFormError] = useState<Error | null>( const [createUserFormError, setCreateUserFormError] = useState<Error | null>(
null, null,
@@ -57,15 +52,21 @@ export default function CreateUserForm({
const form = useForm<CreateUserFormValues>({ const form = useForm<CreateUserFormValues>({
defaultValues: {}, defaultValues: {},
reValidateMode: 'onSubmit', reValidateMode: 'onSubmit',
resolver: yupResolver(CreateUserFormValidationSchema), resolver: yupResolver(validationSchema),
}); });
const { const {
register, register,
formState: { errors, isSubmitting }, formState: { errors, isSubmitting, dirtyFields },
setError, setError,
} = form; } = form;
const isDirty = Object.keys(dirtyFields).length > 0;
useEffect(() => {
onDirtyStateChange(isDirty, location);
}, [isDirty, location, onDirtyStateChange]);
const baseAuthUrl = generateAppServiceUrl( const baseAuthUrl = generateAppServiceUrl(
currentApplication?.subdomain, currentApplication?.subdomain,
currentApplication?.region?.awsName, currentApplication?.region?.awsName,
@@ -81,26 +82,35 @@ export default function CreateUserForm({
await toast.promise( await toast.promise(
fetch(signUpUrl, { fetch(signUpUrl, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }), body: JSON.stringify({ email, password }),
}).then((res) => res.json()), }).then(async (res) => {
const data = await res.json();
if (res.ok) {
return data;
}
if (res.status === 409) {
setError('email', { message: data?.message });
}
throw new Error(data?.message || 'Something went wrong.');
}),
{ {
loading: 'Creating user...', loading: 'Creating user...',
success: 'User created successfully.', success: 'User created successfully.',
error: 'An error occurred while trying to create the user.', error: (arg) =>
arg?.message
? `Error: ${arg.message}`
: 'An error occurred while trying to create the user.',
}, },
getToastStyleProps(), getToastStyleProps(),
); );
onSuccess?.();
onSubmit?.();
} catch (error) { } catch (error) {
if (error.response?.status === 409) { // Note: The error is already handled by the toast promise.
setError('email', {
message: error.response.data.message,
});
return;
}
setCreateUserFormError(
new Error(error.response.data.message || 'Something went wrong.'),
);
} }
} }
@@ -137,7 +147,7 @@ export default function CreateUserForm({
{createUserFormError && ( {createUserFormError && (
<Alert <Alert
severity="error" severity="error"
className="grid items-center justify-between grid-flow-col px-4 py-3" className="grid grid-flow-col items-center justify-between px-4 py-3"
> >
<span className="text-left"> <span className="text-left">
<strong>Error:</strong> {createUserFormError.message} <strong>Error:</strong> {createUserFormError.message}

View File

@@ -2,8 +2,10 @@ import ControlledCheckbox from '@/components/common/ControlledCheckbox';
import ControlledSelect from '@/components/common/ControlledSelect'; import ControlledSelect from '@/components/common/ControlledSelect';
import { useDialog } from '@/components/common/DialogProvider'; import { useDialog } from '@/components/common/DialogProvider';
import Form from '@/components/common/Form'; import Form from '@/components/common/Form';
import EditUserPasswordForm from '@/components/users/EditUserPasswordForm';
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication'; import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
import { useRemoteApplicationGQLClient } from '@/hooks/useRemoteApplicationGQLClient'; import { useRemoteApplicationGQLClient } from '@/hooks/useRemoteApplicationGQLClient';
import type { DialogFormProps } from '@/types/common';
import Avatar from '@/ui/v2/Avatar'; import Avatar from '@/ui/v2/Avatar';
import Box from '@/ui/v2/Box'; import Box from '@/ui/v2/Box';
import Button from '@/ui/v2/Button'; import Button from '@/ui/v2/Button';
@@ -20,6 +22,7 @@ import { copy } from '@/utils/copy';
import getUserRoles from '@/utils/settings/getUserRoles'; import getUserRoles from '@/utils/settings/getUserRoles';
import { getToastStyleProps } from '@/utils/settings/settingsConstants'; import { getToastStyleProps } from '@/utils/settings/settingsConstants';
import { import {
RemoteAppGetUsersDocument,
useGetRolesQuery, useGetRolesQuery,
useUpdateRemoteAppUserMutation, useUpdateRemoteAppUserMutation,
} from '@/utils/__generated__/graphql'; } from '@/utils/__generated__/graphql';
@@ -34,7 +37,7 @@ import { FormProvider, useForm } from 'react-hook-form';
import { toast } from 'react-hot-toast'; import { toast } from 'react-hot-toast';
import * as Yup from 'yup'; import * as Yup from 'yup';
export interface EditUserFormProps { export interface EditUserFormProps extends DialogFormProps {
/** /**
* This is the selected user from the user's table. * This is the selected user from the user's table.
*/ */
@@ -42,10 +45,7 @@ export interface EditUserFormProps {
/** /**
* Function to be called when the form is submitted. * Function to be called when the form is submitted.
*/ */
onEditUser?: ( onSubmit?: (values: EditUserFormValues) => Promise<void>;
values: EditUserFormValues,
user: RemoteAppUser,
) => Promise<void>;
/** /**
* Function to be called when the operation is cancelled. * Function to be called when the operation is cancelled.
*/ */
@@ -53,19 +53,15 @@ export interface EditUserFormProps {
/** /**
* Function to be called when banning the user. * Function to be called when banning the user.
*/ */
onBanUser?: (user: RemoteAppUser) => Promise<void>; onBanUser?: (user: RemoteAppUser) => Promise<void> | void;
/** /**
* Function to be called when deleting the user. * Function to be called when deleting the user.
*/ */
onDeleteUser: (user: RemoteAppUser) => Promise<void>; onDeleteUser: (user: RemoteAppUser) => Promise<void> | void;
/** /**
* User roles * User roles
*/ */
roles: { [key: string]: boolean }[]; roles: { [key: string]: boolean }[];
/**
* Function to be called after a successful action.
*/
onSuccessfulAction?: () => Promise<void> | void;
} }
export const EditUserFormValidationSchema = Yup.object({ export const EditUserFormValidationSchema = Yup.object({
@@ -87,12 +83,12 @@ export type EditUserFormValues = Yup.InferType<
>; >;
export default function EditUserForm({ export default function EditUserForm({
location,
user, user,
onEditUser, onSubmit,
onCancel, onCancel,
onDeleteUser, onDeleteUser,
roles, roles,
onSuccessfulAction,
}: EditUserFormProps) { }: EditUserFormProps) {
const theme = useTheme(); const theme = useTheme();
const { onDirtyStateChange, openDialog } = useDialog(); const { onDirtyStateChange, openDialog } = useDialog();
@@ -104,6 +100,7 @@ export default function EditUserForm({
const [updateUser] = useUpdateRemoteAppUserMutation({ const [updateUser] = useUpdateRemoteAppUserMutation({
client: remoteProjectGQLClient, client: remoteProjectGQLClient,
refetchQueries: [RemoteAppGetUsersDocument],
}); });
const form = useForm<EditUserFormValues>({ const form = useForm<EditUserFormValues>({
@@ -124,20 +121,19 @@ export default function EditUserForm({
const { const {
register, register,
handleSubmit,
formState: { errors, dirtyFields, isSubmitting, isValidating }, formState: { errors, dirtyFields, isSubmitting, isValidating },
} = form; } = form;
const isDirty = Object.keys(dirtyFields).length > 0; const isDirty = Object.keys(dirtyFields).length > 0;
useEffect(() => { useEffect(() => {
onDirtyStateChange(isDirty, 'drawer'); onDirtyStateChange(isDirty, location);
}, [isDirty, onDirtyStateChange]); }, [isDirty, location, onDirtyStateChange]);
function handleChangeUserPassword() { function handleChangeUserPassword() {
openDialog('EDIT_USER_PASSWORD', { openDialog({
title: 'Change Password', title: 'Change Password',
payload: { user }, component: <EditUserPasswordForm user={user} />,
}); });
} }
@@ -156,11 +152,13 @@ export default function EditUserForm({
* both having to refetch this single user from the database again or causing a re-render of the drawer. * both having to refetch this single user from the database again or causing a re-render of the drawer.
*/ */
async function handleUserDisabledStatus() { async function handleUserDisabledStatus() {
const shouldBan = !isUserBanned;
const banUser = updateUser({ const banUser = updateUser({
variables: { variables: {
id: user.id, id: user.id,
user: { user: {
disabled: !isUserBanned, disabled: shouldBan,
}, },
}, },
}); });
@@ -168,26 +166,23 @@ export default function EditUserForm({
await toast.promise( await toast.promise(
banUser, banUser,
{ {
loading: user.disabled ? 'Unbanning user...' : 'Banning user...', loading: shouldBan ? 'Banning user...' : 'Unbanning user...',
success: user.disabled success: shouldBan
? 'User unbanned successfully.' ? 'User banned successfully'
: 'User banned successfully', : 'User unbanned successfully.',
error: user.disabled error: shouldBan
? 'An error occurred while trying to unban the user.' ? 'An error occurred while trying to ban the user.'
: 'An error occurred while trying to ban the user.', : 'An error occurred while trying to unban the user.',
}, },
getToastStyleProps(), getToastStyleProps(),
); );
await onSuccessfulAction();
} }
return ( return (
<FormProvider {...form}> <FormProvider {...form}>
<Form <Form
className="flex flex-col overflow-hidden border-t-1 lg:flex-auto lg:content-between" className="flex flex-col overflow-hidden border-t-1 lg:flex-auto lg:content-between"
onSubmit={handleSubmit(async (values) => { onSubmit={onSubmit}
await onEditUser(values, user);
})}
> >
<Box className="flex-auto divide-y overflow-y-auto"> <Box className="flex-auto divide-y overflow-y-auto">
<Box <Box
@@ -268,7 +263,7 @@ export default function EditUserForm({
Created At Created At
</InputLabel> </InputLabel>
<Text className="col-span-3 font-medium"> <Text className="col-span-3 font-medium">
{format(new Date(user.createdAt), 'yyyy-MM-dd hh:mm:ss')} {format(new Date(user.createdAt), 'yyyy-MM-dd HH:mm:ss')}
</Text> </Text>
<InputLabel as="h3" className="col-span-1 self-center "> <InputLabel as="h3" className="col-span-1 self-center ">
@@ -276,7 +271,7 @@ export default function EditUserForm({
</InputLabel> </InputLabel>
<Text className="col-span-3 font-medium"> <Text className="col-span-3 font-medium">
{user.lastSeen {user.lastSeen
? `${format(new Date(user.lastSeen), 'yyyy-mm-dd hh:mm:ss')}` ? `${format(new Date(user.lastSeen), 'yyyy-MM-dd HH:mm:ss')}`
: '-'} : '-'}
</Text> </Text>
</Box> </Box>

View File

@@ -1,6 +1,7 @@
import { useDialog } from '@/components/common/DialogProvider'; import { useDialog } from '@/components/common/DialogProvider';
import Form from '@/components/common/Form'; import Form from '@/components/common/Form';
import { useRemoteApplicationGQLClient } from '@/hooks/useRemoteApplicationGQLClient'; import { useRemoteApplicationGQLClient } from '@/hooks/useRemoteApplicationGQLClient';
import type { DialogFormProps } from '@/types/common';
import { Alert } from '@/ui/Alert'; import { Alert } from '@/ui/Alert';
import Button from '@/ui/v2/Button'; import Button from '@/ui/v2/Button';
import Input from '@/ui/v2/Input'; import Input from '@/ui/v2/Input';
@@ -14,18 +15,7 @@ import { FormProvider, useForm } from 'react-hook-form';
import { toast } from 'react-hot-toast'; import { toast } from 'react-hot-toast';
import * as Yup from 'yup'; import * as Yup from 'yup';
export interface EditUserPasswordFormValues { export interface EditUserPasswordFormProps extends DialogFormProps {
/**
* Password for the user.
*/
password: string;
/**
* Confirm Password for the user.
*/
cpassword: string;
}
export interface EditUserPasswordFormProps {
/** /**
* Function to be called when the operation is cancelled. * Function to be called when the operation is cancelled.
*/ */
@@ -36,7 +26,7 @@ export interface EditUserPasswordFormProps {
user: RemoteAppGetUsersQuery['users'][0]; user: RemoteAppGetUsersQuery['users'][0];
} }
export const EditUserPasswordFormValidationSchema = Yup.object().shape({ export const validationSchema = Yup.object({
password: Yup.string() password: Yup.string()
.label('Users Password') .label('Users Password')
.min(8, 'Password must be at least 8 characters long.') .min(8, 'Password must be at least 8 characters long.')
@@ -47,6 +37,8 @@ export const EditUserPasswordFormValidationSchema = Yup.object().shape({
.oneOf([Yup.ref('password')], 'Passwords do not match'), .oneOf([Yup.ref('password')], 'Passwords do not match'),
}); });
export type EditUserPasswordFormValues = Yup.InferType<typeof validationSchema>;
export default function EditUserPasswordForm({ export default function EditUserPasswordForm({
onCancel, onCancel,
user, user,
@@ -63,7 +55,7 @@ export default function EditUserPasswordForm({
const form = useForm<EditUserPasswordFormValues>({ const form = useForm<EditUserPasswordFormValues>({
defaultValues: {}, defaultValues: {},
reValidateMode: 'onSubmit', reValidateMode: 'onSubmit',
resolver: yupResolver(EditUserPasswordFormValidationSchema), resolver: yupResolver(validationSchema),
}); });
const handleSubmit = async ({ password }: EditUserPasswordFormValues) => { const handleSubmit = async ({ password }: EditUserPasswordFormValues) => {

View File

@@ -1,4 +1,5 @@
import { useDialog } from '@/components/common/DialogProvider'; import { useDialog } from '@/components/common/DialogProvider';
import FormActivityIndicator from '@/components/common/FormActivityIndicator';
import type { EditUserFormValues } from '@/components/users/EditUserForm'; import type { EditUserFormValues } from '@/components/users/EditUserForm';
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication'; import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
import { useRemoteApplicationGQLClient } from '@/hooks/useRemoteApplicationGQLClient'; import { useRemoteApplicationGQLClient } from '@/hooks/useRemoteApplicationGQLClient';
@@ -17,7 +18,6 @@ import Text from '@/ui/v2/Text';
import getReadableProviderName from '@/utils/common/getReadableProviderName'; import getReadableProviderName from '@/utils/common/getReadableProviderName';
import getUserRoles from '@/utils/settings/getUserRoles'; import getUserRoles from '@/utils/settings/getUserRoles';
import { getToastStyleProps } from '@/utils/settings/settingsConstants'; import { getToastStyleProps } from '@/utils/settings/settingsConstants';
import type { RemoteAppGetUsersQuery } from '@/utils/__generated__/graphql';
import { import {
useDeleteRemoteAppUserRolesMutation, useDeleteRemoteAppUserRolesMutation,
useGetRolesQuery, useGetRolesQuery,
@@ -25,16 +25,21 @@ import {
useRemoteAppDeleteUserMutation, useRemoteAppDeleteUserMutation,
useUpdateRemoteAppUserMutation, useUpdateRemoteAppUserMutation,
} from '@/utils/__generated__/graphql'; } from '@/utils/__generated__/graphql';
import type { ApolloQueryResult } from '@apollo/client';
import { useTheme } from '@mui/material'; import { useTheme } from '@mui/material';
import { formatDistance } from 'date-fns'; import { formatDistance } from 'date-fns';
import kebabCase from 'just-kebab-case'; import kebabCase from 'just-kebab-case';
import dynamic from 'next/dynamic';
import Image from 'next/image'; import Image from 'next/image';
import type { RemoteAppUser } from 'pages/[workspaceSlug]/[appSlug]/users'; import type { RemoteAppUser } from 'pages/[workspaceSlug]/[appSlug]/users';
import { Fragment, useMemo } from 'react'; import { Fragment, useMemo } from 'react';
import toast from 'react-hot-toast'; import toast from 'react-hot-toast';
export interface UsersBodyProps<T = {}> { const EditUserForm = dynamic(() => import('@/components/users/EditUserForm'), {
ssr: false,
loading: () => <FormActivityIndicator />,
});
export interface UsersBodyProps {
/** /**
* The users fetched from entering the users page given a limit and offset. * The users fetched from entering the users page given a limit and offset.
* @remark users will be an empty array if there are no users. * @remark users will be an empty array if there are no users.
@@ -46,13 +51,10 @@ export interface UsersBodyProps<T = {}> {
* @example onSuccessfulAction={() => refetch()} * @example onSuccessfulAction={() => refetch()}
* @example onSuccessfulAction={() => router.reload()} * @example onSuccessfulAction={() => router.reload()}
*/ */
onSuccessfulAction?: () => Promise<void> | void | Promise<T>; onSubmit?: () => Promise<any>;
} }
export default function UsersBody({ export default function UsersBody({ users, onSubmit }: UsersBodyProps) {
users,
onSuccessfulAction,
}: UsersBodyProps<ApolloQueryResult<RemoteAppGetUsersQuery>>) {
const theme = useTheme(); const theme = useTheme();
const { openAlertDialog, openDrawer, closeDrawer } = useDialog(); const { openAlertDialog, openDrawer, closeDrawer } = useDialog();
const { currentApplication } = useCurrentWorkspaceAndApplication(); const { currentApplication } = useCurrentWorkspaceAndApplication();
@@ -151,7 +153,8 @@ export default function UsersBody({
}, },
getToastStyleProps(), getToastStyleProps(),
); );
await onSuccessfulAction?.();
await onSubmit?.();
closeDrawer(); closeDrawer();
} }
@@ -181,7 +184,7 @@ export default function UsersBody({
getToastStyleProps(), getToastStyleProps(),
); );
await onSuccessfulAction(); await onSubmit();
closeDrawer(); closeDrawer();
}, },
primaryButtonColor: 'error', primaryButtonColor: 'error',
@@ -191,20 +194,20 @@ export default function UsersBody({
} }
function handleViewUser(user: RemoteAppUser) { function handleViewUser(user: RemoteAppUser) {
openDrawer('EDIT_USER', { openDrawer({
title: 'User Details', title: 'User Details',
component: (
payload: { <EditUserForm
user, user={user}
onEditUser: handleEditUser, onSubmit={(values) => handleEditUser(values, user)}
onDeleteUser: handleDeleteUser, onDeleteUser={handleDeleteUser}
onSuccessfulAction, roles={allAvailableProjectRoles.map((role) => ({
roles: allAvailableProjectRoles.map((role) => ({ [role.name]: user.roles.some(
[role.name]: user.roles.some( (userRole) => userRole.role === role.name,
(userRole) => userRole.role === role.name, ),
), }))}
})), />
}, ),
}); });
} }

View File

@@ -1,4 +1,5 @@
import { useDialog } from '@/components/common/DialogProvider'; import { useDialog } from '@/components/common/DialogProvider';
import EditWorkspaceNameForm from '@/components/home/EditWorkspaceNameForm';
import RemoveWorkspaceModal from '@/components/workspace/RemoveWorkspaceModal'; import RemoveWorkspaceModal from '@/components/workspace/RemoveWorkspaceModal';
import { useUI } from '@/context/UIContext'; import { useUI } from '@/context/UIContext';
import { useGetWorkspace } from '@/hooks/use-GetWorkspace'; import { useGetWorkspace } from '@/hooks/use-GetWorkspace';
@@ -114,7 +115,7 @@ export default function WorkspaceHeader() {
<Dropdown.Item <Dropdown.Item
className="py-2" className="py-2"
onClick={() => { onClick={() => {
openDialog('EDIT_WORKSPACE_NAME', { openDialog({
title: ( title: (
<span className="grid grid-flow-row"> <span className="grid grid-flow-row">
<span>Change Workspace Name</span> <span>Change Workspace Name</span>
@@ -124,10 +125,12 @@ export default function WorkspaceHeader() {
</Text> </Text>
</span> </span>
), ),
payload: { component: (
currentWorkspaceName: currentWorkspace.name, <EditWorkspaceNameForm
currentWorkspaceId: currentWorkspace.id, currentWorkspaceId={currentWorkspace.id}
}, currentWorkspaceName={currentWorkspace.name}
/>
),
}); });
}} }}
> >

View File

@@ -1,4 +1,5 @@
import { useDialog } from '@/components/common/DialogProvider'; import { useDialog } from '@/components/common/DialogProvider';
import EditWorkspaceNameForm from '@/components/home/EditWorkspaceNameForm';
import Button from '@/ui/v2/Button'; import Button from '@/ui/v2/Button';
import PlusCircleIcon from '@/ui/v2/icons/PlusCircleIcon'; import PlusCircleIcon from '@/ui/v2/icons/PlusCircleIcon';
import Text from '@/ui/v2/Text'; import Text from '@/ui/v2/Text';
@@ -16,7 +17,7 @@ export function WorkspaceSection() {
variant="borderless" variant="borderless"
color="secondary" color="secondary"
onClick={() => { onClick={() => {
openDialog('EDIT_WORKSPACE_NAME', { openDialog({
title: ( title: (
<span className="grid grid-flow-row"> <span className="grid grid-flow-row">
<span>New Workspace</span> <span>New Workspace</span>
@@ -26,6 +27,7 @@ export function WorkspaceSection() {
</Text> </Text>
</span> </span>
), ),
component: <EditWorkspaceNameForm />,
}); });
}} }}
startIcon={<PlusCircleIcon />} startIcon={<PlusCircleIcon />}

View File

@@ -2,6 +2,7 @@ import { useDialog } from '@/components/common/DialogProvider';
import Pagination from '@/components/common/Pagination'; import Pagination from '@/components/common/Pagination';
import Container from '@/components/layout/Container'; import Container from '@/components/layout/Container';
import ProjectLayout from '@/components/layout/ProjectLayout'; import ProjectLayout from '@/components/layout/ProjectLayout';
import CreateUserForm from '@/components/users/CreateUserForm';
import UsersBody from '@/components/users/UsersBody'; import UsersBody from '@/components/users/UsersBody';
import { useRemoteApplicationGQLClient } from '@/hooks/useRemoteApplicationGQLClient'; import { useRemoteApplicationGQLClient } from '@/hooks/useRemoteApplicationGQLClient';
import ActivityIndicator from '@/ui/v2/ActivityIndicator'; import ActivityIndicator from '@/ui/v2/ActivityIndicator';
@@ -25,7 +26,7 @@ export type RemoteAppUser = Exclude<
>; >;
export default function UsersPage() { export default function UsersPage() {
const { openDialog, closeDialog } = useDialog(); const { openDialog } = useDialog();
const remoteProjectGQLClient = useRemoteApplicationGQLClient(); const remoteProjectGQLClient = useRemoteApplicationGQLClient();
const [searchString, setSearchString] = useState<string>(''); const [searchString, setSearchString] = useState<string>('');
@@ -200,14 +201,9 @@ export default function UsersPage() {
); );
function openCreateUserDialog() { function openCreateUserDialog() {
openDialog('CREATE_USER', { openDialog({
title: 'Create User', title: 'Create User',
payload: { component: <CreateUserForm onSubmit={refetchProjectUsers} />,
onSuccess: async () => {
await refetchProjectUsers();
closeDialog();
},
},
}); });
} }
@@ -228,16 +224,16 @@ export default function UsersPage() {
if (loadingRemoteAppUsersQuery) { if (loadingRemoteAppUsersQuery) {
return ( return (
<Container <Container
className="flex flex-col max-w-9xl h-full" className="flex h-full max-w-9xl flex-col"
rootClassName="h-full" rootClassName="h-full"
> >
<div className="flex flex-row place-content-between shrink-0 grow-0"> <div className="flex shrink-0 grow-0 flex-row place-content-between">
<Input <Input
className="rounded-sm" className="rounded-sm"
placeholder="Search users" placeholder="Search users"
startAdornment={ startAdornment={
<SearchIcon <SearchIcon
className="w-4 h-4 ml-2 -mr-1 shrink-0" className="ml-2 -mr-1 h-4 w-4 shrink-0"
sx={{ color: 'text.disabled' }} sx={{ color: 'text.disabled' }}
/> />
} }
@@ -245,14 +241,14 @@ export default function UsersPage() {
/> />
<Button <Button
onClick={openCreateUserDialog} onClick={openCreateUserDialog}
startIcon={<PlusIcon className="w-4 h-4" />} startIcon={<PlusIcon className="h-4 w-4" />}
size="small" size="small"
> >
Create User Create User
</Button> </Button>
</div> </div>
<div className="overflow-hidden flex items-center justify-center flex-auto"> <div className="flex flex-auto items-center justify-center overflow-hidden">
<ActivityIndicator label="Loading users..." /> <ActivityIndicator label="Loading users..." />
</div> </div>
</Container> </Container>
@@ -260,14 +256,14 @@ export default function UsersPage() {
} }
return ( return (
<Container className="mx-auto space-y-5 overflow-x-hidden max-w-9xl"> <Container className="mx-auto max-w-9xl space-y-5 overflow-x-hidden">
<div className="flex flex-row place-content-between"> <div className="flex flex-row place-content-between">
<Input <Input
className="rounded-sm" className="rounded-sm"
placeholder="Search users" placeholder="Search users"
startAdornment={ startAdornment={
<SearchIcon <SearchIcon
className="w-4 h-4 ml-2 -mr-1 shrink-0" className="ml-2 -mr-1 h-4 w-4 shrink-0"
sx={{ color: 'text.disabled' }} sx={{ color: 'text.disabled' }}
/> />
} }
@@ -275,21 +271,21 @@ export default function UsersPage() {
/> />
<Button <Button
onClick={openCreateUserDialog} onClick={openCreateUserDialog}
startIcon={<PlusIcon className="w-4 h-4" />} startIcon={<PlusIcon className="h-4 w-4" />}
size="small" size="small"
> >
Create User Create User
</Button> </Button>
</div> </div>
{usersCount === 0 ? ( {usersCount === 0 ? (
<Box className="flex flex-col items-center justify-center px-48 py-12 space-y-5 border rounded-lg shadow-sm"> <Box className="flex flex-col items-center justify-center space-y-5 rounded-lg border px-48 py-12 shadow-sm">
<UserIcon <UserIcon
strokeWidth={1} strokeWidth={1}
className="w-10 h-10" className="h-10 w-10"
sx={{ color: 'text.disabled' }} sx={{ color: 'text.disabled' }}
/> />
<div className="flex flex-col space-y-1"> <div className="flex flex-col space-y-1">
<Text className="font-medium text-center" variant="h3"> <Text className="text-center font-medium" variant="h3">
There are no users yet There are no users yet
</Text> </Text>
<Text variant="subtitle1" className="text-center"> <Text variant="subtitle1" className="text-center">
@@ -302,34 +298,34 @@ export default function UsersPage() {
color="primary" color="primary"
className="w-full" className="w-full"
onClick={openCreateUserDialog} onClick={openCreateUserDialog}
startIcon={<PlusIcon className="w-4 h-4" />} startIcon={<PlusIcon className="h-4 w-4" />}
> >
Create User Create User
</Button> </Button>
</div> </div>
</Box> </Box>
) : ( ) : (
<div className="grid grid-flow-row gap-2 lg:w-9xl"> <div className="lg:w-9xl grid grid-flow-row gap-2">
<div className="grid w-full h-full grid-flow-row pb-4 overflow-hidden"> <div className="grid h-full w-full grid-flow-row overflow-hidden pb-4">
<Box className="grid w-full p-2 border-b md:grid-cols-6"> <Box className="grid w-full border-b p-2 md:grid-cols-6">
<Text className="font-medium md:col-span-2">Name</Text> <Text className="font-medium md:col-span-2">Name</Text>
<Text className="hidden font-medium md:block">Signed up at</Text> <Text className="hidden font-medium md:block">Signed up at</Text>
<Text className="hidden font-medium md:block">Last Seen</Text> <Text className="hidden font-medium md:block">Last Seen</Text>
<Text className="hidden col-span-2 font-medium md:block"> <Text className="col-span-2 hidden font-medium md:block">
OAuth Providers OAuth Providers
</Text> </Text>
</Box> </Box>
{dataRemoteAppUsers?.filteredUsersAggreggate.aggregate.count === {dataRemoteAppUsers?.filteredUsersAggreggate.aggregate.count ===
0 && 0 &&
usersCount !== 0 && ( usersCount !== 0 && (
<Box className="flex flex-col items-center justify-center px-48 py-12 space-y-5 border-b border-x"> <Box className="flex flex-col items-center justify-center space-y-5 border-x border-b px-48 py-12">
<UserIcon <UserIcon
strokeWidth={1} strokeWidth={1}
className="w-10 h-10" className="h-10 w-10"
sx={{ color: 'text.disabled' }} sx={{ color: 'text.disabled' }}
/> />
<div className="flex flex-col space-y-1"> <div className="flex flex-col space-y-1">
<Text className="font-medium text-center" variant="h3"> <Text className="text-center font-medium" variant="h3">
No results for &quot;{searchString}&quot; No results for &quot;{searchString}&quot;
</Text> </Text>
<Text variant="subtitle1" className="text-center"> <Text variant="subtitle1" className="text-center">
@@ -340,10 +336,7 @@ export default function UsersPage() {
)} )}
{thereAreUsers && ( {thereAreUsers && (
<div className="grid grid-flow-row gap-4"> <div className="grid grid-flow-row gap-4">
<UsersBody <UsersBody users={users} onSubmit={refetchProjectUsers} />
users={users}
onSuccessfulAction={refetchProjectUsers}
/>
<Pagination <Pagination
className="px-2" className="px-2"
totalNrOfPages={nrOfPages} totalNrOfPages={nrOfPages}

View File

@@ -0,0 +1,10 @@
/**
* This interface is used to define the basic properties of a form that is
* rendered inside a drawer or a dialog.
*/
export interface DialogFormProps {
/**
* Determines whether the form is rendered inside a drawer or a dialog.
*/
location?: 'drawer' | 'dialog';
}

View File

@@ -1,5 +1,26 @@
# @nhost-examples/codegen-react-apollo # @nhost-examples/codegen-react-apollo
## 0.1.7
### Patch Changes
- 01318860: fix(nhost-js): use correct URL for functions requests
- Updated dependencies [01318860]
- @nhost/react-apollo@5.0.5
- @nhost/react@2.0.4
## 0.1.6
### Patch Changes
- 445d8ef4: chore(deps): bump `@nhost/react` to 2.0.3
- 445d8ef4: chore(deps): bump `@nhost/react-apollo` to 5.0.4
- Updated dependencies [445d8ef4]
- Updated dependencies [445d8ef4]
- Updated dependencies [445d8ef4]
- @nhost/react-apollo@5.0.4
- @nhost/react@2.0.3
## 0.1.5 ## 0.1.5
### Patch Changes ### Patch Changes

View File

@@ -1,6 +1,6 @@
{ {
"name": "@nhost-examples/codegen-react-apollo", "name": "@nhost-examples/codegen-react-apollo",
"version": "0.1.5", "version": "0.1.7",
"private": true, "private": true,
"scripts": { "scripts": {
"codegen": "graphql-codegen", "codegen": "graphql-codegen",

View File

@@ -1,5 +1,21 @@
# @nhost-examples/codegen-react-query # @nhost-examples/codegen-react-query
## 0.1.7
### Patch Changes
- 01318860: fix(nhost-js): use correct URL for functions requests
- Updated dependencies [01318860]
- @nhost/react@2.0.4
## 0.1.6
### Patch Changes
- 445d8ef4: chore(deps): bump `@nhost/react` to 2.0.3
- Updated dependencies [445d8ef4]
- @nhost/react@2.0.3
## 0.1.5 ## 0.1.5
### Patch Changes ### Patch Changes

View File

@@ -1,6 +1,6 @@
{ {
"name": "@nhost-examples/codegen-react-query", "name": "@nhost-examples/codegen-react-query",
"version": "0.1.5", "version": "0.1.7",
"private": true, "private": true,
"scripts": { "scripts": {
"codegen": "graphql-codegen", "codegen": "graphql-codegen",

View File

@@ -1,5 +1,24 @@
# @nhost-examples/react-urql # @nhost-examples/react-urql
## 0.0.4
### Patch Changes
- 01318860: fix(nhost-js): use correct URL for functions requests
- Updated dependencies [01318860]
- @nhost/react-urql@2.0.4
- @nhost/react@2.0.4
## 0.0.3
### Patch Changes
- 445d8ef4: chore(deps): bump `@nhost/react` to 2.0.3
- Updated dependencies [445d8ef4]
- Updated dependencies [445d8ef4]
- @nhost/react-urql@2.0.3
- @nhost/react@2.0.3
## 0.0.2 ## 0.0.2
### Patch Changes ### Patch Changes

View File

@@ -1,7 +1,7 @@
{ {
"name": "@nhost-examples/codegen-react-urql", "name": "@nhost-examples/codegen-react-urql",
"private": true, "private": true,
"version": "0.0.2", "version": "0.0.4",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "tsc && vite build", "build": "tsc && vite build",

View File

@@ -1,5 +1,11 @@
# @nhost-examples/docker-compose # @nhost-examples/docker-compose
## 0.0.5
### Patch Changes
- 01318860: fix(nhost-js): use correct URL for functions requests
## 0.0.4 ## 0.0.4
### Patch Changes ### Patch Changes

View File

@@ -1,6 +1,6 @@
{ {
"name": "@nhost-examples/docker-compose", "name": "@nhost-examples/docker-compose",
"version": "0.0.4", "version": "0.0.5",
"private": true, "private": true,
"scripts": { "scripts": {
"e2e": "vitest run" "e2e": "vitest run"

View File

@@ -1,5 +1,21 @@
# @nhost-examples/multi-tenant-one-to-many # @nhost-examples/multi-tenant-one-to-many
## 1.0.3
### Patch Changes
- 01318860: fix(nhost-js): use correct URL for functions requests
- Updated dependencies [01318860]
- @nhost/nhost-js@2.0.4
## 1.0.2
### Patch Changes
- 445d8ef4: chore(deps): bump `@nhost/nhost-js` version to 2.0.3
- Updated dependencies [445d8ef4]
- @nhost/nhost-js@2.0.3
## 1.0.1 ## 1.0.1
### Patch Changes ### Patch Changes

View File

@@ -1,7 +1,7 @@
{ {
"name": "@nhost-examples/multi-tenant-one-to-many", "name": "@nhost-examples/multi-tenant-one-to-many",
"private": true, "private": true,
"version": "1.0.1", "version": "1.0.3",
"description": "", "description": "",
"main": "index.js", "main": "index.js",
"scripts": {}, "scripts": {},

View File

@@ -1,5 +1,29 @@
# @nhost-examples/nextjs # @nhost-examples/nextjs
## 0.1.7
### Patch Changes
- 01318860: fix(nhost-js): use correct URL for functions requests
- Updated dependencies [01318860]
- @nhost/react-apollo@5.0.5
- @nhost/nextjs@1.13.10
- @nhost/react@2.0.4
## 0.1.6
### Patch Changes
- 445d8ef4: chore(deps): bump `@nhost/react` to 2.0.3
- 445d8ef4: chore(deps): bump `@nhost/react-apollo` to 5.0.4
- 445d8ef4: chore(deps): bump `@nhost/nextjs` to 1.13.9
- Updated dependencies [445d8ef4]
- Updated dependencies [445d8ef4]
- Updated dependencies [445d8ef4]
- @nhost/react-apollo@5.0.4
- @nhost/nextjs@1.13.9
- @nhost/react@2.0.3
## 0.1.5 ## 0.1.5
### Patch Changes ### Patch Changes

View File

@@ -1,6 +1,6 @@
{ {
"name": "@nhost-examples/nextjs", "name": "@nhost-examples/nextjs",
"version": "0.1.5", "version": "0.1.7",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev", "dev": "next dev",

View File

@@ -1,5 +1,26 @@
# @nhost-examples/react-apollo # @nhost-examples/react-apollo
## 0.1.9
### Patch Changes
- 01318860: fix(nhost-js): use correct URL for functions requests
- Updated dependencies [01318860]
- @nhost/react-apollo@5.0.5
- @nhost/react@2.0.4
## 0.1.8
### Patch Changes
- 445d8ef4: chore(deps): bump `@nhost/react` to 2.0.3
- 445d8ef4: chore(deps): bump `@nhost/react-apollo` to 5.0.4
- Updated dependencies [445d8ef4]
- Updated dependencies [445d8ef4]
- Updated dependencies [445d8ef4]
- @nhost/react-apollo@5.0.4
- @nhost/react@2.0.3
## 0.1.7 ## 0.1.7
### Patch Changes ### Patch Changes

View File

@@ -1,6 +1,6 @@
{ {
"name": "@nhost-examples/react-apollo", "name": "@nhost-examples/react-apollo",
"version": "0.1.7", "version": "0.1.9",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@apollo/client": "^3.6.9", "@apollo/client": "^3.6.9",

View File

@@ -1,5 +1,21 @@
# @nhost-examples/react-gqty # @nhost-examples/react-gqty
## 0.0.6
### Patch Changes
- 01318860: fix(nhost-js): use correct URL for functions requests
- Updated dependencies [01318860]
- @nhost/react@2.0.4
## 0.0.5
### Patch Changes
- 445d8ef4: chore(deps): bump `@nhost/react` to 2.0.3
- Updated dependencies [445d8ef4]
- @nhost/react@2.0.3
## 0.0.4 ## 0.0.4
### Patch Changes ### Patch Changes

View File

@@ -1,7 +1,7 @@
{ {
"name": "@nhost-examples/react-gqty", "name": "@nhost-examples/react-gqty",
"private": true, "private": true,
"version": "0.0.4", "version": "0.0.6",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",

View File

@@ -0,0 +1,7 @@
# @nhost-examples/seed-data-storage
## 0.0.2
### Patch Changes
- 01318860: fix(nhost-js): use correct URL for functions requests

View File

@@ -1,7 +1,7 @@
{ {
"name": "@nhost-examples/seed-data-storage", "name": "@nhost-examples/seed-data-storage",
"private": true, "private": true,
"version": "0.0.1", "version": "0.0.2",
"scripts": { "scripts": {
"seed-storage": "./seed-storage.sh" "seed-storage": "./seed-storage.sh"
} }

View File

@@ -1,5 +1,13 @@
# @nhost-examples/serverless-functions # @nhost-examples/serverless-functions
## 0.0.7
### Patch Changes
- 01318860: fix(nhost-js): use correct URL for functions requests
- Updated dependencies [01318860]
- @nhost/stripe-graphql-js@1.0.2
## 0.0.6 ## 0.0.6
### Patch Changes ### Patch Changes

View File

@@ -1,13 +1,13 @@
{ {
"name": "@nhost-examples/serverless-functions", "name": "@nhost-examples/serverless-functions",
"private": true, "private": true,
"version": "0.0.6", "version": "0.0.7",
"devDependencies": { "devDependencies": {
"@types/express": "^4.17.13" "@types/express": "^4.17.13"
}, },
"dependencies": { "dependencies": {
"@graphql-yoga/node": "^2.13.13", "@graphql-yoga/node": "^2.13.13",
"@nhost/stripe-graphql-js": "^1.0.1", "@nhost/stripe-graphql-js": "^1.0.2",
"@pothos/core": "^3.21.0", "@pothos/core": "^3.21.0",
"cross-fetch": "^3.1.5", "cross-fetch": "^3.1.5",
"graphql": "15.7.2", "graphql": "15.7.2",

View File

@@ -1,5 +1,23 @@
# @nhost-examples/vue-apollo # @nhost-examples/vue-apollo
## 0.0.7
### Patch Changes
- 01318860: fix(nhost-js): use correct URL for functions requests
- Updated dependencies [01318860]
- @nhost/apollo@5.0.4
- @nhost/vue@1.13.10
## 0.0.6
### Patch Changes
- 445d8ef4: chore(deps): bump `@nhost/apollo` to 5.0.3
- Updated dependencies [445d8ef4]
- @nhost/apollo@5.0.3
- @nhost/vue@1.13.9
## 0.0.5 ## 0.0.5
### Patch Changes ### Patch Changes

View File

@@ -1,7 +1,7 @@
{ {
"name": "@nhost-examples/vue-apollo", "name": "@nhost-examples/vue-apollo",
"private": true, "private": true,
"version": "0.0.5", "version": "0.0.7",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "vite build", "build": "vite build",

View File

@@ -1,5 +1,23 @@
# @nhost-examples/vue-quickstart # @nhost-examples/vue-quickstart
## 0.0.6
### Patch Changes
- 01318860: fix(nhost-js): use correct URL for functions requests
- Updated dependencies [01318860]
- @nhost/apollo@5.0.4
- @nhost/vue@1.13.10
## 0.0.5
### Patch Changes
- 445d8ef4: chore(deps): bump `@nhost/apollo` to 5.0.3
- Updated dependencies [445d8ef4]
- @nhost/apollo@5.0.3
- @nhost/vue@1.13.9
## 0.0.4 ## 0.0.4
### Patch Changes ### Patch Changes

View File

@@ -1,6 +1,6 @@
{ {
"name": "@nhost-examples/vue-quickstart", "name": "@nhost-examples/vue-quickstart",
"version": "0.0.4", "version": "0.0.6",
"private": true, "private": true,
"scripts": { "scripts": {
"build": "vite build", "build": "vite build",

View File

@@ -1,5 +1,41 @@
# @nhost/apollo # @nhost/apollo
## 5.0.5
### Patch Changes
- Updated dependencies [3c7cf92e]
- @nhost/nhost-js@2.0.5
## 5.0.4
### Patch Changes
- 01318860: fix(nhost-js): use correct URL for functions requests
- Updated dependencies [01318860]
- @nhost/nhost-js@2.0.4
## 5.0.3
### Patch Changes
- 445d8ef4: chore(deps): bump `@nhost/nhost-js` version to 2.0.3
- Updated dependencies [445d8ef4]
- @nhost/nhost-js@2.0.3
## 5.0.2
### Patch Changes
- Updated dependencies [2d9145f9]
- @nhost/nhost-js@2.0.2
## 5.0.1
### Patch Changes
- @nhost/nhost-js@2.0.1
## 5.0.0 ## 5.0.0
### Patch Changes ### Patch Changes

View File

@@ -1,6 +1,6 @@
{ {
"name": "@nhost/apollo", "name": "@nhost/apollo",
"version": "5.0.0", "version": "5.0.5",
"description": "Nhost Apollo Client library", "description": "Nhost Apollo Client library",
"license": "MIT", "license": "MIT",
"keywords": [ "keywords": [

View File

@@ -1,5 +1,11 @@
# @nhost/google-translation # @nhost/google-translation
## 0.0.3
### Patch Changes
- 01318860: fix(nhost-js): use correct URL for functions requests
## 0.0.2 ## 0.0.2
### Patch Changes ### Patch Changes

View File

@@ -1,6 +1,6 @@
{ {
"name": "@nhost/google-translation", "name": "@nhost/google-translation",
"version": "0.0.2", "version": "0.0.3",
"description": "Google Translation GraphQL API", "description": "Google Translation GraphQL API",
"license": "MIT", "license": "MIT",
"keywords": [ "keywords": [

View File

@@ -1,5 +1,45 @@
# @nhost/react-apollo # @nhost/react-apollo
## 5.0.6
### Patch Changes
- @nhost/apollo@5.0.5
- @nhost/react@2.0.5
## 5.0.5
### Patch Changes
- 01318860: fix(nhost-js): use correct URL for functions requests
- Updated dependencies [01318860]
- @nhost/apollo@5.0.4
- @nhost/react@2.0.4
## 5.0.4
### Patch Changes
- 445d8ef4: chore(deps): bump `@nhost/react` to 2.0.3
- 445d8ef4: chore(deps): bump `@nhost/apollo` to 5.0.3
- Updated dependencies [445d8ef4]
- @nhost/apollo@5.0.3
- @nhost/react@2.0.3
## 5.0.3
### Patch Changes
- @nhost/apollo@5.0.2
- @nhost/react@2.0.2
## 5.0.2
### Patch Changes
- @nhost/apollo@5.0.1
- @nhost/react@2.0.1
## 5.0.1 ## 5.0.1
### Patch Changes ### Patch Changes

View File

@@ -1,6 +1,6 @@
{ {
"name": "@nhost/react-apollo", "name": "@nhost/react-apollo",
"version": "5.0.1", "version": "5.0.6",
"description": "Nhost React Apollo client", "description": "Nhost React Apollo client",
"license": "MIT", "license": "MIT",
"keywords": [ "keywords": [

View File

@@ -1,5 +1,39 @@
# @nhost/react-urql # @nhost/react-urql
## 2.0.5
### Patch Changes
- @nhost/react@2.0.5
## 2.0.4
### Patch Changes
- 01318860: fix(nhost-js): use correct URL for functions requests
- Updated dependencies [01318860]
- @nhost/react@2.0.4
## 2.0.3
### Patch Changes
- 445d8ef4: chore(deps): bump `@nhost/react` to 2.0.3
- Updated dependencies [445d8ef4]
- @nhost/react@2.0.3
## 2.0.2
### Patch Changes
- @nhost/react@2.0.2
## 2.0.1
### Patch Changes
- @nhost/react@2.0.1
## 2.0.0 ## 2.0.0
### Patch Changes ### Patch Changes

View File

@@ -1,6 +1,6 @@
{ {
"name": "@nhost/react-urql", "name": "@nhost/react-urql",
"version": "2.0.0", "version": "2.0.5",
"description": "Nhost React URQL client", "description": "Nhost React URQL client",
"license": "MIT", "license": "MIT",
"keywords": [ "keywords": [

View File

@@ -1,5 +1,11 @@
# @nhost/stripe-graphql-js # @nhost/stripe-graphql-js
## 1.0.2
### Patch Changes
- 01318860: fix(nhost-js): use correct URL for functions requests
## 1.0.1 ## 1.0.1
### Patch Changes ### Patch Changes

View File

@@ -1,6 +1,6 @@
{ {
"name": "@nhost/stripe-graphql-js", "name": "@nhost/stripe-graphql-js",
"version": "1.0.1", "version": "1.0.2",
"description": "Stripe GraphQL API", "description": "Stripe GraphQL API",
"license": "MIT", "license": "MIT",
"keywords": [ "keywords": [

View File

@@ -0,0 +1,14 @@
# @nhost/graphql-js
## 0.0.3
### Patch Changes
- 2d9145f9: chore(deps): revert GraphQL client
## 0.0.2
### Patch Changes
- 2200a0ed: Correct type inference on snake case operations
- 3b48a627: Improve readme instructions

View File

@@ -11,130 +11,6 @@
Nhost GraphQL client. Nhost GraphQL client.
## Install
First, install `graphql-codegen` and the Nhost Typescript plugin:
```sh
npm install @nhost/graphql-js
npm install -D @graphql-codegen/cli @graphql-codegen/typescript-nhost
```
## Configure the code generator
Configure the code generator by adding a `graphql.config.yaml` file:
```yaml filename="graphql.config.yaml"
schema:
- http://localhost:1337/v1/graphql:
headers:
x-hasura-admin-secret: nhost-admin-secret
generates:
./src/schema.ts:
plugins:
- typescript-nhost
```
Generate the schema:
```sh
yarn graphql-codegen
```
## Usage
```ts filename="./src/main.ts"
import { NhostGraphqlClient } from '@nhost/graphql-js'
import schema from './schema.ts'
const client = new NhostGraphqlClient({ url: 'http://localhost:1337/v1/graphql', schema })
```
### Basic GraphQL requests
### Queries
```ts
const todos = await client.query.todos({
select: { createdAt: true, contents: true, user: { select: { displayName: true } } }
})
todos.map(({ createdAt, contents, user: { displayName } }) => {
console.log(`${displayName} created the following todo at ${createdAt}: ${contents}`)
})
```
Select all scalar fields:
```ts
const todos = await client.query.todos()
todos.map(({ createdAt, contents }) => {
console.log(`Todo created at ${createdAt}: ${contents}`)
})
```
Pass on parameters:
```ts
const todos = await client.query.todos({
variables: { where: { contents: { _eq: 'document the sdk' } } },
select: { createdAt: true, contents: true, user: { select: { displayName: true } } }
})
todos.map(({ createdAt, contents }) => {
console.log(`${displayName} created the following todo at ${createdAt}: ${contents}`)
})
```
### Mutations
```ts
const { id } = await client.mutation.insertTodo({
select: { id: true },
variables: { contents: 'document the sdk', userId: 'xxx-yyy-zzz' }
})
```
### Enums
```ts
const todos = await client.query.todos({
variables: {
where: {
category: { _eq: 'essay' } // the client detects 'essay' is a GraphQL enum value
}
},
contents: true,
category: true
})
```
### Unions
```ts
const giraffes = await client.query
.giraffeFacts({
_on: {
GiraffeNumericFact: { value: true },
GiraffeStringFact: { fact: true }
}
})
.run()
giraffes.forEach((giraffe) => {
if (giraffe.__typename === 'GiraffeNumericFact') {
// * We are in the GiraffeNumericFact fragment: only `value` is available
console.log('Value:', giraffe.value)
} else {
// * We are in the GiraffeStringFact fragment: only `fact` is available
console.log('Fact:', giraffe.fact)
}
})
```
### Interfaces
## Documentation ## Documentation
[https://docs.nhost.io/reference/javascript/auth](https://docs.nhost.io/reference/javascript/graphql) [https://docs.nhost.io/reference/javascript/graphql](https://docs.nhost.io/reference/javascript/graphql)

View File

@@ -1,131 +0,0 @@
import SchemaBuilder from '@pothos/core'
import { createYoga } from 'graphql-yoga'
class Human {
public pets: Pet[] = []
constructor(public phoneNumber: string, public firstName: string) {}
}
class Pet {
name: string
diet: Diet
owner: Human
constructor(name: string, diet: Diet, owner: Human) {
this.name = name
this.diet = diet
this.owner = owner
}
// constructor(public name: string, public diet: Diet, public owner: Human) {}
}
enum Diet {
HERBIVOROUS,
CARNIVOROUS,
OMNIVORIOUS
}
export class Dog extends Pet {
constructor(name: string, owner: Human, public barks: boolean) {
super(name, Diet.CARNIVOROUS, owner)
}
}
export class Hamster extends Pet {
constructor(name: string, owner: Human, public squeaks: boolean) {
super(name, Diet.HERBIVOROUS, owner)
}
}
const human1 = new Human('123-456-7890', 'John')
const dog1 = new Dog('Fido', human1, false)
const dog2 = new Dog('Rover', human1, true)
const hamster1 = new Hamster('Hammy', human1, true)
human1.pets = [dog1, dog2, hamster1]
const builder = new SchemaBuilder<{
Objects: {
Pet: Pet
Human: Human
Dog: Dog
}
}>({})
const HumanObject = builder.objectType('Human', {
fields: (t) => ({
phoneNumber: t.exposeString('phoneNumber', {}),
firstName: t.exposeString('firstName', {}),
pets: t.field({
type: [Pet],
resolve: (h) => h.pets
})
})
})
const PetObject = builder.interfaceType(Pet, {
name: 'Pet',
fields: (t) => ({
name: t.exposeString('name', {}),
owner: t.field({ type: HumanObject, resolve: (p) => p.owner }),
diet: t.expose('diet', {
type: Diet
})
})
})
const DogObject = builder.objectType('Dog', {
interfaces: [Pet],
isTypeOf: (value) => value instanceof Dog,
fields: (t) => ({
barks: t.exposeBoolean('barks', {})
})
})
const HamsterObject = builder.objectType(Hamster, {
name: 'Hamster',
interfaces: [Pet],
isTypeOf: (value) => value instanceof Hamster,
fields: (t) => ({
squeaks: t.exposeBoolean('squeaks', {})
})
})
builder.enumType(Diet, {
name: 'Diet'
})
const Anyone = builder.unionType('Anyone', {
types: [DogObject, HamsterObject, HumanObject],
resolveType: (fact) => {
if (fact instanceof Human) {
return HumanObject
}
if (fact instanceof Dog) {
return DogObject
}
if (fact instanceof Hamster) {
return HamsterObject
}
}
})
builder.queryField('everyone', (t) =>
t.field({
type: [Anyone],
resolve: () => {
return [human1, dog1, dog2, hamster1]
}
})
)
builder.queryField('pets', (t) =>
t.field({ type: [PetObject], resolve: () => [dog1, dog2, hamster1] })
)
builder.queryField('dogs', (t) => t.field({ type: [DogObject], resolve: () => [dog1, dog2] }))
builder.queryField('hamsters', (t) => t.field({ type: [HamsterObject], resolve: () => [hamster1] }))
builder.queryType({})
export default createYoga({
schema: builder.toSchema(),
graphqlEndpoint: '/'
})

View File

@@ -1,16 +0,0 @@
{
"name": "functions",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"@pothos/core": "^3.24.0",
"@types/node": "^18.11.18",
"graphql-yoga": "^3.3.0",
"graphql": "^16.6.0"
}
}

View File

@@ -1,24 +0,0 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"compilerOptions": {
"declaration": true,
"declarationMap": true,
"strict": true,
"isolatedModules": true,
"forceConsistentCasingInFileNames": true,
"allowSyntheticDefaultImports": true,
"skipLibCheck": true,
"moduleResolution": "node",
"target": "ES6",
"module": "CommonJS",
"types": [
"node"
],
"esModuleInterop": true,
"sourceMap": true
},
"exclude": [
"node_modules",
"dist"
]
}

View File

@@ -1,366 +0,0 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
"@envelop/core@3.0.4":
version "3.0.4"
resolved "https://registry.yarnpkg.com/@envelop/core/-/core-3.0.4.tgz#6801049bed24487599b4ffa0f836f70cb62714fc"
integrity sha512-AybIZxQsDlFQTWHy6YtX/MSQPVuw+eOFtTW90JsHn6EbmcQnD6N3edQfSiTGjggPRHLoC0+0cuYXp2Ly2r3vrQ==
dependencies:
"@envelop/types" "3.0.1"
tslib "2.4.0"
"@envelop/parser-cache@^5.0.4":
version "5.0.4"
resolved "https://registry.yarnpkg.com/@envelop/parser-cache/-/parser-cache-5.0.4.tgz#4ff19c16c601c6137a6774fc5660f2e18768c05c"
integrity sha512-+kp6nzCVLYI2WQExQcE3FSy6n9ZGB5GYi+ntyjYdxaXU41U1f8RVwiLdyh0Ewn5D/s/zaLin09xkFKITVSAKDw==
dependencies:
lru-cache "^6.0.0"
tslib "^2.4.0"
"@envelop/types@3.0.1":
version "3.0.1"
resolved "https://registry.yarnpkg.com/@envelop/types/-/types-3.0.1.tgz#0afec3b3f1ab282bc828e6c42c5e26d76ffe363c"
integrity sha512-Ok62K1K+rlS+wQw77k8Pis8+1/h7+/9Wk5Fgcc2U6M5haEWsLFAHcHsk8rYlnJdEUl2Y3yJcCSOYbt1dyTaU5w==
dependencies:
tslib "^2.4.0"
"@envelop/validation-cache@^5.0.5":
version "5.0.5"
resolved "https://registry.yarnpkg.com/@envelop/validation-cache/-/validation-cache-5.0.5.tgz#9be1c1ba178460dcaf6d277136a381833cc4f931"
integrity sha512-69sq5H7hvxE+7VV60i0bgnOiV1PX9GEJHKrBrVvyEZAXqYojKO3DP9jnLGryiPgVaBjN5yw12ge0l0s2gXbolQ==
dependencies:
lru-cache "^6.0.0"
tslib "^2.4.0"
"@graphql-tools/executor@0.0.12":
version "0.0.12"
resolved "https://registry.yarnpkg.com/@graphql-tools/executor/-/executor-0.0.12.tgz#d885c7fa98a8aaeaa771163b71fb98ce9f52f9bd"
integrity sha512-bWpZcYRo81jDoTVONTnxS9dDHhEkNVjxzvFCH4CRpuyzD3uL+5w3MhtxIh24QyWm4LvQ4f+Bz3eMV2xU2I5+FA==
dependencies:
"@graphql-tools/utils" "9.1.4"
"@graphql-typed-document-node/core" "3.1.1"
"@repeaterjs/repeater" "3.0.4"
tslib "^2.4.0"
value-or-promise "1.0.12"
"@graphql-tools/merge@8.3.16":
version "8.3.16"
resolved "https://registry.yarnpkg.com/@graphql-tools/merge/-/merge-8.3.16.tgz#fede610687b148e34ff861e8b038dcd71e20039b"
integrity sha512-In0kcOZcPIpYOKaqdrJ3thdLPE7TutFnL9tbrHUy2zCinR2O/blpRC48jPckcs0HHrUQ0pGT4HqvzMkZUeEBAw==
dependencies:
"@graphql-tools/utils" "9.1.4"
tslib "^2.4.0"
"@graphql-tools/schema@^9.0.0":
version "9.0.14"
resolved "https://registry.yarnpkg.com/@graphql-tools/schema/-/schema-9.0.14.tgz#9a658ab82d5a7d4db73f68a44900d4c88a98f0bc"
integrity sha512-U6k+HY3Git+dsOEhq+dtWQwYg2CAgue8qBvnBXoKu5eEeH284wymMUoNm0e4IycOgMCJANVhClGEBIkLRu3FQQ==
dependencies:
"@graphql-tools/merge" "8.3.16"
"@graphql-tools/utils" "9.1.4"
tslib "^2.4.0"
value-or-promise "1.0.12"
"@graphql-tools/utils@9.1.4", "@graphql-tools/utils@^9.0.1":
version "9.1.4"
resolved "https://registry.yarnpkg.com/@graphql-tools/utils/-/utils-9.1.4.tgz#2c9e0aefc9655dd73247667befe3c850ec014f3f"
integrity sha512-hgIeLt95h9nQgQuzbbdhuZmh+8WV7RZ/6GbTj6t3IU4Zd2zs9yYJ2jgW/krO587GMOY8zCwrjNOMzD40u3l7Vg==
dependencies:
tslib "^2.4.0"
"@graphql-typed-document-node/core@3.1.1":
version "3.1.1"
resolved "https://registry.yarnpkg.com/@graphql-typed-document-node/core/-/core-3.1.1.tgz#076d78ce99822258cf813ecc1e7fa460fa74d052"
integrity sha512-NQ17ii0rK1b34VZonlmT2QMJFI70m0TRwbknO/ihlbatXyaktDhN/98vBiUU6kNBPljqGqyIrl2T4nY2RpFANg==
"@graphql-yoga/subscription@^3.1.0":
version "3.1.0"
resolved "https://registry.yarnpkg.com/@graphql-yoga/subscription/-/subscription-3.1.0.tgz#4a0bb0b9db2602d02c68f9828603e1e40329140b"
integrity sha512-Vc9lh8KzIHyS3n4jBlCbz7zCjcbtQnOBpsymcRvHhFr2cuH+knmRn0EmzimMQ58jQ8kxoRXXC3KJS3RIxSdPIg==
dependencies:
"@graphql-yoga/typed-event-target" "^1.0.0"
"@repeaterjs/repeater" "^3.0.4"
"@whatwg-node/events" "0.0.2"
tslib "^2.3.1"
"@graphql-yoga/typed-event-target@^1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@graphql-yoga/typed-event-target/-/typed-event-target-1.0.0.tgz#dae3c0146f08a4dc30b5b890f8bab706c2b62199"
integrity sha512-Mqni6AEvl3VbpMtKw+TIjc9qS9a8hKhiAjFtqX488yq5oJtj9TkNlFTIacAVS3vnPiswNsmDiQqvwUOcJgi1DA==
dependencies:
"@repeaterjs/repeater" "^3.0.4"
tslib "^2.3.1"
"@peculiar/asn1-schema@^2.1.6", "@peculiar/asn1-schema@^2.3.0":
version "2.3.3"
resolved "https://registry.yarnpkg.com/@peculiar/asn1-schema/-/asn1-schema-2.3.3.tgz#21418e1f3819e0b353ceff0c2dad8ccb61acd777"
integrity sha512-6GptMYDMyWBHTUKndHaDsRZUO/XMSgIns2krxcm2L7SEExRHwawFvSwNBhqNPR9HJwv3MruAiF1bhN0we6j6GQ==
dependencies:
asn1js "^3.0.5"
pvtsutils "^1.3.2"
tslib "^2.4.0"
"@peculiar/json-schema@^1.1.12":
version "1.1.12"
resolved "https://registry.yarnpkg.com/@peculiar/json-schema/-/json-schema-1.1.12.tgz#fe61e85259e3b5ba5ad566cb62ca75b3d3cd5339"
integrity sha512-coUfuoMeIB7B8/NMekxaDzLhaYmp0HZNPEjYRm9goRou8UZIC3z21s0sL9AWoCw4EG876QyO3kYrc61WNF9B/w==
dependencies:
tslib "^2.0.0"
"@peculiar/webcrypto@^1.4.0":
version "1.4.1"
resolved "https://registry.yarnpkg.com/@peculiar/webcrypto/-/webcrypto-1.4.1.tgz#821493bd5ad0f05939bd5f53b28536f68158360a"
integrity sha512-eK4C6WTNYxoI7JOabMoZICiyqRRtJB220bh0Mbj5RwRycleZf9BPyZoxsTvpP0FpmVS2aS13NKOuh5/tN3sIRw==
dependencies:
"@peculiar/asn1-schema" "^2.3.0"
"@peculiar/json-schema" "^1.1.12"
pvtsutils "^1.3.2"
tslib "^2.4.1"
webcrypto-core "^1.7.4"
"@pothos/core@^3.24.0":
version "3.24.0"
resolved "https://registry.yarnpkg.com/@pothos/core/-/core-3.24.0.tgz#1a555c501c9fc09c588196f83e6b0075571a59c5"
integrity sha512-LfWzUrmjhg9WQNUntQMJWOfMLb51AMunqBOC66zWEIi2GR4IcAQCPwzy77713Zd/awtIInIuHv4x5/1whAWeeA==
"@repeaterjs/repeater@3.0.4", "@repeaterjs/repeater@^3.0.4":
version "3.0.4"
resolved "https://registry.yarnpkg.com/@repeaterjs/repeater/-/repeater-3.0.4.tgz#a04d63f4d1bf5540a41b01a921c9a7fddc3bd1ca"
integrity sha512-AW8PKd6iX3vAZ0vA43nOUOnbq/X5ihgU+mSXXqunMkeQADGiqw/PY0JNeYtD5sr0PAy51YPgAPbDoeapv9r8WA==
"@types/node@^18.11.18":
version "18.11.18"
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.11.18.tgz#8dfb97f0da23c2293e554c5a50d61ef134d7697f"
integrity sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA==
"@whatwg-node/events@0.0.2":
version "0.0.2"
resolved "https://registry.yarnpkg.com/@whatwg-node/events/-/events-0.0.2.tgz#7b7107268d2982fc7b7aff5ee6803c64018f84dd"
integrity sha512-WKj/lI4QjnLuPrim0cfO7i+HsDSXHxNv1y0CrJhdntuO3hxWZmnXCwNDnwOvry11OjRin6cgWNF+j/9Pn8TN4w==
"@whatwg-node/fetch@0.6.2":
version "0.6.2"
resolved "https://registry.yarnpkg.com/@whatwg-node/fetch/-/fetch-0.6.2.tgz#fe4837505f6fc91bcfd6e12cdcec66f4aecfeecc"
integrity sha512-fCUycF1W+bI6XzwJFnbdDuxIldfKM3w8+AzVCLGlucm0D+AQ8ZMm2j84hdcIhfV6ZdE4Y1HFVrHosAxdDZ+nPw==
dependencies:
"@peculiar/webcrypto" "^1.4.0"
abort-controller "^3.0.0"
busboy "^1.6.0"
form-data-encoder "^1.7.1"
formdata-node "^4.3.1"
node-fetch "^2.6.7"
undici "^5.12.0"
urlpattern-polyfill "^6.0.2"
web-streams-polyfill "^3.2.0"
"@whatwg-node/server@0.5.8":
version "0.5.8"
resolved "https://registry.yarnpkg.com/@whatwg-node/server/-/server-0.5.8.tgz#e47aa4535431b6703652809dc4f7e5ebac6bffdb"
integrity sha512-29f2Ijk663Hr6hF5GU5a8ELGQVbNMMDBWF1lTdpIKGyLrLJTKixarp6COEyEN5H9tGzIRUQar9Z76A+Jb9DyzQ==
dependencies:
"@whatwg-node/fetch" "0.6.2"
tslib "^2.3.1"
abort-controller@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392"
integrity sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==
dependencies:
event-target-shim "^5.0.0"
asn1js@^3.0.1, asn1js@^3.0.5:
version "3.0.5"
resolved "https://registry.yarnpkg.com/asn1js/-/asn1js-3.0.5.tgz#5ea36820443dbefb51cc7f88a2ebb5b462114f38"
integrity sha512-FVnvrKJwpt9LP2lAMl8qZswRNm3T4q9CON+bxldk2iwk3FFpuwhx2FfinyitizWHsVYyaY+y5JzDR0rCMV5yTQ==
dependencies:
pvtsutils "^1.3.2"
pvutils "^1.1.3"
tslib "^2.4.0"
braces@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==
dependencies:
fill-range "^7.0.1"
busboy@^1.6.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/busboy/-/busboy-1.6.0.tgz#966ea36a9502e43cdb9146962523b92f531f6893"
integrity sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==
dependencies:
streamsearch "^1.1.0"
dset@^3.1.1:
version "3.1.2"
resolved "https://registry.yarnpkg.com/dset/-/dset-3.1.2.tgz#89c436ca6450398396dc6538ea00abc0c54cd45a"
integrity sha512-g/M9sqy3oHe477Ar4voQxWtaPIFw1jTdKZuomOjhCcBx9nHUNn0pu6NopuFFrTh/TRZIKEj+76vLWFu9BNKk+Q==
event-target-shim@^5.0.0:
version "5.0.1"
resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789"
integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==
fill-range@^7.0.1:
version "7.0.1"
resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40"
integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==
dependencies:
to-regex-range "^5.0.1"
form-data-encoder@^1.7.1:
version "1.7.2"
resolved "https://registry.yarnpkg.com/form-data-encoder/-/form-data-encoder-1.7.2.tgz#1f1ae3dccf58ed4690b86d87e4f57c654fbab040"
integrity sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==
formdata-node@^4.3.1:
version "4.4.1"
resolved "https://registry.yarnpkg.com/formdata-node/-/formdata-node-4.4.1.tgz#23f6a5cb9cb55315912cbec4ff7b0f59bbd191e2"
integrity sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==
dependencies:
node-domexception "1.0.0"
web-streams-polyfill "4.0.0-beta.3"
graphql-yoga@^3.3.0:
version "3.4.0"
resolved "https://registry.yarnpkg.com/graphql-yoga/-/graphql-yoga-3.4.0.tgz#9ab03552edb1f9ec0d877566ee7f0ecb23726dd7"
integrity sha512-Cjx60mmpoK1qL/sLdM285VdAOQyJBKLuC6oMZrfO8QleneNtu0nDOM6Efv5m0IrRYSONEMtIYA7eNr0u/cCBfg==
dependencies:
"@envelop/core" "3.0.4"
"@envelop/parser-cache" "^5.0.4"
"@envelop/validation-cache" "^5.0.5"
"@graphql-tools/executor" "0.0.12"
"@graphql-tools/schema" "^9.0.0"
"@graphql-tools/utils" "^9.0.1"
"@graphql-yoga/subscription" "^3.1.0"
"@whatwg-node/fetch" "0.6.2"
"@whatwg-node/server" "0.5.8"
dset "^3.1.1"
tslib "^2.3.1"
graphql@^16.6.0:
version "16.6.0"
resolved "https://registry.yarnpkg.com/graphql/-/graphql-16.6.0.tgz#c2dcffa4649db149f6282af726c8c83f1c7c5fdb"
integrity sha512-KPIBPDlW7NxrbT/eh4qPXz5FiFdL5UbaA0XUNz2Rp3Z3hqBSkbj0GVjwFDztsWVauZUWsbKHgMg++sk8UX0bkw==
is-number@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b"
integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==
lru-cache@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94"
integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==
dependencies:
yallist "^4.0.0"
node-domexception@1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/node-domexception/-/node-domexception-1.0.0.tgz#6888db46a1f71c0b76b3f7555016b63fe64766e5"
integrity sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==
node-fetch@^2.6.7:
version "2.6.8"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.8.tgz#a68d30b162bc1d8fd71a367e81b997e1f4d4937e"
integrity sha512-RZ6dBYuj8dRSfxpUSu+NsdF1dpPpluJxwOp+6IoDp/sH2QNDSvurYsAa+F1WxY2RjA1iP93xhcsUoYbF2XBqVg==
dependencies:
whatwg-url "^5.0.0"
pvtsutils@^1.3.2:
version "1.3.2"
resolved "https://registry.yarnpkg.com/pvtsutils/-/pvtsutils-1.3.2.tgz#9f8570d132cdd3c27ab7d51a2799239bf8d8d5de"
integrity sha512-+Ipe2iNUyrZz+8K/2IOo+kKikdtfhRKzNpQbruF2URmqPtoqAs8g3xS7TJvFF2GcPXjh7DkqMnpVveRFq4PgEQ==
dependencies:
tslib "^2.4.0"
pvutils@^1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/pvutils/-/pvutils-1.1.3.tgz#f35fc1d27e7cd3dfbd39c0826d173e806a03f5a3"
integrity sha512-pMpnA0qRdFp32b1sJl1wOJNxZLQ2cbQx+k6tjNtZ8CpvVhNqEPRgivZ2WOUev2YMajecdH7ctUPDvEe87nariQ==
streamsearch@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-1.1.0.tgz#404dd1e2247ca94af554e841a8ef0eaa238da764"
integrity sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==
to-regex-range@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4"
integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==
dependencies:
is-number "^7.0.0"
tr46@~0.0.3:
version "0.0.3"
resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a"
integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==
tslib@2.4.0:
version "2.4.0"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.0.tgz#7cecaa7f073ce680a05847aa77be941098f36dc3"
integrity sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==
tslib@^2.0.0, tslib@^2.3.1, tslib@^2.4.0, tslib@^2.4.1:
version "2.5.0"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.5.0.tgz#42bfed86f5787aeb41d031866c8f402429e0fddf"
integrity sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==
undici@^5.12.0:
version "5.16.0"
resolved "https://registry.yarnpkg.com/undici/-/undici-5.16.0.tgz#6b64f9b890de85489ac6332bd45ca67e4f7d9943"
integrity sha512-KWBOXNv6VX+oJQhchXieUznEmnJMqgXMbs0xxH2t8q/FUAWSJvOSr/rMaZKnX5RIVq7JDn0JbP4BOnKG2SGXLQ==
dependencies:
busboy "^1.6.0"
urlpattern-polyfill@^6.0.2:
version "6.0.2"
resolved "https://registry.yarnpkg.com/urlpattern-polyfill/-/urlpattern-polyfill-6.0.2.tgz#a193fe773459865a2a5c93b246bb794b13d07256"
integrity sha512-5vZjFlH9ofROmuWmXM9yj2wljYKgWstGwe8YTyiqM7hVum/g9LyCizPZtb3UqsuppVwety9QJmfc42VggLpTgg==
dependencies:
braces "^3.0.2"
value-or-promise@1.0.12:
version "1.0.12"
resolved "https://registry.yarnpkg.com/value-or-promise/-/value-or-promise-1.0.12.tgz#0e5abfeec70148c78460a849f6b003ea7986f15c"
integrity sha512-Z6Uz+TYwEqE7ZN50gwn+1LCVo9ZVrpxRPOhOLnncYkY1ZzOYtrX8Fwf/rFktZ8R5mJms6EZf5TqNOMeZmnPq9Q==
web-streams-polyfill@4.0.0-beta.3:
version "4.0.0-beta.3"
resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz#2898486b74f5156095e473efe989dcf185047a38"
integrity sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==
web-streams-polyfill@^3.2.0:
version "3.2.1"
resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz#71c2718c52b45fd49dbeee88634b3a60ceab42a6"
integrity sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==
webcrypto-core@^1.7.4:
version "1.7.5"
resolved "https://registry.yarnpkg.com/webcrypto-core/-/webcrypto-core-1.7.5.tgz#c02104c953ca7107557f9c165d194c6316587ca4"
integrity sha512-gaExY2/3EHQlRNNNVSrbG2Cg94Rutl7fAaKILS1w8ZDhGxdFOaw6EbCfHIxPy9vt/xwp5o0VQAx9aySPF6hU1A==
dependencies:
"@peculiar/asn1-schema" "^2.1.6"
"@peculiar/json-schema" "^1.1.12"
asn1js "^3.0.1"
pvtsutils "^1.3.2"
tslib "^2.4.0"
webidl-conversions@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871"
integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==
whatwg-url@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d"
integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==
dependencies:
tr46 "~0.0.3"
webidl-conversions "^3.0.0"
yallist@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==

View File

@@ -1,132 +0,0 @@
metadata_directory: metadata
services:
hasura:
image: hasura/graphql-engine:v2.15.2
environment:
hasura_graphql_enable_remote_schema_permissions: false
auth:
image: nhost/hasura-auth:0.16.2
storage:
image: nhost/hasura-storage:0.3.0
auth:
access_control:
email:
allowed_email_domains: ''
allowed_emails: ''
blocked_email_domains: ''
blocked_emails: ''
allowed_redirect_urls: ''
anonymous_users_enabled: true
client_url: http://localhost:3000
disable_new_users: false
email:
signin_email_verified_required: false
passwordless:
enabled: true
template_fetch_url: ''
gravatar:
default: ''
enabled: true
rating: ''
locale:
allowed: en
default: en
password:
hibp_enabled: false
min_length: 3
provider:
apple:
client_id: ''
enabled: false
key_id: ''
private_key: ''
scope: name,email
team_id: ''
bitbucket:
client_id: ''
client_secret: ''
enabled: false
facebook:
client_id: ''
client_secret: ''
enabled: false
scope: email,photos,displayName
github:
enabled: false
client_id: ''
client_secret: ''
scope: user:email
token_url: ''
user_profile_url: ''
gitlab:
base_url: ''
client_id: ''
client_secret: ''
enabled: false
scope: read_user
google:
client_id: ''
client_secret: ''
enabled: false
scope: email,profile
linkedin:
client_id: ''
client_secret: ''
enabled: false
scope: r_emailaddress,r_liteprofile
spotify:
client_id: ''
client_secret: ''
enabled: false
scope: user-read-email,user-read-private
strava:
client_id: ''
client_secret: ''
enabled: false
twilio:
account_sid: ''
auth_token: ''
enabled: false
messaging_service_id: ''
twitter:
consumer_key: ''
consumer_secret: ''
enabled: false
windows_live:
client_id: ''
client_secret: ''
enabled: false
scope: wl.basic,wl.emails,wl.contacts_emails
sms:
enabled: false
passwordless:
enabled: false
provider:
twilio:
account_sid: ''
auth_token: ''
from: ''
messaging_service_id: ''
smtp:
host: mailhog
method: ''
pass: password
port: 1025
secure: false
sender: hasura-auth@example.com
user: user
token:
access:
expires_in: 900
refresh:
expires_in: 43200
user:
allowed_roles: user,me
default_allowed_roles: user,me
default_role: user
mfa:
enabled: true
issuer: nhost
storage:
force_download_for_content_types: text/html,application/javascript
version: 3

View File

@@ -1,14 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
</head>
<body>
<h2>Confirm Email Change</h2>
<p>Use this link to confirm changing email:</p>
<p>
<a href="${link}"> Change email </a>
</p>
</body>
</html>

View File

@@ -1 +0,0 @@
Change your email address

View File

@@ -1,14 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
</head>
<body>
<h2>Verify Email</h2>
<p>Use this link to verify your email:</p>
<p>
<a href="${link}"> Verify Email </a>
</p>
</body>
</html>

View File

@@ -1 +0,0 @@
Verify your email

Some files were not shown because too many files have changed in this diff Show More