Przeglądaj źródła

NamedRoute now supports queryPub query function with refresh

asmer 1 miesiąc temu
rodzic
commit
ede5666107
6 zmienionych plików z 371 dodań i 206 usunięć
  1. 18 0
      .eslintrc.cjs
  2. 24 0
      .gitignore
  3. 60 200
      src/App.tsx
  4. 10 0
      src/lib/pub/query/index.ts
  5. 32 6
      src/lib/router/index.tsx
  6. 227 0
      src/test/queryPubNamedRoute.tsx

+ 18 - 0
.eslintrc.cjs

@@ -0,0 +1,18 @@
+module.exports = {
+  root: true,
+  env: { browser: true, es2020: true },
+  extends: [
+    'eslint:recommended',
+    'plugin:@typescript-eslint/recommended',
+    'plugin:react-hooks/recommended',
+  ],
+  ignorePatterns: ['dist', '.eslintrc.cjs'],
+  parser: '@typescript-eslint/parser',
+  plugins: ['react-refresh'],
+  rules: {
+    'react-refresh/only-export-components': [
+      'warn',
+      { allowConstantExport: true },
+    ],
+  },
+}

+ 24 - 0
.gitignore

@@ -0,0 +1,24 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?

+ 60 - 200
src/App.tsx

@@ -1,218 +1,78 @@
-import { useState, useEffect, Fragment } from 'react'
+import { useState, useEffect } from 'react'
+import reactLogo from './assets/react.svg'
+import viteLogo from '/vite.svg'
 import './App.css'
 import {createBrowserHistory} from 'history';
-import {Router, Link} from 'react-router-dom'
-import {createQueryPub, NamedRoute, NamedLink, usePub} from './lib';
-import {MetaPlate} from './lib';
-
+import {Router} from 'react-router-dom'
+import {NamedRoute, NamedLink, HashRoute, createPrivateRoute} from './lib';
 
 const history = createBrowserHistory()
 
-const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOnsiaWQiOiI2NjdmZjBkYTIyMzIxMjBjN2M4NzFmN2MiLCJsb2dpbiI6InRzdDE5IiwiYWNsIjpbIjY2N2ZmMGRhMjIzMjEyMGM3Yzg3MWY3YyIsInVzZXIiXX0sImlhdCI6MTcyMDI1OTEwN30.eDcInxQlskt32LjaCe5sdCcJ3B97BKYZtgUARxThYQM'
-
-console.log(JSON.parse(atob(token.split('.')[1])).sub.id)
-
-const shopUrl = 'http://shop-roles.node.ed.asmer.org.ua/'
-
-const gql = (query, variables={}) => 
-    fetch(shopUrl + 'graphql',{
-        method: 'POST',
-        headers: {
-            'Content-Type': 'application/json',
-            Accept: 'application/json',
-            Authorization: 'Bearer ' + token
-        },
-        body: JSON.stringify({query, variables})
-    }).then(res => res.json())
-
-const {queryPub, createQuery, withQueryFunc} = createQueryPub({
-    queryFunc: gql,
-    cacheTimeout: 5000,
-})
-
-const {useCategoriesQuery} = createQuery({
-    promiseName: 'categories',
-    args: [`query MainCategories{
-              CategoryFind(query: "[{}]"){
-                _id name
-                  image{
-                    url
-                  }
-                goods{
-                _id
-                name description price images{
-                  url
-                }
-              }
-              subCategories{ name _id}
-            }
-        }
-    `]
-})
-
-const {useMainCategoriesQuery} = createQuery({
-    promiseName: 'mainCategories',
-    args: [`query MainCategories{
-              CategoryFind(query: "[{\\"parent\\":null}]"){
-                _id name
-                  image{
-                    url
-                  }
-                goods{
-                _id
-                name description price images{
-                  url
-                }
-              }
-              subCategories{ name _id}
-            }
-        }
-    `]
-})
-
-
-const {query: categoryQuery, useCategoryQuery} = createQuery({
-    promiseName: 'category',
-    args: ({_id}) => [`query categoryById($query:String){
-              CategoryFindOne(query: $query){
-                _id name
-                  image{
-                    url
-                  }
-                goods{
-                _id
-                name description price images{
-                  url
-                }
-              }
-              subCategories{ name _id}
-            }
-        }
-    `, {query: JSON.stringify([{_id}])}]
-})
-
-
-//queryPub.subscribe(() => console.log(JSON.stringify(queryPub.queries.user, null, 4)))
-//
-
-
-
-const Image = ({src}) => <img src={`${shopUrl}${src}`} style={{maxWidth: '50%'}}/>
-
-const Good = ({good, children}) => {
-    useEffect(() => {
-        console.log('good mount', good.name)
-    },[])
-    return (
-        good ? 
-            <div className='Good'>
-                <h2>{good.name}</h2>
-                <p>{good.description}</p>
-                <span>{good.price}</span>
-                {children}
-            </div>
-        : <h3>null</h3>
-    )
-}
-
-const Category = ({category:{name, _id}, children}) =>
-<div className='Category'>
-    <h1>{name}</h1>
-    {_id}
-    {children}
-</div>
-
-const Categories = ({children, data}) => 
-<div style={{border: '2px solid', paddingLeft: '20px'}}>
-    <h1>categories({data && data.length})</h1>
-    {children}
+const SwapiPeople = ({data:{name, eye_color}}) =>
+<div>
+    <h3 style={{color: eye_color}}>{name}</h3>
+    <HashRoute path="/foo"  component={() => <h1>FOO</h1>} />
+    <HashRoute path="/bar"  component={() => <h1>BAR</h1>} />
+    <a href="#/foo">go to foo</a>
+    <a href="#/bar">go to bar</a>
 </div>
 
