|
@@ -1,26 +1,221 @@
|
|
|
-import React from 'react';
|
|
|
+import React, {useState} from 'react';
|
|
|
import logo from './logo.svg';
|
|
|
import './App.css';
|
|
|
|
|
|
+import { GraphQLClient } from 'graphql-request';
|
|
|
+
|
|
|
+let gql = new GraphQLClient("/graphql", {headers: localStorage.authToken ? {Authorization: 'Bearer '+localStorage.authToken} : {}})
|
|
|
+
|
|
|
+
|
|
|
+//TODO: use graphql-tag to get types, or, at least detect query
|
|
|
+//for proper binding between model objects and result of gql query
|
|
|
+
|
|
|
+async function createModels(gql, config={create: 'Upsert', update: 'Upsert', delete: "Delete", findOne: "FindOne", find: 'Find', count: 'Count'}){
|
|
|
+ const getQuery = name =>
|
|
|
+ `query mutations{
|
|
|
+ __type(name: "${name}") {
|
|
|
+ name,
|
|
|
+ kind,
|
|
|
+ fields {
|
|
|
+ name,
|
|
|
+ type {
|
|
|
+ kind,
|
|
|
+ ofType{
|
|
|
+ name,
|
|
|
+ fields{
|
|
|
+ name,
|
|
|
+ type{
|
|
|
+ name,
|
|
|
+ kind
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ name,
|
|
|
+ fields{
|
|
|
+ name,
|
|
|
+ type{
|
|
|
+ name,
|
|
|
+ kind
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ args{
|
|
|
+ name,
|
|
|
+ type{
|
|
|
+ name,
|
|
|
+ inputFields{
|
|
|
+ name,
|
|
|
+ type{
|
|
|
+ name,
|
|
|
+ kind,
|
|
|
+ ofType{
|
|
|
+ name
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }`
|
|
|
+ let { __type: { fields: mutations }} = await gql.request(getQuery('Mutation'))
|
|
|
+ let { __type: { fields: queries }} = await gql.request(getQuery('Query'))
|
|
|
+ console.log(mutations)
|
|
|
+ console.log(queries)
|
|
|
+
|
|
|
+
|
|
|
+ let classes = {}
|
|
|
+
|
|
|
+ const createClass = (name, fields) => {
|
|
|
+ if (!(name in classes))
|
|
|
+ classes[name] = class {
|
|
|
+ constructor(data){
|
|
|
+ Object.assign(this, data)
|
|
|
+ }
|
|
|
+
|
|
|
+ static get fields(){
|
|
|
+ return fields
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return classes[name]
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ for (let query of queries){
|
|
|
+ let className = query.type.name || query.type.ofType.name
|
|
|
+ if (className === 'Int'){ //TODO: fuck this
|
|
|
+ className = query.name.match(new RegExp(`([A-Za-z]+)(${Object.values(config).join('|')})`))[1]
|
|
|
+ }
|
|
|
+ let classFields = query.type.fields || query.type.ofType && query.type.ofType.fields
|
|
|
+
|
|
|
+
|
|
|
+ console.log(classFields)
|
|
|
+
|
|
|
+ let _class = createClass(className, classFields)
|
|
|
+
|
|
|
+ for (let [method, methodName] of Object.entries(config)){
|
|
|
+ if (!_class[method] && query.name.includes(methodName)){
|
|
|
+ let methods = {
|
|
|
+ find(q, cursorCalls={}){
|
|
|
+ let queryText = `query FMFind($q:${query.args[0].type.name}){
|
|
|
+ ${query.name}(${query.args[0].name}: $q)
|
|
|
+ {${classFields.filter(x => x.type.kind === 'SCALAR').map(x => x.name).join(',')}}
|
|
|
+ }
|
|
|
+ `
|
|
|
+ console.log(queryText, {q: JSON.stringify([q, cursorCalls])})
|
|
|
+ return gql.request(queryText, {q: JSON.stringify([q, cursorCalls])}).then(data =>{
|
|
|
+ return data[query.name].map(record => new _class(record))
|
|
|
+ })
|
|
|
+ },
|
|
|
+ findOne(query){
|
|
|
+
|
|
|
+ },
|
|
|
+ count(q, cursorCalls={}){
|
|
|
+ let queryText = `query FMFind($q:${query.args[0].type.name}){
|
|
|
+ ${query.name}(${query.args[0].name}: $q)
|
|
|
+ }
|
|
|
+ `
|
|
|
+ return gql.request(queryText, {q: JSON.stringify([q, cursorCalls])}).then(data =>{
|
|
|
+ return data[query.name]
|
|
|
+ })
|
|
|
+ }
|
|
|
+ }
|
|
|
+ _class[method] = methods[method]
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ return classes
|
|
|
+}
|
|
|
+
|
|
|
+const dataReader = async () => {
|
|
|
+ const { PriceItem } = await createModels(gql)
|
|
|
+ let data = await PriceItem.find({}, {limit: [10]})
|
|
|
+ return {data, PriceItem }
|
|
|
+}
|
|
|
+
|
|
|
+const Th = p =>
|
|
|
+<div className='Th' {...p} />
|
|
|
+
|
|
|
+const Row = ({top, children}) =>
|
|
|
+<div className='Row' style={{position: 'relative',top}}>
|
|
|
+ {children}
|
|
|
+</div>
|
|
|
+
|
|
|
+const Cell = ({children}) =>
|
|
|
+<div className='Cell'>
|
|
|
+ {children}
|
|
|
+</div>
|
|
|
+
|
|
|
+
|
|
|
+function ModelGrid({model, rowHeight, gridHeight, overload}){
|
|
|
+ const onScreenRowCount = gridHeight/rowHeight
|
|
|
+ const overloadedRowCount = overload * onScreenRowCount
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+ const [totalCount, setTotalCount] = useState()
|
|
|
+ totalCount || model.count({}).then(count => setTotalCount(count))
|
|
|
+
|
|
|
+ const [gridContentRef, setGridContentRef] = useState()
|
|
|
+ const [scroll, setScroll] = useState(0)
|
|
|
+
|
|
|
+
|
|
|
+ const onScreenFirstRowIndex = Math.floor(scroll/rowHeight)
|
|
|
+ let skip = onScreenFirstRowIndex - (overloadedRowCount - onScreenRowCount)/2;
|
|
|
+
|
|
|
+ if (skip < 0) skip = 0
|
|
|
+
|
|
|
+ const [sort, setSort] = useState([])
|
|
|
+
|
|
|
+ const [records, setRecords] = useState()
|
|
|
+ const cursorCalls = {skip: [skip], limit: [overloadedRowCount]}
|
|
|
+
|
|
|
+ if (sort.length) cursorCalls.sort = [sort]
|
|
|
+
|
|
|
+ records || model.find({}, cursorCalls)
|
|
|
+ .then(records => setRecords(records))
|
|
|
+
|
|
|
+
|
|
|
+ return(
|
|
|
+ <>
|
|
|
+ <div className='GridHeader'>
|
|
|
+ {model.fields.map(field => <Th onClick={e => (setSort(field.name === sort[0] ? [sort[0], -sort[1]] : [field.name, 1]), setRecords(null))}>{field.name}</Th>)}
|
|
|
+ </div>
|
|
|
+ <div className='GridViewport'
|
|
|
+ onScroll={e => {
|
|
|
+ if (Math.abs(e.target.scrollTop - scroll) > gridHeight*overload/3){
|
|
|
+ setScroll(e.target.scrollTop);
|
|
|
+ setRecords(null)
|
|
|
+ } }}
|
|
|
+ style={{maxHeight: gridHeight, height: gridHeight}}>
|
|
|
+
|
|
|
+ <div className='GridContent'
|
|
|
+ style={{height: totalCount*rowHeight}}
|
|
|
+ ref={e => setGridContentRef(e)}>
|
|
|
+ {records && records.map((record,i) =>
|
|
|
+ <Row top={(skip)*rowHeight}>
|
|
|
+ {model.fields.map(field =>
|
|
|
+ <Cell>
|
|
|
+ {record[field.name]}
|
|
|
+ </Cell>)}
|
|
|
+ </Row>)}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </>
|
|
|
+ );
|
|
|
+}
|
|
|
+
|
|
|
function App() {
|
|
|
- return (
|
|
|
- <div className="App">
|
|
|
- <header className="App-header">
|
|
|
- <img src={logo} className="App-logo" alt="logo" />
|
|
|
- <p>
|
|
|
- Edit <code>src/App.js</code> and save to reload.
|
|
|
- </p>
|
|
|
- <a
|
|
|
- className="App-link"
|
|
|
- href="https://reactjs.org"
|
|
|
- target="_blank"
|
|
|
- rel="noopener noreferrer"
|
|
|
- >
|
|
|
- Learn React
|
|
|
- </a>
|
|
|
- </header>
|
|
|
- </div>
|
|
|
- );
|
|
|
+ let [models, setModels] = useState()
|
|
|
+ models || createModels(gql).then(models => setModels(models))
|
|
|
+
|
|
|
+ return (
|
|
|
+ <div className="App">
|
|
|
+ {models && <ModelGrid model={models.PriceItem} rowHeight={50} gridHeight={800} overload={5}/> }
|
|
|
+ </div>
|
|
|
+ );
|
|
|
}
|
|
|
|
|
|
export default App;
|