diff --git a/.gitignore b/.gitignore index e9c13ee..7c92fb9 100644 --- a/.gitignore +++ b/.gitignore @@ -13,10 +13,11 @@ generated-docs/ .psa* .spago/ -app/prod/ -!app/prod/css +prod/ +prod/css -test/index.js +tests/index.js +tests/prod/ # Temp files: *~ diff --git a/.travis.yml b/.travis.yml index 3cc0c0a..dce6fd8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,11 +7,11 @@ before_script: - chmod a+w -R . script: -- IMG_VER=0.13.0_r1 ./scripts/dist_build.sh +- IMG_VER=0.13.3_r1 ./scripts/dist_build.sh deploy: provider: pages - local_dir: app/dist + local_dir: dist target_branch: gh-pages skip_cleanup: true github_token: $GITHUB_TOKEN diff --git a/README.md b/README.md index 4a39399..c2cb9dd 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,11 @@ Web tools to display # Building +If you have the relevant build tools intalled (npm, spago, pulp, etc.), you can +build using `npm run build && npm run prod`. For a more convient approach, see +the section on Docker below, and for complete build commands used in CI, see +`scripts/dist_build_commands.sh`. + ## Docker * Run `./psc.sh `, e.g. `./psc.sh pulp --psc-package build`. This will run @@ -16,3 +21,8 @@ the command in the container with the CWD mounted and then exit. Alternatively if you want to issue multiple commands in the container quickly, you can run `./psc.sh bash`. +## Debugging + +For an unminified build that is easier to debug from the browser, use +`npm run debug` instead of `npm run prod`. + diff --git a/app/src/Metajelo/FormUtil.purs b/app/src/Metajelo/FormUtil.purs deleted file mode 100644 index 3b8c54e..0000000 --- a/app/src/Metajelo/FormUtil.purs +++ /dev/null @@ -1,120 +0,0 @@ -module Metajelo.FormUtil where - -import Prelude (class Show, Void, bind, join, pure, show, (+), (-), ($), (<$>), (<#>), (<<<), (==), (||)) - -import Concur.Core (Widget) -import Concur.React (HTML) -import Concur.React.DOM as D -import Concur.React.Props as P -import Control.Applicative ((<$)) -import Control.Category ((>>>)) -import Data.Array ((:)) -import Data.Bounded (class Bounded, bottom) -import Data.Either (Either(..), hush) -import Data.Enum (class BoundedEnum, class Enum, upFromIncluding, Cardinality(..), cardinality, fromEnum, toEnum) -import Data.Eq (class Eq) -import Data.Maybe (Maybe(..), maybe) -import Data.Monoid (mempty) -import Data.Newtype (class Newtype, unwrap, wrap) -import Data.Symbol (class IsSymbol, SProxy) -import Data.Time.Duration (Milliseconds(..)) -import Data.Variant (Variant) --- import Data.Unfoldable1 (singleton) -import Formless as F -import Formless.Internal.Transform as Internal -import Metajelo.Types as M -import Metajelo.Validation as V -import Metajelo.XPaths.Read as MR -import Prim.Row (class Cons) -import Prim.RowList (class RowToList) -import React.SyntheticEvent (SyntheticMouseEvent) -import Text.Email.Validate (EmailAddress) - --- Note: Common practice to use `Void` to represent "no error possible" - -type MKFState form = F.State form (Widget HTML) -type MKValidators form = form Record (F.Validation form (Widget HTML)) - --- | No validation is needed for this field type, as the input and ouput --- | types (`io`) are the same. -type IdentityField f io = f Void io io - -mayToString :: forall a. Show a => Maybe a -> String -mayToString (Just v) = show v -mayToString Nothing = "" - -emptyMeansOptional :: forall a. Show a => Maybe a -> String -emptyMeansOptional mayV = case mayV of - Nothing -> "(None)" - x -> mayToString x - -menu :: forall opt s form e o restF restI inputs fields - . IsSymbol s - => IsOption opt - => BoundedEnum opt - => Newtype (form Record F.FormField) (Record fields) - => Cons s (F.FormField e opt o) restF fields - => Newtype (form Variant F.InputFunction) (Variant inputs) - => Cons s (F.InputFunction e opt o) restI inputs - => form Record F.FormField - -> SProxy s - -> Widget HTML (F.Query form) -menu form field = D.select - [ P.defaultValue $ toOptionValue $ F.getInput field form - , (F.set field <<< fromOptionValue <<< P.unsafeTargetValue) <$> P.onChange - ] - (upFromIncluding (bottom :: opt) <#> \opt -> - D.option [P.value (toOptionValue opt)] [D.text (toOptionLabel opt)]) - -class IsOption a where - toOptionValue :: a -> String - toOptionLabel :: a -> String - fromOptionValue :: String -> a - ---TODO: can we automate the creation of these with purescript-reflection? - -instance isOptionMaybeInstitutionContactType - :: IsOption (Maybe M.InstitutionContactType) where - toOptionValue = mayToString - toOptionLabel = emptyMeansOptional - fromOptionValue = join <<< hush <<< MR.readInstitutionContactType - -formSaveButton :: forall form. MKFState form -> Widget HTML SyntheticMouseEvent -formSaveButton fstate = D.button props [D.text "Save"] - where props = if fstate.dirty then [P.onClick] else [P.disabled true] - - --- t1: inputs --- t2: ? --- t3: fields --- t4: form - - ---TODO: this is in formless-independent --- | Initialise the form state with default values. --- | Passing in the initial inputs, and the validations. -initFormState - :: ∀ ixs form is fs m - . RowToList is ixs - => Internal.InputFieldsToFormFields ixs is fs - => Newtype (form Record F.InputField) { | is } - => Newtype (form Record F.FormField) { | fs } - => form Record F.InputField - -> form Record (F.Validation form m) - -> F.State form m -initFormState form validations = - { validity: F.Incomplete - , dirty: false - , submitting: false - , errors: 0 - , submitAttempts: 0 - , form: Internal.inputFieldsToFormFields form - , internal: F.InternalState - { initialInputs: form - , validators: validations - , allTouched: false - -- TODO - -- , debounceRef: ... - -- , validationRef: ... - } - } diff --git a/app/src/Metajelo/UI.purs b/app/src/Metajelo/UI.purs deleted file mode 100644 index ab1b433..0000000 --- a/app/src/Metajelo/UI.purs +++ /dev/null @@ -1,90 +0,0 @@ -module Metajelo.UI where - -import Prelude (Unit, bind, discard, pure, unit, ($), (<>)) - -import Concur.Core (Widget) -import Concur.Core.FRP (Signal, display, dyn, step) -import Concur.React (HTML) -import Concur.React.DOM as D -import Concur.React.Run (runWidgetInDom) -import Data.Array.NonEmpty (NonEmptyArray) -import Data.Foldable (fold, foldMap) -import Data.Maybe (Maybe(..)) -import Data.Show (show) -import Effect (Effect) -import Metajelo.Forms as MF -import Metajelo.Types as M -import Metajelo.View as MV - -main :: Effect Unit -main = pure unit - -runFormSPA :: String -> Effect Unit -runFormSPA divId = runWidgetInDom divId page - -injectLocationFields :: - Maybe M.InstitutionID -> - Maybe String -> - Maybe M.InstitutionType -> - Maybe (Maybe String) -> - Maybe M.InstitutionContact -> - Maybe M.InstitutionSustainability -> - Maybe (NonEmptyArray M.InstitutionPolicy) -> - Maybe Boolean -> - Maybe M.Location -injectLocationFields - (Just institutionID) - (Just institutionName) - (Just institutionType) - (Just superOrganizationName) - (Just institutionContact) - (Just institutionSustainability) - (Just institutionPolicies) - (Just versioning) = pure $ { - institutionID: institutionID - , institutionName: institutionName - , institutionType: institutionType - , superOrganizationName: superOrganizationName - , institutionContact: institutionContact - , institutionSustainability: institutionSustainability - , institutionPolicies: institutionPolicies - , versioning: versioning - } -injectLocationFields _ _ _ _ _ _ _ _ = Nothing - -accumulateLocation :: Maybe M.Location -> Signal HTML (Maybe M.Location) -accumulateLocation locMay = D.div_ [] do - icMay <- MF.contactSignal Nothing - newLocMay <- pure $ injectLocationFields - Nothing - Nothing - Nothing - Nothing - icMay - Nothing - Nothing - Nothing - display $ locWidg - pure newLocMay - where - locWidg :: forall a. Widget HTML a - locWidg = D.div' [ - D.h3' [D.text "Last submitted location summary for this product:"] - , D.br' - , foldMap (\loc -> fold $ MV.spacify $ MV.locElems loc) locMay - ] - --- TODO: so far just a test of retrieving data from signals -accumulateRecord :: String -> Signal HTML String -accumulateRecord str = D.div_ [] do - ic <- MF.contactSignal Nothing - let icString = show $ ic - let newStr = "Completed Data from forms:\n" <> icString - display $ D.text newStr - pure newStr - -page :: forall a. Widget HTML a -page = do - dyn $ accumulateRecord "" - - diff --git a/app/attic/Tabs.purs b/attic/Tabs.purs similarity index 100% rename from app/attic/Tabs.purs rename to attic/Tabs.purs diff --git a/app/attic/icomoon/Read Me.txt b/attic/icomoon/Read Me.txt similarity index 100% rename from app/attic/icomoon/Read Me.txt rename to attic/icomoon/Read Me.txt diff --git a/app/attic/icomoon/demo-files/demo.css b/attic/icomoon/demo-files/demo.css similarity index 100% rename from app/attic/icomoon/demo-files/demo.css rename to attic/icomoon/demo-files/demo.css diff --git a/app/attic/icomoon/demo-files/demo.js b/attic/icomoon/demo-files/demo.js similarity index 100% rename from app/attic/icomoon/demo-files/demo.js rename to attic/icomoon/demo-files/demo.js diff --git a/app/attic/icomoon/demo.html b/attic/icomoon/demo.html similarity index 100% rename from app/attic/icomoon/demo.html rename to attic/icomoon/demo.html diff --git a/app/attic/icomoon/fonts/icomoon.eot b/attic/icomoon/fonts/icomoon.eot similarity index 100% rename from app/attic/icomoon/fonts/icomoon.eot rename to attic/icomoon/fonts/icomoon.eot diff --git a/app/attic/icomoon/fonts/icomoon.svg b/attic/icomoon/fonts/icomoon.svg similarity index 100% rename from app/attic/icomoon/fonts/icomoon.svg rename to attic/icomoon/fonts/icomoon.svg diff --git a/app/attic/icomoon/fonts/icomoon.ttf b/attic/icomoon/fonts/icomoon.ttf similarity index 100% rename from app/attic/icomoon/fonts/icomoon.ttf rename to attic/icomoon/fonts/icomoon.ttf diff --git a/app/attic/icomoon/fonts/icomoon.woff b/attic/icomoon/fonts/icomoon.woff similarity index 100% rename from app/attic/icomoon/fonts/icomoon.woff rename to attic/icomoon/fonts/icomoon.woff diff --git a/app/attic/icomoon/selection.json b/attic/icomoon/selection.json similarity index 100% rename from app/attic/icomoon/selection.json rename to attic/icomoon/selection.json diff --git a/app/attic/icomoon/style.css b/attic/icomoon/style.css similarity index 100% rename from app/attic/icomoon/style.css rename to attic/icomoon/style.css diff --git a/app/attic/metajelo.xml b/attic/metajelo.xml similarity index 100% rename from app/attic/metajelo.xml rename to attic/metajelo.xml diff --git a/app/bower.json b/bower.json similarity index 100% rename from app/bower.json rename to bower.json diff --git a/css/style.css b/css/style.css index 4b66904..0c3661d 100644 --- a/css/style.css +++ b/css/style.css @@ -20,6 +20,14 @@ html { box-sizing: border-box; } +body { + font-size: 14px; + line-height: 1.8; +} + +input { + margin-right: 5px; +} /* ----------------------------------------------------------------------------- ROOT ELEMENTS diff --git a/app/cssnano.config.js b/cssnano.config.js similarity index 100% rename from app/cssnano.config.js rename to cssnano.config.js diff --git a/package-lock.json b/package-lock.json index f3ecc66..0d4732b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -44,9 +44,9 @@ "dev": true }, "ajv": { - "version": "6.10.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.1.tgz", - "integrity": "sha512-w1YQaVGNC6t2UCPjEawK/vo/dG8OOrVtUmhBT1uJJYxbl5kU2Tj3v6LGqBcsysN1yhuCStJCCA3GqdvKY8sqXQ==", + "version": "6.10.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.2.tgz", + "integrity": "sha512-TXtUUEYHuaTEbLZWIKUr5pmBuhDLy+8KYtPYdcV8qC+pOZL+NKqYwvWSRrVXHn+ZmRRAu8vJTAznH7Oag6RVRw==", "dev": true, "requires": { "fast-deep-equal": "^2.0.1", @@ -593,6 +593,12 @@ "restore-cursor": "^2.0.0" } }, + "clones": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/clones/-/clones-1.2.0.tgz", + "integrity": "sha512-FXDYw4TjR8wgPZYui2LeTqWh1BLpfQ8lB6upMtlpDF6WlOOxghmTTxWyngdKTgozqBgKnHbTVwTE+hOHqAykuQ==", + "dev": true + }, "color-convert": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", @@ -2231,9 +2237,9 @@ } }, "execa": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/execa/-/execa-2.0.3.tgz", - "integrity": "sha512-iM124nlyGSrXmuyZF1EMe83ESY2chIYVyDRZKgmcDynid2Q2v/+GuE7gNMl6Sy9Niwf4MC0DDxagOxeMPjuLsw==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/execa/-/execa-2.0.4.tgz", + "integrity": "sha512-VcQfhuGD51vQUQtKIq2fjGDLDbL6N1DTQVpYzxZ7LPIXw3HqTuIz6uxRmpV1qf8i31LHf2kjiaGI+GdHwRgbnQ==", "dev": true, "requires": { "cross-spawn": "^6.0.5", @@ -2870,6 +2876,11 @@ "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", "dev": true }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, "jsbn": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", @@ -2938,9 +2949,9 @@ } }, "lodash": { - "version": "4.17.11", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", - "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==", + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", "dev": true }, "log-symbols": { @@ -2963,6 +2974,14 @@ "wrap-ansi": "^5.0.0" } }, + "loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "requires": { + "js-tokens": "^3.0.0 || ^4.0.0" + } + }, "lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -3266,8 +3285,7 @@ "object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", - "dev": true + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" }, "once": { "version": "1.4.0", @@ -5257,12 +5275,6 @@ "integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=", "dev": true }, - "clones": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/clones/-/clones-1.2.0.tgz", - "integrity": "sha512-FXDYw4TjR8wgPZYui2LeTqWh1BLpfQ8lB6upMtlpDF6WlOOxghmTTxWyngdKTgozqBgKnHbTVwTE+hOHqAykuQ==", - "dev": true - }, "coa": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/coa/-/coa-2.0.2.tgz", @@ -7890,9 +7902,9 @@ } }, "lodash": { - "version": "4.17.11", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", - "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==", + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", "dev": true }, "lodash.clone": { @@ -8098,7 +8110,8 @@ }, "mixin-deep": { "version": "1.3.1", - "resolved": "", + "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.1.tgz", + "integrity": "sha512-8ZItLHeEgaqEvd5lYBXfm4EZSFCX29Jb9K+lAHhDKzReKBQKj3R+7NOF6tjqYi9t4oI8VUfaWITJQm86wnXGNQ==", "dev": true, "requires": { "for-in": "^1.0.2", @@ -9472,15 +9485,6 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "dev": true }, - "safer-eval": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/safer-eval/-/safer-eval-1.3.3.tgz", - "integrity": "sha512-j/qb0rtnwTp5V1D7nR0Ns/14HU8OiHPaoZNJhM+Lfmv1nbXZCXG9LHaVW157agEocdSVAeeRNddK/yuWfalzGQ==", - "dev": true, - "requires": { - "clones": "^1.2.0" - } - }, "sax": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", @@ -9557,7 +9561,8 @@ }, "set-value": { "version": "2.0.0", - "resolved": "", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.0.tgz", + "integrity": "sha512-hw0yxk9GT/Hr5yJEYnHNKYXkIA8mVJgd9ditYZCe16ZczcaELYYcfvaXesNACk2O8O0nTiPQcQhGUQj8JLzeeg==", "dev": true, "requires": { "extend-shallow": "^2.0.1", @@ -10260,7 +10265,8 @@ }, "union-value": { "version": "1.0.0", - "resolved": "", + "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.0.tgz", + "integrity": "sha1-XHHDTLW61dzr4+oM0IIHulqhrqQ=", "dev": true, "requires": { "arr-union": "^3.1.0", @@ -11434,10 +11440,20 @@ "integrity": "sha1-mEcocL8igTL8vdhoEputEsPAKeM=", "dev": true }, + "prop-types": { + "version": "15.7.2", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz", + "integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==", + "requires": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.8.1" + } + }, "psl": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.2.0.tgz", - "integrity": "sha512-GEn74ZffufCmkDDLNcl3uuyF/aSD6exEyh1v/ZSdAomB82t6G9hzJVRx0jBmLDW+VfZqks3aScmMw9DszwUalA==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.3.0.tgz", + "integrity": "sha512-avHdspHO+9rQTLbv1RO+MPYeP/SzsCoxofjVnHanETfQhTJrmB0HlDoW+EiN/R+C0BZ+gERab9NY0lPN2TxNag==", "dev": true }, "public-encrypt": { @@ -11516,9 +11532,9 @@ "dev": true }, "purescript": { - "version": "0.13.2", - "resolved": "https://registry.npmjs.org/purescript/-/purescript-0.13.2.tgz", - "integrity": "sha512-HdB8KzEjXDUq1OXLU+FCfjWWX28suNfsrBYqpAbWVKKg6hvJs+fyCc1earfwZeVL/QimL1734AlmhRP0fQrN+A==", + "version": "0.13.3", + "resolved": "https://registry.npmjs.org/purescript/-/purescript-0.13.3.tgz", + "integrity": "sha512-YFznjWSFrl6pbds0JxWXJ/ztzyGgUsR5pvdF/wH1i1BqSqxpFtWgREUIOCCkzmuc+X9U2Ntf5DMQ56RxawE3gQ==", "dev": true, "requires": { "purescript-installer": "^0.2.0" @@ -11604,115 +11620,31 @@ } }, "react": { - "version": "16.8.6", - "resolved": "https://registry.npmjs.org/react/-/react-16.8.6.tgz", - "integrity": "sha512-pC0uMkhLaHm11ZSJULfOBqV4tIZkx87ZLvbbQYunNixAAvjnC+snJCg0XQXn9VIsttVsbZP/H/ewzgsd5fxKXw==", + "version": "16.9.0", + "resolved": "https://registry.npmjs.org/react/-/react-16.9.0.tgz", + "integrity": "sha512-+7LQnFBwkiw+BobzOF6N//BdoNw0ouwmSJTEm9cglOOmsg/TMiFHZLe2sEoN5M7LgJTj9oHH0gxklfnQe66S1w==", "requires": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1", - "prop-types": "^15.6.2", - "scheduler": "^0.13.6" - }, - "dependencies": { - "js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" - }, - "loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "requires": { - "js-tokens": "^3.0.0 || ^4.0.0" - } - }, - "object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" - }, - "prop-types": { - "version": "15.7.2", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz", - "integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==", - "requires": { - "loose-envify": "^1.4.0", - "object-assign": "^4.1.1", - "react-is": "^16.8.1" - } - }, - "react-is": { - "version": "16.8.6", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.8.6.tgz", - "integrity": "sha512-aUk3bHfZ2bRSVFFbbeVS4i+lNPZr3/WM5jT2J5omUVV1zzcs1nAaf3l51ctA5FFvCRbhrH0bdAsRRQddFJZPtA==" - }, - "scheduler": { - "version": "0.13.6", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.13.6.tgz", - "integrity": "sha512-IWnObHt413ucAYKsD9J1QShUKkbKLQQHdxRyw73sw4FN26iWr3DY/H34xGPe4nmL1DwXyWmSWmMrA9TfQbE/XQ==", - "requires": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1" - } - } + "prop-types": "^15.6.2" } }, "react-dom": { - "version": "16.8.6", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.8.6.tgz", - "integrity": "sha512-1nL7PIq9LTL3fthPqwkvr2zY7phIPjYrT0jp4HjyEQrEROnw4dG41VVwi/wfoCneoleqrNX7iAD+pXebJZwrwA==", + "version": "16.9.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.9.0.tgz", + "integrity": "sha512-YFT2rxO9hM70ewk9jq0y6sQk8cL02xm4+IzYBz75CQGlClQQ1Bxq0nhHF6OtSbit+AIahujJgb/CPRibFkMNJQ==", "requires": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1", "prop-types": "^15.6.2", - "scheduler": "^0.13.6" - }, - "dependencies": { - "js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" - }, - "loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "requires": { - "js-tokens": "^3.0.0 || ^4.0.0" - } - }, - "object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" - }, - "prop-types": { - "version": "15.7.2", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz", - "integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==", - "requires": { - "loose-envify": "^1.4.0", - "object-assign": "^4.1.1", - "react-is": "^16.8.1" - } - }, - "react-is": { - "version": "16.8.6", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.8.6.tgz", - "integrity": "sha512-aUk3bHfZ2bRSVFFbbeVS4i+lNPZr3/WM5jT2J5omUVV1zzcs1nAaf3l51ctA5FFvCRbhrH0bdAsRRQddFJZPtA==" - }, - "scheduler": { - "version": "0.13.6", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.13.6.tgz", - "integrity": "sha512-IWnObHt413ucAYKsD9J1QShUKkbKLQQHdxRyw73sw4FN26iWr3DY/H34xGPe4nmL1DwXyWmSWmMrA9TfQbE/XQ==", - "requires": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1" - } - } + "scheduler": "^0.15.0" } }, + "react-is": { + "version": "16.9.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.9.0.tgz", + "integrity": "sha512-tJBzzzIgnnRfEm046qRcURvwQnZVXmuCbscxUO5RWrGTXpon2d4c8mI0D8WE6ydVIm29JiLB6+RslkIvym9Rjw==" + }, "read": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/read/-/read-1.0.7.tgz", @@ -11951,6 +11883,15 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "dev": true }, + "safer-eval": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/safer-eval/-/safer-eval-1.3.5.tgz", + "integrity": "sha512-BJ//K2Y+EgCbOHEsDGS5YahYBcYy7JcFpKDo2ba5t4MnOGHYtk7HvQkcxTDFvjQvJ0CRcdas/PyF+gTTCay+3w==", + "dev": true, + "requires": { + "clones": "^1.2.0" + } + }, "sander": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/sander/-/sander-0.5.1.tgz", @@ -11963,6 +11904,15 @@ "rimraf": "^2.5.2" } }, + "scheduler": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.15.0.tgz", + "integrity": "sha512-xAefmSfN6jqAa7Kuq7LIJY0bwAPG3xlCj0HMEBQk1lxYiDKZscY2xJ5U/61ZTrYbmNQbXa+gc7czPkVo11tnCg==", + "requires": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1" + } + }, "semver": { "version": "5.7.0", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", @@ -12409,9 +12359,9 @@ "dev": true }, "uuid": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", - "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.3.tgz", + "integrity": "sha512-pW0No1RGHgzlpHJO1nsVrHKpOEIxkGg1xB+v0ZmdNH5OAeAwzAVrCnI2/6Mtx+Uys6iaylxa+D3g4j63IKKjSQ==", "dev": true }, "verror": { diff --git a/package.json b/package.json index 7f32249..d58b411 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,8 @@ "testbrowser": "../scripts/testbrowser", "clean": "rimraf output .cache .psci_modules .pulp-cache .spago dist prod testdist", "build": "spago build", - "prod": "../scripts/prod", + "debug": "./scripts/debug", + "prod": "./scripts/prod", "parcel": "parcel build --public-url ./ prod/index.html", "start": "spago build && parcel --no-hmr --public-url ./ static/index.html", "watch": "spago build && parcel watch static/index.html" @@ -24,13 +25,13 @@ "postcss-font-base64": "^1.0.5", "postcss-modules": "^1.4.1", "pulp": "^13.0.0", - "purescript": "^0.13.2", + "purescript": "^0.13.3", "purescript-psa": "^0.7.3", "rimraf": "^2.6.3" }, "dependencies": { - "react": "^16.8.6", - "react-dom": "^16.8.6" + "react": "^16.9.0", + "react-dom": "^16.9.0" }, "browserslist": [ "defaults" diff --git a/packages.dhall b/packages.dhall index eb3c411..4e41207 100644 --- a/packages.dhall +++ b/packages.dhall @@ -109,15 +109,31 @@ let additions = -} let mkPackage = - https://raw.githubusercontent.com/purescript/package-sets/psc-0.13.0-20190626/src/mkPackage.dhall sha256:0b197efa1d397ace6eb46b243ff2d73a3da5638d8d0ac8473e8e4a8fc528cf57 + https://raw.githubusercontent.com/purescript/package-sets/psc-0.13.2-20190725/src/mkPackage.dhall sha256:0b197efa1d397ace6eb46b243ff2d73a3da5638d8d0ac8473e8e4a8fc528cf57 let upstream = - https://raw.githubusercontent.com/purescript/package-sets/psc-0.13.0-20190626/src/packages.dhall sha256:9905f07c9c3bd62fb3205e2108515811a89d55cff24f4341652f61ddacfcf148 + https://github.com/purescript/package-sets/releases/download/psc-0.13.3-20190831/packages.dhall sha256:852cd4b9e463258baf4e253e8524bcfe019124769472ca50b316fe93217c3a47 -let overrides = {=} +let overrides = { metajelo-ui = ./spago.dhall as Location } let additions = - { concur-formless = + { codec = + mkPackage + [ "profunctor", "transformers" ] + "https://github.com/garyb/purescript-codec.git" + "v3.0.0" + , codec-argonaut = + mkPackage + [ "argonaut-core" + , "codec" + , "generics-rep" + , "variant" + , "ordered-collections" + , "type-equality" + ] + "https://github.com/garyb/purescript-codec-argonaut.git" + "v7.1.0" + , concur-formless = mkPackage [ "variant" , "heterogeneous" @@ -127,6 +143,23 @@ let additions = ] "https://github.com/ajnsit/purescript-concur-formless.git" "master" + , concur-react = + mkPackage + [ "aff" + , "arrays" + , "avar" + , "console" + , "foldable-traversable" + , "free" + , "nonempty" + , "react" + , "react-dom" + , "tailrec" + , "web-dom" + , "web-html" + ] + "https://github.com/ajnsit/purescript-concur.git" + "9887ce7c25699152ec138aefc2a3ba247fcef86c" , enums = mkPackage [ "control" @@ -141,7 +174,7 @@ let additions = , "unfoldable" ] "https://github.com/bbarker/purescript-enums.git" - "ff423fe1f994d2ca38fcf5b57b0ce911cfc889cd" + "1979eb74baec39b5e62567948f402b4194230e9f" , metajelo = mkPackage [ "generics-rep" @@ -155,7 +188,7 @@ let additions = , "xpath-like" ] "https://github.com/labordynamicsinstitute/purescript-metajelo.git" - "5fdaa5338064a94c57141c00ff9f9f4e8d40aa14" + "aad3888cd87bae7b58058ddca8b7ed71f0d246f2" , metajelo-web = mkPackage [ "prelude" @@ -168,12 +201,34 @@ let additions = , "url-validator" ] "https://github.com/labordynamicsinstitute/metajelo-web.git" - "f5d1ff27f80c03c4686bc7fa706097aed3f3a871" + "a6b9d27138ee4b36b13a67af779be1dd832d6ae8" , naturals = mkPackage [ "enums", "maybe", "prelude" ] "https://github.com/LiamGoodacre/purescript-naturals.git" "v3.0.0" + , option = + mkPackage + [ "argonaut-codecs" + , "argonaut-core" + , "codec" + , "codec-argonaut" + , "either" + , "foreign" + , "foreign-object" + , "lists" + , "maybe" + , "profunctor" + , "prelude" + , "record" + , "simple-json" + , "transformers" + , "tuples" + , "type-equality" + , "unsafe-coerce" + ] + "https://github.com/bbarker/purescript-option.git" + "b588335a4d9716237b584a8af91189e157ca349f" , stringutils = mkPackage [ "strings" @@ -190,7 +245,7 @@ let additions = mkPackage [ "nullable" ] "https://github.com/bbarker/purescript-url-validator.git" - "v1.1.0" + "v2.1.0" , web-dom-parser = mkPackage [ "prelude", "effect", "partial", "web-dom" ] @@ -206,24 +261,6 @@ let additions = [ "prelude" ] "https://github.com/bbarker/purescript-xpath-like.git" "v3.0.0" - , concur-react = - mkPackage - [ "aff" - , "arrays" - , "avar" - , "console" - , "foldable-traversable" - , "free" - , "nonempty" - , "react" - , "react-dom" - , "tailrec" - , "web-dom" - , "web-html" - ] - "https://github.com/ajnsit/purescript-concur.git" - "v0.3.8" - } in upstream // overrides // additions diff --git a/app/postcss.config.js b/postcss.config.js similarity index 100% rename from app/postcss.config.js rename to postcss.config.js diff --git a/psc.sh b/psc.sh index a7f923c..6c1bd44 100755 --- a/psc.sh +++ b/psc.sh @@ -45,6 +45,7 @@ docker run --rm -ti \ --workdir /wd \ -e "XDG_CONFIG_HOME=/wd/.xdg_config_home" \ -e "XDG_DATA_HOME=/wd/.xdg_data_home" \ + -e "XDG_CACHE_HOME=/wd/.xdg_cache_home" \ "${DHUB_PREFIX}${IMG_NAME}:${IMG_VER}" "$@" # Add this before the last line (image name) for debugging: diff --git a/scripts/debug b/scripts/debug new file mode 100755 index 0000000..1119b67 --- /dev/null +++ b/scripts/debug @@ -0,0 +1,18 @@ +#!/usr/bin/env sh + +# Like prod, but doesn't minify + +GCC=$(npm bin)/google-closure-compiler + +preGccBuildSteps () { + rimraf prod dist && mkdir prod && ln -sfn ../css prod/css && \ + spago build && spago bundle-module --main Metajelo.UI --to prod/index.prod.js && \ + cp static/index.* prod/ +} + +preGccBuildSteps || { echo 'preGccBuildSteps failed' ; exit 1; } +cp prod/index.prod.js prod/index.opt.js && \ +# "$GCC" --js prod/index.prod.js --js_output_file prod/index.opt.js && \ + parcel build --public-url ./ prod/index.html --no-minify +# parcel build --public-url ./ prod/index.html + diff --git a/scripts/dist_build_commands.sh b/scripts/dist_build_commands.sh index 709d606..e6356d1 100755 --- a/scripts/dist_build_commands.sh +++ b/scripts/dist_build_commands.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash npm install && \ -(cd app && npm run build && npm run prod) && \ -(cd test && npm run testbrowser) +(npm run build && npm run prod) && \ +(cd tests && npm run testbrowser) diff --git a/scripts/prod b/scripts/prod index 6efa698..ee1890c 100755 --- a/scripts/prod +++ b/scripts/prod @@ -3,13 +3,15 @@ GCC=$(npm bin)/google-closure-compiler preGccBuildSteps () { - rimraf prod dist && mkdir prod && ln -sfn ../../css prod/css && \ + rimraf prod dist && mkdir prod && ln -sfn ../css prod/css && \ spago build && spago bundle-module --main Metajelo.UI --to prod/index.prod.js && \ cp static/index.* prod/ } - +# FIXME: parcel's --no-minify with GCC currently creates a runtime error +# FIXME with e.identity; either minifier alone will work. +# FIXME (introduced in purs 0.13.3, but doesn't affect metajelo-web) preGccBuildSteps || { echo 'preGccBuildSteps failed' ; exit 1; } "$GCC" --js prod/index.prod.js --js_output_file prod/index.opt.js && \ - parcel build --public-url ./ prod/index.html + parcel build --public-url ./ prod/index.html --no-minify diff --git a/app/spago.dhall b/spago.dhall similarity index 92% rename from app/spago.dhall rename to spago.dhall index 19d4947..941ff9a 100644 --- a/app/spago.dhall +++ b/spago.dhall @@ -15,11 +15,12 @@ You can edit this file as you like. , "metajelo" , "metajelo-web" , "naturals" + , "option" , "profunctor" , "stringutils" , "url-validator" , "variant" ] , packages = - ../packages.dhall + ./packages.dhall } diff --git a/src/Metajelo/FormUtil.purs b/src/Metajelo/FormUtil.purs new file mode 100644 index 0000000..88d2d28 --- /dev/null +++ b/src/Metajelo/FormUtil.purs @@ -0,0 +1,349 @@ +module Metajelo.FormUtil where + +import Prelude (class Bounded, class Eq, class Ord, class Show, Void, bind, discard, join, map, max, not, pure, show, ($), (+), (-), (<), (<#>), (<$), (<$>), (<<<), (<>)) + +import Concur.Core (Widget) +import Concur.Core.FRP (Signal, display, loopS, step) +import Concur.React (HTML) +import Concur.React.DOM as D +import Concur.React.Props as P +import Control.Applicative (class Applicative) +import Control.Apply (class Apply, apply) +import Control.Extend (class Extend) +import Data.Array (catMaybes, filter, length, replicate, (..)) +import Data.Array.NonEmpty (NonEmptyArray, fromArray, toArray) +import Data.Bounded (bottom) +import Data.Either (Either(..), hush) +import Data.Enum (class BoundedEnum, class Enum, class SmallBounded, upFromIncluding) +import Data.Foldable (class Foldable, fold) +import Data.Functor (class Functor) +import Data.Generic.Rep (class Generic) +import Data.Generic.Rep.Bounded as GBounded +import Data.Generic.Rep.Eq (genericEq) +import Data.Generic.Rep.Enum as GEnum +import Data.Generic.Rep.Ord as GOrd +import Data.Generic.Rep.Show (genericShow) +import Data.Maybe (Maybe(..), fromJust, fromMaybe, maybe) +import Data.Monoid (class Monoid, mempty) +import Data.Newtype (class Newtype) +import Data.Profunctor.Strong (second) +import Data.String (trim) +import Data.String.NonEmpty (NonEmptyString, fromString, toString) +import Data.Symbol (class IsSymbol, SProxy) +-- import Data.Time.Duration (Milliseconds(..)) -- What was I doing with this? +import Data.Traversable (traverse) +import Data.Tuple (Tuple(..), fst, snd) +import Data.Unit (Unit) +import Data.Variant (Variant) +import Effect.Class (liftEffect) +import Effect.Class.Console (logShow) +-- import Data.Unfoldable1 (singleton) +import Formless as F +import Formless.Internal.Transform as Internal +import Metajelo.Types as M +import Metajelo.Validation as V +import Metajelo.XPaths.Read as MR +import Partial.Unsafe (unsafePartial) +import Prim.Row (class Cons) +import Prim.RowList (class RowToList) +import React.SyntheticEvent (SyntheticMouseEvent) +import Text.URL.Validate (URL, parsePublicURL, urlToNEString) + +import Prim.TypeError (QuoteLabel, class Warn) + +-- Note: Common practice to use `Void` to represent "no error possible" + +type MKFState form = F.State form (Widget HTML) +type MKValidators form = form Record (F.Validation form (Widget HTML)) + +-- | No validation is needed for this field type, as the input and ouput +-- | types (`io`) are the same. +type IdentityField f io = f Void io io + +mayToString :: ∀ a. Show a => Maybe a -> String +mayToString mayA = fold $ show <$> mayA + +foldf :: ∀ a f m. Foldable f => Functor f => Monoid m => + (a -> m) -> f a -> m +foldf f vals = fold $ f <$> vals + +emptyMeansOptional :: ∀ a. Show a => Maybe a -> String +emptyMeansOptional mayV = case mayV of + Nothing -> "(None)" + x -> mayToString x + +menu :: ∀ opt s form e o restF restI inputs fields + . IsSymbol s + => IsOption opt + => BoundedEnum opt + => Newtype (form Record F.FormField) (Record fields) + => Cons s (F.FormField e opt o) restF fields + => Newtype (form Variant F.InputFunction) (Variant inputs) + => Cons s (F.InputFunction e opt o) restI inputs + => form Record F.FormField + -> SProxy s + -> Widget HTML (F.Query form) +menu form field = D.select + [ P.defaultValue $ toOptionValue $ F.getInput field form + , (F.set field <<< fromOptionValue <<< P.unsafeTargetValue) <$> P.onChange + ] + (upFromIncluding (bottom :: opt) <#> \opt -> + D.option [P.value (toOptionValue opt)] [D.text (toOptionLabel opt)]) + +-- | A non-formless incantation of menu +menuSignal :: ∀ opt. BoundedEnum opt => IsOption opt => + CtrlSignal HTML (Maybe opt) +menuSignal currentOptMay = step currentOptMay do + newOpt <- D.select [ + P.defaultValue $ maybe "" toOptionValue currentOptMay + , (fromOptionValue <<< P.unsafeTargetValue) <$> P.onChange + ] ( + upFromIncluding (bottom :: opt) <#> \opt -> + D.option [P.value (toOptionValue opt)] [D.text (toOptionLabel opt)]) + pure $ menuSignal $ Just newOpt + + +type CtrlSignal v a = a -> Signal v a + +-- | Prepend a label heading to a siginal +labelSig' :: forall a. D.El' -> String -> Signal HTML a -> Signal HTML a +labelSig' tag label sigIn = do + display $ tag [D.text label] + sigIn + +textInput' :: D.El' -> String -> CtrlSignal HTML String +textInput' tag label initVal = labelSig' tag label $ sig initVal + where + sig :: String -> Signal HTML String + sig txt = step txt do + newTxt <- D.input [P.unsafeTargetValue <$> P.onChange] + pure $ sig newTxt + +-- | Reasonable defaults for filtering input text +textFilter :: Signal HTML String -> Signal HTML (Maybe NonEmptyString) +textFilter txtSig = do + txt <- txtSig + pure $ fromString $ trim txt + +textInput :: D.El' -> String -> CtrlSignal HTML (Maybe NonEmptyString) +textInput tag label iVal = textFilter $ textInput' tag label + (foldf toString iVal) + +urlInput :: D.El' -> String -> CtrlSignal HTML (Either String URL) +urlInput tag label iVal = do + txtMay :: Maybe NonEmptyString <- textInput tag label (fromString prevTxt) + urlEi <- pure $ case txtMay of + Nothing -> Left prevErr + Just txt -> parsePublicURL $ toString txt + display $ case urlEi of + Right _ -> mempty + Left err -> errorDisplay $ Just err + pure urlEi + where + prevErr :: String + prevErr = case iVal of + Left err -> err + Right _ -> "" + prevTxt :: String + prevTxt = case iVal of + Left _ -> "" + Right url -> toString $ urlToNEString url + +checkBoxS :: Boolean -> Signal HTML Boolean +checkBoxS b = step b do + newB <- checkW + pure $ checkBoxS newB + where checkW = checkBoxW b + +checkBoxW :: Boolean -> Widget HTML Boolean +checkBoxW b = not b <$ D.input [P._type "checkbox", P.checked b, P.onChange] + +class IsOption a where + toOptionValue :: a -> String + toOptionLabel :: a -> String + fromOptionValue :: String -> a + +instance isOptionMaybeBoolean + :: IsOption (Maybe Boolean) where + toOptionValue = mayToString + toOptionLabel = emptyMeansOptional + fromOptionValue = hush <<< MR.readBoolean + +instance isOptionIdentifierType + :: IsOption M.IdentifierType where + toOptionValue = show + toOptionLabel = show + fromOptionValue x = unsafePartial $ fromJust $ hush $ MR.readIdentifierType x + +instance isOptionInstitutionType + :: IsOption M.InstitutionType where + toOptionValue = show + toOptionLabel = show + fromOptionValue x = unsafePartial $ fromJust $ hush $ MR.readInstitutionType x + +instance isOptionMaybeInstitutionContactType + :: IsOption (Maybe M.InstitutionContactType) where + toOptionValue = mayToString + toOptionLabel = emptyMeansOptional + fromOptionValue = join <<< hush <<< MR.readInstitutionContactType + +instance isOptionMaybePolicyType + :: IsOption (Maybe M.PolicyType) where + toOptionValue = mayToString + toOptionLabel = emptyMeansOptional + fromOptionValue = join <<< hush <<< MR.readPolicyType + +-- | 0-arg constructors for M.Policy and can be used for dropdown or radio box. +data PolPolType + = FreeTextPolicy + | RefPolicy +derive instance genericPolPolType :: Generic PolPolType _ +instance showPolPolType :: Show PolPolType where + show = genericShow +instance eqPolPolType :: Eq PolPolType where + eq = genericEq +instance ordPolPolType :: Ord PolPolType where + compare x y = GOrd.genericCompare x y +instance boundedPolPolType :: Bounded PolPolType where + bottom = GBounded.genericBottom + top = GBounded.genericTop +instance enumPolPolType :: Enum PolPolType where + pred = GEnum.genericPred + succ = GEnum.genericSucc +instance boundedEnumPolPolType :: BoundedEnum PolPolType where + cardinality = GEnum.genericCardinality + toEnum = GEnum.genericToEnum + fromEnum = GEnum.genericFromEnum +instance smallBoundedPolPolType :: SmallBounded PolPolType + +readPolPolType :: String -> Either String PolPolType +readPolPolType "FreeTextPolicy" = pure FreeTextPolicy +readPolPolType "RefPolicy" = pure RefPolicy +readPolPolType unknown = Left $ "Unknown Policy: '" <> unknown <> "'" + +instance isOptionPolPolType :: IsOption PolPolType where + toOptionValue = show + toOptionLabel = show + fromOptionValue = fromMaybe FreeTextPolicy <<< hush <<< readPolPolType + +formSaveButton :: ∀ form. MKFState form -> Widget HTML SyntheticMouseEvent +formSaveButton fstate = D.button props [D.text "Save"] + where props = if fstate.dirty then [P.onClick] else [P.disabled true] + +data Item a + = Keep (Maybe a) + | Delete (Maybe a) +instance showItem :: Show a => Show (Item a) where + show (Keep x) = "(Keep " <> show x <> ")" + show (Delete x) = "(Delete " <> show x <> ")" +instance functorItem :: Functor Item where + map fun (Keep mVal) = Keep $ map fun mVal + map fun (Delete mVal) = Delete $ map fun mVal +instance applyItem :: Apply Item where + apply (Keep mFun) (Keep mVal) = Keep $ apply mFun mVal + apply (Keep mFun) (Delete mVal) = Keep $ apply mFun mVal + apply (Delete mFun) (Keep mVal) = Keep $ apply mFun mVal + apply (Delete mFun) (Delete mVal) = Delete $ apply mFun mVal +instance applicativeItem :: Applicative Item where + pure x = Keep $ Just x +instance extendItem :: Extend Item where + extend iMayAtoB iMay = Keep $ Just $ iMayAtoB iMay + +toMaybe :: ∀ a. Item a -> Maybe a +toMaybe (Keep mVal) = mVal +toMaybe (Delete mVal) = mVal + +isKeep :: ∀ a. Item a -> Boolean +isKeep (Keep _) = true +isKeep _ = false + +arrayView :: ∀ a. CtrlSignal HTML (Maybe a) -> CtrlSignal HTML (Tuple Int (Array a)) +arrayView mkWidget oldArrTup = D.div_ [] do + mayArrTup <- arrayViewLoop minWidgets initVals + pure $ second (catMaybes <<< (map toMaybe)) mayArrTup + where + minWidgets = fst oldArrTup + oldArr = snd oldArrTup + emptyElem = Keep Nothing + initVals :: Array (Item a) + initVals = (Keep <<< Just <$> oldArr) <> + (dummyArr <#> (\_ -> emptyElem)) + where + numEmpty = max 0 (minWidgets - (length oldArr)) + dummyArr = if numEmpty < 1 then [] else (1 .. numEmpty) + mkItemView :: Item a -> Signal HTML (Item a) + mkItemView item = case item of + Delete _ -> step (Delete Nothing) mempty + Keep _ -> mkItemViewDel item + mkItemViewDel :: Item a -> Signal HTML (Item a) + mkItemViewDel item = D.li_ [] do + curVal <- mkWidget $ toMaybe item + newItem <- delButton $ Keep curVal + pure newItem + delButton :: Item a -> Signal HTML (Item a) + delButton item = step item $ do + delMay <- (Delete $ toMaybe item) <$ D.button [P.onClick] [D.text "Delete"] + pure $ delButton delMay + arrayViewLoop :: Int -> Array (Item a) -> + Signal HTML (Tuple Int (Array (Item a))) + arrayViewLoop widgCountIn mayArr = loopS (Tuple widgCountIn mayArr) \tupIn -> + D.div_ [] do + let widgCountIn' = fst tupIn + let mayArr' = snd tupIn + oneOrZero <- step 0 $ + (pure 1) <$ D.button [P.onClick] [D.text "Add item"] + mayArrNewUnfiltered <- traverse mkItemView mayArr' + let mayArrNew = filter isKeep mayArrNewUnfiltered + let widgCountNew = length mayArrNew + oneOrZero + let emptyArrLen = max 0 oneOrZero + emptyArr <- traverse mkItemViewDel (replicate emptyArrLen emptyElem) + -- _ <- consoleShow $ length $ mayArr -- FIXME DEBUG + pure $ Tuple widgCountNew $ mayArrNew <> emptyArr + +nonEmptyArrayView :: ∀ a. CtrlSignal HTML (Maybe a) -> + CtrlSignal HTML (Tuple Int (Maybe (NonEmptyArray a))) +nonEmptyArrayView mkWidget oldNeArrMay = do + arrayA <- arrayView mkWidget (second (foldf toArray) oldNeArrMay) + pure $ second fromArray arrayA + +errorDisplay :: ∀ a e. V.ToText e => Maybe e -> Widget HTML a +errorDisplay = maybe mempty (\err -> + D.div [P.style {color: "red"}] [D.text $ V.toText err] +) + +--TODO: this is in formless-independent +-- | Initialise the form state with default values. +-- | Passing in the initial inputs, and the validations. +initFormState + :: ∀ ixs form is fs m + . RowToList is ixs + => Internal.InputFieldsToFormFields ixs is fs + => Newtype (form Record F.InputField) { | is } + => Newtype (form Record F.FormField) { | fs } + => form Record F.InputField + -> form Record (F.Validation form m) + -> F.State form m +initFormState form validations = + { validity: F.Incomplete + , dirty: false + , submitting: false + , errors: 0 + , submitAttempts: 0 + , form: Internal.inputFieldsToFormFields form + , internal: F.InternalState + { initialInputs: form + , validators: validations + , allTouched: false + -- TODO + -- , debounceRef: ... + -- , validationRef: ... + } + } + + +-- NOTE: comment out for production builds +consoleShow :: ∀ a. Show a => Warn (QuoteLabel "consoleShow in use") => + a -> Signal HTML Unit +consoleShow val = display $ do + liftEffect $ logShow val -- FIXME: DEBUG + mempty diff --git a/app/src/Metajelo/Forms.purs b/src/Metajelo/Forms.purs similarity index 57% rename from app/src/Metajelo/Forms.purs rename to src/Metajelo/Forms.purs index db6fa0c..9a3e686 100644 --- a/app/src/Metajelo/Forms.purs +++ b/src/Metajelo/Forms.purs @@ -1,5 +1,7 @@ module Metajelo.Forms ( module Metajelo.Forms.InstitutionContact +, module Metajelo.Forms.InstitutionPolicy ) where import Metajelo.Forms.InstitutionContact (contactSignal) +import Metajelo.Forms.InstitutionPolicy (policySigArray) diff --git a/app/src/Metajelo/Forms/InstitutionContact.purs b/src/Metajelo/Forms/InstitutionContact.purs similarity index 72% rename from app/src/Metajelo/Forms/InstitutionContact.purs rename to src/Metajelo/Forms/InstitutionContact.purs index b33e544..8767668 100644 --- a/app/src/Metajelo/Forms/InstitutionContact.purs +++ b/src/Metajelo/Forms/InstitutionContact.purs @@ -3,20 +3,17 @@ module Metajelo.Forms.InstitutionContact where import Prelude (bind, pure, ($), (<$>), (<<<)) import Concur.Core (Widget) -import Concur.Core.FRP (Signal, step) +import Concur.Core.FRP (step) import Concur.React (HTML) import Concur.React.DOM as D import Concur.React.Props as P import Control.Applicative ((<$)) -import Control.Category ((>>>)) import Data.Either (Either(..)) import Data.Foldable (foldMap) -import Data.Maybe (Maybe(..), maybe) -import Data.Monoid (mempty) +import Data.Maybe (Maybe(..)) import Data.Newtype (class Newtype) import Formless as F -import Formless.Internal.Transform as Internal -import Metajelo.FormUtil (IdentityField, MKFState, MKValidators, formSaveButton, initFormState, menu) +import Metajelo.FormUtil (CtrlSignal, IdentityField, MKFState, MKValidators, errorDisplay, formSaveButton, initFormState, labelSig', menu) import Metajelo.Types as M import Metajelo.Validation as V import Metajelo.View (contactWidg) @@ -66,7 +63,7 @@ contactForm fstate = do query <- D.div' [ D.div' [D.text "Email"] , D.input [ - P.value $ F.getInput proxies.email1 fstate.form + P.defaultValue $ F.getInput proxies.email1 fstate.form , (F.setValidate proxies.email1 <<< P.unsafeTargetValue) <$> P.onChange ] , errorDisplay $ F.getError proxies.email1 fstate.form @@ -79,21 +76,16 @@ contactForm fstate = do Right out -> do let form = F.unwrapOutputFields out pure {emailAddress: form.email1, contactType: form.contactType} - where - errorDisplay = maybe mempty (\err -> - D.div [P.style {color: "red"}] [D.text $ V.toText err] - ) - - -contactSignal :: Maybe M.InstitutionContact - -> Signal HTML (Maybe M.InstitutionContact) -contactSignal instContactMay = step instContactMay do - inputs <- pure $ F.wrapInputFields $ outToInRec instContactMay - instContact <- D.div' [ - D.h2' [D.text "Institution Contact"] - , contactForm (initFormState inputs validators) - , foldMap contactWidg instContactMay - ] - pure $ contactSignal $ Just instContact +contactSignal :: CtrlSignal HTML (Maybe M.InstitutionContact) +contactSignal instContactMay = labelSig' D.h2' "Institution Contact" $ + sig instContactMay + where + sig icMay = step icMay do + inputs <- pure $ F.wrapInputFields $ outToInRec icMay + instContact <- D.div' [ + contactForm (initFormState inputs validators) + , foldMap contactWidg icMay + ] + pure $ sig $ Just instContact diff --git a/src/Metajelo/Forms/InstitutionPolicy.purs b/src/Metajelo/Forms/InstitutionPolicy.purs new file mode 100644 index 0000000..ac534ee --- /dev/null +++ b/src/Metajelo/Forms/InstitutionPolicy.purs @@ -0,0 +1,134 @@ +module Metajelo.Forms.InstitutionPolicy where + +import Prelude (class Monad, bind, discard, pure, ($), (<#>), (<$>), (<<<)) + +import Concur.Core (Widget) +import Concur.Core.FRP (step) +import Concur.React (HTML) +import Concur.React.DOM as D +import Concur.React.Props as P +import Control.Applicative ((<$)) +import Data.Array.NonEmpty (NonEmptyArray) +import Data.Either (Either(..)) +import Data.Foldable (foldMap) +import Data.Maybe (Maybe(..)) +import Data.Newtype (class Newtype) +import Data.String.NonEmpty (toString) +import Data.Tuple (Tuple) +import Effect.Class (liftEffect) +import Effect.Class.Console (logShow) +import Formless as F +import Formless.Validation (Validation, hoistFnE) +import Metajelo.FormUtil (CtrlSignal, IdentityField, MKFState, MKValidators, PolPolType(..), errorDisplay, formSaveButton, initFormState, labelSig', menu, nonEmptyArrayView) +import Metajelo.Types as M +import Metajelo.Validation as V +import Metajelo.View (ipolicyWidg) +import Text.URL.Validate (parsePublicURL, urlToString) + + +newtype InstPolicyForm r f = InstPolicyForm (r ( + polPolType :: IdentityField f PolPolType + , policy :: f String String M.Policy + , policyType :: IdentityField f (Maybe M.PolicyType) + , appliesToProd :: IdentityField f (Maybe Boolean) + )) + +derive instance newtypeInstPolicyForm :: Newtype (InstPolicyForm r f) _ + +proxies :: F.SProxies InstPolicyForm +proxies = F.mkSProxies (F.FormProxy :: F.FormProxy InstPolicyForm) + +-- Some type helpers +type InputForm = InstPolicyForm Record F.InputField +type Validators = MKValidators InstPolicyForm +type FState = MKFState InstPolicyForm + +type InputRecord = { + polPolType :: PolPolType +, policy :: String +, policyType :: Maybe M.PolicyType +, appliesToProd :: Maybe Boolean +} + +initialInputsRecord :: InputRecord +initialInputsRecord = { + polPolType: FreeTextPolicy +, policy: "" +, policyType: Nothing +, appliesToProd: Nothing +} + +pol2ZeroArg :: M.Policy -> PolPolType +pol2ZeroArg (M.FreeTextPolicy _) = FreeTextPolicy +pol2ZeroArg (M.RefPolicy _) = RefPolicy + +polStrContent :: M.Policy -> String +polStrContent (M.FreeTextPolicy txt) = toString txt +polStrContent (M.RefPolicy url) = urlToString url + +outToInRec :: Maybe M.InstitutionPolicy -> InputRecord +outToInRec Nothing = initialInputsRecord +outToInRec (Just outRec) = { + polPolType: pol2ZeroArg outRec.policy +, policy: polStrContent outRec.policy +, policyType: outRec.policyType +, appliesToProd: outRec.appliesToProduct +} + +validators :: Validators +validators = InstPolicyForm { + polPolType: V.dummy +, policy: checkPolicy +, policyType: V.dummy +, appliesToProd: V.dummy +} + +-- TODO: add help tooltip (hover?) for reference policy, etc. +policyForm :: FState -> Widget HTML M.InstitutionPolicy +policyForm fstate = do + query <- D.div' [ + D.div' [D.text "Policy: ", menu fstate.form proxies.polPolType] + , D.input [ + P.defaultValue $ F.getInput proxies.policy fstate.form + , (F.setValidate proxies.policy <<< P.unsafeTargetValue) <$> P.onChange + ] + , errorDisplay $ F.getError proxies.policy fstate.form + , D.div' [D.text "Policy type: ", menu fstate.form proxies.policyType] + , D.div' [D.text "Applies to product? ", menu fstate.form proxies.appliesToProd] + , D.div' [ F.submit <$ formSaveButton fstate] + ] + res <- F.eval query fstate + case res of + Left fstate' -> policyForm fstate' + Right out -> do + let form = F.unwrapOutputFields out + pure { + policy: form.policy + , policyType: form.policyType + , appliesToProduct: form.appliesToProd + } + +policySignal ::CtrlSignal HTML (Maybe M.InstitutionPolicy) +policySignal instPolicyMay = labelSig' D.h3' "Institution Policy" $ + sig instPolicyMay + where + sig ipMay = step ipMay do + inputs <- pure $ F.wrapInputFields $ outToInRec ipMay + instPolicy <- D.div' [ + policyForm (initFormState inputs validators) + , foldMap ipolicyWidg ipMay + ] + liftEffect $ logShow instPolicy + pure $ sig $ Just instPolicy + +checkPolicy :: ∀ m. Monad m => Validation InstPolicyForm m String String M.Policy +checkPolicy = hoistFnE $ \form str -> + let pType = F.getInput proxies.polPolType form + in case pType of + FreeTextPolicy -> (V.readNEStringEi str) <#> M.FreeTextPolicy + RefPolicy -> (parsePublicURL str) <#> M.RefPolicy + + -- | The first element of the tuple is the (desired) number of policies +policySigArray :: CtrlSignal HTML (Tuple Int (Maybe (NonEmptyArray M.InstitutionPolicy))) +policySigArray instPoliciesMay = labelSig' D.h2' "Institution Policies" $ + nonEmptyArrayView policySignal instPoliciesMay diff --git a/src/Metajelo/UI.purs b/src/Metajelo/UI.purs new file mode 100644 index 0000000..358e4eb --- /dev/null +++ b/src/Metajelo/UI.purs @@ -0,0 +1,289 @@ +module Metajelo.UI where + +import Prelude (Unit, bind, discard, join, pure, unit, ($), (>>=)) + +import Concur.Core (Widget) +import Concur.Core.FRP (Signal, display, dyn, loopS) +import Concur.React (HTML) +import Concur.React.DOM as D +-- import Concur.React.Props as P +import Concur.React.Run (runWidgetInDom) +import Control.Monad.State +import Data.Array.NonEmpty (NonEmptyArray) +import Data.Either (Either(..), hush) +import Data.Foldable (fold, foldMap) +import Data.Maybe (Maybe(..)) +import Data.String.NonEmpty (NonEmptyString) +import Data.Symbol (class IsSymbol, SProxy(..)) +import Data.Tuple (Tuple(..), fst, snd) +import Effect (Effect) +import Metajelo.Forms as MF +import Metajelo.FormUtil (CtrlSignal, checkBoxS, labelSig', menuSignal, textInput, urlInput {- , consoleShow -}) +import Metajelo.Types as M +import Metajelo.View as MV +import Option as Opt +import Prim.Row as Prim.Row +import Text.URL.Validate (URL) + +-- import Data.Newtype (unwrap) +-- import Data.Semigroup.First (First(..)) + +main :: Effect Unit +main = pure unit + +runFormSPA :: String -> Effect Unit +runFormSPA divId = runWidgetInDom divId page + +-- | Decorated state (Model + ViewModel) for Location +type LocationRowOpts = ( + institutionID_opt :: Opt.Option (M.BaseIdRows ()) +, _numPolicies :: Int +, iSustain_opt :: Opt.Option InstitutionSustainabilityRowOpts +| M.LocationRows +) + +type InstitutionSustainabilityRowOpts = ( + missionUrl_Ei :: Either String URL +, fundingUrl_Ei :: Either String URL +| M.InstitutionSustainabilityRows +) + +injectLocationFields :: + Maybe M.InstitutionID -> + Maybe NonEmptyString -> + Maybe M.InstitutionType -> + Maybe NonEmptyString -> + Maybe M.InstitutionContact -> + Maybe M.InstitutionSustainability -> + Maybe (NonEmptyArray M.InstitutionPolicy) -> + Boolean -> + Maybe M.Location +injectLocationFields + (Just institutionID) + (Just institutionName) + (Just institutionType) + superOrganizationName + (Just institutionContact) + (Just institutionSustainability) + (Just institutionPolicies) + versioning = pure $ { + institutionID: institutionID + , institutionName: institutionName + , institutionType: institutionType + , superOrganizationName: superOrganizationName + , institutionContact: institutionContact + , institutionSustainability: institutionSustainability + , institutionPolicies: institutionPolicies + , versioning: versioning + } +injectLocationFields _ _ _ _ _ _ _ _ = Nothing + +injectLocationFieldsOpt :: + Opt.Option LocationRowOpts -> + Opt.Option (M.BaseIdRows ()) -> + Maybe M.InstitutionID -> + Maybe NonEmptyString -> + Maybe M.InstitutionType -> + Maybe NonEmptyString -> + Maybe M.InstitutionContact -> + Opt.Option InstitutionSustainabilityRowOpts -> + Maybe M.InstitutionSustainability -> + Int -> + Maybe (NonEmptyArray M.InstitutionPolicy) -> + Boolean -> + Opt.Option LocationRowOpts +injectLocationFieldsOpt + oldOpt + institutionID_opt + institutionIDMay + institutionNameMay + institutionTypeMay + superOrganizationName + institutionContactMay + iSustain_opt + institutionSustainabilityMay + _numPolicies + institutionPoliciesMay + versioning = execState (do + get >>= Opt.maySetOptState (SProxy :: _ "institutionID_opt") + (Just institutionID_opt) + get >>= Opt.maySetOptState (SProxy :: _ "institutionID") institutionIDMay + get >>= Opt.maySetOptState (SProxy :: _ "institutionName") institutionNameMay + get >>= Opt.maySetOptState (SProxy :: _ "institutionType") institutionTypeMay + get >>= Opt.maySetOptState (SProxy :: _ "superOrganizationName") + (Just superOrganizationName) + get >>= Opt.maySetOptState (SProxy :: _ "institutionContact") + institutionContactMay + get >>= Opt.maySetOptState (SProxy :: _ "iSustain_opt") + (Just iSustain_opt) + get >>= Opt.maySetOptState (SProxy :: _ "institutionSustainability") + institutionSustainabilityMay + get >>= Opt.maySetOptState (SProxy :: _ "_numPolicies") (Just _numPolicies) + get >>= Opt.maySetOptState (SProxy :: _ "institutionPolicies") + institutionPoliciesMay + get >>= Opt.maySetOptState (SProxy :: _ "versioning") (Just versioning) + ) oldOpt + +accumulateLocation :: Signal HTML (Opt.Option LocationRowOpts) +accumulateLocation = labelSig' D.h1' "Location" $ + loopS Opt.empty \locOpt -> D.div_ [] do + identOpt <- accumulateIdent "Identifier" $ + getOpt (SProxy :: _ "institutionID_opt") locOpt + let identMay = injectIdentFields identOpt + instNameMay <- textInput D.span' "Institution Name: " $ + Opt.get (SProxy :: _ "institutionName") locOpt + instTypeMay <- labelSig' D.h3' "Institution Type" $ menuSignal $ + Opt.get (SProxy :: _ "institutionType") locOpt + display D.br' + sOrgMay <- textInput D.span' "Super Organization (optional): " $ + join $ Opt.get (SProxy :: _ "superOrganizationName") locOpt + icMay <- MF.contactSignal $ Opt.get (SProxy :: _ "institutionContact") locOpt + -- display $ D.div' [D.text $ "Contact" <> (show icMay)] -- FIXME: DEBUG + sustainOpt <- accumulateSustain "Institution Sustainability:" $ + getOpt (SProxy :: _ "iSustain_opt") locOpt + let sustainMay = injectSustainFields sustainOpt + polsMayTup <- MF.policySigArray $ Tuple + (Opt.getWithDefault 1 (SProxy :: _ "_numPolicies") locOpt) + (Opt.get (SProxy :: _ "institutionPolicies") locOpt) + let _numPolicies = fst polsMayTup + let polsMay = snd polsMayTup + versioning <- labelSig' D.span' "versioning? " $ checkBoxS $ + Opt.getWithDefault false (SProxy :: _ "versioning") locOpt + newLoc <- pure $ injectLocationFieldsOpt locOpt + identOpt + identMay + instNameMay + instTypeMay + sOrgMay + icMay + sustainOpt + sustainMay + _numPolicies + polsMay + versioning + newLocMay <- pure $ injectLocationFields -- TODO: use sequencing to get newLocMay from newLoc + identMay + instNameMay + instTypeMay + sOrgMay + icMay + sustainMay + polsMay + versioning +{- _ <- consoleShow $ "identMay: " <> show (Opt.get (SProxy :: _ "institutionID") newLoc) -- FIXME + _ <- consoleShow $ "instNameMay: " <> show (Opt.get (SProxy :: _ "institutionName") newLoc) -- FIXME + _ <- consoleShow $ "instTypeMay: " <> show (Opt.get (SProxy :: _ "institutionType") newLoc) -- FIXME + _ <- consoleShow $ "sOrgMay: " <> show (Opt.get (SProxy :: _ "superOrganizationName") newLoc) -- FIXME + _ <- consoleShow $ "icMay: " <> show (Opt.get (SProxy :: _ "institutionContact") newLoc) -- FIXME + _ <- consoleShow $ "sustainMay: " <> show (Opt.get (SProxy :: _ "institutionSustainability") newLoc) -- FIXME + _ <- consoleShow $ "polsMay: " <> show (Opt.get (SProxy :: _ "institutionPolicies") newLoc) -- FIXME + _ <- consoleShow $ "versioning: " <> show (versioning) -- FIXME -} + + display $ locWidg newLocMay + pure newLoc + where + locWidg :: forall a. Maybe M.Location -> Widget HTML a + locWidg locMay = D.div' [ + D.h3' [D.text "Last submitted location summary for this product:"] + , D.br' + , foldMap (\loc -> fold $ MV.spacify $ MV.locElems loc) locMay + ] + +page :: ∀ a. Widget HTML a +page = do + -- testWidget + dyn $ accumulateLocation + +injectSustainFields :: + Opt.Option InstitutionSustainabilityRowOpts -> + Maybe M.InstitutionSustainability +injectSustainFields sustOpt = go + (Opt.get (SProxy :: _ "missionStatementURL") sustOpt) + (Opt.get (SProxy :: _ "fundingStatementURL") sustOpt) + where + go (Just mission) (Just funding) = pure $ { + missionStatementURL: mission + , fundingStatementURL: funding + } + go _ _ = Nothing + +injectSustainFieldsOpt :: + Opt.Option InstitutionSustainabilityRowOpts -> + Either String URL -> + Maybe URL -> + Either String URL -> + Maybe URL -> + Opt.Option InstitutionSustainabilityRowOpts +injectSustainFieldsOpt + oldOpt + missionUrl_Ei + missionUrlMay + fundingUrl_Ei + fundingUrlMay = execState (do + get >>= Opt.maySetOptState (SProxy :: _ "missionUrl_Ei") + (Just missionUrl_Ei) + get >>= Opt.maySetOptState (SProxy :: _ "missionStatementURL") + missionUrlMay + get >>= Opt.maySetOptState (SProxy :: _ "fundingUrl_Ei") + (Just fundingUrl_Ei) + get >>= Opt.maySetOptState (SProxy :: _ "fundingStatementURL") + fundingUrlMay + ) oldOpt + +accumulateSustain :: String -> + CtrlSignal HTML (Opt.Option InstitutionSustainabilityRowOpts) +accumulateSustain idLabel oldSust = labelSig' D.h3' idLabel do + missionUrl_Ei <- urlInput D.span' "Mission Statement URL: " $ + Opt.getWithDefault (Left "") (SProxy :: _ "missionUrl_Ei") oldSust + let missionUrlMay = hush missionUrl_Ei + fundingUrl_Ei <- urlInput D.span' "Funding Statement URL: " $ + Opt.getWithDefault (Left "") (SProxy :: _ "fundingUrl_Ei") oldSust + let fundingUrlMay = hush fundingUrl_Ei + pure $ injectSustainFieldsOpt oldSust + missionUrl_Ei + missionUrlMay + fundingUrl_Ei + fundingUrlMay + +injectIdentFields :: -- TODO: use "sequence" + Opt.Option (M.BaseIdRows ()) -> Maybe M.Identifier +injectIdentFields idOpt = go + (Opt.get (SProxy :: _ "id") idOpt) + (Opt.get (SProxy :: _ "idType") idOpt) + where + go (Just id) (Just idType) = pure $ { + id: id + , idType: idType + } + go _ _ = Nothing + +injectIdentFieldsOpt :: + Opt.Option (M.BaseIdRows ()) -> + Maybe NonEmptyString -> + Maybe M.IdentifierType -> + Opt.Option (M.BaseIdRows ()) +injectIdentFieldsOpt + oldOpt + idMay + idTypeMay = execState (do + get >>= Opt.maySetOptState (SProxy :: _ "id") idMay + get >>= Opt.maySetOptState (SProxy :: _ "idType") idTypeMay + ) oldOpt + +accumulateIdent :: String -> CtrlSignal HTML (Opt.Option (M.BaseIdRows ())) +accumulateIdent idLabel oldId = labelSig' D.h3' idLabel do + idMay <- textInput D.span' "Record Identifier: " $ + Opt.get (SProxy :: _ "id") oldId + idTypeMay <- labelSig' D.span' "Identifier Type" $ menuSignal $ + Opt.get (SProxy :: _ "idType") oldId + pure $ injectIdentFieldsOpt oldId idMay idTypeMay + +-- TODO: PR to purescript-option +getOpt :: + forall label option option' proxy suboption. + IsSymbol label => + Prim.Row.Cons label (Opt.Option suboption) option' option => + proxy label -> + Opt.Option option -> + Opt.Option suboption +getOpt = Opt.getWithDefault Opt.empty \ No newline at end of file diff --git a/app/src/Metajelo/Validation.purs b/src/Metajelo/Validation.purs similarity index 90% rename from app/src/Metajelo/Validation.purs rename to src/Metajelo/Validation.purs index b9ed14e..58d9cda 100644 --- a/app/src/Metajelo/Validation.purs +++ b/src/Metajelo/Validation.purs @@ -10,9 +10,10 @@ import Data.Generic.Rep (class Generic) import Data.Generic.Rep.Show (genericShow) import Data.Int (fromString) as Int import Data.Lens (preview) -import Data.Maybe (Maybe, maybe) +import Data.Maybe (Maybe(..), maybe) import Data.Newtype (class Newtype, unwrap) -import Data.String (contains, length, null) +import Data.String (contains, length, null, trim) +import Data.String.NonEmpty (NonEmptyString, fromString) import Data.String.Pattern (Pattern(..)) import Effect.Aff (Milliseconds(..), delay) import Effect.Aff.Class (class MonadAff, liftAff) @@ -112,11 +113,13 @@ nonEmptyArray = hoistFnE_ \arr -> else Left EmptyField -- | Validate that an input string is not empty -nonEmptyStr :: ∀ form m. Monad m => Validation form m FieldError String String -nonEmptyStr = hoistFnE_ $ \str -> - if null str - then Left EmptyField - else Right str +nonEmptyStr :: ∀ form m. Monad m => Validation form m String String NonEmptyString +nonEmptyStr = hoistFnE_ readNEStringEi + +readNEStringEi :: String -> Either String NonEmptyString +readNEStringEi str = case fromString $ trim str of + Just nes -> Right nes + Nothing -> Left "Empty string when NonEmptyString expected." -------------------- -- Formless Async Validation diff --git a/app/static/css b/static/css similarity index 100% rename from app/static/css rename to static/css diff --git a/app/static/index.css b/static/index.css similarity index 100% rename from app/static/index.css rename to static/index.css diff --git a/app/static/index.html b/static/index.html similarity index 100% rename from app/static/index.html rename to static/index.html diff --git a/app/static/index.js b/static/index.js similarity index 100% rename from app/static/index.js rename to static/index.js diff --git a/test/node_modules b/test/node_modules deleted file mode 120000 index 68a084a..0000000 --- a/test/node_modules +++ /dev/null @@ -1 +0,0 @@ -../node_modules \ No newline at end of file diff --git a/test/package-lock.json b/test/package-lock.json deleted file mode 120000 index 55f7221..0000000 --- a/test/package-lock.json +++ /dev/null @@ -1 +0,0 @@ -../package-lock.json \ No newline at end of file diff --git a/test/package.json b/test/package.json deleted file mode 120000 index 4e26811..0000000 --- a/test/package.json +++ /dev/null @@ -1 +0,0 @@ -../package.json \ No newline at end of file diff --git a/test/spago.dhall b/test/spago.dhall deleted file mode 100644 index 2812d41..0000000 --- a/test/spago.dhall +++ /dev/null @@ -1,24 +0,0 @@ -{- -Welcome to a Spago project! -You can edit this file as you like. --} -{ name = - "purescript-metajelo-ui-test" -, dependencies = [ - "debug" - , "console" - , "foreign" - , "metajelo-ui" - , "psci-support" - , "test-unit" - ] -, packages = - (../packages.dhall) // - { metajelo-ui = - { repo = "../app" - , version = "" - , dependencies = (../app/spago.dhall).dependencies - } - } - -} diff --git a/test/css b/tests/css similarity index 100% rename from test/css rename to tests/css diff --git a/test/index.html b/tests/index.html similarity index 100% rename from test/index.html rename to tests/index.html diff --git a/app/node_modules b/tests/node_modules similarity index 100% rename from app/node_modules rename to tests/node_modules diff --git a/app/package-lock.json b/tests/package-lock.json similarity index 100% rename from app/package-lock.json rename to tests/package-lock.json diff --git a/app/package.json b/tests/package.json similarity index 100% rename from app/package.json rename to tests/package.json diff --git a/tests/spago.dhall b/tests/spago.dhall new file mode 100644 index 0000000..a2b7a78 --- /dev/null +++ b/tests/spago.dhall @@ -0,0 +1,19 @@ +{- +Welcome to a Spago project! +You can edit this file as you like. +-} +{ sources = + [ "src/**/*.purs", "test/**/*.purs" ] +, name = + "purescript-metajelo-ui-test" +, dependencies = + [ "debug" + , "console" + , "foreign" + , "metajelo-ui" + , "psci-support" + , "test-unit" + ] +, packages = + ../packages.dhall +} diff --git a/test/src/Main.purs b/tests/src/Main.purs similarity index 91% rename from test/src/Main.purs rename to tests/src/Main.purs index 9afdc28..c959ffe 100644 --- a/test/src/Main.purs +++ b/tests/src/Main.purs @@ -8,7 +8,7 @@ import Effect.Class (liftEffect) import Effect.Console (logShow) import Test.Unit.Assert as Assert import Text.Email.Validate as EA -import URL.Validator as URL +import Text.URL.Validate as URL import Metajelo.Types as MJ import Metajelo.XPaths as MXP