├── .gitignore ├── .npmignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE.md ├── MIGRATION.md ├── README.md ├── package.json ├── src ├── __tests__ │ ├── EasyLoadMoreAgent.ts │ ├── EasyLoaderAgent.ts │ └── enzyme.config.ts ├── agents │ ├── EasyLoadMoreAgent.ts │ ├── EasyLoaderAgent.ts │ ├── EasyPagerAgent.ts │ └── index.ts ├── atoms │ ├── EasyFilters │ │ ├── EasyFilters.tsx │ │ ├── index.tsx │ │ └── tools.ts │ ├── EasyList │ │ ├── EasyList.tsx │ │ ├── index.tsx │ │ └── registry.tsx │ ├── EasyLoadMore │ │ ├── EasyLoadMore.tsx │ │ ├── index.tsx │ │ └── registry.tsx │ ├── EasyPager │ │ ├── EasyPager.tsx │ │ ├── index.tsx │ │ └── registry.tsx │ ├── EasyTable │ │ ├── EasyTable.tsx │ │ ├── EasyTableBody.tsx │ │ ├── EasyTableHeader.tsx │ │ ├── defs.ts │ │ ├── index.ts │ │ └── registry.tsx │ └── tools.ts ├── index.ts └── vendor │ └── pagination │ ├── BreakView.tsx │ ├── LICENSE.md │ ├── PageView.tsx │ ├── PaginationBoxView.tsx │ └── index.ts ├── tsconfig.json └── tslint.json /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .vscode 3 | coverage 4 | dist 5 | node_modules 6 | npm-debug.log 7 | package-lock.json 8 | typings 9 | yarn-error.log 10 | yarn.lock 11 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | node_modules 3 | typings 4 | tsconfig.json 5 | typings.json 6 | tslint.json 7 | dist/test 8 | yarn.lock 9 | coverage 10 | .vscode 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '8' 4 | 5 | install: 6 | - npm install -g coveralls 7 | - npm install react-molecule mobx@5 mobx-react # Peer deps 8 | - npm install 9 | 10 | script: 11 | - npm test 12 | - npm run coverage 13 | - coveralls < ./coverage/lcov.info || true # ignore coveralls error 14 | 15 | # Allow Travis tests to run in containers. 16 | # sudo: false 17 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Theodor Diaconu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /MIGRATION.md: -------------------------------------------------------------------------------- 1 | # 0.2.0 2 | 3 | - In the table model you no longer have `actions`, use a `field` with a custom render 4 | - The pagination options have changed we are now passing: `total`, `currentPage`, `perPage`, `onPagechange`, adapt 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Easify 2 | 3 | [![Build Status](https://travis-ci.org/cult-of-coders/easify.svg?branch=master)](https://travis-ci.org/cult-of-coders/easify) 4 | [![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg)](https://github.com/prettier/prettier) 5 | 6 | Easify is API Agnostic and Extremely Hackable and helps you do the following: 7 | 8 | - Creating simple lists with filters 9 | - Creating tables with sort, pagination, search-bars, custom filters 10 | - Creating load more lists 11 | 12 | ...by being **completely agnostic** to the way you fetch your data. 13 | 14 | Start toying around with it here: https://codesandbox.io/s/2l5lvl1nn 15 | 16 | ## Install 17 | 18 | The peer dependencies: 19 | `npm install --save react-molecule mobx mobx-react simpl-schema` 20 | 21 | The package: 22 | `npm install --save easify` 23 | 24 | ## The Agents 25 | 26 | [Agents](https://github.com/cult-of-coders/react-molecule/blob/master/docs/API.md#agent) are the part where we store logic. Easify brings you the mighty 3: 27 | 28 | - Loader - handles data loading and updates his own internal store 29 | - Pager - modifies options on Loader so that it behaves correctly 30 | - LoadMore - modifies options on Loader so that it behaves correctly 31 | 32 | A simple example is illustrated below, how from a `Promise` we render our data. 33 | 34 | ```jsx 35 | import { molecule } from "react-molecule"; 36 | import { EasyLoaderAgent, EasyList } from "easify"; 37 | 38 | // We have to pass a way to load the object to our easy loader, and that function needs to return a promise. 39 | const load = ({ filters, options }) => 40 | // The filters and options here can be from other agents 41 | // Based on these filters you perform the api requests 42 | // For pagination inside options we pass {limit, skip} 43 | new Promise((resolve, reject) => { 44 | setTimeout(() => { 45 | resolve([ 46 | { 47 | _id: "XXX", 48 | name: "John Smith" 49 | }, 50 | { 51 | _id: "YYY", 52 | name: "John Brown" 53 | } 54 | ]); 55 | }, 500); 56 | }); 57 | 58 | const MyList = molecule( 59 | () => { 60 | return { 61 | loader: EasyLoaderAgent.factory({ load }) 62 | }; 63 | }, 64 | () => ( 65 | 66 | {({ data, loading, molecule }) => { 67 | return data.map(item => ); 68 | }} 69 | 70 | ) 71 | ); 72 | ``` 73 | 74 | ## Pagination 75 | 76 | To add pagination, we need a way of counting the data, based on the same filters that data is being loaded against. 77 | 78 | ```jsx 79 | import { EasyLoaderAgent, EasyPagerAgent, EasyList, EasyPager } from "easify"; 80 | 81 | const count = filters => 82 | new Promise(({ resolve, reject }) => { 83 | setTimeout(() => { 84 | resolve(10); 85 | }, 500); 86 | }); 87 | 88 | const MyList = molecule( 89 | () => ({ 90 | loader: EasyLoaderAgent.factory({ load }), 91 | pager: EasyPagerAgent.factory({ count, perPage: 10 }) 92 | }), 93 | () => { 94 | return ( 95 | <> 96 | 97 | 98 | {({ data, loading, molecule }) => { 99 | return data.map(item => ); 100 | }} 101 | 102 | 103 | ); 104 | } 105 | ); 106 | ``` 107 | 108 | ## Load More 109 | 110 | If we want to build a list that contains load more, we simply swap-out Pager, and plug-in LoadMore agents and atoms. 111 | 112 | ```jsx 113 | import { 114 | EasyLoaderAgent, 115 | EasyLoadMoreAgent, 116 | EasyList, 117 | EasyPager 118 | } from "easify"; 119 | 120 | const MyList = molecule( 121 | () => { 122 | return { 123 | agents: { 124 | loader: EasyLoaderAgent.factory({ load }), 125 | loadMore: EasyLoadMoreAgent.factory({ 126 | count, 127 | initialItemsCount: 20, 128 | loadItemsCount: 10 129 | }) 130 | } 131 | }; 132 | }, 133 | () => { 134 | return ( 135 | <> 136 | 137 | {({ data, loading, molecule }) => { 138 | return data.map(item => ); 139 | }} 140 | 141 | 142 | 143 | 144 | ); 145 | } 146 | ); 147 | ``` 148 | 149 | ## EasyTable & Pager 150 | 151 | A place where this package shows the powers of Molecule is when you build a reactive complex table. 152 | 153 | `EasyTable` is a component that accepts a `model` which understands how to render the fields, and it also supports sorting. 154 | 155 | ```jsx 156 | import { 157 | EasyLoaderAgent, 158 | EasyLoadMoreAgent, 159 | EasyList, 160 | EasyPagerAgent 161 | } from "easify"; 162 | 163 | const tableModel = { 164 | // React needs to know the key of an array, so we need to uniquely identify an object 165 | key({ object }) { 166 | return object._id; 167 | }, 168 | 169 | fields: [ 170 | { 171 | label: "First Name", 172 | resolve: "firstName", 173 | sort: "firstName" // The field it should sort on, if not specified it will not have sorting ability 174 | }, 175 | { 176 | label: "Id", 177 | // Resolve can also be a function that returns a React renderable 178 | resolve({ object }) { 179 | return {object._id}; 180 | } 181 | }, 182 | { 183 | label: "Actions", 184 | resolve({ object }) { 185 | return Edit; 186 | } 187 | } 188 | ] 189 | }; 190 | 191 | const MyList = molecule( 192 | () => { 193 | return { 194 | agents: { 195 | loader: EasyLoaderAgent.factory({ load }), 196 | pager: EasyPagerAgent.factory({ count }) 197 | } 198 | }; 199 | }, 200 | () => { 201 | return ( 202 | <> 203 | 204 | 205 | 206 | ); 207 | } 208 | ); 209 | ``` 210 | 211 | ## Filtering your data 212 | 213 | Now it's time to filter, whether you want to search stuff or just have a bar with all sorts of filtering, you want this to be done nicely. Note that due to `react-molecule` nature, EasyFilters work with `EasyList` and `EasyTable` and `Pager` 214 | 215 | We'll show first a simple example where we search stuff in an input: 216 | 217 | ```jsx 218 | const MyList = () => { 219 | return ( 220 | <> 221 | 222 | {({ doFilter }) => { 223 | return ( 224 | doFilter({ name: e.target.value })} 227 | /> 228 | ); 229 | }} 230 | 231 |
232 | 233 | 234 | 235 | ); 236 | }; 237 | ``` 238 | 239 | The `doFilter` function will override the current existing filters on the LoaderAgent. And they will end-up in your `load()` function to reload the data again. 240 | 241 | EasyFilters also supports `simpl-schema` definitions. Which can make it work very nicely with [uniforms](https://github.com/vazco/uniforms) 242 | 243 | ```jsx 244 | import SimpleSchema from "simpl-schema"; 245 | import { AutoForm } from "uniforms/bootstrap4"; 246 | 247 | const FilterSchema = new SimpleSchema({ 248 | firstName: { 249 | type: String, 250 | optional: true, 251 | easify: { 252 | toFilter(value) { 253 | return { 254 | firstName: { 255 | $regex: value, 256 | $options: "i" 257 | } 258 | }; 259 | } 260 | } 261 | } 262 | }); 263 | 264 | // We're passing the schema to EasyFilters as well, because it can read from the `toFilter` 265 | // This makes your life super easy when you have large filter forms, or even when you have simple ones 266 | // No matter how you choose to position them 267 | const MyList = () => { 268 | return ( 269 | <> 270 | 271 | {({ onSubmit }) => ( 272 | // This form is created from your Schema 273 | 274 | )} 275 | 276 |
277 | 278 | 279 | 280 | ); 281 | }; 282 | ``` 283 | 284 | ## Hack the views 285 | 286 | Almost all elements can be hackable, thanks to `Molecule registry`. They are stored in the global registry meaning you can override them, for example, you want to override the Table Header ? No problem: 287 | 288 | ```jsx 289 | import { Registry } from "react-molecule"; 290 | 291 | Registry.blend({ 292 | EasyTableHead: props => 293 | }); 294 | ``` 295 | 296 | How did I find out the name ? Well, the easiest way is the use `React DevTools` to check the component name. And almost all components can be hacked. If not, you can look into the `atoms` folder inside this repository and look for `registry.tsx` files. 297 | 298 | ## Conclusion 299 | 300 | Feel free to submit issues if you want to improve anything. 301 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "easify", 3 | "version": "0.2.0", 4 | "description": "Creating lists and complex tables with pagination and filters very fast", 5 | "main": "dist/index.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/cult-of-coders/easify.git" 9 | }, 10 | "scripts": { 11 | "clean": "rimraf dist coverage", 12 | "compile": "tsc", 13 | "pretest": "npm run compile", 14 | "test": "npm run testonly --", 15 | "lint": "tslint --type-check --project ./tsconfig.json ./src/**/*", 16 | "watch": "tsc -w", 17 | "testonly": "mocha --reporter spec --full-trace ./dist/__tests__/*.js", 18 | "testonly-watch": "mocha --reporter spec --full-trace ./dist/__tests__/*.js --watch", 19 | "coverage": "node ./node_modules/istanbul/lib/cli.js cover _mocha -- --full-trace ./dist/__tests__/*.js", 20 | "postcoverage": "remap-istanbul --input coverage/coverage.json --type lcovonly --output coverage/lcov.info", 21 | "prepublishOnly": "npm run clean && npm run compile" 22 | }, 23 | "peerDependencies": { 24 | "react": "16.x", 25 | "react-molecule": "0.2.x", 26 | "simpl-schema": "1.x", 27 | "mobx": ">= 5.x.x", 28 | "mobx-react": ">= 5.x.x" 29 | }, 30 | "devDependencies": { 31 | "@types/react": "^16.3.9", 32 | "@types/react-form": "^2.16.0", 33 | "chai": "^4.1.2", 34 | "chai-as-promised": "^7.1.1", 35 | "enzyme": "^3.3.0", 36 | "enzyme-adapter-react-16": "^1.1.1", 37 | "istanbul": "^0.4.5", 38 | "mobx": "^5.8.0", 39 | "mobx-react": "^6.1.3", 40 | "mocha": "^3.3.0", 41 | "react": "16.3.1", 42 | "react-dom": "^16.3.1", 43 | "remap-istanbul": "^0.11.1", 44 | "rimraf": "^2.6.2", 45 | "simpl-schema": "^1.5.0", 46 | "tslint": "^5.2.0", 47 | "typescript": "^3.2.2" 48 | }, 49 | "typings": "dist/index.d.ts", 50 | "typescript": { 51 | "definition": "dist/index.d.ts" 52 | }, 53 | "license": "MIT", 54 | "dependencies": { 55 | "react-molecule": "^0.2.1" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/__tests__/EasyLoadMoreAgent.ts: -------------------------------------------------------------------------------- 1 | import { MoleculeModel } from 'react-molecule'; 2 | import { describe, it } from 'mocha'; 3 | import { assert } from 'chai'; 4 | import { shallow } from 'enzyme'; 5 | import './enzyme.config'; 6 | import { EasyLoaderAgent, EasyLoadMoreAgent } from '../agents'; 7 | import { LoaderEvents } from './../agents/EasyLoaderAgent'; 8 | import { toJS } from 'mobx'; 9 | 10 | describe('EasyLoadMore', () => { 11 | it('Should load with propper configuration', done => { 12 | done(); 13 | 14 | // To implement 15 | return; 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /src/__tests__/EasyLoaderAgent.ts: -------------------------------------------------------------------------------- 1 | import { MoleculeModel } from 'react-molecule'; 2 | import { describe, it } from 'mocha'; 3 | import { assert } from 'chai'; 4 | import { shallow } from 'enzyme'; 5 | import './enzyme.config'; 6 | import { EasyLoaderAgent } from '../agents'; 7 | import { LoaderEvents } from './../agents/EasyLoaderAgent'; 8 | import { toJS } from 'mobx'; 9 | 10 | describe('EasyLoaderAgent', () => { 11 | it('Should load with propper configuration', done => { 12 | const agent = new EasyLoaderAgent({ 13 | molecule: {} as MoleculeModel, 14 | filters: { test: 1 }, 15 | options: { test: 2 }, 16 | load: () => 17 | new Promise((resolve, reject) => { 18 | resolve([{ _id: 1, name: 'Johnas Smith' }]); 19 | }), 20 | }); 21 | 22 | agent.prepare(); 23 | 24 | assert.equal(1, agent.config.filters.test); 25 | assert.equal(2, agent.config.options.test); 26 | 27 | agent.on(LoaderEvents.LOADED, payload => { 28 | assert.lengthOf(payload.data, 1); 29 | agent.clean(); 30 | done(); 31 | }); 32 | 33 | agent.on(LoaderEvents.ERROR, ({ error }) => { 34 | done(error); 35 | }); 36 | 37 | agent.init(); 38 | }); 39 | 40 | it('Should properly work with override()', async () => { 41 | const dataSet1 = [{ _id: 1, name: 'Brown John' }]; 42 | 43 | const dataSet2 = [{ _id: 2, name: 'Brown Smith' }]; 44 | 45 | const agent = new EasyLoaderAgent({ 46 | molecule: {} as MoleculeModel, 47 | filters: { 48 | context: 1, 49 | }, 50 | load: ({ filters, options }) => 51 | new Promise((resolve, reject) => { 52 | resolve(filters.context === 1 ? dataSet1 : dataSet2); 53 | }), 54 | }); 55 | 56 | await agent.init(); 57 | 58 | const promise = new Promise((resolve, reject) => { 59 | agent.on(LoaderEvents.LOADED, payload => { 60 | try { 61 | assert.equal('Brown Smith', payload.data[0].name); 62 | } catch (e) { 63 | reject(e); 64 | } 65 | 66 | resolve(); 67 | }); 68 | }); 69 | 70 | agent.override({ 71 | filters: { 72 | context: 2, 73 | }, 74 | }); 75 | 76 | return promise; 77 | }); 78 | 79 | it('Should properly work with update()', async () => { 80 | const dataSet1 = [{ _id: 1, name: 'Brown John' }]; 81 | 82 | const dataSet2 = [{ _id: 2, name: 'Brown Smith' }]; 83 | 84 | const agent = new EasyLoaderAgent({ 85 | molecule: {} as MoleculeModel, 86 | filters: { 87 | context: 1, 88 | }, 89 | load: ({ filters, options }) => 90 | new Promise((resolve, reject) => { 91 | resolve(filters.context && !filters.update ? dataSet1 : dataSet2); 92 | }), 93 | }); 94 | 95 | await agent.init(); 96 | 97 | const promise = new Promise((resolve, reject) => { 98 | agent.on(LoaderEvents.LOADED, payload => { 99 | try { 100 | assert.equal('Brown Smith', payload.data[0].name); 101 | } catch (e) { 102 | reject(e); 103 | } 104 | 105 | resolve(); 106 | }); 107 | }); 108 | 109 | agent.update({ 110 | filters: { 111 | update: true, 112 | }, 113 | }); 114 | 115 | return promise; 116 | }); 117 | }); 118 | -------------------------------------------------------------------------------- /src/__tests__/enzyme.config.ts: -------------------------------------------------------------------------------- 1 | import * as enzyme from 'enzyme'; 2 | import * as Adapter from 'enzyme-adapter-react-16'; 3 | 4 | enzyme.configure({ adapter: new Adapter() }); 5 | -------------------------------------------------------------------------------- /src/agents/EasyLoadMoreAgent.ts: -------------------------------------------------------------------------------- 1 | import { Agent } from 'react-molecule'; 2 | // Leave IObservableObject otherwise it will complain in tsc compilation 3 | import { observable, toJS, IObservableObject } from 'mobx'; 4 | import EasyLoaderAgent, { LoaderEvents } from './EasyLoaderAgent'; 5 | 6 | class EasyLoadMoreAgent extends Agent { 7 | loaderAgent: EasyLoaderAgent; 8 | 9 | // This variable becomes true whenever the selectors inside the molecule change 10 | // It becomes false again after the data has been loaded 11 | duringSelectorsChangedProcess: boolean = false; 12 | 13 | config: { 14 | initialItemsCount: number; 15 | loadItemsCount: number; 16 | agent?: string; 17 | count: (filters) => Promise; 18 | }; 19 | 20 | store = observable({ 21 | totalLoaded: 0, 22 | hasMore: undefined, 23 | totalCount: 0, 24 | }); 25 | 26 | prepare() { 27 | const loaderAgent = this.molecule.getAgent(this.config.agent || 'loader'); 28 | this.loaderAgent = loaderAgent; 29 | 30 | loaderAgent.on(LoaderEvents.LOADING, ({ options }) => { 31 | const { initialItemsCount, loadItemsCount } = this.config; 32 | 33 | // If the selectors have changed we basically need to fully reload the initial items count 34 | if (this.duringSelectorsChangedProcess) { 35 | Object.assign(options, { 36 | limit: initialItemsCount, 37 | skip: 0, 38 | }); 39 | 40 | return; 41 | } 42 | 43 | const { totalLoaded } = this.store; 44 | 45 | Object.assign(options, { 46 | limit: totalLoaded === 0 ? initialItemsCount : loadItemsCount, 47 | skip: totalLoaded, 48 | }); 49 | }); 50 | 51 | loaderAgent.on(LoaderEvents.LOADED, payload => { 52 | const { data } = toJS(loaderAgent.store); 53 | 54 | if (!this.duringSelectorsChangedProcess) { 55 | payload.data = [...data, ...payload.data]; 56 | } else { 57 | this.duringSelectorsChangedProcess = false; 58 | } 59 | 60 | this.store.totalLoaded = payload.data.length; 61 | this.updateHasMore(); 62 | }); 63 | } 64 | 65 | init() { 66 | this.loaderAgent.on(LoaderEvents.SELECTORS_CHANGED, () => { 67 | this.duringSelectorsChangedProcess = true; 68 | 69 | Object.assign(this.store, { 70 | totalLoaded: 0, 71 | hasMore: undefined, 72 | totalCount: 0, 73 | }); 74 | 75 | this.count(); 76 | }); 77 | 78 | this.count(); 79 | } 80 | 81 | count = () => { 82 | const loaderStore = toJS(this.loaderAgent.store); 83 | 84 | this.isDebug() && 85 | console.log('Load-more counting for store: ', loaderStore); 86 | 87 | this.config.count(loaderStore.filters).then(count => { 88 | this.store.totalCount = count; 89 | this.updateHasMore(); 90 | }); 91 | }; 92 | 93 | load() { 94 | return this.loaderAgent.load(); 95 | } 96 | 97 | isLoading() { 98 | return this.loaderAgent.store.loading; 99 | } 100 | 101 | private updateHasMore() { 102 | this.store.hasMore = this.store.totalLoaded < this.store.totalCount; 103 | } 104 | } 105 | 106 | export default EasyLoadMoreAgent; 107 | -------------------------------------------------------------------------------- /src/agents/EasyLoaderAgent.ts: -------------------------------------------------------------------------------- 1 | import { observable, toJS } from 'mobx'; 2 | import React, { Component } from 'react'; 3 | import { withMolecule, Agent } from 'react-molecule'; 4 | import PropTypes from 'prop-types'; 5 | 6 | export const LoaderEvents = { 7 | INIT: 'easify.loader.init.before', 8 | 9 | UPDATE: 'easify.loader.filters.update', 10 | OVERRIDE: 'easify.loader.filters.override', 11 | 12 | FILTERS_CHANGE: 'easify.loader.filters.change', 13 | OPTIONS_CHANGE: 'easify.loader.options.change', 14 | SELECTORS_CHANGE: 'easify.loader.selectors.change', 15 | SELECTORS_CHANGED: 'easify.loader.selectors.changed', 16 | 17 | LOADING: 'easify.loader.data.loading', 18 | LOADED: 'easify.loader.data.loaded', 19 | ERROR: 'easify.loader.data.error', 20 | }; 21 | 22 | export type UpdateFilters = { 23 | filters?: any; 24 | options?: any; 25 | }; 26 | 27 | export default class EasyLoaderAgent extends Agent { 28 | static Events = LoaderEvents; 29 | 30 | public store: any = observable({ 31 | loading: true, 32 | data: [], 33 | error: null, 34 | filters: {}, 35 | options: {}, 36 | }); 37 | 38 | prepare() { 39 | if (this.config.filters) { 40 | this.store.filters = this.config.filters; 41 | } 42 | if (this.config.options) { 43 | this.store.options = this.config.options; 44 | } 45 | } 46 | 47 | async init() { 48 | return this.load(); 49 | } 50 | 51 | async load() { 52 | this.store.loading = true; 53 | 54 | const { filters, options } = toJS(this.store); 55 | 56 | this.emit(LoaderEvents.LOADING, { filters, options }); 57 | 58 | // do loading 59 | try { 60 | this.isDebug() && 61 | console.log(`Loader started loading with: `, { filters, options }); 62 | 63 | const data = await this.config.load({ 64 | filters, 65 | options, 66 | }); 67 | 68 | const payload = { data }; 69 | 70 | // Be careful here, we do it like this because we allow to modify the payload 71 | // It can end-up being quite different, like for example append results instead of storing them 72 | this.emit(LoaderEvents.LOADED, payload); 73 | 74 | Object.assign(this.store, { 75 | data: payload.data, 76 | loading: false, 77 | error: null, 78 | }); 79 | } catch (error) { 80 | Object.assign(this.store, { 81 | loading: false, 82 | error, 83 | }); 84 | 85 | this.emit(LoaderEvents.ERROR, { error }); 86 | } 87 | } 88 | 89 | override({ filters, options }: UpdateFilters) { 90 | const store = this.store; 91 | 92 | this.emit(LoaderEvents.SELECTORS_CHANGE, { filters, options }); 93 | 94 | if (filters) { 95 | this.emit(LoaderEvents.FILTERS_CHANGE, { filters }); 96 | store.filters = filters; 97 | } 98 | if (options) { 99 | this.emit(LoaderEvents.OPTIONS_CHANGE, { options }); 100 | store.options = options; 101 | } 102 | 103 | this.load(); 104 | } 105 | 106 | update({ filters, options }: UpdateFilters) { 107 | const store = this.store; 108 | 109 | this.emit(LoaderEvents.SELECTORS_CHANGE, { filters, options }); 110 | 111 | if (filters) { 112 | this.emit(LoaderEvents.FILTERS_CHANGE, { filters }); 113 | store.filters = Object.assign({}, store.filters, filters); 114 | } 115 | 116 | if (options) { 117 | this.emit(LoaderEvents.OPTIONS_CHANGE, { options }); 118 | store.options = Object.assign({}, store.options, options); 119 | } 120 | 121 | this.emit(LoaderEvents.SELECTORS_CHANGED, { filters, options }); 122 | 123 | this.load(); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/agents/EasyPagerAgent.ts: -------------------------------------------------------------------------------- 1 | import EasyLoaderAgent, { LoaderEvents } from './EasyLoaderAgent'; 2 | import { Agent } from 'react-molecule'; 3 | import { observable, toJS } from 'mobx'; 4 | 5 | export default class EasyPagerAgent extends Agent { 6 | loaderAgent: EasyLoaderAgent; 7 | 8 | config: { 9 | agent: string; 10 | perPage: number; 11 | count(filters: any); 12 | }; 13 | 14 | store: { total: number; currentPage: number; perPage: number } = observable({ 15 | total: 0, 16 | currentPage: 0, 17 | perPage: null, 18 | }); 19 | 20 | prepare() { 21 | const loaderAgent = this.molecule.getAgent(this.config.agent || 'loader'); 22 | 23 | if (!loaderAgent) { 24 | throw new Error(`We could not find a loader agent in the molecule`); 25 | } 26 | 27 | this.loaderAgent = loaderAgent; 28 | 29 | this.store.perPage = this.config.perPage || 10; 30 | 31 | loaderAgent.on(LoaderEvents.LOADING, ({ options }) => { 32 | const { perPage, currentPage } = this.store; 33 | Object.assign(options, { 34 | limit: perPage, 35 | skip: currentPage * perPage, 36 | }); 37 | }); 38 | } 39 | 40 | init() { 41 | this.loaderAgent.on(LoaderEvents.SELECTORS_CHANGED, () => { 42 | this.changePage(0, false); 43 | this.count(); 44 | }); 45 | 46 | this.count(); 47 | } 48 | 49 | count = () => { 50 | const loaderStore = toJS(this.loaderAgent.store); 51 | 52 | this.isDebug() && console.log('Counting with: ', loaderStore); 53 | 54 | this.config.count(loaderStore.filters).then(count => { 55 | this.store.total = count; 56 | }); 57 | }; 58 | 59 | clean() { 60 | // TODO: propper cleaning 61 | } 62 | 63 | changePage(number, andLoad = true) { 64 | this.store.currentPage = number; 65 | andLoad && this.loaderAgent.load(); 66 | } 67 | 68 | changePerPage(number) { 69 | this.store.perPage = number; 70 | this.loaderAgent.load(); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/agents/index.ts: -------------------------------------------------------------------------------- 1 | import EasyLoaderAgent from './EasyLoaderAgent'; 2 | import EasyPagerAgent from './EasyPagerAgent'; 3 | import EasyLoadMoreAgent from './EasyLoadMoreAgent'; 4 | 5 | export { EasyLoaderAgent, EasyPagerAgent, EasyLoadMoreAgent }; 6 | -------------------------------------------------------------------------------- /src/atoms/EasyFilters/EasyFilters.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { MoleculeModel, withMolecule } from 'react-molecule'; 3 | import { EasyLoaderAgent } from '../../agents'; 4 | import { getMongoFilters } from './tools'; 5 | 6 | export interface Props { 7 | molecule: MoleculeModel; 8 | agent?: string; 9 | schema: any; 10 | preFilter?: (filters) => void; 11 | children: ({ onSubmit, doFilter }) => any; 12 | } 13 | 14 | class EasyFilters extends React.Component { 15 | loaderAgent: EasyLoaderAgent; 16 | 17 | static defaultProps = { 18 | agent: 'loader', 19 | }; 20 | 21 | constructor(props) { 22 | super(props); 23 | 24 | const { agent, molecule } = props; 25 | this.loaderAgent = molecule.getAgent(agent); 26 | } 27 | 28 | onSubmit = values => { 29 | const { schema, preFilter } = this.props; 30 | const filters = getMongoFilters(schema, values); 31 | 32 | preFilter && preFilter(filters); 33 | 34 | this.loaderAgent.update({ 35 | filters, 36 | }); 37 | }; 38 | 39 | doFilter = filters => { 40 | const { preFilter } = this.props; 41 | 42 | preFilter && preFilter(filters); 43 | 44 | // Filters get overrided 45 | this.loaderAgent.override({ 46 | filters, 47 | }); 48 | }; 49 | 50 | render() { 51 | const { children } = this.props; 52 | 53 | return children({ 54 | onSubmit: this.onSubmit, 55 | doFilter: this.doFilter, 56 | }); 57 | } 58 | } 59 | 60 | export default withMolecule(EasyFilters); 61 | -------------------------------------------------------------------------------- /src/atoms/EasyFilters/index.tsx: -------------------------------------------------------------------------------- 1 | import EasyFilters from './EasyFilters'; 2 | 3 | export { EasyFilters }; 4 | -------------------------------------------------------------------------------- /src/atoms/EasyFilters/tools.ts: -------------------------------------------------------------------------------- 1 | export function defaultToFilter(value, name) { 2 | if (Array.isArray(value)) { 3 | return { 4 | [name]: { $in: value }, 5 | }; 6 | } 7 | 8 | if (value instanceof Date) { 9 | let startOfDay = new Date(value.getTime()); 10 | let endOfDay = new Date(value.getTime()); 11 | 12 | startOfDay.setHours(0, 0, 0, 0); 13 | endOfDay.setHours(23, 59, 59, 999); 14 | 15 | return { 16 | [name]: { 17 | $gte: startOfDay, 18 | $lte: endOfDay, 19 | }, 20 | }; 21 | } 22 | 23 | // Here we need to carefully prevent null from becoming an actual filter or an empty string 24 | if (value === '' || value === null) { 25 | return null; 26 | } 27 | 28 | return { 29 | [name]: value, 30 | }; 31 | } 32 | 33 | export function getMongoFilters(schema, values) { 34 | let filters = {}; 35 | 36 | for (let KEY in values) { 37 | const value = values[KEY]; 38 | const easifyOptions = schema.get(KEY, 'easify') || {}; 39 | 40 | const toFilter = easifyOptions.toFilter || defaultToFilter; 41 | const filter = toFilter(value, KEY, filters); 42 | 43 | // If it's a falsey value we don't assign anything. 44 | if (filter && typeof filter === 'object') { 45 | Object.assign(filters, { 46 | ...filter, 47 | }); 48 | } 49 | } 50 | 51 | return filters; 52 | } 53 | -------------------------------------------------------------------------------- /src/atoms/EasyList/EasyList.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { withMolecule, MoleculeModel } from 'react-molecule'; 3 | import EasyLoaderAgent from '../../agents/EasyLoaderAgent'; 4 | import { observer } from 'mobx-react'; 5 | 6 | export interface Props { 7 | molecule: MoleculeModel; 8 | agent?: string; 9 | children: (items) => any; 10 | autoLoadMore?: boolean; 11 | } 12 | 13 | class EasyList extends React.Component { 14 | loaderAgent: EasyLoaderAgent; 15 | 16 | static defaultProps = { 17 | agent: 'loader', 18 | autoLoadMore: true, 19 | }; 20 | 21 | state = { 22 | sort: {}, 23 | }; 24 | 25 | constructor(props) { 26 | super(props); 27 | 28 | const { molecule, agent } = props; 29 | this.loaderAgent = molecule.getAgent(agent); 30 | } 31 | 32 | render() { 33 | const { molecule, children } = this.props; 34 | 35 | return ( 36 | 41 | ); 42 | } 43 | } 44 | 45 | const EasyListItems = observer(({ store, molecule, renderer }) => { 46 | const { error, loading, data } = store; 47 | const { EasyListLoading, EasyListError, EasyListWrapper } = molecule.registry; 48 | 49 | if (error) { 50 | return ( 51 | 52 | 53 | 54 | ); 55 | } 56 | 57 | return ( 58 | {renderer({ data, molecule, loading })} 59 | ); 60 | }); 61 | 62 | export default withMolecule(EasyList); 63 | -------------------------------------------------------------------------------- /src/atoms/EasyList/index.tsx: -------------------------------------------------------------------------------- 1 | import './registry'; 2 | import EasyList from './EasyList'; 3 | 4 | export { EasyList }; 5 | -------------------------------------------------------------------------------- /src/atoms/EasyList/registry.tsx: -------------------------------------------------------------------------------- 1 | import { Registry } from 'react-molecule'; 2 | import * as React from 'react'; 3 | 4 | const EasyListLoading = () => 'Please wait...'; 5 | const EasyListError = ({ error }) => 'An error has occured'; 6 | const EasyListWrapper = props => ( 7 |
8 | ); 9 | 10 | Registry.blend({ 11 | EasyListLoading, 12 | EasyListError, 13 | EasyListWrapper, 14 | }); 15 | -------------------------------------------------------------------------------- /src/atoms/EasyLoadMore/EasyLoadMore.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { withMolecule, MoleculeModel } from 'react-molecule'; 3 | import { Observer } from 'mobx-react'; 4 | import { EasyLoadMoreAgent } from '../../agents'; 5 | 6 | export interface Props { 7 | molecule: MoleculeModel; 8 | agent?: string; 9 | } 10 | 11 | class EasyLoadMore extends React.Component { 12 | loadMoreAgent: EasyLoadMoreAgent; 13 | 14 | static defaultProps = { 15 | agent: 'loadMore', 16 | }; 17 | 18 | state = { 19 | sort: {}, 20 | }; 21 | 22 | constructor(props) { 23 | super(props); 24 | 25 | const { molecule, agent } = props; 26 | this.loadMoreAgent = molecule.getAgent(agent); 27 | } 28 | 29 | render() { 30 | const { molecule, children } = this.props; 31 | 32 | const { EasyLoadMoreButton } = molecule.registry; 33 | 34 | return ( 35 | 36 | {() => { 37 | const { store } = this.loadMoreAgent; 38 | 39 | return ( 40 | this.loadMoreAgent.load()} 43 | loading={this.loadMoreAgent.isLoading()} 44 | /> 45 | ); 46 | }} 47 | 48 | ); 49 | } 50 | } 51 | 52 | export default withMolecule(EasyLoadMore); 53 | -------------------------------------------------------------------------------- /src/atoms/EasyLoadMore/index.tsx: -------------------------------------------------------------------------------- 1 | import './registry'; 2 | import EasyLoadMore from './EasyLoadMore'; 3 | 4 | export { EasyLoadMore }; 5 | -------------------------------------------------------------------------------- /src/atoms/EasyLoadMore/registry.tsx: -------------------------------------------------------------------------------- 1 | import { Registry } from 'react-molecule'; 2 | import * as React from 'react'; 3 | 4 | Registry.blend({ 5 | EasyLoadMoreButton: ({ loadMore, hasMore }) => { 6 | if (!hasMore) { 7 | return null; 8 | } 9 | 10 | return ; 11 | }, 12 | }); 13 | -------------------------------------------------------------------------------- /src/atoms/EasyPager/EasyPager.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { withMolecule, MoleculeModel } from 'react-molecule'; 3 | import { observer } from 'mobx-react'; 4 | 5 | interface Props { 6 | molecule: MoleculeModel; 7 | agent?: string; 8 | } 9 | 10 | class EasyPager extends React.Component { 11 | pagerAgent: any; 12 | 13 | static defaultProps = { 14 | agent: 'pager', 15 | }; 16 | 17 | constructor(props) { 18 | super(props); 19 | 20 | const { agent, molecule } = this.props; 21 | this.pagerAgent = molecule.getAgent(agent || 'pager'); 22 | } 23 | 24 | onPageChange = ({ selected }) => { 25 | this.pagerAgent.changePage(selected); 26 | }; 27 | 28 | render() { 29 | const { store } = this.pagerAgent; 30 | const { molecule } = this.props; 31 | const { total, currentPage, perPage } = store; 32 | 33 | const { EasyPagination } = molecule.registry; 34 | 35 | return ( 36 | 43 | ); 44 | } 45 | } 46 | 47 | export default withMolecule(observer(EasyPager)); 48 | -------------------------------------------------------------------------------- /src/atoms/EasyPager/index.tsx: -------------------------------------------------------------------------------- 1 | import './registry'; 2 | import EasyPager from './EasyPager'; 3 | 4 | export { EasyPager }; 5 | -------------------------------------------------------------------------------- /src/atoms/EasyPager/registry.tsx: -------------------------------------------------------------------------------- 1 | import { Registry } from 'react-molecule'; 2 | import Pagination from '../../vendor/pagination'; 3 | import * as React from 'react'; 4 | 5 | const EasyPagination = props => { 6 | const { total, perPage, currentPage, onPageChange } = props; 7 | 8 | let pageCount = parseInt((total / perPage).toString()); 9 | if (total % perPage) { 10 | pageCount++; 11 | } 12 | 13 | return ( 14 | 28 | ); 29 | }; 30 | 31 | Registry.blend({ 32 | EasyPagination, 33 | }); 34 | -------------------------------------------------------------------------------- /src/atoms/EasyTable/EasyTable.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { withMolecule, MoleculeModel, mole } from 'react-molecule'; 3 | import { EasyTableModel } from './defs'; 4 | import EasyLoaderAgent from '../../agents/EasyLoaderAgent'; 5 | import EasyTableBody from './EasyTableBody'; 6 | import EasyTableHeader from './EasyTableHeader'; 7 | 8 | export interface Props { 9 | molecule: MoleculeModel; 10 | model: EasyTableModel; 11 | agent?: string; 12 | } 13 | 14 | class EasyTable extends React.Component { 15 | loaderAgent: EasyLoaderAgent; 16 | 17 | static defaultProps = { 18 | agent: 'loader', 19 | }; 20 | 21 | state = { 22 | sort: {}, 23 | }; 24 | 25 | constructor(props) { 26 | super(props); 27 | 28 | const { molecule, agent } = props; 29 | this.loaderAgent = molecule.getAgent(agent); 30 | } 31 | 32 | onSort(sort) { 33 | const agent = this.loaderAgent; 34 | 35 | let options = agent.store.options; 36 | options.sort = options.sort || {}; 37 | Object.assign(options.sort, sort); 38 | 39 | agent.update({ 40 | options, 41 | }); 42 | } 43 | 44 | render() { 45 | const { molecule, model } = this.props; 46 | const { EasyTable } = molecule.registry; 47 | 48 | return ( 49 | 50 | 55 | 60 | 61 | ); 62 | } 63 | } 64 | 65 | export default withMolecule(EasyTable); 66 | -------------------------------------------------------------------------------- /src/atoms/EasyTable/EasyTableBody.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { observer } from 'mobx-react'; 4 | import { EasyTableModel } from './defs'; 5 | import { MoleculeModel } from 'react-molecule'; 6 | import EasyLoaderAgent from '../../agents/EasyLoaderAgent'; 7 | 8 | export interface Props { 9 | model: EasyTableModel; 10 | molecule: MoleculeModel; 11 | agent: EasyLoaderAgent; 12 | } 13 | 14 | export class EasyTableBody extends React.Component { 15 | renderRow(item) { 16 | const { model } = this.props; 17 | const { fields } = model; 18 | 19 | return fields.map(field => { 20 | return this.renderElement(field, item); 21 | }); 22 | } 23 | 24 | renderElement(field, object) { 25 | const { EasyTableRowElement } = this.props.molecule.registry; 26 | 27 | let value; 28 | if (typeof field.resolve === 'function') { 29 | value = field.resolve({ 30 | object, 31 | molecule: this.props.molecule, 32 | }); 33 | } else { 34 | value = object[field.resolve]; 35 | } 36 | 37 | return ( 38 | 39 | {value} 40 | 41 | ); 42 | } 43 | 44 | render() { 45 | const { model, molecule, agent } = this.props; 46 | const { error, data, loading } = agent.store; 47 | 48 | const { 49 | EasyTableBody, 50 | EasyTableRow, 51 | EasyTableRowElement, 52 | EasyTableLoading, 53 | EasyTableError, 54 | EasyTableNoFoundData, 55 | } = molecule.registry; 56 | 57 | const canShowData = !error; 58 | 59 | return ( 60 | 61 | {loading && ( 62 | 63 | 64 | 65 | 66 | 67 | )} 68 | {error && ( 69 | 70 | 71 | 72 | 73 | 74 | )} 75 | {canShowData && 76 | !loading && 77 | data.length === 0 && ( 78 | 79 | 80 | 81 | 82 | 83 | )} 84 | {canShowData && 85 | data.length > 0 && 86 | data.map(item => ( 87 | 91 | {this.renderRow(item)} 92 | 93 | ))} 94 | 95 | ); 96 | } 97 | } 98 | 99 | export default observer(EasyTableBody); 100 | -------------------------------------------------------------------------------- /src/atoms/EasyTable/EasyTableHeader.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { MoleculeModel } from 'react-molecule'; 3 | import EasyLoaderAgent from '../../agents/EasyLoaderAgent'; 4 | import { EasyTableModel, EasyTableModelField } from './defs'; 5 | 6 | export interface Props { 7 | molecule: MoleculeModel; 8 | agent: EasyLoaderAgent; 9 | model: EasyTableModel; 10 | } 11 | export default class EasyTableHeader extends React.Component { 12 | state = { 13 | sort: {}, 14 | }; 15 | 16 | renderTableHeaderSort(field) { 17 | const { 18 | EasyTableHeadSortUp, 19 | EasyTableHeadSortDown, 20 | } = this.props.molecule.registry; 21 | const { sort } = this.state; 22 | 23 | const sortValue = sort[field.label]; 24 | 25 | if (sortValue === undefined) { 26 | return null; 27 | } else if (sortValue === 1) { 28 | return ( 29 | { 31 | this.toggleSort(field); 32 | }} 33 | /> 34 | ); 35 | } else if (sortValue === -1) { 36 | return ( 37 | { 39 | this.toggleSort(field); 40 | }} 41 | /> 42 | ); 43 | } 44 | 45 | return null; 46 | } 47 | 48 | toggleSort = (field: EasyTableModelField) => { 49 | if (!field.sort) { 50 | return; 51 | } 52 | 53 | const { sort } = this.state; 54 | let newSort = Object.assign({}, sort); 55 | 56 | const sortValue = newSort[field.label]; 57 | 58 | if (sortValue === undefined) { 59 | newSort[field.label] = 1; 60 | } else if (sortValue === 1) { 61 | newSort[field.label] = -1; 62 | } else if (sortValue === -1) { 63 | delete newSort[field.label]; 64 | } 65 | 66 | this.setState( 67 | { 68 | sort: newSort, 69 | }, 70 | () => { 71 | this.dispatchSort(); 72 | } 73 | ); 74 | }; 75 | 76 | dispatchSort() { 77 | const { sort } = this.state; 78 | const { fields } = this.props.model; 79 | let sortOptions = {}; 80 | for (let FIELD_LABEL in sort) { 81 | const field = fields.find(f => f.label === FIELD_LABEL); 82 | sortOptions[field.sort] = sort[FIELD_LABEL]; 83 | } 84 | 85 | this.props.agent.update({ 86 | options: { 87 | sort: sortOptions, 88 | }, 89 | }); 90 | } 91 | 92 | render() { 93 | const { molecule, model } = this.props; 94 | 95 | const { 96 | EasyTableHead, 97 | EasyTableHeadElement, 98 | EasyTableRow, 99 | } = molecule.registry; 100 | 101 | let ths = []; 102 | model.fields.forEach(field => { 103 | ths.push( 104 | this.toggleSort(field)} 107 | > 108 | {field.label} 109 | {field.sort && this.renderTableHeaderSort(field)} 110 | 111 | ); 112 | }); 113 | 114 | return ( 115 | 116 | {ths} 117 | 118 | ); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/atoms/EasyTable/defs.ts: -------------------------------------------------------------------------------- 1 | export type EasyTableModel = { 2 | key?: (object: any) => any; 3 | fields: EasyTableModelField[]; 4 | }; 5 | 6 | export type EasyTableModelActions = { 7 | label: string; 8 | render: (payload: any) => any; 9 | }; 10 | 11 | export type EasyTableModelField = { 12 | label: string; 13 | resolve?: () => any | string; 14 | sort?: string; 15 | }; 16 | -------------------------------------------------------------------------------- /src/atoms/EasyTable/index.ts: -------------------------------------------------------------------------------- 1 | import './registry'; 2 | import EasyTable from './EasyTable'; 3 | 4 | export { EasyTable }; 5 | -------------------------------------------------------------------------------- /src/atoms/EasyTable/registry.tsx: -------------------------------------------------------------------------------- 1 | import { Registry, withAgent } from 'react-molecule'; 2 | import * as React from 'react'; 3 | 4 | const EasyTableHead = props => { 5 | return ; 6 | }; 7 | 8 | const EasyTableHeadElement = props => { 9 | return ; 10 | }; 11 | 12 | const EasyTableHeadSortUp = ({ onClick }) => ; 13 | const EasyTableHeadSortDown = ({ onClick }) => ; 14 | const EasyTable = props => { 15 | return ; 16 | }; 17 | 18 | const EasyTableBody = props => ; 19 | const EasyTableRow = props => ; 20 | 21 | const EasyTableRowElement = props =>
; 22 | const EasyTableNoFoundData = () => 'No items were found.'; 23 | const EasyTableLoading = () => 'Please wait...'; 24 | const EasyTableError = () => 'A weird error occured'; 25 | 26 | Registry.blend({ 27 | EasyTableHead, 28 | EasyTableHeadElement, 29 | EasyTableHeadSortUp, 30 | EasyTableHeadSortDown, 31 | EasyTable, 32 | EasyTableBody, 33 | EasyTableRow, 34 | EasyTableRowElement, 35 | EasyTableNoFoundData, 36 | EasyTableLoading, 37 | EasyTableError, 38 | }); 39 | -------------------------------------------------------------------------------- /src/atoms/tools.ts: -------------------------------------------------------------------------------- 1 | export function humanize(label) { 2 | return capitalizeFirstLetter( 3 | label.replace(/([A-Z]+)/g, ' $1').replace(/([A-Z][a-z])/g, ' $1') 4 | ); 5 | } 6 | 7 | export function capitalizeFirstLetter(string) { 8 | return string.charAt(0).toUpperCase() + string.slice(1); 9 | } 10 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import SimpleSchema from 'simpl-schema'; 2 | 3 | SimpleSchema && SimpleSchema.extendOptions(['easify']); 4 | 5 | export * from './atoms/EasyTable'; 6 | export * from './atoms/EasyPager'; 7 | export * from './atoms/EasyFilters'; 8 | export * from './atoms/EasyLoadMore'; 9 | export * from './atoms/EasyList'; 10 | export * from './agents'; 11 | -------------------------------------------------------------------------------- /src/vendor/pagination/BreakView.tsx: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import * as React from 'react'; 4 | 5 | const BreakView = props => { 6 | const label = props.breakLabel; 7 | const className = props.breakClassName || 'break'; 8 | 9 | return
  • {label}
  • ; 10 | }; 11 | 12 | export default BreakView; 13 | -------------------------------------------------------------------------------- /src/vendor/pagination/LICENSE.md: -------------------------------------------------------------------------------- 1 | This code has been taken from: 2 | https://github.com/AdeleD/react-paginate/tree/b684811bb8f88ab9c3ecb531a8f2532968c6329f 3 | 4 | The license of the code is MIT. 5 | 6 | The reason for copy-pasting it, rather than using it as dependency is to have full-control over it. 7 | -------------------------------------------------------------------------------- /src/vendor/pagination/PageView.tsx: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import * as React from 'react'; 4 | 5 | const PageView = props => { 6 | let cssClassName = props.pageClassName; 7 | const linkClassName = props.pageLinkClassName; 8 | const onClick = props.onClick; 9 | const href = props.href; 10 | let ariaLabel = 11 | 'Page ' + 12 | props.page + 13 | (props.extraAriaContext ? ' ' + props.extraAriaContext : ''); 14 | let ariaCurrent = null; 15 | 16 | if (props.selected) { 17 | ariaCurrent = 'page'; 18 | ariaLabel = 'Page ' + props.page + ' is your current page'; 19 | if (typeof cssClassName !== 'undefined') { 20 | cssClassName = cssClassName + ' ' + props.activeClassName; 21 | } else { 22 | cssClassName = props.activeClassName; 23 | } 24 | } 25 | 26 | return ( 27 |
  • 28 | 38 | {props.page} 39 | 40 |
  • 41 | ); 42 | }; 43 | 44 | export default PageView; 45 | -------------------------------------------------------------------------------- /src/vendor/pagination/PaginationBoxView.tsx: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import * as React from 'react'; 4 | import * as PropTypes from 'prop-types'; 5 | import PageView from './PageView'; 6 | import BreakView from './BreakView'; 7 | 8 | export type Props = { 9 | [key: string]: any; 10 | }; 11 | 12 | export type State = { 13 | [key: string]: any; 14 | }; 15 | 16 | export default class PaginationBoxView extends React.Component { 17 | static propTypes = { 18 | pageCount: PropTypes.number.isRequired, 19 | pageRangeDisplayed: PropTypes.number.isRequired, 20 | marginPagesDisplayed: PropTypes.number.isRequired, 21 | previousLabel: PropTypes.node, 22 | nextLabel: PropTypes.node, 23 | breakLabel: PropTypes.node, 24 | hrefBuilder: PropTypes.func, 25 | onPageChange: PropTypes.func, 26 | initialPage: PropTypes.number, 27 | forcePage: PropTypes.number, 28 | disableInitialCallback: PropTypes.bool, 29 | containerClassName: PropTypes.string, 30 | pageClassName: PropTypes.string, 31 | pageLinkClassName: PropTypes.string, 32 | activeClassName: PropTypes.string, 33 | previousClassName: PropTypes.string, 34 | nextClassName: PropTypes.string, 35 | previousLinkClassName: PropTypes.string, 36 | nextLinkClassName: PropTypes.string, 37 | disabledClassName: PropTypes.string, 38 | breakClassName: PropTypes.string, 39 | }; 40 | 41 | static defaultProps = { 42 | pageCount: 10, 43 | pageRangeDisplayed: 2, 44 | marginPagesDisplayed: 3, 45 | activeClassName: 'selected', 46 | previousClassName: 'previous', 47 | nextClassName: 'next', 48 | previousLabel: 'Previous', 49 | nextLabel: 'Next', 50 | breakLabel: '...', 51 | disabledClassName: 'disabled', 52 | disableInitialCallback: false, 53 | }; 54 | 55 | constructor(props) { 56 | super(props); 57 | 58 | this.state = { 59 | selected: props.initialPage 60 | ? props.initialPage 61 | : props.forcePage 62 | ? props.forcePage 63 | : 0, 64 | }; 65 | } 66 | 67 | componentDidMount() { 68 | const { initialPage, disableInitialCallback } = this.props; 69 | // Call the callback with the initialPage item: 70 | if (typeof initialPage !== 'undefined' && !disableInitialCallback) { 71 | this.callCallback(initialPage); 72 | } 73 | } 74 | 75 | componentWillReceiveProps(nextProps) { 76 | if ( 77 | typeof nextProps.forcePage !== 'undefined' && 78 | this.props.forcePage !== nextProps.forcePage 79 | ) { 80 | this.setState({ selected: nextProps.forcePage }); 81 | } 82 | } 83 | 84 | handlePreviousPage = evt => { 85 | const { selected } = this.state; 86 | evt.preventDefault ? evt.preventDefault() : (evt.returnValue = false); 87 | if (selected > 0) { 88 | this.handlePageSelected(selected - 1, evt); 89 | } 90 | }; 91 | 92 | handleNextPage = evt => { 93 | const { selected } = this.state; 94 | const { pageCount } = this.props; 95 | 96 | evt.preventDefault ? evt.preventDefault() : (evt.returnValue = false); 97 | if (selected < pageCount - 1) { 98 | this.handlePageSelected(selected + 1, evt); 99 | } 100 | }; 101 | 102 | handlePageSelected = (selected, evt) => { 103 | evt.preventDefault ? evt.preventDefault() : (evt.returnValue = false); 104 | 105 | if (this.state.selected === selected) return; 106 | 107 | this.setState({ selected: selected }); 108 | 109 | // Call the callback with the new selected item: 110 | this.callCallback(selected); 111 | }; 112 | 113 | hrefBuilder(pageIndex) { 114 | const { hrefBuilder, pageCount } = this.props; 115 | if ( 116 | hrefBuilder && 117 | pageIndex !== this.state.selected && 118 | pageIndex >= 0 && 119 | pageIndex < pageCount 120 | ) { 121 | return hrefBuilder(pageIndex + 1); 122 | } 123 | } 124 | 125 | callCallback = selectedItem => { 126 | if ( 127 | typeof this.props.onPageChange !== 'undefined' && 128 | typeof this.props.onPageChange === 'function' 129 | ) { 130 | this.props.onPageChange({ selected: selectedItem }); 131 | } 132 | }; 133 | 134 | getPageElement(index) { 135 | const { selected } = this.state; 136 | const { 137 | pageClassName, 138 | pageLinkClassName, 139 | activeClassName, 140 | extraAriaContext, 141 | } = this.props; 142 | 143 | return ( 144 | 155 | ); 156 | } 157 | 158 | pagination = () => { 159 | const items = []; 160 | const { 161 | pageRangeDisplayed, 162 | pageCount, 163 | marginPagesDisplayed, 164 | breakLabel, 165 | breakClassName, 166 | } = this.props; 167 | 168 | const { selected } = this.state; 169 | 170 | if (pageCount <= pageRangeDisplayed) { 171 | for (let index = 0; index < pageCount; index++) { 172 | items.push(this.getPageElement(index)); 173 | } 174 | } else { 175 | let leftSide = pageRangeDisplayed / 2; 176 | let rightSide = pageRangeDisplayed - leftSide; 177 | 178 | if (selected > pageCount - pageRangeDisplayed / 2) { 179 | rightSide = pageCount - selected; 180 | leftSide = pageRangeDisplayed - rightSide; 181 | } else if (selected < pageRangeDisplayed / 2) { 182 | leftSide = selected; 183 | rightSide = pageRangeDisplayed - leftSide; 184 | } 185 | 186 | let index; 187 | let page; 188 | let breakView; 189 | let createPageView = index => this.getPageElement(index); 190 | 191 | for (index = 0; index < pageCount; index++) { 192 | page = index + 1; 193 | 194 | if (page <= marginPagesDisplayed) { 195 | items.push(createPageView(index)); 196 | continue; 197 | } 198 | 199 | if (page > pageCount - marginPagesDisplayed) { 200 | items.push(createPageView(index)); 201 | continue; 202 | } 203 | 204 | if (index >= selected - leftSide && index <= selected + rightSide) { 205 | items.push(createPageView(index)); 206 | continue; 207 | } 208 | 209 | if (breakLabel && items[items.length - 1] !== breakView) { 210 | breakView = ( 211 | 216 | ); 217 | items.push(breakView); 218 | } 219 | } 220 | } 221 | 222 | return items; 223 | }; 224 | 225 | render() { 226 | const { 227 | disabledClassName, 228 | previousClassName, 229 | nextClassName, 230 | pageCount, 231 | containerClassName, 232 | previousLinkClassName, 233 | previousLabel, 234 | nextLinkClassName, 235 | nextLabel, 236 | } = this.props; 237 | 238 | const { selected } = this.state; 239 | 240 | const previousClasses = 241 | previousClassName + (selected === 0 ? ` ${disabledClassName}` : ''); 242 | const nextClasses = 243 | nextClassName + 244 | (selected === pageCount - 1 ? ` ${disabledClassName}` : ''); 245 | 246 | return ( 247 | 276 | ); 277 | } 278 | } 279 | -------------------------------------------------------------------------------- /src/vendor/pagination/index.ts: -------------------------------------------------------------------------------- 1 | import PaginationBoxView from './PaginationBoxView'; 2 | 3 | export default PaginationBoxView; 4 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "declaration": true, 6 | "moduleResolution": "node", 7 | "sourceMap": true, 8 | "lib": ["es6", "dom", "esnext"], 9 | "noImplicitAny": false, 10 | "rootDir": "./src", 11 | "outDir": "./dist", 12 | "allowSyntheticDefaultImports": true, 13 | "pretty": true, 14 | "jsx": "react", 15 | "removeComments": true, 16 | "typeRoots": ["node_modules/@types"] 17 | }, 18 | 19 | "include": ["**/*.ts", "**/*.tsx", "**/*.jsx", "**/*.js"], 20 | "exclude": ["node_modules", "dist"] 21 | } 22 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "align": [ 4 | false, 5 | "parameters", 6 | "arguments", 7 | "statements" 8 | ], 9 | "ban": false, 10 | "class-name": true, 11 | "curly": true, 12 | "eofline": true, 13 | "forin": true, 14 | "indent": [ 15 | true, 16 | "spaces" 17 | ], 18 | "interface-name": false, 19 | "jsdoc-format": true, 20 | "label-position": true, 21 | "max-line-length": [ 22 | true, 23 | 140 24 | ], 25 | "member-access": true, 26 | "member-ordering": [ 27 | true, 28 | "public-before-private", 29 | "static-before-instance", 30 | "variables-before-functions" 31 | ], 32 | "no-any": false, 33 | "no-arg": true, 34 | "no-bitwise": true, 35 | "no-conditional-assignment": true, 36 | "no-consecutive-blank-lines": false, 37 | "no-console": [ 38 | true, 39 | "log", 40 | "debug", 41 | "info", 42 | "time", 43 | "timeEnd", 44 | "trace" 45 | ], 46 | "no-construct": true, 47 | "no-debugger": true, 48 | "no-duplicate-variable": true, 49 | "no-empty": true, 50 | "no-eval": true, 51 | "no-inferrable-types": false, 52 | "no-internal-module": true, 53 | "no-null-keyword": false, 54 | "no-require-imports": false, 55 | "no-shadowed-variable": true, 56 | "no-switch-case-fall-through": true, 57 | "no-trailing-whitespace": true, 58 | "no-unused-expression": true, 59 | "no-unused-variable": true, 60 | "no-use-before-declare": true, 61 | "no-var-keyword": true, 62 | "no-var-requires": true, 63 | "object-literal-sort-keys": false, 64 | "one-line": [ 65 | true, 66 | "check-open-brace", 67 | "check-catch", 68 | "check-else", 69 | "check-finally", 70 | "check-whitespace" 71 | ], 72 | "quotemark": [ 73 | true, 74 | "single", 75 | "avoid-escape" 76 | ], 77 | "radix": true, 78 | "semicolon": [ 79 | true, 80 | "always" 81 | ], 82 | "switch-default": true, 83 | "trailing-comma": [ 84 | true, 85 | { 86 | "multiline": "always", 87 | "singleline": "never" 88 | } 89 | ], 90 | "triple-equals": [ 91 | true, 92 | "allow-null-check" 93 | ], 94 | "typedef": [ 95 | false, 96 | "call-signature", 97 | "parameter", 98 | "arrow-parameter", 99 | "property-declaration", 100 | "variable-declaration", 101 | "member-variable-declaration" 102 | ], 103 | "typedef-whitespace": [ 104 | true, 105 | { 106 | "call-signature": "nospace", 107 | "index-signature": "nospace", 108 | "parameter": "nospace", 109 | "property-declaration": "nospace", 110 | "variable-declaration": "nospace" 111 | }, 112 | { 113 | "call-signature": "space", 114 | "index-signature": "space", 115 | "parameter": "space", 116 | "property-declaration": "space", 117 | "variable-declaration": "space" 118 | } 119 | ], 120 | "variable-name": [ 121 | true, 122 | "check-format", 123 | "allow-leading-underscore", 124 | "ban-keywords", 125 | "allow-pascal-case" 126 | ], 127 | "whitespace": [ 128 | true, 129 | "check-branch", 130 | "check-decl", 131 | "check-operator", 132 | "check-separator", 133 | "check-type" 134 | ] 135 | } 136 | } --------------------------------------------------------------------------------