瀏覽代碼

basic virtual scroll and sort

Ivan Asmer 5 年之前
父節點
當前提交
f16ee34ba1
共有 4 個文件被更改,包括 307 次插入27 次删除
  1. 50 7
      package-lock.json
  2. 3 1
      package.json
  3. 40 0
      src/App.css
  4. 214 19
      src/App.js

+ 50 - 7
package-lock.json

@@ -2800,11 +2800,13 @@
             },
             "balanced-match": {
               "version": "1.0.0",
-              "bundled": true
+              "bundled": true,
+              "optional": true
             },
             "brace-expansion": {
               "version": "1.1.11",
               "bundled": true,
+              "optional": true,
               "requires": {
                 "balanced-match": "^1.0.0",
                 "concat-map": "0.0.1"
@@ -2817,15 +2819,18 @@
             },
             "code-point-at": {
               "version": "1.1.0",
-              "bundled": true
+              "bundled": true,
+              "optional": true
             },
             "concat-map": {
               "version": "0.0.1",
-              "bundled": true
+              "bundled": true,
+              "optional": true
             },
             "console-control-strings": {
               "version": "1.1.0",
-              "bundled": true
+              "bundled": true,
+              "optional": true
             },
             "core-util-is": {
               "version": "1.0.2",
@@ -2928,7 +2933,8 @@
             },
             "inherits": {
               "version": "2.0.3",
-              "bundled": true
+              "bundled": true,
+              "optional": true
             },
             "ini": {
               "version": "1.3.5",
@@ -2938,6 +2944,7 @@
             "is-fullwidth-code-point": {
               "version": "1.0.0",
               "bundled": true,
+              "optional": true,
               "requires": {
                 "number-is-nan": "^1.0.0"
               }
@@ -2950,17 +2957,20 @@
             "minimatch": {
               "version": "3.0.4",
               "bundled": true,
+              "optional": true,
               "requires": {
                 "brace-expansion": "^1.1.7"
               }
             },
             "minimist": {
               "version": "0.0.8",
-              "bundled": true
+              "bundled": true,
+              "optional": true
             },
             "minipass": {
               "version": "2.3.5",
               "bundled": true,
+              "optional": true,
               "requires": {
                 "safe-buffer": "^5.1.2",
                 "yallist": "^3.0.0"
@@ -2977,6 +2987,7 @@
             "mkdirp": {
               "version": "0.5.1",
               "bundled": true,
+              "optional": true,
               "requires": {
                 "minimist": "0.0.8"
               }
@@ -3049,7 +3060,8 @@
             },
             "number-is-nan": {
               "version": "1.0.1",
-              "bundled": true
+              "bundled": true,
+              "optional": true
             },
             "object-assign": {
               "version": "4.1.1",
@@ -3059,6 +3071,7 @@
             "once": {
               "version": "1.4.0",
               "bundled": true,
+              "optional": true,
               "requires": {
                 "wrappy": "1"
               }
@@ -3164,6 +3177,7 @@
             "string-width": {
               "version": "1.0.2",
               "bundled": true,
+              "optional": true,
               "requires": {
                 "code-point-at": "^1.0.0",
                 "is-fullwidth-code-point": "^1.0.0",
@@ -3627,6 +3641,22 @@
         "sha.js": "^2.4.8"
       }
     },
+    "cross-fetch": {
+      "version": "2.2.2",
+      "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-2.2.2.tgz",
+      "integrity": "sha1-pH/09/xxLauo9qaVoRyUhEDUVyM=",
+      "requires": {
+        "node-fetch": "2.1.2",
+        "whatwg-fetch": "2.0.4"
+      },
+      "dependencies": {
+        "whatwg-fetch": {
+          "version": "2.0.4",
+          "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-2.0.4.tgz",
+          "integrity": "sha512-dcQ1GWpOD/eEQ97k66aiEVpNnapVj90/+R+SXTPYGHpYBBypfKJEQjLrvMZ7YXbKm21gXd4NcuxUTjiv1YtLng=="
+        }
+      }
+    },
     "cross-spawn": {
       "version": "6.0.5",
       "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz",
@@ -5558,6 +5588,14 @@
       "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.0.tgz",
       "integrity": "sha512-jpSvDPV4Cq/bgtpndIWbI5hmYxhQGHPC4d4cqBPb4DLniCfhJokdXhwhaDuLBGLQdvvRum/UiX6ECVIPvDXqdg=="
     },
+    "graphql-request": {
+      "version": "1.8.2",
+      "resolved": "https://registry.npmjs.org/graphql-request/-/graphql-request-1.8.2.tgz",
+      "integrity": "sha512-dDX2M+VMsxXFCmUX0Vo0TopIZIX4ggzOtiCsThgtrKR4niiaagsGTDIHj3fsOMFETpa064vzovI+4YV4QnMbcg==",
+      "requires": {
+        "cross-fetch": "2.2.2"
+      }
+    },
     "growly": {
       "version": "1.3.0",
       "resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz",
@@ -8173,6 +8211,11 @@
         "lower-case": "^1.1.1"
       }
     },
+    "node-fetch": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.1.2.tgz",
+      "integrity": "sha1-q4hOjn5X44qUR1POxwb3iNF2i7U="
+    },
     "node-forge": {
       "version": "0.7.5",
       "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.7.5.tgz",

+ 3 - 1
package.json

@@ -3,6 +3,7 @@
   "version": "0.1.0",
   "private": true,
   "dependencies": {
+    "graphql-request": "^1.8.2",
     "react": "^16.8.6",
     "react-dom": "^16.8.6",
     "react-scripts": "3.0.1"
@@ -27,5 +28,6 @@
       "last 1 firefox version",
       "last 1 safari version"
     ]
-  }
+  },
+  "proxy": "http://localhost:4000/"
 }

+ 40 - 0
src/App.css

@@ -31,3 +31,43 @@
     transform: rotate(360deg);
   }
 }
+
+.GridHeader {
+    display: flex;
+    justify-content: space-between;
+}
+
+.Row {
+    display: flex;
+    justify-content: space-between;
+}
+
+.GridContent {
+}
+
+
+.GridViewport {
+    /*max-height: 500px;*/
+    /*height: 500px;*/
+    overflow: auto;
+}
+
+.Cell {
+    max-width: 200px;
+    width: 200px;
+    overflow: hidden;
+    font-size: 0.8em;
+}
+
+.Th {
+    max-width: 200px;
+    width: 200px;
+    overflow: hidden;
+    font-size: 1.2em;
+    font-weight: bold;
+}
+
+.Row {
+    max-height: 50px;
+    height: 50px;
+}

+ 214 - 19
src/App.js

@@ -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;