Procházet zdrojové kódy

selector basically working

Tomi Cvetic před 4 roky
rodič
revize
af5545c218

+ 4 - 6
frontend/pages/admin/user/index.tsx

@@ -4,6 +4,7 @@ import {
   UsersDocument,
   Permission,
   useUpdatePermissionsMutation,
+  UsersQuery,
 } from '../../../src/gql'
 import { FunctionComponent } from 'react'
 import { TUser } from '../../../src/user/types'
@@ -83,12 +84,9 @@ const AdminUsers = () => {
   const props = {
     name: 'Users',
     adminMenu: '/admin/user',
-    get: useUsersQuery(),
-    remove: useUserDeleteMutation({
-      refetchQueries: [{ query: UsersDocument }],
-      update: (args) => console.log(args),
-    }),
-    dataKey: 'users',
+    get: useUsersQuery,
+    remove: useUserDeleteMutation,
+    dataKey: 'users' as keyof UsersQuery,
     Component: AdminUser,
   }
   return <AdminList {...props} />

+ 0 - 4
frontend/src/dropdown/components/Dropdown.tsx

@@ -14,10 +14,6 @@ export interface IDropdownItem {
   title: string
 }
 
-const DropdownItem: FunctionComponent<IDropdownItem> = () => {
-  return <li>nothing</li>
-}
-
 const Dropdown: FunctionComponent<IDropdown> = ({
   items = [],
   name,

+ 204 - 64
frontend/src/training/components/Selector.tsx

@@ -1,33 +1,41 @@
-import { useTrainingTypesQuery, TrainingType } from '../../gql'
 import AddTrainingType from './AddTrainingType'
-import { useState, useEffect } from 'react'
+import { useState, useEffect, FunctionComponent, useRef } from 'react'
 import { Modal } from '../../modal'
-import Dropdown from '../../dropdown/components/Dropdown'
 import { QueryHookOptions, QueryResult } from '@apollo/client'
-import { TextInput } from '../../form'
+import { customEvent } from '../../lib/customEvent'
+import { useOnClickOutside } from '../../lib/onClickOutside'
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
+import {
+  faCogs,
+  faChevronCircleRight,
+  faChevronCircleDown,
+  faPlusCircle,
+  faEdit,
+  faTrash,
+} from '@fortawesome/free-solid-svg-icons'
+import theme from '../../styles/theme'
 
-interface ISelector<TQueryData, TQueryVariables> {
+interface ISelector<TQueryData, TQueryVariables extends { where?: any }> {
   name: string
   value: any
   onChange: GenericEventHandler
-  callback: (selectedIndex: number) => void
-  query: (
-    baseOptions?: QueryHookOptions<TQueryData, { where: { OR: { [key: string]: string }[] } }>
-  ) => QueryResult<TQueryData, TQueryVariables>
-  searchKeys?: (keyof TQueryData)[]
-  dataKey: keyof TQueryData
-  selectedKey: keyof TQueryData
+  query: (baseOptions?: QueryHookOptions) => QueryResult<TQueryData, TQueryVariables>
+  dataKey?: keyof TQueryData
+  searchKeys?: (keyof (TQueryVariables & { where: Pick<TQueryVariables, 'where'> })['where'])[]
   className?: string
+  SelectorItem: FunctionComponent<ISelectorItem>
+  multiple?: boolean
+}
+
+export interface ISelectorItem {
+  data: any
 }
 
 /**
  * The selector allows to select an item from a list and offers tools to add, edit or remove items.
  * @param param0
  */
-const Selector = <
-  TQueryData extends { [dataKey: string]: any[] },
-  TQueryVariables extends Record<string, any>
->({
+const Selector = <TQueryData, TQueryVariables extends { where?: any }>({
   name,
   value,
   onChange,
@@ -35,58 +43,113 @@ const Selector = <
   searchKeys,
   dataKey,
   className,
+  SelectorItem,
+  multiple = false,
 }: ISelector<TQueryData, TQueryVariables>) => {
   const [searchTerm, setSearchTerm] = useState('')
-  const searchVariables =
-    searchKeys && searchTerm !== ''
-      ? { variables: { where: { OR: searchKeys?.map((key) => ({ [key]: searchTerm })) } } }
-      : {}
+  const [modalState, setModalState] = useState(false)
+  const searchVariables = searchKeys
+    ? { variables: { where: { OR: searchKeys?.map((key) => ({ [key]: searchTerm })) } } }
+    : {}
   const { data, error, loading } = query(searchVariables)
+  const debounce = useRef<undefined | NodeJS.Timeout>()
+  const ref = useRef<HTMLDivElement>(null)
+
+  const _dataKey: keyof TQueryData | undefined =
+    dataKey ?? (data && (Object.keys(data)[0] as keyof TQueryData))
+
+  const queryData = data && _dataKey && ((data[_dataKey] as unknown) as any[])
+  console.log({ queryData, value, name, searchVariables })
 
   useEffect(() => {
-    setTimeout(() => console.log('debouonced.'), 300)
+    if (debounce.current) clearTimeout(debounce.current)
+    debounce.current = setTimeout(() => console.log('debouonced', searchTerm), 400)
   }, [searchTerm])
 
+  const [showDropdownList, setShowDropdownList] = useState(false)
+  const [showTools, setShowTools] = useState(false)
+  useOnClickOutside(ref, () => setShowDropdownList(false))
+
   return (
-    <div className={className}>
-      <TextInput
-        name='search'
-        type='search'
-        value={searchTerm}
-        onChange={(event) => setSearchTerm(event.target.vale)}
-      />
-      <select
-        id={name}
-        name={name}
-        value={value}
-        onChange={(event) => {
-          const changeEvent: CustomChangeEvent = {
-            target: {
-              type: 'custom',
-              value: { id: event.target.value },
-              name,
-            },
-          }
-          onChange(changeEvent)
+    <div id='dd-container' ref={ref} className={[className, showDropdownList && 'open'].join(' ')}>
+      {multiple &&
+        queryData?.filter((item: any) => value.includes(item.id)).map((item: any) => item.id)}
+      <div
+        id='dd-header'
+        onClick={() => {
+          console.log('click')
+          setShowDropdownList(!showDropdownList)
         }}
+        className={showDropdownList ? 'open' : undefined}
       >
-        {loading && 'loading training types...'}
-        {error && 'error loading training types'}
-        {data &&
-          data[dataKey].map((item) => (
-            <option key={item.id} value={item.id}>
-              {item.name}
-            </option>
+        <FontAwesomeIcon
+          icon={showDropdownList ? faChevronCircleDown : faChevronCircleRight}
+          height={16}
+        />
+        <span>{value.name}</span>
+        <button
+          onClick={(event) => {
+            event.stopPropagation()
+            if (!showTools) setShowDropdownList(true)
+            setShowTools(!showTools)
+          }}
+        >
+          <FontAwesomeIcon icon={faCogs} height={16} />
+        </button>
+      </div>
+
+      {showDropdownList && (
+        <ul id='dd-items'>
+          {showTools && (
+            <div id='dd-tools'>
+              <input
+                type='search'
+                name='search'
+                value={searchTerm}
+                placeholder='Search'
+                onChange={(event) => setSearchTerm(event.target.value)}
+              />
+              <button
+                type='button'
+                onClick={(event) => {
+                  setModalState(true)
+                }}
+              >
+                <FontAwesomeIcon icon={faPlusCircle} height={16} />
+              </button>
+            </div>
+          )}
+          {queryData?.map((item: any) => (
+            <li
+              key={item.id}
+              onClick={(event) => {
+                setShowDropdownList(false)
+                onChange(customEvent(name, item))
+              }}
+            >
+              <SelectorItem data={item} />
+              <div className='item-tools'>
+                <button
+                  type='button'
+                  onClick={(event) => {
+                    setModalState(true)
+                  }}
+                >
+                  <FontAwesomeIcon icon={faEdit} height={16} />
+                </button>
+                <button
+                  type='button'
+                  onClick={(event) => {
+                    setModalState(true)
+                  }}
+                >
+                  <FontAwesomeIcon icon={faTrash} height={16} />
+                </button>
+              </div>
+            </li>
           ))}
-      </select>
-      <button
-        type='button'
-        onClick={(event) => {
-          setModalState(true)
-        }}
-      >
-        Add type
-      </button>
+        </ul>
+      )}
       <Modal state={[modalState, setModalState]}>
         <AddTrainingType
           onSuccess={(result) => {
@@ -105,20 +168,97 @@ const Selector = <
       </Modal>
 
       <style jsx>{`
-        div {
+        #dd-container {
+          position: relative;
+          user-select: none;
+        }
+
+        #dd-header {
+          margin: 0.3em 0;
+          padding: 0.3em;
+          display: flex;
+          color: ${theme.colors.formColor};
+          background-color: ${theme.colors.formBackground}22;
+          border-bottom: 2px solid ${theme.colors.formHighlightBackground}00;
+          transition: all 250ms ease-in-out;
+          cursor: pointer;
+          align-items: center;
+        }
+
+        #dd-header.open {
+          color: ${theme.colors.formHighlight};
+          background-color: ${theme.colors.formHighlightBackground}22;
+          border-bottom: 2px solid ${theme.colors.formHighlightBackground}22;
+        }
+
+        #dd-header.error {
+          color: ${theme.colors.formError};
+          background-color: ${theme.colors.formErrorBackground}22;
+          border-bottom: 2px solid ${theme.colors.formErrorBackground}22;
+        }
+
+        #dd-items {
+          position: absolute;
+          width: 100%;
+          top: 95%;
+          max-height: 300px;
+          z-index: 5;
+          overflow-y: scroll;
+          background-color: ${theme.colors.background};
+          list-style: none;
+          margin: 0;
+          padding: 0;
+          box-shadow: ${theme.bsSmall};
+        }
+
+        #dd-items.open {
+          background-color: ${theme.colors.white}ff;
+        }
+
+        #dd-items li {
+          background-color: ${theme.colors.formHighlightBackground}00;
           display: flex;
+          justify-content: space-between;
           align-items: center;
+          transition: all 250ms ease-in-out;
+          margin: 0;
+          padding: 0;
+          border-bottom: 1px solid ${theme.colors.highlight}11;
+        }
+        #dd-items li:last-child {
+          border-bottom: none;
+        }
+
+        #dd-items li:hover {
+          background-color: ${theme.colors.formHighlightBackground}22;
         }
 
-        label,
-        select,
-        button {
-          display: inline-block;
+        #dd-header span {
+          flex-grow: 1;
+        }
+
+        #dd-header button {
+          margin: 0.2em;
+          padding: 0.2em;
           width: auto;
+          color: inherit;
+          background-color: transparent;
+        }
+
+        #dd-tools button,
+        #dd-items button {
+          color: ${theme.colors.links};
+          background-color: transparent;
+          margin: 0.3em;
+          padding: 0.3em;
         }
-        select {
+
+        #dd-tools {
+          display: flex;
+        }
+
+        #dd-tools input {
           flex-grow: 1;
-          margin: 0 0.6em;
         }
       `}</style>
     </div>

+ 20 - 91
frontend/src/training/components/TrainingTypeSelector.tsx

@@ -4,9 +4,8 @@ import {
   TrainingTypesQuery,
   TrainingTypesQueryVariables,
 } from '../../gql'
-import AddTrainingType from './AddTrainingType'
-import { useState, useEffect } from 'react'
-import { Modal } from '../../modal'
+import { FunctionComponent } from 'react'
+import Selector, { ISelectorItem } from './Selector'
 
 interface ITrainingTypeSelector {
   value?: TrainingType
@@ -16,6 +15,15 @@ interface ITrainingTypeSelector {
   className?: string
 }
 
+const TrainingTypeSelectItem = ({ data }: { data: TrainingType }) => {
+  return (
+    <div>
+      <h3>{data.name}</h3>
+      <p>{data.description}</p>
+    </div>
+  )
+}
+
 const TrainingTypeSelector = ({
   value,
   onChange,
@@ -23,95 +31,16 @@ const TrainingTypeSelector = ({
   label = 'Training type',
   className = 'training-type',
 }: ITrainingTypeSelector) => {
-  const [modalState, setModalState] = useState(false)
-  const myQuery = {
-    variables: { where: { OR: [{ description_contains: 'hi' }, { name_contains: 'hi' }] } },
-  }
-  const trainingTypes = useTrainingTypesQuery(myQuery)
-  const id = value?.id ?? ''
-
-  useEffect(() => {
-    if (trainingTypes.data && trainingTypes.data.trainingTypes.length > 0 && !id) {
-      onChange({
-        target: {
-          type: 'custom',
-          value: trainingTypes.data.trainingTypes[0],
-          name,
-        },
-      })
-    }
-  }, [trainingTypes.data])
-
   return (
-    <div className={className}>
-      <label>{label}</label>
-      <select
-        id={name}
-        name={name}
-        value={id}
-        onChange={(event) => {
-          const changeEvent: CustomChangeEvent = {
-            target: {
-              type: 'custom',
-              value: { id: event.target.value },
-              name,
-            },
-          }
-          onChange(changeEvent)
-        }}
-      >
-        {trainingTypes.loading && 'loading training types...'}
-        {trainingTypes.error && 'error loading training types'}
-        {trainingTypes.data &&
-          trainingTypes.data.trainingTypes.map((trainingType) => (
-            <option key={trainingType.id} value={trainingType.id}>
-              {trainingType.name}
-            </option>
-          ))}
-      </select>
-      <button
-        type='button'
-        onClick={(event) => {
-          setModalState(true)
-        }}
-      >
-        Add type
-      </button>
-      <Modal state={[modalState, setModalState]}>
-        <AddTrainingType
-          onSuccess={(result) => {
-            setModalState(false)
-            if (result.data) {
-              onChange({
-                target: {
-                  type: 'custom',
-                  value: { id: result.data.createTrainingType.id },
-                  name,
-                },
-              })
-            }
-          }}
-        />
-      </Modal>
-
-      <style jsx>{`
-        div {
-          display: flex;
-          align-items: center;
-        }
-
-        label,
-        select,
-        button {
-          display: inline-block;
-          width: auto;
-        }
-        select {
-          flex-grow: 1;
-          margin: 0 0.6em;
-        }
-      `}</style>
-    </div>
+    <Selector<TrainingTypesQuery, TrainingTypesQueryVariables>
+      SelectorItem={TrainingTypeSelectItem}
+      name={name}
+      value={value}
+      onChange={onChange}
+      query={useTrainingTypesQuery}
+      searchKeys={['name_contains', 'description_contains']}
+      className={className}
+    />
   )
 }
 export default TrainingTypeSelector