mirror of
https://github.com/digital-asset/daml.git
synced 2024-09-20 01:07:18 +03:00
Include create-daml-app as a template project for daml new (#5259)
* Initial commit with create-daml-app master * Include create-daml-app in build rule * Make daml.yaml a template in version and project name * Remove git attributes * Remove license and azure config * Remove scripts * Don't overwrite config files in build rule * Template version numbers in package.json, to be replaced by the assistant * Rename to package.json.template changelog_begin changelog_end * Add copyright headers * Do template substitutions in all .template files And don't special case daml new create-daml-app (so it treats it as a regular template). * Add create-daml-app to integration tests * Remove WIP warning * Move towards setup that works on head * Make local copies of the TS libs in the templates tarball * Hardcode project name for now * Use isExtensionOf * Remove service worker * remove robots.txt (don't even know what it is) * Revert "Make local copies of the TS libs in the templates tarball" This reverts commit 1289581fb4a82af3ab534baf978a2c6ed895d538. * Retemplatize TS lib versions. For head builds these will be installed using npm * Remove daml/ledger from resolutions for daml-ts * Comment about test secret * Remove special create-daml-app assistant command and test that won't work anymore * Remove redundant imports and export * Remove old create-daml-app tests * Remove yarn.lock * Clean up integration test (just daml new and build atm) * Add daml/ledger as a resolution for daml-ts * Remove top level package.json * Update daml.js version * Use new import scheme for generated TS * Update readme with new codegen and build steps * Use start-navigator in daml.yaml * Increase a couple of timeouts in tests (either sandbox or TS lib is a bit slower?) * Update GSG intro with new build steps * Remove daml2ts -p flag and --start-navigator flag from GSG instructions * Don't use start-navigator flag in ui tests * Temporary readme describing how to manually test the create-daml-app template * Update code samples in app arch section of GSG * Update code samples in testing doc * Remove copied create-daml-app code * Indent docs markers to be more subtle * Update visible code in Messages (after) section This needs to be kept up to date properly somehow. * Update text to useLedger * Restore code/ui-before copies until the Bazel magic is figured out We need to make the template code a dependency in the Bazel rule as otherwise we can't find the files in the docs build. * Update create-daml-app/readme and make templates/readme more detailed * Use jsx comments for docs markers so they don't show up in the app
This commit is contained in:
parent
3945708192
commit
0f5d93e0c3
@ -92,28 +92,3 @@ da_haskell_test(
|
||||
"//libs-haskell/test-utils",
|
||||
],
|
||||
)
|
||||
|
||||
# Misc tests for daml-helper that do not deserve their own test suite.
|
||||
da_haskell_test(
|
||||
name = "tests",
|
||||
srcs = ["test/DA/Daml/Helper/Test.hs"],
|
||||
data = [
|
||||
"daml-helper",
|
||||
],
|
||||
hackage_deps = [
|
||||
"base",
|
||||
"directory",
|
||||
"extra",
|
||||
"filepath",
|
||||
"process",
|
||||
"tasty",
|
||||
"tasty-hunit",
|
||||
],
|
||||
main_function = "DA.Daml.Helper.Test.main",
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
"//libs-haskell/bazel-runfiles",
|
||||
"//libs-haskell/da-hs-base",
|
||||
"//libs-haskell/test-utils",
|
||||
],
|
||||
)
|
||||
|
@ -319,10 +319,8 @@ runCommand = \case
|
||||
RunJar {..} ->
|
||||
(if shutdownStdinClose then withCloseOnStdin else id) $
|
||||
runJar jarPath mbLogbackConfig remainingArguments
|
||||
New {..}
|
||||
| templateNameM == Just "create-daml-app" -> runCreateDamlApp targetFolder
|
||||
| otherwise -> runNew targetFolder templateNameM
|
||||
CreateDamlApp{..} -> runCreateDamlApp targetFolder
|
||||
New {..} -> runNew targetFolder templateNameM
|
||||
CreateDamlApp{..} -> runNew targetFolder (Just "create-daml-app")
|
||||
Init {..} -> runInit targetFolderM
|
||||
ListTemplates -> runListTemplates
|
||||
Start {..} ->
|
||||
|
@ -5,7 +5,6 @@ module DA.Daml.Helper.Run
|
||||
( runDamlStudio
|
||||
, runInit
|
||||
, runNew
|
||||
, runCreateDamlApp
|
||||
, runJar
|
||||
, runDaml2ts
|
||||
, runListTemplates
|
||||
@ -51,20 +50,14 @@ import Control.Concurrent
|
||||
import Control.Concurrent.Async
|
||||
import Control.Exception.Safe
|
||||
import Control.Monad
|
||||
import Control.Monad.IO.Class
|
||||
import Control.Monad.Extra hiding (fromMaybeM)
|
||||
import Control.Monad.Loops (untilJust)
|
||||
import Data.Conduit (runConduitRes, (.|))
|
||||
import Data.Conduit.Combinators (sinkHandle)
|
||||
import qualified Data.Conduit.Tar.Extra as Tar
|
||||
import qualified Data.Conduit.Zlib as Zlib
|
||||
import Data.Foldable
|
||||
import qualified Data.HashMap.Strict as HashMap
|
||||
import Data.Maybe
|
||||
import qualified Data.Map.Strict as Map
|
||||
import Data.List.Extra
|
||||
import qualified Data.ByteString as BS
|
||||
import qualified Data.ByteString.Char8 as BSChar8
|
||||
import qualified Data.ByteString.Lazy.UTF8 as UTF8
|
||||
import DA.PortFile
|
||||
import qualified Data.Text as T
|
||||
@ -545,67 +538,24 @@ runNew targetFolder templateNameM = do
|
||||
files <- listFilesRecursive targetFolder
|
||||
mapM_ setWritable files
|
||||
|
||||
-- Update daml.yaml
|
||||
let configPath = targetFolder </> projectConfigName
|
||||
configTemplatePath = configPath <.> "template"
|
||||
|
||||
whenM (doesFileExist configTemplatePath) $ do
|
||||
configTemplate <- readFileUTF8 configTemplatePath
|
||||
-- Substitute strings in template files (not a DAML template!)
|
||||
-- e.g. the SDK version numbers in daml.yaml and package.json
|
||||
let templateFiles = filter (".template" `isExtensionOf`) files
|
||||
forM_ templateFiles $ \templateFile -> do
|
||||
templateContent <- readFileUTF8 templateFile
|
||||
sdkVersion <- getSdkVersion
|
||||
let config = replace "__VERSION__" sdkVersion
|
||||
. replace "__PROJECT_NAME__" projectName
|
||||
$ configTemplate
|
||||
writeFileUTF8 configPath config
|
||||
removeFile configTemplatePath
|
||||
let content = replace "__VERSION__" sdkVersion
|
||||
. replace "__PROJECT_NAME__" projectName
|
||||
$ templateContent
|
||||
realFile = dropExtension templateFile
|
||||
writeFileUTF8 realFile content
|
||||
removeFile templateFile
|
||||
|
||||
-- Done.
|
||||
putStrLn $
|
||||
"Created a new project in \"" <> targetFolder <>
|
||||
"\" based on the template \"" <> templateName <> "\"."
|
||||
|
||||
runCreateDamlApp :: FilePath -> IO ()
|
||||
runCreateDamlApp targetFolder = do
|
||||
whenM (doesDirectoryExist targetFolder) $ do
|
||||
hPutStr stderr $ unlines
|
||||
[ "Directory " <> show targetFolder <> " already exists."
|
||||
, "Please specify a new directory or delete the directory."
|
||||
]
|
||||
exitFailure
|
||||
|
||||
sdkVersion <- getSdkVersion
|
||||
request <- HTTP.parseRequest ("GET " <> url sdkVersion)
|
||||
HTTP.withResponse request $ \response -> do
|
||||
if | HTTP.getResponseStatus response == HTTP.notFound404 -> do
|
||||
-- We treat 404s specially to provide a better error message.
|
||||
hPutStrLn stderr $ unlines
|
||||
[ "create-daml-app is not available for SDK version " <> sdkVersion <> "."
|
||||
, "You need to use at least SDK version 1.0. If this is a new release,"
|
||||
, "try again in a few hours."
|
||||
]
|
||||
exitFailure
|
||||
| not (HTTP.statusIsSuccessful $ HTTP.getResponseStatus response) -> do
|
||||
hPutStrLn stderr $ unlines
|
||||
[ "Failed to download create-daml-app from " <> show (url sdkVersion) <> "."
|
||||
, "Verify that your network is working and that you can"
|
||||
, "access https://github.com/digital-asset/create-daml-app"
|
||||
]
|
||||
hPrint stderr (HTTP.getResponseStatus response)
|
||||
runConduitRes (HTTP.getResponseBody response .| sinkHandle stderr )
|
||||
-- trailing newline
|
||||
BSChar8.hPutStrLn stderr ""
|
||||
exitFailure
|
||||
| otherwise -> do
|
||||
-- Successful request so now extract it to the target folder.
|
||||
let extractError msg e = liftIO $ fail $
|
||||
"Failed to extract tarball: " <> T.unpack msg <> ": " <> T.unpack e
|
||||
runConduitRes $
|
||||
HTTP.getResponseBody response
|
||||
.| Zlib.ungzip
|
||||
.| Tar.untar (Tar.restoreFile extractError targetFolder)
|
||||
putStrLn $ "Created a new DAML app in " <> show targetFolder <> "."
|
||||
where
|
||||
url version = "https://github.com/digital-asset/create-daml-app/archive/v" <> version <> ".tar.gz"
|
||||
|
||||
defaultProjectTemplate :: String
|
||||
defaultProjectTemplate = "skeleton"
|
||||
|
||||
|
@ -1,66 +0,0 @@
|
||||
-- Copyright (c) 2020 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
-- SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
module DA.Daml.Helper.Test (main) where
|
||||
|
||||
import Control.Monad
|
||||
import DA.Bazel.Runfiles
|
||||
import DA.Test.Util
|
||||
import System.Directory
|
||||
import System.Environment.Blank
|
||||
import System.Exit
|
||||
import System.FilePath
|
||||
import System.Info
|
||||
import System.IO.Extra
|
||||
import System.Process
|
||||
import Test.Tasty
|
||||
import Test.Tasty.HUnit
|
||||
|
||||
main :: IO ()
|
||||
main = do
|
||||
setEnv "TASTY_NUM_THREADS" "1" True
|
||||
when (os == "darwin") $ do
|
||||
-- x509-system insists on trying to locate `security`
|
||||
-- in PATH to find the root certificate store.
|
||||
mbPath <- getEnv "PATH"
|
||||
setEnv "PATH" (maybe "/usr/bin" ("/usr/bin:" <>) mbPath) True
|
||||
damlHelper <- locateRunfiles (mainWorkspace </> "daml-assistant" </> "daml-helper" </> exe "daml-helper")
|
||||
defaultMain $
|
||||
testGroup "daml-helper"
|
||||
[ createDamlAppTests damlHelper
|
||||
]
|
||||
|
||||
createDamlAppTests :: FilePath -> TestTree
|
||||
createDamlAppTests damlHelper = testGroup "create-daml-app"
|
||||
[ testCase "Succeeds with SDK 0.13.55" $ withTempDir $ \dir -> do
|
||||
env <- getEnvironment
|
||||
(exit, out, err) <- readCreateProcessWithExitCode
|
||||
(proc damlHelper ["create-daml-app", dir </> "foobar"])
|
||||
{ env = Just (("DAML_SDK_VERSION", "0.13.55") : env) }
|
||||
""
|
||||
err @?= ""
|
||||
assertInfixOf "Created" out
|
||||
exit @?= ExitSuccess
|
||||
assertBool "daml.yaml does not exist" =<<
|
||||
doesFileExist (dir </> "foobar" </> "daml.yaml")
|
||||
, testCase "Fails with SDK 0.0.1" $ withTempDir $ \dir -> do
|
||||
-- Note that we do not test 0.0.0 since people
|
||||
-- might be tempted to create that tag temporarily for
|
||||
-- testing purposes.
|
||||
env <- getEnvironment
|
||||
(exit, out, err) <- readCreateProcessWithExitCode
|
||||
(proc damlHelper ["create-daml-app", dir </> "foobar"])
|
||||
{ env = Just (("DAML_SDK_VERSION", "0.0.1") : env) }
|
||||
""
|
||||
assertInfixOf "not available for SDK version 0.0.1" err
|
||||
out @?= ""
|
||||
exit @?= ExitFailure 1
|
||||
, testCase "Fails if directory already exists" $ withTempDir $ \dir -> do
|
||||
createDirectory (dir </> "foobar")
|
||||
(exit, out, err) <- readCreateProcessWithExitCode
|
||||
(proc damlHelper ["create-daml-app", dir </> "foobar"])
|
||||
""
|
||||
assertInfixOf "already exists" err
|
||||
out @?= ""
|
||||
exit @?= ExitFailure 1
|
||||
]
|
@ -329,7 +329,7 @@ quickstartTests :: FilePath -> FilePath -> TestTree
|
||||
quickstartTests quickstartDir mvnDir = testGroup "quickstart"
|
||||
[ testCase "daml new" $
|
||||
callCommandQuiet $ unwords ["daml", "new", quickstartDir, "quickstart-java"]
|
||||
, testCase "daml build " $ withCurrentDirectory quickstartDir $
|
||||
, testCase "daml build" $ withCurrentDirectory quickstartDir $
|
||||
callCommandQuiet "daml build"
|
||||
, testCase "daml test" $ withCurrentDirectory quickstartDir $
|
||||
callCommandQuiet "daml test"
|
||||
@ -512,6 +512,7 @@ templateTests = testGroup "templates"
|
||||
, "quickstart-scala"
|
||||
, "script-example"
|
||||
, "skeleton"
|
||||
, "create-daml-app"
|
||||
]
|
||||
|
||||
-- | Check we can generate language bindings.
|
||||
|
@ -61,8 +61,8 @@ The last part of the DAML model is the operation to follow users, called a *choi
|
||||
|
||||
.. literalinclude:: code/daml/User.daml
|
||||
:language: daml
|
||||
:start-after: -- ADDFRIEND_BEGIN
|
||||
:end-before: -- ADDFRIEND_END
|
||||
:start-after: -- FOLLOW_BEGIN
|
||||
:end-before: -- FOLLOW_END
|
||||
|
||||
DAML contracts are *immutable* (can not be changed in place), so the only way to "update" one is to archive it and create a new instance.
|
||||
That is what the ``Follow`` choice does: after checking some preconditions, it archives the current user contract and creates a new one with the new user to follow added to the list. Here is a quick explanation of the code:
|
||||
@ -90,11 +90,10 @@ We do this using a DAML to TypeScript code generation tool in the DAML SDK.
|
||||
|
||||
To run code generation, we first need to compile the DAML model to an archive format (a ``.dar`` file).
|
||||
The ``daml codegen ts`` command then takes this file as argument to produce a number of TypeScript packages in the output folder.
|
||||
It also updates our ``package.json`` file with the newly generated dependencies.
|
||||
::
|
||||
|
||||
daml build
|
||||
daml codegen ts .daml/dist/create-daml-app-0.1.0.dar -o daml-ts -p package.json
|
||||
daml codegen ts .daml/dist/create-daml-app-0.1.0.dar -o daml-ts
|
||||
|
||||
Now we have a TypeScript interface (types and companion objects) to our DAML model, which we'll use in our UI code next.
|
||||
|
||||
@ -136,7 +135,7 @@ It uses DAML React hooks to query and update ledger state.
|
||||
The ``useParty`` hook simply returns the current user as stored in the ``DamlLedger`` context.
|
||||
A more interesting example is the ``allUsers`` line.
|
||||
This uses the ``useStreamQuery`` hook to get all ``User`` contracts on the ledger.
|
||||
(``User`` here is an object generated by ``daml codegen ts`` - it stores metadata of the ``User`` template defined in ``User.daml``.)
|
||||
(``User.User`` here is an object generated by ``daml codegen ts`` - it stores metadata of the ``User`` template defined in ``User.daml``.)
|
||||
Note however that this query preserves privacy: only users that follow the current user have their contracts revealed.
|
||||
This behaviour is due to the observers on the ``User`` contract being exactly in the list of users that the current user is following.
|
||||
|
||||
@ -150,7 +149,8 @@ Another example, showing how to *update* ledger state, is how we exercise the ``
|
||||
:start-after: // FOLLOW_BEGIN
|
||||
:end-before: // FOLLOW_END
|
||||
|
||||
The ``useExerciseByKey`` hook returns the ``exerciseFollow`` function.
|
||||
The ``useLedger`` hook returns an object with methods for exercising choices.
|
||||
The core of the ``follow`` function here is the call to ``ledger.exerciseByKey``.
|
||||
The *key* in this case is the username of the current user, used to look up the corresponding ``User`` contract.
|
||||
The wrapper function ``follow`` is then passed to the subcomponents of ``MainView``.
|
||||
For example, ``follow`` is passed to the ``UserList`` component as an argument (a `prop <https://reactjs.org/docs/components-and-props.html>`_ in React terms).
|
||||
|
@ -1,7 +1,6 @@
|
||||
-- Copyright (c) 2020 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
-- SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
|
||||
module User where
|
||||
|
||||
-- MAIN_TEMPLATE_BEGIN
|
||||
@ -16,7 +15,7 @@ template User with
|
||||
key username: Party
|
||||
maintainer key
|
||||
|
||||
-- ADDFRIEND_BEGIN
|
||||
-- FOLLOW_BEGIN
|
||||
nonconsuming choice Follow: ContractId User with
|
||||
userToFollow: Party
|
||||
controller username
|
||||
@ -25,9 +24,9 @@ template User with
|
||||
assertMsg "You cannot follow the same user twice" (userToFollow `notElem` following)
|
||||
archive self
|
||||
create this with following = userToFollow :: following
|
||||
-- ADDFRIEND_END
|
||||
-- FOLLOW_END
|
||||
|
||||
-- SENDMESSAGE_BEGIN
|
||||
-- SENDMESSAGE_BEGIN
|
||||
nonconsuming choice SendMessage: ContractId Message with
|
||||
sender: Party
|
||||
content: Text
|
||||
@ -35,7 +34,7 @@ template User with
|
||||
do
|
||||
assertMsg "Designated user must follow you back to send a message" (elem sender following)
|
||||
create Message with sender, receiver = username, content
|
||||
-- SENDMESSAGE_END
|
||||
-- SENDMESSAGE_END
|
||||
|
||||
-- MESSAGE_BEGIN
|
||||
template Message with
|
||||
|
@ -76,7 +76,7 @@ const MainView: React.FC = () => {
|
||||
onFollow={follow}
|
||||
/>
|
||||
</Segment>
|
||||
// MESSAGES_SEGMENT_BEGIN
|
||||
// MESSAGES_SEGMENT_BEGIN
|
||||
<Segment>
|
||||
<Header as='h2'>
|
||||
<Icon name='pencil square' />
|
||||
@ -91,7 +91,7 @@ const MainView: React.FC = () => {
|
||||
<Divider />
|
||||
<MessageList />
|
||||
</Segment>
|
||||
// MESSAGES_SEGMENT_END
|
||||
// MESSAGES_SEGMENT_END
|
||||
</Grid.Column>
|
||||
</Grid.Row>
|
||||
</Grid>
|
||||
|
@ -5,8 +5,8 @@
|
||||
import React from 'react'
|
||||
import { Form, Button } from 'semantic-ui-react';
|
||||
import { Party } from '@daml/types';
|
||||
import { User } from '@daml-ts/create-daml-app-0.1.0/lib/User';
|
||||
import { useParty, useExerciseByKey } from '@daml/react';
|
||||
import { User } from '@daml.js/create-daml-app-0.1.0';
|
||||
import { useParty, useLedger } from '@daml/react';
|
||||
|
||||
type Props = {
|
||||
followers: Party[];
|
||||
@ -20,7 +20,7 @@ const MessageEdit: React.FC<Props> = ({followers}) => {
|
||||
const [receiver, setReceiver] = React.useState<string | undefined>();
|
||||
const [content, setContent] = React.useState("");
|
||||
const [isSubmitting, setIsSubmitting] = React.useState(false);
|
||||
const exerciseSendMessage = useExerciseByKey(User.SendMessage);
|
||||
const ledger = useLedger();
|
||||
|
||||
const submitMessage = async (event: React.FormEvent) => {
|
||||
try {
|
||||
@ -29,7 +29,7 @@ const MessageEdit: React.FC<Props> = ({followers}) => {
|
||||
return;
|
||||
}
|
||||
setIsSubmitting(true);
|
||||
await exerciseSendMessage(receiver, {sender, content});
|
||||
await ledger.exerciseByKey(User.User.SendMessage, receiver, {sender, content});
|
||||
setContent("");
|
||||
} catch (error) {
|
||||
alert(`Error sending message:\n${JSON.stringify(error)}`);
|
||||
|
@ -4,14 +4,14 @@
|
||||
// MESSAGELIST_BEGIN
|
||||
import React from 'react'
|
||||
import { List, ListItem } from 'semantic-ui-react';
|
||||
import { Message } from '@daml-ts/create-daml-app-0.1.0/lib/User';
|
||||
import { Message } from '@daml.js/create-daml-app-0.1.0';
|
||||
import { useStreamQuery } from '@daml/react';
|
||||
|
||||
/**
|
||||
* React component displaying the list of messages for the current user.
|
||||
*/
|
||||
const MessageList: React.FC = () => {
|
||||
const messagesResult = useStreamQuery(Message);
|
||||
const messagesResult = useStreamQuery(User.Message);
|
||||
|
||||
return (
|
||||
<List relaxed>
|
||||
|
@ -5,7 +5,7 @@ import React, { useCallback } from 'react'
|
||||
import { Button, Form, Grid, Header, Image, Segment } from 'semantic-ui-react'
|
||||
import Credentials, { computeCredentials } from '../Credentials';
|
||||
import Ledger from '@daml/ledger';
|
||||
import { User } from '@daml-ts/create-daml-app-0.1.0/lib/User';
|
||||
import { User } from '@daml.js/create-daml-app-0.1.0';
|
||||
import { DeploymentMode, deploymentMode, ledgerId, httpBaseUrl, wsBaseUrl } from '../config';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
@ -22,10 +22,10 @@ const LoginScreen: React.FC<Props> = ({onLogin}) => {
|
||||
const login = useCallback(async (credentials: Credentials) => {
|
||||
try {
|
||||
const ledger = new Ledger({token: credentials.token, httpBaseUrl, wsBaseUrl});
|
||||
let userContract = await ledger.lookupByKey(User, credentials.party);
|
||||
let userContract = await ledger.fetchByKey(User.User, credentials.party);
|
||||
if (userContract === null) {
|
||||
const user = {username: credentials.party, following: []};
|
||||
userContract = await ledger.create(User, user);
|
||||
userContract = await ledger.create(User.User, user);
|
||||
}
|
||||
onLogin(credentials);
|
||||
} catch(error) {
|
||||
@ -81,7 +81,7 @@ const LoginScreen: React.FC<Props> = ({onLogin}) => {
|
||||
<Segment>
|
||||
{deploymentMode !== DeploymentMode.PROD_DABL
|
||||
? <>
|
||||
// BEGIN_FORM
|
||||
// FORM_BEGIN
|
||||
<Form.Input
|
||||
fluid
|
||||
icon='user'
|
||||
@ -98,7 +98,7 @@ const LoginScreen: React.FC<Props> = ({onLogin}) => {
|
||||
onClick={handleLogin}>
|
||||
Log in
|
||||
</Button>
|
||||
// END_FORM
|
||||
// FORM_END
|
||||
</>
|
||||
: <Button primary fluid onClick={handleDablLogin}>
|
||||
Log in with DABL
|
||||
|
@ -4,17 +4,17 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { Container, Grid, Header, Icon, Segment, Divider } from 'semantic-ui-react';
|
||||
import { Party } from '@daml/types';
|
||||
import { User } from '@daml-ts/create-daml-app-0.1.0/lib/User';
|
||||
import { useParty, useExerciseByKey, useStreamFetchByKey, useStreamQuery } from '@daml/react';
|
||||
import { User } from '@daml.js/create-daml-app-0.1.0';
|
||||
import { useParty, useLedger, useStreamFetchByKey, useStreamQuery } from '@daml/react';
|
||||
import UserList from './UserList';
|
||||
import PartyListEdit from './PartyListEdit';
|
||||
|
||||
// USERS_BEGIN
|
||||
const MainView: React.FC = () => {
|
||||
const username = useParty();
|
||||
const myUserResult = useStreamFetchByKey(User, () => username, [username]);
|
||||
const myUserResult = useStreamFetchByKey(User.User, () => username, [username]);
|
||||
const myUser = myUserResult.contract?.payload;
|
||||
const allUsers = useStreamQuery(User).contracts;
|
||||
const allUsers = useStreamQuery(User.User).contracts;
|
||||
// USERS_END
|
||||
|
||||
// Sorted list of users that are following the current user
|
||||
@ -25,19 +25,19 @@ const MainView: React.FC = () => {
|
||||
.sort((x, y) => x.username.localeCompare(y.username)),
|
||||
[allUsers, username]);
|
||||
|
||||
// FOLLOW_BEGIN
|
||||
const exerciseFollow = useExerciseByKey(User.Follow);
|
||||
// FOLLOW_BEGIN
|
||||
const ledger = useLedger();
|
||||
|
||||
const follow = async (userToFollow: Party): Promise<boolean> => {
|
||||
try {
|
||||
await exerciseFollow(username, {userToFollow});
|
||||
await ledger.exerciseByKey(User.User.Follow, username, {userToFollow});
|
||||
return true;
|
||||
} catch (error) {
|
||||
alert("Unknown error:\n" + JSON.stringify(error));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// FOLLOW_END
|
||||
// FOLLOW_END
|
||||
|
||||
return (
|
||||
<Container>
|
||||
@ -71,12 +71,12 @@ const MainView: React.FC = () => {
|
||||
</Header.Content>
|
||||
</Header>
|
||||
<Divider />
|
||||
// USERLIST_BEGIN
|
||||
// USERLIST_BEGIN
|
||||
<UserList
|
||||
users={followers}
|
||||
onFollow={follow}
|
||||
/>
|
||||
// USERLIST_END
|
||||
// USERLIST_END
|
||||
</Segment>
|
||||
</Grid.Column>
|
||||
</Grid.Row>
|
||||
|
@ -5,7 +5,7 @@ import { ChildProcess, spawn, SpawnOptions } from 'child_process';
|
||||
import waitOn from 'wait-on';
|
||||
|
||||
import Ledger from '@daml/ledger';
|
||||
import { User } from '@daml-ts/create-daml-app-0.1.0/lib/User';
|
||||
import { User } from '@daml.js/create-daml-app-0.1.0';
|
||||
import { computeCredentials } from './Credentials';
|
||||
|
||||
import puppeteer, { Browser, Page } from 'puppeteer';
|
||||
@ -41,11 +41,11 @@ test('Party names are unique', async () => {
|
||||
// Use a single sandbox, JSON API server and browser for all tests for speed.
|
||||
// This means we need to use a different set of parties and a new browser page for each test.
|
||||
beforeAll(async () => {
|
||||
// Run `daml start --start-navigator=no` to start up the sandbox and json api server.
|
||||
// Run `daml start` to start up the sandbox and json api server.
|
||||
// Run it from the repository root, where the `daml.yaml` lives.
|
||||
// The path should already include '.daml/bin' in the environment where this is run.
|
||||
const startOpts: SpawnOptions = { cwd: '..', stdio: 'inherit' };
|
||||
startProc = spawn('daml', ['start', '--start-navigator=no'], startOpts);
|
||||
startProc = spawn('daml', ['start'], startOpts);
|
||||
|
||||
// Run `yarn start` in another shell.
|
||||
// Disable automatically opening a browser using the env var described here:
|
||||
@ -93,13 +93,13 @@ test('create and look up user using ledger library', async () => {
|
||||
const partyName = getParty();
|
||||
const {party, token} = computeCredentials(partyName);
|
||||
const ledger = new Ledger({token});
|
||||
const users0 = await ledger.query(User);
|
||||
const users0 = await ledger.query(User.User);
|
||||
expect(users0).toEqual([]);
|
||||
const user: User = {username: party, following: []};
|
||||
const userContract1 = await ledger.create(User, user);
|
||||
const userContract2 = await ledger.lookupByKey(User, party);
|
||||
const user = {username: party, following: []};
|
||||
const userContract1 = await ledger.create(User.User, user);
|
||||
const userContract2 = await ledger.fetchByKey(User.User, party);
|
||||
expect(userContract1).toEqual(userContract2);
|
||||
const users = await ledger.query(User);
|
||||
const users = await ledger.query(User.User);
|
||||
expect(users[0]).toEqual(userContract1);
|
||||
});
|
||||
|
||||
@ -117,7 +117,7 @@ const newUiPage = async (): Promise<Page> => {
|
||||
return page;
|
||||
}
|
||||
|
||||
// BEGIN_LOGIN_FUNCTION
|
||||
// LOGIN_FUNCTION_BEGIN
|
||||
// Log in using a party name and wait for the main screen to load.
|
||||
const login = async (page: Page, partyName: string) => {
|
||||
const usernameInput = await page.waitForSelector('.test-select-username-field');
|
||||
@ -126,7 +126,7 @@ const login = async (page: Page, partyName: string) => {
|
||||
await page.click('.test-select-login-button');
|
||||
await page.waitForSelector('.test-select-main-menu');
|
||||
}
|
||||
// END_LOGIN_FUNCTION
|
||||
// LOGIN_FUNCTION_END
|
||||
|
||||
// Log out and wait to get back to the login screen.
|
||||
const logout = async (page: Page) => {
|
||||
@ -145,10 +145,10 @@ const follow = async (page: Page, userToFollow: string) => {
|
||||
// We check this by the absence of the `loading` class.
|
||||
// (Both the `test-...` and `loading` classes appear in `div`s surrounding
|
||||
// the `input`, due to the translation of Semantic UI's `Input` element.)
|
||||
await page.waitForSelector('.test-select-follow-input > :not(.loading)');
|
||||
await page.waitForSelector('.test-select-follow-input > :not(.loading)', {timeout: 40_000});
|
||||
}
|
||||
|
||||
// BEGIN_LOGIN_TEST
|
||||
// LOGIN_TEST_BEGIN
|
||||
test('log in as a new user, log out and log back in', async () => {
|
||||
const partyName = getParty();
|
||||
|
||||
@ -159,7 +159,7 @@ test('log in as a new user, log out and log back in', async () => {
|
||||
// Check that the ledger contains the new User contract.
|
||||
const {token} = computeCredentials(partyName);
|
||||
const ledger = new Ledger({token});
|
||||
const users = await ledger.query(User);
|
||||
const users = await ledger.query(User.User);
|
||||
expect(users).toHaveLength(1);
|
||||
expect(users[0].payload.username).toEqual(partyName);
|
||||
|
||||
@ -168,13 +168,13 @@ test('log in as a new user, log out and log back in', async () => {
|
||||
await login(page, partyName);
|
||||
|
||||
// Check we have the same one user.
|
||||
const usersFinal = await ledger.query(User);
|
||||
const usersFinal = await ledger.query(User.User);
|
||||
expect(usersFinal).toHaveLength(1);
|
||||
expect(usersFinal[0].payload.username).toEqual(partyName);
|
||||
|
||||
await page.close();
|
||||
}, 10_000);
|
||||
// END_LOGIN_TEST
|
||||
// LOGIN_TEST_END
|
||||
|
||||
// This tests following users in a few different ways:
|
||||
// - using the text box in the Follow panel
|
||||
@ -268,7 +268,7 @@ test('log in as three different users and start following each other', async ()
|
||||
await page1.close();
|
||||
await page2.close();
|
||||
await page3.close();
|
||||
}, 30_000);
|
||||
}, 40_000);
|
||||
|
||||
test('error when following self', async () => {
|
||||
const party = getParty();
|
||||
|
@ -21,7 +21,7 @@ There are three parts to building and running the messaging feature:
|
||||
|
||||
1. Adding the necessary changes to the DAML model
|
||||
2. Making the corresponding changes in the UI
|
||||
3. Running the new feature. In order to do that we need to terminate the previous ``daml start --start-navigator=no`` process and run it again.
|
||||
3. Running the app with the new feature.
|
||||
|
||||
As usual, we must start with the DAML model and base our UI changes on top of that.
|
||||
|
||||
@ -74,14 +74,8 @@ Since we have changed our DAML code, we also need to rerun the TypeScript code g
|
||||
Open a new terminal and run the following commands::
|
||||
|
||||
daml build
|
||||
daml codegen ts .daml/dist/create-daml-app-0.1.0.dar -o daml-ts -p package.json
|
||||
daml codegen ts .daml/dist/create-daml-app-0.1.0.dar -o daml-ts
|
||||
|
||||
Since we've generated new TypeScript packages in the ``daml-ts`` folder, we need to rebuild our project using these dependencies.
|
||||
From the root ``create-daml-app`` folder, run::
|
||||
|
||||
yarn workspaces run build
|
||||
|
||||
This may take a couple of minutes.
|
||||
The result is an up-to-date TypeScript interface to our DAML model, in particular to the new ``Message`` template and ``SendMessage`` choice.
|
||||
|
||||
We can now implement our messaging feature in the UI!
|
||||
@ -142,8 +136,8 @@ The prop will be passed down from the ``MainView`` component, reusing the work r
|
||||
You can see this ``following`` field bound at the start of the ``MessageEdit`` component.
|
||||
|
||||
We use the React ``useState`` hook to get and set the current choices of message ``receiver`` and ``content``.
|
||||
The DAML-specific ``useExerciseByKey`` hook gives us a function to both look up a ``User`` contract and exercise the ``SendMessage`` choice on it.
|
||||
The call to ``exerciseSendMessage`` in ``sendMessage`` looks up the ``User`` contract with the receiver's username and exercises ``SendMessage`` with the appropriate arguments.
|
||||
The DAML-specific ``useLedger`` hook gives us an object we can use to perform ledger operations.
|
||||
The call to ``ledger.exerciseByKey`` in ``sendMessage`` looks up the ``User`` contract with the receiver's username and exercises ``SendMessage`` with the appropriate arguments.
|
||||
The ``sendMessage`` wrapper reports potential errors to the user, and ``submitMessage`` additionally uses the ``isSubmitting`` state to ensure message requests are processed one at a time.
|
||||
The result of a successful call to ``submitMessage`` is a new ``Message`` contract created on the ledger.
|
||||
|
||||
@ -182,13 +176,15 @@ Let's give the new functionality a spin.
|
||||
Running the New Feature
|
||||
=======================
|
||||
|
||||
We need to terminate the previous ``daml start --start-navigator=no`` process and run it again, as we need to have a Sandbox instance with a DAR file containing the new feature. As a reminder, by running ``daml start --start-navigator=no`` again we will
|
||||
We need to terminate the previous ``daml start`` process and run it again, as we need to have a Sandbox instance with a DAR file containing the new feature. As a reminder, by running ``daml start`` again we will
|
||||
|
||||
- Compile our DAML code into a *DAR file containing the new feature*
|
||||
- Run a fresh instance of the *Sandbox with the new DAR file*
|
||||
- Start the HTTP JSON API
|
||||
|
||||
First, navigate to the terminal window where the ``daml start --start-navigator=no`` process is running and terminate the active process by hitting ``Ctrl-C``. This shuts down the previous instances of the sandbox. Next in the root ``create-daml-app`` folder run ``daml start --start-navigator=no``.
|
||||
First, navigate to the terminal window where the ``daml start`` process is running and terminate the active process by hitting ``Ctrl-C``.
|
||||
This shuts down the previous instances of the sandbox.
|
||||
Then in the root ``create-daml-app`` folder run ``daml start``.
|
||||
|
||||
As mentioned at the beginning of this *Getting Started with DAML* guide, DAML Sandbox uses an in-memory store, which means it loses its state when stopped or restarted. That means that all user data and follower relationships are lost.
|
||||
|
||||
|
@ -51,9 +51,14 @@ Running the app
|
||||
|
||||
We'll start by getting the app up and running, and then explain the different components which we will later extend.
|
||||
|
||||
First off, open a terminal, clone the template project and move to the project folder::
|
||||
First off, open a terminal and instantiate the template project.
|
||||
::
|
||||
|
||||
daml new create-daml-app
|
||||
|
||||
This creates a new folder with contents from our template.
|
||||
Change to the new folder::
|
||||
|
||||
git clone https://github.com/digital-asset/create-daml-app.git
|
||||
cd create-daml-app
|
||||
|
||||
Next we need to compile the DAML code to a DAR file::
|
||||
@ -65,12 +70,13 @@ Once the DAR file is created you will see this message in terminal ``Created .da
|
||||
Any commands starting with ``daml`` are using the :doc:`DAML Assistant </tools/assistant>`, a command line tool in the DAML SDK for building and running DAML apps.
|
||||
In order to connect the UI code to this DAML, we need to run a code generation step::
|
||||
|
||||
daml codegen ts .daml/dist/create-daml-app-0.1.0.dar -o daml-ts -p package.json
|
||||
daml codegen ts .daml/dist/create-daml-app-0.1.0.dar -o daml-ts
|
||||
|
||||
Now, use Yarn to install the project dependencies and build the app::
|
||||
Now, changing to the ``ui`` folder, use Yarn to install the project dependencies and build the app::
|
||||
|
||||
cd ui
|
||||
yarn install
|
||||
yarn workspaces run build
|
||||
yarn build
|
||||
|
||||
These steps may take a couple of minutes each (it's worth it!).
|
||||
You should see ``Compiled successfully.`` in the output if everything worked as expected.
|
||||
@ -81,7 +87,7 @@ We can now run the app in two steps.
|
||||
You'll need two terminal windows running for this.
|
||||
In one terminal, at the root of the ``create-daml-app`` directory, run the command::
|
||||
|
||||
daml start --start-navigator=no
|
||||
daml start
|
||||
|
||||
You will know that the command has started successfully when you see the ``INFO com.digitalasset.http.Main$ - Started server: ServerBinding(/127.0.0.1:7575)`` message in the terminal. The command does a few things:
|
||||
|
||||
@ -142,5 +148,3 @@ Just switch to the window where you are logged in as yourself - the network shou
|
||||
Play around more with the app at your leisure: create new users and start following more users.
|
||||
Observe when a user becomes visible to others - this will be important to understanding DAML's privacy model later.
|
||||
When you're ready, let's move on to the :doc:`architecture of our app <app-architecture>`.
|
||||
|
||||
.. TODO: Add screenshots for the app above
|
||||
|
@ -51,8 +51,8 @@ Let's start at a higher level with a ``test``.
|
||||
|
||||
.. literalinclude:: code/ui-before/index.test.tsx
|
||||
:language: tsx
|
||||
:start-after: // BEGIN_LOGIN_TEST
|
||||
:end-before: // END_LOGIN_TEST
|
||||
:start-after: // LOGIN_TEST_BEGIN
|
||||
:end-before: // LOGIN_TEST_END
|
||||
|
||||
We'll walk though this step by step.
|
||||
|
||||
@ -77,8 +77,8 @@ Let's see how ``login()`` is implemented.
|
||||
|
||||
.. literalinclude:: code/ui-before/index.test.tsx
|
||||
:language: tsx
|
||||
:start-after: // BEGIN_LOGIN_FUNCTION
|
||||
:end-before: // END_LOGIN_FUNCTION
|
||||
:start-after: // LOGIN_FUNCTION_BEGIN
|
||||
:end-before: // LOGIN_FUNCTION_END
|
||||
|
||||
We first wait to receive a handle to the username input element.
|
||||
This is important to ensure the page and relevant elements are loaded by the time we try to act on them.
|
||||
@ -95,8 +95,8 @@ For example, here is a snippet of the ``LoginScreen`` React component with class
|
||||
|
||||
.. literalinclude:: code/ui-before/LoginScreen.tsx
|
||||
:language: tsx
|
||||
:start-after: // BEGIN_FORM
|
||||
:end-before: // END_FORM
|
||||
:start-after: // FORM_BEGIN
|
||||
:end-before: // FORM_END
|
||||
|
||||
You can see the ``className`` attributes in the ``Input`` and ``Button``, which we select in the ``login()`` function.
|
||||
Note that you can use other features of an element in your selector, such as its type and attributes.
|
||||
|
@ -7,6 +7,7 @@ genrule(
|
||||
"default-gitignore",
|
||||
"default-dlint-yaml",
|
||||
"skeleton/**",
|
||||
"create-daml-app/**",
|
||||
"quickstart-java/**",
|
||||
"quickstart-scala/**",
|
||||
]) + [
|
||||
@ -23,11 +24,12 @@ genrule(
|
||||
OUT=templates-tarball
|
||||
|
||||
# templates in templates dir
|
||||
for d in skeleton quickstart-scala quickstart-java; do
|
||||
for d in skeleton create-daml-app quickstart-scala quickstart-java; do
|
||||
mkdir -p $$OUT/$$d
|
||||
cp -rL $$SRC/$$d/* $$OUT/$$d/
|
||||
cp $$SRC/default-gitignore $$OUT/$$d/.gitignore
|
||||
cp $$SRC/default-dlint-yaml $$OUT/$$d/.dlint.yaml
|
||||
# use default .gitignore and .dlint.yaml if they don't exist in the template
|
||||
cp -n $$SRC/default-gitignore $$OUT/$$d/.gitignore
|
||||
cp -n $$SRC/default-dlint-yaml $$OUT/$$d/.dlint.yaml
|
||||
done
|
||||
|
||||
## special cases we should work to remove
|
||||
|
65
templates/README.txt
Normal file
65
templates/README.txt
Normal file
@ -0,0 +1,65 @@
|
||||
These are template projects for `daml new`.
|
||||
|
||||
Testing the create-daml-app template
|
||||
====================================
|
||||
|
||||
While automated integration tests for the create-daml-app template are being built,
|
||||
we have the following manual testing procedure.
|
||||
Note that this is for testing against the head of the daml repo.
|
||||
For testing against a released SDK, you can skip past the package.json step and use
|
||||
`daml` instead of `daml-head` after installing the released version.
|
||||
|
||||
|
||||
First, build the SDK from head using the `daml-sdk-head` command.
|
||||
This gives an executable DAML assistant called `daml-head` in your path.
|
||||
|
||||
Next, instantiate the `create-daml-app` template as follows:
|
||||
|
||||
```
|
||||
daml-head new create-daml-app
|
||||
cd create-daml-app
|
||||
```
|
||||
|
||||
Crucially, you'll need to add a package.json file at the root of the project for testing
|
||||
(this is not required when using the released SDK).
|
||||
It should look as follows, with the dummy paths here replaced by relative paths to locally
|
||||
built TypeScript libraries.
|
||||
(These need to be built from head using Bazel:
|
||||
```
|
||||
bazel build //language-support/ts/daml-types
|
||||
bazel build //language-support/ts/daml-ledger
|
||||
bazel build //language-support/ts/daml-react```)
|
||||
|
||||
package.json:
|
||||
{
|
||||
"resolutions": {
|
||||
"@daml/types": "file:path/to/daml-types/npm_package",
|
||||
"@daml/ledger": "file:path/to/daml-ledger/npm_package",
|
||||
"@daml/react": "file:path/to/daml-react/npm_package"
|
||||
},
|
||||
"private": true,
|
||||
"workspaces": [
|
||||
"daml-ts",
|
||||
"ui"
|
||||
]
|
||||
}
|
||||
|
||||
Now you can continue to build and run the project as described in create-daml-app/README.md,
|
||||
using `daml-head` instead of `daml`.
|
||||
Specifically, you should run the following in the root directory:
|
||||
```
|
||||
daml-head build
|
||||
daml-head codegen ts .daml/dist/create-daml-app-0.1.0.dar -o daml-ts
|
||||
daml-head start
|
||||
```
|
||||
|
||||
Then in another terminal, navigate to `create-daml-app/ui/` and run:
|
||||
```
|
||||
yarn install
|
||||
yarn start
|
||||
```
|
||||
And check that the app works.
|
||||
|
||||
Finally, terminate both the `daml start` and `yarn start` processes and run
|
||||
`yarn test` from the `ui` directory. All tests should pass.
|
||||
|
4
templates/create-daml-app/.dlint.yaml
Normal file
4
templates/create-daml-app/.dlint.yaml
Normal file
@ -0,0 +1,4 @@
|
||||
# Copyright (c) 2020 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
- ignore: {name: Use infix }
|
9
templates/create-daml-app/.gitignore
vendored
Normal file
9
templates/create-daml-app/.gitignore
vendored
Normal file
@ -0,0 +1,9 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
/.daml
|
||||
build
|
||||
node_modules
|
||||
|
||||
*.log
|
||||
|
||||
daml-ts/
|
122
templates/create-daml-app/README.md
Normal file
122
templates/create-daml-app/README.md
Normal file
@ -0,0 +1,122 @@
|
||||
[![DAML logo](https://daml.com/static/images/logo.png)](https://www.daml.com)
|
||||
|
||||
[![Download](https://img.shields.io/github/release/digital-asset/daml.svg?label=Download)](https://docs.daml.com/getting-started/installation.html)
|
||||
[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://github.com/digital-asset/daml/blob/master/LICENSE)
|
||||
|
||||
# Welcome to _Create DAML App_
|
||||
|
||||
This repository contains a template to get started with developing full-stack
|
||||
[DAML](https://daml.com/) applications. The demo application covers the following aspects:
|
||||
|
||||
1. A [DAML](https://docs.daml.com/index.html) model of a simple social network
|
||||
2. A UI written in [TypeScript](https://www.typescriptlang.org/) and [React](https://reactjs.org/)
|
||||
|
||||
The UI is developed using [React](https://reactjs.org/),
|
||||
[Semantic UI](https://react.semantic-ui.com/) and its
|
||||
official [React integration](https://react.semantic-ui.com/).
|
||||
The whole project was bootstrapped with
|
||||
[Create React App](https://github.com/facebook/create-react-app).
|
||||
Regardless of these choices, all DAML specific aspects of the UI client are
|
||||
written in plain TypeScript and the UI framework should hence be easily
|
||||
replaceable.
|
||||
|
||||
|
||||
## Getting started
|
||||
|
||||
Before you can run the application, you need to install the
|
||||
[yarn](https://yarnpkg.com/en/docs/install) package manager for JavaScript.
|
||||
|
||||
There are two steps to build the project.
|
||||
First, we need to generate TypeScript code bindings for the compiled DAML model.
|
||||
At the root of the repository, run
|
||||
```
|
||||
daml build
|
||||
daml codegen ts .daml/dist/create-daml-app-0.1.0.dar -o daml-ts
|
||||
```
|
||||
The latter command generates TypeScript packages in the `daml-ts` directory.
|
||||
|
||||
Next, navigate to the `ui` directory and install the dependencies and build the app by running
|
||||
```
|
||||
cd ui
|
||||
yarn install
|
||||
yarn build
|
||||
```
|
||||
The last step is not absolutely necessary but useful to check that the app compiles.
|
||||
|
||||
To start the application, there are again two steps.
|
||||
In one terminal in the root directory, start a DAML ledger using
|
||||
```
|
||||
daml start
|
||||
```
|
||||
This must continue running to serve ledger requests.
|
||||
|
||||
Then in a second terminal window in the `ui` directory, start the UI server via
|
||||
```
|
||||
yarn start
|
||||
```
|
||||
This should open a browser window with a login screen.
|
||||
If it doesn't, you can manually point your browser to http://localhost:3000.
|
||||
|
||||
|
||||
## A quick tour
|
||||
|
||||
You can log into the app by providing a user name, say `Alice`. For simplicity
|
||||
of this app, there is no password or sign-up required. You will be greeted by
|
||||
a screen indicating that you're not following anyone and that you don't have
|
||||
any followers yet. You can change this by following someone in the upper box,
|
||||
say `Bob`. After that, let's log out in the top right corner and log in as `Bob`.
|
||||
|
||||
As `Bob`, we can see that we are not following anyone and that `Alice` is follwing
|
||||
us. We can follow `Alice` by clicking the plus symbol to the right of here name.
|
||||
|
||||
|
||||
## Deploying to DABL
|
||||
|
||||
Deploying `create-daml-app` to the hosted DAML platform
|
||||
[project:DABL](https://projectdabl.com/) is quite simple. Log into your DABL
|
||||
account, create a new ledger and upload your DAML models and your UI.
|
||||
|
||||
To upload the DAML models, compile them into a DAR by executing
|
||||
```
|
||||
daml build -o create-daml-app.dar
|
||||
```
|
||||
at the root of your repository. Afterwards, open to the DABL website, select
|
||||
the ledger you want to deploy to, go to the "DAML" selection and upload the
|
||||
DAR `create-daml-app.dar` you have just created.
|
||||
|
||||
To upload the UI, create a ZIP file containing all your UI assets by executing
|
||||
```
|
||||
daml build
|
||||
daml codegen ts .daml/dist/create-daml-app-0.1.0.dar -o daml-ts/src
|
||||
yarn workspaces run build
|
||||
(cd ui && zip -r ../create-daml-app-ui.zip build)
|
||||
```
|
||||
at the root of the repository. Afterwards, select the "UI Assets" tab of your
|
||||
chosen ledger on the DABL website, upload the ZIP file
|
||||
(`create-daml-app-ui.zip`) you have just created and publish it.
|
||||
|
||||
To see your deployed instance of `create-daml-app` in action, follow the
|
||||
"Visit site" link at the top right corner of your "UI Assets" page.
|
||||
|
||||
|
||||
## Next steps
|
||||
|
||||
There are many directions in which this application can be extended.
|
||||
Regardless of which direction you pick, the following files will be the most
|
||||
interesting ones to familiarize yourself with:
|
||||
|
||||
- [`daml/User.daml`](daml/User.daml): the DAML model of the social network
|
||||
- [`daml-ts/src/create-daml-app-0.1.0/User.ts`](src/daml/User.ts) (once you've generated it):
|
||||
a reflection of the types contained in the DAML model in TypeScript
|
||||
- [`ui/src/components/MainView.tsx`](ui/src/components/MainView.tsx):
|
||||
a React component using the HTTP Ledger API and rendering the main features
|
||||
|
||||
|
||||
## Useful resources
|
||||
|
||||
TBD
|
||||
|
||||
|
||||
## How to get help
|
||||
|
||||
TBD
|
16
templates/create-daml-app/daml.yaml.template
Normal file
16
templates/create-daml-app/daml.yaml.template
Normal file
@ -0,0 +1,16 @@
|
||||
sdk-version: __VERSION__
|
||||
name: create-daml-app
|
||||
version: 0.1.0
|
||||
source: daml
|
||||
parties:
|
||||
- Alice
|
||||
- Bob
|
||||
- Charlie
|
||||
dependencies:
|
||||
- daml-prim
|
||||
- daml-stdlib
|
||||
- daml-trigger
|
||||
sandbox-options:
|
||||
- --wall-clock-time
|
||||
- --ledgerid=create-daml-app-sandbox
|
||||
start-navigator: false
|
27
templates/create-daml-app/daml/User.daml
Normal file
27
templates/create-daml-app/daml/User.daml
Normal file
@ -0,0 +1,27 @@
|
||||
-- Copyright (c) 2020 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
-- SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
module User where
|
||||
|
||||
-- MAIN_TEMPLATE_BEGIN
|
||||
template User with
|
||||
username: Party
|
||||
following: [Party]
|
||||
where
|
||||
signatory username
|
||||
observer following
|
||||
-- MAIN_TEMPLATE_END
|
||||
|
||||
key username: Party
|
||||
maintainer key
|
||||
|
||||
-- FOLLOW_BEGIN
|
||||
nonconsuming choice Follow: ContractId User with
|
||||
userToFollow: Party
|
||||
controller username
|
||||
do
|
||||
assertMsg "You cannot follow yourself" (userToFollow /= username)
|
||||
assertMsg "You cannot follow the same user twice" (userToFollow `notElem` following)
|
||||
archive self
|
||||
create this with following = userToFollow :: following
|
||||
-- FOLLOW_END
|
53
templates/create-daml-app/ui/package.json.template
Normal file
53
templates/create-daml-app/ui/package.json.template
Normal file
@ -0,0 +1,53 @@
|
||||
{
|
||||
"name": "create-daml-app",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@daml.js/create-daml-app-0.1.0": "file:../daml-ts/create-daml-app-0.1.0",
|
||||
"@daml/types": "__VERSION__",
|
||||
"@daml/ledger": "__VERSION__",
|
||||
"@daml/react": "__VERSION__",
|
||||
"@types/puppeteer": "^2.0.1",
|
||||
"jwt-simple": "^0.5.6",
|
||||
"puppeteer": "^2.1.1",
|
||||
"react": "^16.12.0",
|
||||
"react-dom": "^16.12.0",
|
||||
"react-scripts": "^3.3.0",
|
||||
"semantic-ui-css": "^2.4.1",
|
||||
"semantic-ui-react": "^0.88.1",
|
||||
"typescript": "~3.7.4"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test --testURL='http://localhost:7575'",
|
||||
"eject": "react-scripts eject",
|
||||
"lint": "eslint --ext .js,.jsx,.ts,.tsx src/"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": "react-app"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"proxy": "http://localhost:7575",
|
||||
"devDependencies": {
|
||||
"@types/jest": "24.0.18",
|
||||
"@types/jwt-simple": "^0.5.33",
|
||||
"@types/node": "12.7.12",
|
||||
"@types/react": "^16.9.16",
|
||||
"@types/react-dom": "^16.9.4",
|
||||
"@types/wait-on": "^4.0.0",
|
||||
"wait-on": "^4.0.1",
|
||||
"eslint-config-react-app": "^5.2.0"
|
||||
}
|
||||
}
|
29
templates/create-daml-app/ui/public/daml.svg
Normal file
29
templates/create-daml-app/ui/public/daml.svg
Normal file
@ -0,0 +1,29 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 22.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg width="840px" height="226px" version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 840.2 226" style="enable-background:new 0 0 840.2 226;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#21356A;}
|
||||
.st1{fill-rule:evenodd;clip-rule:evenodd;fill:#517CD9;}
|
||||
.st2{fill-rule:evenodd;clip-rule:evenodd;fill:url(#SVGID_1_);}
|
||||
.st3{fill-rule:evenodd;clip-rule:evenodd;fill:#21356A;}
|
||||
.st4{fill-rule:evenodd;clip-rule:evenodd;fill:#81A9FF;}
|
||||
</style>
|
||||
<path class="st0" d="M687.3,0h-59.2l-59.5,157.2L508.4,0h-60.2v226H499V85.9L549.2,226h38.6l48.6-140.1V226h50.9V0z"/>
|
||||
<path class="st0" d="M776.1,180.8V0H724v226h116.2v-45.2H776.1z"/>
|
||||
<path class="st0" d="M0,0v226h82.7c18.7,0,35.3-3.2,49.9-9.5c14.6-6.3,26.9-14.8,36.9-25.3c10-10.5,17.5-22.6,22.7-36.2
|
||||
c5.1-13.6,7.7-27.5,7.7-42c0-14.4-2.6-28.4-7.9-42c-5.3-13.6-12.9-25.6-22.9-36.2c-10-10.5-22.2-19-36.7-25.3
|
||||
C118,3.2,101.4,0,82.7,0H0z M80.1,45.2c10.5,0,19.9,1.9,28.2,5.6c8.3,3.8,15.2,8.8,20.9,15.2c5.7,6.4,10,13.6,13,21.8
|
||||
c3,8.2,4.5,16.6,4.5,25.2c0,8.8-1.5,17.3-4.5,25.3c-3,8.1-7.4,15.3-13,21.6c-5.7,6.4-12.7,11.4-20.9,15.2
|
||||
c-8.3,3.8-17.7,5.6-28.2,5.6h-28V45.2H80.1z"/>
|
||||
<g>
|
||||
<path class="st1" d="M285.4,2.1h50.3L406.1,226h-50.3L285.4,2.1z"/>
|
||||
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="431.8985" y1="200.2006" x2="259.5836" y2="27.8857">
|
||||
<stop offset="0" style="stop-color:#517CD9"/>
|
||||
<stop offset="1" style="stop-color:#2C58B9"/>
|
||||
</linearGradient>
|
||||
<path class="st2" d="M285.4,2.1h50.3L406.1,226h-50.3L285.4,2.1z"/>
|
||||
</g>
|
||||
<polygon class="st3" points="290.4,146 265.3,226 215,226 240.1,146 "/>
|
||||
<polygon class="st4" points="335.7,2.1 309.9,84.1 259.6,84.1 285.4,2.1 "/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.8 KiB |
BIN
templates/create-daml-app/ui/public/favicon.png
Normal file
BIN
templates/create-daml-app/ui/public/favicon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.3 KiB |
46
templates/create-daml-app/ui/public/index.html
Normal file
46
templates/create-daml-app/ui/public/index.html
Normal file
@ -0,0 +1,46 @@
|
||||
<!DOCTYPE html>
|
||||
<!-- Copyright (c) 2020 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -->
|
||||
<!-- SPDX-License-Identifier: Apache-2.0 -->
|
||||
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Web site created using create-daml-app"
|
||||
/>
|
||||
<link rel="apple-touch-icon" href="logo192.png" />
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
Only files inside the `public` folder can be referenced from the HTML.
|
||||
|
||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<title>Create DAML App</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<!--
|
||||
This HTML file is a template.
|
||||
If you open it directly in the browser, you will see an empty page.
|
||||
|
||||
You can add webfonts, meta tags, or analytics to this file.
|
||||
The build step will place the bundled scripts into the <body> tag.
|
||||
|
||||
To begin the development, run `npm start` or `yarn start`.
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
-->
|
||||
</body>
|
||||
</html>
|
15
templates/create-daml-app/ui/public/manifest.json
Normal file
15
templates/create-daml-app/ui/public/manifest.json
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"short_name": "React App",
|
||||
"name": "Create React App Sample",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.png",
|
||||
"sizes": "32x32",
|
||||
"type": "image/png"
|
||||
}
|
||||
],
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#ffffff"
|
||||
}
|
36
templates/create-daml-app/ui/src/Credentials.ts
Normal file
36
templates/create-daml-app/ui/src/Credentials.ts
Normal file
@ -0,0 +1,36 @@
|
||||
// Copyright (c) 2020 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { encode } from 'jwt-simple';
|
||||
import { ledgerId } from './config';
|
||||
|
||||
export const APPLICATION_ID: string = 'create-daml-app';
|
||||
|
||||
// NOTE: This is for testing purposes only.
|
||||
// To handle authentication properly,
|
||||
// see https://docs.daml.com/app-dev/authentication.html.
|
||||
export const SECRET_KEY: string = 'secret';
|
||||
|
||||
export type Credentials = {
|
||||
party: string;
|
||||
token: string;
|
||||
ledgerId: string;
|
||||
}
|
||||
|
||||
function computeToken(party: string): string {
|
||||
const payload = {
|
||||
"https://daml.com/ledger-api": {
|
||||
"ledgerId": ledgerId,
|
||||
"applicationId": APPLICATION_ID,
|
||||
"actAs": [party]
|
||||
}
|
||||
};
|
||||
return encode(payload, SECRET_KEY, 'HS256');
|
||||
}
|
||||
|
||||
export const computeCredentials = (party: string): Credentials => {
|
||||
const token = computeToken(party);
|
||||
return {party, token, ledgerId};
|
||||
}
|
||||
|
||||
export default Credentials;
|
12
templates/create-daml-app/ui/src/components/App.test.tsx
Normal file
12
templates/create-daml-app/ui/src/components/App.test.tsx
Normal file
@ -0,0 +1,12 @@
|
||||
// Copyright (c) 2020 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import App from './App';
|
||||
|
||||
it('renders without crashing', () => {
|
||||
const div = document.createElement('div');
|
||||
ReactDOM.render(<App />, div);
|
||||
ReactDOM.unmountComponentAtNode(div);
|
||||
});
|
31
templates/create-daml-app/ui/src/components/App.tsx
Normal file
31
templates/create-daml-app/ui/src/components/App.tsx
Normal file
@ -0,0 +1,31 @@
|
||||
// Copyright (c) 2020 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import React from 'react';
|
||||
import LoginScreen from './LoginScreen';
|
||||
import MainScreen from './MainScreen';
|
||||
import DamlLedger from '@daml/react';
|
||||
import Credentials from '../Credentials';
|
||||
import { httpBaseUrl, wsBaseUrl } from '../config';
|
||||
|
||||
/**
|
||||
* React component for the entry point into the application.
|
||||
*/
|
||||
// APP_BEGIN
|
||||
const App: React.FC = () => {
|
||||
const [credentials, setCredentials] = React.useState<Credentials | undefined>();
|
||||
|
||||
return credentials
|
||||
? <DamlLedger
|
||||
token={credentials.token}
|
||||
party={credentials.party}
|
||||
httpBaseUrl={httpBaseUrl}
|
||||
wsBaseUrl={wsBaseUrl}
|
||||
>
|
||||
<MainScreen onLogout={() => setCredentials(undefined)}/>
|
||||
</DamlLedger>
|
||||
: <LoginScreen onLogin={setCredentials} />
|
||||
}
|
||||
// APP_END
|
||||
|
||||
export default App;
|
114
templates/create-daml-app/ui/src/components/LoginScreen.tsx
Normal file
114
templates/create-daml-app/ui/src/components/LoginScreen.tsx
Normal file
@ -0,0 +1,114 @@
|
||||
// Copyright (c) 2020 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import React, { useCallback } from 'react'
|
||||
import { Button, Form, Grid, Header, Image, Segment } from 'semantic-ui-react'
|
||||
import Credentials, { computeCredentials } from '../Credentials';
|
||||
import Ledger from '@daml/ledger';
|
||||
import { User } from '@daml.js/create-daml-app-0.1.0';
|
||||
import { DeploymentMode, deploymentMode, ledgerId, httpBaseUrl, wsBaseUrl } from '../config';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
type Props = {
|
||||
onLogin: (credentials: Credentials) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* React component for the login screen of the `App`.
|
||||
*/
|
||||
const LoginScreen: React.FC<Props> = ({onLogin}) => {
|
||||
const [username, setUsername] = React.useState('');
|
||||
|
||||
const login = useCallback(async (credentials: Credentials) => {
|
||||
try {
|
||||
const ledger = new Ledger({token: credentials.token, httpBaseUrl, wsBaseUrl});
|
||||
let userContract = await ledger.fetchByKey(User.User, credentials.party);
|
||||
if (userContract === null) {
|
||||
const user = {username: credentials.party, following: []};
|
||||
userContract = await ledger.create(User.User, user);
|
||||
}
|
||||
onLogin(credentials);
|
||||
} catch(error) {
|
||||
alert(`Unknown error:\n${JSON.stringify(error)}`);
|
||||
}
|
||||
}, [onLogin]);
|
||||
|
||||
const handleLogin = async (event: React.FormEvent) => {
|
||||
event.preventDefault();
|
||||
const credentials = computeCredentials(username);
|
||||
await login(credentials);
|
||||
}
|
||||
|
||||
const handleDablLogin = () => {
|
||||
window.location.assign(`https://login.projectdabl.com/auth/login?ledgerId=${ledgerId}`);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const url = new URL(window.location.toString());
|
||||
const token = url.searchParams.get('token');
|
||||
if (token === null) {
|
||||
return;
|
||||
}
|
||||
const party = url.searchParams.get('party');
|
||||
if (party === null) {
|
||||
throw Error("When 'token' is passed via URL, 'party' must be passed too.");
|
||||
}
|
||||
url.search = '';
|
||||
window.history.replaceState(window.history.state, '', url.toString());
|
||||
login({token, party, ledgerId});
|
||||
}, [login]);
|
||||
|
||||
return (
|
||||
<Grid textAlign='center' style={{ height: '100vh' }} verticalAlign='middle'>
|
||||
<Grid.Column style={{ maxWidth: 450 }}>
|
||||
<Header as='h1' textAlign='center' size='huge' style={{color: '#223668'}}>
|
||||
<Header.Content>
|
||||
Create
|
||||
<Image
|
||||
as='a'
|
||||
href='https://www.daml.com/'
|
||||
target='_blank'
|
||||
src='/daml.svg'
|
||||
alt='DAML Logo'
|
||||
spaced
|
||||
size='small'
|
||||
verticalAlign='middle'
|
||||
/>
|
||||
App
|
||||
</Header.Content>
|
||||
</Header>
|
||||
<Form size='large' className='test-select-login-screen'>
|
||||
<Segment>
|
||||
{deploymentMode !== DeploymentMode.PROD_DABL
|
||||
? <>
|
||||
{/* FORM_BEGIN */}
|
||||
<Form.Input
|
||||
fluid
|
||||
icon='user'
|
||||
iconPosition='left'
|
||||
placeholder='Username'
|
||||
value={username}
|
||||
className='test-select-username-field'
|
||||
onChange={e => setUsername(e.currentTarget.value)}
|
||||
/>
|
||||
<Button
|
||||
primary
|
||||
fluid
|
||||
className='test-select-login-button'
|
||||
onClick={handleLogin}>
|
||||
Log in
|
||||
</Button>
|
||||
{/* FORM_END */}
|
||||
</>
|
||||
: <Button primary fluid onClick={handleDablLogin}>
|
||||
Log in with DABL
|
||||
</Button>
|
||||
}
|
||||
</Segment>
|
||||
</Form>
|
||||
</Grid.Column>
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoginScreen;
|
49
templates/create-daml-app/ui/src/components/MainScreen.tsx
Normal file
49
templates/create-daml-app/ui/src/components/MainScreen.tsx
Normal file
@ -0,0 +1,49 @@
|
||||
// Copyright (c) 2020 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import React from 'react'
|
||||
import { Image, Menu } from 'semantic-ui-react'
|
||||
import MainView from './MainView';
|
||||
import { useParty } from '@daml/react';
|
||||
|
||||
type Props = {
|
||||
onLogout: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* React component for the main screen of the `App`.
|
||||
*/
|
||||
const MainScreen: React.FC<Props> = ({onLogout}) => {
|
||||
return (
|
||||
<>
|
||||
<Menu icon borderless>
|
||||
<Menu.Item>
|
||||
<Image
|
||||
as='a'
|
||||
href='https://www.daml.com/'
|
||||
target='_blank'
|
||||
src='/daml.svg'
|
||||
alt='DAML Logo'
|
||||
size='mini'
|
||||
/>
|
||||
</Menu.Item>
|
||||
<Menu.Menu position='right' className='test-select-main-menu'>
|
||||
<Menu.Item position='right'>
|
||||
You are logged in as {useParty()}.
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
position='right'
|
||||
active={false}
|
||||
className='test-select-log-out'
|
||||
onClick={onLogout}
|
||||
icon='log out'
|
||||
/>
|
||||
</Menu.Menu>
|
||||
</Menu>
|
||||
|
||||
<MainView/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default MainScreen;
|
88
templates/create-daml-app/ui/src/components/MainView.tsx
Normal file
88
templates/create-daml-app/ui/src/components/MainView.tsx
Normal file
@ -0,0 +1,88 @@
|
||||
// Copyright (c) 2020 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
import { Container, Grid, Header, Icon, Segment, Divider } from 'semantic-ui-react';
|
||||
import { Party } from '@daml/types';
|
||||
import { User } from '@daml.js/create-daml-app-0.1.0';
|
||||
import { useParty, useLedger, useStreamFetchByKey, useStreamQuery } from '@daml/react';
|
||||
import UserList from './UserList';
|
||||
import PartyListEdit from './PartyListEdit';
|
||||
|
||||
// USERS_BEGIN
|
||||
const MainView: React.FC = () => {
|
||||
const username = useParty();
|
||||
const myUserResult = useStreamFetchByKey(User.User, () => username, [username]);
|
||||
const myUser = myUserResult.contract?.payload;
|
||||
const allUsers = useStreamQuery(User.User).contracts;
|
||||
// USERS_END
|
||||
|
||||
// Sorted list of users that are following the current user
|
||||
const followers = useMemo(() =>
|
||||
allUsers
|
||||
.map(user => user.payload)
|
||||
.filter(user => user.username !== username)
|
||||
.sort((x, y) => x.username.localeCompare(y.username)),
|
||||
[allUsers, username]);
|
||||
|
||||
// FOLLOW_BEGIN
|
||||
const ledger = useLedger();
|
||||
|
||||
const follow = async (userToFollow: Party): Promise<boolean> => {
|
||||
try {
|
||||
await ledger.exerciseByKey(User.User.Follow, username, {userToFollow});
|
||||
return true;
|
||||
} catch (error) {
|
||||
alert("Unknown error:\n" + JSON.stringify(error));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// FOLLOW_END
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Grid centered columns={2}>
|
||||
<Grid.Row stretched>
|
||||
<Grid.Column>
|
||||
<Header as='h1' size='huge' color='blue' textAlign='center' style={{padding: '1ex 0em 0ex 0em'}}>
|
||||
{myUser ? `Welcome, ${myUser.username}!` : 'Loading...'}
|
||||
</Header>
|
||||
|
||||
<Segment>
|
||||
<Header as='h2'>
|
||||
<Icon name='user' />
|
||||
<Header.Content>
|
||||
{myUser?.username ?? 'Loading...'}
|
||||
<Header.Subheader>Users I'm following</Header.Subheader>
|
||||
</Header.Content>
|
||||
</Header>
|
||||
<Divider />
|
||||
<PartyListEdit
|
||||
parties={myUser?.following ?? []}
|
||||
onAddParty={follow}
|
||||
/>
|
||||
</Segment>
|
||||
<Segment>
|
||||
<Header as='h2'>
|
||||
<Icon name='globe' />
|
||||
<Header.Content>
|
||||
The Network
|
||||
<Header.Subheader>My followers and users they are following</Header.Subheader>
|
||||
</Header.Content>
|
||||
</Header>
|
||||
<Divider />
|
||||
{/* USERLIST_BEGIN */}
|
||||
<UserList
|
||||
users={followers}
|
||||
onFollow={follow}
|
||||
/>
|
||||
{/* USERLIST_END */}
|
||||
</Segment>
|
||||
</Grid.Column>
|
||||
</Grid.Row>
|
||||
</Grid>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
export default MainView;
|
@ -0,0 +1,67 @@
|
||||
// Copyright (c) 2020 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import React from 'react'
|
||||
import { Form, List, Button } from 'semantic-ui-react';
|
||||
import { Party } from '@daml/types';
|
||||
|
||||
type Props = {
|
||||
parties: Party[];
|
||||
onAddParty: (party: Party) => Promise<boolean>;
|
||||
}
|
||||
|
||||
/**
|
||||
* React component to edit a list of `Party`s.
|
||||
*/
|
||||
const PartyListEdit: React.FC<Props> = ({parties, onAddParty}) => {
|
||||
const [newParty, setNewParty] = React.useState('');
|
||||
const [isSubmitting, setIsSubmitting] = React.useState(false);
|
||||
|
||||
const addParty = async (event?: React.FormEvent) => {
|
||||
if (event) {
|
||||
event.preventDefault();
|
||||
}
|
||||
setIsSubmitting(true);
|
||||
const success = await onAddParty(newParty);
|
||||
setIsSubmitting(false);
|
||||
if (success) {
|
||||
setNewParty('');
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<List relaxed>
|
||||
{[...parties].sort((x, y) => x.localeCompare(y)).map((party) =>
|
||||
<List.Item
|
||||
key={party}
|
||||
>
|
||||
<List.Icon name='user outline' />
|
||||
<List.Content>
|
||||
<List.Header className='test-select-following'>
|
||||
{party}
|
||||
</List.Header>
|
||||
</List.Content>
|
||||
</List.Item>
|
||||
)}
|
||||
<br />
|
||||
<Form onSubmit={addParty}>
|
||||
<Form.Input
|
||||
fluid
|
||||
readOnly={isSubmitting}
|
||||
loading={isSubmitting}
|
||||
className='test-select-follow-input'
|
||||
placeholder="Username to follow"
|
||||
value={newParty}
|
||||
onChange={(event) => setNewParty(event.currentTarget.value)}
|
||||
/>
|
||||
<Button
|
||||
type='submit'
|
||||
className='test-select-follow-button'>
|
||||
Follow
|
||||
</Button>
|
||||
</Form>
|
||||
</List>
|
||||
);
|
||||
};
|
||||
|
||||
export default PartyListEdit;
|
57
templates/create-daml-app/ui/src/components/UserList.tsx
Normal file
57
templates/create-daml-app/ui/src/components/UserList.tsx
Normal file
@ -0,0 +1,57 @@
|
||||
// Copyright (c) 2020 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import React from 'react'
|
||||
import { Icon, List } from 'semantic-ui-react'
|
||||
import { Party } from '@daml/types';
|
||||
import { User } from '@daml.js/create-daml-app-0.1.0';
|
||||
|
||||
type Props = {
|
||||
users: User.User[];
|
||||
onFollow: (userToFollow: Party) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* React component to display a list of `User`s.
|
||||
* Every party in the list can be added as a friend.
|
||||
*/
|
||||
const UserList: React.FC<Props> = ({users, onFollow}) => {
|
||||
return (
|
||||
<List divided relaxed>
|
||||
{[...users].sort((x, y) => x.username.localeCompare(y.username)).map(user =>
|
||||
<List.Item key={user.username}>
|
||||
<List.Icon name='user' />
|
||||
<List.Content>
|
||||
<List.Content floated='right'>
|
||||
<Icon
|
||||
name='add user'
|
||||
link
|
||||
className='test-select-add-user-icon'
|
||||
onClick={() => onFollow(user.username)} />
|
||||
</List.Content>
|
||||
<List.Header className='test-select-user-in-network'>{user.username}</List.Header>
|
||||
</List.Content>
|
||||
<List.List>
|
||||
{[...user.following].sort((x, y) => x.localeCompare(y)).map(userToFollow =>
|
||||
<List.Item key={userToFollow}>
|
||||
<List.Content floated='right'>
|
||||
<Icon
|
||||
name='add user'
|
||||
link
|
||||
className='test-select-add-user-following-icon'
|
||||
onClick={() => onFollow(userToFollow)} />
|
||||
</List.Content>
|
||||
<List.Icon name='user outline' />
|
||||
<List.Content>
|
||||
<List.Header>{userToFollow}</List.Header>
|
||||
</List.Content>
|
||||
</List.Item>
|
||||
)}
|
||||
</List.List>
|
||||
</List.Item>
|
||||
)}
|
||||
</List>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserList;
|
33
templates/create-daml-app/ui/src/config.ts
Normal file
33
templates/create-daml-app/ui/src/config.ts
Normal file
@ -0,0 +1,33 @@
|
||||
// Copyright (c) 2020 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
export enum DeploymentMode {
|
||||
DEV,
|
||||
PROD_DABL,
|
||||
PROD_OTHER,
|
||||
}
|
||||
|
||||
export const deploymentMode: DeploymentMode =
|
||||
process.env.NODE_ENV === 'development'
|
||||
? DeploymentMode.DEV
|
||||
: window.location.hostname.endsWith('.projectdabl.com')
|
||||
? DeploymentMode.PROD_DABL
|
||||
: DeploymentMode.PROD_OTHER;
|
||||
|
||||
export const ledgerId =
|
||||
deploymentMode === DeploymentMode.PROD_DABL
|
||||
? window.location.hostname.split('.')[0]
|
||||
: 'create-daml-app-sandbox';
|
||||
|
||||
export const httpBaseUrl =
|
||||
deploymentMode === DeploymentMode.PROD_DABL
|
||||
? `https://api.projectdabl.com/data/${ledgerId}/`
|
||||
: undefined;
|
||||
|
||||
// Unfortunately, the development server of `create-react-app` does not proxy
|
||||
// websockets properly. Thus, we need to bypass it and talk to the JSON API
|
||||
// directly in development mode.
|
||||
export const wsBaseUrl =
|
||||
deploymentMode === DeploymentMode.DEV
|
||||
? 'ws://localhost:7575/'
|
||||
: undefined;
|
16
templates/create-daml-app/ui/src/index.css
Normal file
16
templates/create-daml-app/ui/src/index.css
Normal file
@ -0,0 +1,16 @@
|
||||
/* Copyright (c) 2020 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. */
|
||||
/* SPDX-License-Identifier: Apache-2.0 */
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||
monospace;
|
||||
}
|
305
templates/create-daml-app/ui/src/index.test.tsx
Normal file
305
templates/create-daml-app/ui/src/index.test.tsx
Normal file
@ -0,0 +1,305 @@
|
||||
// Copyright (c) 2020 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { ChildProcess, spawn, SpawnOptions } from 'child_process';
|
||||
import waitOn from 'wait-on';
|
||||
|
||||
import Ledger from '@daml/ledger';
|
||||
import { User } from '@daml.js/create-daml-app-0.1.0';
|
||||
import { computeCredentials } from './Credentials';
|
||||
|
||||
import puppeteer, { Browser, Page } from 'puppeteer';
|
||||
|
||||
const SANDBOX_PORT = 6865;
|
||||
const JSON_API_PORT = 7575;
|
||||
const UI_PORT = 3000;
|
||||
|
||||
// `daml start` process (which spawns a sandbox and JSON API server)
|
||||
let startProc: ChildProcess | undefined = undefined;
|
||||
|
||||
// Headless Chrome browser:
|
||||
// https://developers.google.com/web/updates/2017/04/headless-chrome
|
||||
let browser: Browser | undefined = undefined;
|
||||
|
||||
let uiProc: ChildProcess | undefined = undefined;
|
||||
|
||||
// Function to generate unique party names for us.
|
||||
// This should be replaced by the party management service once that is exposed
|
||||
// in the HTTP JSON API.
|
||||
let nextPartyId = 1;
|
||||
function getParty(): string {
|
||||
const party = `P${nextPartyId}`;
|
||||
nextPartyId++;
|
||||
return party;
|
||||
}
|
||||
|
||||
test('Party names are unique', async () => {
|
||||
const parties = new Set(Array(10).fill({}).map(() => getParty()));
|
||||
expect(parties.size).toEqual(10);
|
||||
});
|
||||
|
||||
// Use a single sandbox, JSON API server and browser for all tests for speed.
|
||||
// This means we need to use a different set of parties and a new browser page for each test.
|
||||
beforeAll(async () => {
|
||||
// Run `daml start` to start up the sandbox and json api server.
|
||||
// Run it from the repository root, where the `daml.yaml` lives.
|
||||
// The path should already include '.daml/bin' in the environment where this is run.
|
||||
const startOpts: SpawnOptions = { cwd: '..', stdio: 'inherit' };
|
||||
startProc = spawn('daml', ['start'], startOpts);
|
||||
|
||||
// Run `yarn start` in another shell.
|
||||
// Disable automatically opening a browser using the env var described here:
|
||||
// https://github.com/facebook/create-react-app/issues/873#issuecomment-266318338
|
||||
const env = {...process.env, BROWSER: 'none'};
|
||||
uiProc = spawn('yarn', ['start'], { env, stdio: 'inherit', detached: true});
|
||||
// Note(kill-yarn-start): The `detached` flag starts the process in a new process group.
|
||||
// This allows us to kill the process with all its descendents after the tests finish,
|
||||
// following https://azimi.me/2014/12/31/kill-child_process-node-js.html.
|
||||
|
||||
// We know the `daml start` and `yarn start` servers are ready once the relevant ports become available.
|
||||
await waitOn({resources: [
|
||||
`tcp:localhost:${SANDBOX_PORT}`,
|
||||
`tcp:localhost:${JSON_API_PORT}`,
|
||||
`tcp:localhost:${UI_PORT}`
|
||||
]});
|
||||
|
||||
// Launch a browser once for all tests.
|
||||
browser = await puppeteer.launch();
|
||||
}, 40_000);
|
||||
|
||||
afterAll(async () => {
|
||||
// Kill the `daml start` process.
|
||||
// Note that `kill()` sends the `SIGTERM` signal but the actual processes may
|
||||
// not die immediately.
|
||||
// TODO: Test this on Windows.
|
||||
if (startProc) {
|
||||
startProc.kill();
|
||||
}
|
||||
|
||||
// Kill the `yarn start` process including all its descendents.
|
||||
// The `-` indicates to kill all processes in the process group.
|
||||
// See Note(kill-yarn-start).
|
||||
// TODO: Test this on Windows.
|
||||
if (uiProc) {
|
||||
process.kill(-uiProc.pid)
|
||||
}
|
||||
|
||||
if (browser) {
|
||||
browser.close();
|
||||
}
|
||||
});
|
||||
|
||||
test('create and look up user using ledger library', async () => {
|
||||
const partyName = getParty();
|
||||
const {party, token} = computeCredentials(partyName);
|
||||
const ledger = new Ledger({token});
|
||||
const users0 = await ledger.query(User.User);
|
||||
expect(users0).toEqual([]);
|
||||
const user = {username: party, following: []};
|
||||
const userContract1 = await ledger.create(User.User, user);
|
||||
const userContract2 = await ledger.fetchByKey(User.User, party);
|
||||
expect(userContract1).toEqual(userContract2);
|
||||
const users = await ledger.query(User.User);
|
||||
expect(users[0]).toEqual(userContract1);
|
||||
});
|
||||
|
||||
// The tests following use the headless browser to interact with the app.
|
||||
// We select the relevant DOM elements using CSS class names that we embedded
|
||||
// specifically for testing.
|
||||
// See https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors.
|
||||
|
||||
const newUiPage = async (): Promise<Page> => {
|
||||
if (!browser) {
|
||||
throw Error('Puppeteer browser has not been launched');
|
||||
}
|
||||
const page = await browser.newPage();
|
||||
await page.goto(`http://localhost:${UI_PORT}`); // ignore the Response
|
||||
return page;
|
||||
}
|
||||
|
||||
// LOGIN_FUNCTION_BEGIN
|
||||
// Log in using a party name and wait for the main screen to load.
|
||||
const login = async (page: Page, partyName: string) => {
|
||||
const usernameInput = await page.waitForSelector('.test-select-username-field');
|
||||
await usernameInput.click();
|
||||
await usernameInput.type(partyName);
|
||||
await page.click('.test-select-login-button');
|
||||
await page.waitForSelector('.test-select-main-menu');
|
||||
}
|
||||
// LOGIN_FUNCTION_END
|
||||
|
||||
// Log out and wait to get back to the login screen.
|
||||
const logout = async (page: Page) => {
|
||||
await page.click('.test-select-log-out');
|
||||
await page.waitForSelector('.test-select-login-screen');
|
||||
}
|
||||
|
||||
// Follow a user using the text input in the follow panel.
|
||||
const follow = async (page: Page, userToFollow: string) => {
|
||||
await page.click('.test-select-follow-input');
|
||||
await page.type('.test-select-follow-input', userToFollow);
|
||||
await page.click('.test-select-follow-button');
|
||||
|
||||
// Wait for the request to complete, either successfully or after the error
|
||||
// dialog has been handled.
|
||||
// We check this by the absence of the `loading` class.
|
||||
// (Both the `test-...` and `loading` classes appear in `div`s surrounding
|
||||
// the `input`, due to the translation of Semantic UI's `Input` element.)
|
||||
await page.waitForSelector('.test-select-follow-input > :not(.loading)', {timeout: 40_000});
|
||||
}
|
||||
|
||||
// LOGIN_TEST_BEGIN
|
||||
test('log in as a new user, log out and log back in', async () => {
|
||||
const partyName = getParty();
|
||||
|
||||
// Log in as a new user.
|
||||
const page = await newUiPage();
|
||||
await login(page, partyName);
|
||||
|
||||
// Check that the ledger contains the new User contract.
|
||||
const {token} = computeCredentials(partyName);
|
||||
const ledger = new Ledger({token});
|
||||
const users = await ledger.query(User.User);
|
||||
expect(users).toHaveLength(1);
|
||||
expect(users[0].payload.username).toEqual(partyName);
|
||||
|
||||
// Log out and in again as the same user.
|
||||
await logout(page);
|
||||
await login(page, partyName);
|
||||
|
||||
// Check we have the same one user.
|
||||
const usersFinal = await ledger.query(User.User);
|
||||
expect(usersFinal).toHaveLength(1);
|
||||
expect(usersFinal[0].payload.username).toEqual(partyName);
|
||||
|
||||
await page.close();
|
||||
}, 10_000);
|
||||
// LOGIN_TEST_END
|
||||
|
||||
// This tests following users in a few different ways:
|
||||
// - using the text box in the Follow panel
|
||||
// - using the icon in the Network panel
|
||||
// - while the user that is followed is logged in
|
||||
// - while the user that is followed is logged out
|
||||
// These are all successful cases.
|
||||
|
||||
test('log in as three different users and start following each other', async () => {
|
||||
const party1 = getParty();
|
||||
const party2 = getParty();
|
||||
const party3 = getParty();
|
||||
|
||||
// Log in as Party 1.
|
||||
const page1 = await newUiPage();
|
||||
await login(page1, party1);
|
||||
|
||||
// Party 1 should initially follow no one.
|
||||
const noFollowing1 = await page1.$$('.test-select-following');
|
||||
expect(noFollowing1).toEqual([]);
|
||||
|
||||
// Follow Party 2 using the text input.
|
||||
// This should work even though Party 2 has not logged in yet.
|
||||
// Check Party 1 follows exactly Party 2.
|
||||
await follow(page1, party2);
|
||||
await page1.waitForSelector('.test-select-following');
|
||||
const followingList1 = await page1.$$eval('.test-select-following', following => following.map(e => e.innerHTML));
|
||||
expect(followingList1).toEqual([party2]);
|
||||
|
||||
// Add Party 3 as well and check both are in the list.
|
||||
await follow(page1, party3);
|
||||
await page1.waitForSelector('.test-select-following');
|
||||
const followingList11 = await page1.$$eval('.test-select-following', following => following.map(e => e.innerHTML));
|
||||
expect(followingList11).toHaveLength(2);
|
||||
expect(followingList11).toContain(party2);
|
||||
expect(followingList11).toContain(party3);
|
||||
|
||||
// Log in as Party 2.
|
||||
const page2 = await newUiPage();
|
||||
await login(page2, party2);
|
||||
|
||||
// Party 2 should initially follow no one.
|
||||
const noFollowing2 = await page2.$$('.test-select-following');
|
||||
expect(noFollowing2).toEqual([]);
|
||||
|
||||
// However, Party 2 should see Party 1 in the network.
|
||||
await page2.waitForSelector('.test-select-user-in-network');
|
||||
const network2 = await page2.$$eval('.test-select-user-in-network', users => users.map(e => e.innerHTML));
|
||||
expect(network2).toEqual([party1]);
|
||||
|
||||
// Follow Party 1 using the 'add user' icon on the right.
|
||||
await page2.waitForSelector('.test-select-add-user-icon');
|
||||
const userIcons = await page2.$$('.test-select-add-user-icon');
|
||||
expect(userIcons).toHaveLength(1);
|
||||
await userIcons[0].click();
|
||||
|
||||
// Also follow Party 3 using the text input.
|
||||
// Note that we can also use the icon to follow Party 3 as they appear in the
|
||||
// Party 1's Network panel, but that's harder to test at the
|
||||
// moment because there is no loading indicator to tell when it's done.
|
||||
await follow(page2, party3);
|
||||
|
||||
// Check the following list is updated correctly.
|
||||
await page2.waitForSelector('.test-select-following');
|
||||
const followingList2 = await page2.$$eval('.test-select-following', following => following.map(e => e.innerHTML));
|
||||
expect(followingList2).toHaveLength(2);
|
||||
expect(followingList2).toContain(party1);
|
||||
expect(followingList2).toContain(party3);
|
||||
|
||||
// Party 1 should now also see Party 2 in the network (but not Party 3 as they
|
||||
// didn't yet started following Party 1).
|
||||
await page1.waitForSelector('.test-select-user-in-network');
|
||||
const network1 = await page1.$$eval('.test-select-user-in-network', following => following.map(e => e.innerHTML));
|
||||
expect(network1).toEqual([party2]);
|
||||
|
||||
// Log in as Party 3.
|
||||
const page3 = await newUiPage();
|
||||
await login(page3, party3);
|
||||
|
||||
// Party 3 should follow no one.
|
||||
const noFollowing3 = await page3.$$('.test-select-following');
|
||||
expect(noFollowing3).toEqual([]);
|
||||
|
||||
// However, Party 3 should see both Party 1 and Party 2 in the network.
|
||||
await page3.waitForSelector('.test-select-user-in-network');
|
||||
const network3 = await page3.$$eval('.test-select-user-in-network', following => following.map(e => e.innerHTML));
|
||||
expect(network3).toHaveLength(2);
|
||||
expect(network3).toContain(party1);
|
||||
expect(network3).toContain(party2);
|
||||
|
||||
await page1.close();
|
||||
await page2.close();
|
||||
await page3.close();
|
||||
}, 40_000);
|
||||
|
||||
test('error when following self', async () => {
|
||||
const party = getParty();
|
||||
const page = await newUiPage();
|
||||
|
||||
const dismissError = jest.fn(dialog => dialog.dismiss());
|
||||
page.on('dialog', dismissError);
|
||||
|
||||
await login(page, party);
|
||||
await follow(page, party);
|
||||
|
||||
expect(dismissError).toHaveBeenCalled();
|
||||
|
||||
await page.close();
|
||||
});
|
||||
|
||||
test('error when adding a user that you are already following', async () => {
|
||||
const party1 = getParty();
|
||||
const party2 = getParty();
|
||||
const page = await newUiPage();
|
||||
|
||||
const dismissError = jest.fn(dialog => dialog.dismiss());
|
||||
page.on('dialog', dismissError);
|
||||
|
||||
await login(page, party1);
|
||||
// First attempt should succeed
|
||||
await follow(page, party2);
|
||||
// Second attempt should result in an error
|
||||
await follow(page, party2);
|
||||
|
||||
expect(dismissError).toHaveBeenCalled();
|
||||
|
||||
await page.close();
|
||||
});
|
10
templates/create-daml-app/ui/src/index.tsx
Normal file
10
templates/create-daml-app/ui/src/index.tsx
Normal file
@ -0,0 +1,10 @@
|
||||
// Copyright (c) 2020 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import 'semantic-ui-css/semantic.min.css';
|
||||
import './index.css';
|
||||
import App from './components/App';
|
||||
|
||||
ReactDOM.render(<App />, document.getElementById('root'));
|
4
templates/create-daml-app/ui/src/react-app-env.d.ts
vendored
Normal file
4
templates/create-daml-app/ui/src/react-app-env.d.ts
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
// Copyright (c) 2020 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
/// <reference types="react-scripts" />
|
27
templates/create-daml-app/ui/tsconfig.json
Normal file
27
templates/create-daml-app/ui/tsconfig.json
Normal file
@ -0,0 +1,27 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react",
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
]
|
||||
}
|
Loading…
Reference in New Issue
Block a user