From 6f4096030cb3762af0854e15ebbf61d22524521e Mon Sep 17 00:00:00 2001 From: Thomas Rabaix Date: Wed, 21 Mar 2018 08:53:39 +0100 Subject: [PATCH 1/2] feat(backoffice): init backoffice --- app/server.toml.dist | 2 +- assets/README.md | 43 - assets/assets.go | 17 - assets/bindata.go | 511 -- assets/bindata.sh | 8 - assets/doc.go | 6 - backoffice/.babelrc | 4 + backoffice/.gitignore | 5 + backoffice/README.md | 6 + backoffice/package.json | 58 + backoffice/src/app.tsx | 189 + backoffice/src/core/api.test.ts | 143 + backoffice/src/core/api.ts | 539 ++ backoffice/src/fugue/docs/form.md | 38 + .../fugue/packages/fugue-app/dispatcher.ts | 16 + .../src/fugue/packages/fugue-app/index.tsx | 120 + .../src/fugue/packages/fugue-app/package.json | 9 + .../fugue/packages/fugue-form/bootstrap.tsx | 76 + .../src/fugue/packages/fugue-form/index.tsx | 438 + .../fugue/packages/fugue-form/package.json | 11 + .../src/fugue/packages/fugue-ioc/README.md | 4 + .../src/fugue/packages/fugue-ioc/di.test.tsx | 97 + .../src/fugue/packages/fugue-ioc/di.tsx | 155 + .../src/fugue/packages/fugue-ioc/package.json | 6 + .../packages/fugue-validator/index.test.ts | 41 + .../fugue/packages/fugue-validator/index.ts | 112 + .../packages/fugue-validator/package.json | 6 + backoffice/src/index.html | 15 + backoffice/src/module.d.ts | 11 + backoffice/src/traverson.test.tsx | 0 backoffice/src/view/About/index.tsx | 28 + backoffice/src/view/Dashboard/index.tsx | 0 backoffice/src/view/Login/index.tsx | 97 + backoffice/src/view/Shared/Footer.tsx | 18 + backoffice/src/view/Shared/Templates.tsx | 64 + backoffice/tsconfig.json | 46 + backoffice/tsconfig.test.json | 6 + backoffice/tslint.json | 99 + backoffice/webpack.common.js | 40 + backoffice/webpack.dev.js | 9 + backoffice/webpack.prod.js | 17 + backoffice/yarn.lock | 8022 +++++++++++++++++ docker-compose.yml | 3 +- 43 files changed, 10548 insertions(+), 587 deletions(-) delete mode 100644 assets/README.md delete mode 100644 assets/assets.go delete mode 100644 assets/bindata.go delete mode 100755 assets/bindata.sh delete mode 100644 assets/doc.go create mode 100644 backoffice/.babelrc create mode 100644 backoffice/.gitignore create mode 100644 backoffice/README.md create mode 100644 backoffice/package.json create mode 100644 backoffice/src/app.tsx create mode 100644 backoffice/src/core/api.test.ts create mode 100644 backoffice/src/core/api.ts create mode 100644 backoffice/src/fugue/docs/form.md create mode 100644 backoffice/src/fugue/packages/fugue-app/dispatcher.ts create mode 100644 backoffice/src/fugue/packages/fugue-app/index.tsx create mode 100644 backoffice/src/fugue/packages/fugue-app/package.json create mode 100644 backoffice/src/fugue/packages/fugue-form/bootstrap.tsx create mode 100644 backoffice/src/fugue/packages/fugue-form/index.tsx create mode 100644 backoffice/src/fugue/packages/fugue-form/package.json create mode 100644 backoffice/src/fugue/packages/fugue-ioc/README.md create mode 100644 backoffice/src/fugue/packages/fugue-ioc/di.test.tsx create mode 100644 backoffice/src/fugue/packages/fugue-ioc/di.tsx create mode 100644 backoffice/src/fugue/packages/fugue-ioc/package.json create mode 100644 backoffice/src/fugue/packages/fugue-validator/index.test.ts create mode 100644 backoffice/src/fugue/packages/fugue-validator/index.ts create mode 100644 backoffice/src/fugue/packages/fugue-validator/package.json create mode 100644 backoffice/src/index.html create mode 100644 backoffice/src/module.d.ts create mode 100644 backoffice/src/traverson.test.tsx create mode 100644 backoffice/src/view/About/index.tsx create mode 100644 backoffice/src/view/Dashboard/index.tsx create mode 100644 backoffice/src/view/Login/index.tsx create mode 100644 backoffice/src/view/Shared/Footer.tsx create mode 100644 backoffice/src/view/Shared/Templates.tsx create mode 100644 backoffice/tsconfig.json create mode 100644 backoffice/tsconfig.test.json create mode 100644 backoffice/tslint.json create mode 100644 backoffice/webpack.common.js create mode 100644 backoffice/webpack.dev.js create mode 100644 backoffice/webpack.prod.js create mode 100644 backoffice/yarn.lock diff --git a/app/server.toml.dist b/app/server.toml.dist index a695efc..780bd99 100644 --- a/app/server.toml.dist +++ b/app/server.toml.dist @@ -36,7 +36,7 @@ path = "/tmp/gnode" ] [security.cors] - allowed_origins = ["http://localhost:8000"] + allowed_origins = ["*"] allowed_methods = ["GET", "PUT", "POST"] allowed_headers = ["Origin", "Accept", "Content-Type", "Authorization"] diff --git a/assets/README.md b/assets/README.md deleted file mode 100644 index bdf3bfa..0000000 --- a/assets/README.md +++ /dev/null @@ -1,43 +0,0 @@ -Assets -====== - -This directory will only contains a gofile generated by go-bindata with all assets required by the project. - -The ``bindata.go`` is generated only on build and should not be commited. - -The assets can contains any template files or front files (js, css, images, ...) - -Please note, the ``go-bindata`` must be run from ``GOPATH`` folder, so the assets references will be unique accross different projects. - - github.com/rande/gonode/modules/setup/templates/core.setup.base.html.tpl - github.com/rande/gonode/explorer/dist/css/app-0-f3673f84.css - github.com/rande/gonode/explorer/dist/d317f3fe02c697f8b2d3c4c3e940ea7f.gif - github.com/rande/gonode/explorer/dist/fonts/arrow-left-icon-5fc0d629.svg - - -The ``Makefile`` lines to generate the file will be: - - ```Makefile - GO_PATH = $(shell go env GOPATH) - GO_BINDATA_PATHS = $(GO_PATH)/src/github.com/rande/gonode/modules/... $(GO_PATH)/src/github.com/rande/gonode/explorer/dist/... - GO_BINDATA_IGNORE = "(.*)\.(go|DS_Store)" - GO_BINDATA_OUTPUT = $(GO_PATH)/src/github.com/rande/gonode/assets/bindata.go - GO_BINDATA_PACKAGE = assets - - bin: - cd $(GO_PATH)/src && go-bindata -debug -o $(GO_BINDATA_OUTPUT) -pkg $(GO_BINDATA_PACKAGE) -ignore $(GO_BINDATA_IGNORE) $(GO_BINDATA_PATHS) - - run: bin - cd commands && go run main.go server -config=../server.toml.dist - - build: - rm -rf dist && mkdir dist - cd $(GO_PATH)/src && go-bindata -o $(GO_BINDATA_OUTPUT) -pkg $(GO_BINDATA_PACKAGE) -ignore $(GO_BINDATA_IGNORE) $(GO_BINDATA_PATHS) - cd commands && go build -a -o ../dist/gonode - - -Usage ------ - -Just import the package ``assets`` and refer to the ``go-bindata`` documentation. - diff --git a/assets/assets.go b/assets/assets.go deleted file mode 100644 index 81ec5b3..0000000 --- a/assets/assets.go +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright © 2014-2018 Thomas Rabaix . -// -// Use of this source code is governed by an MIT-style -// license that can be found in the LICENSE file. - -package assets - -var rootDir = "" - -func UpdateRootDir(path string) { - - if len(path) == 0 { - return - } - - rootDir = path -} diff --git a/assets/bindata.go b/assets/bindata.go deleted file mode 100644 index a2b923f..0000000 --- a/assets/bindata.go +++ /dev/null @@ -1,511 +0,0 @@ -// Code generated by go-bindata. -// sources: -// github.com/rande/gonode/modules/blog/templates/nodes/blog.post.tpl -// github.com/rande/gonode/modules/feed/templates/nodes/feed.index.atom.tpl -// github.com/rande/gonode/modules/feed/templates/nodes/feed.index.rss.tpl -// github.com/rande/gonode/modules/media/exif/2004-01-11-22-45-15-sep-2004-01-11-22-45-15a.jpg -// github.com/rande/gonode/modules/media/exif/infinite_loop_exif.jpg -// github.com/rande/gonode/modules/prism/templates/layouts/base.tpl -// github.com/rande/gonode/modules/prism/templates/layouts/error.tpl -// github.com/rande/gonode/modules/prism/templates/pages/bad_request.tpl -// github.com/rande/gonode/modules/prism/templates/pages/internal_error.tpl -// github.com/rande/gonode/modules/prism/templates/pages/not_found.tpl -// github.com/rande/gonode/modules/search/templates/nodes/core.index.tpl -// github.com/rande/gonode/modules/setup/templates/core.setup.base.html.tpl -// github.com/rande/gonode/modules/setup/templates/definitions.toml -// github.com/rande/gonode/explorer/dist/.gitkeep -// DO NOT EDIT! - -package assets - -import ( - "fmt" - "io/ioutil" - "os" - "path/filepath" - "strings" -) - -// bindataRead reads the given file from disk. It returns an error on failure. -func bindataRead(path, name string) ([]byte, error) { - buf, err := ioutil.ReadFile(path) - if err != nil { - err = fmt.Errorf("Error reading asset %s at %s: %v", name, path, err) - } - return buf, err -} - -type asset struct { - bytes []byte - info os.FileInfo -} - -// githubComRandeGonodeModulesBlogTemplatesNodesBlogPostTpl reads file data from disk. It returns an error on failure. -func githubComRandeGonodeModulesBlogTemplatesNodesBlogPostTpl() (*asset, error) { - path := filepath.Join(rootDir, "github.com/rande/gonode/modules/blog/templates/nodes/blog.post.tpl") - name := "github.com/rande/gonode/modules/blog/templates/nodes/blog.post.tpl" - bytes, err := bindataRead(path, name) - if err != nil { - return nil, err - } - - fi, err := os.Stat(path) - if err != nil { - err = fmt.Errorf("Error reading asset info %s at %s: %v", name, path, err) - } - - a := &asset{bytes: bytes, info: fi} - return a, err -} - -// githubComRandeGonodeModulesFeedTemplatesNodesFeedIndexAtomTpl reads file data from disk. It returns an error on failure. -func githubComRandeGonodeModulesFeedTemplatesNodesFeedIndexAtomTpl() (*asset, error) { - path := filepath.Join(rootDir, "github.com/rande/gonode/modules/feed/templates/nodes/feed.index.atom.tpl") - name := "github.com/rande/gonode/modules/feed/templates/nodes/feed.index.atom.tpl" - bytes, err := bindataRead(path, name) - if err != nil { - return nil, err - } - - fi, err := os.Stat(path) - if err != nil { - err = fmt.Errorf("Error reading asset info %s at %s: %v", name, path, err) - } - - a := &asset{bytes: bytes, info: fi} - return a, err -} - -// githubComRandeGonodeModulesFeedTemplatesNodesFeedIndexRssTpl reads file data from disk. It returns an error on failure. -func githubComRandeGonodeModulesFeedTemplatesNodesFeedIndexRssTpl() (*asset, error) { - path := filepath.Join(rootDir, "github.com/rande/gonode/modules/feed/templates/nodes/feed.index.rss.tpl") - name := "github.com/rande/gonode/modules/feed/templates/nodes/feed.index.rss.tpl" - bytes, err := bindataRead(path, name) - if err != nil { - return nil, err - } - - fi, err := os.Stat(path) - if err != nil { - err = fmt.Errorf("Error reading asset info %s at %s: %v", name, path, err) - } - - a := &asset{bytes: bytes, info: fi} - return a, err -} - -// githubComRandeGonodeModulesMediaExif20040111224515Sep20040111224515aJpg reads file data from disk. It returns an error on failure. -func githubComRandeGonodeModulesMediaExif20040111224515Sep20040111224515aJpg() (*asset, error) { - path := filepath.Join(rootDir, "github.com/rande/gonode/modules/media/exif/2004-01-11-22-45-15-sep-2004-01-11-22-45-15a.jpg") - name := "github.com/rande/gonode/modules/media/exif/2004-01-11-22-45-15-sep-2004-01-11-22-45-15a.jpg" - bytes, err := bindataRead(path, name) - if err != nil { - return nil, err - } - - fi, err := os.Stat(path) - if err != nil { - err = fmt.Errorf("Error reading asset info %s at %s: %v", name, path, err) - } - - a := &asset{bytes: bytes, info: fi} - return a, err -} - -// githubComRandeGonodeModulesMediaExifInfinite_loop_exifJpg reads file data from disk. It returns an error on failure. -func githubComRandeGonodeModulesMediaExifInfinite_loop_exifJpg() (*asset, error) { - path := filepath.Join(rootDir, "github.com/rande/gonode/modules/media/exif/infinite_loop_exif.jpg") - name := "github.com/rande/gonode/modules/media/exif/infinite_loop_exif.jpg" - bytes, err := bindataRead(path, name) - if err != nil { - return nil, err - } - - fi, err := os.Stat(path) - if err != nil { - err = fmt.Errorf("Error reading asset info %s at %s: %v", name, path, err) - } - - a := &asset{bytes: bytes, info: fi} - return a, err -} - -// githubComRandeGonodeModulesPrismTemplatesLayoutsBaseTpl reads file data from disk. It returns an error on failure. -func githubComRandeGonodeModulesPrismTemplatesLayoutsBaseTpl() (*asset, error) { - path := filepath.Join(rootDir, "github.com/rande/gonode/modules/prism/templates/layouts/base.tpl") - name := "github.com/rande/gonode/modules/prism/templates/layouts/base.tpl" - bytes, err := bindataRead(path, name) - if err != nil { - return nil, err - } - - fi, err := os.Stat(path) - if err != nil { - err = fmt.Errorf("Error reading asset info %s at %s: %v", name, path, err) - } - - a := &asset{bytes: bytes, info: fi} - return a, err -} - -// githubComRandeGonodeModulesPrismTemplatesLayoutsErrorTpl reads file data from disk. It returns an error on failure. -func githubComRandeGonodeModulesPrismTemplatesLayoutsErrorTpl() (*asset, error) { - path := filepath.Join(rootDir, "github.com/rande/gonode/modules/prism/templates/layouts/error.tpl") - name := "github.com/rande/gonode/modules/prism/templates/layouts/error.tpl" - bytes, err := bindataRead(path, name) - if err != nil { - return nil, err - } - - fi, err := os.Stat(path) - if err != nil { - err = fmt.Errorf("Error reading asset info %s at %s: %v", name, path, err) - } - - a := &asset{bytes: bytes, info: fi} - return a, err -} - -// githubComRandeGonodeModulesPrismTemplatesPagesBad_requestTpl reads file data from disk. It returns an error on failure. -func githubComRandeGonodeModulesPrismTemplatesPagesBad_requestTpl() (*asset, error) { - path := filepath.Join(rootDir, "github.com/rande/gonode/modules/prism/templates/pages/bad_request.tpl") - name := "github.com/rande/gonode/modules/prism/templates/pages/bad_request.tpl" - bytes, err := bindataRead(path, name) - if err != nil { - return nil, err - } - - fi, err := os.Stat(path) - if err != nil { - err = fmt.Errorf("Error reading asset info %s at %s: %v", name, path, err) - } - - a := &asset{bytes: bytes, info: fi} - return a, err -} - -// githubComRandeGonodeModulesPrismTemplatesPagesInternal_errorTpl reads file data from disk. It returns an error on failure. -func githubComRandeGonodeModulesPrismTemplatesPagesInternal_errorTpl() (*asset, error) { - path := filepath.Join(rootDir, "github.com/rande/gonode/modules/prism/templates/pages/internal_error.tpl") - name := "github.com/rande/gonode/modules/prism/templates/pages/internal_error.tpl" - bytes, err := bindataRead(path, name) - if err != nil { - return nil, err - } - - fi, err := os.Stat(path) - if err != nil { - err = fmt.Errorf("Error reading asset info %s at %s: %v", name, path, err) - } - - a := &asset{bytes: bytes, info: fi} - return a, err -} - -// githubComRandeGonodeModulesPrismTemplatesPagesNot_foundTpl reads file data from disk. It returns an error on failure. -func githubComRandeGonodeModulesPrismTemplatesPagesNot_foundTpl() (*asset, error) { - path := filepath.Join(rootDir, "github.com/rande/gonode/modules/prism/templates/pages/not_found.tpl") - name := "github.com/rande/gonode/modules/prism/templates/pages/not_found.tpl" - bytes, err := bindataRead(path, name) - if err != nil { - return nil, err - } - - fi, err := os.Stat(path) - if err != nil { - err = fmt.Errorf("Error reading asset info %s at %s: %v", name, path, err) - } - - a := &asset{bytes: bytes, info: fi} - return a, err -} - -// githubComRandeGonodeModulesSearchTemplatesNodesCoreIndexTpl reads file data from disk. It returns an error on failure. -func githubComRandeGonodeModulesSearchTemplatesNodesCoreIndexTpl() (*asset, error) { - path := filepath.Join(rootDir, "github.com/rande/gonode/modules/search/templates/nodes/core.index.tpl") - name := "github.com/rande/gonode/modules/search/templates/nodes/core.index.tpl" - bytes, err := bindataRead(path, name) - if err != nil { - return nil, err - } - - fi, err := os.Stat(path) - if err != nil { - err = fmt.Errorf("Error reading asset info %s at %s: %v", name, path, err) - } - - a := &asset{bytes: bytes, info: fi} - return a, err -} - -// githubComRandeGonodeModulesSetupTemplatesCoreSetupBaseHtmlTpl reads file data from disk. It returns an error on failure. -func githubComRandeGonodeModulesSetupTemplatesCoreSetupBaseHtmlTpl() (*asset, error) { - path := filepath.Join(rootDir, "github.com/rande/gonode/modules/setup/templates/core.setup.base.html.tpl") - name := "github.com/rande/gonode/modules/setup/templates/core.setup.base.html.tpl" - bytes, err := bindataRead(path, name) - if err != nil { - return nil, err - } - - fi, err := os.Stat(path) - if err != nil { - err = fmt.Errorf("Error reading asset info %s at %s: %v", name, path, err) - } - - a := &asset{bytes: bytes, info: fi} - return a, err -} - -// githubComRandeGonodeModulesSetupTemplatesDefinitionsToml reads file data from disk. It returns an error on failure. -func githubComRandeGonodeModulesSetupTemplatesDefinitionsToml() (*asset, error) { - path := filepath.Join(rootDir, "github.com/rande/gonode/modules/setup/templates/definitions.toml") - name := "github.com/rande/gonode/modules/setup/templates/definitions.toml" - bytes, err := bindataRead(path, name) - if err != nil { - return nil, err - } - - fi, err := os.Stat(path) - if err != nil { - err = fmt.Errorf("Error reading asset info %s at %s: %v", name, path, err) - } - - a := &asset{bytes: bytes, info: fi} - return a, err -} - -// githubComRandeGonodeExplorerDistGitkeep reads file data from disk. It returns an error on failure. -func githubComRandeGonodeExplorerDistGitkeep() (*asset, error) { - path := filepath.Join(rootDir, "github.com/rande/gonode/explorer/dist/.gitkeep") - name := "github.com/rande/gonode/explorer/dist/.gitkeep" - bytes, err := bindataRead(path, name) - if err != nil { - return nil, err - } - - fi, err := os.Stat(path) - if err != nil { - err = fmt.Errorf("Error reading asset info %s at %s: %v", name, path, err) - } - - a := &asset{bytes: bytes, info: fi} - return a, err -} - -// Asset loads and returns the asset for the given name. -// It returns an error if the asset could not be found or -// could not be loaded. -func Asset(name string) ([]byte, error) { - cannonicalName := strings.Replace(name, "\\", "/", -1) - if f, ok := _bindata[cannonicalName]; ok { - a, err := f() - if err != nil { - return nil, fmt.Errorf("Asset %s can't read by error: %v", name, err) - } - return a.bytes, nil - } - return nil, fmt.Errorf("Asset %s not found", name) -} - -// MustAsset is like Asset but panics when Asset would return an error. -// It simplifies safe initialization of global variables. -func MustAsset(name string) []byte { - a, err := Asset(name) - if err != nil { - panic("asset: Asset(" + name + "): " + err.Error()) - } - - return a -} - -// AssetInfo loads and returns the asset info for the given name. -// It returns an error if the asset could not be found or -// could not be loaded. -func AssetInfo(name string) (os.FileInfo, error) { - cannonicalName := strings.Replace(name, "\\", "/", -1) - if f, ok := _bindata[cannonicalName]; ok { - a, err := f() - if err != nil { - return nil, fmt.Errorf("AssetInfo %s can't read by error: %v", name, err) - } - return a.info, nil - } - return nil, fmt.Errorf("AssetInfo %s not found", name) -} - -// AssetNames returns the names of the assets. -func AssetNames() []string { - names := make([]string, 0, len(_bindata)) - for name := range _bindata { - names = append(names, name) - } - return names -} - -// _bindata is a table, holding each asset generator, mapped to its name. -var _bindata = map[string]func() (*asset, error){ - "github.com/rande/gonode/modules/blog/templates/nodes/blog.post.tpl": githubComRandeGonodeModulesBlogTemplatesNodesBlogPostTpl, - "github.com/rande/gonode/modules/feed/templates/nodes/feed.index.atom.tpl": githubComRandeGonodeModulesFeedTemplatesNodesFeedIndexAtomTpl, - "github.com/rande/gonode/modules/feed/templates/nodes/feed.index.rss.tpl": githubComRandeGonodeModulesFeedTemplatesNodesFeedIndexRssTpl, - "github.com/rande/gonode/modules/media/exif/2004-01-11-22-45-15-sep-2004-01-11-22-45-15a.jpg": githubComRandeGonodeModulesMediaExif20040111224515Sep20040111224515aJpg, - "github.com/rande/gonode/modules/media/exif/infinite_loop_exif.jpg": githubComRandeGonodeModulesMediaExifInfinite_loop_exifJpg, - "github.com/rande/gonode/modules/prism/templates/layouts/base.tpl": githubComRandeGonodeModulesPrismTemplatesLayoutsBaseTpl, - "github.com/rande/gonode/modules/prism/templates/layouts/error.tpl": githubComRandeGonodeModulesPrismTemplatesLayoutsErrorTpl, - "github.com/rande/gonode/modules/prism/templates/pages/bad_request.tpl": githubComRandeGonodeModulesPrismTemplatesPagesBad_requestTpl, - "github.com/rande/gonode/modules/prism/templates/pages/internal_error.tpl": githubComRandeGonodeModulesPrismTemplatesPagesInternal_errorTpl, - "github.com/rande/gonode/modules/prism/templates/pages/not_found.tpl": githubComRandeGonodeModulesPrismTemplatesPagesNot_foundTpl, - "github.com/rande/gonode/modules/search/templates/nodes/core.index.tpl": githubComRandeGonodeModulesSearchTemplatesNodesCoreIndexTpl, - "github.com/rande/gonode/modules/setup/templates/core.setup.base.html.tpl": githubComRandeGonodeModulesSetupTemplatesCoreSetupBaseHtmlTpl, - "github.com/rande/gonode/modules/setup/templates/definitions.toml": githubComRandeGonodeModulesSetupTemplatesDefinitionsToml, - "github.com/rande/gonode/explorer/dist/.gitkeep": githubComRandeGonodeExplorerDistGitkeep, -} - -// AssetDir returns the file names below a certain -// directory embedded in the file by go-bindata. -// For example if you run go-bindata on data/... and data contains the -// following hierarchy: -// data/ -// foo.txt -// img/ -// a.png -// b.png -// then AssetDir("data") would return []string{"foo.txt", "img"} -// AssetDir("data/img") would return []string{"a.png", "b.png"} -// AssetDir("foo.txt") and AssetDir("notexist") would return an error -// AssetDir("") will return []string{"data"}. -func AssetDir(name string) ([]string, error) { - node := _bintree - if len(name) != 0 { - cannonicalName := strings.Replace(name, "\\", "/", -1) - pathList := strings.Split(cannonicalName, "/") - for _, p := range pathList { - node = node.Children[p] - if node == nil { - return nil, fmt.Errorf("Asset %s not found", name) - } - } - } - if node.Func != nil { - return nil, fmt.Errorf("Asset %s not found", name) - } - rv := make([]string, 0, len(node.Children)) - for childName := range node.Children { - rv = append(rv, childName) - } - return rv, nil -} - -type bintree struct { - Func func() (*asset, error) - Children map[string]*bintree -} -var _bintree = &bintree{nil, map[string]*bintree{ - "github.com": &bintree{nil, map[string]*bintree{ - "rande": &bintree{nil, map[string]*bintree{ - "gonode": &bintree{nil, map[string]*bintree{ - "explorer": &bintree{nil, map[string]*bintree{ - "dist": &bintree{nil, map[string]*bintree{ - ".gitkeep": &bintree{githubComRandeGonodeExplorerDistGitkeep, map[string]*bintree{}}, - }}, - }}, - "modules": &bintree{nil, map[string]*bintree{ - "blog": &bintree{nil, map[string]*bintree{ - "templates": &bintree{nil, map[string]*bintree{ - "nodes": &bintree{nil, map[string]*bintree{ - "blog.post.tpl": &bintree{githubComRandeGonodeModulesBlogTemplatesNodesBlogPostTpl, map[string]*bintree{}}, - }}, - }}, - }}, - "feed": &bintree{nil, map[string]*bintree{ - "templates": &bintree{nil, map[string]*bintree{ - "nodes": &bintree{nil, map[string]*bintree{ - "feed.index.atom.tpl": &bintree{githubComRandeGonodeModulesFeedTemplatesNodesFeedIndexAtomTpl, map[string]*bintree{}}, - "feed.index.rss.tpl": &bintree{githubComRandeGonodeModulesFeedTemplatesNodesFeedIndexRssTpl, map[string]*bintree{}}, - }}, - }}, - }}, - "media": &bintree{nil, map[string]*bintree{ - "exif": &bintree{nil, map[string]*bintree{ - "2004-01-11-22-45-15-sep-2004-01-11-22-45-15a.jpg": &bintree{githubComRandeGonodeModulesMediaExif20040111224515Sep20040111224515aJpg, map[string]*bintree{}}, - "infinite_loop_exif.jpg": &bintree{githubComRandeGonodeModulesMediaExifInfinite_loop_exifJpg, map[string]*bintree{}}, - }}, - }}, - "prism": &bintree{nil, map[string]*bintree{ - "templates": &bintree{nil, map[string]*bintree{ - "layouts": &bintree{nil, map[string]*bintree{ - "base.tpl": &bintree{githubComRandeGonodeModulesPrismTemplatesLayoutsBaseTpl, map[string]*bintree{}}, - "error.tpl": &bintree{githubComRandeGonodeModulesPrismTemplatesLayoutsErrorTpl, map[string]*bintree{}}, - }}, - "pages": &bintree{nil, map[string]*bintree{ - "bad_request.tpl": &bintree{githubComRandeGonodeModulesPrismTemplatesPagesBad_requestTpl, map[string]*bintree{}}, - "internal_error.tpl": &bintree{githubComRandeGonodeModulesPrismTemplatesPagesInternal_errorTpl, map[string]*bintree{}}, - "not_found.tpl": &bintree{githubComRandeGonodeModulesPrismTemplatesPagesNot_foundTpl, map[string]*bintree{}}, - }}, - }}, - }}, - "search": &bintree{nil, map[string]*bintree{ - "templates": &bintree{nil, map[string]*bintree{ - "nodes": &bintree{nil, map[string]*bintree{ - "core.index.tpl": &bintree{githubComRandeGonodeModulesSearchTemplatesNodesCoreIndexTpl, map[string]*bintree{}}, - }}, - }}, - }}, - "setup": &bintree{nil, map[string]*bintree{ - "templates": &bintree{nil, map[string]*bintree{ - "core.setup.base.html.tpl": &bintree{githubComRandeGonodeModulesSetupTemplatesCoreSetupBaseHtmlTpl, map[string]*bintree{}}, - "definitions.toml": &bintree{githubComRandeGonodeModulesSetupTemplatesDefinitionsToml, map[string]*bintree{}}, - }}, - }}, - }}, - }}, - }}, - }}, -}} - -// RestoreAsset restores an asset under the given directory -func RestoreAsset(dir, name string) error { - data, err := Asset(name) - if err != nil { - return err - } - info, err := AssetInfo(name) - if err != nil { - return err - } - err = os.MkdirAll(_filePath(dir, filepath.Dir(name)), os.FileMode(0755)) - if err != nil { - return err - } - err = ioutil.WriteFile(_filePath(dir, name), data, info.Mode()) - if err != nil { - return err - } - err = os.Chtimes(_filePath(dir, name), info.ModTime(), info.ModTime()) - if err != nil { - return err - } - return nil -} - -// RestoreAssets restores an asset under the given directory recursively -func RestoreAssets(dir, name string) error { - children, err := AssetDir(name) - // File - if err != nil { - return RestoreAsset(dir, name) - } - // Dir - for _, child := range children { - err = RestoreAssets(dir, filepath.Join(name, child)) - if err != nil { - return err - } - } - return nil -} - -func _filePath(dir, name string) string { - cannonicalName := strings.Replace(name, "\\", "/", -1) - return filepath.Join(append([]string{dir}, strings.Split(cannonicalName, "/")...)...) -} - diff --git a/assets/bindata.sh b/assets/bindata.sh deleted file mode 100755 index e323b55..0000000 --- a/assets/bindata.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/usr/bin/env bash - -GO_BINDATA_PATHS="${GOPATH}/src/github.com/rande/gonode/modules/... ${GOPATH}/src/github.com/rande/gonode/explorer/dist/..." -GO_BINDATA_IGNORE="(.*)\.(go|DS_Store)" -GO_BINDATA_OUTPUT="${GOPATH}/src/github.com/rande/gonode/assets/bindata.go" -GO_BINDATA_PACKAGE="assets" - -cd ${GOPATH}/src && go-bindata -dev -prefix ${GOPATH}/src -o ${GO_BINDATA_OUTPUT} -pkg ${GO_BINDATA_PACKAGE} -ignore ${GO_BINDATA_IGNORE} ${GO_BINDATA_PATHS} \ No newline at end of file diff --git a/assets/doc.go b/assets/doc.go deleted file mode 100644 index 585ba02..0000000 --- a/assets/doc.go +++ /dev/null @@ -1,6 +0,0 @@ -// Copyright © 2014-2018 Thomas Rabaix . -// -// Use of this source code is governed by an MIT-style -// license that can be found in the LICENSE file. - -package assets diff --git a/backoffice/.babelrc b/backoffice/.babelrc new file mode 100644 index 0000000..70ed379 --- /dev/null +++ b/backoffice/.babelrc @@ -0,0 +1,4 @@ +{ + "presets": ["env"], + "plugins": [["transform-react-jsx", { "pragma": "h" }]] +} \ No newline at end of file diff --git a/backoffice/.gitignore b/backoffice/.gitignore new file mode 100644 index 0000000..5ffee86 --- /dev/null +++ b/backoffice/.gitignore @@ -0,0 +1,5 @@ +.cache +node_modules +dist +.idea +.DS_Store \ No newline at end of file diff --git a/backoffice/README.md b/backoffice/README.md new file mode 100644 index 0000000..d6faf0f --- /dev/null +++ b/backoffice/README.md @@ -0,0 +1,6 @@ +## Requirements + + + + +https://uppy.io/ \ No newline at end of file diff --git a/backoffice/package.json b/backoffice/package.json new file mode 100644 index 0000000..594e250 --- /dev/null +++ b/backoffice/package.json @@ -0,0 +1,58 @@ +{ + "name": "ek-hyperapp", + "version": "1.0.0", + "license": "MIT", + "main": "dist/counter.js", + "dependencies": { + "@types/jest": "^22.2.0", + "@types/node": "^9.4.7", + "@types/query-string": "^5.1.0", + "@types/traverson": "^2.0.28", + "autocomplete-js": "^2.6.4", + "awesome-typescript-loader": "5.0.0", + "babel-plugin-transform-react-jsx": "^6.24.1", + "babel-preset-env": "^1.6.1", + "clean-webpack-plugin": "^0.1.18", + "get-value": "^3.0.1", + "html-webpack-plugin": "^3.0.7", + "hyperapp": "^1.2.0", + "jest": "^22.3.0", + "kuker-emitters": "^6.7.4", + "lerna": "^2.10.1", + "query-string": "^6.0.0", + "set-value": "^3.0.0", + "stylus": "^0.54.5", + "traverson": "^6.0.3", + "ts-jest": "^22.4.1", + "ts-loader": "^4.2.0", + "typescript": "^2.7.2", + "uglifyjs-webpack-plugin": "^1.2.0", + "web-request": "^1.0.7", + "webpack": "^4.1.1", + "webpack-cli": "^2.0.12", + "webpack-dev-server": "^3.1.1", + "webpack-merge": "^4.1.1" + }, + "scripts": { + "start": "webpack-dev-server --open --config webpack.dev.js", + "build": "webpack --config webpack.prod.js", + "test": "jest src" + }, + "jest": { + "roots": [ + "/src" + ], + "transform": { + "^.+\\.tsx?$": "ts-jest" + }, + "testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$", + "moduleFileExtensions": [ + "ts", + "tsx", + "js", + "jsx", + "json", + "node" + ] + } +} diff --git a/backoffice/src/app.tsx b/backoffice/src/app.tsx new file mode 100644 index 0000000..3f51bff --- /dev/null +++ b/backoffice/src/app.tsx @@ -0,0 +1,189 @@ +import {ActionsType, app, View} from "hyperapp"; + +import {Container, ContainerBuilder, Definition} from "fugue-ioc/di"; + +import {AppState, AppActions, Context, UpdateStatePanel} from "fugue-app"; +import ActionDispatcher from "fugue-app/dispatcher"; + +import {createLoginPanel} from './view/Login'; +import {createAboutPanel} from './view/About'; + +import {ApiClient} from "./core/api"; + +export const kuker = (data: any) => { + try { + window.postMessage({ + ...data, + kuker: true, + time: (new Date()).getTime(), + }, '*'); + } catch(e) { + console.log(e); + } +}; + +export const deepCopy = (state: AppState) : AppState => { + let panels = state.panels; + + delete state.panels; + + state = JSON.parse(JSON.stringify(state)); + + state.panels = panels; + + return state; +}; + +let createContext: () => Context; + +const actions: ActionsType = { + dispatch: () => {}, + pushPanel: (panelCreator: Function) => (state: AppState) => { + let ctx = createContext(); + + state = deepCopy(state); + + state.panels[ctx.panelRef] = panelCreator(ctx); + state.values[ctx.panelRef] = ctx.state; // this state should be empty as not yet called by the panel closure. + + state.currentPanel = ctx.panelRef; + + state.panelStack.push(ctx.panelRef); + + kuker({ + type: 'pushPanel', + state: state, + icon: 'fa-plus-square', + color: '#002099' + }); + + return state; + }, + popPanel: () => (state: AppState) => { + state = deepCopy(state); + + let ref = (state.panelStack.pop() as string); + + delete state.panels[ref]; + delete state.values[ref]; + + state.currentPanel = state.panelStack[state.panelStack.length - 1]; + + kuker({ + type: 'popPanel', + state: state, + icon: 'fa-minus-square', + color: '#da7c00' + }); + + return state; + }, + updateState: (data: UpdateStatePanel) => (state: AppState) => { + if (data.ref == undefined) { + console.error("updateState global, panelRef is undefined"); + + return state; + } + + if (data.state == undefined) { + console.error("updateState global, state is undefined"); + + return state; + } + + state = deepCopy(state); + + state.values[data.ref] = data.state; + + kuker({ + type: 'updateState', + state: state, + icon: 'fa-sync-alt', + color: '#bada55' + }); + + return state; + } +}; + +const state: AppState = { + title: "App Administration", + values: new Map(), // store the state for the each panel + panels: new Map(), // store the panel (views) + currentPanel: '', + connected: false, + panelStack: [], +}; + +const view: View = (state: AppState, actions: AppActions) => { + console.log('Refresh UI'); + + return state.panels[state.currentPanel]; +}; + +const defaultConfiguration = { + api: { + url: "http://localhost:2508/api" + } +} + +function loadApp(target: Element | null, configuration = defaultConfiguration) { + + // get the configuration somewhere + + const builder = new ContainerBuilder(); + const container = new Container(); + + // core services + builder.set("actions.dispatcher", new Definition(ActionDispatcher, [])) + + // view constructors + builder.set("panels.login", new Definition(createLoginPanel, [])); + builder.set("panels.about", new Definition(createAboutPanel, [])); + + // action reaction + builder.set("actions.user.connection", new Definition(configuration.api.url, [], ['dispatcher.action'])) + + // external services + builder.set("gonode.api", new Definition(ApiClient, [])); + + + builder.build(container); + + let appActions = app( + state, + actions, + view, + target + ); + + createContext = (): Context => { + return { + actions: false, + state: false, + appActions: appActions, + panelRef: Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15), + updateState: (state: any) => {}, + get: (name: string): any => { + let [service, err] = container.get(name); + + if (err) { + console.error("Unable to retrieve the service", {name}); + } + + return service; + } + }; + }; + + let [service, err] = container.get('panels.login'); + + if (err) { + console.log("Unable to load the valid login panel"); + } + + appActions.pushPanel(service); +} + + +loadApp(document.body); diff --git a/backoffice/src/core/api.test.ts b/backoffice/src/core/api.test.ts new file mode 100644 index 0000000..ede9b7a --- /dev/null +++ b/backoffice/src/core/api.test.ts @@ -0,0 +1,143 @@ +import { Client, Query } from './api'; +import {stringify} from 'query-string'; + +async function getClient() : Promise { + let c = new Client('http://localhost:2508/api', 'v1.0'); + await c.signin('admin', 'admin'); + + return c; +} + +describe('Test api', () => { + it('should authenticate', async () => { + // with + let c = new Client('http://localhost:2508/api', 'v1.0'); + + // when + let result = await c.signin('admin', 'admin'); + + // then + expect(result).toBe(true); + expect(c.isAuthenticated()).toBe(true); + }); + + it('should not authenticate', async () => { + // with + let c = new Client('http://localhost:2508/api', 'v1.0'); + + // when + let result = await c.signin('admin', ''); + + // then + expect(result).toBe(false); + expect(c.isAuthenticated()).toBe(false); + }); + + it('should logout', async () => { + // with + let c = await getClient(); + + // when + let result = await c.logout(); + + // then + expect(result).toBe(true); + expect(c.isAuthenticated()).toBe(false); + }); + + it('should search node limit', async () => { + // with + let c = await getClient(); + + let query = new Query(1, 2); + + let pager = await c.find(query); + + expect(pager).toBeDefined(); + expect(pager.perPage).toEqual(2); + }); + + it('should search node type media.image', async () => { + // with + let c = await getClient(); + + let query = new Query(1, 2) + .addType('media.image'); + + let pager = await c.find(query); + + expect(pager).toBeDefined(); + expect(pager.perPage).toEqual(2); + expect(pager.items).toHaveLength(2); + + pager.items.map((node) => { + expect(node.type).toEqual('media.image'); + }) + }); + + it('should search node with order by', async () => { + // with + let c = await getClient(); + + let query = new Query(1, 2) + .addOrderBy('weight', 'DESC') + .addOrderBy('name'); + + let pager = await c.find(query); + + expect(pager).toBeDefined(); + expect(pager.perPage).toEqual(2); + expect(pager.items).toHaveLength(2); + }); + + it('should search node with meta and data', async () => { + // with + let c = await getClient(); + + let query = new Query(1, 2) + .addOrderBy('weight', 'DESC') + .addOrderBy('name') + .addData('username', 'user12') + ; + + let pager = await c.find(query); + + expect(pager).toBeDefined(); + expect(pager.perPage).toEqual(2); + expect(pager.items).toHaveLength(1); + }); + + it ('should find one', async () => { + let c = await getClient(); + + let query = new Query(1, 1); + + let pager = await c.find(query); + + query.addUuid(pager.items[0].uuid); + + let result = await c.findOne(query); + + expect(result).not.toBe(false); + //expect(result.uuid).toEqual(pager.items[0].uuid); + }); + + it('test queryString (format: bracket)', () => { + let r = stringify({ + 'foo': 'bar', + 'type': ['one', 'two'] + }, {arrayFormat: 'bracket'}); + + expect(r).toEqual("foo=bar&type[]=one&type[]=two"); + + }); + + it('test queryString (format: none)', () => { + let r = stringify({ + 'foo': 'bar', + 'type': ['one', 'two'] + }, {arrayFormat: 'none'}); + + expect(r).toEqual("foo=bar&type=one&type=two"); + }); +}); diff --git a/backoffice/src/core/api.ts b/backoffice/src/core/api.ts new file mode 100644 index 0000000..2cd0cce --- /dev/null +++ b/backoffice/src/core/api.ts @@ -0,0 +1,539 @@ +import {stringify} from 'query-string'; + +// export NodeMeta: Map; +// export NodeData: Map; + +export class Node { + private _uuid: string = ""; + private _type: string = ""; + private _name: string = ""; + private _slug: string = ""; + private _path: string = ""; + private _status: number = 0; + private _weight: number = 0; + private _revision: number = 0; + private _createdAt: string = ""; + private _updatedAt: string = ""; + private _enabled: boolean = false; + private _deleted: boolean = false; + private _parents: Array = []; + private _updatedBy: string = ""; + private _createdBy: string = ""; + private _parentUuid: string = ""; + private _source: string = ""; + private _modules: Array = []; + private _access: Array = []; + private _meta: Map = new Map(); + private _data: Map = new Map(); + + get uuid(): string { + return this._uuid; + } + + set uuid(value: string) { + this._uuid = value; + } + + get type(): string { + return this._type; + } + + set type(value: string) { + this._type = value; + } + + get name(): string { + return this._name; + } + + set name(value: string) { + this._name = value; + } + + get slug(): string { + return this._slug; + } + + set slug(value: string) { + this._slug = value; + } + + get path(): string { + return this._path; + } + + set path(value: string) { + this._path = value; + } + + get status(): number { + return this._status; + } + + set status(value: number) { + this._status = value; + } + + get weight(): number { + return this._weight; + } + + set weight(value: number) { + this._weight = value; + } + + get revision(): number { + return this._revision; + } + + set revision(value: number) { + this._revision = value; + } + + get createdAt(): string { + return this._createdAt; + } + + set createdAt(value: string) { + this._createdAt = value; + } + + get updatedAt(): string { + return this._updatedAt; + } + + set updatedAt(value: string) { + this._updatedAt = value; + } + + get enabled(): boolean { + return this._enabled; + } + + set enabled(value: boolean) { + this._enabled = value; + } + + get deleted(): boolean { + return this._deleted; + } + + set deleted(value: boolean) { + this._deleted = value; + } + + get parents(): Array { + return this._parents; + } + + set parents(value: Array) { + this._parents = value; + } + + get updatedBy(): string { + return this._updatedBy; + } + + set updatedBy(value: string) { + this._updatedBy = value; + } + + get createdBy(): string { + return this._createdBy; + } + + set createdBy(value: string) { + this._createdBy = value; + } + + get parentUuid(): string { + return this._parentUuid; + } + + set parentUuid(value: string) { + this._parentUuid = value; + } + + get source(): string { + return this._source; + } + + set source(value: string) { + this._source = value; + } + + get modules(): Array { + return this._modules; + } + + set modules(value: Array) { + this._modules = value; + } + + get access(): Array { + return this._access; + } + + set access(value: Array) { + this._access = value; + } + + get meta(): Map { + return this._meta; + } + + set meta(value: Map) { + this._meta = value; + } + + get data(): Map { + return this._data; + } + + set data(value: Map) { + this._data = value; + } +} + +export class Query { + private _page: number; + private _perPage: number; + private _orderBy: Array; + private _types: Array; + private _uuid: Array; + private _status: Array; + private _weight: Array; + private _enabled: boolean; + private _deleted: boolean; + private _updatedBy: Array; + private _createdBy: Array; + private _parentUuid: Array; + private _source: Array; + private _meta: Map>; + private _data: Map>; + + constructor(page = 1, perPage = 32) { + this._page = page; + this._perPage = perPage; + this._orderBy = []; + this._types = []; + this._uuid = []; + this._status = []; + this._weight = []; + this._updatedBy = []; + this._createdBy = []; + this._parentUuid = []; + this._source = []; + this._meta = new Map(); + this._data = new Map(); + this._enabled = true; + this._deleted = false; + } + + setPage(page: number): Query { + this._page = page; + + return this; + } + + setPerPage(perPage: number): Query { + this._perPage = perPage; + + return this; + } + + addOrderBy(field: string, order: string = 'ASC'): Query { + this._orderBy.push(`${field},${order}`); + + return this; + } + + addType(type: string): Query { + this._types.push(type); + + return this; + } + + addUuid(uuid: string): Query { + this._uuid.push(uuid); + + return this; + } + + setUuid(uuid: Array): Query { + this._uuid = uuid; + + return this; + } + + setEnabled(enabled: boolean): Query { + this._enabled = enabled; + + return this; + } + + setDeleted(deleted: boolean): Query { + this._deleted = deleted; + + return this; + } + + addUpdatedBy(updatedBy: string): Query { + this._updatedBy.push(updatedBy); + + return this; + } + + addCreatedBy(createdBy: string): Query { + this._createdBy.push(createdBy); + + return this; + } + + addData(field: string, value: any): Query { + if (!this._data.has(field)) { + this._data.set(field, []); + } + + let data = this._data.get(field); + if (data) { + data.push(value) + } + + return this + } + + addMeta(field: string, value: any): Query { + if (!this._meta.has(field)) { + this._meta.set(field, []); + } + + let meta = this._data.get(field); + if (meta) { + meta.push(value) + } + + return this + } + + get page(): number { + return this._page; + } + + get perPage(): number { + return this._perPage; + } + + get orderBy(): Array { + return this._orderBy; + } + + get types(): Array { + return this._types; + } + + get uuid(): Array { + return this._uuid; + } + + get status(): Array { + return this._status; + } + + get weight(): Array { + return this._weight; + } + + get enabled(): boolean { + return this._enabled; + } + + get deleted(): boolean { + return this._deleted; + } + + get updatedBy(): Array { + return this._updatedBy; + } + + get createdBy(): Array { + return this._createdBy; + } + + get parentUuid(): Array { + return this._parentUuid; + } + + get source(): Array { + return this._source; + } + + get meta(): Map> { + return this._meta; + } + + get data(): Map> { + return this._data; + } +} + +export interface Pager { + readonly page: number; + readonly perPage: number; + readonly items: Array; + readonly previous: number; + readonly next: number; +} + +export class ApiClient { + private readonly _url: string; + private readonly _version: string; + private _token: string; + + /** + * + * @param {string} url + * @param {string} version + */ + constructor(url: string, version: string = "1.0") { + this._url = url; + this._version = version; + this._token = ""; + } + + /** + * + * @param {Query} query + * @returns {Pager} + */ + async find(query: Query): Promise { + let qs = { + per_page: query.perPage, + page: query.page, + type: query.types, + uuid: query.uuid, + status: query.status, + weight: query.weight, + enabled: query.enabled, + deleted: query.deleted, + updated_by: query.updatedBy, + created_by: query.createdBy, + parent_uuid: query.parentUuid, + source: query.source, + order_by: query.orderBy, + }; + + query.meta.forEach((v, k) => { + qs[`meta.${k}`] = v; + }); + + query.data.forEach((v, k) => { + qs[`data.${k}`] = v; + }); + + let response = await this.doAuthRequest('GET', `/nodes?${stringify(qs)}`); + + return { + page: response.page, + perPage: response.per_page, + items: response.elements, + previous: response.previous, + next: response.next, + }; + } + + private async doAuthRequest(method: string, url: string, body = null): Promise { + + let response: Response; + + try { + response = await fetch(`${this._url}/v${this._version}/${url}`, { + method: method, + headers: { + Authorization: `Bearer ${this._token}` + } + }); + } catch(e) { + return false; + } + + if (!response.ok) { + return false; + } + + return await response.json(); + } + + async findOne(query: Query): Promise { + let pager = await this.find(query); + + if (pager.items.length != 1) { + return false; + } + + return pager.items[0]; + } + + /** + * + * @param {string} username + * @param {string} password + * @returns {Promise} + */ + async signin(username: string, password: string): Promise { + const params = new URLSearchParams(); + params.append('username', username); + params.append('password', password); + + let response: Response; + + try { + response = await fetch(`${this._url}/v${this._version}/login`, { + method: 'POST', + body: params + }); + } catch (e) { + console.error(e); + + return false; + } + + if (!response.ok) { + return false; + } + + const auth = await (response.json() as Promise); + + if (auth.status === "OK") { + this._token = auth.token; + + return true; + } + + return false; + } + + /** + * + * @returns {boolean} + */ + isAuthenticated() { + return this._token.length > 0; + } + + /** + * + * @returns {Promise} + */ + async logout(): Promise { + this._token = ""; + + return true; + } +} + +export interface AuthResult { + message: string, + status: string, + token: string +} diff --git a/backoffice/src/fugue/docs/form.md b/backoffice/src/fugue/docs/form.md new file mode 100644 index 0000000..bb7b8d7 --- /dev/null +++ b/backoffice/src/fugue/docs/form.md @@ -0,0 +1,38 @@ +Form +===== + + +### Email Type + + + +### Textarea Type + + + +### Select Type + + + + + + +### Multiselect Type + + + + + + + +### Checkbox Type + + + + + +### Radio Type + + + + \ No newline at end of file diff --git a/backoffice/src/fugue/packages/fugue-app/dispatcher.ts b/backoffice/src/fugue/packages/fugue-app/dispatcher.ts new file mode 100644 index 0000000..405524a --- /dev/null +++ b/backoffice/src/fugue/packages/fugue-app/dispatcher.ts @@ -0,0 +1,16 @@ +export default class ActionDispatcher { + private actions: Map = new Map(); + + register(name: string, action: any): void { + this.actions[name] = action; + } + + dispatch(name: string, args: Array): any | boolean { + if (!(name in this.actions)) { + console.log(`Action ${name} is not registered`); + return false; + } + + return this.actions[name](...args); + } +} \ No newline at end of file diff --git a/backoffice/src/fugue/packages/fugue-app/index.tsx b/backoffice/src/fugue/packages/fugue-app/index.tsx new file mode 100644 index 0000000..595a4e2 --- /dev/null +++ b/backoffice/src/fugue/packages/fugue-app/index.tsx @@ -0,0 +1,120 @@ +import {VNode, h} from "hyperapp"; + +export interface UpdateStatePanel { + ref: string + state: any +} + +export interface AppActions { + dispatch(name: string, arg: Array): AppState + pushPanel(panel: VNode): AppState + updateState(data: UpdateStatePanel): AppState + popPanel(data: UpdateStatePanel): AppState +} + +export interface AppState { + title: string + values: Map + panels: Map + panelStack: Array + connected: boolean, + currentPanel: string +} + +export interface Context { + actions: any + state: any + appActions: any + readonly panelRef: string + updateState(state: any): void + get(id: string): any +} + +export interface PanelProps { + ctx: Context + actions: any + state: any +} + +export const currier = (callback: Function, ...innerArgs: Array) => { + return (...args: Array) => { + return callback(...args)(...innerArgs); + } +}; + +export const applyCurry = (subject: Object, ...innerArgs: Array) => { + Object.keys(subject).map((key) => { + subject[key] = currier(subject[key], ...innerArgs) + }); + + return subject; +}; + +export const Panel = (props: PanelProps, children: Array) => (state: AppState, actions: AppActions) : VNode => { + // console.log('Render panel', {props, state}); + + if (!props.ctx) { + console.error("Missing the ctx parameter", {props}); + + return
; + } + + console.log("Create ctx", props.ctx, props.actions, props.state, state.values[props.ctx.panelRef]); + + let ctx = props.ctx; + ctx.actions = applyCurry(props.actions || {}, props.ctx); + ctx.state = state.values[ctx.panelRef] || props.state; + ctx.updateState = (callback) => { + let panelState = {}; + + if (typeof callback === 'function') { + panelState = callback(state.values[ctx.panelRef]) + } else { + panelState = callback; + } + + actions.updateState({ref: ctx.panelRef, state: panelState}); + }; + + let finalChildren = children + .filter((child) => typeof child === 'function') + .map((child) => { + return ({...args}) => (state: AppState, actions: AppActions): VNode => { + const callback = (child as Function); + + return callback(ctx); + } + }); + + if (finalChildren.length === 0) { + return
; + } + + return (finalChildren[0] as Function)(ctx); +}; + +export const Zone = (props: any, children: Array) => { + let finalProps = { + ...props, + class: `${props.class ? props.class + ' ' : ''}zone-${props.name}` + }; + + return + { children } + +}; + +export const renderZone = (name: string, children: Array): VNode | void => { + // remove non zone element + let zone = children + .filter((child: VNode) => { + return child.nodeName === 'zone' && child.attributes && child.attributes['name'] === name; + }) + .shift(); + + if (!zone) { + return; + } + + return h("div", zone.attributes, zone.children); +}; \ No newline at end of file diff --git a/backoffice/src/fugue/packages/fugue-app/package.json b/backoffice/src/fugue/packages/fugue-app/package.json new file mode 100644 index 0000000..4c95910 --- /dev/null +++ b/backoffice/src/fugue/packages/fugue-app/package.json @@ -0,0 +1,9 @@ +{ + "name": "fugue-app", + "version": "0.0.1", + "main": "index.tsx", + "license": "MIT", + "dependencies": { + "hyperapp": "^1.2.0" + } +} \ No newline at end of file diff --git a/backoffice/src/fugue/packages/fugue-form/bootstrap.tsx b/backoffice/src/fugue/packages/fugue-form/bootstrap.tsx new file mode 100644 index 0000000..6bbc490 --- /dev/null +++ b/backoffice/src/fugue/packages/fugue-form/bootstrap.tsx @@ -0,0 +1,76 @@ +import {h, VNode} from "hyperapp"; +import {getAttributes, InputDefinition, MetaRenderFieldOption, defaultRenderField} from "./index"; + +const renderFormGroup = (definition: InputDefinition, meta: MetaRenderFieldOption): VNode => { + const attr = getAttributes(definition); + + attr.class = `${attr.class ? attr.class : ''} form-control`; + + if (meta.touched && meta.error) { + attr.class += ' is-invalid'; + } + + const input = h(definition.nodeName, attr, definition.children); + + return
+ + + {input} + {meta.touched && ( + (meta.error &&
{meta.error}
) || (meta.warning && {meta.warning}) + )} +
; +}; + +const renderCheckGroup = (definition: InputDefinition, meta: MetaRenderFieldOption): VNode => { + const attr = getAttributes(definition); + + attr.class = `${attr.class ? attr.class : ''} form-check-input`; + + if (meta.touched && meta.error) { + attr.class += ' is-invalid'; + } + + const input = h(definition.nodeName, attr, definition.children); + + return
+ {input} + + {meta.touched && ( + (meta.error &&
{meta.error}
) || (meta.warning && {meta.warning}) + )} +
; +}; + +const renderButton = (definition: InputDefinition, meta: MetaRenderFieldOption): VNode => { + + const attr = getAttributes(definition); + + attr.class = `btn ${attr.class ? attr.class : ''}`; + + const input = h(definition.nodeName, attr, definition.children); + + return input; +} + +export const renderField = (definition: InputDefinition, meta: MetaRenderFieldOption): VNode => { + switch (`${definition.nodeName}${definition.type ? '-' + definition.type : ''}`) { + case 'input-text': + case 'input-password': + case 'input-email': + case 'select': + case 'textarea': + return renderFormGroup(definition, meta); + + case 'input-checkbox': + case 'input-radio': + return renderCheckGroup(definition, meta); + + case 'input-button': + return renderButton(definition, meta); + + default: + console.log('bootstrap: using default renderer', `${definition.nodeName}${definition.type ? '-' + definition.type : ''}`); + return defaultRenderField(definition, meta); + } +}; \ No newline at end of file diff --git a/backoffice/src/fugue/packages/fugue-form/index.tsx b/backoffice/src/fugue/packages/fugue-form/index.tsx new file mode 100644 index 0000000..9e10676 --- /dev/null +++ b/backoffice/src/fugue/packages/fugue-form/index.tsx @@ -0,0 +1,438 @@ +import {h, VNode} from "hyperapp"; + +import getValue from "get-value"; +import setValue from "set-value"; + +interface FugueHTMLInputElement extends HTMLInputElement { + definition: InputDefinition +} + +interface Context { + getState(): any + setState(state: any): any + name: string + renderField?(definition: InputDefinition, meta: MetaRenderFieldOption): VNode +} + +interface NodeHandler { + (node: VNode): VNode; +} + +export interface MetaRenderFieldOption { + touched: boolean + error: string + warning: string +} + +// public interface with optional parameters (DX) +export interface FieldProps { + name?: string + type: string | Function + component?: string | Function + label?: string + placeholder?: string + meta?: MetaRenderFieldOption + value?: string | number + validators?: Function[] + onclick?: Function | null + disabled?: boolean + class?: string + // format(value: any, name: string): any, + // normalize(value, previousValue, allValues, previousAllValues): value + // validate: Array +} + +// internal interface with default valid values. +interface InnerFieldProps { + name: string + type: string | Function + component: string | Function + label: string + placeholder: string + meta: MetaRenderFieldOption + value: string | number + validators: Function[] + onclick: Function | null + disabled: boolean + class: string + // format(value: any, name: string): any, + // normalize(value, previousValue, allValues, previousAllValues): value + // validate: Array +} + +export interface InputDefinition { + name: string + nodeName: string + value: any + type: string | null + label: string + placeholder: string + children: VNode[] + events: { + oncreate: Function | null + oninput: Function | null + onclick: Function | null + onchange: Function | null + } + validators: Function[] + extra: Object + class: string +} + +export const writeValue = (state: any, key: string, value: any): void => { + return setValue(state, key, value, {}); +}; + +export const readValue = (state: any, key: string): any => { + return getValue(state, key, null) +}; + +const visitor = (node: VNode | Function, callback: NodeHandler): VNode => { + if (typeof node === "function") { + node = (node() as VNode); + } + + node = callback(node); + + if (!node.children) { + return node; + } + + node.children = node.children.map((cnode: VNode | string): VNode | string => { + if (typeof cnode === "string") { + return cnode; + } + + return visitor(cnode, callback); + }); + + return node; +}; + +export const getAttributes = (definition: InputDefinition): any => { + const attributes = definition.extra; + + const validAttributes = ['class', 'value', 'type', 'placeholder']; + + validAttributes.map((a) => { + if (!definition[a]) { + return; + } + + attributes[a] = definition[a]; + }); + + Object.keys(definition.events).map((name) => { + attributes[name] = definition.events[name]; + }); + + return attributes; +}; + +export const defaultRenderField = (definition: InputDefinition, meta: MetaRenderFieldOption): VNode => { + + // if (meta.touched && meta.error) { + // definition.class += ' is-invalid'; + // } + + const input = h(definition.nodeName, getAttributes(definition), definition.children); + + return
+ + + {input} + {meta.touched && ( + (meta.error &&
{meta.error}
) || (meta.warning && {meta.warning}) + )} +
; +}; + +export const normalizeFieldProps = (attributes: any) : InnerFieldProps => { + return { + name: '', + type: 'text', + component: 'input', + label: '', + placeholder: '', + meta: { + touched: false, + error: '', + warning: '', + }, + value: '', + class: '', + disabled: false, + validators: [], + ...attributes + } +}; + +const getMeta = (state: any, name: string) : MetaRenderFieldOption => { + let meta = readValue(state, 'meta.' + name); + + if (!meta) { + meta = { + touched: false, + error: '', + warning: '' + } + } + + return meta; +}; + +export const Field = (props: FieldProps, children: VNode[]): VNode => { + return {children} +}; + + +const inputHandler = (ctx: Context, name: string) => (evt: Event) => { + const target = (evt.target as FugueHTMLInputElement); + + let value : any; + + let meta = readValue(ctx.getState(), 'meta.' + name); + if (!meta) { + meta = { + touched: true, + error: '', + warning: '' + } + } + + if (target.type === 'checkbox') { + value = target.checked ? target.value : ''; + } else { + value = target.value; + } + + meta.error = ''; + + const definition = (target.definition as InputDefinition); + + if (definition) { + definition.validators.map((v) => { + const error = v(value); + + if (meta.error !== '') { + return; + } + + meta.error = error ? error.msg : ''; // first error win. + }); + } + + let state = writeValue(ctx.getState(), name, value); + + state = writeValue(state, 'meta.' + name, meta); + + // console.log("Form > inputHandler", {state, value, evt}); + + ctx.setState(state); +}; + +const createInputDefinition = (def: any) : InputDefinition => { + + return { + nodeName: 'input', + value: null, + type: null, + label: '', + placeholder: '', + children: [], + events: { + oncreate: null, + oninput: null, + onclick: null, + onchange: null, + }, + extra: {}, + class: '', + validators: [], + ...def + } +}; + +const getInputDefinition = (node: VNode, ctx: Context): InputDefinition | boolean => { + const attributes = normalizeFieldProps(node.attributes); + + const extra = { + disabled: attributes.disabled + }; + + switch (attributes.type) { + case 'email': + case 'password': + case 'text': + return createInputDefinition({ + name: attributes.name, + label: attributes.label, + class: attributes.class, + value: readValue(ctx.getState(), attributes.name), + type: attributes.type, + placeholder: attributes.placeholder, + events: { + oninput: inputHandler(ctx, attributes.name), + }, + extra: extra, + validators: attributes.validators, + }); + + case 'textarea': + return createInputDefinition({ + name: attributes.name, + label: attributes.label, + class: attributes.class, + nodeName: 'textarea', + value: readValue(ctx.getState(), attributes.name), + placeholder: attributes.placeholder, + events: { + oninput: inputHandler(ctx, attributes.name), + }, + children: [ + readValue(ctx.getState(), attributes.name) + ], + extra: extra, + validators: attributes.validators, + }); + + case 'select-multiple': + case 'select': + let values = readValue(ctx.getState(), attributes.name); + + if (typeof values === "number") { + values = [values.toString()]; + } + + if (typeof values === "string") { + values = [values]; + } + + if (attributes.type == 'select-multiple') { + extra['multiple'] = 'multiple'; + } + + return createInputDefinition({ + name: attributes.name, + label: attributes.label, + class: attributes.class, + nodeName: 'select', + value: readValue(ctx.getState(), attributes.name), + events: { + oninput: inputHandler(ctx, attributes.name), + }, + children: (node.children as Array).map((option: VNode) => { + if (!option.attributes) { + option.attributes = {}; + } + + if (!values.includes(option.attributes['value'])) { + return option; + } + + option.attributes['selected'] = 'selected'; + + return option; + }), + extra: extra, + validators: attributes.validators, + }); + + case 'checkbox': + case 'radio': + if (readValue(ctx.getState(), attributes.name) == attributes.value) { + extra['checked'] = 'checked'; + } + + return createInputDefinition({ + name: attributes.name, + label: attributes.label, + value: attributes.value, + class: attributes.class, + type: attributes.type, + events: { + onchange: inputHandler(ctx, attributes.name), + }, + extra: extra, + validators: attributes.validators, + }); + + case 'button': + let submitHandler = (callback: any) => (e: Event) => { + e.preventDefault(); + + // console.log("event from form", e); + + callback(ctx.getState()); + }; + + return createInputDefinition({ + name: attributes.name, + value: attributes.label, + class: attributes.class, + type: attributes.type, + events: { + onclick: submitHandler(attributes.onclick), + }, + extra: extra, + validators: attributes.validators, + }); + + default: + return false + + } +}; + +export const Form = (props: Context, children: Array) => { + const handler = (node: VNode): VNode => { + if (node.nodeName !== 'field') { + // console.debug("Form > ignore node, not handled node", {nodeName: node.nodeName, node: node}); + + return node; + } + + const field = (node as VNode); + + if (!field.attributes) { + // console.debug("Form > ignore node, missing `attributes` property", {nodeName: field.nodeName, field: field}); + + return field; + } + + // if (field.attributes && !field.attributes['name']) { + // console.debug("Form > ignore valid node, missing `name` attribute", {nodeName: field.nodeName, type: field.attributes.type, field: field}); + // + // return field; + // } + + // console.debug("Form > Configure input", {type: field.attributes.type, field: field}); + + const innerInputDefinition = getInputDefinition(field, props); + + if (innerInputDefinition === false || innerInputDefinition === true) { // not a VNode + return field; + } + + innerInputDefinition.events['oncreate'] = (element: FugueHTMLInputElement) => { + element.definition = innerInputDefinition; + }; + + switch (field.attributes.component) { + // case 'input': + // return innerInput; + default: + // if (typeof field.attributes.component === 'function') { + // return field.attributes.component(innerInput, getMeta(props.getState(), field.attributes['name'])); + // } + + const renderField = props.renderField ? props.renderField : defaultRenderField; + + return renderField(innerInputDefinition, getMeta(props.getState(), innerInputDefinition.name)); + } + }; + + return
+ {children.map((cnode: VNode) => visitor(cnode, handler))} +
+}; diff --git a/backoffice/src/fugue/packages/fugue-form/package.json b/backoffice/src/fugue/packages/fugue-form/package.json new file mode 100644 index 0000000..9d6b6d4 --- /dev/null +++ b/backoffice/src/fugue/packages/fugue-form/package.json @@ -0,0 +1,11 @@ +{ + "name": "fugue-form", + "version": "0.0.1", + "main": "index.tsx", + "license": "MIT", + "dependencies": { + "hyperapp": "^1.2.0", + "set-value": "^3.0.0", + "get-value": "^3.0.1" + } +} \ No newline at end of file diff --git a/backoffice/src/fugue/packages/fugue-ioc/README.md b/backoffice/src/fugue/packages/fugue-ioc/README.md new file mode 100644 index 0000000..c796f2a --- /dev/null +++ b/backoffice/src/fugue/packages/fugue-ioc/README.md @@ -0,0 +1,4 @@ +IOC +=== + + diff --git a/backoffice/src/fugue/packages/fugue-ioc/di.test.tsx b/backoffice/src/fugue/packages/fugue-ioc/di.test.tsx new file mode 100644 index 0000000..b923d0e --- /dev/null +++ b/backoffice/src/fugue/packages/fugue-ioc/di.test.tsx @@ -0,0 +1,97 @@ + +import { Container, Definition, ContainerBuilder } from './di'; + +class FooBar { + private counter: number; + + constructor(counter: number) { + this.counter = counter; + } +} + +function ServiceConstructor(arg: string) { + return () => { + return arg; + }; +} + +function AnyService(...args: Array) { + return () => { + return args; + }; +} + +describe('Container', () => { + it('should get a value', () => { + let c = new Container(); + + c.set('foo', 'bar'); + let [r, err] = c.get('foo'); + + expect(r).toBe('bar'); + expect(err).toBe(null); + }); + + it('test container', () => { + let c = new Container(); + let [s, err] = c.get('non existant service'); + + expect(s).toBe(null); + expect(err).toBeDefined(); + }); + + it('test service builder', () => { + let b = new ContainerBuilder(); + let c = new Container(); + + b.set('service.constructor', new Definition(ServiceConstructor, ["hello thomas"])); + b.set('foo.42', new Definition(FooBar, [42])); + b.set('foo.2', new Definition(FooBar, [2])); + b.set('foo.1', new Definition(FooBar, [1])); + + b.build(c); + + let [s1, err1] = c.get('service.constructor'); + + expect(s1).toBeDefined(); + expect(err1).toBe(null); + + let [s2, err2] = c.get('foo.42'); + expect(s2).toBeDefined(); + expect(err2).toBe(null); + expect(s2.counter).toBe(42); + }); + + it('test service alias', () => { + let b = new ContainerBuilder(); + let c = new Container(); + + b.def('service.child', AnyService, ['child']); + b.def('service.parent', AnyService, ['parent', '@service.child']); + + b.build(c); + + let [s, err] = c.get('service.parent'); + expect(s).toBeDefined(); + expect(err).toBe(null); + + let [arg1, arg2] = s(); + + expect(arg1).toEqual('parent'); + expect(typeof arg2 === 'function').toBe(true); + expect(arg2()).toEqual(['child']); + }); + + it('test infinite loop with circular reference', () => { + let b = new ContainerBuilder(); + let c = new Container(); + + b.def('service.child', AnyService, ['child', '@service.parent']); + b.def('service.parent', AnyService, ['parent', '@service.child']); + + let [container, err] = b.build(c); + + expect(container.services).toBeDefined(); + expect(err).toBeDefined(); + }); +}); \ No newline at end of file diff --git a/backoffice/src/fugue/packages/fugue-ioc/di.tsx b/backoffice/src/fugue/packages/fugue-ioc/di.tsx new file mode 100644 index 0000000..55524cc --- /dev/null +++ b/backoffice/src/fugue/packages/fugue-ioc/di.tsx @@ -0,0 +1,155 @@ +type ResultValue = any | null; +type ResultError = Error | null; +type FuncResult = [ResultValue, ResultError]; +type Arg = string | any; + +let nonExistantError = new Error('Service does not exist'); +let circularReferenceError = new Error('Circular reference error'); + +export interface ContainerInterface { + get(name: string): FuncResult; + set(name: string, service: any): ContainerInterface; +} + +export class Definition { + private klass: any; + private args: Array; + private tags: string[]; + + constructor(klass: any, args: Arg[] = [], tags: string[] = []) { + this.klass = klass; + this.args = args; + this.tags = tags; + } + + getArguments() { + return this.args; + } + + getClass() { + return this.klass; + } + + getTags() { + return this.tags; + } +} + +export class ContainerBuilder { + private definitions: Map; + private currents: Array; + + constructor() { + this.definitions = new Map(); + this.currents = []; + } + + get(name: string): Definition | undefined { + return this.definitions.get(name); + } + + set(name: string, definition: Definition): ContainerBuilder { + this.definitions.set(name, definition); + + return this; + } + + def(name: string, klass: any, args: Arg[]): ContainerBuilder { + this.set(name, new Definition(klass, args)); + + return this; + } + + build(container: Container): FuncResult { + let error: Error; + this.definitions.forEach((v: Definition, k: string) => { + + let [service, err] = this.buildService(k, v); + + if (error) { + return; + } + + if (err) { + error = err; + return; + } + + container.set(k, service); + }); + + return [container, null]; + } + + private buildService(name: string, def: Definition): FuncResult { + let lastError = null; + + if (this.currents.includes(name)) { + return [null, circularReferenceError]; + } + + this.currents.push(name); + + // resolve argument + let args = def.getArguments().map((arg: any) => { + if (typeof arg !== 'string') { + return arg; + } + + if (arg.length > 0 && arg[0] === '@') { + // solve deps + let childDefinition = this.get(arg.substring(1)); + + if (!childDefinition) { + lastError = nonExistantError; + return arg; + } + + let [child, err] = this.buildService(arg.substring(1), childDefinition); + + if (err) { + lastError = err; + return arg; + } + + return child; + } + + return arg; + }); + + this.currents.pop(); + + let service: any; + + try { + service = new (def.getClass())(...args); + } catch (err) { // not a class + service = (def.getClass())(...args); + } + + return [service, lastError]; + } +} + +export class Container implements ContainerInterface { + private services: Map; + + constructor() { + this.services = new Map(); + } + + get(name: string): FuncResult { + if (name in this.services) { + return [this.services[name], null]; + } + + return [null, new Error('Not know service')]; + } + + set(name: string, service: any): Container { + this.services[name] = service; + + return this; + } +} diff --git a/backoffice/src/fugue/packages/fugue-ioc/package.json b/backoffice/src/fugue/packages/fugue-ioc/package.json new file mode 100644 index 0000000..04f2928 --- /dev/null +++ b/backoffice/src/fugue/packages/fugue-ioc/package.json @@ -0,0 +1,6 @@ +{ + "name": "fugue-ioc", + "version": "0.0.1", + "main": "di.tsx", + "license": "MIT" +} \ No newline at end of file diff --git a/backoffice/src/fugue/packages/fugue-validator/index.test.ts b/backoffice/src/fugue/packages/fugue-validator/index.test.ts new file mode 100644 index 0000000..0143a79 --- /dev/null +++ b/backoffice/src/fugue/packages/fugue-validator/index.test.ts @@ -0,0 +1,41 @@ +import {Email, MinLength, MaxLength, NotEmpty, Url, MustContains} from './index'; + +const tests = [ + ['Test Email: invalid emails', Email, [], 'thomas.rabaix@gmail', {"keys": {}, "msg": "invalid email"}], + ['Test Email: valid emails', Email, [], 'thomas.rabaix@gmail.com', null], + + ['Test MinLength: valid string', MinLength, [2], 'hello', null], + ['Test MinLength: invalid length', MinLength, [10], 'hello', {"keys": {"length": "5", "min": 10}, "msg": "invalid length"}], + ['Test MinLength: invalid string (number)', MinLength, [10], 18, {"keys": {}, "msg": "invalid string"}], + + ['Test MaxLength: string', MaxLength, [2], 'hello', {"keys": {"length": "5", "max": 2}, "msg": "invalid length"}], + ['Test MaxLength: invalid length', MaxLength, [10], 'hello', null], + ['Test MaxLength: invalid length', MaxLength, [10], 18, {"keys": {}, "msg": "invalid string"}], + + ['Test NotEmpty: empty string', NotEmpty, [], '', {"keys": {}, "msg": "value is empty"}], + ['Test NotEmpty: empty object', NotEmpty, [], {}, {"keys": {}, "msg": "value is empty"}], + ['Test NotEmpty: empty array', NotEmpty, [], [], {"keys": {}, "msg": "value is empty"}], + ['Test NotEmpty: valid string', NotEmpty, [], 'hello', null], + ['Test NotEmpty: valid object', NotEmpty, [], {foo: 2}, null], + ['Test NotEmpty: valid array', NotEmpty, [], [1], null], + + ['Test Url: invalid Url', Url, [], 'http:foo.bar', {"keys": {}, "msg": "invalid url"}], + ['Test Url: valid Url', Url, [], 'http://foo.bar', null], + + ['Test MustContains: contains any', MustContains, [['@', '!', '$'], 2], 'thomas', {"keys": {"atleast": 2, "chars": ["@", "!", "$"]}, "msg": 'words must contains some characters'}], + ['Test MustContains: contains only 1', MustContains, [['@', '!', '$'], 2], 'thomas@', {"keys": {"atleast": 2, "chars": ["@", "!", "$"]}, "msg": 'words must contains some characters'}], + ['Test MustContains: valid', MustContains, [['@', '!', '$'], 2], 'thomas@$', null] +]; + + +describe('Test validators', () => { + tests.map((test) => { + it(test[0], () => { + const v = test[1](...test[2]); + const r = v(test[3]); + const e = test[4]; + + expect(r).toEqual(e); + }) + }) +}; diff --git a/backoffice/src/fugue/packages/fugue-validator/index.ts b/backoffice/src/fugue/packages/fugue-validator/index.ts new file mode 100644 index 0000000..194dd71 --- /dev/null +++ b/backoffice/src/fugue/packages/fugue-validator/index.ts @@ -0,0 +1,112 @@ + + +let rEmail = /^((([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))$/i; +let rUrl = /^(https?|ftp):\/\/(((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:)*@)?(((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?)(:\d*)?)(\/((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)+(\/(([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)*)*)?)?(\?((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|[\uE000-\uF8FF]|\/|\?)*)?(\#((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|\/|\?)*)?$/i; + + +// interface Validator { +// (source: string, subString: string): boolean; +// } + +type ValidationResult = ValidationMessage | null; + +interface ValidationMessage { + msg: string, + keys: Object +} + +export const Email = () => (value: string, values: Object): ValidationResult => { + return rEmail.test(value) ? null : {msg: 'invalid email', keys: {}}; +}; + +export const Url = () => (value: string, values: Object): ValidationResult => { + return rUrl.test(value) ? null : {msg: 'invalid url', keys: {}}; +}; + +export const MinLength = (min: number) => (value: string, values: Object): ValidationResult => { + if (typeof value !== 'string') { + return { + msg: 'invalid string', + keys: {} + }; + } + + if (value.length < min) { + return { + msg: 'invalid length', + keys: { + 'length': value.length.toString(10), + 'min': min + } + } + } + + return null +}; + +export const MaxLength = (max: number) => (value: string, values: Object): ValidationResult => { + if (typeof value !== 'string') { + return { + msg: 'invalid string', + keys: {} + } + } + + if (value.length > max) { + return { + msg: 'invalid length', + keys: { + length: value.length.toString(10), + max: max + } + } + } + + return null +}; + +export const MustContains = (chars: Array, atleast: number = 1) => (value: string, values: Object): ValidationResult => { + if (typeof value !== 'string') { + return { + msg: 'invalid string', + keys: {} + } + } + + let found: Array = []; + + chars.map((char) => { + if (value.includes(char) && !found.includes(char)) { + found.push(char); + } + }); + + if (found.length >= atleast) { + return null; + } + + return { + msg: 'words must contains some characters', + keys: { + chars, + atleast + } + }; +}; + +export const NotEmpty = () => (value: any, values: Object): ValidationResult => { + const error = { + msg: 'value is empty', + keys: {} + }; + + if (typeof value == 'string' && value.length == 0) { + return error; + } + + if (typeof value === 'object' && Object.keys(value).length === 0) { + return error; + } + + return null; +}; diff --git a/backoffice/src/fugue/packages/fugue-validator/package.json b/backoffice/src/fugue/packages/fugue-validator/package.json new file mode 100644 index 0000000..8a2e2ff --- /dev/null +++ b/backoffice/src/fugue/packages/fugue-validator/package.json @@ -0,0 +1,6 @@ +{ + "name": "fugue-validator", + "version": "0.0.1", + "main": "index.tsx", + "license": "MIT" +} \ No newline at end of file diff --git a/backoffice/src/index.html b/backoffice/src/index.html new file mode 100644 index 0000000..15bb70f --- /dev/null +++ b/backoffice/src/index.html @@ -0,0 +1,15 @@ + + + + + <%= htmlWebpackPlugin.options.title %> + + + + + +
+ + + + diff --git a/backoffice/src/module.d.ts b/backoffice/src/module.d.ts new file mode 100644 index 0000000..751b533 --- /dev/null +++ b/backoffice/src/module.d.ts @@ -0,0 +1,11 @@ +declare module 'set-value' { + function set(target: any, path: string, value: any, options: any): any + + export default set +} + +declare module 'get-value' { + function get(target: any, path: string, options: any): any + + export default get +} diff --git a/backoffice/src/traverson.test.tsx b/backoffice/src/traverson.test.tsx new file mode 100644 index 0000000..e69de29 diff --git a/backoffice/src/view/About/index.tsx b/backoffice/src/view/About/index.tsx new file mode 100644 index 0000000..86e7b27 --- /dev/null +++ b/backoffice/src/view/About/index.tsx @@ -0,0 +1,28 @@ +import {h, VNode} from "hyperapp"; +import {Context, Panel, Zone} from "fugue-app"; +import {NotConnectedTemplate} from "../Shared/Templates"; + +export const createAboutPanel = () => (ctx: Context) => (): VNode => { + return + {(ctx: Context) => { + + return + +

About the page

+

+ Hello, this is a solution to create application
+ based on hyperapp. +

+ +

+ { + console.log("onclick", ctx); + //ctx.appActions.popPanel(); + e.preventDefault(); + }} href="/about">Close About Page +

+
+
+ }} +
+}; \ No newline at end of file diff --git a/backoffice/src/view/Dashboard/index.tsx b/backoffice/src/view/Dashboard/index.tsx new file mode 100644 index 0000000..e69de29 diff --git a/backoffice/src/view/Login/index.tsx b/backoffice/src/view/Login/index.tsx new file mode 100644 index 0000000..fe6d9bc --- /dev/null +++ b/backoffice/src/view/Login/index.tsx @@ -0,0 +1,97 @@ +import {h, VNode} from "hyperapp"; + +import {renderField} from "fugue-form/bootstrap"; +import {Form, Field} from "fugue-form"; +import {Context, Panel, Zone} from "fugue-app"; + +import {NotConnectedTemplate} from "../Shared/Templates"; +import {Footer} from "../Shared/Footer"; + +import {ApiClient} from "../../core/api"; + +enum LoginStatus {Loading, Error, Valid, UserInput} + +interface LoginState { + status: LoginStatus + model: { + username: string + password: string + } +} + +interface LoginActions { + login: () => LoginState + updateStatus: (value: LoginStatus) => LoginState + inlineAction: (value: number) => LoginState +} + +export const LoginAction = (api: ApiClient) => () => async (ctx: Context) => { + const actions = (ctx.actions as LoginActions); + + actions.updateStatus(LoginStatus.Loading); + + const result = await api.signin(ctx.state.model.username, ctx.state.model.password); + + if (result) { + // valid, + actions.updateStatus(LoginStatus.Valid); + + ctx.appActions.dispatch("user.connection", {result: true}); + + return + } + + actions.updateStatus(LoginStatus.Error); +}; + +export const UpdateStatus = () => (value: LoginStatus) => (ctx: Context) => { + ctx.updateState({...ctx.state, status: value}); +}; + +export const createLoginPanel = () => (ctx: Context) => (): VNode => { + const state = { + status: LoginStatus.UserInput, + model: { + username: 'admin', + password: 'admin', + }, + }; + + // need to put this in the DI. + const actions = { + updateStatus: UpdateStatus(), + login: LoginAction(ctx.get("gonode.api")), + }; + + return + {(ctx: Context) => { + const state = (ctx.state as LoginState); + const actions = (ctx.actions as LoginActions); + + return + + + { state.status === LoginStatus.Valid ? +
Login Successful!
: null + } + + { state.status === LoginStatus.Loading ? +
Please wait while authentication!
: null + } + +
ctx.updateState(state)} getState={() => ctx.state} name={"login"} renderField={renderField}> + + + actions.login()} + disabled={state.status === LoginStatus.Loading} + /> + +
+