-const Goods = ({children}) => 
-<div style={{border: '2px solid blue', paddingLeft: '10px'}}>
-    <h2>goods</h2>
-    {children}
+const SwapiPlanet = ({data:{name, diameter}}) =>
+<div>
+    <h3>{name}</h3>
+    <h4>{diameter}</h4>
 </div>
 
-const PageCategory = ({data}) => {
-    //usePub(queryPub) //IS IT REQUIRED?, or route mechanics enough (looks like this required for auto-refresh queries in PUB)
-    //if its required, is useCategoryQuery gives same result?
-    //yes, exactly. it required due to auto update of queryPub.
-    //
-    //useCategoryQuery({_id: useParams()._id}) //here we need id from url...
-    console.log('page category update')
-    return (
-    <>
-        <MetaPlate data={data.payload.data.CategoryFindOne}>
-            <Category prop="category" keyDataKey="_id">
-                <Goods sourceDataKey="goods"> 
-                    <Good prop="good" keyDataKey="_id">
-                        <div sourceDataKey="images">
-                            <Image prop="src" sourceDataKey="url"/>
-                        </div>
-                    </Good>
-                </Goods>
-                <Categories sourceDataKey="subCategories" prop='data'>
-                    <LeftMenuItem prop="category" keyDataKey="_id" />
-                </Categories>
-            </Category>
-        </MetaPlate> 
-    </>
-    )
-}
-
-const LeftMenuItem = ({category:{_id,name}})=>
-<li><NamedLink routeName='category' params={{_id}}>{name}</NamedLink></li>
-//<li><Link  to={`/category/${_id}`}>{name}</Link></li>
-
-const LeftMenu = () => {
-    const {payload} = useMainCategoriesQuery()
-    return (
-        <ul>
-            {payload && 
-                <MetaPlate data={payload.data.CategoryFind}>
-                    <>
-                        <LeftMenuItem prop="category" keyDataKey="_id"/>
-                    </>
-                </MetaPlate>}
-        </ul>
-    )
+const queries = {
+    people({id}){
+        return fetch('https://swapi.dev/api/people/' + id).then(res => res.json())
+    },
+    planet({id}){
+        if (id == 2) 
+            return {
+                "name": "Alderaan",
+                "diameter": "12500"
+            }
+        return fetch('https://swapi.dev/api/planets/' + id).then(res => res.json())
+    }
 }
 
-function App() {
-    //const {payload} = useCategoriesQuery()
-    //console.log('update', performance.now())
-    //useEffect(() => console.log('effect',performance.now()), [payload])
-    return (
-        <Router history={history}>
+const Pending = () => <h1>Loading</h1>
+const Error   = ({error}) => <h1>Error: {error.message}</h1>
 
-            <LeftMenu />
-            <NamedRoute name="category" 
-                query={categoryQuery}
-                path="/category/:_id" 
-                component={PageCategory}/>
+const UserNamedRoute = createPrivateRoute(() => ['user'], history => history.goBack())(NamedRoute)
 
+const Dashboard = () => <h1>dashboard</h1>
 
 
-            {/*payload && 
-            <MetaPlate data={payload.data.CategoryFind}>
-                <Categories prop='data'>
-                    <Category prop="category" keyDataKey="_id">
-                        <Goods sourceDataKey="goods"> 
-                            <Good prop="good" keyDataKey="_id">
-                                <div sourceDataKey="images">
-                                    <Image prop="src" sourceDataKey="url"/>
-                                </div>
-                            </Good>
-                        </Goods>
-                        <Categories sourceDataKey="subCategories" prop='data'>
-                            <Category prop="category" keyDataKey="_id" />
-                        </Categories>
-                    </Category>
-                </Categories>
-            </MetaPlate> */ }
-        </Router>
-    )
+function App() {
+  return (
+    <Router history={history}>
+      <div>
+        <NamedRoute name="people" 
+                    query={queries.people}
+                    path="/people/:id" 
+                    pendingQueryComponent={Pending}
+                    errorQueryComponent={Error}
+                    component={SwapiPeople}/>
+        <NamedRoute routeName="planet" 
+                    query={queries.planet}
+                    pendingQueryComponent={Pending}
+                    errorQueryComponent={Error}
+                    path="/planet/:id" 
+                    component={SwapiPlanet}/>
+        <UserNamedRoute routeName="dashboard" path="/dashboard" component={Dashboard} roles={['admin']} />
+      </div>
+      <p className="read-the-docs">
+        <NamedLink routeName="people" params={{id: 1}}>Luke</NamedLink>
+        <NamedLink routeName="people" params={{id: 2}}>C3PO</NamedLink>
+      </p>
+      <p className="read-the-docs">
+        <NamedLink routeName="planet" params={{id: 1}}>Tatooin</NamedLink>
+        <NamedLink routeName="planet" params={{id: 2}}>Alderaan</NamedLink>
+      </p>
+    </Router>
+  )
 }
 
 export default App

+ 10 - 0
src/lib/pub/query/index.ts

@@ -204,6 +204,16 @@ export default (options:CreateQueryPubOptions) => {
                 return queryPub.queries[promiseName][stringifiedArgs]
             }
 
+            /*
+             * Gets sub pub for this promiseName and this parameters. Used by NamedRoute
+             * to subscribe on pub and refresh when pub updates. Working only for Query,
+             * not for Mutations
+             */
+            query.getQueryPubBranch = (...args) => {
+                const stringifiedArgs = stringifyArgs(args)
+                return [queryPub.queries[promiseName], stringifiedArgs] 
+            }
+
             return {makeQuery, query, forceQuery, [hookName]: isMutation ? useMutation : useQuery}
         }
     }

