mirror of
https://github.com/wasp-lang/wasp.git
synced 2024-09-11 15:26:19 +03:00
Adds support for root component (#1009)
This commit is contained in:
parent
bfbb57ecd0
commit
c2a3e04283
@ -1,4 +1,5 @@
|
||||
# Changelog
|
||||
|
||||
## v0.8.2
|
||||
|
||||
### Non-breaking Changes
|
||||
@ -7,6 +8,9 @@
|
||||
### Bug fixes
|
||||
- Fixes a file lock error that kills CLI when changing entities with `wasp start` running on newer Macs.
|
||||
|
||||
### Support for defining the web app's root component
|
||||
You can now define a root component for your client app. This is useful if you want to wrap your app in a provider or have a common layout. You can define it in `app.client.rootComponent` in your `.wasp` file.
|
||||
|
||||
### `wasp deploy` CLI command added
|
||||
We have made it much easier to deploy your Wasp apps via a new CLI command, `wasp deploy`. 🚀 This release adds support for Fly.io, but we hope to add more hosting providers soon!
|
||||
|
||||
|
@ -4,22 +4,25 @@ import ReactDOM from 'react-dom'
|
||||
import { QueryClientProvider } from '@tanstack/react-query'
|
||||
|
||||
import router from './router'
|
||||
import {
|
||||
import {
|
||||
initializeQueryClient,
|
||||
queryClientInitialized,
|
||||
} from './queryClient'
|
||||
import * as serviceWorker from './serviceWorker'
|
||||
|
||||
{=# doesClientSetupFnExist =}
|
||||
{=& clientSetupJsFnImportStatement =}
|
||||
{=/ doesClientSetupFnExist =}
|
||||
{=# setupFn.isDefined =}
|
||||
{=& setupFn.importStatement =}
|
||||
{=/ setupFn.isDefined =}
|
||||
{=# rootComponent.isDefined =}
|
||||
{=& rootComponent.importStatement =}
|
||||
{=/ rootComponent.isDefined =}
|
||||
|
||||
startApp()
|
||||
|
||||
async function startApp() {
|
||||
{=# doesClientSetupFnExist =}
|
||||
await {= clientSetupJsFnIdentifier =}()
|
||||
{=/ doesClientSetupFnExist =}
|
||||
{=# setupFn.isDefined =}
|
||||
await {= setupFn.importIdentifier =}()
|
||||
{=/ setupFn.isDefined =}
|
||||
initializeQueryClient()
|
||||
|
||||
await render()
|
||||
@ -34,7 +37,13 @@ async function render() {
|
||||
const queryClient = await queryClientInitialized
|
||||
ReactDOM.render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
{ router }
|
||||
{=# rootComponent.isDefined =}
|
||||
<{= rootComponent.importIdentifier =}>
|
||||
{=/ rootComponent.isDefined =}
|
||||
{router}
|
||||
{=# rootComponent.isDefined =}
|
||||
</{= rootComponent.importIdentifier =}>
|
||||
{=/ rootComponent.isDefined =}
|
||||
</QueryClientProvider>,
|
||||
document.getElementById('root')
|
||||
)
|
||||
|
@ -333,7 +333,7 @@
|
||||
"file",
|
||||
"web-app/src/index.js"
|
||||
],
|
||||
"e7627d838b78383b5c95312fb205b15978dcd66aad16327e000b7e71810a066b"
|
||||
"278ad569e8dbf14568c481e7929718362d40d4b6dc1940d4975453594e5568e9"
|
||||
],
|
||||
[
|
||||
[
|
||||
|
@ -3,7 +3,7 @@ import ReactDOM from 'react-dom'
|
||||
import { QueryClientProvider } from '@tanstack/react-query'
|
||||
|
||||
import router from './router'
|
||||
import {
|
||||
import {
|
||||
initializeQueryClient,
|
||||
queryClientInitialized,
|
||||
} from './queryClient'
|
||||
@ -27,7 +27,7 @@ async function render() {
|
||||
const queryClient = await queryClientInitialized
|
||||
ReactDOM.render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
{ router }
|
||||
{router}
|
||||
</QueryClientProvider>,
|
||||
document.getElementById('root')
|
||||
)
|
||||
|
@ -333,7 +333,7 @@
|
||||
"file",
|
||||
"web-app/src/index.js"
|
||||
],
|
||||
"e7627d838b78383b5c95312fb205b15978dcd66aad16327e000b7e71810a066b"
|
||||
"278ad569e8dbf14568c481e7929718362d40d4b6dc1940d4975453594e5568e9"
|
||||
],
|
||||
[
|
||||
[
|
||||
|
@ -3,7 +3,7 @@ import ReactDOM from 'react-dom'
|
||||
import { QueryClientProvider } from '@tanstack/react-query'
|
||||
|
||||
import router from './router'
|
||||
import {
|
||||
import {
|
||||
initializeQueryClient,
|
||||
queryClientInitialized,
|
||||
} from './queryClient'
|
||||
@ -27,7 +27,7 @@ async function render() {
|
||||
const queryClient = await queryClientInitialized
|
||||
ReactDOM.render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
{ router }
|
||||
{router}
|
||||
</QueryClientProvider>,
|
||||
document.getElementById('root')
|
||||
)
|
||||
|
@ -347,7 +347,7 @@
|
||||
"file",
|
||||
"web-app/src/index.js"
|
||||
],
|
||||
"e7627d838b78383b5c95312fb205b15978dcd66aad16327e000b7e71810a066b"
|
||||
"278ad569e8dbf14568c481e7929718362d40d4b6dc1940d4975453594e5568e9"
|
||||
],
|
||||
[
|
||||
[
|
||||
|
@ -3,7 +3,7 @@ import ReactDOM from 'react-dom'
|
||||
import { QueryClientProvider } from '@tanstack/react-query'
|
||||
|
||||
import router from './router'
|
||||
import {
|
||||
import {
|
||||
initializeQueryClient,
|
||||
queryClientInitialized,
|
||||
} from './queryClient'
|
||||
@ -27,7 +27,7 @@ async function render() {
|
||||
const queryClient = await queryClientInitialized
|
||||
ReactDOM.render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
{ router }
|
||||
{router}
|
||||
</QueryClientProvider>,
|
||||
document.getElementById('root')
|
||||
)
|
||||
|
@ -333,7 +333,7 @@
|
||||
"file",
|
||||
"web-app/src/index.js"
|
||||
],
|
||||
"e7627d838b78383b5c95312fb205b15978dcd66aad16327e000b7e71810a066b"
|
||||
"278ad569e8dbf14568c481e7929718362d40d4b6dc1940d4975453594e5568e9"
|
||||
],
|
||||
[
|
||||
[
|
||||
|
@ -3,7 +3,7 @@ import ReactDOM from 'react-dom'
|
||||
import { QueryClientProvider } from '@tanstack/react-query'
|
||||
|
||||
import router from './router'
|
||||
import {
|
||||
import {
|
||||
initializeQueryClient,
|
||||
queryClientInitialized,
|
||||
} from './queryClient'
|
||||
@ -27,7 +27,7 @@ async function render() {
|
||||
const queryClient = await queryClientInitialized
|
||||
ReactDOM.render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
{ router }
|
||||
{router}
|
||||
</QueryClientProvider>,
|
||||
document.getElementById('root')
|
||||
)
|
||||
|
10
waspc/examples/todoApp/src/client/App.jsx
Normal file
10
waspc/examples/todoApp/src/client/App.jsx
Normal file
@ -0,0 +1,10 @@
|
||||
export function App({ children }) {
|
||||
return (
|
||||
<div className="p-6">
|
||||
<header className="mb-6">
|
||||
<h1 className="text-3xl font-bold">ToDo App</h1>
|
||||
</header>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
@ -29,6 +29,7 @@ app todoApp {
|
||||
setupFn: import setup from "@server/serverSetup.js"
|
||||
},
|
||||
client: {
|
||||
rootComponent: import { App } from "@client/App.jsx",
|
||||
setupFn: import setup from "@client/clientSetup"
|
||||
},
|
||||
db: {
|
||||
|
@ -9,6 +9,7 @@ import Data.Data (Data)
|
||||
import Wasp.AppSpec.ExtImport (ExtImport)
|
||||
|
||||
data Client = Client
|
||||
{ setupFn :: Maybe ExtImport
|
||||
{ setupFn :: Maybe ExtImport,
|
||||
rootComponent :: Maybe ExtImport
|
||||
}
|
||||
deriving (Show, Eq, Data)
|
||||
|
@ -1,9 +1,11 @@
|
||||
module Wasp.Generator.JsImport
|
||||
( extImportToJsImport,
|
||||
ImportLocation,
|
||||
jsImportToImportJson,
|
||||
)
|
||||
where
|
||||
|
||||
import Data.Aeson (KeyValue ((.=)), object)
|
||||
import qualified Data.Aeson as Aeson
|
||||
import StrongPath (Dir, File', Path, Posix, Rel, (</>))
|
||||
import qualified StrongPath as SP
|
||||
import qualified Wasp.AppSpec.ExtImport as EI
|
||||
@ -12,15 +14,14 @@ import Wasp.Generator.ExternalCodeGenerator.Common (GeneratedExternalCodeDir)
|
||||
import Wasp.JsImport
|
||||
( JsImport,
|
||||
JsImportName (JsImportField, JsImportModule),
|
||||
getJsImportStmtAndIdentifier,
|
||||
makeJsImport,
|
||||
)
|
||||
|
||||
type ImportLocation = ()
|
||||
|
||||
extImportToJsImport ::
|
||||
GeneratedSrcDir d =>
|
||||
Path Posix (Rel d) (Dir GeneratedExternalCodeDir) ->
|
||||
Path Posix (Rel ImportLocation) (Dir d) ->
|
||||
Path Posix (Rel importLocation) (Dir d) ->
|
||||
EI.ExtImport ->
|
||||
JsImport
|
||||
extImportToJsImport pathFromSrcDirToExtCodeDir pathFromImportLocationToSrcDir extImport = makeJsImport importPath importName
|
||||
@ -32,3 +33,17 @@ extImportToJsImport pathFromSrcDirToExtCodeDir pathFromImportLocationToSrcDir ex
|
||||
extImportNameToJsImportName :: EI.ExtImportName -> JsImportName
|
||||
extImportNameToJsImportName (EI.ExtImportModule name) = JsImportModule name
|
||||
extImportNameToJsImportName (EI.ExtImportField name) = JsImportField name
|
||||
|
||||
jsImportToImportJson :: Maybe JsImport -> Aeson.Value
|
||||
jsImportToImportJson maybeJsImport = maybe notDefinedValue mkTmplData maybeJsImport
|
||||
where
|
||||
notDefinedValue = object ["isDefined" .= False]
|
||||
|
||||
mkTmplData :: JsImport -> Aeson.Value
|
||||
mkTmplData jsImport =
|
||||
let (jsImportStmt, jsImportIdentifier) = getJsImportStmtAndIdentifier jsImport
|
||||
in object
|
||||
[ "isDefined" .= True,
|
||||
"importStatement" .= jsImportStmt,
|
||||
"importIdentifier" .= jsImportIdentifier
|
||||
]
|
||||
|
@ -222,7 +222,7 @@ genServerJs spec =
|
||||
(maybeSetupJsFnImportStmt, maybeSetupJsFnImportIdentifier) =
|
||||
(fst <$> maybeSetupJsFnImportDetails, snd <$> maybeSetupJsFnImportDetails)
|
||||
|
||||
relPathToServerSrcDir :: Path Posix (Rel ()) (Dir C.ServerSrcDir)
|
||||
relPathToServerSrcDir :: Path Posix (Rel importLocation) (Dir C.ServerSrcDir)
|
||||
relPathToServerSrcDir = [reldirP|./|]
|
||||
|
||||
genRoutesDir :: AppSpec -> Generator [FileDraft]
|
||||
|
@ -142,7 +142,7 @@ getTmplDataForAuthMethodConfig auth authMethod =
|
||||
maybeOnSignInFnImportDetails = getJsImportStmtAndIdentifier' <$> maybeGetUserFieldsFn
|
||||
(maybeOnSignInFnImportStmt, maybeOnSignInFnImportIdentifier) = (fst <$> maybeOnSignInFnImportDetails, snd <$> maybeOnSignInFnImportDetails)
|
||||
|
||||
relPathFromAuthConfigToServerSrcDir :: Path Posix (Rel ()) (Dir C.ServerSrcDir)
|
||||
relPathFromAuthConfigToServerSrcDir :: Path Posix (Rel importLocation) (Dir C.ServerSrcDir)
|
||||
relPathFromAuthConfigToServerSrcDir = [reldirP|../../../../|]
|
||||
|
||||
depsRequiredByPassport :: AppSpec -> [App.Dependency.Dependency]
|
||||
|
@ -78,7 +78,7 @@ genJob (jobName, job) =
|
||||
]
|
||||
maybeJobSchedule = jobScheduleTmplData <$> J.schedule job
|
||||
|
||||
relPathFromJobsDirToServerSrcDir :: Path Posix (Rel ()) (Dir C.ServerSrcDir)
|
||||
relPathFromJobsDirToServerSrcDir :: Path Posix (Rel importLocation) (Dir C.ServerSrcDir)
|
||||
relPathFromJobsDirToServerSrcDir = [reldirP|../|]
|
||||
|
||||
-- Creates a file that is imported on the server to ensure all job JS modules are loaded
|
||||
|
@ -4,7 +4,6 @@ import Data.Maybe (fromJust)
|
||||
import StrongPath (Dir, Path, Posix, Rel)
|
||||
import qualified StrongPath as SP
|
||||
import qualified Wasp.AppSpec.ExtImport as EI
|
||||
import Wasp.Generator.JsImport (ImportLocation)
|
||||
import qualified Wasp.Generator.JsImport as GJI
|
||||
import Wasp.Generator.ServerGenerator.Common (ServerSrcDir)
|
||||
import Wasp.Generator.ServerGenerator.ExternalCodeGenerator (extServerCodeDirInServerSrcDir)
|
||||
@ -16,13 +15,13 @@ import Wasp.JsImport
|
||||
import qualified Wasp.JsImport as JI
|
||||
|
||||
getJsImportStmtAndIdentifier ::
|
||||
Path Posix (Rel ImportLocation) (Dir ServerSrcDir) ->
|
||||
Path Posix (Rel importLocation) (Dir ServerSrcDir) ->
|
||||
EI.ExtImport ->
|
||||
(JsImportStatement, JsImportIdentifier)
|
||||
getJsImportStmtAndIdentifier pathFromImportLocationToExtCodeDir = JI.getJsImportStmtAndIdentifier . extImportToJsImport pathFromImportLocationToExtCodeDir
|
||||
|
||||
extImportToJsImport ::
|
||||
Path Posix (Rel ImportLocation) (Dir ServerSrcDir) ->
|
||||
Path Posix (Rel importLocation) (Dir ServerSrcDir) ->
|
||||
EI.ExtImport ->
|
||||
JsImport
|
||||
extImportToJsImport = GJI.extImportToJsImport serverExtDir
|
||||
|
@ -134,5 +134,5 @@ operationTmplData operation =
|
||||
where
|
||||
(importStmt, importIdentifier) = getJsImportStmtAndIdentifier relPathFromOperationsDirToServerSrcDir (AS.Operation.getFn operation)
|
||||
|
||||
relPathFromOperationsDirToServerSrcDir :: Path Posix (Rel ()) (Dir C.ServerSrcDir)
|
||||
relPathFromOperationsDirToServerSrcDir :: Path Posix (Rel importLocation) (Dir C.ServerSrcDir)
|
||||
relPathFromOperationsDirToServerSrcDir = [reldirP|../|]
|
||||
|
@ -8,7 +8,6 @@ where
|
||||
|
||||
import Data.Aeson (object, (.=))
|
||||
import Data.List (intercalate)
|
||||
import Data.Maybe (fromMaybe, isJust)
|
||||
import StrongPath
|
||||
( Dir,
|
||||
File',
|
||||
@ -46,7 +45,7 @@ import Wasp.Generator.WebAppGenerator.ExternalCodeGenerator
|
||||
( extClientCodeGeneratorStrategy,
|
||||
extSharedCodeGeneratorStrategy,
|
||||
)
|
||||
import Wasp.Generator.WebAppGenerator.JsImport (getJsImportStmtAndIdentifier)
|
||||
import Wasp.Generator.WebAppGenerator.JsImport (extImportToImportJson)
|
||||
import Wasp.Generator.WebAppGenerator.OperationsGenerator (genOperations)
|
||||
import Wasp.Generator.WebAppGenerator.RouterGenerator (genRouter)
|
||||
import Wasp.Util ((<++>))
|
||||
@ -246,16 +245,13 @@ genIndexJs spec =
|
||||
(C.asWebAppFile [relfile|src/index.js|])
|
||||
( Just $
|
||||
object
|
||||
[ "doesClientSetupFnExist" .= isJust maybeSetupJsFunction,
|
||||
"clientSetupJsFnImportStatement" .= fromMaybe "" maybeSetupJsFnImportStmt,
|
||||
"clientSetupJsFnIdentifier" .= fromMaybe "" maybeSetupJsFnImportIdentifier
|
||||
[ "setupFn" .= extImportToImportJson relPathToWebAppSrcDir maybeSetupJsFunction,
|
||||
"rootComponent" .= extImportToImportJson relPathToWebAppSrcDir maybeRootComponent
|
||||
]
|
||||
)
|
||||
where
|
||||
maybeSetupJsFunction = AS.App.Client.setupFn =<< AS.App.client (snd $ getApp spec)
|
||||
maybeSetupJsFnImportDetails = getJsImportStmtAndIdentifier relPathToWebAppSrcDir <$> maybeSetupJsFunction
|
||||
(maybeSetupJsFnImportStmt, maybeSetupJsFnImportIdentifier) =
|
||||
(fst <$> maybeSetupJsFnImportDetails, snd <$> maybeSetupJsFnImportDetails)
|
||||
maybeRootComponent = AS.App.Client.rootComponent =<< AS.App.client (snd $ getApp spec)
|
||||
|
||||
relPathToWebAppSrcDir :: Path Posix (Rel ()) (Dir C.WebAppSrcDir)
|
||||
relPathToWebAppSrcDir :: Path Posix (Rel importLocation) (Dir C.WebAppSrcDir)
|
||||
relPathToWebAppSrcDir = [reldirP|./|]
|
||||
|
@ -1,10 +1,14 @@
|
||||
module Wasp.Generator.WebAppGenerator.JsImport where
|
||||
|
||||
import qualified Data.Aeson as Aeson
|
||||
import Data.Maybe (fromJust)
|
||||
import StrongPath (Dir, Path, Posix, Rel)
|
||||
import qualified StrongPath as SP
|
||||
import Wasp.AppSpec.ExtImport (ExtImport)
|
||||
import qualified Wasp.AppSpec.ExtImport as EI
|
||||
import Wasp.Generator.JsImport (ImportLocation)
|
||||
import Wasp.Generator.JsImport
|
||||
( jsImportToImportJson,
|
||||
)
|
||||
import qualified Wasp.Generator.JsImport as GJI
|
||||
import Wasp.Generator.WebAppGenerator.Common (WebAppSrcDir)
|
||||
import Wasp.Generator.WebAppGenerator.ExternalCodeGenerator (extClientCodeDirInWebAppSrcDir)
|
||||
@ -15,14 +19,22 @@ import Wasp.JsImport
|
||||
)
|
||||
import qualified Wasp.JsImport as JI
|
||||
|
||||
extImportToImportJson ::
|
||||
Path Posix (Rel importLocation) (Dir WebAppSrcDir) ->
|
||||
Maybe ExtImport ->
|
||||
Aeson.Value
|
||||
extImportToImportJson pathFromImportLocationToSrcDir maybeExtImport = jsImportToImportJson jsImport
|
||||
where
|
||||
jsImport = extImportToJsImport pathFromImportLocationToSrcDir <$> maybeExtImport
|
||||
|
||||
getJsImportStmtAndIdentifier ::
|
||||
Path Posix (Rel ImportLocation) (Dir WebAppSrcDir) ->
|
||||
Path Posix (Rel importLocation) (Dir WebAppSrcDir) ->
|
||||
EI.ExtImport ->
|
||||
(JsImportStatement, JsImportIdentifier)
|
||||
getJsImportStmtAndIdentifier pathFromImportLocationToSrcDir = JI.getJsImportStmtAndIdentifier . extImportToJsImport pathFromImportLocationToSrcDir
|
||||
|
||||
extImportToJsImport ::
|
||||
Path Posix (Rel ImportLocation) (Dir WebAppSrcDir) ->
|
||||
Path Posix (Rel importLocation) (Dir WebAppSrcDir) ->
|
||||
EI.ExtImport ->
|
||||
JsImport
|
||||
extImportToJsImport = GJI.extImportToJsImport webAppExtDir
|
||||
|
@ -167,7 +167,7 @@ createPageTemplateData page =
|
||||
importStmt :: String
|
||||
(importStmt, _) = getJsImportStmtAndIdentifier $ applyJsImportAlias (Just importAlias) $ extImportToJsImport relPathToWebAppSrcDir pageComponent
|
||||
|
||||
relPathToWebAppSrcDir :: Path Posix (Rel ()) (Dir C.WebAppSrcDir)
|
||||
relPathToWebAppSrcDir :: Path Posix (Rel importLocation) (Dir C.WebAppSrcDir)
|
||||
relPathToWebAppSrcDir = [reldirP|./|]
|
||||
|
||||
pageComponent :: AS.ExtImport.ExtImport
|
||||
|
@ -57,6 +57,7 @@ spec_Analyzer = do
|
||||
" setupFn: import { setupServer } from \"@server/bar.js\"",
|
||||
" },",
|
||||
" client: {",
|
||||
" rootComponent: import { App } from \"@client/App.jsx\",",
|
||||
" setupFn: import { setupClient } from \"@client/baz.js\"",
|
||||
" },",
|
||||
" db: {",
|
||||
@ -148,7 +149,10 @@ spec_Analyzer = do
|
||||
Client.Client
|
||||
{ Client.setupFn =
|
||||
Just $
|
||||
ExtImport (ExtImportField "setupClient") (fromJust $ SP.parseRelFileP "baz.js")
|
||||
ExtImport (ExtImportField "setupClient") (fromJust $ SP.parseRelFileP "baz.js"),
|
||||
Client.rootComponent =
|
||||
Just $
|
||||
ExtImport (ExtImportField "App") (fromJust $ SP.parseRelFileP "App.jsx")
|
||||
},
|
||||
App.db = Just Db.Db {Db.system = Just Db.PostgreSQL}
|
||||
}
|
||||
|
@ -485,7 +485,7 @@ In other words, the hook returns a function whose API matches the original Actio
|
||||
|
||||
The `useAction` hook accepts two arguments:
|
||||
- `actionFn` (required) - The Wasp Action (i.e., the client-side query function generated by Wasp based on a query declaration) you wish to enhance.
|
||||
- `actionOptions` (optional) - An object configuring the extra features you want to add to the given Action. While this argument is technically optional, there is no point in using the `useAction` hook without providing it (it would be the same as using the Action directly). The Action options object supports the following fields:
|
||||
- `actionOptions` (optional) - An object configuring the extra features you want to add to the given Action. While this argument is technically optional, there is no point in using the `useAction` hook without providing it (it would be the same as using the Action directly). The Action options object supports the following fields:
|
||||
- `optimisticUpdates` (optional) - An array of objects where each object defines an [optimistic update](https://stackoverflow.com/a/33009713) to perform on the query cache. To define an optimistic update, you must specify the following properties:
|
||||
- `getQuerySpecifier` (required) - A function returning the query specifier (i.e., a value used to address the query you want to update). A query specifier is an array specifying the query function and arguments. For example, to optimistically update the query used with `useQuery(fetchFilteredTasks, {isDone: true }]`, your `getQuerySpecifier` function would have to return the array `[fetchFilteredTasks, { isDone: true}]`. Wasp will forward the argument you pass into the decorated Action to this function (i.e., you can use the properties of the added/change item to address the query).
|
||||
- `updateQuery` (required) - The function used to perform the optimistic update. It should return the desired state of the cache. Wasp will call it with the following arguments:
|
||||
@ -494,7 +494,7 @@ The `useAction` hook accepts two arguments:
|
||||
|
||||
**NOTE:** The `updateQuery` function must be a pure function. It must return the desired cache value identified by the `getQuerySpecifier` function and _must not_ perform any side effects. Also, make sure you only update the query caches affected by your action causing the optimistic update (Wasp cannot yet verify this). Finally, your implementation of the `updateQuery` function should work correctly regardless of the state of `oldData` (e.g., don't rely on array positioning). If you need to do something else during your optimistic update, you can directly use _react-query_'s lower-level API (read more about it [here](#advanced-usage)).
|
||||
|
||||
Here's an example showing how to configure the Action from the previous example to perform an optimistic update:
|
||||
Here's an example showing how to configure the Action from the previous example to perform an optimistic update:
|
||||
```jsx {3,9,10,11,12,13,14,15,16,27} title=src/client/pages/Task.js
|
||||
import React from 'react'
|
||||
import { useQuery } from '@wasp/queries'
|
||||
@ -532,7 +532,7 @@ const TaskPage = ({ id }) => {
|
||||
)
|
||||
}
|
||||
|
||||
export default TaskPage
|
||||
export default TaskPage
|
||||
```
|
||||
#### Advanced usage
|
||||
The `useAction` hook currently only supports specifying optimistic updates. You can expect more features in future versions of Wasp.
|
||||
@ -590,8 +590,8 @@ If you have server tasks that you do not want to handle as part of the normal re
|
||||
* persist between server restarts
|
||||
* can be retried if they fail
|
||||
* can be delayed until the future
|
||||
* can have a recurring schedule!
|
||||
|
||||
* can have a recurring schedule!
|
||||
|
||||
Some examples where you may want to use a `job` on the server include sending an email, making an HTTP request to some external API, or doing some nightly calculations.
|
||||
|
||||
### Job Executors
|
||||
@ -606,7 +606,7 @@ Currently, Wasp supports only one type of job executor, which is `PgBoss`, but i
|
||||
|
||||
We have selected [pg-boss](https://github.com/timgit/pg-boss/) as our first job executor to handle the low-volume, basic job queue workloads many web applications have. By using PostgreSQL (and [SKIP LOCKED](https://www.2ndquadrant.com/en/blog/what-is-select-skip-locked-for-in-postgresql-9-5/)) as its storage and synchronization mechanism, it allows us to provide many job queue pros without any additional infrastructure or complex management.
|
||||
|
||||
:::info
|
||||
:::info
|
||||
Keep in mind that pg-boss jobs run alongside your other server-side code, so they are not appropriate for CPU-heavy workloads. Additionally, some care is required if you modify scheduled jobs. Please see pg-boss details below for more information.
|
||||
|
||||
<details>
|
||||
@ -654,9 +654,9 @@ console.log(await submittedJob.pgBoss.details())
|
||||
await mySpecialJob.delay(10).submit({ job: "args" })
|
||||
```
|
||||
|
||||
And that is it! Your job will be executed by the job executor (pg-boss, in this case) as if you called `foo({ job: "args" })`.
|
||||
And that is it! Your job will be executed by the job executor (pg-boss, in this case) as if you called `foo({ job: "args" })`.
|
||||
|
||||
Note that in our example, `foo` takes an argument, but this does not always have to be the case. It all depends on how you've implemented your worker function.
|
||||
Note that in our example, `foo` takes an argument, but this does not always have to be the case. It all depends on how you've implemented your worker function.
|
||||
|
||||
### Recurring jobs
|
||||
|
||||
@ -715,7 +715,7 @@ job mySpecialJob {
|
||||
// Can reference context.entities.Task, for example.
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
- ##### `executorOptions: dict` (optional)
|
||||
Executor-specific default options to use when submitting jobs. These are passed directly through and you should consult the documentation for the job executor. These can be overridden during invocation with `submit()` or in a `schedule`.
|
||||
|
||||
@ -723,15 +723,15 @@ job mySpecialJob {
|
||||
See the docs for [pg-boss](https://github.com/timgit/pg-boss/blob/8.0.0/docs/readme.md#sendname-data-options).
|
||||
|
||||
#### `schedule: dict` (optional)
|
||||
|
||||
|
||||
- ##### `cron: string` (required)
|
||||
A 5-placeholder format cron expression string. See rationale for minute-level precision [here](https://github.com/timgit/pg-boss/blob/8.0.0/docs/readme.md#scheduling).
|
||||
|
||||
|
||||
_If you need help building cron expressions, Check out_ <em>[Crontab guru](https://crontab.guru/#0_*_*_*_*).</em>
|
||||
|
||||
|
||||
- ##### `args: JSON` (optional)
|
||||
The arguments to pass to the `perform.fn` function when invoked.
|
||||
|
||||
|
||||
- ##### `executorOptions: dict` (optional)
|
||||
Executor-specific options to use when submitting jobs. These are passed directly through and you should consult the documentation for the job executor. The `perform.executorOptions` are the default options, and `schedule.executorOptions` can override/extend those.
|
||||
|
||||
@ -814,7 +814,7 @@ In the future, we will add support for picking any version you like, but we have
|
||||
|
||||
Wasp provides authentication and authorization support out-of-the-box. Enabling it for your app is optional and can be done by configuring the `auth` field of the `app` declaration:
|
||||
|
||||
```c
|
||||
```c
|
||||
app MyApp {
|
||||
title: "My app",
|
||||
//...
|
||||
@ -855,7 +855,7 @@ Check out this [section of our Todo app tutorial](/docs/tutorials/todo-app/06-au
|
||||
Path where a successfully authenticated user will be sent upon successful login/signup.
|
||||
Default value is "/".
|
||||
|
||||
:::note
|
||||
:::note
|
||||
Automatic redirect on successful login only works when using the Wasp provided [`Signup` and `Login` forms](#high-level-api)
|
||||
:::
|
||||
|
||||
@ -864,7 +864,7 @@ Automatic redirect on successful login only works when using the Wasp provided [
|
||||
`usernameAndPassword` authentication method makes it possible to signup/login into the app by using a username and password.
|
||||
This method requires that `userEntity` specified in `auth` contains `username: string` and `password: string` fields:
|
||||
|
||||
```c
|
||||
```c
|
||||
app MyApp {
|
||||
title: "My app",
|
||||
//...
|
||||
@ -916,10 +916,10 @@ export const signUp = async (args, context) => {
|
||||
// ...
|
||||
|
||||
const newUser = context.entities.User.create({
|
||||
data: {
|
||||
username: args.username,
|
||||
data: {
|
||||
username: args.username,
|
||||
password: args.password // password hashed automatically by Wasp! 🐝
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Your custom code after sign-up.
|
||||
@ -937,8 +937,8 @@ You don't need to worry about hashing the password yourself! Even when you are u
|
||||
To disable/enable default validations, or add your own, you can modify your custom signUp function like so:
|
||||
```js
|
||||
const newUser = context.entities.User.create({
|
||||
data: {
|
||||
username: args.username,
|
||||
data: {
|
||||
username: args.username,
|
||||
password: args.password // password hashed automatically by Wasp! 🐝
|
||||
},
|
||||
_waspSkipDefaultValidations: false, // can be omitted if false (default), or explicitly set to true
|
||||
@ -1133,7 +1133,7 @@ import AuthError from '@wasp/core/AuthError.js'
|
||||
```
|
||||
|
||||
## Social Login Providers (OAuth 2.0)
|
||||
Wasp allows you to easily add social login providers to your app.
|
||||
Wasp allows you to easily add social login providers to your app.
|
||||
|
||||
The following is a list of links to guides that will help you get started with the currently supported providers:
|
||||
- [GitHub](/docs/integrations/github)
|
||||
@ -1150,7 +1150,7 @@ When using Social Login Providers, Wasp gives you the following options:
|
||||
<Tabs>
|
||||
<TabItem value="google" label="Google" default>
|
||||
|
||||
```c
|
||||
```c
|
||||
auth: {
|
||||
userEntity: User,
|
||||
externalAuthEntity: SocialLogin,
|
||||
@ -1159,7 +1159,7 @@ When using Social Login Providers, Wasp gives you the following options:
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
<p>Add <code>google: {}</code> to your <code>auth.methods</code> dictionary to use it with default settings</p>
|
||||
<p>By default, Wasp expects you to set two environment variables in order to use Google authentication:</p>
|
||||
<ul>
|
||||
@ -1266,7 +1266,7 @@ If you would like to allow the user to select their own username, or some other
|
||||
Alternatively, you could add a `displayName` property to your User entity and assign it using the details of their provider account. Below is an example of how to do this by using:
|
||||
- the `getUserFieldsFn` function to configure the user's `username` or `displayName` from their provider account
|
||||
|
||||
We also show you how to customize the configuration of the Provider's settings using:
|
||||
We also show you how to customize the configuration of the Provider's settings using:
|
||||
- the `configFn` function
|
||||
|
||||
```c title=main.wasp {9,10,13,14,26}
|
||||
@ -1304,7 +1304,7 @@ psl=}
|
||||
```
|
||||
|
||||
|
||||
#### `configFn`
|
||||
#### `configFn`
|
||||
|
||||
This function should return an object with the following shape:
|
||||
<Tabs>
|
||||
@ -1377,13 +1377,14 @@ This function should return the user fields to use when creating a new user upon
|
||||
## Client configuration
|
||||
|
||||
You can configure the client using the `client` field inside the `app`
|
||||
declaration,
|
||||
declaration,
|
||||
|
||||
```c
|
||||
app MyApp {
|
||||
title: "My app",
|
||||
// ...
|
||||
client: {
|
||||
rootComponent: import Root from "@client/Root.jsx",
|
||||
setupFn: import mySetupFunction from "@client/myClientSetupCode.js"
|
||||
}
|
||||
}
|
||||
@ -1391,6 +1392,79 @@ app MyApp {
|
||||
|
||||
`app.client` is a dictionary with the following fields:
|
||||
|
||||
#### `rootComponent: ClientImport` (optional)
|
||||
|
||||
`rootComponent` defines the root component of your client application. It is
|
||||
expected to be a React component, and Wasp will use it to wrap your entire app.
|
||||
It must render its children, which are the actual pages of your application.
|
||||
|
||||
You can use it to define a common layout for your application:
|
||||
|
||||
```jsx title="src/client/Root.jsx"
|
||||
export default async function Root({ children }) {
|
||||
return (
|
||||
<div>
|
||||
<header>
|
||||
<h1>My App</h1>
|
||||
</header>
|
||||
{children}
|
||||
<footer>
|
||||
<p>My App footer</p>
|
||||
</footer>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
You can use it to set up various providers that your application needs:
|
||||
|
||||
```jsx title="src/client/Root.jsx"
|
||||
import store from './store'
|
||||
import { Provider } from 'react-redux'
|
||||
|
||||
export default async function Root({ children }) {
|
||||
return (
|
||||
<Provider store={store}>
|
||||
{children}
|
||||
</Provider>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
As long as you render the children, you can do whatever you want in your root
|
||||
component. Here's an example of a root component both sets up a provider and
|
||||
renders a custom layout:
|
||||
|
||||
```jsx title="src/client/Root.jsx"
|
||||
import store from './store'
|
||||
import { Provider } from 'react-redux'
|
||||
|
||||
export default function Root({ children }) {
|
||||
return (
|
||||
<Provider store={store}>
|
||||
<Layout>
|
||||
{children}
|
||||
</Layout>
|
||||
</Provider>
|
||||
)
|
||||
}
|
||||
|
||||
function Layout({ children }) {
|
||||
return (
|
||||
<div>
|
||||
<header>
|
||||
<h1>My App</h1>
|
||||
</header>
|
||||
{children}
|
||||
<footer>
|
||||
<p>My App footer</p>
|
||||
</footer>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
#### `setupFn: ClientImport` (optional)
|
||||
|
||||
`setupFn` declares a JavaScript function that Wasp executes on the client
|
||||
|
Loading…
Reference in New Issue
Block a user