Compare commits
196 Commits
@nhost/das
...
@nhost/das
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
decb0b057c | ||
|
|
fc79b890df | ||
|
|
211eb42af5 | ||
|
|
a7398451e3 | ||
|
|
4b4f0d0150 | ||
|
|
f37e2a23e2 | ||
|
|
1a4a061284 | ||
|
|
78555c7e85 | ||
|
|
01ded8ffff | ||
|
|
3c7cf92edf | ||
|
|
bb4301fd34 | ||
|
|
c8c8948755 | ||
|
|
17e9e5899e | ||
|
|
bd22c48131 | ||
|
|
89a239ff3a | ||
|
|
0131886011 | ||
|
|
340c014fe8 | ||
|
|
bc9c8b9456 | ||
|
|
c22b2621ba | ||
|
|
726746c4d3 | ||
|
|
c431570783 | ||
|
|
445d8ef449 | ||
|
|
0f4ea18e42 | ||
|
|
dae7c5d517 | ||
|
|
f673adea00 | ||
|
|
1c6f1e3b33 | ||
|
|
d1365ea516 | ||
|
|
72dbba7881 | ||
|
|
a3f3991d5a | ||
|
|
c71fe2cf72 | ||
|
|
24c5ed3ea4 | ||
|
|
2d9145f918 | ||
|
|
9a0ab5b887 | ||
|
|
1ddf704c5b | ||
|
|
6f4ee845c6 | ||
|
|
0368663dea | ||
|
|
76ce7d7b6e | ||
|
|
538bfbcb3e | ||
|
|
07b35d1e5f | ||
|
|
2200a0ed07 | ||
|
|
df23d97126 | ||
|
|
104f149369 | ||
|
|
01228583a0 | ||
|
|
93309dd851 | ||
|
|
2cc18dcb51 | ||
|
|
3b48a62790 | ||
|
|
8897dec056 | ||
|
|
324dda8309 | ||
|
|
95f62bed07 | ||
|
|
0e4d8ff118 | ||
|
|
baec5bada7 | ||
|
|
4e56cfc628 | ||
|
|
54bc91923f | ||
|
|
77b12feb95 | ||
|
|
32d4670bbb | ||
|
|
1dc09942d2 | ||
|
|
3343a36358 | ||
|
|
b755e9086c | ||
|
|
48866d0ee1 | ||
|
|
5b3b76bd41 | ||
|
|
284ef7e7f2 | ||
|
|
6d5c202da9 | ||
|
|
962563d6a0 | ||
|
|
8bf58ba26b | ||
|
|
0c175e7a11 | ||
|
|
70f2fbcfc2 | ||
|
|
d2c4ad3260 | ||
|
|
a9ca2c2946 | ||
|
|
d854dd74b1 | ||
|
|
9342937440 | ||
|
|
e89cd4e262 | ||
|
|
a05438352b | ||
|
|
78437959bb | ||
|
|
e1a7433adb | ||
|
|
e23cf74975 | ||
|
|
a3d01c4fad | ||
|
|
4cdcef9ef5 | ||
|
|
df894ef7e2 | ||
|
|
f7dd6a9fc6 | ||
|
|
2949ff0f62 | ||
|
|
1527b0a455 | ||
|
|
375e53a3f0 | ||
|
|
96e3ca5a32 | ||
|
|
0e570df9c5 | ||
|
|
1f4c67283e | ||
|
|
fc1c4861a3 | ||
|
|
74feaf6add | ||
|
|
8cd97206cc | ||
|
|
02197639f2 | ||
|
|
38b594aef9 | ||
|
|
f3a8886cd0 | ||
|
|
8d76cf8d40 | ||
|
|
3e1fb974e4 | ||
|
|
f74871d872 | ||
|
|
3f26056688 | ||
|
|
6a7801be93 | ||
|
|
7bc5bb857c | ||
|
|
c957039d75 | ||
|
|
96c4032424 | ||
|
|
ec70126b56 | ||
|
|
86b9f9040c | ||
|
|
222f03725b | ||
|
|
10b786e5c6 | ||
|
|
aa8ae88d12 | ||
|
|
0f2c86b41a | ||
|
|
a4c76892dd | ||
|
|
00d278b2cc | ||
|
|
cb6b5faeb9 | ||
|
|
7c4c847b91 | ||
|
|
908887d8c5 | ||
|
|
a2d67bc2db | ||
|
|
1a6cd78254 | ||
|
|
6500629c4b | ||
|
|
add3c2c10e | ||
|
|
dd29b06260 | ||
|
|
490cb25a0f | ||
|
|
0df0dd741e | ||
|
|
2172946879 | ||
|
|
40e50f0e75 | ||
|
|
65cf0888b5 | ||
|
|
21833019ca | ||
|
|
b3171ba3e9 | ||
|
|
6f01f19d02 | ||
|
|
ce92b01eac | ||
|
|
e24a177434 | ||
|
|
56a52b6d48 | ||
|
|
92bfa8c723 | ||
|
|
2a52aaa4a6 | ||
|
|
8280a3e9d8 | ||
|
|
523f60bf68 | ||
|
|
19b11d4084 | ||
|
|
805bae1507 | ||
|
|
f6c014c06f | ||
|
|
c5794f4596 | ||
|
|
fc28817380 | ||
|
|
80bbd3a165 | ||
|
|
7a10617a72 | ||
|
|
f0b6dca1a5 | ||
|
|
5db20adc34 | ||
|
|
12dc41a517 | ||
|
|
768fd56891 | ||
|
|
8a508cb1cc | ||
|
|
34f6a8eef4 | ||
|
|
c9d2d31a9b | ||
|
|
68fb23a361 | ||
|
|
476139e528 | ||
|
|
6a850818a0 | ||
|
|
3970dbba0d | ||
|
|
8ee2166f0d | ||
|
|
e13500a185 | ||
|
|
411f574a51 | ||
|
|
7fc91b992e | ||
|
|
b840012be0 | ||
|
|
645c51a9dc | ||
|
|
0ce6f05539 | ||
|
|
8b1188af53 | ||
|
|
12b01f8dee | ||
|
|
60f4faf409 | ||
|
|
528dff3f1b | ||
|
|
d429fb4a3e | ||
|
|
816c916709 | ||
|
|
b7a2b8b537 | ||
|
|
261d8cf434 | ||
|
|
41f49bde76 | ||
|
|
65f685bdb2 | ||
|
|
f52a7f4aac | ||
|
|
e71b9903d9 | ||
|
|
325fd08aef | ||
|
|
3888704464 | ||
|
|
38e8a10a29 | ||
|
|
d8545eae12 | ||
|
|
3d5bfd87d2 | ||
|
|
e66c5626bd | ||
|
|
a227c6561e | ||
|
|
e885c159df | ||
|
|
09fcb74bef | ||
|
|
a089197197 | ||
|
|
34f843875b | ||
|
|
ca278a8c39 | ||
|
|
75603786e0 | ||
|
|
4e4e699b94 | ||
|
|
da31fa9fba | ||
|
|
95e2afaf47 | ||
|
|
958a56dde9 | ||
|
|
74cb15930e | ||
|
|
aa37a98424 | ||
|
|
11cbdda3a5 | ||
|
|
6d1f4adf10 | ||
|
|
ddbc50c15e | ||
|
|
b2cbf570a3 | ||
|
|
22b8e65031 | ||
|
|
63c94d2036 | ||
|
|
010df48c1e | ||
|
|
fdc11db93d | ||
|
|
cb4749f168 | ||
|
|
46a8fcf471 |
18
.github/CODEOWNERS
vendored
18
.github/CODEOWNERS
vendored
@@ -1,14 +1,14 @@
|
||||
# Documentation
|
||||
# https://help.github.com/en/articles/about-code-owners
|
||||
|
||||
/packages @plmercereau @szilarddoro
|
||||
/packages @szilarddoro
|
||||
/packages/docgen @szilarddoro
|
||||
/integrations/stripe-graphql-js @elitan
|
||||
/.github @plmercereau
|
||||
/dashboard/ @szilarddoro @guicurcio
|
||||
/docs/ @guicurcio @elitan
|
||||
/config/ @plmercereau @szilarddoro
|
||||
/examples/ @plmercereau
|
||||
/examples/codegen-react-apollo @elitan @plmercereau
|
||||
/examples/codegen-react-query @elitan @plmercereau
|
||||
/examples/react-apollo-crm @elitan @plmercereau
|
||||
/.github @szilarddoro
|
||||
/dashboard/ @szilarddoro
|
||||
/docs/ @elitan
|
||||
/config/ @szilarddoro
|
||||
/examples/ @szilarddoro
|
||||
/examples/codegen-react-apollo @elitan @szilarddoro
|
||||
/examples/codegen-react-query @elitan @szilarddoro
|
||||
/examples/react-apollo-crm @elitan @szilarddoro
|
||||
|
||||
@@ -40,14 +40,14 @@ runs:
|
||||
- shell: bash
|
||||
name: Build packages
|
||||
if: ${{ inputs.BUILD == 'all' }}
|
||||
run: pnpm build:all
|
||||
run: pnpm run build:all
|
||||
env:
|
||||
TURBO_TOKEN: ${{ inputs.TURBO_TOKEN }}
|
||||
TURBO_TEAM: ${{ inputs.TURBO_TEAM }}
|
||||
- shell: bash
|
||||
name: Build everything in the monorepo
|
||||
if: ${{ inputs.BUILD == 'default' }}
|
||||
run: pnpm build
|
||||
run: pnpm run build
|
||||
env:
|
||||
TURBO_TOKEN: ${{ inputs.TURBO_TOKEN }}
|
||||
TURBO_TEAM: ${{ inputs.TURBO_TEAM }}
|
||||
|
||||
3
.github/workflows/ci.yaml
vendored
3
.github/workflows/ci.yaml
vendored
@@ -8,7 +8,6 @@ on:
|
||||
- '**.md'
|
||||
- 'LICENSE'
|
||||
pull_request:
|
||||
branches: [main]
|
||||
types: [opened, synchronize]
|
||||
paths-ignore:
|
||||
- 'assets/**'
|
||||
@@ -56,7 +55,7 @@ jobs:
|
||||
| xargs -I@ realpath --relative-to=$PWD @ \
|
||||
| xargs -I@ jq "if (.scripts.e2e | length) != 0 then {name: .name, path: \"@\"} else null end" @/package.json \
|
||||
| awk "!/null/" \
|
||||
| jq -c --slurp)
|
||||
| jq -c --slurp 'map(select(length > 0))')
|
||||
echo "matrix=$PACKAGES" >> $GITHUB_OUTPUT
|
||||
outputs:
|
||||
matrix: ${{ steps.set-matrix.outputs.matrix }}
|
||||
|
||||
@@ -45,6 +45,9 @@
|
||||
"@nhost/docgen": [
|
||||
"../packages/docgen/src/index.ts"
|
||||
],
|
||||
"@nhost/graphql-js": [
|
||||
"../packages/graphql-js/src/index.ts"
|
||||
],
|
||||
"@nhost/hasura-auth-js": [
|
||||
"../packages/hasura-auth-js/src/index.ts"
|
||||
],
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import replace from '@rollup/plugin-replace'
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
|
||||
import { defineConfig } from 'vite'
|
||||
import dts from 'vite-plugin-dts'
|
||||
import tsconfigPaths from 'vite-tsconfig-paths'
|
||||
@@ -61,7 +60,6 @@ export default defineConfig({
|
||||
'@apollo/client/utilities': '@apollo/client/utilities',
|
||||
'graphql-ws': 'graphql-ws',
|
||||
xstate: 'xstate',
|
||||
axios: 'axios',
|
||||
'js-cookie': 'Cookies',
|
||||
react: 'React',
|
||||
'react-dom': 'ReactDOM',
|
||||
|
||||
@@ -1,5 +1,71 @@
|
||||
# @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
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 3343a363: chore(dashboard): bump `@testing-library/react` to v14 and `@testing-library/dom` to v9
|
||||
- @nhost/react-apollo@5.0.1
|
||||
- @nhost/nextjs@1.13.6
|
||||
|
||||
## 0.11.12
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nhost/dashboard",
|
||||
"version": "0.11.12",
|
||||
"version": "0.11.20",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"preinstall": "npx only-allow pnpm",
|
||||
@@ -44,7 +44,6 @@
|
||||
"@tanstack/react-table": "^8.5.30",
|
||||
"@tanstack/react-virtual": "^3.0.0-beta.23",
|
||||
"analytics-node": "^6.2.0",
|
||||
"axios": "^0.27.2",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"clsx": "^1.2.1",
|
||||
"cross-fetch": "^3.1.5",
|
||||
@@ -98,9 +97,9 @@
|
||||
"@storybook/manager-webpack5": "^6.5.14",
|
||||
"@storybook/react": "^6.5.14",
|
||||
"@storybook/testing-library": "^0.0.13",
|
||||
"@testing-library/dom": "^8.19.0",
|
||||
"@testing-library/dom": "^9.0.0",
|
||||
"@testing-library/jest-dom": "^5.16.5",
|
||||
"@testing-library/react": "^13.4.0",
|
||||
"@testing-library/react": "^14.0.0",
|
||||
"@testing-library/user-event": "^14.4.3",
|
||||
"@types/lodash.debounce": "^4.0.7",
|
||||
"@types/node": "^16.11.7",
|
||||
|
||||
@@ -52,7 +52,9 @@ function ControlledAutocomplete(
|
||||
|
||||
return (
|
||||
<Autocomplete
|
||||
inputValue={typeof field.value === 'string' ? field.value : undefined}
|
||||
inputValue={
|
||||
typeof field.value !== 'object' ? field.value.toString() : undefined
|
||||
}
|
||||
{...props}
|
||||
{...field}
|
||||
ref={mergeRefs([field.ref, ref])}
|
||||
|
||||
@@ -1,31 +1,8 @@
|
||||
import type { DialogFormProps } from '@/types/common';
|
||||
import type { CommonDialogProps } from '@/ui/v2/Dialog';
|
||||
import type { ReactNode } from 'react';
|
||||
import type { ReactElement, ReactNode } 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> {
|
||||
/**
|
||||
* Title of the dialog.
|
||||
@@ -41,21 +18,36 @@ export interface DialogConfig<TPayload = unknown> {
|
||||
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 {
|
||||
/**
|
||||
* 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>(
|
||||
type: DialogType,
|
||||
config?: DialogConfig<TPayload>,
|
||||
) => void;
|
||||
openDialog: (options: OpenDialogOptions) => 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>(
|
||||
type: DialogType,
|
||||
config?: DialogConfig<TPayload>,
|
||||
) => void;
|
||||
openDrawer: (options: OpenDialogOptions) => void;
|
||||
/**
|
||||
* Call this function to open an alert dialog.
|
||||
*/
|
||||
@@ -87,7 +79,7 @@ export interface DialogContextProps {
|
||||
*/
|
||||
onDirtyStateChange: (
|
||||
isDirty: boolean,
|
||||
location?: 'drawer' | 'dialog',
|
||||
location?: DialogFormProps['location'],
|
||||
) => void;
|
||||
/**
|
||||
* Call this function to open a dirty confirmation dialog.
|
||||
|
||||
@@ -1,30 +1,12 @@
|
||||
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 { BaseDialog } from '@/ui/v2/Dialog';
|
||||
import Drawer from '@/ui/v2/Drawer';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { useRouter } from 'next/router';
|
||||
import type {
|
||||
BaseSyntheticEvent,
|
||||
DetailedHTMLProps,
|
||||
HTMLProps,
|
||||
PropsWithChildren,
|
||||
} from 'react';
|
||||
import type { BaseSyntheticEvent, PropsWithChildren } from 'react';
|
||||
import {
|
||||
cloneElement,
|
||||
isValidElement,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
@@ -33,7 +15,7 @@ import {
|
||||
useState,
|
||||
} from 'react';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
import type { DialogConfig, DialogType } from './DialogContext';
|
||||
import type { DialogConfig, OpenDialogOptions } from './DialogContext';
|
||||
import DialogContext from './DialogContext';
|
||||
import {
|
||||
alertDialogReducer,
|
||||
@@ -41,67 +23,11 @@ import {
|
||||
drawerReducer,
|
||||
} 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>) {
|
||||
const router = useRouter();
|
||||
|
||||
const [
|
||||
{
|
||||
open: dialogOpen,
|
||||
activeDialogType,
|
||||
dialogProps,
|
||||
title: dialogTitle,
|
||||
payload: dialogPayload,
|
||||
},
|
||||
{ open: dialogOpen, title: dialogTitle, activeDialog, dialogProps },
|
||||
dialogDispatch,
|
||||
] = useReducer(dialogReducer, {
|
||||
open: false,
|
||||
@@ -110,10 +36,9 @@ function DialogProvider({ children }: PropsWithChildren<unknown>) {
|
||||
const [
|
||||
{
|
||||
open: drawerOpen,
|
||||
activeDialogType: activeDrawerType,
|
||||
dialogProps: drawerProps,
|
||||
title: drawerTitle,
|
||||
payload: drawerPayload,
|
||||
activeDialog: activeDrawer,
|
||||
dialogProps: drawerProps,
|
||||
},
|
||||
drawerDispatch,
|
||||
] = useReducer(drawerReducer, {
|
||||
@@ -136,12 +61,9 @@ function DialogProvider({ children }: PropsWithChildren<unknown>) {
|
||||
const isDialogDirty = useRef(false);
|
||||
const [showDirtyConfirmation, setShowDirtyConfirmation] = useState(false);
|
||||
|
||||
const openDialog = useCallback(
|
||||
<TConfig,>(type: DialogType, config?: DialogConfig<TConfig>) => {
|
||||
dialogDispatch({ type: 'OPEN_DIALOG', payload: { type, config } });
|
||||
},
|
||||
[],
|
||||
);
|
||||
const openDialog = useCallback((options: OpenDialogOptions) => {
|
||||
dialogDispatch({ type: 'OPEN_DIALOG', payload: options });
|
||||
}, []);
|
||||
|
||||
const closeDialog = useCallback(() => {
|
||||
dialogDispatch({ type: 'HIDE_DIALOG' });
|
||||
@@ -152,12 +74,9 @@ function DialogProvider({ children }: PropsWithChildren<unknown>) {
|
||||
dialogDispatch({ type: 'CLEAR_DIALOG_CONTENT' });
|
||||
}, []);
|
||||
|
||||
const openDrawer = useCallback(
|
||||
<TConfig,>(type: DialogType, config?: DialogConfig<TConfig>) => {
|
||||
drawerDispatch({ type: 'OPEN_DRAWER', payload: { type, config } });
|
||||
},
|
||||
[],
|
||||
);
|
||||
const openDrawer = useCallback((options: OpenDialogOptions) => {
|
||||
drawerDispatch({ type: 'OPEN_DRAWER', payload: options });
|
||||
}, []);
|
||||
|
||||
const closeDrawer = useCallback(() => {
|
||||
drawerDispatch({ type: 'HIDE_DRAWER' });
|
||||
@@ -228,9 +147,6 @@ function DialogProvider({ children }: PropsWithChildren<unknown>) {
|
||||
[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(
|
||||
(dirty: boolean, location: 'drawer' | 'dialog' = 'drawer') => {
|
||||
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(() => {
|
||||
function handleCloseDrawerAndDialog() {
|
||||
if (isDrawerDirty.current || isDialogDirty.current) {
|
||||
@@ -367,56 +264,20 @@ function DialogProvider({ children }: PropsWithChildren<unknown>) {
|
||||
<RetryableErrorBoundary
|
||||
errorMessageProps={{ className: 'pt-0 pb-5 px-6' }}
|
||||
>
|
||||
{activeDialogType === 'EDIT_WORKSPACE_NAME' && (
|
||||
<EditWorkspaceNameForm {...sharedDialogProps} />
|
||||
)}
|
||||
|
||||
{activeDialogType === 'CREATE_FOREIGN_KEY' && (
|
||||
<CreateForeignKeyForm {...sharedDialogProps} />
|
||||
)}
|
||||
|
||||
{activeDialogType === 'EDIT_FOREIGN_KEY' && (
|
||||
<EditForeignKeyForm {...sharedDialogProps} />
|
||||
)}
|
||||
|
||||
{activeDialogType === 'CREATE_ROLE' && (
|
||||
<CreateRoleForm {...sharedDialogProps} />
|
||||
)}
|
||||
|
||||
{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} />
|
||||
)}
|
||||
{isValidElement(activeDialog)
|
||||
? cloneElement(activeDialog, {
|
||||
...activeDialog.props,
|
||||
location: 'dialog',
|
||||
onSubmit: async (values?: any) => {
|
||||
await activeDialog?.props?.onSubmit?.(values);
|
||||
closeDialog();
|
||||
},
|
||||
onCancel: () => {
|
||||
activeDialog?.props?.onCancel?.();
|
||||
closeDialogWithDirtyGuard();
|
||||
},
|
||||
})
|
||||
: null}
|
||||
</RetryableErrorBoundary>
|
||||
</BaseDialog>
|
||||
|
||||
@@ -436,51 +297,20 @@ function DialogProvider({ children }: PropsWithChildren<unknown>) {
|
||||
}}
|
||||
>
|
||||
<RetryableErrorBoundary>
|
||||
{activeDrawerType === 'CREATE_RECORD' && (
|
||||
<CreateRecordForm
|
||||
{...sharedDrawerProps}
|
||||
columns={drawerPayload?.columns}
|
||||
/>
|
||||
)}
|
||||
|
||||
{activeDrawerType === 'CREATE_COLUMN' && (
|
||||
<CreateColumnForm {...sharedDrawerProps} />
|
||||
)}
|
||||
|
||||
{activeDrawerType === 'EDIT_COLUMN' && (
|
||||
<EditColumnForm
|
||||
{...sharedDrawerProps}
|
||||
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} />
|
||||
)}
|
||||
{isValidElement(activeDrawer)
|
||||
? cloneElement(activeDrawer, {
|
||||
...activeDrawer.props,
|
||||
location: 'drawer',
|
||||
onSubmit: async (values?: any) => {
|
||||
await activeDrawer?.props?.onSubmit?.(values);
|
||||
closeDrawer();
|
||||
},
|
||||
onCancel: () => {
|
||||
activeDrawer?.props?.onCancel?.();
|
||||
closeDrawerWithDirtyGuard();
|
||||
},
|
||||
})
|
||||
: null}
|
||||
</RetryableErrorBoundary>
|
||||
</Drawer>
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { CommonDialogProps } from '@/ui/v2/Dialog';
|
||||
import type { ReactNode } from 'react';
|
||||
import type { DialogConfig, DialogType } from './DialogContext';
|
||||
import type { ReactElement, ReactNode } from 'react';
|
||||
import type { DialogConfig, OpenDialogOptions } from './DialogContext';
|
||||
|
||||
export interface DialogState {
|
||||
/**
|
||||
@@ -12,9 +12,13 @@ export interface DialogState {
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
@@ -27,10 +31,7 @@ export interface DialogState {
|
||||
}
|
||||
|
||||
export type DialogAction =
|
||||
| {
|
||||
type: 'OPEN_DIALOG';
|
||||
payload: { type: DialogType; config?: DialogConfig };
|
||||
}
|
||||
| { type: 'OPEN_DIALOG'; payload: OpenDialogOptions }
|
||||
| { type: 'HIDE_DIALOG' }
|
||||
| { type: 'CLEAR_DIALOG_CONTENT' };
|
||||
|
||||
@@ -50,10 +51,9 @@ export function dialogReducer(
|
||||
return {
|
||||
...state,
|
||||
open: true,
|
||||
activeDialogType: action.payload?.type,
|
||||
dialogProps: action.payload.config?.props,
|
||||
title: action.payload.config?.title,
|
||||
payload: action.payload.config?.payload,
|
||||
title: action.payload.title,
|
||||
activeDialog: action.payload.component,
|
||||
dialogProps: action.payload.props,
|
||||
};
|
||||
case 'HIDE_DIALOG':
|
||||
return {
|
||||
@@ -64,8 +64,7 @@ export function dialogReducer(
|
||||
return {
|
||||
...state,
|
||||
title: undefined,
|
||||
payload: undefined,
|
||||
activeDialogType: undefined,
|
||||
activeDialog: undefined,
|
||||
dialogProps: undefined,
|
||||
};
|
||||
default:
|
||||
@@ -74,10 +73,7 @@ export function dialogReducer(
|
||||
}
|
||||
|
||||
export type DrawerAction =
|
||||
| {
|
||||
type: 'OPEN_DRAWER';
|
||||
payload: { type: DialogType; config?: DialogConfig };
|
||||
}
|
||||
| { type: 'OPEN_DRAWER'; payload: OpenDialogOptions }
|
||||
| { type: 'HIDE_DRAWER' }
|
||||
| { type: 'CLEAR_DRAWER_CONTENT' };
|
||||
|
||||
@@ -97,10 +93,9 @@ export function drawerReducer(
|
||||
return {
|
||||
...state,
|
||||
open: true,
|
||||
activeDialogType: action.payload?.type,
|
||||
dialogProps: action.payload.config?.props,
|
||||
title: action.payload.config?.title,
|
||||
payload: action.payload.config?.payload,
|
||||
title: action.payload.title,
|
||||
activeDialog: action.payload.component,
|
||||
dialogProps: action.payload.props,
|
||||
};
|
||||
case 'HIDE_DRAWER':
|
||||
return {
|
||||
@@ -111,8 +106,7 @@ export function drawerReducer(
|
||||
return {
|
||||
...state,
|
||||
title: undefined,
|
||||
payload: undefined,
|
||||
activeDialogType: undefined,
|
||||
activeDialog: undefined,
|
||||
dialogProps: undefined,
|
||||
};
|
||||
default:
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './FormActivityIndicator';
|
||||
export { default } from './FormActivityIndicator';
|
||||
@@ -3,6 +3,7 @@ import ControlledCheckbox from '@/components/common/ControlledCheckbox';
|
||||
import { useDialog } from '@/components/common/DialogProvider';
|
||||
import Form from '@/components/common/Form';
|
||||
import InlineCode from '@/components/common/InlineCode';
|
||||
import type { DialogFormProps } from '@/types/common';
|
||||
import type { ColumnType, DatabaseColumn } from '@/types/dataBrowser';
|
||||
import Box from '@/ui/v2/Box';
|
||||
import Button from '@/ui/v2/Button';
|
||||
@@ -22,7 +23,7 @@ import ForeignKeyEditor from './ForeignKeyEditor';
|
||||
|
||||
export type BaseColumnFormValues = DatabaseColumn;
|
||||
|
||||
export interface BaseColumnFormProps {
|
||||
export interface BaseColumnFormProps extends DialogFormProps {
|
||||
/**
|
||||
* Function to be called when the form is submitted.
|
||||
*/
|
||||
@@ -60,6 +61,7 @@ export default function BaseColumnForm({
|
||||
onSubmit: handleExternalSubmit,
|
||||
onCancel,
|
||||
submitButtonText = 'Save',
|
||||
location,
|
||||
}: BaseColumnFormProps) {
|
||||
const { onDirtyStateChange } = useDialog();
|
||||
|
||||
@@ -91,8 +93,8 @@ export default function BaseColumnForm({
|
||||
const isDirty = Object.keys(dirtyFields).length > 0;
|
||||
|
||||
useEffect(() => {
|
||||
onDirtyStateChange(isDirty, 'drawer');
|
||||
}, [isDirty, onDirtyStateChange]);
|
||||
onDirtyStateChange(isDirty, location);
|
||||
}, [isDirty, location, onDirtyStateChange]);
|
||||
|
||||
return (
|
||||
<Form
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
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 Box from '@/ui/v2/Box';
|
||||
import Button from '@/ui/v2/Button';
|
||||
@@ -29,7 +30,7 @@ const ForeignKeyEditorInput = forwardRef(
|
||||
) => {
|
||||
const { openDialog } = useDialog();
|
||||
const { setValue } = useFormContext();
|
||||
const column = useWatch<Partial<DatabaseColumn>>();
|
||||
const column = useWatch() as DatabaseColumn;
|
||||
const { foreignKeyRelation } = column;
|
||||
|
||||
if (!column.foreignKeyRelation) {
|
||||
@@ -39,8 +40,8 @@ const ForeignKeyEditorInput = forwardRef(
|
||||
className="py-1"
|
||||
disabled={!column.name || !column.type}
|
||||
ref={ref}
|
||||
onClick={() =>
|
||||
openDialog('CREATE_FOREIGN_KEY', {
|
||||
onClick={() => {
|
||||
openDialog({
|
||||
title: (
|
||||
<span className="grid grid-flow-row">
|
||||
<span>Add a Foreign Key Relation</span>
|
||||
@@ -51,16 +52,18 @@ const ForeignKeyEditorInput = forwardRef(
|
||||
</Text>
|
||||
</span>
|
||||
),
|
||||
payload: {
|
||||
selectedColumn: column.name,
|
||||
availableColumns: [column],
|
||||
onSubmit: (values: BaseForeignKeyFormValues) => {
|
||||
setValue('foreignKeyRelation', values);
|
||||
onCreateSubmit();
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
component: (
|
||||
<CreateForeignKeyForm
|
||||
selectedColumn={column.name}
|
||||
availableColumns={[column]}
|
||||
onSubmit={(values) => {
|
||||
setValue('foreignKeyRelation', values);
|
||||
onCreateSubmit();
|
||||
}}
|
||||
/>
|
||||
),
|
||||
});
|
||||
}}
|
||||
>
|
||||
Add Foreign Key
|
||||
</Button>
|
||||
@@ -86,20 +89,22 @@ const ForeignKeyEditorInput = forwardRef(
|
||||
<div className="grid grid-flow-col">
|
||||
<Button
|
||||
ref={ref}
|
||||
onClick={() =>
|
||||
openDialog('EDIT_FOREIGN_KEY', {
|
||||
onClick={() => {
|
||||
openDialog({
|
||||
title: 'Edit Foreign Key Relation',
|
||||
payload: {
|
||||
foreignKeyRelation,
|
||||
availableColumns: [column],
|
||||
selectedColumn: column.name,
|
||||
onSubmit: (values: BaseForeignKeyFormValues) => {
|
||||
setValue('foreignKeyRelation', values);
|
||||
onEditSubmit();
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
component: (
|
||||
<EditForeignKeyForm
|
||||
foreignKeyRelation={foreignKeyRelation}
|
||||
selectedColumn={column.name}
|
||||
availableColumns={[column]}
|
||||
onSubmit={(values) => {
|
||||
setValue('foreignKeyRelation', values);
|
||||
onEditSubmit();
|
||||
}}
|
||||
/>
|
||||
),
|
||||
});
|
||||
}}
|
||||
variant="borderless"
|
||||
className="min-w-[initial] py-1 px-2"
|
||||
>
|
||||
|
||||
@@ -2,6 +2,7 @@ import ControlledSelect from '@/components/common/ControlledSelect';
|
||||
import { useDialog } from '@/components/common/DialogProvider';
|
||||
import Form from '@/components/common/Form';
|
||||
import useDatabaseQuery from '@/hooks/dataBrowser/useDatabaseQuery';
|
||||
import type { DialogFormProps } from '@/types/common';
|
||||
import type { DatabaseColumn, ForeignKeyRelation } from '@/types/dataBrowser';
|
||||
import Box from '@/ui/v2/Box';
|
||||
import Button from '@/ui/v2/Button';
|
||||
@@ -23,7 +24,7 @@ export interface BaseForeignKeyFormValues extends ForeignKeyRelation {
|
||||
disableOriginColumn?: boolean;
|
||||
}
|
||||
|
||||
export interface BaseForeignKeyFormProps {
|
||||
export interface BaseForeignKeyFormProps extends DialogFormProps {
|
||||
/**
|
||||
* Available columns in the table.
|
||||
*/
|
||||
@@ -64,6 +65,7 @@ export function BaseForeignKeyForm({
|
||||
onSubmit: handleExternalSubmit,
|
||||
onCancel,
|
||||
submitButtonText = 'Save',
|
||||
location,
|
||||
}: BaseForeignKeyFormProps) {
|
||||
const { onDirtyStateChange } = useDialog();
|
||||
|
||||
@@ -86,8 +88,8 @@ export function BaseForeignKeyForm({
|
||||
const isDirty = Object.keys(dirtyFields).length > 0;
|
||||
|
||||
useEffect(() => {
|
||||
onDirtyStateChange(isDirty, 'dialog');
|
||||
}, [isDirty, onDirtyStateChange]);
|
||||
onDirtyStateChange(isDirty, location);
|
||||
}, [isDirty, location, onDirtyStateChange]);
|
||||
|
||||
return (
|
||||
<Form
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useDialog } from '@/components/common/DialogProvider';
|
||||
import Form from '@/components/common/Form';
|
||||
import DatabaseRecordInputGroup from '@/components/dataBrowser/DatabaseRecordInputGroup';
|
||||
import type { DialogFormProps } from '@/types/common';
|
||||
import type {
|
||||
ColumnInsertOptions,
|
||||
DataBrowserGridColumn,
|
||||
@@ -10,7 +11,7 @@ import Button from '@/ui/v2/Button';
|
||||
import { useEffect } from 'react';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
|
||||
export interface BaseRecordFormProps {
|
||||
export interface BaseRecordFormProps extends DialogFormProps {
|
||||
/**
|
||||
* The columns of the table.
|
||||
*/
|
||||
@@ -36,6 +37,7 @@ export default function BaseRecordForm({
|
||||
onSubmit: handleExternalSubmit,
|
||||
onCancel,
|
||||
submitButtonText = 'Save',
|
||||
location,
|
||||
}: BaseRecordFormProps) {
|
||||
const { onDirtyStateChange } = useDialog();
|
||||
const { requiredColumns, optionalColumns } = columns.reduce(
|
||||
@@ -70,8 +72,8 @@ export default function BaseRecordForm({
|
||||
const isDirty = Object.keys(dirtyFields).length > 0;
|
||||
|
||||
useEffect(() => {
|
||||
onDirtyStateChange(isDirty, 'drawer');
|
||||
}, [isDirty, onDirtyStateChange]);
|
||||
onDirtyStateChange(isDirty, location);
|
||||
}, [isDirty, location, onDirtyStateChange]);
|
||||
|
||||
// Stores columns in a map to have constant time lookup. This is necessary
|
||||
// for tables with many columns.
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useDialog } from '@/components/common/DialogProvider';
|
||||
import Form from '@/components/common/Form';
|
||||
import { baseColumnValidationSchema } from '@/components/dataBrowser/BaseColumnForm';
|
||||
import type { DialogFormProps } from '@/types/common';
|
||||
import type { DatabaseTable, ForeignKeyRelation } from '@/types/dataBrowser';
|
||||
import Box from '@/ui/v2/Box';
|
||||
import Button from '@/ui/v2/Button';
|
||||
@@ -30,7 +31,7 @@ export interface BaseTableFormValues
|
||||
foreignKeyRelations?: ForeignKeyRelation[];
|
||||
}
|
||||
|
||||
export interface BaseTableFormProps {
|
||||
export interface BaseTableFormProps extends DialogFormProps {
|
||||
/**
|
||||
* Function to be called when the form is submitted.
|
||||
*/
|
||||
@@ -99,7 +100,9 @@ function NameInput() {
|
||||
function FormFooter({
|
||||
onCancel,
|
||||
submitButtonText,
|
||||
}: Pick<BaseTableFormProps, 'onCancel' | 'submitButtonText'>) {
|
||||
location,
|
||||
}: Pick<BaseTableFormProps, 'onCancel' | 'submitButtonText'> &
|
||||
Pick<DialogFormProps, 'location'>) {
|
||||
const { onDirtyStateChange } = useDialog();
|
||||
const { isSubmitting, dirtyFields } = useFormState();
|
||||
|
||||
@@ -108,8 +111,8 @@ function FormFooter({
|
||||
const isDirty = Object.keys(dirtyFields).length > 0;
|
||||
|
||||
useEffect(() => {
|
||||
onDirtyStateChange(isDirty, 'drawer');
|
||||
}, [isDirty, onDirtyStateChange]);
|
||||
onDirtyStateChange(isDirty, location);
|
||||
}, [isDirty, location, onDirtyStateChange]);
|
||||
|
||||
return (
|
||||
<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({
|
||||
location,
|
||||
onSubmit: handleExternalSubmit,
|
||||
onCancel,
|
||||
submitButtonText = 'Save',
|
||||
@@ -168,7 +172,11 @@ export default function BaseTableForm({
|
||||
<ForeignKeyEditorSection />
|
||||
</div>
|
||||
|
||||
<FormFooter onCancel={onCancel} submitButtonText={submitButtonText} />
|
||||
<FormFooter
|
||||
onCancel={onCancel}
|
||||
submitButtonText={submitButtonText}
|
||||
location={location}
|
||||
/>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
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, ForeignKeyRelation } from '@/types/dataBrowser';
|
||||
import Button from '@/ui/v2/Button';
|
||||
import PlusIcon from '@/ui/v2/icons/PlusIcon';
|
||||
@@ -68,18 +70,19 @@ export default function ForeignKeyEditorSection() {
|
||||
onEdit={() => {
|
||||
const primaryKeyIndex = getValues('primaryKeyIndex');
|
||||
|
||||
openDialog('EDIT_FOREIGN_KEY', {
|
||||
openDialog({
|
||||
title: 'Edit Foreign Key Relation',
|
||||
payload: {
|
||||
foreignKeyRelation: fields[index],
|
||||
availableColumns: columns.map((column, columnIndex) =>
|
||||
columnIndex === primaryKeyIndex
|
||||
? { ...column, isPrimary: true }
|
||||
: column,
|
||||
),
|
||||
onSubmit: (values: BaseForeignKeyFormValues) =>
|
||||
handleEdit(values, index),
|
||||
},
|
||||
component: (
|
||||
<EditForeignKeyForm
|
||||
foreignKeyRelation={fields[index] as ForeignKeyRelation}
|
||||
availableColumns={columns.map((column, columnIndex) =>
|
||||
columnIndex === primaryKeyIndex
|
||||
? { ...column, isPrimary: true }
|
||||
: column,
|
||||
)}
|
||||
onSubmit={(values) => handleEdit(values, index)}
|
||||
/>
|
||||
),
|
||||
});
|
||||
}}
|
||||
onDelete={() => remove(index)}
|
||||
@@ -105,7 +108,7 @@ export default function ForeignKeyEditorSection() {
|
||||
onClick={() => {
|
||||
const primaryKeyIndex = getValues('primaryKeyIndex');
|
||||
|
||||
openDialog('CREATE_FOREIGN_KEY', {
|
||||
openDialog({
|
||||
title: (
|
||||
<span className="grid grid-flow-row">
|
||||
<span>Add a Foreign Key Relation</span>
|
||||
@@ -116,14 +119,16 @@ export default function ForeignKeyEditorSection() {
|
||||
</Text>
|
||||
</span>
|
||||
),
|
||||
payload: {
|
||||
availableColumns: columns.map((column, index) =>
|
||||
index === primaryKeyIndex
|
||||
? { ...column, isPrimary: true }
|
||||
: column,
|
||||
),
|
||||
onSubmit: handleCreate,
|
||||
},
|
||||
component: (
|
||||
<CreateForeignKeyForm
|
||||
availableColumns={columns.map((column, index) =>
|
||||
index === primaryKeyIndex
|
||||
? { ...column, isPrimary: true }
|
||||
: column,
|
||||
)}
|
||||
onSubmit={handleCreate}
|
||||
/>
|
||||
),
|
||||
});
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -15,11 +15,11 @@ import { useRouter } from 'next/router';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
|
||||
export interface CreateColumnFormProps
|
||||
extends Pick<BaseColumnFormProps, 'onCancel'> {
|
||||
extends Pick<BaseColumnFormProps, 'onCancel' | 'location'> {
|
||||
/**
|
||||
* Function to be called when the form is submitted.
|
||||
*/
|
||||
onSubmit?: () => Promise<void>;
|
||||
onSubmit?: (args?: any) => Promise<any>;
|
||||
}
|
||||
|
||||
export default function CreateColumnForm({
|
||||
|
||||
@@ -13,7 +13,10 @@ import { useState } from 'react';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
|
||||
export interface CreateForeignKeyFormProps
|
||||
extends Pick<BaseForeignKeyFormProps, 'onCancel' | 'availableColumns'> {
|
||||
extends Pick<
|
||||
BaseForeignKeyFormProps,
|
||||
'onCancel' | 'availableColumns' | 'location'
|
||||
> {
|
||||
/**
|
||||
* Column selected by default.
|
||||
*/
|
||||
@@ -21,7 +24,7 @@ export interface CreateForeignKeyFormProps
|
||||
/**
|
||||
* Function to be called when the form is submitted.
|
||||
*/
|
||||
onSubmit?: (values: BaseForeignKeyFormValues) => Promise<void>;
|
||||
onSubmit?: (values: BaseForeignKeyFormValues) => Promise<void> | void;
|
||||
}
|
||||
|
||||
export default function CreateForeignKeyForm({
|
||||
@@ -51,9 +54,7 @@ export default function CreateForeignKeyForm({
|
||||
setError(undefined);
|
||||
|
||||
try {
|
||||
if (onSubmit) {
|
||||
await onSubmit(values);
|
||||
}
|
||||
await onSubmit?.(values);
|
||||
} catch (submitError) {
|
||||
if (submitError && submitError instanceof Error) {
|
||||
setError(submitError);
|
||||
|
||||
@@ -10,11 +10,11 @@ import { yupResolver } from '@hookform/resolvers/yup';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
|
||||
export interface CreateRecordFormProps
|
||||
extends Pick<BaseRecordFormProps, 'columns' | 'onCancel'> {
|
||||
extends Pick<BaseRecordFormProps, 'columns' | 'onCancel' | 'location'> {
|
||||
/**
|
||||
* Function to be called when the form is submitted.
|
||||
*/
|
||||
onSubmit?: () => Promise<void>;
|
||||
onSubmit?: (args?: any) => Promise<any>;
|
||||
}
|
||||
|
||||
export default function CreateRecordForm({
|
||||
|
||||
@@ -17,7 +17,7 @@ import { useRouter } from 'next/router';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
|
||||
export interface CreateTableFormProps
|
||||
extends Pick<BaseTableFormProps, 'onCancel'> {
|
||||
extends Pick<BaseTableFormProps, 'onCancel' | 'location'> {
|
||||
/**
|
||||
* Schema where the table should be created.
|
||||
*/
|
||||
@@ -25,7 +25,7 @@ export interface CreateTableFormProps
|
||||
/**
|
||||
* Function to be called when the form is submitted.
|
||||
*/
|
||||
onSubmit?: () => Promise<void>;
|
||||
onSubmit?: (args?: any) => Promise<any>;
|
||||
}
|
||||
|
||||
export default function CreateTableForm({
|
||||
|
||||
@@ -5,6 +5,7 @@ import DataGridDateCell from '@/components/common/DataGridDateCell';
|
||||
import DataGridNumericCell from '@/components/common/DataGridNumericCell';
|
||||
import DataGridTextCell from '@/components/common/DataGridTextCell';
|
||||
import { useDialog } from '@/components/common/DialogProvider';
|
||||
import FormActivityIndicator from '@/components/common/FormActivityIndicator';
|
||||
import InlineCode from '@/components/common/InlineCode';
|
||||
import DataBrowserEmptyState from '@/components/dataBrowser/DataBrowserEmptyState';
|
||||
import DataBrowserGridControls from '@/components/dataBrowser/DataBrowserGridControls';
|
||||
@@ -28,9 +29,25 @@ import {
|
||||
} from '@/utils/dataBrowser/postgresqlConstants';
|
||||
import { isSchemaLocked } from '@/utils/dataBrowser/schemaHelpers';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { useRouter } from 'next/router';
|
||||
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 function createDataGridColumn(
|
||||
@@ -273,33 +290,36 @@ export default function DataBrowserGrid({
|
||||
const memoizedData = useMemo(() => rows, [rows]);
|
||||
|
||||
async function handleInsertRowClick() {
|
||||
openDrawer('CREATE_RECORD', {
|
||||
openDrawer({
|
||||
title: 'Insert a New Row',
|
||||
payload: {
|
||||
columns: memoizedColumns,
|
||||
onSubmit: refetch,
|
||||
},
|
||||
component: (
|
||||
<CreateRecordForm
|
||||
// TODO: Create proper typings for data browser columns
|
||||
columns={memoizedColumns as unknown as DataBrowserGridColumn[]}
|
||||
onSubmit={refetch}
|
||||
/>
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
async function handleInsertColumnClick() {
|
||||
openDrawer('CREATE_COLUMN', {
|
||||
openDrawer({
|
||||
title: 'Insert a New Column',
|
||||
payload: {
|
||||
onSubmit: refetch,
|
||||
},
|
||||
component: <CreateColumnForm onSubmit={refetch} />,
|
||||
});
|
||||
}
|
||||
|
||||
async function handleEditColumnClick(
|
||||
column: DataBrowserGridColumn<NormalizedQueryDataRow>,
|
||||
) {
|
||||
openDrawer('EDIT_COLUMN', {
|
||||
openDrawer({
|
||||
title: 'Edit Column',
|
||||
payload: {
|
||||
column,
|
||||
onSubmit: () => queryClient.refetchQueries([currentTablePath]),
|
||||
},
|
||||
component: (
|
||||
<EditColumnForm
|
||||
column={column}
|
||||
onSubmit={() => queryClient.refetchQueries([currentTablePath])}
|
||||
/>
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useDialog } from '@/components/common/DialogProvider';
|
||||
import FormActivityIndicator from '@/components/common/FormActivityIndicator';
|
||||
import InlineCode from '@/components/common/InlineCode';
|
||||
import NavLink from '@/components/common/NavLink';
|
||||
import RetryableErrorBoundary from '@/components/common/RetryableErrorBoundary';
|
||||
@@ -31,11 +32,36 @@ import Select from '@/ui/v2/Select';
|
||||
import Text from '@/ui/v2/Text';
|
||||
import { isSchemaLocked } from '@/utils/dataBrowser/schemaHelpers';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import dynamic from 'next/dynamic';
|
||||
import Image from 'next/image';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useEffect, useState } from 'react';
|
||||
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'> {
|
||||
/**
|
||||
* Function to be called when a sidebar item is clicked.
|
||||
@@ -200,7 +226,7 @@ function DataBrowserSidebarContent({
|
||||
table: string,
|
||||
disabled?: boolean,
|
||||
) {
|
||||
openDrawer('EDIT_PERMISSIONS', {
|
||||
openDrawer({
|
||||
title: (
|
||||
<span className="inline-grid grid-flow-col items-center gap-2">
|
||||
Permissions
|
||||
@@ -208,22 +234,18 @@ function DataBrowserSidebarContent({
|
||||
<Chip label="Preview" size="small" color="info" component="span" />
|
||||
</span>
|
||||
),
|
||||
component: (
|
||||
<EditPermissionsForm
|
||||
disabled={disabled}
|
||||
schema={schema}
|
||||
table={table}
|
||||
/>
|
||||
),
|
||||
props: {
|
||||
PaperProps: {
|
||||
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 />}
|
||||
className="mt-1 w-full justify-between px-2"
|
||||
onClick={() => {
|
||||
openDrawer('CREATE_TABLE', {
|
||||
openDrawer({
|
||||
title: 'Create a New Table',
|
||||
payload: { onSubmit: refetch, schema: selectedSchema },
|
||||
component: (
|
||||
<CreateTableForm onSubmit={refetch} schema={selectedSchema} />
|
||||
),
|
||||
});
|
||||
|
||||
onSidebarItemClick();
|
||||
@@ -328,69 +352,68 @@ function DataBrowserSidebarContent({
|
||||
className="group"
|
||||
key={tablePath}
|
||||
secondaryAction={
|
||||
!isSelectedSchemaLocked && (
|
||||
<Dropdown.Root
|
||||
id="table-management-menu"
|
||||
onOpen={() => setSidebarMenuTable(tablePath)}
|
||||
onClose={() => setSidebarMenuTable(undefined)}
|
||||
<Dropdown.Root
|
||||
id="table-management-menu"
|
||||
onOpen={() => setSidebarMenuTable(tablePath)}
|
||||
onClose={() => setSidebarMenuTable(undefined)}
|
||||
>
|
||||
<Dropdown.Trigger
|
||||
asChild
|
||||
hideChevron
|
||||
disabled={tablePath === removableTable}
|
||||
>
|
||||
<Dropdown.Trigger
|
||||
asChild
|
||||
hideChevron
|
||||
disabled={tablePath === removableTable}
|
||||
<IconButton
|
||||
variant="borderless"
|
||||
color={isSelected ? 'primary' : 'secondary'}
|
||||
className={twMerge(
|
||||
!isSelected &&
|
||||
'opacity-0 group-focus-within:opacity-100 group-hover:opacity-100 group-active:opacity-100',
|
||||
)}
|
||||
>
|
||||
<IconButton
|
||||
variant="borderless"
|
||||
color={isSelected ? 'primary' : 'secondary'}
|
||||
className={twMerge(
|
||||
!isSelected &&
|
||||
'opacity-0 group-focus-within:opacity-100 group-hover:opacity-100 group-active:opacity-100',
|
||||
)}
|
||||
<DotsHorizontalIcon />
|
||||
</IconButton>
|
||||
</Dropdown.Trigger>
|
||||
|
||||
<Dropdown.Content menu PaperProps={{ className: 'w-52' }}>
|
||||
{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 />
|
||||
</IconButton>
|
||||
</Dropdown.Trigger>
|
||||
<UsersIcon
|
||||
className="h-4 w-4"
|
||||
sx={{ color: 'text.secondary' }}
|
||||
/>
|
||||
|
||||
<Dropdown.Content
|
||||
menu
|
||||
PaperProps={{ className: 'w-52' }}
|
||||
>
|
||||
{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,
|
||||
)
|
||||
}
|
||||
>
|
||||
<UsersIcon
|
||||
className="h-4 w-4"
|
||||
sx={{ color: 'text.secondary' }}
|
||||
/>
|
||||
|
||||
<span>View Permissions</span>
|
||||
</Dropdown.Item>
|
||||
) : (
|
||||
[
|
||||
<span>View Permissions</span>
|
||||
</Dropdown.Item>
|
||||
) : (
|
||||
[
|
||||
!isSelectedSchemaLocked && (
|
||||
<Dropdown.Item
|
||||
key="edit-table"
|
||||
className="grid grid-flow-col items-center gap-2 p-2 text-sm+ font-medium"
|
||||
onClick={() =>
|
||||
openDrawer('EDIT_TABLE', {
|
||||
openDrawer({
|
||||
title: 'Edit Table',
|
||||
payload: {
|
||||
onSubmit: async () => {
|
||||
await queryClient.refetchQueries([
|
||||
`${dataSourceSlug}.${table.table_schema}.${table.table_name}`,
|
||||
]);
|
||||
await refetch();
|
||||
},
|
||||
schema: table.table_schema,
|
||||
table,
|
||||
},
|
||||
component: (
|
||||
<EditTableForm
|
||||
onSubmit={async () => {
|
||||
await queryClient.refetchQueries([
|
||||
`${dataSourceSlug}.${table.table_schema}.${table.table_name}`,
|
||||
]);
|
||||
await refetch();
|
||||
}}
|
||||
schema={table.table_schema}
|
||||
table={table}
|
||||
/>
|
||||
),
|
||||
})
|
||||
}
|
||||
>
|
||||
@@ -400,32 +423,38 @@ function DataBrowserSidebarContent({
|
||||
/>
|
||||
|
||||
<span>Edit Table</span>
|
||||
</Dropdown.Item>,
|
||||
</Dropdown.Item>
|
||||
),
|
||||
!isSelectedSchemaLocked && (
|
||||
<Divider
|
||||
key="edit-table-separator"
|
||||
component="li"
|
||||
/>,
|
||||
<Dropdown.Item
|
||||
key="edit-permissions"
|
||||
className="grid grid-flow-col items-center gap-2 p-2 text-sm+ font-medium"
|
||||
onClick={() =>
|
||||
handleEditPermissionClick(
|
||||
table.table_schema,
|
||||
table.table_name,
|
||||
)
|
||||
}
|
||||
>
|
||||
<UsersIcon
|
||||
className="h-4 w-4"
|
||||
sx={{ color: 'text.secondary' }}
|
||||
/>
|
||||
/>
|
||||
),
|
||||
<Dropdown.Item
|
||||
key="edit-permissions"
|
||||
className="grid grid-flow-col items-center gap-2 p-2 text-sm+ font-medium"
|
||||
onClick={() =>
|
||||
handleEditPermissionClick(
|
||||
table.table_schema,
|
||||
table.table_name,
|
||||
)
|
||||
}
|
||||
>
|
||||
<UsersIcon
|
||||
className="h-4 w-4"
|
||||
sx={{ color: 'text.secondary' }}
|
||||
/>
|
||||
|
||||
<span>Edit Permissions</span>
|
||||
</Dropdown.Item>,
|
||||
<span>Edit Permissions</span>
|
||||
</Dropdown.Item>,
|
||||
!isSelectedSchemaLocked && (
|
||||
<Divider
|
||||
key="edit-permissions-separator"
|
||||
component="li"
|
||||
/>,
|
||||
/>
|
||||
),
|
||||
!isSelectedSchemaLocked && (
|
||||
<Dropdown.Item
|
||||
key="delete-table"
|
||||
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>
|
||||
</Dropdown.Item>,
|
||||
]
|
||||
)}
|
||||
</Dropdown.Content>
|
||||
</Dropdown.Root>
|
||||
)
|
||||
</Dropdown.Item>
|
||||
),
|
||||
]
|
||||
)}
|
||||
</Dropdown.Content>
|
||||
</Dropdown.Root>
|
||||
}
|
||||
>
|
||||
<ListItem.Button
|
||||
|
||||
@@ -18,7 +18,7 @@ import { useRouter } from 'next/router';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
|
||||
export interface EditColumnFormProps
|
||||
extends Pick<BaseColumnFormProps, 'onCancel'> {
|
||||
extends Pick<BaseColumnFormProps, 'onCancel' | 'location'> {
|
||||
/**
|
||||
* Column to be edited.
|
||||
*/
|
||||
|
||||
@@ -14,7 +14,10 @@ import { useState } from 'react';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
|
||||
export interface EditForeignKeyFormProps
|
||||
extends Pick<BaseForeignKeyFormProps, 'onCancel' | 'availableColumns'> {
|
||||
extends Pick<
|
||||
BaseForeignKeyFormProps,
|
||||
'onCancel' | 'availableColumns' | 'location'
|
||||
> {
|
||||
/**
|
||||
* Foreign key relation to be edited.
|
||||
*/
|
||||
@@ -26,7 +29,7 @@ export interface EditForeignKeyFormProps
|
||||
/**
|
||||
* Function to be called when the form is submitted.
|
||||
*/
|
||||
onSubmit?: (values: BaseForeignKeyFormValues) => Promise<void>;
|
||||
onSubmit?: (values: BaseForeignKeyFormValues) => Promise<void> | void;
|
||||
}
|
||||
|
||||
export default function EditForeignKeyForm({
|
||||
@@ -57,9 +60,7 @@ export default function EditForeignKeyForm({
|
||||
setError(undefined);
|
||||
|
||||
try {
|
||||
if (onSubmit) {
|
||||
await onSubmit(values);
|
||||
}
|
||||
await onSubmit?.(values);
|
||||
} catch (submitError) {
|
||||
if (submitError && submitError instanceof Error) {
|
||||
setError(submitError);
|
||||
|
||||
@@ -3,6 +3,7 @@ import useMetadataQuery from '@/hooks/dataBrowser/useMetadataQuery';
|
||||
import useTableQuery from '@/hooks/dataBrowser/useTableQuery';
|
||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||
import { useRemoteApplicationGQLClient } from '@/hooks/useRemoteApplicationGQLClient';
|
||||
import type { DialogFormProps } from '@/types/common';
|
||||
import type {
|
||||
DatabaseAccessLevel,
|
||||
DatabaseAction,
|
||||
@@ -30,7 +31,7 @@ import { twMerge } from 'tailwind-merge';
|
||||
import RolePermissionEditorForm from './RolePermissionEditorForm';
|
||||
import RolePermissionsRow from './RolePermissionsRow';
|
||||
|
||||
export interface EditPermissionsFormProps {
|
||||
export interface EditPermissionsFormProps extends DialogFormProps {
|
||||
/**
|
||||
* Determines whether the form is disabled or not.
|
||||
*/
|
||||
@@ -54,6 +55,7 @@ export default function EditPermissionsForm({
|
||||
schema,
|
||||
table,
|
||||
onCancel,
|
||||
location,
|
||||
}: EditPermissionsFormProps) {
|
||||
const [role, setRole] = useState<string>();
|
||||
const [action, setAction] = useState<DatabaseAction>();
|
||||
@@ -181,6 +183,7 @@ export default function EditPermissionsForm({
|
||||
|
||||
return (
|
||||
<RolePermissionEditorForm
|
||||
location={location}
|
||||
resourceVersion={metadata?.resourceVersion}
|
||||
disabled={disabled}
|
||||
schema={schema}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useDialog } from '@/components/common/DialogProvider';
|
||||
import Form from '@/components/common/Form';
|
||||
import HighlightedText from '@/components/common/HighlightedText';
|
||||
import useManagePermissionMutation from '@/hooks/dataBrowser/useManagePermissionMutation';
|
||||
import type { DialogFormProps } from '@/types/common';
|
||||
import type {
|
||||
DatabaseAction,
|
||||
HasuraMetadataPermission,
|
||||
@@ -72,7 +73,7 @@ export interface RolePermissionEditorFormValues {
|
||||
computedFields?: string[];
|
||||
}
|
||||
|
||||
export interface RolePermissionEditorFormProps {
|
||||
export interface RolePermissionEditorFormProps extends DialogFormProps {
|
||||
/**
|
||||
* Determines whether or not the form is disabled.
|
||||
*/
|
||||
@@ -169,6 +170,7 @@ export default function RolePermissionEditorForm({
|
||||
onCancel,
|
||||
permission,
|
||||
disabled,
|
||||
location,
|
||||
}: RolePermissionEditorFormProps) {
|
||||
const queryClient = useQueryClient();
|
||||
const {
|
||||
@@ -214,8 +216,8 @@ export default function RolePermissionEditorForm({
|
||||
const isDirty = Object.keys(dirtyFields).length > 0;
|
||||
|
||||
useEffect(() => {
|
||||
onDirtyStateChange(isDirty, 'drawer');
|
||||
}, [isDirty, onDirtyStateChange]);
|
||||
onDirtyStateChange(isDirty, location);
|
||||
}, [isDirty, location, onDirtyStateChange]);
|
||||
|
||||
async function handleSubmit(values: RolePermissionEditorFormValues) {
|
||||
const managePermissionPromise = managePermission({
|
||||
@@ -245,7 +247,7 @@ export default function RolePermissionEditorForm({
|
||||
: permission?.check,
|
||||
backend_only: values.backendOnly,
|
||||
computed_fields:
|
||||
permission?.computed_fields.length > 0
|
||||
permission?.computed_fields?.length > 0
|
||||
? permission?.computed_fields
|
||||
: null,
|
||||
},
|
||||
@@ -261,7 +263,7 @@ export default function RolePermissionEditorForm({
|
||||
getToastStyleProps(),
|
||||
);
|
||||
|
||||
onDirtyStateChange(false, 'drawer');
|
||||
onDirtyStateChange(false, location);
|
||||
onSubmit?.();
|
||||
}
|
||||
|
||||
@@ -270,7 +272,7 @@ export default function RolePermissionEditorForm({
|
||||
openDirtyConfirmation({
|
||||
props: {
|
||||
onPrimaryAction: () => {
|
||||
onDirtyStateChange(false, 'drawer');
|
||||
onDirtyStateChange(false, location);
|
||||
onCancel?.();
|
||||
},
|
||||
},
|
||||
@@ -300,7 +302,7 @@ export default function RolePermissionEditorForm({
|
||||
getToastStyleProps(),
|
||||
);
|
||||
|
||||
onDirtyStateChange(false, 'drawer');
|
||||
onDirtyStateChange(false, location);
|
||||
onSubmit?.();
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import Input from '@/ui/v2/Input';
|
||||
import Radio from '@/ui/v2/Radio';
|
||||
import RadioGroup from '@/ui/v2/RadioGroup';
|
||||
import Text from '@/ui/v2/Text';
|
||||
import type { FocusEvent } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
import PermissionSettingsSection from './PermissionSettingsSection';
|
||||
@@ -130,7 +131,13 @@ export default function RowPermissionsSection({
|
||||
|
||||
{action === 'select' && (
|
||||
<Input
|
||||
{...register('limit')}
|
||||
{...register('limit', {
|
||||
onBlur: (event: FocusEvent<HTMLInputElement>) => {
|
||||
if (!event.target.value) {
|
||||
setValue('limit', null);
|
||||
}
|
||||
},
|
||||
})}
|
||||
disabled={disabled}
|
||||
id="limit"
|
||||
type="number"
|
||||
|
||||
@@ -43,7 +43,10 @@ const baseValidationSchema = Yup.object().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),
|
||||
queryRootFields: Yup.array().of(Yup.string()).nullable(true),
|
||||
subscriptionRootFields: Yup.array().of(Yup.string()).nullable(true),
|
||||
|
||||
@@ -23,7 +23,7 @@ import { useEffect, useState } from 'react';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
|
||||
export interface EditTableFormProps
|
||||
extends Pick<BaseTableFormProps, 'onCancel'> {
|
||||
extends Pick<BaseTableFormProps, 'onCancel' | 'location'> {
|
||||
/**
|
||||
* Schema where the table is located.
|
||||
*/
|
||||
|
||||
@@ -6,6 +6,7 @@ import ColumnAutocomplete from '@/components/dataBrowser/ColumnAutocomplete';
|
||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||
import type { HasuraOperator } from '@/types/dataBrowser';
|
||||
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
||||
import type { AutocompleteOption } from '@/ui/v2/Autocomplete';
|
||||
import type { InputProps } from '@/ui/v2/Input';
|
||||
import { inputClasses } from '@/ui/v2/Input';
|
||||
import Option from '@/ui/v2/Option';
|
||||
@@ -211,12 +212,13 @@ export default function RuleValueInput({
|
||||
<ControlledAutocomplete
|
||||
disabled={disabled}
|
||||
freeSolo={!isHasuraInput}
|
||||
autoSelect={!isHasuraInput}
|
||||
autoHighlight={isHasuraInput}
|
||||
open
|
||||
isOptionEqualToValue={(option, value) => {
|
||||
if (typeof value === 'string') {
|
||||
return option.value.toLowerCase() === (value as string).toLowerCase();
|
||||
isOptionEqualToValue={(
|
||||
option,
|
||||
value: string | number | AutocompleteOption<string>,
|
||||
) => {
|
||||
if (typeof value !== 'object') {
|
||||
return option.value.toLowerCase() === value?.toString().toLowerCase();
|
||||
}
|
||||
|
||||
return option.value.toLowerCase() === value.value.toLowerCase();
|
||||
|
||||
@@ -277,7 +277,7 @@ export default function FilesDataGrid(props: FilesDataGridProps) {
|
||||
}
|
||||
|
||||
if (fileError) {
|
||||
throw fileError;
|
||||
throw new Error(fileError.message);
|
||||
}
|
||||
|
||||
triggerToast(`File has been uploaded successfully (${fileMetadata?.id})`);
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { useDialog } from '@/components/common/DialogProvider';
|
||||
import Form from '@/components/common/Form';
|
||||
import type { DialogFormProps } from '@/types/common';
|
||||
import Button from '@/ui/v2/Button';
|
||||
import Input from '@/ui/v2/Input';
|
||||
import { slugifyString } from '@/utils/helpers';
|
||||
@@ -11,11 +13,12 @@ import {
|
||||
import { yupResolver } from '@hookform/resolvers/yup';
|
||||
import { useUserData } from '@nhost/nextjs';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useEffect } from 'react';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import * as Yup from 'yup';
|
||||
|
||||
export interface EditWorkspaceNameFormProps {
|
||||
export interface EditWorkspaceNameFormProps extends DialogFormProps {
|
||||
/**
|
||||
* The current workspace name if this is an edit operation.
|
||||
*/
|
||||
@@ -44,14 +47,7 @@ export interface EditWorkspaceNameFormProps {
|
||||
onCancel?: VoidFunction;
|
||||
}
|
||||
|
||||
export interface EditWorkspaceNameFormValues {
|
||||
/**
|
||||
* New workspace name.
|
||||
*/
|
||||
newWorkspaceName: string;
|
||||
}
|
||||
|
||||
const validationSchema = Yup.object().shape({
|
||||
const validationSchema = Yup.object({
|
||||
newWorkspaceName: Yup.string()
|
||||
.required('Workspace name is required.')
|
||||
.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,
|
||||
onSubmit,
|
||||
onCancel,
|
||||
currentWorkspaceName,
|
||||
currentWorkspaceId,
|
||||
submitButtonText = 'Create',
|
||||
location,
|
||||
}: EditWorkspaceNameFormProps) {
|
||||
const { onDirtyStateChange } = useDialog();
|
||||
const currentUser = useUserData();
|
||||
const [insertWorkspace, { client }] = useInsertWorkspaceMutation();
|
||||
const [updateWorkspaceName] = useUpdateWorkspaceMutation({
|
||||
@@ -105,6 +107,10 @@ export default function EditWorkspaceName({
|
||||
} = form;
|
||||
const isDirty = Object.keys(dirtyFields).length > 0;
|
||||
|
||||
useEffect(() => {
|
||||
onDirtyStateChange(isDirty, location);
|
||||
}, [isDirty, location, onDirtyStateChange]);
|
||||
|
||||
async function handleSubmit({
|
||||
newWorkspaceName,
|
||||
}: EditWorkspaceNameFormValues) {
|
||||
@@ -112,6 +118,8 @@ export default function EditWorkspaceName({
|
||||
|
||||
try {
|
||||
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`.
|
||||
// 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.
|
||||
@@ -186,6 +194,9 @@ export default function EditWorkspaceName({
|
||||
include: ['getOneUser'],
|
||||
});
|
||||
|
||||
// The form has been submitted, it's not dirty anymore
|
||||
onDirtyStateChange(false, location);
|
||||
|
||||
await router.push(slug);
|
||||
onSubmit?.();
|
||||
}
|
||||
@@ -194,9 +205,9 @@ export default function EditWorkspaceName({
|
||||
<FormProvider {...form}>
|
||||
<Form
|
||||
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
|
||||
{...register('newWorkspaceName')}
|
||||
error={Boolean(errors.newWorkspaceName?.message)}
|
||||
|
||||
@@ -58,16 +58,19 @@ export function InviteAnnounce() {
|
||||
error: null,
|
||||
loading: true,
|
||||
});
|
||||
const res = await nhost.functions.call('/accept-workspace-invite', {
|
||||
workspaceMemberInviteId: invite.id,
|
||||
isAccepted: true,
|
||||
});
|
||||
const { res, error: acceptError } = await nhost.functions.call(
|
||||
'/accept-workspace-invite',
|
||||
{
|
||||
workspaceMemberInviteId: invite.id,
|
||||
isAccepted: true,
|
||||
},
|
||||
);
|
||||
|
||||
if (res?.res?.status !== 200) {
|
||||
if (res?.status !== 200) {
|
||||
triggerToast('An error occurred when trying to accept the invitation.');
|
||||
|
||||
return setSubmitState({
|
||||
error: res.error,
|
||||
error: new Error(acceptError.message),
|
||||
loading: false,
|
||||
});
|
||||
}
|
||||
@@ -90,7 +93,7 @@ export function InviteAnnounce() {
|
||||
error: null,
|
||||
});
|
||||
|
||||
const res = await nhost.functions.call(
|
||||
const { error: ignoreError } = await nhost.functions.call(
|
||||
'/accept-workspace-invite',
|
||||
{
|
||||
workspaceMemberInviteId: inviteId,
|
||||
@@ -99,12 +102,12 @@ export function InviteAnnounce() {
|
||||
{ useAxios: false },
|
||||
);
|
||||
|
||||
if (res?.error) {
|
||||
if (ignoreError) {
|
||||
triggerToast('An error occurred when trying to ignore the invitation.');
|
||||
|
||||
setIgnoreState({
|
||||
loading: false,
|
||||
error: new Error(res.error.message),
|
||||
error: new Error(ignoreError.message),
|
||||
});
|
||||
|
||||
return;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useDialog } from '@/components/common/DialogProvider';
|
||||
import Form from '@/components/common/Form';
|
||||
import type { DialogFormProps } from '@/types/common';
|
||||
import Button from '@/ui/v2/Button';
|
||||
import Input from '@/ui/v2/Input';
|
||||
import Text from '@/ui/v2/Text';
|
||||
@@ -26,7 +27,7 @@ export interface BaseEnvironmentVariableFormValues {
|
||||
prodValue: string;
|
||||
}
|
||||
|
||||
export interface BaseEnvironmentVariableFormProps {
|
||||
export interface BaseEnvironmentVariableFormProps extends DialogFormProps {
|
||||
/**
|
||||
* Determines the mode of the form.
|
||||
*
|
||||
@@ -89,6 +90,7 @@ export default function BaseEnvironmentVariableForm({
|
||||
onSubmit,
|
||||
onCancel,
|
||||
submitButtonText = 'Save',
|
||||
location,
|
||||
}: BaseEnvironmentVariableFormProps) {
|
||||
const { onDirtyStateChange } = useDialog();
|
||||
const form = useFormContext<BaseEnvironmentVariableFormValues>();
|
||||
@@ -103,8 +105,8 @@ export default function BaseEnvironmentVariableForm({
|
||||
const isDirty = Object.keys(dirtyFields).length > 0;
|
||||
|
||||
useEffect(() => {
|
||||
onDirtyStateChange(isDirty, 'dialog');
|
||||
}, [isDirty, onDirtyStateChange]);
|
||||
onDirtyStateChange(isDirty, location);
|
||||
}, [isDirty, location, onDirtyStateChange]);
|
||||
|
||||
return (
|
||||
<div className="grid grid-flow-row gap-6 px-6 pb-6">
|
||||
|
||||
@@ -17,7 +17,7 @@ import { FormProvider, useForm } from 'react-hook-form';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
export interface CreateEnvironmentVariableFormProps
|
||||
extends Pick<BaseEnvironmentVariableFormProps, 'onCancel'> {
|
||||
extends Pick<BaseEnvironmentVariableFormProps, 'onCancel' | 'location'> {
|
||||
/**
|
||||
* Function to be called when the form is submitted.
|
||||
*/
|
||||
|
||||
@@ -18,7 +18,7 @@ import { FormProvider, useForm } from 'react-hook-form';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
export interface EditEnvironmentVariableFormProps
|
||||
extends Pick<BaseEnvironmentVariableFormProps, 'onCancel'> {
|
||||
extends Pick<BaseEnvironmentVariableFormProps, 'onCancel' | 'location'> {
|
||||
/**
|
||||
* The environment variable to edit.
|
||||
*/
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useDialog } from '@/components/common/DialogProvider';
|
||||
import Form from '@/components/common/Form';
|
||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||
import type { DialogFormProps } from '@/types/common';
|
||||
import Button from '@/ui/v2/Button';
|
||||
import Input from '@/ui/v2/Input';
|
||||
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
||||
@@ -14,7 +15,7 @@ import { FormProvider, useForm } from 'react-hook-form';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import * as Yup from 'yup';
|
||||
|
||||
export interface EditJwtSecretFormProps {
|
||||
export interface EditJwtSecretFormProps extends DialogFormProps {
|
||||
/**
|
||||
* Initial JWT secret.
|
||||
*/
|
||||
@@ -39,14 +40,7 @@ export interface EditJwtSecretFormProps {
|
||||
onCancel?: VoidFunction;
|
||||
}
|
||||
|
||||
export interface EditJwtSecretFormValues {
|
||||
/**
|
||||
* JWT secret.
|
||||
*/
|
||||
jwtSecret: string;
|
||||
}
|
||||
|
||||
const validationSchema = Yup.object().shape({
|
||||
const validationSchema = Yup.object({
|
||||
jwtSecret: Yup.string()
|
||||
.nullable()
|
||||
.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({
|
||||
disabled,
|
||||
jwtSecret,
|
||||
onSubmit,
|
||||
onCancel,
|
||||
submitButtonText = 'Save',
|
||||
location,
|
||||
}: EditJwtSecretFormProps) {
|
||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||
const [updateApplication] = useUpdateApplicationMutation({
|
||||
@@ -89,8 +86,8 @@ export default function EditJwtSecretForm({
|
||||
const isDirty = Object.keys(dirtyFields).length > 0;
|
||||
|
||||
useEffect(() => {
|
||||
onDirtyStateChange(isDirty, 'dialog');
|
||||
}, [isDirty, onDirtyStateChange]);
|
||||
onDirtyStateChange(isDirty, location);
|
||||
}, [isDirty, location, onDirtyStateChange]);
|
||||
|
||||
async function handleSubmit(values: EditJwtSecretFormValues) {
|
||||
const updateAppPromise = updateApplication({
|
||||
@@ -121,7 +118,7 @@ export default function EditJwtSecretForm({
|
||||
onSubmit={handleSubmit}
|
||||
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
|
||||
{...register('jwtSecret')}
|
||||
error={Boolean(errors.jwtSecret?.message)}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
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 { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||
import type { EnvironmentVariable } from '@/types/application';
|
||||
@@ -23,9 +25,9 @@ import { Fragment } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
export interface PermissionVariableSettingsFormValues {
|
||||
export interface EnvironmentVariableSettingsFormValues {
|
||||
/**
|
||||
* Permission variables.
|
||||
* Environment variables.
|
||||
*/
|
||||
environmentVariables: EnvironmentVariable[];
|
||||
}
|
||||
@@ -75,8 +77,9 @@ export default function EnvironmentVariableSettings() {
|
||||
}
|
||||
|
||||
function handleOpenCreator() {
|
||||
openDialog('CREATE_ENVIRONMENT_VARIABLE', {
|
||||
openDialog({
|
||||
title: 'Create Environment Variable',
|
||||
component: <CreateEnvironmentVariableForm />,
|
||||
props: {
|
||||
titleProps: { className: '!pb-0' },
|
||||
PaperProps: { className: 'gap-2 max-w-sm' },
|
||||
@@ -85,9 +88,13 @@ export default function EnvironmentVariableSettings() {
|
||||
}
|
||||
|
||||
function handleOpenEditor(originalVariable: EnvironmentVariable) {
|
||||
openDialog('EDIT_ENVIRONMENT_VARIABLE', {
|
||||
title: 'Edit Environment Variables',
|
||||
payload: { originalEnvironmentVariable: originalVariable },
|
||||
openDialog({
|
||||
title: 'Edit Environment Variable',
|
||||
component: (
|
||||
<EditEnvironmentVariableForm
|
||||
originalEnvironmentVariable={originalVariable}
|
||||
/>
|
||||
),
|
||||
props: {
|
||||
titleProps: { className: '!pb-0' },
|
||||
PaperProps: { className: 'gap-2 max-w-sm' },
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useDialog } from '@/components/common/DialogProvider';
|
||||
import InlineCode from '@/components/common/InlineCode';
|
||||
import EditJwtSecretForm from '@/components/settings/environmentVariables/EditJwtSecretForm';
|
||||
import SettingsContainer from '@/components/settings/SettingsContainer';
|
||||
import useIsPlatform from '@/hooks/common/useIsPlatform';
|
||||
import { useAppClient } from '@/hooks/useAppClient';
|
||||
@@ -50,7 +51,7 @@ export default function SystemEnvironmentVariableSettings() {
|
||||
}
|
||||
|
||||
function showViewJwtSecretModal() {
|
||||
openDialog('EDIT_JWT_SECRET', {
|
||||
openDialog({
|
||||
title: (
|
||||
<span className="grid grid-flow-row">
|
||||
<span>Auth JWT Secret</span>
|
||||
@@ -61,15 +62,17 @@ export default function SystemEnvironmentVariableSettings() {
|
||||
</Text>
|
||||
</span>
|
||||
),
|
||||
payload: {
|
||||
disabled: true,
|
||||
jwtSecret: data?.app?.hasuraGraphqlJwtSecret,
|
||||
},
|
||||
component: (
|
||||
<EditJwtSecretForm
|
||||
disabled
|
||||
jwtSecret={data?.app?.hasuraGraphqlJwtSecret}
|
||||
/>
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
function showEditJwtSecretModal() {
|
||||
openDialog('EDIT_JWT_SECRET', {
|
||||
openDialog({
|
||||
title: (
|
||||
<span className="grid grid-flow-row">
|
||||
<span>Edit JWT Secret</span>
|
||||
@@ -80,9 +83,9 @@ export default function SystemEnvironmentVariableSettings() {
|
||||
</Text>
|
||||
</span>
|
||||
),
|
||||
payload: {
|
||||
jwtSecret: data?.app?.hasuraGraphqlJwtSecret,
|
||||
},
|
||||
component: (
|
||||
<EditJwtSecretForm jwtSecret={data?.app?.hasuraGraphqlJwtSecret} />
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -107,7 +110,7 @@ export default function SystemEnvironmentVariableSettings() {
|
||||
),
|
||||
},
|
||||
{ key: 'NHOST_AUTH_URL', value: appClient.auth.url },
|
||||
{ key: 'NHOST_GRAPHQL_URL', value: appClient.graphql.url },
|
||||
{ key: 'NHOST_GRAPHQL_URL', value: appClient.graphql.httpUrl },
|
||||
{ key: 'NHOST_STORAGE_URL', value: appClient.storage.url },
|
||||
{ key: 'NHOST_FUNCTIONS_URL', value: appClient.functions.url },
|
||||
];
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useDialog } from '@/components/common/DialogProvider';
|
||||
import Form from '@/components/common/Form';
|
||||
import type { DialogFormProps } from '@/types/common';
|
||||
import Button from '@/ui/v2/Button';
|
||||
import Input from '@/ui/v2/Input';
|
||||
import Text from '@/ui/v2/Text';
|
||||
@@ -7,18 +8,7 @@ import { useEffect } from 'react';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
import * as Yup from 'yup';
|
||||
|
||||
export interface BasePermissionVariableFormValues {
|
||||
/**
|
||||
* Permission variable key.
|
||||
*/
|
||||
key: string;
|
||||
/**
|
||||
* Permission variable value.
|
||||
*/
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface BasePermissionVariableFormProps {
|
||||
export interface BasePermissionVariableFormProps extends DialogFormProps {
|
||||
/**
|
||||
* 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.'),
|
||||
});
|
||||
|
||||
export type BasePermissionVariableFormValues = Yup.InferType<
|
||||
typeof basePermissionVariableValidationSchema
|
||||
>;
|
||||
|
||||
export default function BasePermissionVariableForm({
|
||||
onSubmit,
|
||||
onCancel,
|
||||
submitButtonText = 'Save',
|
||||
location,
|
||||
}: BasePermissionVariableFormProps) {
|
||||
const { onDirtyStateChange } = useDialog();
|
||||
const form = useFormContext<BasePermissionVariableFormValues>();
|
||||
@@ -56,8 +51,8 @@ export default function BasePermissionVariableForm({
|
||||
const isDirty = Object.keys(dirtyFields).length > 0;
|
||||
|
||||
useEffect(() => {
|
||||
onDirtyStateChange(isDirty, 'dialog');
|
||||
}, [isDirty, onDirtyStateChange]);
|
||||
onDirtyStateChange(isDirty, location);
|
||||
}, [isDirty, location, onDirtyStateChange]);
|
||||
|
||||
return (
|
||||
<div className="grid grid-flow-row gap-2 px-6 pb-6">
|
||||
|
||||
@@ -19,7 +19,7 @@ import { FormProvider, useForm } from 'react-hook-form';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
export interface CreatePermissionVariableFormProps
|
||||
extends Pick<BasePermissionVariableFormProps, 'onCancel'> {
|
||||
extends Pick<BasePermissionVariableFormProps, 'onCancel' | 'location'> {
|
||||
/**
|
||||
* Function to be called when the form is submitted.
|
||||
*/
|
||||
|
||||
@@ -20,7 +20,7 @@ import { FormProvider, useForm } from 'react-hook-form';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
export interface EditPermissionVariableFormProps
|
||||
extends Pick<BasePermissionVariableFormProps, 'onCancel'> {
|
||||
extends Pick<BasePermissionVariableFormProps, 'onCancel' | 'location'> {
|
||||
/**
|
||||
* The permission variable to be edited.
|
||||
*/
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
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 { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||
import type { CustomClaim } from '@/types/application';
|
||||
@@ -88,8 +90,9 @@ export default function PermissionVariableSettings() {
|
||||
}
|
||||
|
||||
function handleOpenCreator() {
|
||||
openDialog('CREATE_PERMISSION_VARIABLE', {
|
||||
openDialog({
|
||||
title: 'Create Permission Variable',
|
||||
component: <CreatePermissionVariableForm />,
|
||||
props: {
|
||||
titleProps: { className: '!pb-0' },
|
||||
PaperProps: { className: 'max-w-sm' },
|
||||
@@ -98,9 +101,11 @@ export default function PermissionVariableSettings() {
|
||||
}
|
||||
|
||||
function handleOpenEditor(originalVariable: CustomClaim) {
|
||||
openDialog('EDIT_PERMISSION_VARIABLE', {
|
||||
openDialog({
|
||||
title: 'Edit Permission Variable',
|
||||
payload: { originalVariable },
|
||||
component: (
|
||||
<EditPermissionVariableForm originalVariable={originalVariable} />
|
||||
),
|
||||
props: {
|
||||
titleProps: { className: '!pb-0' },
|
||||
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."
|
||||
docsLink="https://docs.nhost.io/graphql/permissions"
|
||||
rootClassName="gap-0"
|
||||
className="px-0 my-2"
|
||||
className="my-2 px-0"
|
||||
slotProps={{ submitButton: { className: 'invisible' } }}
|
||||
>
|
||||
<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) => (
|
||||
<Fragment key={customClaim.key}>
|
||||
<ListItem.Root
|
||||
className="px-4 grid grid-cols-2"
|
||||
className="grid grid-cols-2 px-4"
|
||||
secondaryAction={
|
||||
<Dropdown.Root>
|
||||
<Tooltip
|
||||
@@ -215,7 +220,7 @@ export default function PermissionVariableSettings() {
|
||||
<>
|
||||
X-Hasura-{customClaim.key}{' '}
|
||||
{customClaim.isSystemClaim && (
|
||||
<LockIcon className="w-4 h-4" />
|
||||
<LockIcon className="h-4 w-4" />
|
||||
)}
|
||||
</>
|
||||
}
|
||||
@@ -237,7 +242,7 @@ export default function PermissionVariableSettings() {
|
||||
</List>
|
||||
|
||||
<Button
|
||||
className="justify-self-start mx-4"
|
||||
className="mx-4 justify-self-start"
|
||||
variant="borderless"
|
||||
startIcon={<PlusIcon />}
|
||||
onClick={handleOpenCreator}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useDialog } from '@/components/common/DialogProvider';
|
||||
import Form from '@/components/common/Form';
|
||||
import type { DialogFormProps } from '@/types/common';
|
||||
import { Alert } from '@/ui/Alert';
|
||||
import Button from '@/ui/v2/Button';
|
||||
import Input from '@/ui/v2/Input';
|
||||
@@ -8,14 +9,7 @@ import { useEffect } from 'react';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
import * as Yup from 'yup';
|
||||
|
||||
export interface BaseRoleFormValues {
|
||||
/**
|
||||
* The name of the role.
|
||||
*/
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface BaseRoleFormProps {
|
||||
export interface BaseRoleFormProps extends DialogFormProps {
|
||||
/**
|
||||
* 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.'),
|
||||
});
|
||||
|
||||
export type BaseRoleFormValues = Yup.InferType<
|
||||
typeof baseRoleFormValidationSchema
|
||||
>;
|
||||
|
||||
export default function BaseRoleForm({
|
||||
onSubmit,
|
||||
onCancel,
|
||||
submitButtonText = 'Save',
|
||||
location,
|
||||
}: BaseRoleFormProps) {
|
||||
const { onDirtyStateChange } = useDialog();
|
||||
const form = useFormContext<BaseRoleFormValues>();
|
||||
@@ -52,8 +51,8 @@ export default function BaseRoleForm({
|
||||
const isDirty = Object.keys(dirtyFields).length > 0;
|
||||
|
||||
useEffect(() => {
|
||||
onDirtyStateChange(isDirty, 'dialog');
|
||||
}, [isDirty, onDirtyStateChange]);
|
||||
onDirtyStateChange(isDirty, location);
|
||||
}, [isDirty, location, onDirtyStateChange]);
|
||||
|
||||
return (
|
||||
<div className="grid grid-flow-row gap-3 px-6 pb-6">
|
||||
|
||||
@@ -18,7 +18,7 @@ import { FormProvider, useForm } from 'react-hook-form';
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
export interface CreateRoleFormProps
|
||||
extends Pick<BaseRoleFormProps, 'onCancel'> {
|
||||
extends Pick<BaseRoleFormProps, 'onCancel' | 'location'> {
|
||||
/**
|
||||
* Function to be called when the form is submitted.
|
||||
*/
|
||||
|
||||
@@ -18,7 +18,8 @@ import { yupResolver } from '@hookform/resolvers/yup';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
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.
|
||||
*/
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
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 { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||
import type { Role } from '@/types/application';
|
||||
@@ -108,8 +110,9 @@ export default function RoleSettings() {
|
||||
}
|
||||
|
||||
function handleOpenCreator() {
|
||||
openDialog('CREATE_ROLE', {
|
||||
openDialog({
|
||||
title: 'Create Allowed Role',
|
||||
component: <CreateRoleForm />,
|
||||
props: {
|
||||
titleProps: { className: '!pb-0' },
|
||||
PaperProps: { className: 'max-w-sm' },
|
||||
@@ -118,9 +121,9 @@ export default function RoleSettings() {
|
||||
}
|
||||
|
||||
function handleOpenEditor(originalRole: Role) {
|
||||
openDialog('EDIT_ROLE', {
|
||||
openDialog({
|
||||
title: 'Edit Allowed Role',
|
||||
payload: { originalRole },
|
||||
component: <EditRoleForm originalRole={originalRole} />,
|
||||
props: {
|
||||
titleProps: { className: '!pb-0' },
|
||||
PaperProps: { className: 'max-w-sm' },
|
||||
|
||||
@@ -222,9 +222,9 @@ function Autocomplete(
|
||||
inputValue: inputValue || '',
|
||||
getOptionLabel: props.getOptionLabel
|
||||
? props.getOptionLabel
|
||||
: (option) => {
|
||||
if (typeof option === 'string') {
|
||||
return option;
|
||||
: (option: string | number | AutocompleteOption<string>) => {
|
||||
if (typeof option !== 'object') {
|
||||
return option.toString();
|
||||
}
|
||||
|
||||
return option.label ?? option.dropdownLabel;
|
||||
@@ -284,33 +284,46 @@ function Autocomplete(
|
||||
}}
|
||||
PopperComponent={AutocompletePopper}
|
||||
popupIcon={<ChevronDownIcon sx={{ width: 12, height: 12 }} />}
|
||||
getOptionLabel={(option) => {
|
||||
if (typeof option === 'string') {
|
||||
return option;
|
||||
getOptionLabel={(
|
||||
option: string | number | AutocompleteOption<string>,
|
||||
) => {
|
||||
if (!option) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (typeof option !== 'object') {
|
||||
return option.toString();
|
||||
}
|
||||
|
||||
return option.label ?? option.dropdownLabel;
|
||||
}}
|
||||
isOptionEqualToValue={(option, value) => {
|
||||
isOptionEqualToValue={(
|
||||
option,
|
||||
value: string | number | AutocompleteOption<string>,
|
||||
) => {
|
||||
if (!value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (typeof value === 'string') {
|
||||
return option.value === value;
|
||||
if (typeof value !== 'object') {
|
||||
return option.value.toString() === value.toString();
|
||||
}
|
||||
|
||||
return option.value === value.value && option.custom === value.custom;
|
||||
}}
|
||||
renderTags={(value, getTagProps) =>
|
||||
value.map((option, index) => (
|
||||
<StyledTag
|
||||
deleteIcon={<XIcon />}
|
||||
size="small"
|
||||
label={typeof option === 'string' ? option : option.value}
|
||||
{...getTagProps({ index })}
|
||||
/>
|
||||
))
|
||||
value.map(
|
||||
(option: string | number | AutocompleteOption<string>, index) => (
|
||||
<StyledTag
|
||||
deleteIcon={<XIcon />}
|
||||
size="small"
|
||||
label={
|
||||
typeof option !== 'object' ? option.toString() : option.value
|
||||
}
|
||||
{...getTagProps({ index })}
|
||||
/>
|
||||
),
|
||||
)
|
||||
}
|
||||
renderGroup={({ group, key, children }) =>
|
||||
group ? (
|
||||
@@ -323,9 +336,12 @@ function Autocomplete(
|
||||
<div key={key}>{children}</div>
|
||||
)
|
||||
}
|
||||
renderOption={(optionProps, option) => {
|
||||
if (typeof option === 'string') {
|
||||
return <OptionBase {...optionProps}>{option}</OptionBase>;
|
||||
renderOption={(
|
||||
optionProps,
|
||||
option: string | number | AutocompleteOption<string>,
|
||||
) => {
|
||||
if (typeof option !== 'object') {
|
||||
return <OptionBase {...optionProps}>{option.toString()}</OptionBase>;
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import Backdrop from '@/ui/v2/Backdrop';
|
||||
import type { DialogTitleProps } from '@/ui/v2/Dialog';
|
||||
import { DialogTitle } from '@/ui/v2/Dialog';
|
||||
import { styled } from '@mui/material';
|
||||
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?: ReactNode;
|
||||
/**
|
||||
* Props to pass to the title component.
|
||||
*/
|
||||
titleProps?: DialogTitleProps;
|
||||
/**
|
||||
* Determines whether or not a close button is hidden in the drawer.
|
||||
*
|
||||
@@ -33,13 +38,18 @@ function Drawer({
|
||||
children,
|
||||
onClose,
|
||||
title,
|
||||
titleProps: { sx: titleSx, ...titleProps } = {},
|
||||
...props
|
||||
}: DrawerProps) {
|
||||
return (
|
||||
<StyledDrawer components={{ Backdrop }} onClose={onClose} {...props}>
|
||||
{onClose && !hideCloseButton && (
|
||||
<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')}
|
||||
>
|
||||
{title}
|
||||
|
||||
@@ -1,29 +1,20 @@
|
||||
import { useDialog } from '@/components/common/DialogProvider';
|
||||
import Form from '@/components/common/Form';
|
||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||
import type { DialogFormProps } from '@/types/common';
|
||||
import { Alert } from '@/ui/Alert';
|
||||
import Button from '@/ui/v2/Button';
|
||||
import Input from '@/ui/v2/Input';
|
||||
import generateAppServiceUrl from '@/utils/common/generateAppServiceUrl';
|
||||
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
||||
import { yupResolver } from '@hookform/resolvers/yup';
|
||||
import axios from 'axios';
|
||||
import { useState } from 'react';
|
||||
import fetch from 'cross-fetch';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import * as Yup from 'yup';
|
||||
|
||||
export interface CreateUserFormValues {
|
||||
/**
|
||||
* Email of the user to add to this project.
|
||||
*/
|
||||
email: string;
|
||||
/**
|
||||
* Password for the user.
|
||||
*/
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface CreateUserFormProps {
|
||||
export interface CreateUserFormProps extends DialogFormProps {
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
onSuccess?: VoidFunction;
|
||||
onSubmit?: VoidFunction | ((args?: any) => Promise<any>);
|
||||
}
|
||||
|
||||
export const CreateUserFormValidationSchema = Yup.object({
|
||||
export const validationSchema = Yup.object({
|
||||
email: Yup.string()
|
||||
.min(5, 'Email must be at least 5 characters long.')
|
||||
.email('Invalid email address')
|
||||
@@ -45,10 +36,14 @@ export const CreateUserFormValidationSchema = Yup.object({
|
||||
.required('This field is required.'),
|
||||
});
|
||||
|
||||
export type CreateUserFormValues = Yup.InferType<typeof validationSchema>;
|
||||
|
||||
export default function CreateUserForm({
|
||||
onSuccess,
|
||||
onSubmit,
|
||||
onCancel,
|
||||
location,
|
||||
}: CreateUserFormProps) {
|
||||
const { onDirtyStateChange } = useDialog();
|
||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||
const [createUserFormError, setCreateUserFormError] = useState<Error | null>(
|
||||
null,
|
||||
@@ -57,15 +52,21 @@ export default function CreateUserForm({
|
||||
const form = useForm<CreateUserFormValues>({
|
||||
defaultValues: {},
|
||||
reValidateMode: 'onSubmit',
|
||||
resolver: yupResolver(CreateUserFormValidationSchema),
|
||||
resolver: yupResolver(validationSchema),
|
||||
});
|
||||
|
||||
const {
|
||||
register,
|
||||
formState: { errors, isSubmitting },
|
||||
formState: { errors, isSubmitting, dirtyFields },
|
||||
setError,
|
||||
} = form;
|
||||
|
||||
const isDirty = Object.keys(dirtyFields).length > 0;
|
||||
|
||||
useEffect(() => {
|
||||
onDirtyStateChange(isDirty, location);
|
||||
}, [isDirty, location, onDirtyStateChange]);
|
||||
|
||||
const baseAuthUrl = generateAppServiceUrl(
|
||||
currentApplication?.subdomain,
|
||||
currentApplication?.region?.awsName,
|
||||
@@ -79,28 +80,37 @@ export default function CreateUserForm({
|
||||
|
||||
try {
|
||||
await toast.promise(
|
||||
axios.post(signUpUrl, {
|
||||
email,
|
||||
password,
|
||||
fetch(signUpUrl, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email, password }),
|
||||
}).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...',
|
||||
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(),
|
||||
);
|
||||
onSuccess?.();
|
||||
|
||||
onSubmit?.();
|
||||
} catch (error) {
|
||||
if (error.response?.status === 409) {
|
||||
setError('email', {
|
||||
message: error.response.data.message,
|
||||
});
|
||||
return;
|
||||
}
|
||||
setCreateUserFormError(
|
||||
new Error(error.response.data.message || 'Something went wrong.'),
|
||||
);
|
||||
// Note: The error is already handled by the toast promise.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,7 +147,7 @@ export default function CreateUserForm({
|
||||
{createUserFormError && (
|
||||
<Alert
|
||||
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">
|
||||
<strong>Error:</strong> {createUserFormError.message}
|
||||
|
||||
@@ -2,8 +2,10 @@ import ControlledCheckbox from '@/components/common/ControlledCheckbox';
|
||||
import ControlledSelect from '@/components/common/ControlledSelect';
|
||||
import { useDialog } from '@/components/common/DialogProvider';
|
||||
import Form from '@/components/common/Form';
|
||||
import EditUserPasswordForm from '@/components/users/EditUserPasswordForm';
|
||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||
import { useRemoteApplicationGQLClient } from '@/hooks/useRemoteApplicationGQLClient';
|
||||
import type { DialogFormProps } from '@/types/common';
|
||||
import Avatar from '@/ui/v2/Avatar';
|
||||
import Box from '@/ui/v2/Box';
|
||||
import Button from '@/ui/v2/Button';
|
||||
@@ -20,6 +22,7 @@ import { copy } from '@/utils/copy';
|
||||
import getUserRoles from '@/utils/settings/getUserRoles';
|
||||
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
||||
import {
|
||||
RemoteAppGetUsersDocument,
|
||||
useGetRolesQuery,
|
||||
useUpdateRemoteAppUserMutation,
|
||||
} from '@/utils/__generated__/graphql';
|
||||
@@ -34,7 +37,7 @@ import { FormProvider, useForm } from 'react-hook-form';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import * as Yup from 'yup';
|
||||
|
||||
export interface EditUserFormProps {
|
||||
export interface EditUserFormProps extends DialogFormProps {
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
onEditUser?: (
|
||||
values: EditUserFormValues,
|
||||
user: RemoteAppUser,
|
||||
) => Promise<void>;
|
||||
onSubmit?: (values: EditUserFormValues) => Promise<void>;
|
||||
/**
|
||||
* Function to be called when the operation is cancelled.
|
||||
*/
|
||||
@@ -53,19 +53,15 @@ export interface EditUserFormProps {
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
onDeleteUser: (user: RemoteAppUser) => Promise<void>;
|
||||
onDeleteUser: (user: RemoteAppUser) => Promise<void> | void;
|
||||
/**
|
||||
* User roles
|
||||
*/
|
||||
roles: { [key: string]: boolean }[];
|
||||
/**
|
||||
* Function to be called after a successful action.
|
||||
*/
|
||||
onSuccessfulAction?: () => Promise<void> | void;
|
||||
}
|
||||
|
||||
export const EditUserFormValidationSchema = Yup.object({
|
||||
@@ -87,12 +83,12 @@ export type EditUserFormValues = Yup.InferType<
|
||||
>;
|
||||
|
||||
export default function EditUserForm({
|
||||
location,
|
||||
user,
|
||||
onEditUser,
|
||||
onSubmit,
|
||||
onCancel,
|
||||
onDeleteUser,
|
||||
roles,
|
||||
onSuccessfulAction,
|
||||
}: EditUserFormProps) {
|
||||
const theme = useTheme();
|
||||
const { onDirtyStateChange, openDialog } = useDialog();
|
||||
@@ -104,6 +100,7 @@ export default function EditUserForm({
|
||||
|
||||
const [updateUser] = useUpdateRemoteAppUserMutation({
|
||||
client: remoteProjectGQLClient,
|
||||
refetchQueries: [RemoteAppGetUsersDocument],
|
||||
});
|
||||
|
||||
const form = useForm<EditUserFormValues>({
|
||||
@@ -124,20 +121,19 @@ export default function EditUserForm({
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors, dirtyFields, isSubmitting, isValidating },
|
||||
} = form;
|
||||
|
||||
const isDirty = Object.keys(dirtyFields).length > 0;
|
||||
|
||||
useEffect(() => {
|
||||
onDirtyStateChange(isDirty, 'drawer');
|
||||
}, [isDirty, onDirtyStateChange]);
|
||||
onDirtyStateChange(isDirty, location);
|
||||
}, [isDirty, location, onDirtyStateChange]);
|
||||
|
||||
function handleChangeUserPassword() {
|
||||
openDialog('EDIT_USER_PASSWORD', {
|
||||
openDialog({
|
||||
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.
|
||||
*/
|
||||
async function handleUserDisabledStatus() {
|
||||
const shouldBan = !isUserBanned;
|
||||
|
||||
const banUser = updateUser({
|
||||
variables: {
|
||||
id: user.id,
|
||||
user: {
|
||||
disabled: !isUserBanned,
|
||||
disabled: shouldBan,
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -168,26 +166,23 @@ export default function EditUserForm({
|
||||
await toast.promise(
|
||||
banUser,
|
||||
{
|
||||
loading: user.disabled ? 'Unbanning user...' : 'Banning user...',
|
||||
success: user.disabled
|
||||
? 'User unbanned successfully.'
|
||||
: 'User banned successfully',
|
||||
error: user.disabled
|
||||
? 'An error occurred while trying to unban the user.'
|
||||
: 'An error occurred while trying to ban the user.',
|
||||
loading: shouldBan ? 'Banning user...' : 'Unbanning user...',
|
||||
success: shouldBan
|
||||
? 'User banned successfully'
|
||||
: 'User unbanned successfully.',
|
||||
error: shouldBan
|
||||
? 'An error occurred while trying to ban the user.'
|
||||
: 'An error occurred while trying to unban the user.',
|
||||
},
|
||||
getToastStyleProps(),
|
||||
);
|
||||
await onSuccessfulAction();
|
||||
}
|
||||
|
||||
return (
|
||||
<FormProvider {...form}>
|
||||
<Form
|
||||
className="flex flex-col overflow-hidden border-t-1 lg:flex-auto lg:content-between"
|
||||
onSubmit={handleSubmit(async (values) => {
|
||||
await onEditUser(values, user);
|
||||
})}
|
||||
onSubmit={onSubmit}
|
||||
>
|
||||
<Box className="flex-auto divide-y overflow-y-auto">
|
||||
<Box
|
||||
@@ -268,7 +263,7 @@ export default function EditUserForm({
|
||||
Created At
|
||||
</InputLabel>
|
||||
<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>
|
||||
|
||||
<InputLabel as="h3" className="col-span-1 self-center ">
|
||||
@@ -276,7 +271,7 @@ export default function EditUserForm({
|
||||
</InputLabel>
|
||||
<Text className="col-span-3 font-medium">
|
||||
{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>
|
||||
</Box>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useDialog } from '@/components/common/DialogProvider';
|
||||
import Form from '@/components/common/Form';
|
||||
import { useRemoteApplicationGQLClient } from '@/hooks/useRemoteApplicationGQLClient';
|
||||
import type { DialogFormProps } from '@/types/common';
|
||||
import { Alert } from '@/ui/Alert';
|
||||
import Button from '@/ui/v2/Button';
|
||||
import Input from '@/ui/v2/Input';
|
||||
@@ -14,18 +15,7 @@ import { FormProvider, useForm } from 'react-hook-form';
|
||||
import { toast } from 'react-hot-toast';
|
||||
import * as Yup from 'yup';
|
||||
|
||||
export interface EditUserPasswordFormValues {
|
||||
/**
|
||||
* Password for the user.
|
||||
*/
|
||||
password: string;
|
||||
/**
|
||||
* Confirm Password for the user.
|
||||
*/
|
||||
cpassword: string;
|
||||
}
|
||||
|
||||
export interface EditUserPasswordFormProps {
|
||||
export interface EditUserPasswordFormProps extends DialogFormProps {
|
||||
/**
|
||||
* Function to be called when the operation is cancelled.
|
||||
*/
|
||||
@@ -36,7 +26,7 @@ export interface EditUserPasswordFormProps {
|
||||
user: RemoteAppGetUsersQuery['users'][0];
|
||||
}
|
||||
|
||||
export const EditUserPasswordFormValidationSchema = Yup.object().shape({
|
||||
export const validationSchema = Yup.object({
|
||||
password: Yup.string()
|
||||
.label('Users Password')
|
||||
.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'),
|
||||
});
|
||||
|
||||
export type EditUserPasswordFormValues = Yup.InferType<typeof validationSchema>;
|
||||
|
||||
export default function EditUserPasswordForm({
|
||||
onCancel,
|
||||
user,
|
||||
@@ -63,7 +55,7 @@ export default function EditUserPasswordForm({
|
||||
const form = useForm<EditUserPasswordFormValues>({
|
||||
defaultValues: {},
|
||||
reValidateMode: 'onSubmit',
|
||||
resolver: yupResolver(EditUserPasswordFormValidationSchema),
|
||||
resolver: yupResolver(validationSchema),
|
||||
});
|
||||
|
||||
const handleSubmit = async ({ password }: EditUserPasswordFormValues) => {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useDialog } from '@/components/common/DialogProvider';
|
||||
import FormActivityIndicator from '@/components/common/FormActivityIndicator';
|
||||
import type { EditUserFormValues } from '@/components/users/EditUserForm';
|
||||
import { useCurrentWorkspaceAndApplication } from '@/hooks/useCurrentWorkspaceAndApplication';
|
||||
import { useRemoteApplicationGQLClient } from '@/hooks/useRemoteApplicationGQLClient';
|
||||
@@ -17,7 +18,6 @@ import Text from '@/ui/v2/Text';
|
||||
import getReadableProviderName from '@/utils/common/getReadableProviderName';
|
||||
import getUserRoles from '@/utils/settings/getUserRoles';
|
||||
import { getToastStyleProps } from '@/utils/settings/settingsConstants';
|
||||
import type { RemoteAppGetUsersQuery } from '@/utils/__generated__/graphql';
|
||||
import {
|
||||
useDeleteRemoteAppUserRolesMutation,
|
||||
useGetRolesQuery,
|
||||
@@ -25,16 +25,21 @@ import {
|
||||
useRemoteAppDeleteUserMutation,
|
||||
useUpdateRemoteAppUserMutation,
|
||||
} from '@/utils/__generated__/graphql';
|
||||
import type { ApolloQueryResult } from '@apollo/client';
|
||||
import { useTheme } from '@mui/material';
|
||||
import { formatDistance } from 'date-fns';
|
||||
import kebabCase from 'just-kebab-case';
|
||||
import dynamic from 'next/dynamic';
|
||||
import Image from 'next/image';
|
||||
import type { RemoteAppUser } from 'pages/[workspaceSlug]/[appSlug]/users';
|
||||
import { Fragment, useMemo } from 'react';
|
||||
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.
|
||||
* @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={() => router.reload()}
|
||||
*/
|
||||
onSuccessfulAction?: () => Promise<void> | void | Promise<T>;
|
||||
onSubmit?: () => Promise<any>;
|
||||
}
|
||||
|
||||
export default function UsersBody({
|
||||
users,
|
||||
onSuccessfulAction,
|
||||
}: UsersBodyProps<ApolloQueryResult<RemoteAppGetUsersQuery>>) {
|
||||
export default function UsersBody({ users, onSubmit }: UsersBodyProps) {
|
||||
const theme = useTheme();
|
||||
const { openAlertDialog, openDrawer, closeDrawer } = useDialog();
|
||||
const { currentApplication } = useCurrentWorkspaceAndApplication();
|
||||
@@ -151,7 +153,8 @@ export default function UsersBody({
|
||||
},
|
||||
getToastStyleProps(),
|
||||
);
|
||||
await onSuccessfulAction?.();
|
||||
|
||||
await onSubmit?.();
|
||||
|
||||
closeDrawer();
|
||||
}
|
||||
@@ -181,7 +184,7 @@ export default function UsersBody({
|
||||
getToastStyleProps(),
|
||||
);
|
||||
|
||||
await onSuccessfulAction();
|
||||
await onSubmit();
|
||||
closeDrawer();
|
||||
},
|
||||
primaryButtonColor: 'error',
|
||||
@@ -191,20 +194,20 @@ export default function UsersBody({
|
||||
}
|
||||
|
||||
function handleViewUser(user: RemoteAppUser) {
|
||||
openDrawer('EDIT_USER', {
|
||||
openDrawer({
|
||||
title: 'User Details',
|
||||
|
||||
payload: {
|
||||
user,
|
||||
onEditUser: handleEditUser,
|
||||
onDeleteUser: handleDeleteUser,
|
||||
onSuccessfulAction,
|
||||
roles: allAvailableProjectRoles.map((role) => ({
|
||||
[role.name]: user.roles.some(
|
||||
(userRole) => userRole.role === role.name,
|
||||
),
|
||||
})),
|
||||
},
|
||||
component: (
|
||||
<EditUserForm
|
||||
user={user}
|
||||
onSubmit={(values) => handleEditUser(values, user)}
|
||||
onDeleteUser={handleDeleteUser}
|
||||
roles={allAvailableProjectRoles.map((role) => ({
|
||||
[role.name]: user.roles.some(
|
||||
(userRole) => userRole.role === role.name,
|
||||
),
|
||||
}))}
|
||||
/>
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useDialog } from '@/components/common/DialogProvider';
|
||||
import EditWorkspaceNameForm from '@/components/home/EditWorkspaceNameForm';
|
||||
import RemoveWorkspaceModal from '@/components/workspace/RemoveWorkspaceModal';
|
||||
import { useUI } from '@/context/UIContext';
|
||||
import { useGetWorkspace } from '@/hooks/use-GetWorkspace';
|
||||
@@ -114,7 +115,7 @@ export default function WorkspaceHeader() {
|
||||
<Dropdown.Item
|
||||
className="py-2"
|
||||
onClick={() => {
|
||||
openDialog('EDIT_WORKSPACE_NAME', {
|
||||
openDialog({
|
||||
title: (
|
||||
<span className="grid grid-flow-row">
|
||||
<span>Change Workspace Name</span>
|
||||
@@ -124,10 +125,12 @@ export default function WorkspaceHeader() {
|
||||
</Text>
|
||||
</span>
|
||||
),
|
||||
payload: {
|
||||
currentWorkspaceName: currentWorkspace.name,
|
||||
currentWorkspaceId: currentWorkspace.id,
|
||||
},
|
||||
component: (
|
||||
<EditWorkspaceNameForm
|
||||
currentWorkspaceId={currentWorkspace.id}
|
||||
currentWorkspaceName={currentWorkspace.name}
|
||||
/>
|
||||
),
|
||||
});
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useDialog } from '@/components/common/DialogProvider';
|
||||
import EditWorkspaceNameForm from '@/components/home/EditWorkspaceNameForm';
|
||||
import Button from '@/ui/v2/Button';
|
||||
import PlusCircleIcon from '@/ui/v2/icons/PlusCircleIcon';
|
||||
import Text from '@/ui/v2/Text';
|
||||
@@ -16,7 +17,7 @@ export function WorkspaceSection() {
|
||||
variant="borderless"
|
||||
color="secondary"
|
||||
onClick={() => {
|
||||
openDialog('EDIT_WORKSPACE_NAME', {
|
||||
openDialog({
|
||||
title: (
|
||||
<span className="grid grid-flow-row">
|
||||
<span>New Workspace</span>
|
||||
@@ -26,6 +27,7 @@ export function WorkspaceSection() {
|
||||
</Text>
|
||||
</span>
|
||||
),
|
||||
component: <EditWorkspaceNameForm />,
|
||||
});
|
||||
}}
|
||||
startIcon={<PlusCircleIcon />}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useDialog } from '@/components/common/DialogProvider';
|
||||
import Pagination from '@/components/common/Pagination';
|
||||
import Container from '@/components/layout/Container';
|
||||
import ProjectLayout from '@/components/layout/ProjectLayout';
|
||||
import CreateUserForm from '@/components/users/CreateUserForm';
|
||||
import UsersBody from '@/components/users/UsersBody';
|
||||
import { useRemoteApplicationGQLClient } from '@/hooks/useRemoteApplicationGQLClient';
|
||||
import ActivityIndicator from '@/ui/v2/ActivityIndicator';
|
||||
@@ -25,7 +26,7 @@ export type RemoteAppUser = Exclude<
|
||||
>;
|
||||
|
||||
export default function UsersPage() {
|
||||
const { openDialog, closeDialog } = useDialog();
|
||||
const { openDialog } = useDialog();
|
||||
const remoteProjectGQLClient = useRemoteApplicationGQLClient();
|
||||
const [searchString, setSearchString] = useState<string>('');
|
||||
|
||||
@@ -200,14 +201,9 @@ export default function UsersPage() {
|
||||
);
|
||||
|
||||
function openCreateUserDialog() {
|
||||
openDialog('CREATE_USER', {
|
||||
openDialog({
|
||||
title: 'Create User',
|
||||
payload: {
|
||||
onSuccess: async () => {
|
||||
await refetchProjectUsers();
|
||||
closeDialog();
|
||||
},
|
||||
},
|
||||
component: <CreateUserForm onSubmit={refetchProjectUsers} />,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -228,16 +224,16 @@ export default function UsersPage() {
|
||||
if (loadingRemoteAppUsersQuery) {
|
||||
return (
|
||||
<Container
|
||||
className="flex flex-col max-w-9xl h-full"
|
||||
className="flex h-full max-w-9xl flex-col"
|
||||
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
|
||||
className="rounded-sm"
|
||||
placeholder="Search users"
|
||||
startAdornment={
|
||||
<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' }}
|
||||
/>
|
||||
}
|
||||
@@ -245,14 +241,14 @@ export default function UsersPage() {
|
||||
/>
|
||||
<Button
|
||||
onClick={openCreateUserDialog}
|
||||
startIcon={<PlusIcon className="w-4 h-4" />}
|
||||
startIcon={<PlusIcon className="h-4 w-4" />}
|
||||
size="small"
|
||||
>
|
||||
Create User
|
||||
</Button>
|
||||
</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..." />
|
||||
</div>
|
||||
</Container>
|
||||
@@ -260,14 +256,14 @@ export default function UsersPage() {
|
||||
}
|
||||
|
||||
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">
|
||||
<Input
|
||||
className="rounded-sm"
|
||||
placeholder="Search users"
|
||||
startAdornment={
|
||||
<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' }}
|
||||
/>
|
||||
}
|
||||
@@ -275,21 +271,21 @@ export default function UsersPage() {
|
||||
/>
|
||||
<Button
|
||||
onClick={openCreateUserDialog}
|
||||
startIcon={<PlusIcon className="w-4 h-4" />}
|
||||
startIcon={<PlusIcon className="h-4 w-4" />}
|
||||
size="small"
|
||||
>
|
||||
Create User
|
||||
</Button>
|
||||
</div>
|
||||
{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
|
||||
strokeWidth={1}
|
||||
className="w-10 h-10"
|
||||
className="h-10 w-10"
|
||||
sx={{ color: 'text.disabled' }}
|
||||
/>
|
||||
<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
|
||||
</Text>
|
||||
<Text variant="subtitle1" className="text-center">
|
||||
@@ -302,34 +298,34 @@ export default function UsersPage() {
|
||||
color="primary"
|
||||
className="w-full"
|
||||
onClick={openCreateUserDialog}
|
||||
startIcon={<PlusIcon className="w-4 h-4" />}
|
||||
startIcon={<PlusIcon className="h-4 w-4" />}
|
||||
>
|
||||
Create User
|
||||
</Button>
|
||||
</div>
|
||||
</Box>
|
||||
) : (
|
||||
<div className="grid grid-flow-row gap-2 lg:w-9xl">
|
||||
<div className="grid w-full h-full grid-flow-row pb-4 overflow-hidden">
|
||||
<Box className="grid w-full p-2 border-b md:grid-cols-6">
|
||||
<div className="lg:w-9xl grid grid-flow-row gap-2">
|
||||
<div className="grid h-full w-full grid-flow-row overflow-hidden pb-4">
|
||||
<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="hidden font-medium md:block">Signed up at</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
|
||||
</Text>
|
||||
</Box>
|
||||
{dataRemoteAppUsers?.filteredUsersAggreggate.aggregate.count ===
|
||||
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
|
||||
strokeWidth={1}
|
||||
className="w-10 h-10"
|
||||
className="h-10 w-10"
|
||||
sx={{ color: 'text.disabled' }}
|
||||
/>
|
||||
<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 "{searchString}"
|
||||
</Text>
|
||||
<Text variant="subtitle1" className="text-center">
|
||||
@@ -340,10 +336,7 @@ export default function UsersPage() {
|
||||
)}
|
||||
{thereAreUsers && (
|
||||
<div className="grid grid-flow-row gap-4">
|
||||
<UsersBody
|
||||
users={users}
|
||||
onSuccessfulAction={refetchProjectUsers}
|
||||
/>
|
||||
<UsersBody users={users} onSubmit={refetchProjectUsers} />
|
||||
<Pagination
|
||||
className="px-2"
|
||||
totalNrOfPages={nrOfPages}
|
||||
|
||||
10
dashboard/src/types/common.ts
Normal file
10
dashboard/src/types/common.ts
Normal 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';
|
||||
}
|
||||
@@ -28,7 +28,7 @@ The GraphQL API is available at `https://[subdomain].graphql.[region].nhost.run/
|
||||
|
||||
## GraphQL Clients for JavaScript
|
||||
|
||||
The [Nhost JavaScript client](/reference/javascript) comes with a simple [GraphQL client](/reference/javascript/nhost-js/graphql) that works well for the backend or simple applications.
|
||||
The [Nhost JavaScript client](/reference/javascript) comes with a simple [GraphQL client](/reference/javascript/graphql) that works well for the backend or simple applications.
|
||||
|
||||
When building more complex frontend applications, we recommend using a more advanced GraphQL client such as:
|
||||
|
||||
|
||||
@@ -10,8 +10,8 @@ In this section:
|
||||
- [Overview](/reference/javascript)
|
||||
- [Authentication](/reference/javascript/auth)
|
||||
- [Storage](/reference/javascript/storage)
|
||||
- [Functions](/reference/javascript/nhost-js/functions)
|
||||
- [GraphQL](/reference/javascript/nhost-js/graphql)
|
||||
- [Functions](/reference/javascript/functions)
|
||||
- [GraphQL](/reference/javascript/graphql)
|
||||
|
||||
### React
|
||||
|
||||
|
||||
60
docs/docs/reference/javascript/functions/content/01-call.mdx
Normal file
60
docs/docs/reference/javascript/functions/content/01-call.mdx
Normal file
@@ -0,0 +1,60 @@
|
||||
---
|
||||
title: call()
|
||||
sidebar_label: call()
|
||||
slug: /reference/javascript/functions/call
|
||||
description: Use `nhost.functions.call` to call (sending a POST request to) a serverless function.
|
||||
custom_edit_url: https://github.com/nhost/nhost/edit/main/packages/nhost-js/src/clients/functions/index.ts#L55
|
||||
---
|
||||
|
||||
# `call()`
|
||||
|
||||
## Overload 1 of 2
|
||||
|
||||
Use `nhost.functions.call` to call (sending a POST request to) a serverless function.
|
||||
|
||||
:::caution Deprecated
|
||||
Axios will be replaced by cross-fetch in the near future. Only the headers configuration will be kept.
|
||||
:::
|
||||
|
||||
### Parameters
|
||||
|
||||
---
|
||||
|
||||
**<span className="parameter-name">url</span>** <span className="optional-status">required</span> <code>string</code>
|
||||
|
||||
---
|
||||
|
||||
**<span className="parameter-name">data</span>** <span className="optional-status">optional</span> <code>D</code>
|
||||
|
||||
---
|
||||
|
||||
**<span className="parameter-name">config</span>** <span className="optional-status">optional</span> <code>AxiosRequestConfig<any> & { useAxios: "true" } & [`NhostFunctionCallConfig`](/reference/javascript/functions/types/nhost-function-call-config) & { useAxios: "true" }</code>
|
||||
|
||||
---
|
||||
|
||||
## Overload 2 of 2
|
||||
|
||||
Use `nhost.functions.call` to call (sending a POST request to) a serverless function.
|
||||
|
||||
```ts
|
||||
await nhost.functions.call('send-welcome-email', {
|
||||
email: 'joe@example.com',
|
||||
name: 'Joe Doe'
|
||||
})
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
---
|
||||
|
||||
**<span className="parameter-name">url</span>** <span className="optional-status">required</span> <code>string</code>
|
||||
|
||||
---
|
||||
|
||||
**<span className="parameter-name">data</span>** <span className="optional-status">required</span> <code>D</code>
|
||||
|
||||
---
|
||||
|
||||
**<span className="parameter-name">config</span>** <span className="optional-status">optional</span> <code>[`NhostFunctionCallConfig`](/reference/javascript/functions/types/nhost-function-call-config) & { useAxios: "false" }</code>
|
||||
|
||||
---
|
||||
@@ -0,0 +1,23 @@
|
||||
---
|
||||
title: setAccessToken()
|
||||
sidebar_label: setAccessToken()
|
||||
slug: /reference/javascript/functions/set-access-token
|
||||
description: Use `nhost.functions.setAccessToken` to a set an access token to be used in subsequent functions requests. Note that if you're signin in users with `nhost.auth.signIn()` the access token will be set automatically.
|
||||
custom_edit_url: https://github.com/nhost/nhost/edit/main/packages/nhost-js/src/clients/functions/index.ts#L155
|
||||
---
|
||||
|
||||
# `setAccessToken()`
|
||||
|
||||
Use `nhost.functions.setAccessToken` to a set an access token to be used in subsequent functions requests. Note that if you're signin in users with `nhost.auth.signIn()` the access token will be set automatically.
|
||||
|
||||
```ts
|
||||
nhost.functions.setAccessToken('some-access-token')
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
||||
---
|
||||
|
||||
**<span className="parameter-name">accessToken</span>** <span className="optional-status">required</span> <code>undefined | string</code>
|
||||
|
||||
---
|
||||
22
docs/docs/reference/javascript/functions/index.mdx
Normal file
22
docs/docs/reference/javascript/functions/index.mdx
Normal file
@@ -0,0 +1,22 @@
|
||||
---
|
||||
title: NhostFunctionsClient
|
||||
sidebar_label: Functions
|
||||
description: No description provided.
|
||||
slug: /reference/javascript/functions
|
||||
custom_edit_url: https://github.com/nhost/nhost/edit/main/docs/docs/reference/javascript/functions/index.mdx
|
||||
---
|
||||
|
||||
# `NhostFunctionsClient`
|
||||
|
||||
## Parameters
|
||||
|
||||
---
|
||||
|
||||
**<span className="parameter-name">params</span>** <span className="optional-status">required</span> [`NhostFunctionsConstructorParams`](/reference/javascript/functions/types/nhost-functions-constructor-params)
|
||||
|
||||
| Property | Type | Required | Notes |
|
||||
| :--------------------------------------------------------------------------------------------- | :------------------ | :------: | :---------------------------------------------------------------------------------------- |
|
||||
| <span className="parameter-name"><span className="light-grey">params.</span>url</span> | <code>string</code> | ✔️ | Serverless Functions endpoint. |
|
||||
| <span className="parameter-name"><span className="light-grey">params.</span>adminSecret</span> | <code>string</code> | | Admin secret. When set, it is sent as an `x-hasura-admin-secret` header for all requests. |
|
||||
|
||||
---
|
||||
@@ -0,0 +1,19 @@
|
||||
---
|
||||
title: NhostFunctionCallConfig
|
||||
sidebar_label: NhostFunctionCallConfig
|
||||
description: Subset of RequestInit parameters that are supported by the functions client
|
||||
displayed_sidebar: referenceSidebar
|
||||
custom_edit_url: https://github.com/nhost/nhost/edit/main/packages/nhost-js/src/clients/functions/types.ts#L41
|
||||
---
|
||||
|
||||
# `NhostFunctionCallConfig`
|
||||
|
||||
Subset of RequestInit parameters that are supported by the functions client
|
||||
|
||||
## Parameters
|
||||
|
||||
---
|
||||
|
||||
**<span className="parameter-name">headers</span>** <span className="optional-status">optional</span> <code>Record<string, string></code>
|
||||
|
||||
---
|
||||
@@ -0,0 +1,15 @@
|
||||
---
|
||||
title: NhostFunctionCallResponse
|
||||
sidebar_label: NhostFunctionCallResponse
|
||||
description: No description provided.
|
||||
displayed_sidebar: referenceSidebar
|
||||
custom_edit_url: https://github.com/nhost/nhost/edit/main/packages/nhost-js/src/clients/functions/types.ts#L15
|
||||
---
|
||||
|
||||
# `NhostFunctionCallResponse`
|
||||
|
||||
```ts
|
||||
type NhostFunctionCallResponse =
|
||||
| { res: { data: T; status: number; statusText: string }; error: null }
|
||||
| { res: null; error: ErrorPayload }
|
||||
```
|
||||
@@ -0,0 +1,25 @@
|
||||
---
|
||||
title: NhostFunctionsConstructorParams
|
||||
sidebar_label: NhostFunctionsConstructorParams
|
||||
description: No description provided.
|
||||
displayed_sidebar: referenceSidebar
|
||||
custom_edit_url: https://github.com/nhost/nhost/edit/main/packages/nhost-js/src/clients/functions/types.ts#L4
|
||||
---
|
||||
|
||||
# `NhostFunctionsConstructorParams`
|
||||
|
||||
## Parameters
|
||||
|
||||
---
|
||||
|
||||
**<span className="parameter-name">url</span>** <span className="optional-status">required</span> <code>string</code>
|
||||
|
||||
Serverless Functions endpoint.
|
||||
|
||||
---
|
||||
|
||||
**<span className="parameter-name">adminSecret</span>** <span className="optional-status">optional</span> <code>string</code>
|
||||
|
||||
Admin secret. When set, it is sent as an `x-hasura-admin-secret` header for all requests.
|
||||
|
||||
---
|
||||
@@ -10,12 +10,12 @@ The Nhost JavaScript client is the primary way of interacting with your Nhost pr
|
||||
|
||||
- [Authentication](/reference/javascript/auth)
|
||||
- [Storage](/reference/javascript/storage)
|
||||
- [Functions](/reference/javascript/nhost-js/functions)
|
||||
- [GraphQL](/reference/javascript/nhost-js/graphql)
|
||||
- [Functions](/reference/javascript/functions)
|
||||
- [GraphQL](/reference/javascript/graphql)
|
||||
|
||||
## Installation
|
||||
|
||||
Install the the Nhost client together with GraphQL:
|
||||
Install the Nhost client together with GraphQL:
|
||||
|
||||
<Tabs groupId="package-manager">
|
||||
<TabItem value="npm" label="npm" default>
|
||||
|
||||
@@ -111,12 +111,12 @@ const sidebars = {
|
||||
label: 'Functions',
|
||||
link: {
|
||||
type: 'doc',
|
||||
id: 'reference/docgen/javascript/nhost-js/content/nhost-functions-client/index'
|
||||
id: 'reference/javascript/functions/index'
|
||||
},
|
||||
items: [
|
||||
{
|
||||
type: 'autogenerated',
|
||||
dirName: 'reference/docgen/javascript/nhost-js/content/nhost-functions-client/content'
|
||||
dirName: 'reference/javascript/functions/content'
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -125,12 +125,12 @@ const sidebars = {
|
||||
label: 'GraphQL',
|
||||
link: {
|
||||
type: 'doc',
|
||||
id: 'reference/docgen/javascript/nhost-js/content/nhost-graphql-client/index'
|
||||
id: 'reference/docgen/javascript/graphql/content/nhost-graphql-client/index'
|
||||
},
|
||||
items: [
|
||||
{
|
||||
type: 'autogenerated',
|
||||
dirName: 'reference/docgen/javascript/nhost-js/content/nhost-graphql-client/content'
|
||||
dirName: 'reference/docgen/javascript/graphql/content/nhost-graphql-client/content'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,5 +1,26 @@
|
||||
# @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
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nhost-examples/codegen-react-apollo",
|
||||
"version": "0.1.5",
|
||||
"version": "0.1.7",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"codegen": "graphql-codegen",
|
||||
|
||||
@@ -1,5 +1,21 @@
|
||||
# @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
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nhost-examples/codegen-react-query",
|
||||
"version": "0.1.5",
|
||||
"version": "0.1.7",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"codegen": "graphql-codegen",
|
||||
|
||||
@@ -1,5 +1,24 @@
|
||||
# @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
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@nhost-examples/codegen-react-urql",
|
||||
"private": true,
|
||||
"version": "0.0.2",
|
||||
"version": "0.0.4",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# @nhost-examples/docker-compose
|
||||
|
||||
## 0.0.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 01318860: fix(nhost-js): use correct URL for functions requests
|
||||
|
||||
## 0.0.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nhost-examples/docker-compose",
|
||||
"version": "0.0.4",
|
||||
"version": "0.0.5",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"e2e": "vitest run"
|
||||
|
||||
@@ -1,5 +1,21 @@
|
||||
# @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
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@nhost-examples/multi-tenant-one-to-many",
|
||||
"private": true,
|
||||
"version": "1.0.1",
|
||||
"version": "1.0.3",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {},
|
||||
|
||||
@@ -1,5 +1,29 @@
|
||||
# @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
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -18,6 +18,7 @@ export function authProtected(Comp) {
|
||||
if (isLoading) {
|
||||
return <div>Loading...</div>
|
||||
}
|
||||
|
||||
return <Comp {...props} />
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nhost-examples/nextjs",
|
||||
"version": "0.1.5",
|
||||
"version": "0.1.7",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
@@ -35,8 +35,8 @@
|
||||
"@types/react": "18.0.25",
|
||||
"@xstate/inspect": "^0.6.2",
|
||||
"eslint-config-next": "12.0.10",
|
||||
"typescript": "4.5.5",
|
||||
"typescript": "^4.8.2",
|
||||
"ws": "^8.8.1",
|
||||
"xstate": "^4.33.5"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,26 @@
|
||||
# @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
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nhost-examples/react-apollo",
|
||||
"version": "0.1.7",
|
||||
"version": "0.1.9",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@apollo/client": "^3.6.9",
|
||||
|
||||
@@ -1,5 +1,21 @@
|
||||
# @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
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@nhost-examples/react-gqty",
|
||||
"private": true,
|
||||
"version": "0.0.4",
|
||||
"version": "0.0.6",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
7
examples/seed-data-storage/CHANGELOG.md
Normal file
7
examples/seed-data-storage/CHANGELOG.md
Normal 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
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@nhost-examples/seed-data-storage",
|
||||
"private": true,
|
||||
"version": "0.0.1",
|
||||
"version": "0.0.2",
|
||||
"scripts": {
|
||||
"seed-storage": "./seed-storage.sh"
|
||||
}
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
# @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
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"name": "@nhost-examples/serverless-functions",
|
||||
"private": true,
|
||||
"version": "0.0.6",
|
||||
"version": "0.0.7",
|
||||
"devDependencies": {
|
||||
"@types/express": "^4.17.13"
|
||||
},
|
||||
"dependencies": {
|
||||
"@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",
|
||||
"cross-fetch": "^3.1.5",
|
||||
"graphql": "15.7.2",
|
||||
|
||||
@@ -1,5 +1,23 @@
|
||||
# @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
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"name": "@nhost-examples/vue-apollo",
|
||||
"private": true,
|
||||
"version": "0.0.5",
|
||||
"version": "0.0.7",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vue-tsc --noEmit && vite build",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"prettier": "prettier --check src/",
|
||||
"prettier:fix": "prettier --write src/",
|
||||
@@ -34,7 +34,7 @@
|
||||
"@vitejs/plugin-vue": "^4.0.0",
|
||||
"@xstate/inspect": "^0.6.2",
|
||||
"sass": "1.32.0",
|
||||
"typescript": "^4.8.4",
|
||||
"typescript": "4.9.4",
|
||||
"vite": "^4.0.2",
|
||||
"vue-tsc": "^0.38.9"
|
||||
},
|
||||
|
||||
@@ -1,5 +1,23 @@
|
||||
# @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
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nhost-examples/vue-quickstart",
|
||||
"version": "0.0.4",
|
||||
"version": "0.0.6",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "vite build",
|
||||
|
||||
@@ -1,5 +1,51 @@
|
||||
# @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
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [c9d2d31a]
|
||||
- Updated dependencies [80bbd3a1]
|
||||
- Updated dependencies [80bbd3a1]
|
||||
- Updated dependencies [2949ff0f]
|
||||
- @nhost/nhost-js@2.0.0
|
||||
|
||||
## 4.13.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nhost/apollo",
|
||||
"version": "4.13.4",
|
||||
"version": "5.0.5",
|
||||
"description": "Nhost Apollo Client library",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# @nhost/google-translation
|
||||
|
||||
## 0.0.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 01318860: fix(nhost-js): use correct URL for functions requests
|
||||
|
||||
## 0.0.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@nhost/google-translation",
|
||||
"version": "0.0.2",
|
||||
"version": "0.0.3",
|
||||
"description": "Google Translation GraphQL API",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
|
||||
@@ -1,5 +1,54 @@
|
||||
# @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
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [19b11d40]
|
||||
- Updated dependencies [19b11d40]
|
||||
- @nhost/react@2.0.0
|
||||
- @nhost/apollo@5.0.0
|
||||
|
||||
## 4.13.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user