123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527 |
- import React, {useState, useEffect, useMemo, useRef, Fragment} from 'react';
- import logo from './logo.svg';
- import './App.css';
- import Select from 'react-select/async'
- import {sortableContainer, sortableElement} from 'react-sortable-hoc';
- import Dropzone from 'react-dropzone'
- import { GraphQLClient } from 'graphql-request';
- import createModels2 from './front-models'
- let gql;
- const getGQL = () => new GraphQLClient(localStorage.url + 'graphql', {headers: localStorage.authToken ? {Authorization: 'Bearer '+localStorage.authToken} : {}})
- if (localStorage.authToken && localStorage.url){
- gql = getGQL()
- }
- const ShortName = ({record, options}) => {
- if (record && record.constructor)
- if (options.view.relations[record.constructor.name]){
- const Formatter = options.view.relations[record.constructor.name]
- return <Formatter options={options}>{record}</Formatter>
- }
- return (<>{record && (record.name || record.key || record.login || record.originalFileName || record._id)}</>)
- }
- const EditFormRow = ({field, models={}, value, options=defaultAdminOptions, record, onChange}) => {
- return (
- <tr>
- <th>{field.name}</th>
- <td><Cell models={models} model={getModelByField(field, models)} options={options} record={record} field={field} selected onChange={onChange}>{value}</Cell></td>
- </tr>
- )
- }
- const EditForm = ({record, models={}, model, options=defaultAdminOptions, Components:{EditFormRow:EFR}={EditFormRow}}) => {
- const [edit, setEdit] = useState({...record})
- if (!record) return <></>
- if (!record.save || !record._id){
- Object.assign(record, edit)
- }
- const fields = model.fields
- const onlyInputs = model.inputs.filter(input => !fields.find(field => field.name === input.name))
- return (
- <>
- <table>
- <tbody>
- {Object.entries(fields).map(([key,field]) => <EFR key={key} field={field}
- models={models}
- record={record}
- value={edit[field.name]}
- options={options}
- onChange={event => {
- setEdit({...edit, [field.name]: event && event.target ? event.target.value : event})
- }}
- />)}
- {Object.entries(onlyInputs).map(([key,field]) => <EFR key={key} field={field}
- models={models}
- record={record}
- value={edit[field.name]}
- options={options}
- onChange={event => setEdit({...edit, [field.name]: event && event.target ? event.target.value : event})}
- />)}
- </tbody>
- </table>
- {record._id && record.save &&
- <button onClick={() => {
- Object.assign(record, edit);
- if ('_id' in record && 'save' in record)
- record.save()
- }}>
- Save
- </button>}
- </>
- )
- }
- const Row = ({top, selected, children}) => {
- console.log('row')
- return (
- <>
- <div className={`Row ${selected ? 'selected' : ''}` } >
- {children}
- </div>
- </>
- )
- }
- const Cell = ({children, options, record, field, selected, model, models={}, onClick, ...props}) => {
-
- let Formatter = React.Fragment
- const type = (children && typeof children === 'object' && children.constructor) || model
- const viewOrEdit = (!record.constructor.inputs || record.constructor.inputs.some(input => input.name === field.name)) && selected ? 'edit' : 'view'
- const typeName = field.type.name || field.type.ofType.name
- if (typeName in options[viewOrEdit].formatters){
- Formatter = options[viewOrEdit].formatters[typeName]
- }
- if (field.name in options[viewOrEdit].fields){
- Formatter = options[viewOrEdit].fields[field.name]
- }
- if (type){
- if (type.name in options[viewOrEdit].formatters)
- Formatter = options[viewOrEdit].formatters[type.name]
- else
- Formatter = options[viewOrEdit].formatters.Object
- }
- return(
- <div className='Cell' onClick={onClick}>
- <Formatter record={record} options={options} model={model} models={models} {...props} field={field}>
- {Formatter === React.Fragment ? (children && children.toString()) : children}
- </Formatter>
- </div>
- )
- }
- const ModelListItem = "div"
- const ModelList = ({models, selected, onChange, Item=ModelListItem}) => {
- return (
- <aside>
- {Object.entries(models).map(([name, model]) => {
- return (
- <Item key={name}
- selected={name === selected}
- className={name === selected ? "selected" : undefined}
- onClick = {() => onChange(name)}
- >
- {name}
- </Item>
- )
- })}
- </aside>
- )
- }
- const Search = ({...props}) =>
- <input placeholder="Search..." {...props} className="Search"/>
- const Count = ({...props}) => <div className="Count" {...props} />
- const searchQueryBuilder = (search, model) => {
- if (!search) return {}
- const queryRegexp = `/${search.trim().split(/\s+/).join('|')}/`
- return {$or: model.fields.filter(field => field.type.kind == 'SCALAR').map(field => ({[field.name]: queryRegexp}))}
- }
- const GridHeaderItem = ({field, sort:[sort], ...props}) =>
- <div className="GridHeaderItem" {...props}>
- {field.name in sort && sort[field.name] === -1 && '^ '}
- {field.name}
- {field.name in sort && sort[field.name] === 1 && ' v'}
- </div>
- const GridHeader = ({fields, sort, onSort}) =>
- <div className="GridHeader">
- {fields.map(field => <GridHeaderItem key={field.name} field={field} sort={sort} onClick={() => onSort(field.name)}/>)}
- </div>
- const getModelByField = (field, models={}) => {
- if (field.type.kind === 'OBJECT') {
- return models[field.type.name]
- }
- if (field.type.kind === 'LIST') return models[field.type.ofType.name]
- }
- const VirtualScroll = ({options, gridHeight, count, rowHeight, onScroll, records, skip, models={}}) => {
- const limit = gridHeight/rowHeight
- //const {Row, Cell} = Components
- const [edit, setEdit] = useState({field: null, record: null})
- useEffect(() => setEdit({field: null, record: null}), [records])
- const fields = records && records[0] && records[0].constructor.fields
- //console.log(Fragment, Row)
- return (
- <>
- <div className='GridViewport'
- style={{maxHeight: gridHeight, height: gridHeight}} >
- <div className='GridContent'
- style={{height: count*rowHeight, minHeight: count*rowHeight, maxHeight: count*rowHeight}} >
- {records && records.map((record,i) =><Fragment key={i}>
- {edit.record && edit.record._id === record._id ? <EditForm model={record.constructor} models={models} record={record} options={options}/>:
- <Row options={options}>
- {fields.map(field =>
- <Cell models={models} model={getModelByField(field, models)} record={record} key={field.name} field={field} options={options}
- onClick={() => setEdit({record: {...record}, field})}>
- {record[field.name]}
- </Cell>)}
- </Row>}
- </Fragment>
- )}
- </div>
- </div>
- </>
- )
- }
- const ModelView = ({model, models={}, options, components:Components={Search, Count, GridHeader, Grid:VirtualScroll}, rowHeight=150, gridHeight=500, overload=2}) => {
- const [records, setRecords] = useState([])
- const [search, setSearch] = useState("")
- const [count, setCount] = useState(0)
- const [query, setQuery] = useState({})
- const [cursorCalls, setCursorCalls] = useState({sort:[{}], skip: [0]/*, limit: [gridHeight/rowHeight]*/})
- const skip = cursorCalls.skip[0]
- console.log(cursorCalls)
- useEffect(() => {
- model.count(query, cursorCalls).then(count => setCount(count))
- model.find(query, cursorCalls).then(records => Promise.all(records)).then(records => setRecords(records))
- }, [query, model, cursorCalls])
- const timeout = useRef(0)
- useEffect(() => {
- clearInterval(timeout.current)
- if (!search)
- setQuery(searchQueryBuilder(search, model))
- else {
- timeout.current = setTimeout(() => {
- setQuery(searchQueryBuilder(search, model))
- },1000)
- }
- },[search, model])
- //console.log(Components)
- //
- const Add = options.add[model.name] || "button"
- return (
- <>
- <Components.Search options={options} value={search} onChange={({target: {value}}) => setSearch(value)}/>
- <Components.Count options={options}>
- {count}
- </Components.Count>
- <Components.GridHeader fields={model.fields}
- sort={cursorCalls.sort}
- onSort={sort => setCursorCalls({...cursorCalls,
- sort: [{[sort]: cursorCalls.sort[0][sort] === 1 ? -1 : 1}]
- })}/>
- <Add onClick={() => {
- if (Add === 'button') (new model).save()
- setCursorCalls({...cursorCalls, sort: [{_id: -1}]})
- setSearch('')
- }} model={model}>+</Add>
- {records && <Components.Grid
- models={models}
- options={options}
- skip={skip}
- count={count}
- records={records}
- gridHeight={700}
- rowHeight={50}
- onScroll={(skip, limit) => {
- limit = undefined;
- model.find(query, {...cursorCalls, limit: [limit], skip: [skip]}).then(records => Promise.all(records)).then(records => setRecords(records)).then(() =>
- setCursorCalls({...cursorCalls, limit: [limit], skip: [skip]}))
- }}/> }
- </>
- )
- }
- const ObjectShortView = ({children, options}) => {
- const [record, setRecord] = useState(children)
- if (!children) return <></>
- if (typeof children === 'object' && 'then' in children){
- children.then(child => setRecord({...child}))
- }
- if (typeof children === 'object' && children._id){
- return (
- <div className="ObjectShortView">
- <ShortName record={record} options={options}/>
- </div>
- )
- }
- else {
- return (
- <pre>
- {JSON.stringify(record, null, 4)}
- </pre>
- )
- }
- }
- const ForeignAutocomplete = ({models={}, children:value, onChange, model, options={}}) => {
- return (
- <Select cacheOptions
- defaultOptions
- placeholder={model.name}
- loadOptions={async search => {
- const suggesions = await model.find(searchQueryBuilder(search, model)) || []
- return [{_id: null, key: 'REMOVE'}, ...suggesions]//.filter(record => !exclude.find(e => (e === record._id) || (e._id === record._id)))
- .map((record) => ({value: record._id, label: <ShortName record={record} options={options}/>, record}))
-
- }}
- value={value && {value: value._id, label: <ShortName record={value} options={options}/>, record: value}}
- onChange={(obj) => {
- onChange(obj.record._id ? obj.record : null)
- }}
- />
- )
- }
- const ObjectShortEdit = ({...props}) =>
- <>
- <ForeignAutocomplete {...props}/>
- </>
- const SortableCell = sortableElement(({onAdd, onDelete, onCh, ...props}) =>
- <div className='SortableCell'>
- <button onClick={onAdd}>+</button>
- <button onClick={onDelete}>x</button>
- <Cell onChange={onCh} {...props} />
- </div>)
- const SortableContainer = sortableContainer(({children}) => {
- return <div>{children}</div>;
- });
- const arrayMove = (arr, newIndex, oldIndex) => {
- const withoutOld = arr.filter((item, i) => i !== oldIndex)
- return [...withoutOld.slice(0, newIndex), arr[oldIndex], ...withoutOld.slice(newIndex)]
- }
- const defaultAdminOptions =
- {
- view: {
- formatters:{
- ID: ({children}) => <b>{children && children.slice(-6).toUpperCase()}</b>,
- String: ({children}) => <>{children && children.length > 100 ? children.slice(0,100) + '...' : children}</>,
- Object: ObjectShortView,
- Array: ({children, ...props}) => <>{children.map((child,i) => <ObjectShortView key={child && (child._id || child.key || child.name) || i} children={child} {...props}/>)}</>
- },
- fields:{
- createdAt: ({children}) => <>{new Date(+children).toISOString()}</> ,
- url: ({children,record}, type, url) => <>
- {(url = localStorage.url + record.url), null}
- {(type = (record.mimeType && record.mimeType.split('/')[0]) || 'image') === 'image' ?
- <img className="rowView" src={url}/> :
- <video className="rowView" autoplay controls>
- <source src={url}/>
- </video>}
- <a href={children} className='Media'>
- {children}
- </a></>,
- color: (props) => <span style={{backgroundColor: props.value}}>{props.value}</span>,
- size: ({children}) => <>{(['B', 'K','M','G','T'].reduce((result, letter, i, value) => result ? result : (value = (children/1024**i)) < 1024 ? `${value.toFixed(2)} ${letter}`: '', ''))}</>
- },
- relations: {
- Image: ({children}) => <span className="ImageRelation"><img src={localStorage.url + children.url}/>{children.originalFileName}</span>,
- Content: ({children}) => <span className="ImageRelation"><img src={localStorage.url + children.url}/>{children.originalFileName}</span>
- },
- models: {
- }
- },
- edit: {
- formatters:{
- ID: ({children}) => <b>{children && children.slice(-6).toUpperCase()}</b>,
- String: ({children, field, ...props}) => <textarea placeholder={field.name} value={children} {...props}/>,
- Int: ({children, ...props}) => <input type='number' value={children} {...props}/>,
- Float: ({children, ...props}) => <input type='number' value={children} {...props} onChange={(e) => props.onChange(+e.target.value)}/>,
- Object: ({children, options, model, models, ...props}) => {
- return model.fields[0].name === '_id' ? <ObjectShortEdit children={children} options={options} model={model} {...props}/> : <EditForm model={model} models={models} record={children} options={options} />
- },
- Array: ({children, onChange, model, ...props}) => {
- const newItem = !model || (model && model.fields[0].name === '_id') ? null : new model
- return (<><SortableContainer
- pressDelay={200}
- onSortEnd={({newIndex, oldIndex}) => {
- onChange(arrayMove(children, newIndex, oldIndex))
- }}>{children.map((child, i) =>
- <SortableCell onDelete={() => onChange(children.filter((item, j) => j !== i))}
- onAdd={() => onChange([...children.slice(0, i),newItem,...children.slice(i)])}
- index={i} key={(child && (child._id || child.key || child.name)) || i} model={model} {...props} children={child} selected
- onCh={data => {
- data = (data && data.target) ? data.target.value : data
- console.log(data, children)
- if (!children.find(record => record && record._id && record._id === data._id)){
- const copy = [...children]
- copy[i] = data
- onChange(copy)
- }
- }}
- />)}
- </SortableContainer>
- <button onClick={() => onChange([...children, newItem])}>+</button></>)
- }
- },
- fields:{
- createdAt: ({children}) => <input value={new Date(+children).toISOString()} />,
- url: ({children}) => <a href={children}>{children}</a>,
- color:({children, ...props}) => <input type='color' value={children} {...props}/>
- },
-
- models: {
- }
- },
- add: {
- Content({model, onClick}){
- return (
- <Dropzone onDrop={async acceptedFiles => {
- await Promise.all(acceptedFiles.map(file => {
- const body = new FormData
- body.append('upload',file)
- fetch(`${localStorage.url}upload`, {
- method: "POST",
- headers: localStorage.authToken ? {Authorization: 'Bearer ' + localStorage.authToken} : {},
- body
- })
- }))
- onClick()
- }
- }>
- {({getRootProps, getInputProps}) => (
- <section className='drop'>
- <div {...getRootProps()}>
- <input {...getInputProps()} />
- <p>Drag 'n' drop some files here, or click to select files</p>
- </div>
- </section>
- )}
- </Dropzone>
- )
- }
- }
- }
- const Admin = ({models, components:{ModelList:ML=ModelList, Search:S=Search, ModelListItem:MLI=ModelListItem, ModelView:MV=ModelView, Count:C=Count, GridHeader:GH=GridHeader, Grid:G=VirtualScroll}={},
- options=defaultAdminOptions
- }) => {
- const [selected, setSelected] = useState()
- const mergedOptions = {...defaultAdminOptions, ...options}
- return (
- <>
- <ML Item={MLI} models={models} onChange={(name) => setSelected(name)} selected={selected}/>
- <content>
- {selected && <MV models={models} options={mergedOptions} model={models[selected]} components={{Search: S, Count: C, GridHeader:GH, Grid:G}} /> }
- </content>
- </>
- )
- }
- const LoginForm = ({onLogin}) => {
- const [url, setUrl] = useState(localStorage.url)
- const [login, setLogin] = useState('')
- const [password, setPassword] = useState('')
- return (
- <>
- <input placeholder="url" value={url} onChange={e => setUrl(e.target.value)}/>
- <input placeholder='login' value={login} onChange={e => setLogin(e.target.value)}/>
- <input type='password' placeholder='password' value={password} onChange={e => setPassword(e.target.value)}/>
- <button onClick={() => {
- onLogin({url, login, password})
- }}>Login</button>
- </>
- )
- }
- function App() {
- let [models, setModels] = useState()
- models || createModels2(gql).then(models => setModels(models))
- const classes = models
- return (
- <>
- <LoginForm onLogin={async ({url, login, password}) => {
- let gql = new GraphQLClient(url + 'graphql')
- let token = await gql.request(`query login ($login: String, $password: String){
- login(login: $login, password: $password)
- }`, {login, password})
- if (token.login){
- localStorage.authToken = token.login
- localStorage.url = url
- }
- else {
- localStorage.removeItem('authToken')
- }
- gql = getGQL()
- createModels2(gql).then(models => setModels(models))
- }}/>
- <div className="App">
- {models && <Admin models={models} />}
- </div>
- </>
- );
- }
- export default App;
|