+ 32 - 6
src/lib/router/index.tsx

@@ -1,5 +1,5 @@
 import {ComponentType} from 'react';
-import {useState, useEffect} from 'react';
+import {useState, useEffect, useRef} from 'react';
 import {Route, Link, generatePath, matchPath, useHistory, useLocation} from 'react-router-dom';
 
 const routes = {
@@ -14,23 +14,49 @@ export const NamedRoute = ({name, routeName, query, pendingQueryComponent:P, err
 
     const ComponentWrapper = (props) => {
         const [queryResult, setQueryResult] = useState()
-        const [queryError, setQueryError] = useState()
+        const [queryError, setQueryError]   = useState()
+        const queryPubUnsubscribeRef        = useRef()
         useEffect(() => {
             if (query && typeof query === 'function'){
-                const querySyncResult = query(props.match.params, props, name)
-                if (querySyncResult && typeof querySyncResult.then === 'function'){
-                    querySyncResult.then(setQueryResult, setQueryError)
+                if (typeof query.getQueryPubBranch === 'function'){ //if it's queryPub query function
+                    query(props.match.params, props, name)
+                    const [queryPub, key] = query.getQueryPubBranch(props.match.params)
+                    queryPubUnsubscribeRef.current = queryPub.subscribe(() => {
+                        const {status, payload, error} = queryPub[key];
+                        ({
+                            PENDING(){
+                                setQueryError()
+                                setQueryResult()
+                            },
+                            FULFILLED(){
+                                setQueryResult(payload)
+                            },
+                            REJECTED(){
+                                setQueryError(error)
+                            }
+                        })[status]();
+                    })
                 }
                 else {
-                    setQueryResult(querySyncResult)
+                    const querySyncResult = query(props.match.params, props, name)
+                    if (querySyncResult && typeof querySyncResult.then === 'function'){
+                        querySyncResult.then(setQueryResult, setQueryError)
+                    }
+                    else {
+                        setQueryResult(querySyncResult)
+                    }
                 }
             }
             return () => {
                 setQueryError()
                 setQueryResult()
+                if (queryPubUnsubscribeRef.current)
+                    queryPubUnsubscribeRef.current()
             }
         },Object.values(props.match.params))
 
+        console.log('NAMED ROUTE STATE', queryResult, queryError)
+
         if (!query || typeof query !== 'function')
             return <Render {...{[componentQueryResultProp]:query, ...props}} />
 

+ 227 - 0
src/test/queryPubNamedRoute.tsx

@@ -0,0 +1,227 @@
+import { useState, useEffect, Fragment } from 'react'
+import './App.css'
+import {createBrowserHistory} from 'history';
+import {Router, Link} from 'react-router-dom'
+import {createQueryPub, NamedRoute, NamedLink, usePub} from './lib';
+import {MetaPlate} from './lib';
+
+
+const history = createBrowserHistory()
+
+const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOnsiaWQiOiI2NjdmZjBkYTIyMzIxMjBjN2M4NzFmN2MiLCJsb2dpbiI6InRzdDE5IiwiYWNsIjpbIjY2N2ZmMGRhMjIzMjEyMGM3Yzg3MWY3YyIsInVzZXIiXX0sImlhdCI6MTcyMDI1OTEwN30.eDcInxQlskt32LjaCe5sdCcJ3B97BKYZtgUARxThYQM'
+
+console.log(JSON.parse(atob(token.split('.')[1])).sub.id)
+
+const shopUrl = 'http://shop-roles.node.ed.asmer.org.ua/'
+
+const gql = (query, variables={}) => 
+    fetch(shopUrl + 'graphql',{
+        method: 'POST',
+        headers: {
+            'Content-Type': 'application/json',
+            Accept: 'application/json',
+            Authorization: 'Bearer ' + token
+        },
+        body: JSON.stringify({query, variables})
+    })
+    .then(res => res.json())
+    .then(result => {
+        if (result.errors && !result.data){
+            throw new Error(JSON.stringify(result.errors))
+        }
+        return Object.values(result.data)[0]
+    })
+
+const {queryPub, createQuery, withQueryFunc} = createQueryPub({
+    queryFunc: gql,
+    cacheTimeout: 5000,
+})
+
+const {useCategoriesQuery} = createQuery({
+    promiseName: 'categories',
+    args: [`query MainCategories{
+              CategoryFind(query: "[{}]"){
+                _id name
+                  image{
+                    url
+                  }
+                goods{
+                _id
+                name description price images{
+                  url
+                }
+              }
+              subCategories{ name _id}
+            }
+        }
+    `]
+})
+
+const {useMainCategoriesQuery} = createQuery({
+    promiseName: 'mainCategories',
+    args: [`query MainCategories{
+              CategoryFind(query: "[{\\"parent\\":null}]"){
+                _id name
+                  image{
+                    url 
+                  }
+                goods{
+                _id
+                name description price images{
+                  url
+                }
+              }
+              subCategories{ name _id}
+            }
+        }
+    `]
+})
+
+
+const {query: categoryQuery, useCategoryQuery} = createQuery({
+    promiseName: 'category',
+    args: ({_id}) => [`query categoryById($query:String){
+              CategoryFindOne(query: $query){
+                _id name
+                  image{
+                    url
+                  }
+                goods{
+                _id
+                name description price images{
+                  url
+                }
+              }
+              subCategories{ name _id}
+            }
+        }
+    `, {query: JSON.stringify([{_id}])}]
+})
+
+
+//queryPub.subscribe(() => console.log(JSON.stringify(queryPub.queries.user, null, 4)))
+//
+
+
+
+const Image = ({src}) => <img src={`${shopUrl}${src}`} style={{maxWidth: '50%'}}/>
+
+const Good = ({good, children}) => {
+    useEffect(() => {
+        console.log('good mount', good.name)
+    },[])
+    return (
+        good ? 
+            <div className='Good'>
+                <h2>{good.name}</h2>
+                <p>{good.description}</p>
+                <span>{good.price}</span>
+                {children}
+            </div>
+        : <h3>null</h3>
+    )
+}
+
+const Category = ({category:{name, _id}, children}) =>
+<div className='Category'>
+    <h1>{name}</h1>
+    {_id}
+    {children}
+</div>
+
+const Categories = ({children, data}) => 
+<div style={{border: '2px solid', paddingLeft: '20px'}}>
+    <h1>categories({data && data.length})</h1>
+    {children}
+</div>
+
+const Goods = ({children}) => 
+<div style={{border: '2px solid blue', paddingLeft: '10px'}}>
+    <h2>goods</h2>
+    {children}
+</div>
+
+const PageCategory = ({data}) => {
+    //usePub(queryPub) //IS IT REQUIRED?, or route mechanics enough (looks like this required for auto-refresh queries in PUB)
+    //if its required, is useCategoryQuery gives same result?
+    //yes, exactly. it required due to auto update of queryPub.
+    //
+    //useCategoryQuery({_id: useParams()._id}) //here we need id from url...
+    console.log('page category update',data)
+    return (
+    <>
+        <MetaPlate data={data}>
+            <Category prop="category" keyDataKey="_id">
+                <Goods sourceDataKey="goods"> 
+                    <Good prop="good" keyDataKey="_id">
+                        <div sourceDataKey="images">
+                            <Image prop="src" sourceDataKey="url"/>
+                        </div>
+                    </Good>
+                </Goods>
+                <Categories sourceDataKey="subCategories" prop='data'>
+                    <LeftMenuItem prop="category" keyDataKey="_id" />
+                </Categories>
+            </Category>
+        </MetaPlate> 
+    </>
+    )
+}
+
+const LeftMenuItem = ({category:{_id,name}})=>
+<li><NamedLink routeName='category' params={{_id}}>{name}</NamedLink></li>
+//<li><Link  to={`/category/${_id}`}>{name}</Link></li>
+
+const LeftMenu = () => {
+    const {payload} = useMainCategoriesQuery()
+    return (
+        <ul>
+            {payload && 
+                <MetaPlate data={payload}>
+                    <>
+                        <LeftMenuItem prop="category" keyDataKey="_id"/>
+                    </>
+                </MetaPlate>}
+        </ul>
+    )
+}
+
+function App() {
+    //const {payload} = useCategoriesQuery()
+    //console.log('update', performance.now())
+    //useEffect(() => console.log('effect',performance.now()), [payload])
+    return (
+        <Router history={history}>
+
+            <LeftMenu />
+            <NamedRoute name="category" 
+                query={categoryQuery}
+                path="/category/:_id" 
+                errorQueryComponent={({error}) => <h1><pre>{error.message}</pre></h1>}
+                pendingQueryComponent={() => <h1>Loading</h1>}
+                component={PageCategory}/>
+
+
+
+            {/*payload && 
+            <MetaPlate data={payload.data.CategoryFind}>
+                <Categories prop='data'>
+                    <Category prop="category" keyDataKey="_id">
+                        <Goods sourceDataKey="goods"> 
+                            <Good prop="good" keyDataKey="_id">
+                                <div sourceDataKey="images">
+                                    <Image prop="src" sourceDataKey="url"/>
+                                </div>
+                            </Good>
+                        </Goods>
+                        <Categories sourceDataKey="subCategories" prop='data'>
+                            <Category prop="category" keyDataKey="_id" />
+                        </Categories>
+                    </Category>
+                </Categories>
+            </MetaPlate> */ }
+        </Router>
+    )
+}
+
+export default App