├── .github
└── FUNDING.yml
├── public
├── favicon.png
├── pix
│ ├── bug.png
│ ├── cd.png
│ ├── comics.png
│ ├── music.png
│ ├── resto.gif
│ ├── resto.png
│ ├── star.png
│ ├── todo.gif
│ ├── todo.png
│ ├── user.png
│ ├── wine.gif
│ ├── contact.gif
│ ├── contact.png
│ ├── doc
│ │ ├── cog.png
│ │ ├── model.png
│ │ ├── object.png
│ │ ├── wrench.png
│ │ ├── metadata.png
│ │ └── tag_pink.png
│ ├── logos
│ │ ├── react.png
│ │ ├── graphql.png
│ │ └── hasura.png
│ ├── music
│ │ ├── moby.jpg
│ │ ├── sade.jpg
│ │ ├── ashana.jpg
│ │ ├── moby-18.jpg
│ │ ├── moby-play.jpg
│ │ ├── snatam_kaur.jpg
│ │ ├── ashana-beloved.jpg
│ │ ├── naid-varanasi.jpg
│ │ ├── sade-loversrock.jpg
│ │ ├── snatamkaur-beloved.jpg
│ │ └── snatamkaur-sacredchants.jpg
│ ├── wine-bottle.png
│ ├── wine-glass.png
│ ├── wine
│ │ ├── stjean.png
│ │ ├── wineno.png
│ │ ├── yquem.png
│ │ ├── flags
│ │ │ ├── ar.png
│ │ │ ├── at.png
│ │ │ ├── bg.png
│ │ │ ├── ca.png
│ │ │ ├── ch.png
│ │ │ ├── cl.png
│ │ │ ├── cy.png
│ │ │ ├── de.png
│ │ │ ├── es.png
│ │ │ ├── fr.png
│ │ │ ├── gr.png
│ │ │ ├── hu.png
│ │ │ ├── it.png
│ │ │ ├── lu.png
│ │ │ ├── nz.png
│ │ │ ├── pt.png
│ │ │ ├── us.png
│ │ │ └── za.png
│ │ ├── winered.png
│ │ ├── winerose.png
│ │ ├── macrostie.png
│ │ ├── montelena.png
│ │ ├── vinecliff.png
│ │ ├── winespark.png
│ │ ├── winesweet.png
│ │ └── winewhite.png
│ ├── comics
│ │ ├── alim1.jpg
│ │ ├── incal1.jpg
│ │ ├── lama1.jpg
│ │ ├── quete1.jpg
│ │ ├── ronin.jpg
│ │ ├── saga1.jpg
│ │ ├── surfer.jpg
│ │ ├── carmenmc1.jpg
│ │ ├── codemc1.jpg
│ │ ├── flags
│ │ │ ├── fr.png
│ │ │ └── us.png
│ │ ├── garulfo1.jpg
│ │ ├── lanfeust1.jpg
│ │ ├── neffous1.jpg
│ │ ├── salammbo1.jpg
│ │ ├── skydoll1.jpg
│ │ ├── fleaudieux1.jpg
│ │ ├── metabaron1.jpg
│ │ ├── androidssheep.jpg
│ │ ├── imperfect-future.jpg
│ │ └── ghost-in-the-shell.jpg
│ └── screenshots
│ │ ├── many-list.png
│ │ ├── one-edit.png
│ │ ├── many-cards.png
│ │ ├── one-browse.png
│ │ ├── analytics-charts.png
│ │ ├── analytics-stats.png
│ │ ├── comfort-activity.png
│ │ └── comfort-overview.png
├── manifest.json
└── index.html
├── src
├── pages
│ ├── Demos
│ │ ├── Demos.scss
│ │ └── Demos.jsx
│ ├── PageNotFound
│ │ ├── PageNotFound.scss
│ │ └── PageNotFound.jsx
│ ├── Home
│ │ ├── Gallery.scss
│ │ ├── Gallery.jsx
│ │ └── Home.scss
│ └── Docs
│ │ ├── components
│ │ ├── ProjectBadges.jsx
│ │ ├── PrettyJSON.scss
│ │ └── SampleModel.jsx
│ │ ├── Doc.scss
│ │ ├── SampleModels.jsx
│ │ ├── Views.scss
│ │ ├── docConfig.js
│ │ ├── Configuration.jsx
│ │ ├── Views.jsx
│ │ └── Doc.jsx
├── components
│ ├── shell
│ │ ├── SideBar
│ │ │ ├── spirals.png
│ │ │ ├── svg
│ │ │ │ ├── eye.svg
│ │ │ │ ├── book.svg
│ │ │ │ └── cogs.svg
│ │ │ ├── SideBar.test.js
│ │ │ ├── appMenus.js
│ │ │ └── SideBar.jsx
│ │ ├── TopBar
│ │ │ ├── evologo.png
│ │ │ ├── GitHubLink.jsx
│ │ │ ├── TopBar.scss
│ │ │ └── TopBar.jsx
│ │ └── Footer
│ │ │ ├── Footer.test.js
│ │ │ ├── Footer.jsx
│ │ │ └── Footer.scss
│ ├── views
│ │ ├── one
│ │ │ ├── shared
│ │ │ │ ├── Collection
│ │ │ │ │ ├── Collection.scss
│ │ │ │ │ └── Collection.jsx
│ │ │ │ └── Timestamps
│ │ │ │ │ ├── Timestamps.scss
│ │ │ │ │ ├── Timestamps.jsx
│ │ │ │ │ └── Timestamps.test.js
│ │ │ ├── One.scss
│ │ │ └── Card.jsx
│ │ ├── analytics
│ │ │ ├── Charts
│ │ │ │ ├── chartOptions.js
│ │ │ │ ├── ChartTable.scss
│ │ │ │ ├── chartProps.js
│ │ │ │ ├── Pie.jsx
│ │ │ │ ├── Bars.jsx
│ │ │ │ ├── ChartTable.jsx
│ │ │ │ └── Charts.scss
│ │ │ └── Stats
│ │ │ │ ├── PercentBar.jsx
│ │ │ │ ├── PercentBar.scss
│ │ │ │ └── Stats.scss
│ │ ├── many
│ │ │ ├── shared
│ │ │ │ ├── EmptyState
│ │ │ │ │ ├── EmptyState.scss
│ │ │ │ │ ├── EmptyState.test.js
│ │ │ │ │ └── EmptyState.jsx
│ │ │ │ ├── Pagination
│ │ │ │ │ ├── Pagination.test.js
│ │ │ │ │ ├── Pagination.scss
│ │ │ │ │ └── Pagination.jsx
│ │ │ │ └── TableBody
│ │ │ │ │ ├── TableBody.test.js
│ │ │ │ │ └── TableBody.jsx
│ │ │ ├── Many.scss
│ │ │ ├── List
│ │ │ │ ├── List.scss
│ │ │ │ └── List.jsx
│ │ │ └── Cards
│ │ │ │ ├── Cards.jsx
│ │ │ │ └── Cards.scss
│ │ ├── comfort
│ │ │ ├── ModelLinks.scss
│ │ │ ├── Overview
│ │ │ │ ├── SearchBox.scss
│ │ │ │ ├── InvalidRoute.jsx
│ │ │ │ ├── Activity.scss
│ │ │ │ ├── Overview.scss
│ │ │ │ ├── SearchBox.jsx
│ │ │ │ └── Activity.jsx
│ │ │ ├── Activity
│ │ │ │ ├── Activity.scss
│ │ │ │ └── Activity.jsx
│ │ │ └── ModelLinks.jsx
│ │ ├── ViewHeader
│ │ │ ├── ViewHeader.test.js
│ │ │ ├── FilterTags.jsx
│ │ │ ├── ViewHeader.scss
│ │ │ ├── ViewsNavIcons.jsx
│ │ │ └── ViewHeader.jsx
│ │ └── modelPropTypes.js
│ ├── widgets
│ │ ├── Badge
│ │ │ ├── Badge.scss
│ │ │ ├── Badge.jsx
│ │ │ └── Badge.test.js
│ │ ├── Spinner
│ │ │ ├── Spinner.test.js
│ │ │ ├── Spinner.jsx
│ │ │ └── Spinner.scss
│ │ ├── Alert
│ │ │ ├── Alert.jsx
│ │ │ ├── Alert.scss
│ │ │ └── Alert.test.js
│ │ ├── global.scss
│ │ ├── Panel
│ │ │ ├── Panel.scss
│ │ │ ├── Panel.test.js
│ │ │ └── Panel.jsx
│ │ └── Button
│ │ │ ├── Button.jsx
│ │ │ ├── Button.scss
│ │ │ └── Button.test.js
│ ├── Field
│ │ ├── FieldLabel.scss
│ │ ├── edit
│ │ │ ├── FieldUpload.scss
│ │ │ ├── FieldDate.scss
│ │ │ ├── FieldDate.jsx
│ │ │ └── FieldObject.jsx
│ │ ├── FieldLabel.test.js
│ │ ├── Field.test.js
│ │ ├── FieldLabel.jsx
│ │ ├── Field.scss
│ │ ├── browse
│ │ │ ├── FieldValue.jsx
│ │ │ └── FieldElemBrowse.jsx
│ │ └── Field.jsx
│ └── ErrorBoundary.jsx
├── App-custom.scss
├── utils
│ ├── localStorage.js
│ ├── moMa.js
│ ├── url.js
│ ├── dicoViews.js
│ ├── activity.js
│ ├── format.test.js
│ └── dico.js
├── index.js
├── setupTests.js
├── routes
│ ├── DemoRoutes.jsx
│ ├── DocRoutes.jsx
│ └── EvolRoutes.jsx
├── dao
│ └── cache.js
├── index.scss
├── i18n
│ └── i18n.js
├── config.js
├── models
│ ├── all_models.js
│ ├── music
│ │ ├── artist.js
│ │ ├── album.js
│ │ └── track.js
│ └── organizer
│ │ ├── winetasting.js
│ │ └── todo.js
├── variables.scss
├── App.jsx
└── App.scss
├── .gitignore
├── jsconfig.json
├── sql
└── README.md
├── ascii-art.js
├── .eslintrc.json
└── TODO.md
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: evoluteur
4 |
--------------------------------------------------------------------------------
/public/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evoluteur/evolutility-ui-react/HEAD/public/favicon.png
--------------------------------------------------------------------------------
/public/pix/bug.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evoluteur/evolutility-ui-react/HEAD/public/pix/bug.png
--------------------------------------------------------------------------------
/public/pix/cd.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evoluteur/evolutility-ui-react/HEAD/public/pix/cd.png
--------------------------------------------------------------------------------
/public/pix/comics.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evoluteur/evolutility-ui-react/HEAD/public/pix/comics.png
--------------------------------------------------------------------------------
/public/pix/music.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evoluteur/evolutility-ui-react/HEAD/public/pix/music.png
--------------------------------------------------------------------------------
/public/pix/resto.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evoluteur/evolutility-ui-react/HEAD/public/pix/resto.gif
--------------------------------------------------------------------------------
/public/pix/resto.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evoluteur/evolutility-ui-react/HEAD/public/pix/resto.png
--------------------------------------------------------------------------------
/public/pix/star.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evoluteur/evolutility-ui-react/HEAD/public/pix/star.png
--------------------------------------------------------------------------------
/public/pix/todo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evoluteur/evolutility-ui-react/HEAD/public/pix/todo.gif
--------------------------------------------------------------------------------
/public/pix/todo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evoluteur/evolutility-ui-react/HEAD/public/pix/todo.png
--------------------------------------------------------------------------------
/public/pix/user.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evoluteur/evolutility-ui-react/HEAD/public/pix/user.png
--------------------------------------------------------------------------------
/public/pix/wine.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evoluteur/evolutility-ui-react/HEAD/public/pix/wine.gif
--------------------------------------------------------------------------------
/src/pages/Demos/Demos.scss:
--------------------------------------------------------------------------------
1 | .evo-demos {
2 | .evo-models-list {
3 | margin-top: 20px;
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/public/pix/contact.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evoluteur/evolutility-ui-react/HEAD/public/pix/contact.gif
--------------------------------------------------------------------------------
/public/pix/contact.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evoluteur/evolutility-ui-react/HEAD/public/pix/contact.png
--------------------------------------------------------------------------------
/public/pix/doc/cog.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evoluteur/evolutility-ui-react/HEAD/public/pix/doc/cog.png
--------------------------------------------------------------------------------
/public/pix/doc/model.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evoluteur/evolutility-ui-react/HEAD/public/pix/doc/model.png
--------------------------------------------------------------------------------
/public/pix/doc/object.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evoluteur/evolutility-ui-react/HEAD/public/pix/doc/object.png
--------------------------------------------------------------------------------
/public/pix/doc/wrench.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evoluteur/evolutility-ui-react/HEAD/public/pix/doc/wrench.png
--------------------------------------------------------------------------------
/public/pix/logos/react.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evoluteur/evolutility-ui-react/HEAD/public/pix/logos/react.png
--------------------------------------------------------------------------------
/public/pix/music/moby.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evoluteur/evolutility-ui-react/HEAD/public/pix/music/moby.jpg
--------------------------------------------------------------------------------
/public/pix/music/sade.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evoluteur/evolutility-ui-react/HEAD/public/pix/music/sade.jpg
--------------------------------------------------------------------------------
/public/pix/wine-bottle.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evoluteur/evolutility-ui-react/HEAD/public/pix/wine-bottle.png
--------------------------------------------------------------------------------
/public/pix/wine-glass.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evoluteur/evolutility-ui-react/HEAD/public/pix/wine-glass.png
--------------------------------------------------------------------------------
/public/pix/wine/stjean.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evoluteur/evolutility-ui-react/HEAD/public/pix/wine/stjean.png
--------------------------------------------------------------------------------
/public/pix/wine/wineno.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evoluteur/evolutility-ui-react/HEAD/public/pix/wine/wineno.png
--------------------------------------------------------------------------------
/public/pix/wine/yquem.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evoluteur/evolutility-ui-react/HEAD/public/pix/wine/yquem.png
--------------------------------------------------------------------------------
/public/pix/comics/alim1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evoluteur/evolutility-ui-react/HEAD/public/pix/comics/alim1.jpg
--------------------------------------------------------------------------------
/public/pix/comics/incal1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evoluteur/evolutility-ui-react/HEAD/public/pix/comics/incal1.jpg
--------------------------------------------------------------------------------
/public/pix/comics/lama1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evoluteur/evolutility-ui-react/HEAD/public/pix/comics/lama1.jpg
--------------------------------------------------------------------------------
/public/pix/comics/quete1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evoluteur/evolutility-ui-react/HEAD/public/pix/comics/quete1.jpg
--------------------------------------------------------------------------------
/public/pix/comics/ronin.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evoluteur/evolutility-ui-react/HEAD/public/pix/comics/ronin.jpg
--------------------------------------------------------------------------------
/public/pix/comics/saga1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evoluteur/evolutility-ui-react/HEAD/public/pix/comics/saga1.jpg
--------------------------------------------------------------------------------
/public/pix/comics/surfer.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evoluteur/evolutility-ui-react/HEAD/public/pix/comics/surfer.jpg
--------------------------------------------------------------------------------
/public/pix/doc/metadata.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evoluteur/evolutility-ui-react/HEAD/public/pix/doc/metadata.png
--------------------------------------------------------------------------------
/public/pix/doc/tag_pink.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evoluteur/evolutility-ui-react/HEAD/public/pix/doc/tag_pink.png
--------------------------------------------------------------------------------
/public/pix/logos/graphql.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evoluteur/evolutility-ui-react/HEAD/public/pix/logos/graphql.png
--------------------------------------------------------------------------------
/public/pix/logos/hasura.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evoluteur/evolutility-ui-react/HEAD/public/pix/logos/hasura.png
--------------------------------------------------------------------------------
/public/pix/music/ashana.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evoluteur/evolutility-ui-react/HEAD/public/pix/music/ashana.jpg
--------------------------------------------------------------------------------
/public/pix/music/moby-18.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evoluteur/evolutility-ui-react/HEAD/public/pix/music/moby-18.jpg
--------------------------------------------------------------------------------
/public/pix/wine/flags/ar.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evoluteur/evolutility-ui-react/HEAD/public/pix/wine/flags/ar.png
--------------------------------------------------------------------------------
/public/pix/wine/flags/at.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evoluteur/evolutility-ui-react/HEAD/public/pix/wine/flags/at.png
--------------------------------------------------------------------------------
/public/pix/wine/flags/bg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evoluteur/evolutility-ui-react/HEAD/public/pix/wine/flags/bg.png
--------------------------------------------------------------------------------
/public/pix/wine/flags/ca.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evoluteur/evolutility-ui-react/HEAD/public/pix/wine/flags/ca.png
--------------------------------------------------------------------------------
/public/pix/wine/flags/ch.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evoluteur/evolutility-ui-react/HEAD/public/pix/wine/flags/ch.png
--------------------------------------------------------------------------------
/public/pix/wine/flags/cl.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evoluteur/evolutility-ui-react/HEAD/public/pix/wine/flags/cl.png
--------------------------------------------------------------------------------
/public/pix/wine/flags/cy.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evoluteur/evolutility-ui-react/HEAD/public/pix/wine/flags/cy.png
--------------------------------------------------------------------------------
/public/pix/wine/flags/de.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evoluteur/evolutility-ui-react/HEAD/public/pix/wine/flags/de.png
--------------------------------------------------------------------------------
/public/pix/wine/flags/es.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evoluteur/evolutility-ui-react/HEAD/public/pix/wine/flags/es.png
--------------------------------------------------------------------------------
/public/pix/wine/flags/fr.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evoluteur/evolutility-ui-react/HEAD/public/pix/wine/flags/fr.png
--------------------------------------------------------------------------------
/public/pix/wine/flags/gr.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evoluteur/evolutility-ui-react/HEAD/public/pix/wine/flags/gr.png
--------------------------------------------------------------------------------
/public/pix/wine/flags/hu.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evoluteur/evolutility-ui-react/HEAD/public/pix/wine/flags/hu.png
--------------------------------------------------------------------------------
/public/pix/wine/flags/it.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evoluteur/evolutility-ui-react/HEAD/public/pix/wine/flags/it.png
--------------------------------------------------------------------------------
/public/pix/wine/flags/lu.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evoluteur/evolutility-ui-react/HEAD/public/pix/wine/flags/lu.png
--------------------------------------------------------------------------------
/public/pix/wine/flags/nz.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evoluteur/evolutility-ui-react/HEAD/public/pix/wine/flags/nz.png
--------------------------------------------------------------------------------
/public/pix/wine/flags/pt.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evoluteur/evolutility-ui-react/HEAD/public/pix/wine/flags/pt.png
--------------------------------------------------------------------------------
/public/pix/wine/flags/us.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evoluteur/evolutility-ui-react/HEAD/public/pix/wine/flags/us.png
--------------------------------------------------------------------------------
/public/pix/wine/flags/za.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evoluteur/evolutility-ui-react/HEAD/public/pix/wine/flags/za.png
--------------------------------------------------------------------------------
/public/pix/wine/winered.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evoluteur/evolutility-ui-react/HEAD/public/pix/wine/winered.png
--------------------------------------------------------------------------------
/public/pix/wine/winerose.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evoluteur/evolutility-ui-react/HEAD/public/pix/wine/winerose.png
--------------------------------------------------------------------------------
/public/pix/comics/carmenmc1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evoluteur/evolutility-ui-react/HEAD/public/pix/comics/carmenmc1.jpg
--------------------------------------------------------------------------------
/public/pix/comics/codemc1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evoluteur/evolutility-ui-react/HEAD/public/pix/comics/codemc1.jpg
--------------------------------------------------------------------------------
/public/pix/comics/flags/fr.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evoluteur/evolutility-ui-react/HEAD/public/pix/comics/flags/fr.png
--------------------------------------------------------------------------------
/public/pix/comics/flags/us.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evoluteur/evolutility-ui-react/HEAD/public/pix/comics/flags/us.png
--------------------------------------------------------------------------------
/public/pix/comics/garulfo1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evoluteur/evolutility-ui-react/HEAD/public/pix/comics/garulfo1.jpg
--------------------------------------------------------------------------------
/public/pix/comics/lanfeust1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evoluteur/evolutility-ui-react/HEAD/public/pix/comics/lanfeust1.jpg
--------------------------------------------------------------------------------
/public/pix/comics/neffous1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evoluteur/evolutility-ui-react/HEAD/public/pix/comics/neffous1.jpg
--------------------------------------------------------------------------------
/public/pix/comics/salammbo1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evoluteur/evolutility-ui-react/HEAD/public/pix/comics/salammbo1.jpg
--------------------------------------------------------------------------------
/public/pix/comics/skydoll1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evoluteur/evolutility-ui-react/HEAD/public/pix/comics/skydoll1.jpg
--------------------------------------------------------------------------------
/public/pix/music/moby-play.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evoluteur/evolutility-ui-react/HEAD/public/pix/music/moby-play.jpg
--------------------------------------------------------------------------------
/public/pix/wine/macrostie.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evoluteur/evolutility-ui-react/HEAD/public/pix/wine/macrostie.png
--------------------------------------------------------------------------------
/public/pix/wine/montelena.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evoluteur/evolutility-ui-react/HEAD/public/pix/wine/montelena.png
--------------------------------------------------------------------------------
/public/pix/wine/vinecliff.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evoluteur/evolutility-ui-react/HEAD/public/pix/wine/vinecliff.png
--------------------------------------------------------------------------------
/public/pix/wine/winespark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evoluteur/evolutility-ui-react/HEAD/public/pix/wine/winespark.png
--------------------------------------------------------------------------------
/public/pix/wine/winesweet.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evoluteur/evolutility-ui-react/HEAD/public/pix/wine/winesweet.png
--------------------------------------------------------------------------------
/public/pix/wine/winewhite.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evoluteur/evolutility-ui-react/HEAD/public/pix/wine/winewhite.png
--------------------------------------------------------------------------------
/public/pix/comics/fleaudieux1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evoluteur/evolutility-ui-react/HEAD/public/pix/comics/fleaudieux1.jpg
--------------------------------------------------------------------------------
/public/pix/comics/metabaron1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evoluteur/evolutility-ui-react/HEAD/public/pix/comics/metabaron1.jpg
--------------------------------------------------------------------------------
/public/pix/music/snatam_kaur.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evoluteur/evolutility-ui-react/HEAD/public/pix/music/snatam_kaur.jpg
--------------------------------------------------------------------------------
/public/pix/comics/androidssheep.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evoluteur/evolutility-ui-react/HEAD/public/pix/comics/androidssheep.jpg
--------------------------------------------------------------------------------
/public/pix/music/ashana-beloved.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evoluteur/evolutility-ui-react/HEAD/public/pix/music/ashana-beloved.jpg
--------------------------------------------------------------------------------
/public/pix/music/naid-varanasi.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evoluteur/evolutility-ui-react/HEAD/public/pix/music/naid-varanasi.jpg
--------------------------------------------------------------------------------
/public/pix/music/sade-loversrock.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evoluteur/evolutility-ui-react/HEAD/public/pix/music/sade-loversrock.jpg
--------------------------------------------------------------------------------
/public/pix/screenshots/many-list.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evoluteur/evolutility-ui-react/HEAD/public/pix/screenshots/many-list.png
--------------------------------------------------------------------------------
/public/pix/screenshots/one-edit.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evoluteur/evolutility-ui-react/HEAD/public/pix/screenshots/one-edit.png
--------------------------------------------------------------------------------
/public/pix/comics/imperfect-future.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evoluteur/evolutility-ui-react/HEAD/public/pix/comics/imperfect-future.jpg
--------------------------------------------------------------------------------
/public/pix/screenshots/many-cards.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evoluteur/evolutility-ui-react/HEAD/public/pix/screenshots/many-cards.png
--------------------------------------------------------------------------------
/public/pix/screenshots/one-browse.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evoluteur/evolutility-ui-react/HEAD/public/pix/screenshots/one-browse.png
--------------------------------------------------------------------------------
/public/pix/comics/ghost-in-the-shell.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evoluteur/evolutility-ui-react/HEAD/public/pix/comics/ghost-in-the-shell.jpg
--------------------------------------------------------------------------------
/public/pix/music/snatamkaur-beloved.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evoluteur/evolutility-ui-react/HEAD/public/pix/music/snatamkaur-beloved.jpg
--------------------------------------------------------------------------------
/src/components/shell/SideBar/spirals.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evoluteur/evolutility-ui-react/HEAD/src/components/shell/SideBar/spirals.png
--------------------------------------------------------------------------------
/src/components/shell/TopBar/evologo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evoluteur/evolutility-ui-react/HEAD/src/components/shell/TopBar/evologo.png
--------------------------------------------------------------------------------
/public/pix/screenshots/analytics-charts.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evoluteur/evolutility-ui-react/HEAD/public/pix/screenshots/analytics-charts.png
--------------------------------------------------------------------------------
/public/pix/screenshots/analytics-stats.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evoluteur/evolutility-ui-react/HEAD/public/pix/screenshots/analytics-stats.png
--------------------------------------------------------------------------------
/public/pix/screenshots/comfort-activity.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evoluteur/evolutility-ui-react/HEAD/public/pix/screenshots/comfort-activity.png
--------------------------------------------------------------------------------
/public/pix/screenshots/comfort-overview.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evoluteur/evolutility-ui-react/HEAD/public/pix/screenshots/comfort-overview.png
--------------------------------------------------------------------------------
/public/pix/music/snatamkaur-sacredchants.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/evoluteur/evolutility-ui-react/HEAD/public/pix/music/snatamkaur-sacredchants.jpg
--------------------------------------------------------------------------------
/src/components/views/one/shared/Collection/Collection.scss:
--------------------------------------------------------------------------------
1 | @import "variables";
2 |
3 | .evo-collec {
4 | padding: 0 10px 6px 10px;
5 | > .empty-collec {
6 | margin: 10px 10px 20px;
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 |
3 | build
4 | coverage
5 |
6 | public/bundle.js
7 |
8 | *.zip
9 |
10 | *-nogit/
11 | *-nogit.*
12 | *copy.*
13 |
14 | Thumbs.db
15 | Desktop.ini
16 | .DS_Store
17 | .idea
18 |
--------------------------------------------------------------------------------
/src/App-custom.scss:
--------------------------------------------------------------------------------
1 | // CSS exceptions for specific models thanks to ".model_X" selector
2 |
3 | .evol-many {
4 | &.model_contact,
5 | &.model_winetasting {
6 | td:first-child {
7 | min-width: 130px;
8 | }
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/jsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "ES6",
4 | "baseUrl": "./src",
5 | "target": "es6",
6 | "jsx": "react",
7 | "moduleResolution": "node"
8 | },
9 | "include": ["src"],
10 | "exclude": ["node_modules", "dist", "coverage"]
11 | }
12 |
--------------------------------------------------------------------------------
/src/components/views/analytics/Charts/chartOptions.js:
--------------------------------------------------------------------------------
1 | export const colors = { scheme: "category10" };
2 |
3 | export const labelColor = "#333333";
4 | export const innerLabelColor = "white";
5 |
6 | const chartOptions = {
7 | colors,
8 | };
9 |
10 | export default chartOptions;
11 |
--------------------------------------------------------------------------------
/src/components/views/many/shared/EmptyState/EmptyState.scss:
--------------------------------------------------------------------------------
1 | @import "variables";
2 |
3 | .empty-state {
4 | > a > span {
5 | position: relative;
6 | top: -8px;
7 | }
8 | .btn {
9 | margin-top: 20px;
10 | }
11 | .too-much {
12 | margin-top: $gap;
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/components/views/comfort/ModelLinks.scss:
--------------------------------------------------------------------------------
1 | @import "variables";
2 |
3 | .evo-models-list {
4 | padding-left: 20px;
5 | > div {
6 | display: inline-block;
7 | width: 200px;
8 | white-space: nowrap;
9 | overflow-x: hidden;
10 | text-overflow: ellipsis;
11 | margin: 2px 10px 5px;
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/pages/PageNotFound/PageNotFound.scss:
--------------------------------------------------------------------------------
1 | @import "variables";
2 |
3 | .err404 {
4 | position: relative;
5 | }
6 |
7 | .center404 {
8 | position: relative;
9 | margin: 40px;
10 | text-align: center;
11 | }
12 | .center404 > p {
13 | margin: 10px;
14 | }
15 | .bad-route {
16 | color: $color-text-secondary;
17 | }
18 |
--------------------------------------------------------------------------------
/src/components/views/analytics/Stats/PercentBar.jsx:
--------------------------------------------------------------------------------
1 | import React, { memo } from "react";
2 |
3 | import "./PercentBar.scss";
4 |
5 | const PercentBar = memo(({ percent }) => (
6 |
9 | ));
10 |
11 | export default PercentBar;
12 |
--------------------------------------------------------------------------------
/src/pages/Home/Gallery.scss:
--------------------------------------------------------------------------------
1 | @import "variables";
2 |
3 | .alice-carousel {
4 | margin: 30px 0;
5 | .item {
6 | max-height: 240px;
7 | width: 250px;
8 | padding: 10px;
9 | > div {
10 | max-height: 239px;
11 | margin-top: 5px;
12 | border: $border;
13 | overflow-y: hidden;
14 | }
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/utils/localStorage.js:
--------------------------------------------------------------------------------
1 | // Wrapper for localStorage
2 | const prefix = "evol-";
3 |
4 | export const lcWrite = (key, value) =>
5 | localStorage.setItem(prefix + key, value);
6 |
7 | export const lcRead = (key) => localStorage.getItem(prefix + key) || null;
8 |
9 | export const lcRemove = (key) => localStorage.removeItem(prefix + key);
10 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { createRoot } from "react-dom/client";
3 | import App from "./App";
4 |
5 | import "./index.scss";
6 |
7 | const container = document.getElementById("root");
8 | const root = createRoot(container);
9 | root.render(
10 |
11 |
12 |
13 | );
14 |
--------------------------------------------------------------------------------
/src/components/views/many/Many.scss:
--------------------------------------------------------------------------------
1 | @import "variables";
2 |
3 | .evol-many {
4 | .evol-loading {
5 | margin: 10% auto 20% auto;
6 | }
7 | }
8 | .e-icon {
9 | position: relative;
10 | top: -2px;
11 | margin-right: 6px !important;
12 | height: $icon-size;
13 | width: $icon-size;
14 | }
15 | .lov-icon {
16 | margin-right: 5px;
17 | }
18 |
--------------------------------------------------------------------------------
/sql/README.md:
--------------------------------------------------------------------------------
1 | # Evolutility-UI-React demo database
2 |
3 | This folder contains the SQL scripts to setup the demo database on Postgres.
4 |
5 | Run evol-db-schema.sql to create the database structure, then run evol-db-data.sql to populated it w/ sample data.
6 |
7 | This SQL scripts were generated using Evolutility-Models (https://github.com/evoluteur/evolutility-models).
--------------------------------------------------------------------------------
/src/components/views/one/shared/Timestamps/Timestamps.scss:
--------------------------------------------------------------------------------
1 | @import "variables";
2 |
3 | .timestamps {
4 | display: block;
5 | width: 100%;
6 | margin: 20px 0 0 10px;
7 | color: $color-label;
8 | font-size: $label-font-size;
9 | > div {
10 | > label {
11 | margin-right: 5px;
12 | &:after {
13 | content: ":";
14 | }
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/components/views/analytics/Stats/PercentBar.scss:
--------------------------------------------------------------------------------
1 | @import "variables";
2 |
3 | .pc-bar {
4 | position: relative;
5 | height: 7px;
6 | width: 100%;
7 | background-color: rgb(30, 142, 62);
8 | margin-bottom: 10px;
9 | .pcb-red {
10 | position: absolute;
11 | top: 0;
12 | right: 0;
13 | height: 7px;
14 | background-color: rgb(217, 48, 37);
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "Evolutility-UI-React",
3 | "name": "Evolutility-UI-React",
4 | "icons": [
5 | {
6 | "src": "favicon.png",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | }
10 | ],
11 | "start_url": "./index.html",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/src/setupTests.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable import/no-extraneous-dependencies */
2 |
3 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
4 | // allows you to do things like:
5 | // expect(element).toHaveTextContent(/react/i)
6 | // learn more: https://github.com/testing-library/jest-dom
7 | import "@testing-library/jest-dom";
8 |
9 | global.structuredClone = (v) => JSON.parse(JSON.stringify(v));
10 |
--------------------------------------------------------------------------------
/src/routes/DemoRoutes.jsx:
--------------------------------------------------------------------------------
1 | // Routes for Evolutility views
2 | import React from "react";
3 | import { Routes, Route } from "react-router-dom";
4 |
5 | import Demos from "pages/Demos/Demos";
6 | import EvolRoutes from "./EvolRoutes";
7 |
8 | const DocsRoutes = () => (
9 |
10 | } />
11 | } />
12 |
13 | );
14 |
15 | export default DocsRoutes;
16 |
--------------------------------------------------------------------------------
/src/components/widgets/Badge/Badge.scss:
--------------------------------------------------------------------------------
1 | @import "variables";
2 |
3 | .evo-badge {
4 | display: inline-block;
5 | min-width: 28px;
6 | padding: 5px 10px;
7 | border-radius: 14px;
8 | font-size: 14px;
9 | font-weight: 400;
10 | margin: 0 5px;
11 | text-align: center;
12 | border-style: solid;
13 | border-width: 1px;
14 | }
15 |
16 | .alert-default {
17 | color: $color-almostblack;
18 | background-color: $bgcolor-badge;
19 | border-color: silver;
20 | }
21 |
--------------------------------------------------------------------------------
/src/components/views/comfort/Overview/SearchBox.scss:
--------------------------------------------------------------------------------
1 | @import "variables";
2 |
3 | .evo-search-box {
4 | > div {
5 | display: flex;
6 | width: 140px;
7 | > button {
8 | position: relative;
9 | height: 38px;
10 | width: 40px;
11 | border-top-left-radius: 0;
12 | border-bottom-left-radius: 0;
13 | padding: 0 5px;
14 | border-left: none;
15 | > svg {
16 | height: 24px;
17 | width: 24px;
18 | }
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/components/shell/SideBar/svg/eye.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/components/shell/SideBar/SideBar.test.js:
--------------------------------------------------------------------------------
1 | import { render, screen } from "@testing-library/react";
2 | import { MemoryRouter as Router } from "react-router-dom";
3 | import SideBar from "./SideBar";
4 |
5 | describe("SideBar widget tests", () => {
6 | it("SideBar shows doc and demo", async () => {
7 | render(
8 |
9 |
10 |
11 | );
12 | const sidebar = screen.getByTestId("sidebar");
13 | expect(sidebar).toHaveTextContent("Documentation");
14 | expect(sidebar).toHaveTextContent("Demos");
15 | });
16 | });
17 |
--------------------------------------------------------------------------------
/src/components/views/comfort/Overview/InvalidRoute.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { i18n_errors } from "i18n/i18n";
3 | import ModelLinks from "components/views/comfort/ModelLinks";
4 | import Alert from "components/widgets/Alert/Alert";
5 |
6 | const InvalidRoute = ({ entity }) => {
7 | const msg = i18n_errors.badEntity.replace("{0}", entity);
8 | return (
9 |
14 | );
15 | };
16 |
17 | export default InvalidRoute;
18 |
--------------------------------------------------------------------------------
/src/components/shell/Footer/Footer.test.js:
--------------------------------------------------------------------------------
1 | import { render, screen } from "@testing-library/react";
2 | import packageInfo from "package.json";
3 | import Footer from "./Footer";
4 |
5 | const { version } = packageInfo;
6 |
7 | describe("footer widget tests", () => {
8 | it("footer shows version and author", async () => {
9 | render();
10 | const footer = screen.getByTestId("footer");
11 | expect(footer).toHaveTextContent("Olivier Giulieri");
12 | expect(footer).toHaveTextContent("Evolutility-UI-React");
13 | expect(footer).toHaveTextContent(version);
14 | });
15 | });
16 |
--------------------------------------------------------------------------------
/src/components/views/comfort/Activity/Activity.scss:
--------------------------------------------------------------------------------
1 | @import "variables";
2 |
3 | .evol-activity {
4 | > section > div {
5 | display: flex;
6 | > a {
7 | min-width: 120px;
8 | width: 220px;
9 | white-space: nowrap;
10 | overflow-x: hidden;
11 | text-overflow: ellipsis;
12 | }
13 | > div {
14 | display: inline-block;
15 | margin-left: 20px;
16 | color: silver;
17 | &.visits {
18 | color: $color-text-secondary;
19 | min-width: 70px;
20 | }
21 | }
22 | }
23 | button {
24 | margin: 10px 20px;
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/components/views/analytics/Charts/ChartTable.scss:
--------------------------------------------------------------------------------
1 | @import "variables";
2 |
3 | .chartTable {
4 | padding: 0 10px;
5 | height: 100%;
6 | overflow-y: scroll;
7 | .align-right {
8 | text-align: right;
9 | }
10 | .table {
11 | text-align: left;
12 | th {
13 | cursor: pointer;
14 | &:nth-child(2) {
15 | width: 90px;
16 | }
17 | }
18 | }
19 | .footer,
20 | .footer:hover {
21 | background-color: $bgcolor-header;
22 | }
23 | }
24 | .chart-card.size-tiny .chartTable {
25 | font-size: 0.8em;
26 | padding: 5px 8px;
27 | height: 230px;
28 | overflow-y: scroll;
29 | }
30 |
--------------------------------------------------------------------------------
/src/components/views/analytics/Charts/chartProps.js:
--------------------------------------------------------------------------------
1 | import PropTypes from "prop-types";
2 |
3 | export const chartTypes = ["bars", "pie", "table"];
4 |
5 | export const chartSizes = ["tiny", "small", "large"];
6 |
7 | export const chartDataPropType = PropTypes.arrayOf(
8 | PropTypes.shape({
9 | id: PropTypes.number.isRequired,
10 | label: PropTypes.string.isRequired,
11 | value: PropTypes.number.isRequired,
12 | })
13 | );
14 |
15 | const chartPropTypes = {
16 | data: chartDataPropType,
17 | size: PropTypes.oneOf(chartSizes),
18 | showLegend: PropTypes.bool,
19 | };
20 |
21 | export default chartPropTypes;
22 |
--------------------------------------------------------------------------------
/src/components/widgets/Spinner/Spinner.test.js:
--------------------------------------------------------------------------------
1 | import { render, screen } from "@testing-library/react";
2 | import Spinner from "./Spinner";
3 | import { i18n_nav } from "i18n/i18n";
4 |
5 | describe("Spinner widget tests", () => {
6 | it("check default text", () => {
7 | render( );
8 | const spinner = screen.getByTestId("spinner");
9 | expect(spinner).toHaveTextContent(i18n_nav.loading);
10 | });
11 | it("check children", () => {
12 | render( );
13 | const spinner = screen.getByTestId("spinner");
14 | expect(spinner).toHaveTextContent("I am Spinner");
15 | });
16 | });
17 |
--------------------------------------------------------------------------------
/src/pages/Docs/components/ProjectBadges.jsx:
--------------------------------------------------------------------------------
1 | const Badges = () => (
2 |
16 | );
17 |
18 | export default Badges;
19 |
--------------------------------------------------------------------------------
/src/components/views/comfort/ModelLinks.jsx:
--------------------------------------------------------------------------------
1 | import React, { memo } from "react";
2 | import { Link } from "react-router-dom";
3 | import { pixPath } from "utils/format";
4 | import { modelsArray } from "utils/moMa";
5 |
6 | import "./ModelLinks.scss";
7 |
8 | const ModelLinks = memo(() => (
9 |
10 | {modelsArray?.map((m) => (
11 |
12 |
13 |
14 | {m.title}
15 |
16 |
17 | ))}
18 |
19 | ));
20 |
21 | export default ModelLinks;
22 |
--------------------------------------------------------------------------------
/src/components/views/comfort/Overview/Activity.scss:
--------------------------------------------------------------------------------
1 | @import "variables";
2 |
3 | .ovw-hist {
4 | position: relative;
5 | padding: 0 20px 20px;
6 | h4 {
7 | position: relative;
8 | left: -10px;
9 | top: -5px;
10 | font-weight: bold;
11 | margin-bottom: 20px;
12 | > a > i {
13 | position: relative;
14 | top: 5px;
15 | }
16 | }
17 | > span {
18 | display: block;
19 | margin-bottom: 10px;
20 | }
21 | }
22 | .ovw-hist-list > a {
23 | display: inline-block;
24 | min-width: 140px;
25 | width: 40%;
26 | margin-right: 20px;
27 | @include ellipsis();
28 | > img {
29 | margin-right: 6px;
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/utils/moMa.js:
--------------------------------------------------------------------------------
1 | // Evolutility-UI-React :: utils/moMa.js
2 |
3 | // Models manager: fetch and cache models
4 | // models can be stored in JSON files or in the database
5 |
6 | // https://github.com/evoluteur/evolutility-ui-react
7 | // (c) 2023 Olivier Giulieri
8 |
9 | import all_models from "models/all_models";
10 | import prepModels from "./moMaPrep";
11 |
12 | prepModels(all_models);
13 |
14 | export const modelsArray = Object.values(all_models);
15 |
16 | export const models = all_models;
17 |
18 | export const getModel = (mId) => all_models[mId] || null;
19 |
20 | const moma = {
21 | getModel,
22 | models,
23 | modelsArray,
24 | };
25 |
26 | export default moma;
27 |
--------------------------------------------------------------------------------
/src/components/Field/FieldLabel.scss:
--------------------------------------------------------------------------------
1 | @import "variables";
2 |
3 | .evol-field-label {
4 | > label {
5 | color: $color-label;
6 | &::after {
7 | content: ":";
8 | }
9 | svg {
10 | cursor: pointer;
11 | margin: 0 5px;
12 | height: 14px;
13 | width: 14px;
14 | margin-right: 0;
15 | fill: $color-help-icon;
16 | transition: fill $ease-time ease $delay-time;
17 | }
18 | &:hover {
19 | svg {
20 | fill: $color-icon-hover;
21 | }
22 | }
23 | }
24 | .crud-icon > svg {
25 | fill: $color-label;
26 | }
27 | }
28 |
29 | .field-required {
30 | margin-left: 3px;
31 | font-weight: bold;
32 | color: $color-red;
33 | }
34 |
--------------------------------------------------------------------------------
/src/components/shell/SideBar/appMenus.js:
--------------------------------------------------------------------------------
1 | import { modelsArray } from "utils/moMa";
2 |
3 | export const docMenus = [
4 | { id: "install", text: "Installation", icon: "doc/cog.png" },
5 | { id: "config", text: "Configuration", icon: "doc/wrench.png" },
6 | { id: "views", text: "Views", icon: "doc/object.png" },
7 | {
8 | id: "metamodel",
9 | text: "Metamodel",
10 | icon: "doc/tag_pink.png",
11 | },
12 | {
13 | id: "models",
14 | text: "Sample Models",
15 | icon: "doc/model.png",
16 | },
17 | ];
18 |
19 | export const demosMenu = modelsArray.map(
20 | ({ id, title: text, icon, defaultViewMany = "list" }) => ({
21 | id,
22 | text,
23 | icon,
24 | defaultViewMany,
25 | })
26 | );
27 |
--------------------------------------------------------------------------------
/src/components/widgets/Badge/Badge.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 |
4 | import "./Badge.scss";
5 | import "components/widgets/Alert/Alert.scss";
6 |
7 | const Badge = ({ text, type }) => (
8 |
9 | {text}
10 |
11 | );
12 |
13 | export default Badge;
14 |
15 | Badge.propTypes = {
16 | text: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
17 | type: PropTypes.oneOf([
18 | "default", // - light grey
19 | "info", // - blue
20 | "success", // - green
21 | "warning", // - yellow
22 | "danger", // - red
23 | ]),
24 | };
25 |
26 | Badge.defaultProps = {
27 | type: "default",
28 | };
29 |
--------------------------------------------------------------------------------
/ascii-art.js:
--------------------------------------------------------------------------------
1 | var pkg = require("./package.json");
2 |
3 | const splash = `
4 | ______ _ _ _ _ _ _
5 | | ____| | | | | (_) (_) |
6 | | |____ _____ | |_ _| |_ _| |_| |_ _ _
7 | | __\\ \\ / / _ \\| | | | | __| | | | __| | | |
8 | | |___\\ V / (_) | | |_| | |_| | | | |_| |_| |
9 | |______\\_/ \\___/|_|\\__,_|\\__|_|_|_|\\__|\\__, |
10 | _ _ _____ _____ __/ |
11 | | | | |_ _| | __ \\ |___/ |
12 | | | | | | |______| |__) |___ __ _ ___| |_
13 | | | | | | |______| _ // _ \\/ _\` |/ __| __|
14 | | |__| |_| |_ | | \\ \\ __/ (_| | (__| |_
15 | \\____/|_____| |_| \\_\\___|\\__,_|\\___|\\__|
16 | v.${pkg.version}
17 | `;
18 |
19 | console.log(splash);
20 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["react-app", "react-app/jest", "prettier"],
3 | "plugins": ["prettier"],
4 | "parser":"@babel/eslint-parser",
5 | "settings": {
6 | "import/resolver": {
7 | "node": {
8 | "paths": ["src"]
9 | }
10 | }
11 | },
12 | "parserOptions": {
13 | "babelOptions": {
14 | "presets": [
15 | ["babel-preset-react-app", false],
16 | "babel-preset-react-app/prod"
17 | ]
18 | }
19 | },
20 | "rules": {
21 | "react/jsx-props-no-spreading": "off",
22 | "camelcase": 0,
23 | "react/jsx-filename-extension": [1, { "extensions": [".js", ".jsx"] }],
24 | "import/no-extraneous-dependencies": ["error", { "devDependencies": true }],
25 | "react/no-unescaped-entities": "off"
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/components/widgets/Spinner/Spinner.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 | import { i18n_nav } from "i18n/i18n";
4 |
5 | import "./Spinner.scss";
6 |
7 | // Credits: HTML & CSS from http://tobiasahlin.com/spinkit/
8 |
9 | const Spinner = ({ message }) => (
10 |
18 | );
19 |
20 | export default Spinner;
21 |
22 | Spinner.propTypes = {
23 | message: PropTypes.string,
24 | };
25 |
26 | Spinner.defaultProps = {
27 | message: i18n_nav.loading,
28 | };
29 |
--------------------------------------------------------------------------------
/src/pages/Docs/Doc.scss:
--------------------------------------------------------------------------------
1 | .evo-doc {
2 | position: relative;
3 | .lic-npm {
4 | margin-bottom: 10px;
5 | .anchor {
6 | display: block;
7 | height: 40px;
8 | }
9 | }
10 | .cols-2 > div {
11 | text-align: right;
12 | }
13 | }
14 | .anchor {
15 | position: relative;
16 | top: -70px;
17 | }
18 | .code {
19 | margin: 20px 0;
20 | padding: 10px;
21 | font-size: 14px;
22 | line-height: 20px;
23 | font-family: monospace, monospace;
24 | background-color: #f5f5f5;
25 | border: solid 1px #b0bec5;
26 | border-radius: 4px;
27 | }
28 | .samples {
29 | > label {
30 | margin-right: 5px;
31 | }
32 | .pretty-json {
33 | margin-top: 10px;
34 | }
35 | }
36 | .evo-doc-setup {
37 | ol,
38 | ul {
39 | line-height: 32px;
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/components/shell/SideBar/svg/book.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/pages/Docs/SampleModels.jsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable jsx-a11y/anchor-is-valid */
2 | /* eslint-disable jsx-a11y/anchor-has-content */
3 | /*
4 | Evolutility-UI-React
5 | https://github.com/evoluteur/evolutility-ui-react
6 | (c) 2023 Olivier Giulieri
7 | */
8 |
9 | import React, { useEffect } from "react";
10 | import SampleModel from "./components/SampleModel";
11 |
12 | import "./Doc.scss";
13 |
14 | const SampleModels = () => {
15 | useEffect(() => {
16 | document.title = "Doc > Sample Models";
17 | window.scrollTo(0, 0);
18 | }, []);
19 |
20 | return (
21 |
22 |
Sample Models
23 |
24 |
25 | Here are the models behind the demos on this site.
26 |
27 |
28 |
29 |
30 | );
31 | };
32 |
33 | export default SampleModels;
34 |
--------------------------------------------------------------------------------
/src/pages/PageNotFound/PageNotFound.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from "react";
2 | import { i18n_404 as i18n } from "i18n/i18n";
3 | import Button from "components/widgets/Button/Button";
4 |
5 | import "./PageNotFound.scss";
6 |
7 | const PageNotFound = () => {
8 | const url = window.location.pathname;
9 |
10 | useEffect(() => {
11 | document.title = "Evolutility";
12 | window.scrollTo(0, 0);
13 | });
14 |
15 | return (
16 |
17 |
{i18n.title}
18 |
19 |
20 |
21 | {i18n.msg}
22 |
23 |
24 | {i18n.badRoute}: "{url}"
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 | );
33 | };
34 |
35 | export default PageNotFound;
36 |
--------------------------------------------------------------------------------
/src/routes/DocRoutes.jsx:
--------------------------------------------------------------------------------
1 | // Routes for Evolutility views
2 | import React from "react";
3 | import { Routes, Route } from "react-router-dom";
4 |
5 | import Doc from "pages/Docs/Doc";
6 | import Metamodel from "pages/Docs/Metamodel";
7 | import Views from "pages/Docs/Views";
8 | import Installation from "pages/Docs/Installation";
9 | import Configuration from "pages/Docs/Configuration";
10 | import SampleModels from "pages/Docs/SampleModels";
11 |
12 | const DocRoutes = () => (
13 |
14 | } />
15 | } />
16 | } />
17 | } />
18 | } />
19 | } />
20 |
21 | );
22 |
23 | export default DocRoutes;
24 |
--------------------------------------------------------------------------------
/src/dao/cache.js:
--------------------------------------------------------------------------------
1 | import config from "config";
2 |
3 | const { useCache, cacheDuration } = config;
4 |
5 | let cacheObj = {};
6 |
7 | export const setCache = (key, value, seconds = cacheDuration) => {
8 | if (useCache) {
9 | cacheObj[key] = value;
10 | setTimeout(() => {
11 | delCache(key);
12 | }, seconds * 1000);
13 | }
14 | };
15 |
16 | export const getCache = (key) => cacheObj[key];
17 |
18 | export const delCache = (key) => delete cacheObj[key];
19 |
20 | export const clearCache = (prefix) => {
21 | if (prefix) {
22 | cacheKeys().forEach((k) => {
23 | if (k.startsWith(prefix)) {
24 | delCache(k);
25 | }
26 | });
27 | } else {
28 | cacheObj = {};
29 | }
30 | };
31 |
32 | export const cacheKeys = () => Object.keys(cacheObj);
33 |
34 | const cache = {
35 | setCache,
36 | getCache,
37 | delCache,
38 | clearCache,
39 | cacheKeys,
40 | };
41 |
42 | export default cache;
43 |
--------------------------------------------------------------------------------
/src/components/Field/edit/FieldUpload.scss:
--------------------------------------------------------------------------------
1 | @import "variables";
2 |
3 | .evol-fld {
4 | .filename {
5 | margin: 10px 0;
6 | > .file-name {
7 | margin-right: $gap;
8 | }
9 | > .file-size {
10 | color: $color-text-secondary;
11 | white-space: nowrap;
12 | }
13 | }
14 | }
15 |
16 | .doc-drop {
17 | margin-top: 7px;
18 | width: 100%;
19 | min-height: 100px;
20 | border: 2px dashed $color-border;
21 | border-radius: 5px;
22 | padding: 10px;
23 | cursor: pointer;
24 | &:hover {
25 | border-color: grey;
26 | }
27 | &.dropzone--isActive {
28 | border-color: $color-blue1;
29 | background-color: #fcf4dc;
30 | }
31 | > p {
32 | font-style: italic;
33 | font-size: 0.9em;
34 | }
35 | }
36 |
37 | .btn-remove {
38 | margin: 5px 5px 0 $gap;
39 | svg {
40 | position: relative;
41 | top: 2px;
42 | height: $icon-size;
43 | margin-right: 5px;
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/index.scss:
--------------------------------------------------------------------------------
1 | @import "variables";
2 |
3 | html {
4 | height: 100%; // IE
5 | font-size: 14px; // datepicker
6 | }
7 | body {
8 | margin: 0;
9 | padding: 0;
10 | width: 100%; // IE
11 | height: 100%; // IE
12 | font-size: $text-font-size;
13 | font-family: "Open Sans", sans-serif;
14 | color: $color-almostblack;
15 | }
16 | p {
17 | font-size: $text-font-size;
18 | }
19 | h1 {
20 | margin-bottom: 30px;
21 | }
22 | h2,
23 | h3 {
24 | margin-bottom: 20px;
25 | }
26 | h3 {
27 | margin-bottom: 16px;
28 | }
29 |
30 | /* TODO: for now here */
31 |
32 | .black {
33 | color: $color-almostblack !important;
34 | }
35 | .grey {
36 | color: silver;
37 | }
38 |
39 | .extlink:after {
40 | content: " "
41 | url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAVklEQVR4Xn3PgQkAMQhDUXfqTu7kTtkpd5RA8AInfArtQ2iRXFWT2QedAfttj2FsPIOE1eCOlEuoWWjgzYaB/IkeGOrxXhqB+uA9Bfcm0lAZuh+YIeAD+cAqSz4kCMUAAAAASUVORK5CYII=);
42 | }
43 |
--------------------------------------------------------------------------------
/src/components/views/one/One.scss:
--------------------------------------------------------------------------------
1 | @import "variables";
2 |
3 | /* --- panels --- */
4 | .evol-pnls {
5 | @include flex-holder();
6 | margin-left: -10px;
7 |
8 | .panel {
9 | height: calc(100% - 20px);
10 | margin-bottom: 10px;
11 | }
12 | .evol-pnl {
13 | @include flex-item();
14 | padding-left: 10px !important;
15 | min-width: 150px;
16 | margin-bottom: -10px;
17 | fieldset {
18 | padding: 0;
19 | margin: 0;
20 | border: 0;
21 | }
22 | .table {
23 | img {
24 | max-height: 60px;
25 | }
26 | .evol-money {
27 | //TODO: better fix
28 | width: 200px;
29 | }
30 | }
31 | }
32 | }
33 |
34 | /* --- fields --- */
35 | .evol-fset {
36 | @include flex-holder();
37 | padding: 16px $field-h-spacing 16px 0;
38 | }
39 | .form-buttons {
40 | width: 100%;
41 | > button,
42 | > a {
43 | margin: 10px;
44 | > i {
45 | margin-right: 5px;
46 | }
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/i18n/i18n.js:
--------------------------------------------------------------------------------
1 | /*
2 | Evolutility-UI-React Localized strings in ENGLISH
3 | (c) 2023 Olivier Giulieri
4 | https://github.com/evoluteur/evolutility-ui-react
5 | */
6 |
7 | import allStrings from "./en";
8 |
9 | export default allStrings;
10 |
11 | export const id = allStrings.id;
12 | export const locale = allStrings.name;
13 | export const i18n_nav = allStrings.i18n_nav;
14 | export const i18n_actions = allStrings.i18n_actions;
15 | export const i18n_msg = allStrings.i18n_msg;
16 | export const i18n_validation = allStrings.i18n_validation;
17 | export const i18n_charts = allStrings.i18n_charts;
18 | export const i18n_comments = allStrings.i18n_comments;
19 | export const i18n_stats = allStrings.i18n_stats;
20 | export const i18n_activity = allStrings.i18n_activity;
21 | export const i18n_upload = allStrings.i18n_upload;
22 | export const i18n_errors = allStrings.i18n_errors;
23 | export const i18n_404 = allStrings.i18n_404;
24 |
25 | // export const i18n_login = allStrings. ;
26 |
--------------------------------------------------------------------------------
/src/components/views/one/shared/Timestamps/Timestamps.jsx:
--------------------------------------------------------------------------------
1 | import React, { memo } from "react";
2 | import PropTypes from "prop-types";
3 | import config from "config";
4 | import { datetimeString } from "utils/format";
5 | import { i18n_activity } from "i18n/i18n";
6 |
7 | import "./Timestamps.scss";
8 |
9 | const showTimestamp = config.withTimestamp;
10 |
11 | const Timestamps = memo(({ updated, created }) => {
12 | if (!showTimestamp) {
13 | return null;
14 | }
15 | return (
16 |
17 |
18 | {i18n_activity.updated}
19 | {datetimeString(updated)}
20 |
21 |
22 | {i18n_activity.created}
23 | {datetimeString(created)}
24 |
25 |
26 | );
27 | });
28 |
29 | export default Timestamps;
30 |
31 | Timestamps.propTypes = {
32 | created: PropTypes.any.isRequired,
33 | updated: PropTypes.any.isRequired,
34 | };
35 |
--------------------------------------------------------------------------------
/src/components/Field/edit/FieldDate.scss:
--------------------------------------------------------------------------------
1 | .evol-fld {
2 | .react-datepicker-wrapper {
3 | display: inline;
4 | }
5 | }
6 |
7 | .react-datepicker__navigation {
8 | top: 3px !important;
9 | border: none !important;
10 | }
11 | /* https://github.com/Hacker0x01/react-datepicker/issues/624 */
12 | .react-datepicker {
13 | font-size: 0.8em;
14 | }
15 | .react-datepicker__header {
16 | padding-top: 0.8em;
17 | }
18 | .react-datepicker__month {
19 | margin: 0.4em;
20 | }
21 | .react-datepicker__day-name,
22 | .react-datepicker__day {
23 | width: 1.9em;
24 | line-height: 1.9em;
25 | margin: 0.166em;
26 | }
27 | .react-datepicker__current-month {
28 | font-size: 1em;
29 | }
30 | .react-datepicker__navigation {
31 | top: 1em;
32 | line-height: 1.7em;
33 | border: 0.45em solid transparent;
34 | }
35 | .react-datepicker__navigation--previous {
36 | border-right-color: #ccc;
37 | left: 1em;
38 | }
39 | .react-datepicker__navigation--next {
40 | border-left-color: #ccc;
41 | right: 1em;
42 | }
43 |
--------------------------------------------------------------------------------
/src/pages/Docs/Views.scss:
--------------------------------------------------------------------------------
1 | @import "variables";
2 |
3 | .evo-doc-views {
4 | .anchor {
5 | position: absolute;
6 | top: -60px;
7 | height: 0;
8 | width: 0;
9 | }
10 | h3 {
11 | margin-top: 30px;
12 | }
13 | .views-list {
14 | line-height: 30px;
15 | margin-bottom: 30px;
16 | > li {
17 | margin-bottom: 10px;
18 | }
19 | }
20 | .doc-view {
21 | padding: 0 0 10px 20px;
22 | h3 {
23 | position: relative;
24 | > svg {
25 | position: relative;
26 | top: 6px;
27 | margin-right: 8px;
28 | height: 30px;
29 | width: 30px;
30 | }
31 | }
32 | .shadowpix {
33 | max-width: 600px;
34 | width: 100%;
35 | border: $border;
36 | box-shadow: 2px 2px 4px #aaaaaa;
37 | }
38 | }
39 | .toc-view {
40 | position: relative;
41 | display: inline-block;
42 | margin-right: 20px;
43 | > a > i {
44 | position: relative;
45 | top: 4px;
46 | }
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/components/views/one/shared/Timestamps/Timestamps.test.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable jest/no-conditional-expect */
2 | import { render, screen } from "@testing-library/react";
3 | import config from "config";
4 | import { i18n_activity as i18n } from "i18n/i18n";
5 | import { datetimeString } from "utils/format";
6 |
7 | import Timestamps from "./Timestamps";
8 |
9 | const showTimestamp = config.withTimestamp;
10 |
11 | describe("Timestamps widget tests", () => {
12 | it("check children", () => {
13 | const t1 = "1789-05-05T08:54:52.673864";
14 | const t2 = "2023-11-11T08:54:52.673864";
15 | render( );
16 | const ts = screen.queryByTestId("timestamps");
17 | if (showTimestamp) {
18 | expect(ts).toHaveTextContent(i18n.created);
19 | expect(ts).toHaveTextContent(datetimeString(t1));
20 | expect(ts).toHaveTextContent(i18n.updated);
21 | expect(ts).toHaveTextContent(datetimeString(t2));
22 | } else {
23 | expect(ts).not.toBeInTheDocument();
24 | }
25 | });
26 | });
27 |
--------------------------------------------------------------------------------
/src/components/shell/TopBar/GitHubLink.jsx:
--------------------------------------------------------------------------------
1 | const GitHubLink = () => (
2 |
9 |
16 |
17 |
18 |
19 | );
20 |
21 | export default GitHubLink;
22 |
--------------------------------------------------------------------------------
/src/routes/EvolRoutes.jsx:
--------------------------------------------------------------------------------
1 | // Routes for Evolutility views
2 | import React from "react";
3 | import { Routes, Route } from "react-router-dom";
4 | import config from "config";
5 |
6 | import One from "components/views/one/One";
7 | import Many from "components/views/many/Many";
8 | import Stats from "components/views/analytics/Stats/Stats";
9 | import Charts from "components/views/analytics/Charts/Charts";
10 | import Overview from "components/views/comfort/Overview/Overview";
11 | import Activity from "components/views/comfort/Activity/Activity";
12 |
13 | const EvolRoutes = () => (
14 |
15 | } />
16 | } />
17 | } />
18 | } />
19 | } />
20 | {config.withActivity && (
21 | } />
22 | )}
23 |
24 | );
25 |
26 | export default EvolRoutes;
27 |
--------------------------------------------------------------------------------
/src/components/views/comfort/Overview/Overview.scss:
--------------------------------------------------------------------------------
1 | @import "variables";
2 |
3 | .evol-overview {
4 | .evo-search-box {
5 | margin: 20px 10px 20px 0;
6 | input {
7 | width: 220px;
8 | }
9 | }
10 | .panel {
11 | margin-bottom: 10px;
12 | }
13 | .ovw-actions {
14 | margin-bottom: 20px;
15 | width: 100%;
16 | @include flex-holder();
17 | gap: $gap;
18 | > div {
19 | flex: 1 1 0px;
20 | flex-grow: 1;
21 | display: inline-block;
22 | margin-right: 20px;
23 | > a {
24 | @include link-focus;
25 | display: block;
26 | > i {
27 | position: relative;
28 | top: 6px;
29 | }
30 | }
31 | }
32 | }
33 | .ovw-chart {
34 | position: relative;
35 | min-width: 400px;
36 | select {
37 | position: absolute;
38 | top: 10px;
39 | left: 15px;
40 | width: auto;
41 | }
42 | .table {
43 | margin-top: 20px;
44 | }
45 | .chart-pie {
46 | position: relative;
47 | top: 20px;
48 | }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/components/ErrorBoundary.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Alert from "components/widgets/Alert/Alert";
3 |
4 | export default class ErrorBoundary extends React.Component {
5 | constructor(props) {
6 | super(props);
7 | this.state = { hasError: false };
8 | }
9 |
10 | static getDerivedStateFromError(error) {
11 | // Update state so the next render will show the fallback UI.
12 | return { hasError: true };
13 | }
14 |
15 | componentDidCatch(error, errorInfo) {
16 | // You can also log the error to an error reporting service
17 | // logErrorToMyService(error, errorInfo);
18 | console.error(error, errorInfo);
19 | }
20 |
21 | render() {
22 | if (this.state.hasError) {
23 | // You can render any custom fallback UI
24 | const msg = (
25 | <>
26 | Oops! Something went wrong.
27 | Please refresh the browser window.
28 | >
29 | );
30 | return ;
31 | }
32 |
33 | return this.props.children;
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/components/views/ViewHeader/ViewHeader.test.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable jest/no-conditional-expect */
2 | import { render, screen } from "@testing-library/react";
3 | import { MemoryRouter as Router } from "react-router-dom";
4 |
5 | import { withComments } from "config";
6 |
7 | import ViewHeader from "./ViewHeader";
8 |
9 | describe("ViewHeader tests", () => {
10 | it("ViewHeader shows props values", async () => {
11 | render(
12 |
13 |
22 |
23 | );
24 | const vh = screen.queryByTestId("viewheader");
25 | expect(vh).toHaveTextContent("title-test");
26 | expect(vh).toHaveTextContent("50");
27 | if (withComments) {
28 | expect(vh).toHaveTextContent("2");
29 | } else {
30 | expect(vh).not.toHaveTextContent("2");
31 | }
32 |
33 | expect(vh).toHaveTextContent("text-test");
34 | });
35 | });
36 |
--------------------------------------------------------------------------------
/src/components/widgets/Alert/Alert.jsx:
--------------------------------------------------------------------------------
1 | import React, { memo } from "react";
2 | import PropTypes from "prop-types";
3 | import Icon from "react-crud-icons";
4 |
5 | import "./Alert.scss";
6 |
7 | const icons = {
8 | info: "info",
9 | success: "check",
10 | warning: "alert",
11 | danger: "error",
12 | };
13 |
14 | const icon = (name) => ;
15 |
16 | const Alert = memo(({ title, message, type }) => (
17 |
18 |
19 | {title && icon(type)}
20 | {title}
21 |
22 |
{message}
23 |
24 | ));
25 |
26 | export default Alert;
27 |
28 | Alert.propTypes = {
29 | title: PropTypes.string,
30 | message: PropTypes.node.isRequired,
31 | type: PropTypes.oneOf([
32 | "info", // - blue
33 | "success", // - green
34 | "warning", // - yellow
35 | "danger", // - red
36 | ]),
37 | };
38 |
39 | Alert.defaultProps = {
40 | title: null,
41 | type: "danger",
42 | };
43 |
--------------------------------------------------------------------------------
/src/components/shell/Footer/Footer.jsx:
--------------------------------------------------------------------------------
1 | import React, { memo } from "react";
2 | import packageInfo from "../../../../package.json";
3 |
4 | import "./Footer.scss";
5 |
6 | const { version } = packageInfo;
7 | const currentYear = new Date().getFullYear();
8 |
9 | const Footer = memo(() => {
10 | return (
11 |
35 | );
36 | });
37 |
38 | export default Footer;
39 |
--------------------------------------------------------------------------------
/src/components/views/analytics/Stats/Stats.scss:
--------------------------------------------------------------------------------
1 | @import "variables";
2 |
3 | $label-width: 270px;
4 | $label2-width: 100px;
5 |
6 | .evol-stats {
7 | .evo-label {
8 | margin-bottom: 7px;
9 | }
10 | .label-count {
11 | margin-bottom: 10px;
12 | }
13 | }
14 | .stat-field-title {
15 | margin: 10px 0 15px;
16 | color: $color-text;
17 | font-size: 20px;
18 | > span {
19 | font-weight: 300;
20 | }
21 | }
22 | .stat-field-id {
23 | color: $color-text-secondary;
24 | position: absolute;
25 | top: 18px;
26 | right: 20px;
27 | }
28 | .stats-fields {
29 | @include flex-holder();
30 | gap: $gap;
31 | .f-stats {
32 | flex: 1 1 0px;
33 | flex-grow: 1;
34 | position: relative;
35 | @include flex-item();
36 | min-width: 340px;
37 | padding: 10px 20px;
38 | .stat-values > div {
39 | margin-bottom: 10px;
40 | > span {
41 | margin-left: 10px;
42 | color: $color-text-secondary;
43 | }
44 | > label {
45 | margin-right: $gap;
46 | &:after {
47 | content: ":";
48 | }
49 | }
50 | }
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
9 |
10 |
11 |
12 |
13 |
14 |
18 | Evolutility
19 |
23 |
24 |
25 |
26 | You need to enable JavaScript to run this app.
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/src/components/views/many/shared/Pagination/Pagination.test.js:
--------------------------------------------------------------------------------
1 | import { render, screen } from "@testing-library/react";
2 | import Pagination from "./Pagination";
3 | import config from "config";
4 |
5 | const { pageSize = 50 } = config;
6 | const noOp = () => {};
7 |
8 | describe("Pagination tests", () => {
9 | it("Pagination shows if fullCount > pageSize", async () => {
10 | render(
11 |
12 | );
13 | const pagination = screen.queryByTestId("pagination");
14 | expect(pagination).toBeInTheDocument();
15 | });
16 | it("Pagination doesn't shows if fullCount = pageSize", async () => {
17 | render( );
18 | const pagination = screen.queryByTestId("pagination");
19 | expect(pagination).not.toBeInTheDocument();
20 | });
21 | it("Pagination doesn't shows if fullCount < pageSize", async () => {
22 | render(
23 |
24 | );
25 | const pagination = screen.queryByTestId("pagination");
26 | expect(pagination).not.toBeInTheDocument();
27 | });
28 | });
29 |
--------------------------------------------------------------------------------
/src/pages/Home/Gallery.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { pixPath } from "utils/format";
3 | import AliceCarousel from "react-alice-carousel";
4 | import { viewDoc } from "pages/Docs/docMetadata";
5 |
6 | import "react-alice-carousel/lib/alice-carousel.css";
7 | import "./Gallery.scss";
8 |
9 | const handleDragStart = (e) => e.preventDefault();
10 |
11 | const allViewsInfo = [...viewDoc.one, ...viewDoc.many, ...viewDoc.comfort];
12 | const items = allViewsInfo.map((v) => (
13 |
14 | {v.name}
15 |
16 |
22 |
23 |
24 | ));
25 |
26 | const Gallery = () => {
27 | return (
28 |
40 | );
41 | };
42 |
43 | export default Gallery;
44 |
--------------------------------------------------------------------------------
/src/utils/url.js:
--------------------------------------------------------------------------------
1 | import queryString from "query-string";
2 |
3 | export function querySearch(query) {
4 | // - make uri params string from query object
5 | // - example: {a:1, b: 'bbb'} => "a=1&b=bbb"
6 | const urlParams = [];
7 | for (const prop in query) {
8 | if (query[prop] !== "") {
9 | urlParams.push(prop + "=" + encodeURI(query[prop]));
10 | }
11 | }
12 | return urlParams.join("&");
13 | }
14 |
15 | export const parseQuery = (qString) => {
16 | return qString ? queryString.parse(qString) : null;
17 | };
18 |
19 | // export const hasFilters = (qString) => {
20 | // if (!qString) {
21 | // return false;
22 | // }
23 | // let res = false;
24 | // qString
25 | // .slice(1)
26 | // .split("&")
27 | // .forEach((p) => {
28 | // if (
29 | // !(
30 | // p.startsWith("order=") ||
31 | // p.startsWith("page=") ||
32 | // p.startsWith("pageSize=")
33 | // )
34 | // ) {
35 | // res = true;
36 | // return;
37 | // }
38 | // });
39 | // return res;
40 | // };
41 |
42 | const urlModule = {
43 | querySearch,
44 | parseQuery,
45 | // hasFilters,
46 | };
47 |
48 | export default urlModule;
49 |
--------------------------------------------------------------------------------
/src/components/views/many/shared/Pagination/Pagination.scss:
--------------------------------------------------------------------------------
1 | @import "variables";
2 |
3 | .evo-pagination {
4 | display: flex;
5 | justify-content: center;
6 | margin: 20px 0;
7 | gap: 5px;
8 | color: $color-btn-primary;
9 | > div {
10 | display: flex;
11 | justify-content: center;
12 | align-items: center;
13 | border-radius: 20px;
14 | height: 32px;
15 | min-width: 32px;
16 | border: $border;
17 | padding: 6px;
18 | cursor: pointer;
19 | &:hover,
20 | &:focus {
21 | color: $color-link-hover;
22 | background-color: #eee;
23 | border-color: $color-border-hover;
24 | }
25 | &.active,
26 | &.active :hover,
27 | &.active :focus {
28 | color: #fff;
29 | cursor: default;
30 | background-color: $color-btn-primary;
31 | border-color: $color-btn-primary;
32 | }
33 | &.disabled,
34 | &.disabled :hover,
35 | &.disabled :focus {
36 | color: #777;
37 | cursor: not-allowed;
38 | background-color: #fff;
39 | border: none;
40 | padding: 6px 0;
41 | min-width: 12px;
42 | cursor: auto;
43 | }
44 | &.w-border {
45 | border: $border;
46 | min-width: 32px;
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/pages/Docs/components/PrettyJSON.scss:
--------------------------------------------------------------------------------
1 | @import "variables";
2 |
3 | $color-hover: blue;
4 |
5 | .pretty-json {
6 | background-color: #f5f5f5;
7 | border: 1px solid silver;
8 | padding: 10px 10px 10px 30px;
9 | border-radius: 5px;
10 | .tab {
11 | position: relative;
12 | margin-left: 20px;
13 | }
14 | .key {
15 | color: #548dd4;
16 | }
17 | .dots {
18 | margin: 0 4px;
19 | cursor: pointer;
20 | &:hover {
21 | color: $color-hover;
22 | }
23 | }
24 | .obj {
25 | position: relative;
26 | }
27 | .num {
28 | color: #009933;
29 | }
30 | .string {
31 | color: #e36c09;
32 | }
33 | .bool {
34 | color: blue;
35 | }
36 | .func {
37 | color: #9933ff;
38 | }
39 | .icoco {
40 | position: absolute;
41 | top: 3px;
42 | left: -20px;
43 | height: $icon-size;
44 | cursor: pointer;
45 | svg {
46 | width: $icon-size;
47 | height: $icon-size;
48 | border: 1px solid silver;
49 | border-radius: 2px;
50 | background-color: $color-almostwhite;
51 | &:hover {
52 | fill: $color-hover;
53 | border-color: $color-hover;
54 | }
55 | }
56 | &.up > svg {
57 | transform: rotate(90deg);
58 | }
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/components/views/many/shared/EmptyState/EmptyState.test.js:
--------------------------------------------------------------------------------
1 | import { render, screen } from "@testing-library/react";
2 | import { MemoryRouter as Router } from "react-router-dom";
3 |
4 | import { i18n_msg as i18n } from "i18n/i18n";
5 |
6 | import EmptyState from "./EmptyState";
7 |
8 | describe("EmptyState tests", () => {
9 | const model = { id: "test", name: "test", namePlural: "tests", fields: [] };
10 | it("EmptyState shows message w/ no filters", async () => {
11 | render(
12 |
13 |
14 |
15 | );
16 | const component = screen.queryByTestId("emptystate");
17 | expect(component).toHaveTextContent(i18n.noResults);
18 | const msg = i18n.empty.replaceAll("{0}", model.namePlural);
19 | expect(component).toHaveTextContent(msg);
20 | });
21 | it("EmptyState shows message w/ filters", async () => {
22 | render(
23 |
24 |
25 |
26 | );
27 | const component = screen.queryByTestId("emptystate");
28 | expect(component).toHaveTextContent(i18n.noResults);
29 | const msg = i18n.noData.replaceAll("{0}", model.namePlural);
30 | expect(component).toHaveTextContent(msg);
31 | });
32 | });
33 |
--------------------------------------------------------------------------------
/src/config.js:
--------------------------------------------------------------------------------
1 | /* Evolutility config options */
2 |
3 | const config = {
4 | // - Path to GraphQL API
5 | apiPath: "TODO: ENTER GRAPHQL API PATH HERE",
6 | adminSecret: "TODO: ENTER ADMIN SECRET HERE",
7 |
8 | // - prefix run app in sub-directory (also define as "homepage" in package.json)
9 | // baseName: "/evodemo/",
10 |
11 | // - Path to uploaded files
12 | filesUrl: "/pix/",
13 |
14 | // - get models from server at startup (and add/replace json models)
15 | queryModels: false,
16 |
17 | // - Pagination
18 | pageSize: 50,
19 |
20 | // - Language
21 | locale: "en",
22 |
23 | // - Data Caching
24 | useCache: true,
25 | cacheDuration: 180, // time in seconds
26 |
27 | // - Timestamp columns updated_at and created_at w/ date of record creation and last update
28 | withTimestamp: true,
29 | // - "WhoIs" columns updated_by and created_by w/ userid of creator and last modifier
30 | withWhoIs: false,
31 | // - Track last viewed record names in localstorage
32 | withActivity: false,
33 | // max number of activity records tracked
34 | activityListSize: 50,
35 |
36 | // - Comments & Ratings (community feature)
37 | withComments: false, // not implemented yet
38 | withRating: false, // not implemented yet
39 | };
40 |
41 | export default config;
42 |
--------------------------------------------------------------------------------
/src/components/views/many/List/List.scss:
--------------------------------------------------------------------------------
1 | @import "variables";
2 |
3 | .evol-many-list {
4 | .align-right {
5 | text-align: right;
6 | }
7 | > div {
8 | overflow-x: auto;
9 | overflow-y: hidden;
10 | }
11 | .table {
12 | margin-bottom: 0;
13 | th {
14 | position: sticky;
15 | top: 49px;
16 | z-index: 2;
17 | white-space: nowrap;
18 | transition: border-color $ease-time;
19 | &:first-child {
20 | min-width: 100px;
21 | }
22 | svg {
23 | position: relative;
24 | top: 2px;
25 | fill: $color-almostblack;
26 | height: $icon-size;
27 | width: $icon-size;
28 | }
29 | }
30 | td {
31 | position: relative;
32 | }
33 | img {
34 | max-height: 60px;
35 | padding: 0;
36 | }
37 | }
38 | .sortable {
39 | th {
40 | cursor: pointer;
41 | &:hover {
42 | color: $color-icon-hover;
43 | border-bottom-color: grey;
44 | }
45 | }
46 | }
47 | .td-check {
48 | padding-top: 0;
49 | padding-bottom: 0;
50 | > svg {
51 | height: $icon-size;
52 | width: $icon-size;
53 | }
54 | }
55 | .td-url {
56 | max-width: 200px;
57 | > a {
58 | word-wrap: break-word;
59 | }
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/src/components/widgets/global.scss:
--------------------------------------------------------------------------------
1 | // copied and modified from bootstrap-sass v3.4.1
2 |
3 | * {
4 | box-sizing: border-box;
5 | &:before,
6 | &:after {
7 | box-sizing: border-box;
8 | }
9 | }
10 | input,
11 | button,
12 | select,
13 | textarea {
14 | font-family: inherit;
15 | font-size: inherit;
16 | line-height: inherit;
17 | }
18 | img {
19 | vertical-align: middle;
20 | }
21 | .img-thumbnail {
22 | padding: 0;
23 | line-height: 1.428571429;
24 | background-color: #fff;
25 | border: 1px solid #ddd;
26 | border-radius: 4px;
27 | display: inline-block;
28 | max-width: 100%;
29 | height: auto;
30 | }
31 | h1,
32 | h2,
33 | h3,
34 | h4 {
35 | font-family: inherit;
36 | font-weight: 500;
37 | line-height: 1.1;
38 | color: inherit;
39 | }
40 | h1,
41 | h2,
42 | h3 {
43 | margin-top: 20px;
44 | margin-bottom: 10px;
45 | }
46 | h4 {
47 | font-size: 1.2em;
48 | margin-top: 10px;
49 | margin-bottom: 10px;
50 | }
51 | p {
52 | margin: 0 0 10px;
53 | }
54 | ul,
55 | ol {
56 | margin-top: 0;
57 | margin-bottom: 10px;
58 | }
59 | ul ul,
60 | ul ol,
61 | ol ul,
62 | ol ol {
63 | margin-bottom: 0;
64 | }
65 |
66 | // React-Tooltip customizations
67 | .rc-tooltip,
68 | .rc-tooltip-inner {
69 | max-width: 180px;
70 | border-radius: 10px !important;
71 | }
72 |
--------------------------------------------------------------------------------
/src/components/Field/edit/FieldDate.jsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback, memo } from "react";
2 | import PropTypes from "prop-types";
3 | import Datepicker from "react-datepicker";
4 | import { trueDate } from "utils/format";
5 |
6 | import "react-datepicker/dist/react-datepicker.css";
7 | import "./FieldDate.scss";
8 |
9 | // TODO: set maxDate and minDate (not urgent if caught w/ validation)
10 |
11 | const FieldDate = memo(({ id, value, onChange }) => {
12 | const onDateChange = useCallback(
13 | (value) =>
14 | onChange({
15 | target: {
16 | id,
17 | value,
18 | },
19 | }),
20 | [id, onChange]
21 | );
22 |
23 | return (
24 |
31 | );
32 | });
33 |
34 | export default FieldDate;
35 |
36 | FieldDate.propTypes = {
37 | /** Field id */
38 | id: PropTypes.string.isRequired,
39 | /** Callback functions for changed field value */
40 | onChange: PropTypes.func.isRequired,
41 | /** Field value (date as string like "2023-12-24" or date) */
42 | value: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
43 | };
44 |
45 | FieldDate.defaultProps = {
46 | value: null,
47 | };
48 |
--------------------------------------------------------------------------------
/src/components/Field/FieldLabel.test.js:
--------------------------------------------------------------------------------
1 | import { render, screen } from "@testing-library/react";
2 | import FieldLabel from "./FieldLabel";
3 |
4 | const fieldRequired = {
5 | id: "name",
6 | label: "Name",
7 | type: "text",
8 | help: "hello",
9 | required: true,
10 | };
11 | const fieldNotRequired = { ...fieldRequired, required: false, help: null };
12 |
13 | describe("fieldlabel tests", () => {
14 | it("FieldLabel shows label and * for required", async () => {
15 | render( );
16 | const fLabel = screen.getByTestId("fieldlabel");
17 | expect(fLabel).toHaveTextContent("Name");
18 | expect(fLabel).toHaveTextContent("*");
19 | const star = screen.queryByTestId("fl-required");
20 | expect(star).toBeInTheDocument();
21 | const help = screen.queryByTestId("fl-help");
22 | expect(help).toBeInTheDocument();
23 | });
24 | it("FieldLabel shows label and no *", async () => {
25 | render( );
26 | const fLabel = screen.getByTestId("fieldlabel");
27 | expect(fLabel).toHaveTextContent("Name");
28 | const star = screen.queryByTestId("fl-required");
29 | expect(star).not.toBeInTheDocument();
30 | const help = screen.queryByTestId("fl-help");
31 | expect(help).not.toBeInTheDocument();
32 | });
33 | });
34 |
--------------------------------------------------------------------------------
/src/components/widgets/Panel/Panel.scss:
--------------------------------------------------------------------------------
1 | @import "variables";
2 |
3 | .panel {
4 | background-color: $bgcolor-background;
5 | border: $border;
6 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.05);
7 | border-radius: $radius;
8 | &.collapsed {
9 | .panel-heading {
10 | border-radius: $radius !important;
11 | }
12 | fieldset {
13 | display: none;
14 | }
15 | }
16 | }
17 | .panel-heading {
18 | position: relative;
19 | border-radius: $radius $radius 0 0;
20 | padding: 10px 15px;
21 | border-bottom: $border;
22 | background-color: $bgcolor-panel-header;
23 | svg {
24 | position: absolute;
25 | top: 8px;
26 | right: 5px;
27 | cursor: pointer;
28 | height: 26px;
29 | width: 26px;
30 | fill: $color-icon;
31 | &:hover {
32 | fill: $color-icon-hover;
33 | }
34 | }
35 | }
36 | .panel-title {
37 | display: inline-block;
38 | font-weight: 400;
39 | font-size: 1.2em;
40 | margin-top: 0;
41 | margin-bottom: 0;
42 | color: inherit;
43 | }
44 | .panel-header,
45 | .panel-footer {
46 | padding: 10px 15px;
47 | background-color: $bgcolor-panel-footer;
48 | }
49 | .panel-header {
50 | border-bottom: $border;
51 | }
52 | .panel-footer {
53 | position: relative;
54 | top: -10px;
55 | border-top: $border;
56 | border-radius: 0 0 $radius $radius;
57 | }
58 |
--------------------------------------------------------------------------------
/src/models/all_models.js:
--------------------------------------------------------------------------------
1 | /*
2 | Evolutility UI Models
3 | https://github.com/evoluteur/evolutility-ui-react
4 | */
5 |
6 | // - Organizer
7 | import todo from "./organizer/todo";
8 | import contact from "./organizer/contact";
9 | import comics from "./organizer/comics";
10 | import restaurant from "./organizer/restaurant";
11 | import winecellar from "./organizer/winecellar";
12 | import winetasting from "./organizer/winetasting";
13 |
14 | // - Music
15 | import artist from "./music/artist";
16 | import album from "./music/album";
17 | import track from "./music/track";
18 |
19 | // // - Designer
20 | // import field from "./designer/field";
21 | // import object from "./designer/object";
22 | // import group from "./designer/group";
23 | // import collection from "./designer/collection";
24 | // import world from "./designer/world";
25 |
26 | // // - Tests
27 | // import test from "./tests/test";
28 |
29 | let models = {
30 | // - Organizer
31 | todo,
32 | contact,
33 | comics,
34 | restaurant,
35 | winecellar,
36 | winetasting,
37 |
38 | // - Music
39 | artist: artist,
40 | album: album,
41 | track: track,
42 |
43 | // // - Designer
44 | // world,
45 | // object,
46 | // field,
47 | // group,
48 | // collection,
49 |
50 | // - Tests
51 | // test: test,
52 | };
53 |
54 | export default models;
55 |
--------------------------------------------------------------------------------
/src/components/Field/Field.test.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable jest/no-conditional-expect */
2 | import { render, screen } from "@testing-library/react";
3 | import Field from "./Field";
4 |
5 | const noOp = () => {};
6 |
7 | const simpleTest = (fType, value) =>
8 | it(`Field ${fType} shows`, async () => {
9 | const meta = { id: fType, type: fType, label: "Field " + fType };
10 | render( );
11 | const f = screen.getByTestId("field-" + fType);
12 | expect(f).toBeInTheDocument();
13 | if (value) {
14 | expect(screen.getByDisplayValue(value)).toBeInTheDocument();
15 | }
16 | const label = screen.getByTestId("fieldlabel");
17 | expect(label).toHaveTextContent("Field " + fType);
18 | });
19 |
20 | describe("Editable field tests", () => {
21 | // TODO: all field types w/ more complex use-cases
22 | simpleTest("text", "abc");
23 | simpleTest("textmultiline", "abc");
24 | simpleTest("lov");
25 | simpleTest("boolean");
26 | simpleTest("email", "olivier@evolutility.com");
27 | simpleTest("url", "http://evolutility.com");
28 | simpleTest("integer", 1212);
29 | simpleTest("decimal", 12.12);
30 | simpleTest("money", 119.99);
31 | // simpleTest("date");
32 | simpleTest("time");
33 | simpleTest("image");
34 | simpleTest("document");
35 | simpleTest("json");
36 | });
37 |
--------------------------------------------------------------------------------
/src/components/views/ViewHeader/FilterTags.jsx:
--------------------------------------------------------------------------------
1 | import React, { memo } from "react";
2 | import PropTypes from "prop-types";
3 | import Badge from "components/widgets/Badge/Badge";
4 |
5 | import "./ViewHeader.scss";
6 |
7 | const operators = {
8 | eq: "=",
9 | gt: ">",
10 | lt: "<",
11 | };
12 |
13 | const FilterTags = memo(({ params }) => {
14 | if (!params) {
15 | return null;
16 | }
17 | const ps = params.slice(1).split("&");
18 | const filters = [];
19 | ps.forEach((p) => {
20 | const [field, cond] = p.split("=");
21 | if (!["order", "page", "pageSize"].includes(field)) {
22 | if (field === "search") {
23 | filters.unshift(
24 |
25 | );
26 | }
27 | const idx = cond.indexOf(".");
28 | if (idx > 0) {
29 | const op = operators[cond.substring(0, idx)] || " ";
30 | const v = cond.substring(idx + 1);
31 | // TODO: use model to get correct field label
32 | filters.push( );
33 | }
34 | }
35 | });
36 | return filters.length ? filters : null;
37 | });
38 |
39 | export default FilterTags;
40 |
41 | FilterTags.propTypes = {
42 | /** filters and search (as in location search) */
43 | params: PropTypes.string,
44 | };
45 |
46 | FilterTags.defaultProps = {
47 | params: "",
48 | };
49 |
--------------------------------------------------------------------------------
/src/components/widgets/Spinner/Spinner.scss:
--------------------------------------------------------------------------------
1 | /*
2 | SpinKit
3 | http://tobiasahlin.com/spinkit/
4 | */
5 |
6 | @import "variables";
7 |
8 | .evol-loading {
9 | text-align: center;
10 | width: 100%;
11 | .loading_txt {
12 | color: $color-text-secondary;
13 | margin: 40px 0 0 0;
14 | }
15 | }
16 |
17 | .spinner {
18 | margin: 20px auto 0;
19 | width: 90px;
20 | height: 40px;
21 | text-align: center;
22 | > div {
23 | width: 18px;
24 | height: 18px;
25 | margin: 5px;
26 | background-color: $color-blue1;
27 | border-radius: 100%;
28 | display: inline-block;
29 | -webkit-animation: sk-bouncedelay 1.4s infinite ease-in-out both;
30 | animation: sk-bouncedelay 1.4s infinite ease-in-out both;
31 | &.bounce1 {
32 | -webkit-animation-delay: -0.32s;
33 | animation-delay: -0.32s;
34 | }
35 | &.bounce2 {
36 | -webkit-animation-delay: -0.16s;
37 | animation-delay: -0.16s;
38 | }
39 | }
40 | }
41 |
42 | @-webkit-keyframes sk-bouncedelay {
43 | 0%,
44 | 80%,
45 | 100% {
46 | -webkit-transform: scale(0);
47 | }
48 | 40% {
49 | -webkit-transform: scale(1);
50 | }
51 | }
52 |
53 | @keyframes sk-bouncedelay {
54 | 0%,
55 | 80%,
56 | 100% {
57 | -webkit-transform: scale(0);
58 | transform: scale(0);
59 | }
60 | 40% {
61 | -webkit-transform: scale(1);
62 | transform: scale(1);
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/pages/Docs/docConfig.js:
--------------------------------------------------------------------------------
1 | const configOptions = [
2 | {
3 | name: "apiPath",
4 | description: "Path to GraphQL API.",
5 | example: '"https://myapp.hasura.app/v1/graphql"',
6 | },
7 | {
8 | name: "adminSecret",
9 | description: "Hasura admin secret.",
10 | example: "",
11 | },
12 | {
13 | name: "useCache",
14 | description: "Enable/disable data caching.",
15 | example: "true",
16 | },
17 | {
18 | name: "cacheDuration",
19 | description: "Cache duration in seconds.",
20 | example: "120 (for 2 minutes)",
21 | },
22 | {
23 | name: "pageSize",
24 | description: "Page size in pagination.",
25 | example: "50",
26 | },
27 | {
28 | name: "filesUrl",
29 | description: "Path to upload files to (not implemented yet).",
30 | example: '"/pix/"',
31 | },
32 | {
33 | name: "withActivity",
34 | description:
35 | "Tracks and shows records activity (last visited and most visited). Currently implemented w/ the browser's localStorage, it will be moved to the server later.",
36 | example: "true",
37 | },
38 | {
39 | name: "withTimestamp",
40 | description:
41 | 'Tracks and shows timestamp for creation date and last update for every record. The DB tables need timestamp columns "updated_at" and "created_at" for the feature to work.',
42 | example: "true",
43 | },
44 | ];
45 |
46 | export default configOptions;
47 |
--------------------------------------------------------------------------------
/src/components/views/many/shared/EmptyState/EmptyState.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 | import modelPropTypes from "components/views/modelPropTypes";
4 | import Alert from "components/widgets/Alert/Alert";
5 | import Button from "components/widgets/Button/Button";
6 | import { i18n_msg as i18n } from "i18n/i18n";
7 |
8 | import "./EmptyState.scss";
9 |
10 | const EmptyState = ({ model, hasFilters }) => {
11 | let msg = hasFilters ? i18n.noData : i18n.empty;
12 | msg = msg.replaceAll("{0}", model.namePlural);
13 | const content = (
14 | <>
15 | {msg}
16 |
17 | {hasFilters && {i18n.newCriteria}
}
18 |
19 | {!hasFilters && (
20 |
26 | )}
27 | >
28 | );
29 | return (
30 |
33 | );
34 | };
35 |
36 | export default EmptyState;
37 |
38 | EmptyState.propTypes = {
39 | model: modelPropTypes.isRequired,
40 | /** Does the user have search or filter criterias? */
41 | inSearch: PropTypes.bool,
42 | };
43 |
44 | EmptyState.defaultProps = { inSearch: false };
45 |
--------------------------------------------------------------------------------
/src/components/shell/TopBar/TopBar.scss:
--------------------------------------------------------------------------------
1 | @import "variables";
2 |
3 | $topbar-height: 50px;
4 |
5 | .evo-topbar {
6 | position: fixed;
7 | top: 0;
8 | left: 0;
9 | height: $topbar-height;
10 | width: 100%;
11 | display: block;
12 | z-index: 101;
13 | background-color: $color-almostblack;
14 | color: $color-almostwhite;
15 | .evo-logo {
16 | position: absolute;
17 | top: 8px;
18 | left: 14px;
19 | > img {
20 | width: 160px;
21 | height: 40px;
22 | }
23 | }
24 | > .github {
25 | position: absolute;
26 | right: 10px;
27 | top: 10px;
28 | > svg {
29 | fill: $color-almostwhite;
30 | }
31 | &:hover {
32 | > svg {
33 | fill: $color-blue1;
34 | }
35 | }
36 | @media (max-width: 600px) {
37 | display: none;
38 | }
39 | }
40 | }
41 | .icons-always,
42 | .icons-context {
43 | height: $topbar-height;
44 | > a {
45 | display: inline-block;
46 | &:focus {
47 | > .crud-icon > div {
48 | display: none;
49 | }
50 | }
51 | }
52 | }
53 | .topbar-icons {
54 | position: absolute;
55 | top: 2px;
56 | left: 190px;
57 | z-index: 110;
58 | > .icons-context {
59 | position: absolute;
60 | top: 0;
61 | left: 260px;
62 | min-width: 160px;
63 | }
64 | .crud-icon > div {
65 | white-space: nowrap;
66 | }
67 | }
68 |
69 | @media print {
70 | .topbar-icons {
71 | display: none;
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/src/utils/dicoViews.js:
--------------------------------------------------------------------------------
1 | import config from "config";
2 | import { i18n_actions as i18n } from "i18n/i18n";
3 |
4 | const { withActivity } = config;
5 | const view = (name) => ({
6 | id: name,
7 | label: i18n[name],
8 | icon: name,
9 | });
10 |
11 | export const views = {
12 | browse: view("browse"),
13 | edit: view("edit"),
14 | list: view("list"),
15 | cards: view("cards"),
16 | charts: {
17 | id: "charts",
18 | label: i18n.charts,
19 | icon: "dashboard",
20 | },
21 | // scatter: {id:'scatter', label: i18n.bScatter, icon:'certificate'},
22 | stats: view("stats"),
23 | activity: {
24 | id: "activity",
25 | label: i18n.activity,
26 | icon: "history",
27 | },
28 | overview: {
29 | id: "overview",
30 | label: i18n.overview,
31 | icon: "home-circle",
32 | },
33 | };
34 |
35 | export const modelViewsAnalytics = (model) => {
36 | const vs = [];
37 | if (!model.noCharts) {
38 | vs.push(views.charts);
39 | }
40 | if (!model.noStats) {
41 | vs.push(views.stats);
42 | }
43 | return vs;
44 | };
45 |
46 | export const modelViewsMany = (model, all) => {
47 | const vs = [views.list, views.cards];
48 | if (all) {
49 | const va = modelViewsAnalytics(model);
50 | if (va.length) {
51 | vs.push(...va);
52 | }
53 | if (withActivity && !model.noActivity) {
54 | vs.push(views.activity);
55 | }
56 | }
57 | return vs;
58 | };
59 |
60 | export default views;
61 |
--------------------------------------------------------------------------------
/src/pages/Home/Home.scss:
--------------------------------------------------------------------------------
1 | @import "variables";
2 |
3 | .evo-home {
4 | h1 {
5 | margin: 30px 0 40px !important;
6 | .version {
7 | font-size: 0.3em;
8 | }
9 | }
10 | section {
11 | margin-top: 40px;
12 | > div:first-child {
13 | margin-bottom: 20px;
14 | }
15 | }
16 | h2 > .cpnSvg {
17 | position: relative;
18 | top: -3px;
19 | margin-right: 10px;
20 | height: 30px;
21 | width: 30px;
22 | }
23 | .text-center {
24 | text-align: center;
25 | }
26 | .home-demos {
27 | margin: 20px;
28 | }
29 | .comp {
30 | h2 {
31 | margin-bottom: 30px;
32 | }
33 | h3 {
34 | margin-top: 0;
35 | }
36 | }
37 | .siteTitle {
38 | color: #2e9ec9;
39 | font-size: 70px;
40 | padding: 2px 0;
41 | margin: 0 10px 20px 0;
42 | .evol {
43 | color: orange;
44 | }
45 | .utility {
46 | color: #2e9ec9;
47 | }
48 | .rest {
49 | color: silver;
50 | }
51 | .evol,
52 | .utility,
53 | .rest {
54 | text-shadow: 2px 2px 4px grey;
55 | }
56 | .ns {
57 | white-space: nowrap;
58 | }
59 | }
60 | .tBlue {
61 | color: #2e9ec9;
62 | }
63 | .w100 {
64 | width: 100%;
65 | }
66 | .tech-logos {
67 | margin-top: 20px;
68 | > a {
69 | display: inline-block;
70 | }
71 | img {
72 | margin: 10px 20px;
73 | height: 80px;
74 | width: 80px;
75 | }
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/src/components/widgets/Button/Button.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 | import { Link } from "react-router-dom";
4 | import Icon, { keys } from "react-crud-icons";
5 | import classnames from "classnames";
6 |
7 | import "./Button.scss";
8 |
9 | const Button = ({ label, type, icon, url, onClick, className }) => {
10 | const css = classnames("btn btn-" + type, className);
11 | const content = (
12 | <>
13 | {icon && }
14 | {label}
15 | >
16 | );
17 | return onClick ? (
18 |
19 | {content}
20 |
21 | ) : (
22 |
23 | {content}
24 |
25 | );
26 | };
27 |
28 | export default Button;
29 |
30 | Button.propTypes = {
31 | /** Button label */
32 | label: PropTypes.string.isRequired,
33 | /** Button type */
34 | type: PropTypes.oneOf(["primary", "default"]),
35 | /** Icon name from react-crud-icon */
36 | icon: PropTypes.oneOf(keys),
37 | /** If url is specified the button uses a "a" tag */
38 | url: PropTypes.string,
39 | /** If onClick is specified the button uses a "button" tag */
40 | onClick: PropTypes.func,
41 | className: PropTypes.string,
42 | };
43 |
44 | Button.defaultProps = {
45 | type: "default",
46 | icon: null,
47 | url: null,
48 | onClick: null,
49 | className: null,
50 | };
51 |
--------------------------------------------------------------------------------
/src/pages/Docs/components/SampleModel.jsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable jsx-a11y/anchor-is-valid */
2 | /* eslint-disable jsx-a11y/anchor-has-content */
3 | /*
4 | Evolutility-UI-React
5 | https://github.com/evoluteur/evolutility-ui-react
6 | (c) 2023 Olivier Giulieri
7 | */
8 |
9 | import React, { useState } from "react";
10 | import { modelsArray, getModel } from "utils/moMa";
11 | import PrettyJSON from "./PrettyJSON";
12 |
13 | const calculatedProps = [
14 | "titleFunction", // Do not display functions
15 | "fieldsH",
16 | "_lovNoList",
17 | "_prepared",
18 | "_preparedCollecs",
19 | ];
20 |
21 | const unPrepModel = (m) => {
22 | const m2 = { ...m };
23 | calculatedProps.forEach((prop) => delete m2[prop]);
24 | if (!m2.collections?.length) {
25 | delete m2.collections;
26 | }
27 | if (m2.qid === m2.id) {
28 | delete m2.qid;
29 | }
30 | return m2;
31 | };
32 |
33 | const SampleModel = () => {
34 | const [mid, setModel] = useState("todo");
35 | const m = unPrepModel(getModel(mid));
36 | const onSelectModel = (evt) => {
37 | setModel(evt.currentTarget.value);
38 | };
39 |
40 | return (
41 |
42 |
Model:
43 |
44 | {modelsArray.map((m) => (
45 |
46 | {m.title}
47 |
48 | ))}
49 |
50 |
51 |
52 | );
53 | };
54 |
55 | export default SampleModel;
56 |
--------------------------------------------------------------------------------
/src/components/views/many/Cards/Cards.jsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable react-hooks/exhaustive-deps */
2 | // Evolutility-UI-React :: /views/many/Cards.js
3 |
4 | // Cards view to display a collection as a set of Cards.
5 |
6 | // https://github.com/evoluteur/evolutility-ui-react
7 | // (c) 2023 Olivier Giulieri
8 |
9 | import React, { useMemo } from "react";
10 | import PropTypes from "prop-types";
11 | import modelPropTypes from "components/views/modelPropTypes";
12 | import { i18n_errors } from "i18n/i18n";
13 | import Card from "components/views/one/Card";
14 | import Alert from "components/widgets/Alert/Alert";
15 |
16 | import "./Cards.scss";
17 |
18 | const Cards = ({ entity, model, data }) => {
19 | const fields = useMemo(() => model?.fields.filter((f) => f.inMany), [entity]);
20 | if (model) {
21 | return (
22 |
23 |
24 | {data?.map((d) => (
25 |
26 | ))}
27 |
28 |
29 | );
30 | }
31 | return (
32 |
36 | );
37 | };
38 |
39 | export default Cards;
40 |
41 | Cards.propTypes = {
42 | entity: PropTypes.string.isRequired,
43 | model: modelPropTypes.isRequired,
44 | data: PropTypes.arrayOf(
45 | PropTypes.shape({
46 | id: PropTypes.number.isRequired,
47 | })
48 | ),
49 | };
50 |
--------------------------------------------------------------------------------
/src/components/views/many/shared/TableBody/TableBody.test.js:
--------------------------------------------------------------------------------
1 | import { render, screen } from "@testing-library/react";
2 | import { MemoryRouter as Router } from "react-router-dom";
3 |
4 | import TableBody from "./TableBody";
5 |
6 | describe("TableBody tests", () => {
7 | const fields = [
8 | { id: "name", type: "text", label: "text" },
9 | { id: "num", type: "integer", label: "integer" },
10 | { id: "bool", type: "boolean", label: "boolean" },
11 | { id: "img", type: "image", label: "image" },
12 | { id: "color", type: "color", label: "color" },
13 | { id: "url", type: "url", label: "url" },
14 | ];
15 | const data = [
16 | {
17 | id: 1,
18 | name: "abc",
19 | num: 5,
20 | bool: true,
21 | img: "/pix/abc.png",
22 | color: "blue",
23 | url: "http://evolutility.com",
24 | },
25 | { id: 2, name: "xyz", num: 99, bool: false, color: "green" },
26 | { id: 12 },
27 | ];
28 | it("TableBody shows message w/ no search", async () => {
29 | render(
30 |
31 |
39 |
40 | );
41 | const component = screen.queryByTestId("tbody");
42 | ["abc", "5", "xyz", "12", "http://evolutility.com"].forEach((v) =>
43 | expect(component).toHaveTextContent(v)
44 | );
45 | expect(component).toHaveTextContent("(12)");
46 | });
47 | });
48 |
--------------------------------------------------------------------------------
/src/pages/Docs/Configuration.jsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable jsx-a11y/anchor-is-valid */
2 | /* eslint-disable jsx-a11y/anchor-has-content */
3 | import React, { useEffect } from "react";
4 | import configOptions from "./docConfig";
5 |
6 | import "./Doc.scss";
7 |
8 | const Configuration = () => {
9 | useEffect(() => {
10 | document.title = "Doc > Configuration";
11 | window.scrollTo(0, 0);
12 | });
13 |
14 | return (
15 |
16 |
Configuration
17 |
18 |
19 | Configurations options are specified in the{" "}
20 |
26 | /src/config.js
27 | {" "}
28 | file. They apply to all apps (app specific options are specified in
29 | models).
30 |
31 |
32 |
33 |
34 | Option
35 | Description
36 | Example
37 |
38 |
39 |
40 | {configOptions.map((c) => (
41 |
42 | {c.name}
43 | {c.description}
44 | {c.example}
45 |
46 | ))}
47 |
48 |
49 |
50 | );
51 | };
52 |
53 | export default Configuration;
54 |
--------------------------------------------------------------------------------
/src/components/views/modelPropTypes.js:
--------------------------------------------------------------------------------
1 | import PropTypes from "prop-types";
2 | import { fieldTypeStrings } from "utils/dico";
3 |
4 | export const fieldPropTypes = PropTypes.shape({
5 | id: PropTypes.string.isRequired,
6 | type: PropTypes.oneOf(fieldTypeStrings).isRequired,
7 | label: PropTypes.string.isRequired,
8 | required: PropTypes.bool,
9 | height: PropTypes.number,
10 | width: PropTypes.number,
11 | help: PropTypes.string,
12 | inMany: PropTypes.bool,
13 | inSearch: PropTypes.bool,
14 | });
15 |
16 | export const fieldGroupPropTypes = PropTypes.shape({
17 | // type: PropTypes.oneOf(["panel"]),
18 | label: PropTypes.string.isRequired,
19 | fields: PropTypes.arrayOf(PropTypes.string).isRequired,
20 | width: PropTypes.number,
21 | header: PropTypes.string,
22 | footer: PropTypes.string,
23 | });
24 |
25 | export const collectionPropTypes = PropTypes.shape({
26 | id: PropTypes.string.isRequired,
27 | title: PropTypes.string.isRequired,
28 | object: PropTypes.string.isRequired,
29 | fields: PropTypes.arrayOf(
30 | PropTypes.oneOfType([PropTypes.string, fieldPropTypes])
31 | ).isRequired,
32 | });
33 |
34 | const modelPropTypes = PropTypes.shape({
35 | id: PropTypes.string.isRequired,
36 | name: PropTypes.string.isRequired,
37 | namePlural: PropTypes.string.isRequired,
38 | fields: PropTypes.arrayOf(fieldPropTypes).isRequired,
39 | title: PropTypes.string,
40 | icon: PropTypes.string,
41 | groups: PropTypes.arrayOf(fieldGroupPropTypes),
42 | collections: PropTypes.arrayOf(collectionPropTypes),
43 | });
44 |
45 | export default modelPropTypes;
46 |
--------------------------------------------------------------------------------
/src/components/widgets/Alert/Alert.scss:
--------------------------------------------------------------------------------
1 | // copied and modified from bootstrap-sass v3.4.1
2 |
3 | @import "variables";
4 |
5 | .alert {
6 | position: relative;
7 | padding: 15px;
8 | margin-bottom: 20px;
9 | border: 1px solid silver;
10 | border-left-width: 12px;
11 | border-radius: $radius;
12 | > .alert-title {
13 | position: relative;
14 | margin: 0 0 20px;
15 | font-size: 22px;
16 | > svg {
17 | position: relative;
18 | top: 4px;
19 | margin-right: 7px;
20 | width: 24px;
21 | height: 24px;
22 | }
23 | }
24 | a {
25 | text-decoration: underline;
26 | }
27 | > div,
28 | > ul {
29 | margin-bottom: 0;
30 | &:first-letter {
31 | text-transform: uppercase;
32 | }
33 | }
34 | }
35 |
36 | // the following are shared b/w Alert and Badge components
37 | .alert-success {
38 | color: #3c763d;
39 | background-color: #dff0d8;
40 | border-color: #d6e9c6;
41 | a {
42 | color: #3c763d;
43 | }
44 | > .alert-title > svg {
45 | fill: #3c763d;
46 | }
47 | }
48 | .alert-info {
49 | color: #31708f;
50 | background-color: #d9edf7;
51 | border-color: #bce8f1;
52 | a {
53 | color: #31708f;
54 | }
55 | > .alert-title > svg {
56 | fill: #31708f;
57 | }
58 | }
59 | .alert-warning {
60 | color: #8a6d3b;
61 | background-color: #fcf8e3;
62 | border-color: #faebcc;
63 | a {
64 | color: #8a6d3b;
65 | }
66 | > .alert-title > svg {
67 | fill: #8a6d3b;
68 | }
69 | }
70 | .alert-danger {
71 | color: #a94442;
72 | background-color: #f2dede;
73 | border-color: #ebccd1;
74 | a {
75 | color: #a94442;
76 | }
77 | > .alert-title > svg {
78 | fill: #a94442;
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/src/pages/Demos/Demos.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from "react";
2 | import ModelLinks from "components/views/comfort/ModelLinks";
3 |
4 | import "./Demos.scss";
5 |
6 | const Demos = () => {
7 | useEffect(() => {
8 | document.title = "Evolutility Demos";
9 | window.scrollTo(0, 0);
10 | });
11 |
12 | return (
13 |
14 |
Evolutility Demos
15 |
16 |
These are a few sample apps built with Evolutility.
17 |
18 |
19 |
20 |
21 |
22 | These sample applications are not anything you haven't seen before. The
23 | interesting thing is that{" "}
24 | these demo apps are built with models rather than code ... and you
25 | can easily make more apps simply by making new{" "}
26 |
32 | models
33 |
34 | .
35 |
36 |
37 |
38 | These demos use a{" "}
39 |
45 | GraphQL API
46 |
47 | {" of "}
48 |
54 | Hasura
55 |
56 | .
57 |
58 |
59 | );
60 | };
61 |
62 | export default Demos;
63 |
--------------------------------------------------------------------------------
/src/components/views/comfort/Overview/SearchBox.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useCallback } from "react";
2 | import PropTypes from "prop-types";
3 | import { useNavigate } from "react-router-dom";
4 | import Icon from "react-crud-icons";
5 | import FieldObject from "components/Field/edit/FieldObject";
6 |
7 | import "./SearchBox.scss";
8 |
9 | const SearchBox = ({ entity, placeHolder }) => {
10 | const [value, setValue] = useState(null);
11 | const navigate = useNavigate();
12 | const onChange = useCallback(
13 | (valueObj) => {
14 | const id = valueObj?.value;
15 | if (id) {
16 | navigate(`../${entity}/browse/${id}`);
17 | }
18 | },
19 | [entity, navigate]
20 | );
21 | const onInputChange = useCallback((value) => setValue(value), []);
22 | const onClick = useCallback(() => {
23 | if (value) {
24 | navigate(`../${entity}/list/?search=${value}`);
25 | }
26 | }, [entity, value, navigate]);
27 |
28 | return (
29 |
30 |
31 |
38 |
39 |
40 |
41 |
42 |
43 | );
44 | };
45 |
46 | export default SearchBox;
47 |
48 | SearchBox.propTypes = {
49 | /** Model id */
50 | entity: PropTypes.string.isRequired,
51 | /** Placeholder text */
52 | placeHolder: PropTypes.string,
53 | };
54 |
55 | SearchBox.defaultProps = {
56 | placeHolder: null,
57 | };
58 |
--------------------------------------------------------------------------------
/src/components/widgets/Badge/Badge.test.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable testing-library/no-node-access */
2 | /* eslint-disable testing-library/await-async-query */
3 | import { render, screen } from "@testing-library/react";
4 | import Badge from "./Badge";
5 | import renderer from "react-test-renderer";
6 |
7 | describe("badge props test", () => {
8 | let badgeToTest;
9 | const props = {
10 | text: "My Badge!",
11 | };
12 | beforeEach(async () => {
13 | const testInstance = await renderer.create( );
14 | badgeToTest = testInstance.root;
15 | });
16 | it("should render text My Badge in span", () => {
17 | const spanRender = badgeToTest.findByType("span");
18 | expect(spanRender.children).toEqual([props.text]);
19 | });
20 | });
21 |
22 | describe("badge text tests", () => {
23 | it("badge shows text string correctly", async () => {
24 | render( );
25 | const badge = screen.getByTestId("badge");
26 | expect(badge).toHaveTextContent("You got a badge!");
27 | });
28 | it("badge shows number correctly", async () => {
29 | render( );
30 | const badge = screen.getByTestId("badge");
31 | expect(badge).toHaveTextContent(4152341234);
32 | });
33 | it("badge shows double byte text correctly", async () => {
34 | render( );
35 | const badge = screen.getByTestId("badge");
36 | expect(badge).toHaveTextContent("美味しい");
37 | });
38 | it("badge shows special character text correctly", async () => {
39 | render( );
40 | const badge = screen.getByTestId("badge");
41 | expect(badge).toHaveTextContent("Ça va? #@$%");
42 | });
43 | });
44 |
--------------------------------------------------------------------------------
/src/components/shell/TopBar/TopBar.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Link, useLocation } from "react-router-dom";
3 | import Icon from "react-crud-icons";
4 | import logoEvol from "./evologo.png";
5 | import { views, modelViewsMany } from "utils/dicoViews";
6 | import { getModel } from "utils/moMa";
7 | import { evoPath } from "utils/format";
8 | import ViewActions from "./ViewActions";
9 | import GitHubLink from "./GitHubLink";
10 |
11 | import "./TopBar.scss";
12 |
13 | const ovw = views.overview;
14 | const TopBar = () => {
15 | const loc = useLocation();
16 | const path = loc.pathname?.split("/");
17 | const [entity, view, id] = path.splice(2); // no access to useParams here
18 | const model = path[1] === evoPath ? getModel(entity) : null;
19 | const entityLink = `/${evoPath}/${entity}/`;
20 | return (
21 |
22 |
23 |
24 |
25 | {model && (
26 |
27 |
28 |
29 |
30 |
31 | {modelViewsMany(model, true).map((v) => (
32 |
33 |
34 |
35 | ))}
36 |
37 |
38 |
39 | )}
40 |
41 |
42 | );
43 | };
44 |
45 | export default TopBar;
46 |
--------------------------------------------------------------------------------
/src/components/views/ViewHeader/ViewHeader.scss:
--------------------------------------------------------------------------------
1 | @import "variables";
2 |
3 | .evo-page-header {
4 | position: relative;
5 | display: block;
6 | width: 100%;
7 | margin: 0 0 30px;
8 | .page-title {
9 | width: 100%;
10 | width: calc(100% - 90px);
11 | min-height: 40px;
12 | @include ellipsis();
13 | > .title-txt {
14 | margin-right: 5px;
15 | }
16 | .evo-badge {
17 | position: relative;
18 | top: -4px;
19 | }
20 | .h-txt {
21 | margin-left: 5px;
22 | font-size: $text-font-size;
23 | font-weight: normal;
24 | color: $color-text-secondary;
25 | }
26 | }
27 | .title-icons {
28 | position: absolute;
29 | top: 0;
30 | right: 0;
31 | display: inline-block;
32 | background-color: $bgcolor-background;
33 | > a {
34 | display: inline-block;
35 | border-radius: 50%;
36 | > .crud-icon {
37 | border: solid 1px transparent;
38 | > svg {
39 | fill: $color-icon;
40 | }
41 | > div {
42 | display: none;
43 | }
44 | }
45 | &.active > .crud-icon {
46 | border-color: $color-icon-active;
47 | cursor: default !important;
48 | > svg {
49 | fill: $color-text;
50 | }
51 | }
52 | &:hover {
53 | .crud-icon {
54 | > svg {
55 | fill: $color-icon-hover;
56 | }
57 | > div {
58 | display: block;
59 | }
60 | }
61 | }
62 | }
63 | }
64 |
65 | @media (max-width: $media-small) {
66 | .title-icons {
67 | display: none !important;
68 | }
69 | .page-title {
70 | width: 100%;
71 | }
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/src/components/Field/FieldLabel.jsx:
--------------------------------------------------------------------------------
1 | import React, { memo } from "react";
2 | import PropTypes from "prop-types";
3 | import { fieldPropTypes } from "components/views/modelPropTypes";
4 | import Tooltip from "rc-tooltip";
5 | import Icon from "react-crud-icons";
6 |
7 | import "./FieldLabel.scss";
8 |
9 | const FieldLabel = memo(({ field, label, required, readOnly }) => {
10 | const isRequired =
11 | (field.required || required) && !(field.readOnly || readOnly);
12 |
13 | return (
14 |
15 |
16 | {label || field.label}
17 | {isRequired && (
18 |
19 | *
20 |
21 | )}
22 | {field.help && (
23 | {field.help}}
27 | >
28 |
29 |
30 |
31 |
32 | )}
33 |
34 |
35 | );
36 | });
37 |
38 | export default FieldLabel;
39 |
40 | FieldLabel.propTypes = {
41 | /** Field metadata w/ label, required, readOnly, help props */
42 | field: fieldPropTypes.isRequired,
43 | /** Override for field.label */
44 | label: PropTypes.string,
45 | /** Override for field.required */
46 | required: PropTypes.bool,
47 | /** Override for field.readOnly */
48 | readOnly: PropTypes.bool,
49 | };
50 |
51 | FieldLabel.defaultProps = {
52 | label: null,
53 | required: false,
54 | readOnly: false,
55 | };
56 |
--------------------------------------------------------------------------------
/src/components/widgets/Panel/Panel.test.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable testing-library/await-async-query */
2 | /* eslint-disable testing-library/no-node-access */
3 | import { render, screen } from "@testing-library/react";
4 | import Panel from "./Panel";
5 | import renderer from "react-test-renderer";
6 |
7 | describe("panel props test", () => {
8 | let panelToTest;
9 | const props = {
10 | title: "my panel",
11 | collapsible: true,
12 | width: 120,
13 | header: null,
14 | footer: null,
15 | children: null,
16 | className: null,
17 | };
18 | beforeEach(() => {
19 | const testInstance = renderer.create( );
20 | panelToTest = testInstance.root;
21 | });
22 | it("should render title in h2", () => {
23 | const strongRender = panelToTest.findByType("h2");
24 | expect(strongRender.children).toEqual([props.title]);
25 | });
26 | });
27 |
28 | describe("panel widget tests", () => {
29 | it("test default props", () => {
30 | render( );
31 | const panel = screen.getByTestId("panel");
32 | expect(panel).toHaveTextContent("My Panel");
33 | });
34 | });
35 |
36 | describe("panel widget shows children", () => {
37 | it("test default props", () => {
38 | render(children );
39 | const panel = screen.getByTestId("panel");
40 | expect(panel).toHaveTextContent("children");
41 | });
42 | });
43 |
44 | describe("panel widget shows header and footer", () => {
45 | it("test default props", () => {
46 | render( );
47 | const panel = screen.getByTestId("panel");
48 | expect(panel).toHaveTextContent("my head");
49 | expect(panel).toHaveTextContent("my foot");
50 | });
51 | });
52 |
--------------------------------------------------------------------------------
/src/components/views/comfort/Overview/Activity.jsx:
--------------------------------------------------------------------------------
1 | // #region ---------------- Imports ----------------
2 | import React from "react";
3 | import { Link } from "react-router-dom";
4 | import Icon from "react-crud-icons";
5 | import config from "config";
6 | import { pixPath } from "utils/format";
7 | import { getModel } from "utils/moMa";
8 | import { getActivity } from "utils/activity";
9 | import { views } from "utils/dicoViews";
10 | import { i18n_activity } from "i18n/i18n";
11 | // #endregion
12 |
13 | import "./Activity.scss";
14 |
15 | const { withActivity } = config;
16 |
17 | const Overview = ({ entity }) => {
18 | if (!withActivity) {
19 | return null;
20 | }
21 | const m = getModel(entity);
22 | const activityData = getActivity(entity);
23 | const iconPath = pixPath + m.icon;
24 | const urlBegin = `../${entity}/`;
25 | const viewLink = (v) => (
26 |
27 |
28 | {v.label}
29 |
30 | );
31 | const activityLink = ({ id, title }) => (
32 |
33 |
34 | {title}
35 |
36 | );
37 |
38 | return (
39 |
40 |
{viewLink(views.activity)}
41 |
{i18n_activity.mostViewed.replace("{0}", m.namePlural)}:
42 |
43 | {activityData?.mostViewed?.map(activityLink)}
44 |
45 |
46 |
{i18n_activity.lastViewed.replace("{0}", m.namePlural)}:
47 |
48 | {activityData?.lastViewed?.slice(0, 10).map(activityLink)}
49 |
50 |
51 | );
52 | };
53 |
54 | export default Overview;
55 |
--------------------------------------------------------------------------------
/src/models/music/artist.js:
--------------------------------------------------------------------------------
1 | /*
2 | Evolutility UI model for Artists
3 | https://github.com/evoluteur/evolutility-ui-react
4 | */
5 |
6 | const modelArtist = {
7 | id: "artist",
8 | qid: "music_artist",
9 | title: "Artists",
10 | world: "music",
11 | name: "artist",
12 | namePlural: "artists",
13 | icon: "star.png",
14 | defaultViewMany: "cards",
15 | titleField: "name",
16 | fields: [
17 | {
18 | id: "name",
19 | type: "text",
20 | label: "Name",
21 | required: true,
22 | inMany: true,
23 | inSearch: true,
24 | },
25 | {
26 | id: "url",
27 | type: "url",
28 | label: "Web site",
29 | width: 70,
30 | },
31 | {
32 | id: "bdate",
33 | type: "date",
34 | label: "Birth date",
35 | width: 30,
36 | },
37 | {
38 | id: "photo",
39 | type: "image",
40 | label: "Photo",
41 | inMany: true,
42 | width: 100,
43 | },
44 | {
45 | id: "description",
46 | type: "textmultiline",
47 | label: "Description",
48 | height: 9,
49 | inSearch: true,
50 | },
51 | ],
52 | groups: [
53 | {
54 | id: "g1",
55 | type: "panel",
56 | label: "Artist",
57 | width: 70,
58 | fields: ["name", "url", "bdate", "description"],
59 | },
60 | {
61 | id: "g2",
62 | type: "panel",
63 | label: "Photo",
64 | width: 30,
65 | fields: ["photo"],
66 | },
67 | ],
68 | collections: [
69 | {
70 | id: "albums",
71 | title: "Albums",
72 | object: "album",
73 | column: "artist_id",
74 | order: "title",
75 | icon: "cd.png",
76 | fields: ["title", "cover", "length"],
77 | },
78 | ],
79 | noCharts: true,
80 | };
81 |
82 | export default modelArtist;
83 |
--------------------------------------------------------------------------------
/src/components/views/ViewHeader/ViewsNavIcons.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 | import { Link } from "react-router-dom";
4 | import Icon from "react-crud-icons";
5 | import { getModel } from "utils/moMa";
6 | import { views, modelViewsAnalytics } from "utils/dicoViews";
7 |
8 | const ViewsNavIcons = ({ id, view, entity, params }) => {
9 | let iconViews = [];
10 | if (view === "overview" || view === "activity") {
11 | return null;
12 | }
13 | if (view === "edit" || view === "browse") {
14 | if (!id || id === "0") {
15 | return null;
16 | } else {
17 | iconViews = [views.edit, views.browse];
18 | }
19 | } else if (view === "stats" || view === "charts") {
20 | const model = getModel(entity);
21 | iconViews = modelViewsAnalytics(model);
22 | // } else if (view === "list" || view === "cards") {
23 | } else {
24 | iconViews = [views.list, views.cards];
25 | }
26 |
27 | const urlFrag = (id ? "/" + id : "") + params;
28 | const iconLink = (ico) => (
29 |
35 |
36 |
37 | );
38 |
39 | return {iconViews?.map(iconLink)}
;
40 | };
41 |
42 | ViewsNavIcons.propTypes = {
43 | /** Model id */
44 | entity: PropTypes.string.isRequired,
45 | /** Active view */
46 | view: PropTypes.string,
47 | /** Record id */
48 | id: PropTypes.string,
49 | // Extra parameters (url search)
50 | params: PropTypes.string,
51 | };
52 |
53 | ViewsNavIcons.defaultProps = {
54 | view: null,
55 | id: null,
56 | params: "",
57 | };
58 |
59 | export default ViewsNavIcons;
60 |
--------------------------------------------------------------------------------
/src/components/views/one/shared/Collection/Collection.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 |
4 | import TableBody from "components/views/many/shared/TableBody/TableBody";
5 | // import EmptyState from "components/views/many/EmptyState";
6 |
7 | import "./Collection.scss";
8 |
9 | const Collection = ({ collecModel, collecData }) => {
10 | const isEmpty = !(collecData && collecData?.length > 0);
11 | if (isEmpty) {
12 | return (
13 |
16 | );
17 | // return ;
18 | }
19 | const link = "/" + (collecModel.object || collecModel.id) + "/browse/";
20 | const tableHeader = (
21 |
22 |
23 | {collecModel.fields.map((f) => (
24 | {f.labelShort || f.label}
25 | ))}
26 |
27 |
28 | );
29 |
30 | return (
31 |
32 |
33 | {tableHeader}
34 |
40 |
41 |
42 | );
43 | };
44 |
45 | export default Collection;
46 |
47 | Collection.propTypes = {
48 | collecModel: PropTypes.shape({
49 | fields: PropTypes.arrayOf(
50 | PropTypes.shape({
51 | id: PropTypes.string.isRequired,
52 | })
53 | ),
54 | }).isRequired,
55 | collecData: PropTypes.oneOfType([
56 | PropTypes.arrayOf(
57 | PropTypes.shape({
58 | id: PropTypes.number.isRequired,
59 | })
60 | ),
61 | PropTypes.shape({
62 | errors: PropTypes.arrayOf(PropTypes.shape()),
63 | }),
64 | ]),
65 | };
66 |
--------------------------------------------------------------------------------
/src/components/Field/Field.scss:
--------------------------------------------------------------------------------
1 | @import "variables";
2 |
3 | .form-control {
4 | padding: 6px;
5 | }
6 | .scroll-y {
7 | overflow-y: scroll;
8 | }
9 | .evol-fld {
10 | position: relative;
11 | @include flex-item();
12 | padding-left: $field-h-spacing !important;
13 | margin-bottom: 12px;
14 | min-width: 62px;
15 | input,
16 | textarea,
17 | select {
18 | font-size: $text-font-size;
19 | }
20 | .evo-rdonly {
21 | > img {
22 | position: relative;
23 | top: -2px;
24 | margin-right: 3px;
25 | }
26 | > pre {
27 | margin-top: 0;
28 | padding: 5px;
29 | font-size: 12px;
30 | background-color: #f5f5f5;
31 | border: solid 1px silver;
32 | border-radius: 5px;
33 | min-height: 38px;
34 | }
35 | }
36 | > div {
37 | width: 100%;
38 | }
39 |
40 | input[type="time"] {
41 | display: inline-block;
42 | width: 150px;
43 | }
44 | textarea {
45 | max-width: 100%;
46 | resize: vertical;
47 | }
48 | }
49 |
50 | .evo-fld-invalid {
51 | display: inline-block;
52 | border-radius: 2px;
53 | font-size: 12px;
54 | clear: both;
55 | background-color: #f2dede;
56 | color: #a94442;
57 | border: solid 1px #ebccd1;
58 | padding: 4px 5px 4px 10px;
59 | margin: 5px 5px 0 0;
60 | border-radius: $radius;
61 | }
62 |
63 | .evo-rdonly {
64 | min-height: 38px;
65 | padding-top: 5px;
66 | > a {
67 | display: block;
68 | @include ellipsis();
69 | }
70 | }
71 | div > .checkbox {
72 | position: relative;
73 | top: -7px;
74 | height: $icon-size;
75 | width: $icon-size;
76 | }
77 | .lov-icon {
78 | margin-right: 5px;
79 | }
80 | .evo-color-box {
81 | display: inline-block;
82 | height: 20px;
83 | width: 20px;
84 | border: $border;
85 | span {
86 | margin-left: 24px;
87 | }
88 | }
89 | .lov-wicon {
90 | > img {
91 | margin-right: 5px;
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/src/components/shell/SideBar/svg/cogs.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/models/organizer/winetasting.js:
--------------------------------------------------------------------------------
1 | /*
2 | Evolutility UI model for Wine tastings
3 | https://github.com/evoluteur/evolutility-ui-react
4 | */
5 |
6 | const model = {
7 | qid: "wine_tasting",
8 | id: "winetasting",
9 | title: "Wine tastings",
10 | world: "demos",
11 | name: "wine tasting",
12 | namePlural: "wine tastings",
13 | icon: "wine-glass.png",
14 | titleField: "drink_date",
15 | titleFunction: (d) => d?.wine?.name + " " + d?.drink_date,
16 | fields: [
17 | {
18 | id: "drink_date",
19 | type: "date",
20 | label: "Date",
21 | required: true,
22 | inMany: true,
23 | width: 38,
24 | },
25 | {
26 | id: "wine",
27 | type: "lov",
28 | label: "Wine",
29 | object: "winecellar",
30 | required: true,
31 | inMany: true,
32 | width: 62,
33 | },
34 | {
35 | id: "taste",
36 | type: "text",
37 | label: "Taste",
38 | maxLength: 100,
39 | inMany: true,
40 | width: 100,
41 | },
42 | {
43 | id: "robe",
44 | type: "text",
45 | label: "Robe",
46 | maxLength: 100,
47 | inMany: true,
48 | width: 100,
49 | },
50 | {
51 | id: "nose",
52 | type: "text",
53 | label: "Nose",
54 | maxLength: 100,
55 | inMany: true,
56 | width: 100,
57 | },
58 | {
59 | id: "notes",
60 | type: "textmultiline",
61 | label: "Note",
62 | inMany: true,
63 | inSearch: true,
64 | width: 100,
65 | height: 5,
66 | },
67 | ],
68 | groups: [
69 | {
70 | id: "p1",
71 | type: "panel",
72 | label: "Degustation",
73 | width: 62,
74 | fields: ["drink_date", "wine", "notes"],
75 | },
76 | {
77 | id: "p2",
78 | type: "panel",
79 | label: "Evaluation",
80 | width: 38,
81 | fields: ["taste", "robe", "nose"],
82 | },
83 | ],
84 | collections: [],
85 | };
86 |
87 | export default model;
88 |
--------------------------------------------------------------------------------
/TODO.md:
--------------------------------------------------------------------------------
1 | # Evolutility-UI-React
2 |
3 | # Roadmap
4 |
5 | - Live demo
6 |
7 | - Rethink metamodel (add transform, more separation b/w backend and front end, new "expression" prop for fieldTypes and FieldGroups, different field type for lov and object, maybe single lov table...
8 |
9 | - Make a JIRA style app to track evolutility development (and replace this list) or do it in GitHub.
10 |
11 | - Plug UI library? - thinking mantine or @adobe/react-spectrum...
12 | - search
13 | - filters
14 | - Add "comfort" models & views (saved filters, handpicked sets, customizable dashboards...)
15 | - Every field type can also be an array of values of that type
16 | - replace numeral and moment by Intl
17 | - Choice of UX pattern: Drawer / navigation / dual pane
18 | - translate in other languages (/src/i18n/XX.js)
19 | - Add checkboxes for multi-rows selection to the List and Cards views
20 | - Dark & light themes
21 | - Add JSON view (w/ react-json-view?)
22 | - Add "Compare" view for side-by-side comparaison and averages
23 | - Add "Kanban" view w/ drag & drop
24 | - Dependent fields
25 | - Integrate Designer inside each views
26 | - plug SWR or RTKQuery or TanStack Query
27 | - Add checkboxes for selection to the List and Cards views
28 | - Add filtering for List and Card views (and later for Groups)
29 | - Add "Clone" action
30 | - CSS for print
31 | - Theme Dark/Light, Comfortable/Compact
32 | - Better 404 page
33 | - Drawer for editing metadata
34 | - Add User settings & preferences (move most of config.js there)
35 | - a pluggin system for new Field Types or Views
36 | - Add sorting for Cards
37 | - Tooltip style Confirmation on delete.
38 | - Warning when leaving page w/ unsaved changes.
39 | - pluggins for views ( w/ single-spa?) + make Stats & Charts pluggins
40 | - Add Kaggle style table view
41 | - Use Yup for validation (maybe validation rules in json or keep the main ones in separate columns like now)
42 | - Adding tests
43 | - CI/CD pipelines on GitHub
44 | - scripts to run all tests on each model
45 | - json-schema to Evolutility models script
--------------------------------------------------------------------------------
/src/components/Field/browse/FieldValue.jsx:
--------------------------------------------------------------------------------
1 | import React, { memo } from "react";
2 | import Icon from "react-crud-icons";
3 | import {
4 | pixPath,
5 | filesUrl,
6 | dateString,
7 | timeString,
8 | jsonString,
9 | image,
10 | numFieldValue,
11 | } from "utils/format";
12 | import { fieldTypes as ft } from "utils/dico";
13 |
14 | import "../Field.scss";
15 |
16 | const FieldValue = memo(({ fieldDef, value, compact }) => {
17 | const f = fieldDef;
18 | const fType = f.type;
19 |
20 | if (fType === ft.bool) {
21 | return value ? : "";
22 | }
23 | if (fType === ft.int || fType === ft.dec || fType === ft.money) {
24 | return numFieldValue(f, value);
25 | }
26 | if (fType === ft.email && value) {
27 | return {value} ;
28 | }
29 | if (fType === ft.json && value) {
30 | return jsonString(value);
31 | }
32 | if (fType === ft.lov) {
33 | return (
34 |
35 | {f.lovIcon && value?.icon && (
36 |
37 | )}
38 | {value?.name}
39 |
40 | );
41 | }
42 | if (fType === ft.date) {
43 | return dateString(value);
44 | }
45 | if (fType === ft.time) {
46 | return timeString(value);
47 | }
48 | if (fType === ft.color) {
49 | return (
50 |
51 |
57 | {!compact && value && {value} }
58 |
59 |
60 | );
61 | }
62 | if (fType === ft.image && value) {
63 | return image(filesUrl + value);
64 | }
65 | if (fType === ft.url && value) {
66 | return (
67 |
68 | {value}
69 |
70 | );
71 | }
72 | return value;
73 | });
74 |
75 | export default FieldValue;
76 |
--------------------------------------------------------------------------------
/src/components/views/analytics/Charts/Pie.jsx:
--------------------------------------------------------------------------------
1 | // - Wrapper for @nivo ResponsivePie
2 |
3 | import React from "react";
4 | import { ResponsivePie } from "@nivo/pie";
5 | import chartPropTypes from "./chartProps";
6 | import { colors, labelColor, innerLabelColor } from "./chartOptions";
7 |
8 | const Pie = ({ data, showLegend = true }) => {
9 | const pData = data?.map((d) => ({
10 | id: d.label,
11 | _id: d.id,
12 | value: d.value,
13 | }));
14 | return (
15 |
16 |
56 |
57 | );
58 | };
59 |
60 | export default Pie;
61 |
62 | Pie.propTypes = chartPropTypes;
63 |
--------------------------------------------------------------------------------
/src/components/Field/Field.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 | import classnames from "classnames";
4 | import { fieldPropTypes } from "components/views/modelPropTypes";
5 | import FieldLabel from "./FieldLabel";
6 | import FieldElemEdit from "./edit/FieldElemEdit";
7 | import FieldElemBrowse from "./browse/FieldElemBrowse";
8 |
9 | import "./Field.scss";
10 |
11 | const Field = ({
12 | fieldDef,
13 | onChange,
14 | label,
15 | readOnly,
16 | icon,
17 | value,
18 | invalid,
19 | message,
20 | }) => {
21 | const fReadOnly = readOnly || fieldDef.readOnly;
22 |
23 | return (
24 |
28 |
33 | {fReadOnly ? (
34 |
35 | ) : (
36 |
37 | )}
38 | {invalid && message &&
{message}
}
39 |
40 | );
41 | };
42 |
43 | export default Field;
44 |
45 | Field.propTypes = {
46 | /** Field metadata */
47 | fieldDef: fieldPropTypes.isRequired,
48 | /** Callback functions for changed field value */
49 | onChange: PropTypes.func,
50 | /** Field value (object or scalar values depending on field type) */
51 | value: PropTypes.any,
52 | /** Field label (override label in fieldDef) */
53 | label: PropTypes.string,
54 | /** Field readOnly (override readOnly in fieldDef) */
55 | readOnly: PropTypes.bool,
56 | /** Field icon (only for lov fields) */
57 | icon: PropTypes.string,
58 | /** Validation error message */
59 | message: PropTypes.string,
60 | };
61 |
62 | Field.defaultProps = {
63 | value: null,
64 | label: null,
65 | readOnly: null,
66 | icon: null,
67 | message: null,
68 | onChange: null,
69 | };
70 |
--------------------------------------------------------------------------------
/src/components/shell/Footer/Footer.scss:
--------------------------------------------------------------------------------
1 | @import "variables";
2 |
3 | .evo-footer {
4 | z-index: 100;
5 | margin: 10px 0 0 180px;
6 | padding: 10px 10px 20px;
7 | color: $color-text-footer;
8 | text-align: center;
9 | font-size: 0.8em;
10 | > div > a {
11 | color: black;
12 | }
13 | > .copyright {
14 | display: block;
15 | margin-top: 10px;
16 | color: silver;
17 | color: $color-text-footer !important;
18 | }
19 | > * {
20 | margin-top: 10px !important;
21 | }
22 | }
23 |
24 | @media print {
25 | .evo-footer {
26 | font-size: 0.6em;
27 | page-break-inside: avoid;
28 | }
29 | }
30 |
31 | .heart {
32 | color: silver;
33 | font-size: 20px;
34 | position: relative;
35 | top: 4px;
36 | display: inline-block;
37 | text-align: center;
38 | transition: color 1s;
39 | }
40 | .evo-footer:hover .heart {
41 | color: #bf360c;
42 | -webkit-animation: beat 0.35s infinite alternate;
43 | -moz-animation: beat 0.35s infinite alternate;
44 | -ms-animation: beat 0.35s infinite alternate;
45 | -o-animation: beat 0.35s infinite alternate;
46 | animation: beat 0.35s infinite alternate;
47 | -webkit-transform-origin: center;
48 | -moz-transform-origin: center;
49 | -o-transform-origin: center;
50 | -ms-transform-origin: center;
51 | transform-origin: center;
52 | }
53 |
54 | @keyframes beat {
55 | to {
56 | -webkit-transform: scale(1.4);
57 | -moz-transform: scale(1.4);
58 | -o-transform: scale(1.4);
59 | -ms-transform: scale(1.4);
60 | transform: scale(1.4);
61 | }
62 | }
63 |
64 | @-moz-keyframes beat {
65 | to {
66 | -moz-transform: scale(1.4);
67 | transform: scale(1.4);
68 | }
69 | }
70 |
71 | @-webkit-keyframes beat {
72 | to {
73 | -webkit-transform: scale(1.4);
74 | transform: scale(1.4);
75 | }
76 | }
77 |
78 | @-ms-keyframes beat {
79 | to {
80 | -ms-transform: scale(1.4);
81 | transform: scale(1.4);
82 | }
83 | }
84 |
85 | @-o-keyframes beat {
86 | to {
87 | -o-transform: scale(1.4);
88 | transform: scale(1.4);
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/src/components/views/ViewHeader/ViewHeader.jsx:
--------------------------------------------------------------------------------
1 | import React, { memo } from "react";
2 | import PropTypes from "prop-types";
3 | import Badge from "components/widgets/Badge/Badge";
4 | import ViewsNavIcons from "./ViewsNavIcons";
5 | import FilterTags from "./FilterTags";
6 |
7 | import "./ViewHeader.scss";
8 |
9 | // TODO: make charts work w/ search & filters (and switch comment below)
10 | const ViewHeader = memo(
11 | ({ entity, title, id, view, count, comments, text, params }) => {
12 | const search = (view === "list" || view === "cards") && (
13 |
14 | );
15 | return (
16 |
17 |
18 | {title}
19 | {count !== null && }
20 | {comments > 0 && (
21 |
24 | )}
25 | {search}
26 | {text && {text} }
27 |
28 |
29 |
30 |
31 |
32 | );
33 | }
34 | );
35 |
36 | export default ViewHeader;
37 |
38 | ViewHeader.propTypes = {
39 | /** Model id */
40 | entity: PropTypes.string.isRequired,
41 | /** Page title */
42 | title: PropTypes.string, //.isRequired,
43 | /** Record id */
44 | id: PropTypes.string,
45 | /** Active view */
46 | view: PropTypes.string,
47 | /** Number of records (for views "many") */
48 | count: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
49 | /** Number of comments (for views "one") */
50 | comments: PropTypes.number,
51 | /** Extra text beside the title */
52 | text: PropTypes.string,
53 | /** extra parameters (filters in location search) */
54 | params: PropTypes.string,
55 | };
56 |
57 | ViewHeader.defaultProps = {
58 | view: null,
59 | count: null,
60 | comments: null,
61 | text: null,
62 | params: "",
63 | };
64 |
--------------------------------------------------------------------------------
/src/components/views/many/Cards/Cards.scss:
--------------------------------------------------------------------------------
1 | @import "variables";
2 |
3 | .evol-cards {
4 | display: flex;
5 | flex-wrap: wrap;
6 | gap: $gap;
7 | width: 100%;
8 | > .panel {
9 | flex-grow: 1;
10 | position: relative;
11 | width: 240px;
12 | min-width: 240px;
13 | padding: 10px 20px;
14 | overflow-x: hidden;
15 | &:hover {
16 | border-color: $color-border-hover;
17 | .card-actions {
18 | right: 0;
19 | visibility: visible;
20 | }
21 | }
22 | > div {
23 | padding: 3px 0;
24 | min-height: 30px;
25 | > h2,
26 | > label {
27 | @include ellipsis();
28 | }
29 | > h2 {
30 | margin: 0;
31 | min-height: 1.2em;
32 | font-size: 20px;
33 | font-weight: bold;
34 | }
35 | > label {
36 | width: $golden-ratio-small;
37 | max-width: 120px;
38 | vertical-align: top;
39 | font-weight: 300;
40 | }
41 | > div:not(.card-actions) {
42 | display: inline-block;
43 | padding-right: 5px;
44 | width: $golden-ratio-big;
45 | vertical-align: top;
46 | > a {
47 | display: inline-block;
48 | @include ellipsis();
49 | width: 100%;
50 | }
51 | }
52 | > img {
53 | margin-top: 5px;
54 | }
55 | }
56 | }
57 | }
58 |
59 | .card-title {
60 | margin-bottom: 20px;
61 | .card-actions {
62 | z-index: 50;
63 | position: absolute;
64 | top: 0;
65 | right: -50px;
66 | visibility: hidden;
67 | padding: 0 4px 4px 4px;
68 | background-color: $bgcolor-background;
69 | border: 1px solid $bgcolor-background;
70 | border-left-color: $color-border;
71 | border-bottom-color: $color-border;
72 | border-bottom-left-radius: $radius;
73 | transition:
74 | visibility $ease-time ease $delay-time,
75 | right $ease-time ease $delay-time;
76 | @include icons-actions();
77 | i {
78 | margin: 5px 0 0 0;
79 | }
80 | }
81 | }
82 | .card-fld-center {
83 | margin-top: 5px;
84 | text-align: center;
85 | }
86 |
--------------------------------------------------------------------------------
/src/pages/Docs/Views.jsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable jsx-a11y/anchor-is-valid */
2 | /* eslint-disable jsx-a11y/anchor-has-content */
3 | import React, { useEffect } from "react";
4 | import { pixPath } from "utils/format";
5 | import Icon from "react-crud-icons";
6 | import { viewDoc } from "./docMetadata";
7 |
8 | import "./Doc.scss";
9 | import "./Views.scss";
10 |
11 | const view = (v) => (
12 |
13 |
16 |
17 |
18 | { }
19 | {v.name}
20 |
21 |
{v.description}
22 |
Route: "{v.route}"
23 |
24 |
25 |
30 |
31 |
32 |
33 | );
34 |
35 | const viewsSet = (fam) => {
36 | const vs = viewDoc[fam];
37 | return <>{vs.map((v) => view(v, fam))}>;
38 | };
39 |
40 | const Views = () => {
41 | useEffect(() => {
42 | document.title = "Doc > Views";
43 | window.scrollTo(0, 0);
44 | }, []);
45 |
46 | return (
47 |
48 |
Views
49 |
50 | Evolutility-UI-React provides different types of model-driven view (as
51 | React components).
52 |
53 |
54 |
55 | For each object, all views are defined by a single UI model in a simple
56 | declarative way.
57 |
58 |
59 |
60 |
61 | Views for One object
62 |
63 | {viewsSet("one")}
64 |
65 |
66 |
67 |
68 | Views for Many objects
69 |
70 | {viewsSet("many")}
71 |
72 |
73 |
74 | "Comfort" Views
75 | {viewsSet("comfort")}
76 |
77 |
78 | );
79 | };
80 |
81 | export default Views;
82 |
--------------------------------------------------------------------------------
/src/models/music/album.js:
--------------------------------------------------------------------------------
1 | /*
2 | Evolutility UI model for Albums
3 | https://github.com/evoluteur/evolutility-ui-react
4 | */
5 |
6 | const modelAlbum = {
7 | id: "album",
8 | qid: "music_album",
9 | title: "Albums",
10 | world: "music",
11 | name: "album",
12 | namePlural: "albums",
13 | icon: "cd.png",
14 | defaultViewMany: "cards",
15 | titleField: "title",
16 | fields: [
17 | {
18 | id: "title",
19 | type: "text",
20 | label: "Title",
21 | required: true,
22 | inMany: true,
23 | inSearch: true,
24 | width: 62,
25 | },
26 | {
27 | id: "artist",
28 | type: "lov",
29 | label: "Artist",
30 | object: "artist",
31 | aggregate: "albums_aggregate",
32 | required: true,
33 | inMany: true,
34 | width: 38,
35 | },
36 | {
37 | id: "url",
38 | type: "url",
39 | label: "Amazon",
40 | width: 62,
41 | },
42 | {
43 | id: "length",
44 | type: "text",
45 | label: "Length",
46 | inMany: true,
47 | width: 38,
48 | },
49 | {
50 | id: "description",
51 | type: "textmultiline",
52 | label: "Description",
53 | maxLength: 1000,
54 | inMany: false,
55 | inSearch: true,
56 | width: 100,
57 | height: 8,
58 | },
59 | {
60 | id: "cover",
61 | type: "image",
62 | label: "Cover",
63 | inMany: true,
64 | width: 100,
65 | },
66 | ],
67 | groups: [
68 | {
69 | id: "p-album",
70 | type: "panel",
71 | label: "Album",
72 | width: 70,
73 | fields: ["title", "artist", "url", "length", "description"],
74 | },
75 | {
76 | id: "p-cover",
77 | type: "panel",
78 | label: "Cover",
79 | width: 30,
80 | fields: ["cover"],
81 | },
82 | ],
83 | collections: [
84 | {
85 | id: "tracks",
86 | title: "Tracks",
87 | object: "track",
88 | column: "album_id",
89 | order: "name",
90 | icon: "music.png",
91 | fields: ["name", "genre", "length"],
92 | },
93 | ],
94 | };
95 |
96 | export default modelAlbum;
97 |
--------------------------------------------------------------------------------
/src/utils/activity.js:
--------------------------------------------------------------------------------
1 | import config from "config";
2 | import { lcWrite, lcRead, lcRemove } from "./localStorage";
3 |
4 | // TODO: do not show deleted records
5 | // TODO: persist in DB?
6 | const { withActivity, activityListSize } = config;
7 |
8 | export const logActivity = (modelId, recordId, recordTitle, actionId) => {
9 | if (withActivity) {
10 | if (recordTitle && modelId && recordId) {
11 | const key = `${modelId}-activity`;
12 | const now = new Date();
13 | const date =
14 | now.toLocaleDateString() +
15 | " " +
16 | now.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
17 | const v = {
18 | id: recordId,
19 | title: recordTitle,
20 | action: actionId,
21 | date,
22 | visits: 1,
23 | };
24 | let activity = lcRead(key);
25 | if (activity) {
26 | activity = JSON.parse(activity);
27 | const prevActivity = activity.find((a) => a.id === recordId);
28 | const visits = prevActivity?.visits;
29 | if (visits) {
30 | v.visits += visits;
31 | }
32 | activity = activity.filter((a) => a.id !== recordId);
33 | activity.unshift(v);
34 | activity = activity.slice(0, activityListSize || 50);
35 | } else {
36 | activity = [v];
37 | }
38 | lcWrite(key, JSON.stringify(activity));
39 | }
40 | }
41 | return null;
42 | };
43 |
44 | const mostViewed = (activity, max = 5) =>
45 | activity
46 | .filter((a) => a.visits > 1)
47 | .sort((a, b) => b.visits - a.visits)
48 | .slice(0, max);
49 |
50 | export const getActivity = (modelId) => {
51 | const key = `${modelId}-activity`;
52 | let activity = lcRead(key);
53 | if (activity) {
54 | activity = JSON.parse(activity);
55 | return {
56 | lastViewed: activity,
57 | mostViewed: mostViewed(activity),
58 | firstActivityDate: activity[0]?.date || null,
59 | lastActivityDate: activity[activity.length - 1]?.date || null,
60 | };
61 | }
62 | return null;
63 | };
64 |
65 | export const clearActivity = (modelId) => {
66 | lcRemove(`${modelId}-activity`);
67 | };
68 |
--------------------------------------------------------------------------------
/src/models/music/track.js:
--------------------------------------------------------------------------------
1 | /*
2 | Evolutility UI model for Tracks
3 | https://github.com/evoluteur/evolutility-ui-react
4 | */
5 |
6 | const modelTrack = {
7 | id: "track",
8 | qid: "music_track",
9 | title: "Tracks",
10 | world: "music",
11 | name: "track",
12 | namePlural: "tracks",
13 | icon: "music.png",
14 | titleField: "name",
15 | fields: [
16 | {
17 | id: "name",
18 | type: "text",
19 | label: "Name",
20 | required: true,
21 | inMany: true,
22 | inSearch: true,
23 | width: 100,
24 | height: 3,
25 | },
26 | {
27 | id: "album",
28 | type: "lov",
29 | label: "Album",
30 | object: "album",
31 | aggregate: "tracks_aggregate",
32 | inMany: true,
33 | width: 100,
34 | height: 1,
35 | },
36 | {
37 | id: "length",
38 | type: "text",
39 | label: "Length",
40 | inMany: true,
41 | width: 38,
42 | },
43 | {
44 | id: "genre",
45 | type: "lov",
46 | chartObject: "music_genre",
47 | aggregate: "tracks_aggregate",
48 | label: "Genre",
49 | list: [
50 | {
51 | id: 1,
52 | text: "Blues",
53 | },
54 | {
55 | id: 2,
56 | text: "Classical",
57 | },
58 | {
59 | id: 3,
60 | text: "Country",
61 | },
62 | {
63 | id: 4,
64 | text: "Electronic",
65 | },
66 | {
67 | id: 5,
68 | text: "Folk",
69 | },
70 | {
71 | id: 6,
72 | text: "Jazz",
73 | },
74 | {
75 | id: 7,
76 | text: "New age",
77 | },
78 | {
79 | id: 8,
80 | text: "Reggae",
81 | },
82 | {
83 | id: 9,
84 | text: "Soul",
85 | },
86 | ],
87 | inMany: true,
88 | width: 62,
89 | },
90 | {
91 | id: "description",
92 | type: "textmultiline",
93 | label: "Description",
94 | height: 3,
95 | inSearch: true,
96 | },
97 | ],
98 | };
99 |
100 | export default modelTrack;
101 |
--------------------------------------------------------------------------------
/src/pages/Docs/Doc.jsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable jsx-a11y/anchor-is-valid */
2 | /* eslint-disable jsx-a11y/anchor-has-content */
3 | /*
4 | Evolutility-UI-React
5 | https://github.com/evoluteur/evolutility-ui-react
6 | (c) 2023 Olivier Giulieri
7 | */
8 |
9 | import React, { useEffect } from "react";
10 | import { Link } from "react-router-dom";
11 | import ProjectBadges from "./components/ProjectBadges";
12 | import { docMenus } from "components/shell/SideBar/appMenus";
13 |
14 | import "./Doc.scss";
15 |
16 | const Doc = () => {
17 | useEffect(() => {
18 | document.title = "Documentation";
19 | window.scrollTo(0, 0);
20 | });
21 |
22 | return (
23 |
24 |
25 |
Documentation
26 |
27 |
28 |
29 |
30 |
31 | Evolutility is a model-driven UI for GraphQL. With it you can easily
32 | build modern SPAs by writing models rather than code.
33 |
34 | Table of Contents
35 |
36 | {docMenus.map((m) => (
37 |
38 | {m.text}
39 |
40 | ))}
41 |
42 |
43 |
44 |
45 |
46 | It's still a work in progress. To suggest a feature or report a bug:{" "}
47 |
53 | https://github.com/evoluteur/evolutility-ui-react/issues
54 |
55 |
56 |
57 | Evolutility-UI-React is released under the{" "}
58 |
64 | AGPL license
65 |
66 | .
67 |
68 |
69 |
70 | );
71 | };
72 |
73 | export default Doc;
74 |
--------------------------------------------------------------------------------
/src/utils/format.test.js:
--------------------------------------------------------------------------------
1 | import {
2 | integerString,
3 | decimalString,
4 | moneyString,
5 | capitalize,
6 | xItemsCount,
7 | } from "./format";
8 |
9 | test("check Integer formatting", () => {
10 | expect(integerString(-1000)).toEqual("-1,000");
11 | expect(integerString(-10)).toEqual("-10");
12 | expect(integerString(0)).toEqual("0");
13 | expect(integerString(1)).toEqual("1");
14 | expect(integerString(10)).toEqual("10");
15 | expect(integerString(1000)).toEqual("1,000");
16 | });
17 |
18 | test("check Decimal formatting", () => {
19 | expect(decimalString(-1.2)).toEqual("-1.20");
20 | expect(decimalString(-1.124)).toEqual("-1.12");
21 | expect(decimalString(-1.125)).toEqual("-1.12");
22 | expect(decimalString(-1.126)).toEqual("-1.13");
23 | expect(decimalString(0)).toEqual("0.00");
24 | expect(decimalString(1.1)).toEqual("1.10");
25 | expect(decimalString(1.12)).toEqual("1.12");
26 | expect(decimalString(1.124)).toEqual("1.12");
27 | expect(decimalString(1.125)).toEqual("1.13");
28 | expect(decimalString(1.126)).toEqual("1.13");
29 | expect(decimalString(10)).toEqual("10.00");
30 | expect(decimalString(101.5)).toEqual("101.50");
31 | expect(decimalString(1000)).toEqual("1,000.00");
32 | });
33 |
34 | test("check Money formatting", () => {
35 | expect(moneyString(-1.2)).toEqual("-$1.20");
36 | expect(moneyString(-1.124)).toEqual("-$1.12");
37 | expect(moneyString(-1.125)).toEqual("-$1.12");
38 | expect(moneyString(-1.126)).toEqual("-$1.13");
39 | expect(moneyString(0)).toEqual("$0.00");
40 | expect(moneyString(1.1)).toEqual("$1.10");
41 | expect(moneyString(1.12)).toEqual("$1.12");
42 | expect(moneyString(1.124)).toEqual("$1.12");
43 | expect(moneyString(1.125)).toEqual("$1.13");
44 | expect(moneyString(1.126)).toEqual("$1.13");
45 | expect(moneyString(10)).toEqual("$10.00");
46 | expect(moneyString(101.5)).toEqual("$101.50");
47 | expect(moneyString(1000)).toEqual("$1,000.00");
48 | });
49 |
50 | test("check capitalize", () => {
51 | expect(capitalize("aaa")).toEqual("Aaa");
52 | expect(capitalize("aaa bb")).toEqual("Aaa bb");
53 | expect(capitalize("Aaa")).toEqual("Aaa");
54 | });
55 |
56 | test("check xItemsCount", () => {
57 | expect(xItemsCount(0, "a", "as")).toEqual("No as");
58 | expect(xItemsCount(1, "a", "as")).toEqual("1 a");
59 | expect(xItemsCount(2, "a", "as")).toEqual("2 as");
60 | });
61 |
--------------------------------------------------------------------------------
/src/components/widgets/Panel/Panel.jsx:
--------------------------------------------------------------------------------
1 | // Evolutility-UI-React :: /widget/Panel.js
2 |
3 | // Panel to group fields in views Edit and Browse (styled w/ Bootstrap).
4 |
5 | // https://github.com/evoluteur/evolutility-ui-react
6 | // (c) 2023 Olivier Giulieri
7 |
8 | import React, { useState } from "react";
9 | import PropTypes from "prop-types";
10 | import Icon from "react-crud-icons";
11 | import classnames from "classnames";
12 |
13 | import "./Panel.scss";
14 |
15 | const Panel = ({
16 | title,
17 | header,
18 | width,
19 | collapsible,
20 | children,
21 | footer,
22 | className,
23 | }) => {
24 | const [isCollapsed, setIsCollapsed] = useState(false);
25 |
26 | const clickToggle = () => {
27 | setIsCollapsed(!isCollapsed);
28 | };
29 |
30 | return (
31 |
37 |
38 | {title && (
39 |
40 | {collapsible && (
41 |
47 | )}
48 |
{title}
49 |
50 | )}
51 | {header &&
{header}
}
52 | {children}
53 | {footer &&
{footer}
}
54 |
55 |
56 | );
57 | };
58 |
59 | export default Panel;
60 |
61 | Panel.propTypes = {
62 | /** Panel title */
63 | title: PropTypes.string,
64 | /** Panel width (% width of parent) */
65 | width: PropTypes.number,
66 | /** Panel can be collapsed */
67 | collapsible: PropTypes.bool,
68 | /** Panel header */
69 | header: PropTypes.node,
70 | /** Panel footer */
71 | footer: PropTypes.node,
72 | /** Panel content */
73 | children: PropTypes.node,
74 | /** Optional additional CSS class name */
75 | className: PropTypes.string,
76 | };
77 |
78 | Panel.defaultProps = {
79 | title: null,
80 | collapsible: false,
81 | width: 100,
82 | header: null,
83 | footer: null,
84 | children: null,
85 | className: null,
86 | };
87 |
--------------------------------------------------------------------------------
/src/variables.scss:
--------------------------------------------------------------------------------
1 | $color-almostwhite: #fcfcfc;
2 | $color-almostblack: #0d0d0d;
3 |
4 | $bgcolor-main: $color-almostwhite;
5 | $bgcolor-background: white;
6 | $bgcolor-header: #fbfbfb;
7 | $bgcolor-badge: #ebebeb;
8 |
9 | $color-text: $color-almostblack;
10 | $color-text-secondary: grey;
11 | $color-label: grey;
12 | $color-border: #ddd;
13 | $color-border-hover: silver;
14 | $color-disabled: silver;
15 |
16 | $bgcolor-panel-header: $bgcolor-header;
17 | $bgcolor-panel-footer: $bgcolor-header;
18 |
19 | $color-btn-primary: #337ab7;
20 | $color-btn-primary-border: #2e6da4;
21 | $color-icon-active: $color-border;
22 |
23 | $color-blue1: #259dcb;
24 | $color-red: #b94a48;
25 |
26 | $border: solid 1px $color-border;
27 | $gap: 10px;
28 |
29 | $text-font-size: 16px;
30 | $label-font-size: 14px;
31 |
32 | $color-text-footer: #607d8b;
33 |
34 | // golden ratio
35 | $golden-ratio-big: 62%;
36 | $golden-ratio-small: 38%;
37 |
38 | // top nav bar including toolbar
39 | $color-nav: #bbdefb; //#23527c;//#FFE0B2;
40 | $color-nav-hover: white; //#337ab7; //#E0F7FA;
41 | $color-nav-active: #e0f7fa;
42 | $color-icon-hover: #0d47a1;
43 | $color-icon: #428bca;
44 | $color-help-icon: silver;
45 |
46 | $color-link: #337ab7;
47 | $color-link-hover: #23527c;
48 |
49 | $color-svg-hover: $color-blue1;
50 |
51 | $ease-time: 0.2s;
52 | $delay-time: 0.2s;
53 | $delay-time2: 0.6s;
54 |
55 | $field-h-spacing: 16px;
56 |
57 | $radius: 12px;
58 | $media-small: 600px;
59 |
60 | $icon-size: 16px;
61 |
62 | @mixin flex-holder() {
63 | display: flex;
64 | flex-wrap: wrap;
65 | // width: 100%;
66 | }
67 | @mixin flex-item() {
68 | box-sizing: border-box;
69 | flex-grow: 1;
70 | }
71 | @mixin ellipsis() {
72 | white-space: nowrap;
73 | overflow-x: hidden;
74 | text-overflow: ellipsis;
75 | }
76 |
77 | @mixin link-focus {
78 | display: inline-block;
79 | outline: none;
80 | &:focus {
81 | svg {
82 | fill: $color-svg-hover;
83 | }
84 | }
85 | }
86 |
87 | @mixin icons-actions {
88 | svg {
89 | fill: $color-icon;
90 | cursor: pointer;
91 | &:hover {
92 | fill: $color-icon-hover;
93 | }
94 | &.active {
95 | fill: $color-almostblack;
96 | cursor: default;
97 | }
98 | }
99 | &.sort {
100 | position: absolute;
101 | top: 30px;
102 | right: 5px;
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/src/components/widgets/Button/Button.scss:
--------------------------------------------------------------------------------
1 | // copied and modified from bootstrap-sass v3.4.1
2 |
3 | @import "variables";
4 |
5 | .btn {
6 | display: inline-block;
7 | margin-bottom: 0;
8 | font-weight: 600;
9 | // white-space: nowrap;
10 | vertical-align: middle;
11 | touch-action: manipulation;
12 | cursor: pointer;
13 | border: 1px solid rgba(0, 0, 0, 0);
14 | font-size: $text-font-size;
15 | border-radius: 20px;
16 | padding: 7px 20px;
17 | line-height: 1.428571429;
18 | -webkit-user-select: none;
19 | -moz-user-select: none;
20 | -ms-user-select: none;
21 | user-select: none;
22 | > svg {
23 | position: relative;
24 | top: 2px;
25 | height: $icon-size;
26 | margin-right: 10px;
27 | }
28 | &:hover,
29 | &:focus {
30 | color: #333;
31 | text-decoration: none;
32 | }
33 | &:active {
34 | background-image: none;
35 | outline: 0;
36 | box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
37 | }
38 | }
39 |
40 | .btn.disabled,
41 | .btn[disabled],
42 | fieldset[disabled] .btn {
43 | cursor: not-allowed;
44 | opacity: 0.65;
45 | box-shadow: none;
46 | }
47 | a.btn.disabled,
48 | fieldset[disabled] a.btn {
49 | pointer-events: none;
50 | }
51 | .btn-default {
52 | color: #333;
53 | background-color: #fff;
54 | border-color: #ccc;
55 | > svg {
56 | fill: #333 !important;
57 | }
58 | &:focus,
59 | &:hover,
60 | &:active {
61 | color: #333;
62 | background-color: #e6e6e6;
63 | border-color: #8c8c8c;
64 | }
65 | }
66 | .btn-default.disabled:hover,
67 | .btn-default.disabled:focus,
68 | .btn-default[disabled]:hover,
69 | .btn-default[disabled]:focus,
70 | fieldset[disabled] .btn-default:hover,
71 | fieldset[disabled] .btn-default:focus {
72 | background-color: #fff;
73 | border-color: #ccc;
74 | }
75 | .btn-primary {
76 | color: #fff;
77 | background-color: $color-btn-primary;
78 | border-color: $color-btn-primary-border;
79 | > svg {
80 | fill: #fff;
81 | }
82 | &:focus,
83 | &:hover,
84 | &:active {
85 | color: #fff;
86 | background-color: #286090;
87 | border-color: #122b40;
88 | }
89 | }
90 | .btn-primary.disabled:hover,
91 | .btn-primary.disabled:focus,
92 | .btn-primary[disabled]:hover,
93 | .btn-primary[disabled]:focus,
94 | fieldset[disabled] .btn-primary:hover,
95 | fieldset[disabled] .btn-primary:focus {
96 | background-color: $color-btn-primary;
97 | border-color: #2e6da4;
98 | }
99 |
--------------------------------------------------------------------------------
/src/utils/dico.js:
--------------------------------------------------------------------------------
1 | /*!
2 | Evolutility-UI-React
3 | https://github.com/evoluteur/evolutility-ui-react
4 | (c) 2023 Olivier Giulieri
5 | */
6 |
7 | // Helpers for models
8 |
9 | // - Supported field types
10 | const ft = {
11 | text: "text",
12 | textml: "textmultiline",
13 | bool: "boolean",
14 | int: "integer",
15 | dec: "decimal",
16 | money: "money",
17 | date: "date",
18 | time: "time",
19 | lov: "lov",
20 | // list: "list", // many values for one field (behave like tags - an array of strings or id?)
21 | // html: "html",
22 | // formula: "formula", // maybe a field attribute rather than a field type
23 | email: "email",
24 | image: "image",
25 | doc: "document",
26 | // geoloc: 'geolocation',
27 | url: "url",
28 | color: "color",
29 | // hidden: "hidden",
30 | json: "json",
31 | // rating: 'rating',
32 | // widget: 'widget'
33 | };
34 |
35 | export const fieldTypes = ft;
36 | export const fieldTypeStrings = Object.keys(ft).map((k) => ft[k]);
37 |
38 | export const fieldIsNumber = (f) =>
39 | f.type === ft.int || f.type === ft.dec || f.type === ft.money;
40 |
41 | export const fieldIsDateOrTime = (f) =>
42 | f.type === ft.date || f.type === ft.time;
43 |
44 | export const fieldIsNumeric = (f) => fieldIsNumber(f) || fieldIsDateOrTime(f);
45 |
46 | export const fieldChartable = (f) =>
47 | //TODO: charts for number fields
48 | f.type === ft.lov || f.type === ft.bool;
49 |
50 | export const fieldInCharts = (f) => fieldChartable(f) && !f.noCharts;
51 | export const fieldInStats = (f) => fieldIsNumeric(f) && !f.noStats;
52 | export const fieldInSearch = (f) => f.inSearch;
53 | // export const fieldInSearch = (f) => f.inSearch || (f.inMany && fieldIsText(f));
54 |
55 | export const fieldIsText = (f) =>
56 | [ft.text, ft.textml, ft.url, ft.email].includes(f.type);
57 |
58 | export const fieldId2Field = (fieldIds, fieldsH) =>
59 | fieldIds?.map((id) => fieldsH[id]) || null;
60 |
61 | export const allStats = ["avg", "stddev", "variance", "min", "max"];
62 | export const fieldStatsFunctions = (f) => {
63 | if (fieldIsDateOrTime(f)) {
64 | return ["avg", "stddev", "min", "max"];
65 | }
66 | return allStats;
67 | };
68 |
69 | const dico = {
70 | fieldTypes,
71 | fieldTypeStrings,
72 | fieldIsText,
73 | fieldIsNumber,
74 | fieldIsDateOrTime,
75 | fieldIsNumeric,
76 | fieldInCharts,
77 | fieldChartable,
78 | fieldId2Field,
79 | };
80 |
81 | export default dico;
82 |
--------------------------------------------------------------------------------
/src/components/widgets/Alert/Alert.test.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable testing-library/no-await-sync-query */
2 | /* eslint-disable testing-library/no-node-access */
3 | /* eslint-disable testing-library/await-async-query */
4 | /* eslint-disable import/no-extraneous-dependencies */
5 | import { render, screen, cleanup } from "@testing-library/react";
6 | import Alert from "./Alert";
7 | import renderer from "react-test-renderer";
8 |
9 | afterEach(cleanup);
10 |
11 | describe("alert props test", () => {
12 | let alertToTest;
13 | const props = {
14 | title: "Alert!",
15 | message: "You got alert!",
16 | type: "success",
17 | };
18 | beforeEach(async () => {
19 | const testInstance = renderer.create( );
20 | alertToTest = testInstance.root;
21 | });
22 | it("should render title in strong", () => {
23 | const strongRender = alertToTest.findByType("strong");
24 | expect(strongRender.children).toEqual([props.title]);
25 | });
26 | // it("should render message in p", () => {
27 | // const pRender = alertToTest.findByType("p");
28 | // expect(pRender.children).toEqual([props.message]);
29 | // });
30 | });
31 | describe("alert widget tests", () => {
32 | it("alert has title and message", async () => {
33 | render( );
34 | const alert = screen.getByTestId("alert");
35 | expect(alert).toHaveTextContent("Hello!");
36 | expect(alert).toHaveTextContent("you have alert");
37 | });
38 | it("alert has null title and message", async () => {
39 | render( );
40 | const alert = screen.getByTestId("alert");
41 | expect(alert).toHaveTextContent("you have alert");
42 | });
43 | it("test type: info", async () => {
44 | render( );
45 | const alert = screen.getByTestId("alert");
46 | expect(alert).toHaveTextContent("info alert");
47 | });
48 | it("test type: success", async () => {
49 | render( );
50 | const alert = screen.getByTestId("alert");
51 | expect(alert).toHaveTextContent("success alert");
52 | });
53 | it("test type: warning", async () => {
54 | render( );
55 | const alert = screen.getByTestId("alert");
56 | expect(alert).toHaveTextContent("warning alert");
57 | });
58 | });
59 |
--------------------------------------------------------------------------------
/src/App.jsx:
--------------------------------------------------------------------------------
1 | // Evolutility-UI-React
2 | // https://github.com/evoluteur/evolutility-ui-react
3 | // (c) 2023 Olivier Giulieri
4 |
5 | import React, { useState } from "react";
6 | import { BrowserRouter, Routes, Route } from "react-router-dom";
7 | import classnames from "classnames";
8 | import { ToastContainer } from "react-toastify";
9 | import config from "config";
10 | import { evoPath } from "utils/format";
11 |
12 | import SideBar from "components/shell/SideBar/SideBar";
13 | import TopBar from "components/shell/TopBar/TopBar";
14 | import Footer from "components/shell/Footer/Footer";
15 | import ErrorBoundary from "components/ErrorBoundary";
16 |
17 | import Home from "pages/Home/Home";
18 | import PageNotFound from "pages/PageNotFound/PageNotFound";
19 | import DocRoutes from "routes/DocRoutes";
20 | import DemoRoutes from "routes/DemoRoutes";
21 |
22 | import "./App.scss";
23 | import "./App-custom.scss";
24 | import "react-crud-icons/src/Icon.scss";
25 | import "react-toastify/scss/main.scss";
26 | import "rc-tooltip/assets/bootstrap_white.css";
27 | import "components/widgets/Modal.scss";
28 | import "components/widgets/global.scss";
29 |
30 | const baseName = config.baseName || "/";
31 |
32 | const AppRoutes = () => (
33 |
34 | } />
35 | } />
36 | } />
37 | } />
38 |
39 | );
40 |
41 | const App = () => {
42 | const [isCollapsed, setIsCollapsed] = useState(false);
43 |
44 | const onClickToggle = () => {
45 | setIsCollapsed(!isCollapsed);
46 | };
47 |
48 | const css = classnames("app", { "side-collapsed": isCollapsed });
49 |
50 | return (
51 |
52 |
53 |
54 |
58 |
59 |
60 | >
61 | }
62 | />
63 |
64 |
69 |
70 |
71 |
72 |
73 | );
74 | };
75 |
76 | export default App;
77 |
--------------------------------------------------------------------------------
/src/components/views/analytics/Charts/Bars.jsx:
--------------------------------------------------------------------------------
1 | // - Wrapper for @nivo ResponsiveBar
2 |
3 | import React, { useMemo } from "react";
4 | import { ResponsiveBar } from "@nivo/bar";
5 | import chartPropTypes from "./chartProps";
6 | import { colors, labelColor, innerLabelColor } from "./chartOptions";
7 |
8 | const theme = {
9 | axis: {
10 | ticks: { text: { fill: labelColor } },
11 | },
12 | };
13 |
14 | const Bars = ({ data, showLegend = true }) => {
15 | const { cleanData, keys } = useMemo(() => {
16 | const cleanData = {};
17 | data.forEach((row) => {
18 | cleanData[row.label] = row.value;
19 | });
20 | cleanData["_"] = "";
21 | return {
22 | cleanData,
23 | keys: Object.keys(cleanData),
24 | };
25 | }, [data]);
26 |
27 | return (
28 |
78 | );
79 | };
80 |
81 | export default Bars;
82 |
83 | Bars.propTypes = chartPropTypes;
84 |
--------------------------------------------------------------------------------
/src/components/widgets/Button/Button.test.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable testing-library/no-await-sync-query */
2 | import { render, screen } from "@testing-library/react";
3 | import user from "@testing-library/user-event";
4 | import Button from "./Button";
5 | import { MemoryRouter as Router } from "react-router-dom";
6 |
7 | describe("button widget tests", () => {
8 | const noop = () => {};
9 | it("reder Button component", () => {
10 | render( );
11 | const button = screen.getByTestId("button");
12 | expect(button).toBeInTheDocument();
13 | });
14 | it("Button renders with correct label", () => {
15 | render( );
16 | const button = screen.getByTestId("button");
17 | expect(button).toBeInTheDocument();
18 | expect(button).toHaveTextContent("Submit");
19 | });
20 | it("Button click event", async () => {
21 | render( );
22 | const button = await screen.findByTestId("button");
23 | user.click(button);
24 | //expect(noop).toHaveBeenCalled();
25 | expect(button).toBeInTheDocument();
26 | });
27 | it("Button click event with onClick", async () => {
28 | render( );
29 | const button = await screen.findByTestId("button");
30 | user.click(button);
31 | //expect(noop).toHaveBeenCalled();
32 | expect(button).toBeInTheDocument();
33 | });
34 | it("Button event with url", async () => {
35 | render(
36 |
37 |
38 |
39 | );
40 | // user.click(link);
41 | const hello = await screen.getByText("Submit");
42 | expect(hello).toHaveTextContent("Submit");
43 | });
44 | it("Button property test add type", async () => {
45 | render( );
46 | const button = await screen.findByTestId("button");
47 | expect(button).toBeInTheDocument();
48 | });
49 | it("Button property test add icon", async () => {
50 | render( );
51 | const button = await screen.findByTestId("button");
52 | expect(button).toBeInTheDocument();
53 | });
54 | it("Button property test add class", async () => {
55 | render( );
56 | const button = await screen.findByTestId("button");
57 | expect(button).toBeInTheDocument();
58 | });
59 | });
60 |
--------------------------------------------------------------------------------
/src/components/views/many/List/List.jsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable react-hooks/exhaustive-deps */
2 | // Evolutility-UI-React :: /views/many/List.js
3 |
4 | // List view to display a collection as a list (table w/ sorting and paging).
5 |
6 | // https://github.com/evoluteur/evolutility-ui-react
7 | // (c) 2023 Olivier Giulieri
8 |
9 | import React, { useMemo } from "react";
10 | import PropTypes from "prop-types";
11 | import modelPropType from "components/views/modelPropTypes";
12 | import Icon from "react-crud-icons";
13 | import Alert from "components/widgets/Alert/Alert";
14 | import TableBody from "../shared/TableBody/TableBody";
15 |
16 | import "./List.scss";
17 |
18 | const tableHeader = (fields, onClickSort, sortFieldId, sortDirection) => (
19 |
20 |
21 | {fields.map((f) => (
22 |
23 | {f.labelShort || f.label}
24 | {f.id === sortFieldId && (
25 |
29 | )}
30 |
31 | ))}
32 |
33 |
34 | );
35 |
36 | const List = ({
37 | entity,
38 | model,
39 | data,
40 | sortField,
41 | sortDirection,
42 | onClickSort,
43 | }) => {
44 | let body;
45 | const fields = useMemo(() => model?.fields.filter((f) => f.inMany), [entity]);
46 | if (!fields.length) {
47 | body = (
48 |
52 | );
53 | } else if (data?.length) {
54 | const link = `../${entity}/${model?.defaultViewOne || "browse"}/`;
55 | body = (
56 |
57 | {tableHeader(fields, onClickSort, sortField, sortDirection)}
58 |
64 |
65 | );
66 | }
67 | return (
68 |
69 | {body}
70 |
71 | );
72 | };
73 |
74 | export default List;
75 |
76 | List.propTypes = {
77 | entity: PropTypes.string.isRequired,
78 | model: modelPropType.isRequired,
79 | data: PropTypes.oneOfType([
80 | PropTypes.arrayOf(
81 | PropTypes.shape({
82 | id: PropTypes.number.isRequired,
83 | })
84 | ),
85 | PropTypes.shape({
86 | errors: PropTypes.arrayOf(PropTypes.shape()),
87 | }),
88 | ]),
89 | onClickSort: PropTypes.func.isRequired,
90 | };
91 |
--------------------------------------------------------------------------------
/src/App.scss:
--------------------------------------------------------------------------------
1 | @import "variables";
2 |
3 | .app {
4 | position: relative;
5 | min-height: 100vh;
6 | display: flex;
7 | flex-direction: column;
8 | background-color: $bgcolor-main;
9 | }
10 |
11 | img {
12 | border: 0;
13 | max-width: 100%;
14 | }
15 | a {
16 | text-decoration: none;
17 | color: $color-link;
18 | &:hover,
19 | &:focus {
20 | color: $color-link-hover;
21 | text-decoration: underline;
22 | }
23 | }
24 | table {
25 | border-spacing: 0;
26 | }
27 | tr,
28 | img {
29 | page-break-inside: avoid;
30 | }
31 | .list-tags {
32 | > div {
33 | border: $border;
34 | border-radius: 8px;
35 | padding: 2px 7px;
36 | margin: 0 5px 0 0;
37 | width: auto !important;
38 | display: inline-block;
39 | }
40 | }
41 | label {
42 | color: $color-label;
43 | font-size: $label-font-size;
44 | }
45 |
46 | .cols-2 {
47 | @include flex-holder();
48 | > div {
49 | margin-right: 10px;
50 | @include flex-item();
51 | flex: 1 1 0px;
52 | }
53 | }
54 |
55 | .table {
56 | background-color: $bgcolor-background;
57 | border-bottom: $border;
58 | width: 100%;
59 | max-width: 100%;
60 | margin-bottom: 20px;
61 | > thead > tr > th,
62 | > tbody > tr > td {
63 | padding: 8px;
64 | line-height: 1.428571429;
65 | vertical-align: top;
66 | border-top: $border;
67 | }
68 | > thead > tr > th {
69 | text-align: left;
70 | background-color: $bgcolor-header;
71 | border-bottom: $border;
72 | }
73 | > tbody > tr:first-child > td {
74 | border-top: none;
75 | }
76 | }
77 | .table-hover > tbody > tr:hover {
78 | background-color: #f5f5f5;
79 | }
80 |
81 | @media print {
82 | a,
83 | a:visited {
84 | text-decoration: underline;
85 | }
86 | a[href]:after {
87 | content: " (" attr(href) ")";
88 | }
89 | a[href^="/"]:after,
90 | a[href^="#"]:after,
91 | a[href^="javascript:"]:after {
92 | content: "";
93 | }
94 | pre,
95 | blockquote {
96 | border: 1px solid #999;
97 | page-break-inside: avoid;
98 | }
99 | thead {
100 | display: table-header-group;
101 | }
102 | tr,
103 | img {
104 | page-break-inside: avoid;
105 | }
106 | img {
107 | max-width: 100% !important;
108 | }
109 | .table {
110 | border-collapse: collapse !important;
111 | td,
112 | th {
113 | background-color: #fff !important;
114 | }
115 | }
116 | .noprint {
117 | display: none !important;
118 | }
119 | }
120 |
121 | .Toastify__toast {
122 | margin-bottom: 4px !important;
123 | min-height: 42px !important;
124 | }
125 | .Toastify__toast-container {
126 | width: 260px !important;
127 | }
128 |
--------------------------------------------------------------------------------
/src/components/views/comfort/Activity/Activity.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from "react";
2 | import { Link, useParams } from "react-router-dom";
3 |
4 | import { i18n_activity as i18n } from "i18n/i18n";
5 | import { getActivity, clearActivity } from "utils/activity";
6 | import { getModel } from "utils/moMa";
7 | import { capitalize, pixPath } from "utils/format";
8 | import Button from "components/widgets/Button/Button";
9 | import ViewHeader from "components/views/ViewHeader/ViewHeader";
10 |
11 | import "./Activity.scss";
12 |
13 | const visitsCount = (n) => {
14 | if (n === 1) {
15 | return i18n.views1;
16 | }
17 | if (n === 0) {
18 | // should never happen
19 | return i18n.views1;
20 | }
21 | return i18n.viewsN?.replace("{0}", n);
22 | };
23 |
24 | const Activity = () => {
25 | const { entity } = useParams();
26 | const [act, setFullActivity] = useState(getActivity(entity));
27 | const m = getModel(entity);
28 | const title = capitalize(m.namePlural) + " Activity";
29 |
30 | useEffect(() => {
31 | window.scrollTo(0, 0);
32 | document.title = title;
33 | }, [title]);
34 |
35 | const activityItem = (act) => {
36 | return (
37 | act && (
38 |
39 |
40 |
41 | {act.title}
42 |
43 |
{visitsCount(act.visits)}
44 |
{act.date}
45 |
46 | )
47 | );
48 | };
49 |
50 | const activityList = (title, items) => (
51 |
52 | {title.replace("{0}", m.namePlural)}
53 | {items.map(activityItem)}
54 |
55 | );
56 |
57 | const onClearActivity = () => {
58 | // TODO: confirmation
59 | clearActivity(entity);
60 | setFullActivity(null);
61 | };
62 |
63 | return (
64 |
65 |
66 | {act?.lastViewed?.length > 0 && (
67 |
68 | {i18n.activitySince
69 | ?.replace("{0}", m.namePlural)
70 | .replace("{1}", act.firstActivityDate)}
71 |
76 |
77 | )}
78 | {act?.mostViewed.length > 0 &&
79 | activityList(i18n.mostViewed, act?.mostViewed)}
80 | {act?.lastViewed.length ? (
81 | activityList(i18n.lastViewed, act?.lastViewed)
82 | ) : (
83 |
{i18n.noActivity}
84 | )}
85 |
86 | );
87 | };
88 |
89 | export default Activity;
90 |
--------------------------------------------------------------------------------
/src/components/Field/edit/FieldObject.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, useCallback, memo } from "react";
2 | import PropTypes from "prop-types";
3 | import { AsyncTypeahead } from "react-bootstrap-typeahead";
4 | import { getObjectSearch } from "dao/dao";
5 | import { i18n_actions } from "i18n/i18n";
6 |
7 | import "./FieldObject.scss";
8 | import "react-bootstrap-typeahead/css/Typeahead.css";
9 | import "react-bootstrap-typeahead/css/Typeahead.bs5.css";
10 |
11 | const noOp = () => {};
12 | const filterBy = () => true;
13 |
14 | const FieldObject = memo(
15 | ({ entity, id, value, placeHolder, onChange, onInputChange }) => {
16 | const [isLoading, setIsLoading] = useState(false);
17 | const [options, setOptions] = useState(value ? [value] : []);
18 |
19 | useEffect(() => {
20 | setOptions(value ? [value] : []);
21 | }, [value]);
22 |
23 | const handleSearch = useCallback(
24 | (search) => {
25 | setIsLoading(true);
26 | const dataPromise = getObjectSearch(entity, search);
27 | dataPromise.then((v) => {
28 | setOptions(v);
29 | setIsLoading(false);
30 | });
31 | },
32 | [entity]
33 | );
34 |
35 | const handleChange = useCallback(
36 | (values) => {
37 | const v = values?.[0];
38 | if (v) {
39 | onChange({ value: v.id, label: v.name }, { name: id });
40 | }
41 | },
42 | [id, onChange]
43 | );
44 |
45 | return (
46 | option.name}
55 | onSearch={handleSearch}
56 | onChange={handleChange}
57 | onInputChange={onInputChange || noOp}
58 | selectHint={noOp}
59 | selected={value ? [value] : []}
60 | />
61 | );
62 | }
63 | );
64 |
65 | export default FieldObject;
66 |
67 | FieldObject.propTypes = {
68 | /** Model id */
69 | entity: PropTypes.string.isRequired,
70 | /** Element id */
71 | id: PropTypes.string.isRequired,
72 | /** Callback function triggered on selection */
73 | onChange: PropTypes.func.isRequired,
74 | /** Callback function triggered on value change */
75 | onInputChange: PropTypes.func,
76 | /** Field value (object w/ id and name props) */
77 | value: PropTypes.shape({
78 | id: PropTypes.number.isRequired,
79 | name: PropTypes.string.isRequired,
80 | }),
81 | /** Placeholder text */
82 | placeHolder: PropTypes.string,
83 | };
84 |
85 | FieldObject.defaultProps = {
86 | id: "tphd",
87 | value: null,
88 | placeHolder: i18n_actions.search,
89 | onInputChange: null,
90 | };
91 |
--------------------------------------------------------------------------------
/src/components/views/analytics/Charts/ChartTable.jsx:
--------------------------------------------------------------------------------
1 | // Evolutility-UI-React :: /views/Charts/Chart_Table.js
2 |
3 | // Shows a table with the chart data
4 |
5 | import React from "react";
6 | import PropTypes from "prop-types";
7 | import { Link } from "react-router-dom";
8 | import { chartDataPropType } from "./chartProps";
9 | import { i18n_charts } from "i18n/i18n";
10 |
11 | import "./ChartTable.scss";
12 |
13 | const percent = (value, total) =>
14 | `${parseInt((10000 * value) / total, 10) / 100}%`;
15 |
16 | const ChartTable = ({ entity, field, sortTable, data, showTotal }) => {
17 | const sLink = `../${entity}/list?${field.id}=`;
18 | const makeLink = (d) => {
19 | let param = "" + (d.id || d.label);
20 | param = param === "null" ? "null" : "eq." + param;
21 | return sLink + param;
22 | };
23 | let totalCount = 0;
24 | if (showTotal) {
25 | data?.forEach((d) => (totalCount += d.value));
26 | }
27 |
28 | return (
29 |
30 |
31 |
32 |
33 |
34 | {field.label}
35 |
36 |
37 | {i18n_charts.count}
38 |
39 |
40 | {i18n_charts.percentage}
41 |
42 |
43 |
44 |
45 | {data?.map((d) => (
46 |
47 |
48 | {d.label || "N/A"}
49 |
50 | {d.value}
51 | {percent(d.value, totalCount)}
52 |
53 | ))}
54 | {showTotal && totalCount && (
55 |
56 | {i18n_charts.total}
57 | {totalCount}
58 | 100%
59 |
60 | )}
61 |
62 |
63 |
64 | );
65 | };
66 |
67 | export default ChartTable;
68 |
69 | ChartTable.propTypes = {
70 | entity: PropTypes.string.isRequired,
71 | /** Field to aggregate data by */
72 | field: PropTypes.shape({
73 | id: PropTypes.string.isRequired,
74 | label: PropTypes.string.isRequired,
75 | }).isRequired,
76 | /** Callback function for sorting */
77 | sortTable: PropTypes.func,
78 | /** Chart data */
79 | data: chartDataPropType,
80 | /** Add a last row w/ total */
81 | showTotal: PropTypes.bool,
82 | };
83 |
84 | ChartTable.defaultProps = {
85 | sortTable: null,
86 | data: null,
87 | showTotal: true,
88 | };
89 |
--------------------------------------------------------------------------------
/src/components/views/one/Card.jsx:
--------------------------------------------------------------------------------
1 | // Evolutility-UI-React :: /views/one/Card.js
2 |
3 | // Single card (usually part of a set of Cards)
4 |
5 | // https://github.com/evoluteur/evolutility-ui-react
6 | // (c) 2023 Olivier Giulieri
7 |
8 | import React, { memo } from "react";
9 | import PropTypes from "prop-types";
10 | import Icon from "react-crud-icons";
11 | import { Link } from "react-router-dom";
12 | import { getModel } from "utils/moMa";
13 | import { pixPath } from "utils/format";
14 | import { fieldTypes as ft } from "utils/dico";
15 | import { fieldPropTypes } from "components/views/modelPropTypes";
16 | import FieldValue from "components/Field/browse/FieldValue";
17 |
18 | const Card = memo(({ entity, data, fields }) => {
19 | const d = data || {};
20 | const m = getModel(entity);
21 | const linkBrowse = `../${entity}/${m.defaultViewOne || "browse"}/`;
22 | const linkEdit = `../${entity}/edit/`;
23 |
24 | return (
25 |
26 | {fields?.map((f, idx) => {
27 | const fv =
;
28 | if (idx === 0) {
29 | return (
30 |
31 |
32 |
33 | {m.icon && (
34 |
35 | )}
36 | {fv || `( ${d.id} )`}
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 | );
46 | }
47 | if (f.type === ft.image) {
48 | return (
49 |
50 |
51 | {fv}
52 |
53 |
54 | );
55 | }
56 | const icon = f.type === ft.lov && f.lovIcon ? d[`${f.id}_icon`] : "";
57 | const rfid = f.id + d.id;
58 | return (
59 |
60 |
{f.labelShort || f.label}:
61 |
62 | {icon &&
}
63 | {fv}
64 |
65 |
66 | );
67 | })}
68 |
69 | );
70 | });
71 |
72 | export default Card;
73 |
74 | Card.propTypes = {
75 | /** Model id (Object unique key). */
76 | entity: PropTypes.string.isRequired,
77 | /** List of fields metadata. */
78 | fields: PropTypes.arrayOf(fieldPropTypes).isRequired,
79 | /** Data (1 single record/Object). */
80 | data: PropTypes.shape({
81 | id: PropTypes.number.isRequired,
82 | }),
83 | };
84 |
--------------------------------------------------------------------------------
/src/components/views/analytics/Charts/Charts.scss:
--------------------------------------------------------------------------------
1 | /*
2 | evolutility-ui-react : many-charts
3 | */
4 | @import "variables";
5 |
6 | $chart-gap: 5px;
7 |
8 | .evol-many-charts {
9 | width: 100%;
10 | @include flex-holder();
11 | gap: $gap;
12 | > .chart-card {
13 | @include flex-item();
14 | width: 400px;
15 | }
16 | }
17 |
18 | .chart-card {
19 | position: relative;
20 | text-align: center;
21 | background-color: $bgcolor-background;
22 | min-height: 300px;
23 | &.size-tiny {
24 | // width: 280px !important;
25 | height: 300px;
26 | .chart-content > h2 {
27 | font-size: 1em;
28 | padding-left: 0;
29 | }
30 | // .i-chart {
31 | // position: relative;
32 | // top: -30px;
33 | // left: -10px;
34 | // width: 300px;
35 | // }
36 | }
37 | &.small {
38 | min-height: 200px;
39 | }
40 | &.size-large {
41 | width: 100%;
42 | .i-chart {
43 | height: 500px;
44 | }
45 | /*
46 | @media only screen and (max-width: 700px) {
47 | .i-chart{
48 | height: 400px;
49 | }
50 | }*/
51 | }
52 | &.hidden-chart {
53 | display: none;
54 | }
55 | }
56 | .chart-content {
57 | position: relative;
58 | border: 1px solid white;
59 | overflow: hidden;
60 | min-height: 100%;
61 | border-radius: $radius;
62 | > h2 {
63 | display: block;
64 | font-weight: 400;
65 | font-size: 1.2em;
66 | text-align: center;
67 | padding: 10px 40px 30px;
68 | margin-right: 70px;
69 | @include ellipsis();
70 | }
71 | > i {
72 | position: relative;
73 | top: 50px;
74 | }
75 | > .i-chart {
76 | padding: 0 10px;
77 | height: 300px;
78 | min-height: 100px;
79 | }
80 | > .alert {
81 | // no data in charts
82 | margin: 50px 10px 10px 10px;
83 | text-align: left;
84 | }
85 | }
86 |
87 | .single-chart {
88 | display: block;
89 | padding: 0 5px 20px;
90 | .chart-actions-left,
91 | .chart-actions-right {
92 | top: $chart-gap;
93 | }
94 | }
95 | .chart-actions-left,
96 | .chart-actions-right {
97 | position: absolute;
98 | top: 1px;
99 | @include icons-actions();
100 | > i {
101 | border: solid 1px transparent;
102 | &.active {
103 | cursor: default !important;
104 | border-color: $color-icon-active;
105 | > svg {
106 | fill: $color-almostblack !important;
107 | }
108 | }
109 | > svg {
110 | fill: $color-icon !important;
111 | }
112 | }
113 | }
114 | .chart-actions-left {
115 | left: $chart-gap;
116 | }
117 | .chart-actions-right {
118 | right: $chart-gap;
119 | }
120 | .charts-stats {
121 | margin-top: 20px;
122 | i {
123 | margin-right: $chart-gap;
124 | }
125 | }
126 |
127 | // hack: white block to hide group label
128 | .label-patch {
129 | position: absolute;
130 | bottom: 40px;
131 | right: 22px;
132 | background-color: white;
133 | height: 30px;
134 | width: 100px;
135 | }
136 |
--------------------------------------------------------------------------------
/src/components/Field/browse/FieldElemBrowse.jsx:
--------------------------------------------------------------------------------
1 | import React, { memo } from "react";
2 | import { Link } from "react-router-dom";
3 | import { fieldTypes as ft } from "utils/dico";
4 | import { image, pixPath, jsonString } from "utils/format";
5 | import FieldValue from "./FieldValue";
6 | import config from "config";
7 |
8 | const { filesUrl } = config;
9 |
10 | const emHeight = (f) => {
11 | let fh = parseInt(f.height || 2, 10);
12 | if (fh < 2) {
13 | fh = 2;
14 | }
15 | return Math.trunc(fh * 1.6);
16 | };
17 |
18 | const createMarkup = (d) => ({
19 | __html: d
20 | ? d.replace(//g, ">").replace(/\n/g, " ")
21 | : "",
22 | });
23 |
24 | // const itemInList = (id, list) => {
25 | // const tag = list.find((item) => item.id === id);
26 | // if (tag) {
27 | // return tag.text;
28 | // }
29 | // return "N/A";
30 | // };
31 |
32 | const doc = (d, path) => {
33 | if (!d) {
34 | return null;
35 | }
36 | return (
37 |
38 | {d}
39 |
40 | );
41 | };
42 |
43 | const FieldElemBrowse = memo(({ fieldDef, value, icon }) => {
44 | // - return the formatted field value
45 | const f = fieldDef;
46 | const fType = f.type;
47 | let fw;
48 |
49 | if (fType === ft.textml) {
50 | const height = `${emHeight(f)}em`;
51 | return (
52 |
57 | );
58 | }
59 | if (fType === ft.image && value) {
60 | fw = image(filesUrl + value);
61 | } else if (fType === ft.doc) {
62 | fw = doc(value, filesUrl);
63 | } else if (fType === ft.lov) {
64 | if (f.object) {
65 | fw = (
66 |
67 |
68 |
69 | );
70 | } else if (f.lovIcon && icon) {
71 | fw = (
72 |
73 |
74 |
75 | {value?.name}
76 |
77 | );
78 | // } else if (fType === ft.list) {
79 | // if (f.list) {
80 | // fw = (
81 | //
82 | // {value?.map((itemid) => (
83 | //
{itemInList(itemid, f.list)}
84 | // ))}
85 | //
86 | // );
87 | // }
88 | } else {
89 | fw = ;
90 | }
91 | } else if (fType === ft.json) {
92 | fw = {jsonString(value)} ;
93 | } else {
94 | fw = ;
95 | }
96 | return {fw}
;
97 | });
98 |
99 | export default FieldElemBrowse;
100 |
--------------------------------------------------------------------------------
/src/models/organizer/todo.js:
--------------------------------------------------------------------------------
1 | /*
2 | Evolutility UI model for To-Do List
3 | https://github.com/evoluteur/evolutility-ui-react
4 | */
5 |
6 | const model = {
7 | id: "todo",
8 | qid: "task",
9 | title: "To-Do List",
10 | world: "demos",
11 | name: "task",
12 | namePlural: "tasks",
13 | icon: "todo.png",
14 | active: true,
15 | position: 1,
16 | titleField: "title",
17 | fields: [
18 | {
19 | id: "title",
20 | type: "text",
21 | label: "Title",
22 | required: true,
23 | maxLength: 255,
24 | inMany: true,
25 | inSearch: true,
26 | width: 100,
27 | },
28 | {
29 | id: "duedate",
30 | type: "date",
31 | label: "Due Date",
32 | inMany: true,
33 | width: 38,
34 | },
35 | {
36 | id: "category",
37 | type: "lov",
38 | label: "Category",
39 | list: [
40 | {
41 | id: 1,
42 | text: "Home",
43 | },
44 | {
45 | id: 2,
46 | text: "Work",
47 | },
48 | {
49 | id: 3,
50 | text: "Fun",
51 | },
52 | {
53 | id: 4,
54 | text: "Others",
55 | },
56 | {
57 | id: 5,
58 | text: "Misc.",
59 | },
60 | ],
61 | inMany: true,
62 | width: 62,
63 | },
64 | {
65 | id: "priority",
66 | type: "lov",
67 | label: "Priority",
68 | required: true,
69 | list: [
70 | {
71 | id: 1,
72 | text: "1 - ASAP",
73 | },
74 | {
75 | id: 2,
76 | text: "2 - Urgent",
77 | },
78 | {
79 | id: 3,
80 | text: "3 - Important",
81 | },
82 | {
83 | id: 4,
84 | text: "4 - Medium",
85 | },
86 | {
87 | id: 5,
88 | text: "5 - Low",
89 | },
90 | ],
91 | defaultValue: 4,
92 | inMany: true,
93 | width: 100,
94 | },
95 | {
96 | id: "complete",
97 | type: "boolean",
98 | label: "Complete",
99 | inMany: true,
100 | width: 100,
101 | },
102 | {
103 | id: "description",
104 | type: "textmultiline",
105 | label: "Description",
106 | maxLength: 1000,
107 | inMany: false,
108 | inSearch: true,
109 | height: 3,
110 | },
111 | ],
112 | groups: [
113 | {
114 | id: "p1",
115 | type: "panel",
116 | label: "Task",
117 | width: 62,
118 | fields: ["title", "duedate", "category"],
119 | },
120 | {
121 | id: "p2",
122 | type: "panel",
123 | label: "Status",
124 | width: 38,
125 | fields: ["priority", "complete"],
126 | },
127 | {
128 | id: "p3",
129 | type: "panel",
130 | label: "Task Description",
131 | width: 100,
132 | fields: ["description"],
133 | },
134 | ],
135 | };
136 |
137 | export default model;
138 |
--------------------------------------------------------------------------------
/src/components/shell/SideBar/SideBar.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Link, useLocation } from "react-router-dom";
3 | import classnames from "classnames";
4 | import Icon from "react-crud-icons";
5 | import { i18n_nav } from "i18n/i18n";
6 | import { demosMenu, docMenus } from "./appMenus";
7 | import { evoPath, pixPath } from "utils/format";
8 |
9 | import demoSVG from "./svg/eye.svg";
10 | import docSVG from "./svg/book.svg";
11 |
12 | import "./SideBar.scss";
13 |
14 | // #region ------- Helpers ---------
15 | const ShortCuts = ({ entity }) => (
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | );
25 |
26 | const sLink = (label, url, icon, css) => (
27 |
28 |
29 | {label}
30 |
31 | );
32 | const hDemos = sLink("Demos", evoPath, demoSVG);
33 | const hDocs = sLink("Documentation", "docs", docSVG);
34 | //#endregion
35 |
36 | const SideBar = ({ onClickToggle }) => {
37 | // const { entity, view } = useParams(); // not outside of evolutility routes
38 | const [entity, view] = useLocation().pathname.slice(1).split("/");
39 | const menuLinkDemo = (menu) => (
40 |
41 | {sLink(
42 | menu.text,
43 | `/${evoPath}/${menu.id}/${menu.defaultViewMany || "list"}`,
44 | pixPath + menu.icon,
45 | "e-icon"
46 | )}
47 |
48 |
49 | );
50 |
51 | const menuLinkDoc = (menu) => (
52 |
53 |
54 |
55 | {menu.text}
56 |
57 |
58 | );
59 |
60 | let title, links, menus;
61 | if (!entity) {
62 | links = [hDemos, hDocs];
63 | } else if (entity === "docs") {
64 | title = hDocs;
65 | menus = docMenus.map(menuLinkDoc);
66 | links = hDemos;
67 | } else {
68 | title = hDemos;
69 | menus = demosMenu.map(menuLinkDemo);
70 | links = hDocs;
71 | }
72 |
73 | return (
74 |
75 |
76 | {i18n_nav.skip}
77 |
78 |
79 |
80 |
81 | {title && (
82 | <>
83 | {title}
84 |
85 |
86 | >
87 | )}
88 | {links}
89 |
90 |
91 | );
92 | };
93 |
94 | export default SideBar;
95 |
--------------------------------------------------------------------------------
/src/components/views/many/shared/Pagination/Pagination.jsx:
--------------------------------------------------------------------------------
1 | import React, { memo } from "react";
2 | import PropTypes from "prop-types";
3 | import config from "config";
4 |
5 | import "./Pagination.scss";
6 |
7 | const { pageSize = 50 } = config;
8 |
9 | const Pagination = memo(({ count, fullCount, pageIndex, onClick }) => {
10 | if (fullCount > pageSize) {
11 | let gapIdx = 0;
12 | const paginationBody = [];
13 |
14 | if (fullCount > count && !(pageIndex === 0 && count < pageSize)) {
15 | const nbPages = Math.ceil(fullCount / pageSize);
16 | const wPrev = pageIndex > 0;
17 | const wNext = nbPages > pageIndex + 1;
18 | const pId = pageIndex + 1;
19 | const bPage = (id) => {
20 | paginationBody.push(
21 |
26 | {id}
27 |
28 | );
29 | };
30 | const bPageRange = (pStart, pEnd) => {
31 | for (let i = pStart; i <= pEnd; i++) {
32 | bPage(i);
33 | }
34 | };
35 | const bGap = (idx) => {
36 | paginationBody.push(
37 |
38 | ...
39 |
40 | );
41 | };
42 |
43 | paginationBody.push(
44 |
49 | <
50 |
51 | );
52 | bPage(1);
53 |
54 | if (nbPages < 17) {
55 | bPageRange(2, nbPages);
56 | } else if (pId < 5) {
57 | bPageRange(2, 5);
58 | if (nbPages > 5) {
59 | bGap(gapIdx++);
60 | bPage(nbPages);
61 | }
62 | } else {
63 | bGap(gapIdx++);
64 | bPageRange(pId - 2, Math.min(pId + 2, nbPages));
65 | if (nbPages > pId + 2) {
66 | if (nbPages > pId + 3) {
67 | bGap(gapIdx++);
68 | }
69 | bPage(nbPages);
70 | }
71 | }
72 |
73 | paginationBody.push(
74 |
79 | >
80 |
81 | );
82 | }
83 |
84 | return (
85 |
86 | {paginationBody}
87 |
88 | );
89 | }
90 | return null;
91 | });
92 |
93 | export default Pagination;
94 |
95 | Pagination.propTypes = {
96 | /** Page index (default 0) */
97 | pageIndex: PropTypes.number,
98 | /** Number of records in the page */
99 | count: PropTypes.number.isRequired,
100 | /** Total number of records */
101 | fullCount: PropTypes.number.isRequired,
102 | /** Callback function for pagination click */
103 | onClick: PropTypes.func.isRequired,
104 | };
105 |
106 | Pagination.defaultProps = {
107 | pageIndex: 0,
108 | };
109 |
--------------------------------------------------------------------------------
/src/components/views/many/shared/TableBody/TableBody.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 | import { Link } from "react-router-dom";
4 | import { fieldIsNumber, fieldTypes as ft } from "utils/dico";
5 | import { pixPath } from "utils/format";
6 | import FieldValue from "components/Field/browse/FieldValue";
7 | import { fieldPropTypes } from "components/views/modelPropTypes";
8 |
9 | const TableBody = ({ fields, data, iconPath, link }) => {
10 | const icon = iconPath && (
11 |
12 | );
13 |
14 | const tableCell = (d, f, idx) => {
15 | const value = d[f.id];
16 | if (idx === 0) {
17 | // - First column is a link
18 | return (
19 |
20 |
21 | {icon}
22 | {value ? (
23 |
24 | ) : (
25 | `(${d.id})`
26 | )}
27 |
28 | {d.nb_comments && " " + d.nb_comments + " comments"}
29 |
30 | );
31 | }
32 | if (f.type === ft.image) {
33 | return (
34 |
35 | {value && (
36 |
37 |
38 |
39 | )}
40 |
41 | );
42 | }
43 | if (f.type === ft.color) {
44 | return (
45 |
46 |
52 |
53 | );
54 | }
55 | // } else if (f.type === ft.list) {
56 | // const lovMap = getLovMap(f);
57 | // return (
58 | //
59 | //
60 | // {(value || []).map((v) => (
61 | //
{lovMap[v] || "N/A"}
62 | // ))}
63 | //
64 | //
65 | // );
66 | // }
67 | let css;
68 | if (fieldIsNumber(f)) {
69 | css = "align-right";
70 | } else if (f.type === ft.bool) {
71 | css = "td-check";
72 | } else if (f.type === ft.url) {
73 | css = "td-url";
74 | }
75 | return (
76 |
77 |
78 |
79 | );
80 | };
81 |
82 | return (
83 |
84 | {data?.map((d) => (
85 | {fields.map((f, idx) => tableCell(d, f, idx))}
86 | ))}
87 |
88 | );
89 | };
90 |
91 | export default TableBody;
92 |
93 | TableBody.propTypes = {
94 | /** Array of field definitions */
95 | fields: PropTypes.arrayOf(fieldPropTypes).isRequired,
96 | /** Table data */
97 | data: PropTypes.arrayOf(
98 | PropTypes.shape({
99 | id: PropTypes.number.isRequired,
100 | })
101 | ).isRequired,
102 | /** Link for first column (rowid added at the end of it) */
103 | link: PropTypes.string.isRequired,
104 | /** Path to images (image value added at the end of it) */
105 | iconPath: PropTypes.string.isRequired,
106 | };
107 |
--------------------------------------------------------------------------------