├── .babelrc ├── .flowconfig ├── .gitignore ├── .storybook ├── addons.js ├── config.js ├── middleware.js └── preview-head.html ├── README.md ├── package.json ├── public ├── favicon.ico ├── index.html └── manifest.json ├── src ├── App.css ├── App.js ├── App.test.js ├── colors.js ├── components │ ├── BaseCell │ │ └── index.js │ ├── BooleanCell │ │ └── index.js │ ├── Cell │ │ └── index.js │ ├── CellErrorBoundary │ │ └── index.js │ ├── ColumnHeaderCell │ │ └── index.js │ ├── ConfigureButtonBase │ │ └── index.js │ ├── ConfigureFilters │ │ └── index.js │ ├── ConfigureHiddenFields │ │ └── index.js │ ├── ConfigureRowHeight │ │ └── index.js │ ├── ConfigureSorting │ │ └── index.js │ ├── DynamicCell │ │ ├── index.js │ │ └── index.story.js │ ├── FileCell │ │ └── index.js │ ├── FilterCreater │ │ └── index.js │ ├── ImageCell │ │ └── index.js │ ├── JSONArrayCell │ │ └── index.js │ ├── JSONCell │ │ └── index.js │ ├── MarkdownCell │ │ └── index.js │ ├── NumericCell │ │ └── index.js │ ├── RawJSONEditor │ │ └── index.js │ ├── SearchBar │ │ └── index.js │ ├── SelectCell │ │ └── index.js │ ├── SimpleDialog │ │ └── index.js │ ├── TableHeader │ │ └── index.js │ ├── TextCell │ │ └── index.js │ ├── Theme │ │ └── index.js │ ├── TypeIcon │ │ └── index.js │ ├── Waterobject │ │ ├── index.js │ │ └── index.story.js │ └── Watertable │ │ ├── Row.js │ │ ├── index.js │ │ └── index.story.js ├── hooks │ ├── use-event-callback.js │ └── use-selected-cell.js ├── index.css ├── index.js ├── lib │ └── get-type-from-value.js ├── logo.svg ├── serviceWorker.js └── types.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ "react-app", { "absoluteRuntime": false } ] 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seveibar/react-watertable/a9219c15d29cf47a06a68d7d8e39743a17f3c19e/.flowconfig -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | dist 26 | -------------------------------------------------------------------------------- /.storybook/addons.js: -------------------------------------------------------------------------------- 1 | import '@storybook/addon-actions/register'; 2 | import '@storybook/addon-links/register'; 3 | -------------------------------------------------------------------------------- /.storybook/config.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { configure, addDecorator } from "@storybook/react" 3 | import Theme from "../src/components/Theme" 4 | import { withPrettierSource } from "storybook-addon-prettier-source" 5 | 6 | addDecorator((story, context) => withPrettierSource()(story)(context)) 7 | addDecorator(storyFn => {storyFn()}) 8 | function loadStories() { 9 | const importAll = r => r.keys().map(r) 10 | importAll(require.context("../src/components", true, /\.story\.js$/)) 11 | } 12 | 13 | configure(loadStories, module) 14 | -------------------------------------------------------------------------------- /.storybook/middleware.js: -------------------------------------------------------------------------------- 1 | // const proxy = require("http-proxy-middleware") 2 | // 3 | module.exports = router => { 4 | // router.use(proxy("/api", { target: "http://localhost:3700/" })) 5 | } 6 | -------------------------------------------------------------------------------- /.storybook/preview-head.html: -------------------------------------------------------------------------------- 1 | 2 | 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Watertable 2 | 3 | [![npm version](https://badge.fury.io/js/react-watertable.svg)](https://badge.fury.io/js/react-watertable) 4 | 5 | ![screenshot](https://user-images.githubusercontent.com/1910070/59980268-80953680-95c1-11e9-84ab-d715c9d1ded2.png) 6 | 7 | A table with advanced editable controls. Great for admin panels, customer portals and building in-house tools where something like airtable would be used. 8 | 9 | [Check out the demo here](https://www.seveibar.com/react-watertable/?path=/story/watertable--readme-example). 10 | 11 | Watertable values simplicity, great user experience and extensive capability. 12 | 13 | # Usage 14 | 15 | ```javascript 16 | import React from "react" 17 | import Watertable from "react-watertable" 18 | 19 | const mySchema = { 20 | name: { 21 | title: "Name", 22 | type: "text" 23 | }, 24 | color: { 25 | title: "Favorite Color", 26 | type: "select", 27 | options: [ 28 | { value: "red", label: "Red", color: "#f00" }, 29 | { value: "blue", label: "Blue", color: "#00f" } 30 | ] 31 | } 32 | } 33 | 34 | const MyApp = () => ( 35 | 39 | ) 40 | ``` 41 | 42 | # Features 43 | 44 | - Validate user input 45 | - Editable/read-only cells 46 | - Asynchronous row retrieval (i.e. make API calls to populate table) 47 | - Asynchronous option retrieval/search 48 | - Filtering and sorting 49 | - Hiding columns 50 | - Nested objects as cell values 51 | - Custom cell types 52 | 53 | # Props 54 | 55 | | Props | Type | Description | 56 | | --------------------- | --------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | 57 | | schema | `{[ColumnName: string]: SchemaDefinition}` | Describes the data type of each column. | 58 | | data | `Array` | (optional) All the rows of the table. | 59 | | getData | `(DataSearchQuery) => Promise>` | (optional) Method to retrieve data. | 60 | | renderCell | `(schemaInfo, value, onChange) => React.Component` | (optional) Method to render a cell. Return null to fallback to watertable renderer. | 61 | | onChangeData | `(newData) => any` | (optional) Called whenever a cell has changed. | 62 | | onSave | `(newData: Array) => any` | (optional) If specified, a save button is placed in the table header. This method is called on save press. | 63 | | getOptions | `(columnName: string, input: string) => Promise>` | (optional) Columns without static options will call this method on user input. | 64 | | onChangeDisplayConfig | `(DisplayConfig) => any` | (optional) Called whenever the view changes e.g. column width changes, filter applied. If not specified, all display configuration disabled. | 65 | | onChangeSchema | `(TableSchema) => any` | (optional) Called when schema is modified. If not specified, schema changes are disabled. | 66 | | tablename | `string` | (optional) Displayed above table data. | 67 | | onUpdateCell | | | 68 | | onDeleteRow | `(record: Object) => any` | | 69 | | canAddMore | `boolean` | | 70 | | canDelete | `boolean` | | 71 | | recordActions | `Array` | | 72 | | onClickRecordAction | `(record: Object, action: string) => any` | | 73 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-watertable", 3 | "version": "0.1.22", 4 | "main": "./components/Watertable", 5 | "homepage": "https://www.seveibar.com/react-watertable/?path=/story/watertable--readme-example", 6 | "repository": "github:seveibar/react-watertable", 7 | "dependencies": { 8 | "@rehooks/window-size": "^1.0.2", 9 | "brace": "^0.11.1", 10 | "color-convert": "^2.0.0", 11 | "file-saver": "^2.0.1", 12 | "jsoneditor-react": "^1.0.0", 13 | "react-contextmenu": "^2.11.0", 14 | "react-dropzone": "^10.1.0", 15 | "react-mde": "^7.0.4", 16 | "react-select": "^2.4.2", 17 | "showdown": "^1.9.0" 18 | }, 19 | "scripts": { 20 | "start": "react-scripts start", 21 | "build": "NODE_ENV=production babel ./src --ignore \"src/**/*.story.js\" --out-dir=./dist && cp package.json dist/ && cp README.md dist/", 22 | "dist": "npm run build && cd dist && npm publish", 23 | "test": "react-scripts test", 24 | "eject": "react-scripts eject", 25 | "storybook": "start-storybook -p 9009 -s public", 26 | "build-storybook": "build-storybook -s public", 27 | "gh-pages": "storybook-to-ghpages" 28 | }, 29 | "eslintConfig": { 30 | "extends": "react-app" 31 | }, 32 | "browserslist": [ 33 | ">0.2%", 34 | "not dead", 35 | "not ie <= 11", 36 | "not op_mini all" 37 | ], 38 | "devDependencies": { 39 | "@babel/cli": "^7.4.3", 40 | "@babel/core": "^7.4.3", 41 | "@material-ui/core": "^4.6.1", 42 | "@material-ui/icons": "^4.4.3", 43 | "@material-ui/styles": "^4.6.0", 44 | "@storybook/addon-actions": "^5.1.9", 45 | "@storybook/addon-links": "^5.1.9", 46 | "@storybook/addons": "^5.1.9", 47 | "@storybook/cli": "^5.1.9", 48 | "@storybook/react": "^5.1.9", 49 | "@storybook/storybook-deployer": "^2.8.1", 50 | "babel-preset-react-app": "^7.0.2", 51 | "flow-bin": "^0.95.1", 52 | "react": "^16.8.5", 53 | "react-dom": "^16.8.5", 54 | "react-scripts": "2.1.8", 55 | "storybook-addon-prettier-source": "^2.1.2" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seveibar/react-watertable/a9219c15d29cf47a06a68d7d8e39743a17f3c19e/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | 15 | 16 | 25 | React App 26 | 27 | 28 | 29 |
30 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | animation: App-logo-spin infinite 20s linear; 7 | height: 40vmin; 8 | pointer-events: none; 9 | } 10 | 11 | .App-header { 12 | background-color: #282c34; 13 | min-height: 100vh; 14 | display: flex; 15 | flex-direction: column; 16 | align-items: center; 17 | justify-content: center; 18 | font-size: calc(10px + 2vmin); 19 | color: white; 20 | } 21 | 22 | .App-link { 23 | color: #61dafb; 24 | } 25 | 26 | @keyframes App-logo-spin { 27 | from { 28 | transform: rotate(0deg); 29 | } 30 | to { 31 | transform: rotate(360deg); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import logo from './logo.svg'; 3 | import './App.css'; 4 | 5 | class App extends Component { 6 | render() { 7 | return ( 8 |
9 |
10 | logo 11 |

12 | Edit src/App.js and save to reload. 13 |

14 | 20 | Learn React 21 | 22 |
23 |
24 | ); 25 | } 26 | } 27 | 28 | export default App; 29 | -------------------------------------------------------------------------------- /src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | it('renders without crashing', () => { 6 | const div = document.createElement('div'); 7 | ReactDOM.render(, div); 8 | ReactDOM.unmountComponentAtNode(div); 9 | }); 10 | -------------------------------------------------------------------------------- /src/colors.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import convert from "color-convert" 4 | 5 | export const softenColor = (color?: string) => { 6 | if (!color) return 7 | 8 | const channels = color.includes("#") 9 | ? convert.hex.hsv(color.replace("#", "")) 10 | : convert.keyword.hsv(color) 11 | 12 | channels[1] = 120 13 | channels[2] = 100 14 | 15 | return "#" + convert.hsv.hex(channels) 16 | } 17 | -------------------------------------------------------------------------------- /src/components/BaseCell/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import React, { useState, useEffect } from "react" 4 | import { makeStyles } from "@material-ui/styles" 5 | import { useTheme } from "../Theme" 6 | import { grey, blue } from "@material-ui/core/colors" 7 | import useSelectedCell from "../../hooks/use-selected-cell" 8 | import useEventCallback from "../../hooks/use-event-callback.js" 9 | 10 | const useStyles = makeStyles({}) 11 | 12 | export const BaseCell = ({ 13 | width, 14 | height, 15 | editable = true, 16 | first, 17 | last, 18 | grow, 19 | centered, 20 | bold, 21 | backgroundColor, 22 | editContent, 23 | readContent, 24 | onDeselect, 25 | onClear, 26 | children 27 | }) => { 28 | const c = useStyles() 29 | const theme = useTheme() 30 | let selected, onSelect, onUnselect 31 | if (editable) { 32 | ;[selected, onSelect, onUnselect] = useSelectedCell() 33 | } 34 | const latestOnDeselect = useEventCallback(onDeselect) 35 | 36 | useEffect(() => { 37 | if (!window) return () => {} 38 | if (!selected) return () => {} 39 | const listener = e => { 40 | if (e.key === "Delete" || e.key === "Backspace") { 41 | onClear(e) 42 | } else if (e.key === "Escape") { 43 | onUnselect() 44 | } 45 | } 46 | window.addEventListener("keydown", listener) 47 | return () => { 48 | if (selected && onDeselect) latestOnDeselect() 49 | window.removeEventListener("keydown", listener) 50 | } 51 | }, [selected]) 52 | 53 | return ( 54 |
84 | {!selected ? readContent || children : editContent || children} 85 |
86 | ) 87 | } 88 | 89 | export default BaseCell 90 | -------------------------------------------------------------------------------- /src/components/BooleanCell/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import React from "react" 4 | import BaseCell from "../BaseCell" 5 | import InputBase from "@material-ui/core/InputBase" 6 | import Checkbox from "@material-ui/core/Checkbox" 7 | 8 | export const BooleanCell = props => { 9 | return ( 10 | props.onChange(false)} centered> 11 | props.onChange(checked)} 14 | checked={Boolean(props.value)} 15 | /> 16 | 17 | ) 18 | } 19 | 20 | export default BooleanCell 21 | -------------------------------------------------------------------------------- /src/components/Cell/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import React from "react" 4 | import type { ColumnSchema } from "../../types" 5 | import TextCell from "../TextCell" 6 | import SelectCell from "../SelectCell" 7 | import FileCell from "../FileCell" 8 | import ImageCell from "../ImageCell" 9 | import NumericCell from "../NumericCell" 10 | import BooleanCell from "../BooleanCell" 11 | import JSONCell from "../JSONCell" 12 | import JSONArrayCell from "../JSONArrayCell" 13 | import MarkdownCell from "../MarkdownCell" 14 | import DynamicCell from "../DynamicCell" 15 | import CellErrorBoundary from "../CellErrorBoundary" 16 | 17 | export const OmniCell = (props: ColumnSchema) => { 18 | switch (props.type) { 19 | case "text": { 20 | return 21 | } 22 | case "text-array": { 23 | return 24 | } 25 | case "select": { 26 | return 27 | } 28 | case "file": { 29 | return 30 | } 31 | case "image": { 32 | return 33 | } 34 | case "boolean": { 35 | return 36 | } 37 | case "numeric": { 38 | return 39 | } 40 | case "json": { 41 | return 42 | } 43 | case "json-array": { 44 | return 45 | } 46 | case "markdown": { 47 | return 48 | } 49 | case "dynamic": { 50 | return 51 | } 52 | } 53 | throw new Error( 54 | `Unknown Cell Configuration: "${JSON.stringify(props, null, " ")}"` 55 | ) 56 | } 57 | 58 | export const Cell = (props: ColumnSchema) => { 59 | return ( 60 | 61 | 62 | 63 | ) 64 | } 65 | 66 | export default Cell 67 | -------------------------------------------------------------------------------- /src/components/CellErrorBoundary/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import React from "react" 4 | import BaseCell from "../BaseCell" 5 | 6 | export class CellErrorBoundary extends React.Component { 7 | constructor(props) { 8 | super(props) 9 | this.state = { hasError: false } 10 | } 11 | 12 | static getDerivedStateFromError(error) { 13 | // Update state so the next render will show the fallback UI. 14 | return { hasError: true } 15 | } 16 | 17 | componentDidCatch(error, info) { 18 | // TODO 19 | } 20 | 21 | render() { 22 | if (this.state.hasError) { 23 | return ( 24 | 25 |
Error Rendering Cell
26 |
27 | ) 28 | } 29 | 30 | return this.props.children 31 | } 32 | } 33 | 34 | export default CellErrorBoundary 35 | -------------------------------------------------------------------------------- /src/components/ColumnHeaderCell/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import React, { useState } from "react" 4 | import { makeStyles } from "@material-ui/styles" 5 | import BaseCell from "../BaseCell" 6 | import { grey } from "@material-ui/core/colors" 7 | import TypeIcon from "../TypeIcon" 8 | 9 | const useStyles = makeStyles({ 10 | content: { 11 | display: "flex", 12 | alignItems: "center" 13 | } 14 | }) 15 | 16 | export const ColumnHeaderCell = props => { 17 | const c = useStyles() 18 | return ( 19 | 20 |
21 | 26 |
{props.title}
27 |
28 | {/* eventually the dropdown */} 29 |
30 | 31 | ) 32 | } 33 | 34 | export default ColumnHeaderCell 35 | -------------------------------------------------------------------------------- /src/components/ConfigureButtonBase/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import React from "react" 4 | import Button from "@material-ui/core/Button" 5 | import { makeStyles } from "@material-ui/styles" 6 | import { useTheme } from "../Theme" 7 | import { grey } from "@material-ui/core/colors" 8 | import Menu from "@material-ui/core/Menu" 9 | import MenuItem from "@material-ui/core/MenuItem" 10 | 11 | const useStyles = makeStyles({ 12 | button: { marginLeft: 2, marginRight: 2 }, 13 | icon: { color: grey[700] }, 14 | text: { marginLeft: 8, marginRight: 8 } 15 | }) 16 | 17 | export const ConfigureButtonBase = props => { 18 | const c = useStyles() 19 | const { windowSize } = useTheme() 20 | return ( 21 | 26 | ) 27 | } 28 | 29 | export default ConfigureButtonBase 30 | -------------------------------------------------------------------------------- /src/components/ConfigureFilters/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import React from "react" 4 | import ConfigureButtonBase from "../ConfigureButtonBase" 5 | import FilterIcon from "@material-ui/icons/FilterList" 6 | 7 | export const ConfigureFilters = () => { 8 | return null 9 | } 10 | 11 | export const ConfigureFiltersButton = () => { 12 | return 13 | } 14 | -------------------------------------------------------------------------------- /src/components/ConfigureHiddenFields/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import React from "react" 4 | import ConfigureButtonBase from "../ConfigureButtonBase" 5 | import CancelIcon from "@material-ui/icons/Cancel" 6 | 7 | export const ConfigureHiddenFields = () => { 8 | return null 9 | } 10 | 11 | export const ConfigureHiddenFieldsButton = () => { 12 | return 13 | } 14 | -------------------------------------------------------------------------------- /src/components/ConfigureRowHeight/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import React from "react" 4 | import ConfigureButtonBase from "../ConfigureButtonBase" 5 | import LineWeightIcon from "@material-ui/icons/LineWeight" 6 | 7 | export const ConfigureRowHeight = () => { 8 | return null 9 | } 10 | 11 | export const ConfigureRowHeightButton = () => { 12 | return 13 | } 14 | -------------------------------------------------------------------------------- /src/components/ConfigureSorting/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import React from "react" 4 | import ConfigureButtonBase from "../ConfigureButtonBase" 5 | import SortIcon from "@material-ui/icons/Sort" 6 | 7 | export const ConfigureSorting = () => { 8 | return null 9 | } 10 | 11 | export const ConfigureSortingButton = () => { 12 | return 13 | } 14 | -------------------------------------------------------------------------------- /src/components/DynamicCell/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import React, { useState } from "react" 4 | import Cell from "../Cell" 5 | import { customStyles } from "../SelectCell" 6 | import type { ColumnSchema } from "../../types" 7 | import DropdownIcon from "@material-ui/icons/ArrowDropDown" 8 | import { blue } from "@material-ui/core/colors" 9 | import Select from "react-select" 10 | import TypeIcon from "../TypeIcon" 11 | 12 | export const defaultDynamicTypes = [ 13 | { value: { type: "text" }, label: "Text", example: "" }, 14 | { value: { type: "text-array" }, label: "Text Array", example: [""] }, 15 | { value: { type: "markdown" }, label: "Markdown", example: "#Markdown" }, 16 | { value: { type: "boolean" }, label: "Boolean", example: false }, 17 | { value: { type: "numeric" }, label: "Numeric", example: 0 }, 18 | { value: { type: "json" }, label: "JSON", example: {} }, 19 | { value: { type: "json-array" }, label: "JSON Array", example: [{}] } 20 | ] 21 | 22 | export default (props: ColumnSchema) => { 23 | const [typesOpen, changeTypesOpen] = useState(false) 24 | const [currentType, changeCurrentType] = useState(() => { 25 | if (props.value) { 26 | if (typeof props.value === "string") 27 | return props.value.trim().startsWith("#") ? "markdown" : "text" 28 | if ( 29 | Array.isArray(props.value) && 30 | props.length > 0 && 31 | typeof props.value[0] === "object" 32 | ) 33 | return "json-array" 34 | if ( 35 | Array.isArray(props.value) && 36 | props.length > 0 && 37 | typeof props.value[0] === "string" 38 | ) 39 | return "text-array" 40 | if (typeof props.value === "object") return "json" 41 | if (typeof props.value === "number") return "numeric" 42 | if (typeof props.value === "boolean") return "boolean" 43 | } 44 | return props.defaultType || "text" 45 | }) 46 | 47 | return ( 48 |
59 | {typesOpen ? ( 60 |
61 | 57 | 58 |
59 |
60 | } 61 | /> 62 | ) 63 | } 64 | 65 | export default FileCell 66 | -------------------------------------------------------------------------------- /src/components/FilterCreater/index.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seveibar/react-watertable/a9219c15d29cf47a06a68d7d8e39743a17f3c19e/src/components/FilterCreater/index.js -------------------------------------------------------------------------------- /src/components/ImageCell/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import React from "react" 4 | import BaseCell from "../BaseCell" 5 | 6 | export const ImageCell = props => { 7 | return {props.value} 8 | } 9 | 10 | export default ImageCell 11 | -------------------------------------------------------------------------------- /src/components/JSONArrayCell/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import React, { useState } from "react" 4 | import BaseCell from "../BaseCell" 5 | import InputBase from "@material-ui/core/InputBase" 6 | import Button from "@material-ui/core/Button" 7 | import IconButton from "@material-ui/core/IconButton" 8 | import Dialog from "@material-ui/core/Dialog" 9 | import DialogContent from "@material-ui/core/DialogContent" 10 | import DialogTitle from "@material-ui/core/DialogTitle" 11 | import DialogActions from "@material-ui/core/DialogActions" 12 | import Watertable from "../Watertable" 13 | import { makeStyles } from "@material-ui/styles" 14 | import { grey } from "@material-ui/core/colors" 15 | import EditIcon from "@material-ui/icons/Edit" 16 | 17 | const useStyles = makeStyles({ 18 | soft: { 19 | color: grey[600] 20 | }, 21 | button: { marginLeft: 10 }, 22 | icon: { width: 16, height: 16, opacity: 0.5 } 23 | }) 24 | 25 | export const JSONArrayCell = props => { 26 | const c = useStyles() 27 | const [editing, changeEditing] = useState(false) 28 | let primaryKey 29 | if (props.schema) { 30 | const columns = Object.entries(props.schema).map(([id, def]) => ({ 31 | id, 32 | ...def 33 | })) 34 | const primaryCol = columns.find(col => col.primary) 35 | primaryKey = primaryCol ? primaryCol.id : undefined 36 | } 37 | return ( 38 | 39 |
({(props.value || []).length} Items)
40 | changeEditing(true)}> 41 | 42 | 43 | {editing && ( 44 | changeEditing(false)}> 45 |
46 | props.onChange(data)} 50 | schema={props.schema} 51 | data={props.value || []} 52 | /> 53 |
54 | 55 | 56 | 57 |
58 | )} 59 |
60 | ) 61 | } 62 | 63 | export default JSONArrayCell 64 | -------------------------------------------------------------------------------- /src/components/JSONCell/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import React, { useState } from "react" 4 | import BaseCell from "../BaseCell" 5 | import InputBase from "@material-ui/core/InputBase" 6 | import Button from "@material-ui/core/Button" 7 | import Dialog from "@material-ui/core/Dialog" 8 | import DialogContent from "@material-ui/core/DialogContent" 9 | import DialogTitle from "@material-ui/core/DialogTitle" 10 | import DialogActions from "@material-ui/core/DialogActions" 11 | import Waterobject from "../Waterobject" 12 | import { makeStyles } from "@material-ui/styles" 13 | import { grey } from "@material-ui/core/colors" 14 | import IconButton from "@material-ui/core/IconButton" 15 | import EditIcon from "@material-ui/icons/Edit" 16 | import JSONArrayCell from "../JSONArrayCell" 17 | 18 | const useStyles = makeStyles({ 19 | soft: { 20 | color: grey[600] 21 | }, 22 | button: { marginLeft: 10 }, 23 | icon: { width: 16, height: 16, opacity: 0.5 } 24 | }) 25 | 26 | export const JSONCell = props => { 27 | if (!props.schema && Array.isArray(props.value)) { 28 | return 29 | } 30 | const c = useStyles() 31 | const [editing, changeEditing] = useState(false) 32 | return ( 33 | 34 |
35 | ({Object.keys(props.value || {}).length} Properties) 36 |
37 | changeEditing(true)}> 38 | 39 | 40 | {editing && ( 41 | changeEditing(false)}> 42 |
43 | { 46 | props.onChange(newObject) 47 | }} 48 | downloadable={props.downloadable} 49 | schema={props.schema} 50 | data={props.value} 51 | /> 52 |
53 | 54 | 55 | 56 |
57 | )} 58 |
59 | ) 60 | } 61 | 62 | export default JSONCell 63 | -------------------------------------------------------------------------------- /src/components/MarkdownCell/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import React, { useState } from "react" 4 | import BaseCell from "../BaseCell" 5 | import InputBase from "@material-ui/core/InputBase" 6 | import Button from "@material-ui/core/Button" 7 | import Dialog from "@material-ui/core/Dialog" 8 | import DialogContent from "@material-ui/core/DialogContent" 9 | import DialogTitle from "@material-ui/core/DialogTitle" 10 | import DialogActions from "@material-ui/core/DialogActions" 11 | import Waterobject from "../Waterobject" 12 | import { makeStyles } from "@material-ui/styles" 13 | import { grey } from "@material-ui/core/colors" 14 | import ReactMde from "react-mde" 15 | import * as Showdown from "showdown" 16 | import "react-mde/lib/styles/css/react-mde-all.css" 17 | 18 | const useStyles = makeStyles({ 19 | soft: { 20 | color: grey[600] 21 | } 22 | }) 23 | 24 | const converter = new Showdown.Converter({ 25 | tables: true, 26 | simplifiedAutoLink: true, 27 | strikethrough: true, 28 | tasklists: true 29 | }) 30 | 31 | export const MarkdownCell = props => { 32 | const c = useStyles() 33 | const [selectedTab, changeSelectedTab] = useState("write") 34 | const [editing, changeEditing] = useState(false) 35 | return ( 36 | 37 | 40 | {editing && ( 41 | changeEditing(false)}> 42 |
43 | props.onChange(newValue)} 47 | value={props.value || ""} 48 | generateMarkdownPreview={md => 49 | Promise.resolve(converter.makeHtml(md)) 50 | } 51 | /> 52 |
53 | 54 | 55 | 56 |
57 | )} 58 |
59 | ) 60 | } 61 | 62 | export default MarkdownCell 63 | -------------------------------------------------------------------------------- /src/components/NumericCell/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import React, { useState } from "react" 4 | import BaseCell from "../BaseCell" 5 | import InputBase from "@material-ui/core/InputBase" 6 | 7 | export const NumericCell = props => { 8 | const [hasError, changeHasError] = useState(false) 9 | const [unsafeVal, changeUnsafeVal] = useState((props.value || "").toString()) 10 | return ( 11 | e.key === "Delete" && props.onChange(undefined)} 14 | readContent={props.value} 15 | editContent={ 16 |
17 | { 21 | changeUnsafeVal(e.target.value) 22 | if (!isNaN(e.target.value)) { 23 | props.onChange(parseFloat(e.target.value)) 24 | } 25 | }} 26 | autoFocus 27 | value={unsafeVal} 28 | /> 29 |
30 | } 31 | /> 32 | ) 33 | } 34 | 35 | export default NumericCell 36 | -------------------------------------------------------------------------------- /src/components/RawJSONEditor/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import React from "react" 4 | import { JsonEditor as Editor } from "jsoneditor-react" 5 | import "jsoneditor-react/es/editor.min.css" 6 | import ace from "brace" 7 | import "brace/mode/json" 8 | import "brace/theme/github" 9 | 10 | export const RawJSONEditor = ({ initialValue, onChange }) => { 11 | return ( 12 | { 16 | try { 17 | onChange(JSON.parse(t)) 18 | } catch (e) {} 19 | }} 20 | ace={ace} 21 | mode="code" 22 | allowedModes={["code", "tree"]} 23 | theme="ace/theme/github" 24 | /> 25 | ) 26 | } 27 | 28 | export default RawJSONEditor 29 | -------------------------------------------------------------------------------- /src/components/SearchBar/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import React from "react" 4 | import ConfigureButtonBase from "../ConfigureButtonBase" 5 | import SearchIcon from "@material-ui/icons/Search" 6 | 7 | export const SearchBar = () => { 8 | return null 9 | } 10 | 11 | export const SearchButton = () => { 12 | return 13 | } 14 | -------------------------------------------------------------------------------- /src/components/SelectCell/index.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React, { useMemo } from "react" 3 | import BaseCell from "../BaseCell" 4 | import Select from "react-select" 5 | import { grey, blue } from "@material-ui/core/colors" 6 | import { makeStyles } from "@material-ui/styles" 7 | import { softenColor } from "../../colors" 8 | 9 | const useStyles = makeStyles({ 10 | tagContainer: { display: "flex" }, 11 | tag: { 12 | margin: 4, 13 | padding: 4, 14 | paddingLeft: 6, 15 | paddingRight: 6, 16 | borderRadius: 4 17 | } 18 | }) 19 | 20 | export const customStyles = { 21 | container: provided => ({ 22 | ...provided, 23 | border: "none", 24 | backgroundColor: blue[50] 25 | }), 26 | control: provided => ({ 27 | ...provided, 28 | border: "none", 29 | boxShadow: "none", 30 | backgroundColor: blue[50] 31 | }), 32 | multiValueLabel: (provided, state) => ({ 33 | ...provided, 34 | backgroundColor: softenColor(state.data.color) 35 | }), 36 | multiValueRemove: (provided, state) => ({ 37 | ...provided, 38 | backgroundColor: softenColor(state.data.color) 39 | }), 40 | singleValue: (provided, state) => ({ 41 | ...provided, 42 | padding: 4, 43 | borderRadius: 4, 44 | backgroundColor: softenColor(state.data.color) 45 | }), 46 | option: (provided, state) => ({ 47 | ...provided, 48 | color: "rgba(0,0,0,0.8)", 49 | backgroundColor: softenColor(state.data.color) 50 | }), 51 | menuPortal: (provided, state) => ({ 52 | ...provided, 53 | zIndex: 99999 54 | }) 55 | } 56 | 57 | export const SelectCell = props => { 58 | const c = useStyles() 59 | if (!props.options) throw new Error("Missing Options for SelectCell") 60 | const optionMap = useMemo(() => { 61 | const om = {} 62 | for (let option of props.options) { 63 | om[option.value] = { 64 | ...option, 65 | color: softenColor(option.color) || grey[50] 66 | } 67 | } 68 | return om 69 | }, props.options) 70 | const values = 71 | props.value === undefined 72 | ? [] 73 | : typeof props.value === "string" 74 | ? [props.value] 75 | : props.value || [] 76 | 77 | const invalidValue = values.find(v => !optionMap[v]) 78 | if (invalidValue) 79 | throw new Error( 80 | `Value "${invalidValue}" not found in options. Options are: "${JSON.stringify( 81 | props.options 82 | )}"` 83 | ) 84 | 85 | return ( 86 | { 89 | if (props.multiple) return props.onChange([]) 90 | return props.onChange(undefined) 91 | }} 92 | readContent={ 93 | props.multiple ? ( 94 |
95 | {values.map((t, i) => ( 96 |
101 | {optionMap[t].label} 102 |
103 | ))} 104 |
105 | ) : ( 106 |
114 | {props.value ? optionMap[props.value].label : ""} 115 |
116 | ) 117 | } 118 | editContent={ 119 |
120 |