diff --git a/.github/workflows/firebase_hosting.yml b/.github/workflows/firebase_hosting.yml deleted file mode 100644 index e69de29b..00000000 diff --git a/Dockerfile b/Dockerfile index edfc9216..5bcc8ddd 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,13 +1,5 @@ -# Stage 1 -FROM node:8 as react-build -WORKDIR /www -COPY . ./ -RUN yarn -RUN yarn build - -# Stage 2 - the production environment -FROM nginx:alpine -COPY nginx.conf /etc/nginx/conf.d/default.conf -COPY --from=react-build /app/build /usr/share/nginx/html -EXPOSE 80 -CMD ["nginx", "-g", "daemon off;"] \ No newline at end of file +FROM node:10.15.1 +WORKDIR cloud_functions/functions +COPY cloud_functions/functions/package.json ./ +RUN yarn install +COPY . . \ No newline at end of file diff --git a/cloud_functions/functions/updateDeployStatus.ts b/cloud_functions/functions/updateDeployStatus.ts index 17740a8d..b5d4aadd 100644 --- a/cloud_functions/functions/updateDeployStatus.ts +++ b/cloud_functions/functions/updateDeployStatus.ts @@ -20,12 +20,15 @@ if (serviceAccount) { }); const db = admin.firestore(); - const main = async deployRequestPath => { - await db.doc(deployRequestPath).update({ deployedAt: serverTimestamp() }); + const main = async (deployRequestPath: string, currentBuild) => { + await db.doc(deployRequestPath).update({ + deployedAt: serverTimestamp(), + currentBuild: currentBuild ?? "", + }); return true; }; - main(process.argv[2]) + main(process.argv[2], process.argv[3]) .catch(err => console.log(err)) .then(() => console.log("this will succeed")) .catch(() => "obligatory catch"); diff --git a/cloudbuildfunctions.yaml b/cloudbuildfunctions.yaml index 81d99c76..2598e222 100644 --- a/cloudbuildfunctions.yaml +++ b/cloudbuildfunctions.yaml @@ -1,4 +1,9 @@ steps: + # - name: "gcr.io/kaniko-project/executor:latest" + # args: + # - --destination=gcr.io/$PROJECT_ID/image + # - --cache=true + # - --cache-ttl=24h - name: gcr.io/cloud-builders/gcloud args: - kms @@ -34,6 +39,7 @@ steps: args: - "updateStatus" - "${_REQUEST_DOC_PATH}" + - "${_FUNCTION_CONFIG}" dir: "cloud_functions/functions" substitutions: diff --git a/firetable b/firetable new file mode 160000 index 00000000..655a4ec3 --- /dev/null +++ b/firetable @@ -0,0 +1 @@ +Subproject commit 655a4ec3985e6359416a5b2501bf878ce7a8830b diff --git a/www/package.json b/www/package.json index 671a00ee..93d866bd 100644 --- a/www/package.json +++ b/www/package.json @@ -4,7 +4,7 @@ "private": true, "dependencies": { "@antlerengineering/components": "^0.2.7", - "@antlerengineering/multiselect": "^0.3.9", + "@antlerengineering/multiselect": "^0.3.14", "@date-io/date-fns": "1.x", "@material-ui/core": "^4.9.13", "@material-ui/icons": "^4.9.1", diff --git a/www/src/assets/Firetable.tsx b/www/src/assets/Firetable.tsx index a5828d13..6fa6d541 100644 --- a/www/src/assets/Firetable.tsx +++ b/www/src/assets/Firetable.tsx @@ -16,30 +16,30 @@ export default function Firetable(props: SvgIconProps) { const strokeColor = "#fff"; return ( - + - + diff --git a/www/src/assets/icons/CellResize.tsx b/www/src/assets/icons/CellResize.tsx new file mode 100644 index 00000000..687d4f3a --- /dev/null +++ b/www/src/assets/icons/CellResize.tsx @@ -0,0 +1,10 @@ +import React from "react"; +import SvgIcon, { SvgIconProps } from "@material-ui/core/SvgIcon"; + +export default function FileDownload(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/www/src/assets/icons/ColumnPlusAfter.tsx b/www/src/assets/icons/ColumnPlusAfter.tsx new file mode 100644 index 00000000..5b707006 --- /dev/null +++ b/www/src/assets/icons/ColumnPlusAfter.tsx @@ -0,0 +1,13 @@ +import React from "react"; +import SvgIcon, { SvgIconProps } from "@material-ui/core/SvgIcon"; + +export default function FileDownload(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/www/src/assets/icons/ColumnPlusBefore.tsx b/www/src/assets/icons/ColumnPlusBefore.tsx new file mode 100644 index 00000000..6ee2f15f --- /dev/null +++ b/www/src/assets/icons/ColumnPlusBefore.tsx @@ -0,0 +1,13 @@ +import React from "react"; +import SvgIcon, { SvgIconProps } from "@material-ui/core/SvgIcon"; + +export default function FileDownload(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/www/src/assets/icons/ColumnRemove.tsx b/www/src/assets/icons/ColumnRemove.tsx new file mode 100644 index 00000000..e09a8de6 --- /dev/null +++ b/www/src/assets/icons/ColumnRemove.tsx @@ -0,0 +1,13 @@ +import React from "react"; +import SvgIcon, { SvgIconProps } from "@material-ui/core/SvgIcon"; + +export default function FileDownload(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/www/src/assets/icons/Freeze.tsx b/www/src/assets/icons/Freeze.tsx new file mode 100644 index 00000000..fd66e7c5 --- /dev/null +++ b/www/src/assets/icons/Freeze.tsx @@ -0,0 +1,13 @@ +import React from "react"; +import SvgIcon, { SvgIconProps } from "@material-ui/core/SvgIcon"; + +export default function FileDownload(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/www/src/assets/icons/Unfreeze.tsx b/www/src/assets/icons/Unfreeze.tsx new file mode 100644 index 00000000..f2a88b81 --- /dev/null +++ b/www/src/assets/icons/Unfreeze.tsx @@ -0,0 +1,13 @@ +import React from "react"; +import SvgIcon, { SvgIconProps } from "@material-ui/core/SvgIcon"; + +export default function FileDownload(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/www/src/components/ConnectTableSelect/PopupContents.tsx b/www/src/components/ConnectTableSelect/PopupContents.tsx index e9d29429..139af606 100644 --- a/www/src/components/ConnectTableSelect/PopupContents.tsx +++ b/www/src/components/ConnectTableSelect/PopupContents.tsx @@ -42,13 +42,14 @@ export default function PopupContents({ multiple = true, row, }: IPopupContentsProps) { + const index = collectionPath ?? config.index; //temporary for pre column restructure migration const classes = useStyles(); const { userClaims } = useFiretableContext(); const algoliaIndex = useMemo(() => { - return searchClient.initIndex(collectionPath); - }, [collectionPath]); + return searchClient.initIndex(index); + }, [index]); // Algolia search query const [query, setQuery] = useState(""); @@ -62,11 +63,10 @@ export default function PopupContents({ console.log("SEARCH", query, algoliaIndex, row); const data = { ...userClaims, ...row }; - const filters = config.filters.replace( - /\{\{(.*?)\}\}/g, - (m, k) => data[k] - ); - console.log(filters); + const filters = config?.filters + ? config?.filters.replace(/\{\{(.*?)\}\}/g, (m, k) => data[k]) + : ""; + const resp = await algoliaIndex.search(query, { filters, }); @@ -86,7 +86,7 @@ export default function PopupContents({ const { _highlightResult, ...snapshot } = hit; const output = { snapshot, - docPath: `${collectionPath}/${snapshot.objectID}`, + docPath: `${index}/${snapshot.objectID}`, }; if (multiple) onChange([...value, output]); diff --git a/www/src/components/CreateTableDialog.tsx b/www/src/components/CreateTableDialog.tsx deleted file mode 100644 index 4fb4a040..00000000 --- a/www/src/components/CreateTableDialog.tsx +++ /dev/null @@ -1,136 +0,0 @@ -import React, { useState, useEffect } from "react"; -import _camelCase from "lodash/camelCase"; - -import AddIcon from "@material-ui/icons/Add"; -import useRouter from "../hooks/useRouter"; -import MultiSelect from "@antlerengineering/multiselect"; -import { - Tooltip, - Fab, - Dialog, - DialogActions, - DialogContent, - DialogContentText, - DialogTitle, - TextField, - Button, - Select, -} from "@material-ui/core"; -import { useFiretableContext } from "../contexts/firetableContext"; - -export interface ICreateTableDialogProps { - /** Optional class overrides */ - classes?: Partial>; - /** Function to handle table creation */ - createTable: (tableName: string, collectionName: string) => void; -} - -export default function CreateTableDialog({ - classes = {}, - createTable, -}: ICreateTableDialogProps) { - const router = useRouter(); - const { userClaims } = useFiretableContext(); - const [open, setOpen] = React.useState(false); - const [tableName, setTableName] = useState(""); - const [collectionName, setCollectionName] = useState(""); - useEffect(() => { - setCollectionName(_camelCase(tableName)); - }, [tableName]); - function handleClickOpen() { - setOpen(true); - } - - function handleClose() { - setTableName(""); - setCollectionName(""); - setOpen(false); - } - function handleCreate() { - createTable(tableName, collectionName); - if (router.location.pathname === "/") { - router.history.push(`table/${collectionName}`); - } else { - router.history.push(collectionName); - } - - handleClose(); - } - if (userClaims && userClaims.roles?.includes("ADMIN")) - return ( -
- - - - - - - - New table - - Create a new Table - { + const { tableState } = useFiretableContext(); + + const configDocPath = tableState?.config.tableConfig.path; + const handleColumnMigration = async () => { + console.log({ columns }); + + const newColumns = columns.reduce((acc, currCol, currIndex) => { + const baseCol = { + ...currCol, + fieldName: currCol.key, + index: currIndex, + }; + if (currCol.type === FieldType.connectTable) { + const index = currCol.collectionPath; + delete baseCol.collectionPath; + return { + ...acc, + [currCol.key]: { ...baseCol, config: { ...currCol.config, index } }, + }; + } else if (currCol.type === FieldType.subTable) { + const parentLabel = [currCol.parentLabel]; + delete baseCol.parentLabel; + return { + ...acc, + [currCol.key]: { + ...baseCol, + config: { ...currCol.config, parentLabel }, + }, + }; + } else if (currCol.type === FieldType.action) { + const callableName = currCol.callableName; + delete baseCol.callableName; + return { + ...acc, + [currCol.key]: { + ...baseCol, + config: { ...currCol.config, callableName }, + }, + }; + } else if ( + [FieldType.multiSelect, FieldType.singleSelect].includes(currCol.type) + ) { + const options = currCol.options; + delete baseCol.options; + return { + ...acc, + [currCol.key]: { + ...baseCol, + config: { ...currCol.config, options }, + }, + }; + } else return { ...acc, [currCol.key]: baseCol }; + }, {}); + await db + .doc(configDocPath) + .update({ columns: newColumns, oldColumns: columns }); + window.location.reload(); + }; + if (needsMigration) + return ( + + + + + + ); + else return <>; + // else + // return ( + // + // ); +}; +export default MigrateButton; diff --git a/www/src/components/Table/TableHeader.tsx b/www/src/components/Table/TableHeader.tsx index 7c7c49ac..5644d95a 100644 --- a/www/src/components/Table/TableHeader.tsx +++ b/www/src/components/Table/TableHeader.tsx @@ -8,6 +8,7 @@ import { MenuItem, Typography, Button, + Tooltip, } from "@material-ui/core"; import AddIcon from "@material-ui/icons/Add"; @@ -19,6 +20,7 @@ import { FireTableFilter } from "hooks/useFiretable"; import { DRAWER_COLLAPSED_WIDTH } from "components/SideDrawer"; import { useFiretableContext } from "contexts/firetableContext"; import { FieldType } from "constants/fields"; +import MigrateButton from "./MigrateButton"; export const TABLE_HEADER_HEIGHT = 56; @@ -55,11 +57,8 @@ const useStyles = makeStyles(theme => ); interface ITableHeaderProps { - collection: string; rowHeight: number; updateConfig: Function; - - columns: any; filters: FireTableFilter[]; } @@ -67,14 +66,18 @@ interface ITableHeaderProps { * TODO: Make this properly mobile responsive, not just horizontally scrolling */ export default function TableHeader({ - collection, rowHeight, updateConfig, - columns, + filters, }: ITableHeaderProps) { const classes = useStyles(); - const { tableActions, userClaims } = useFiretableContext(); + const { tableActions, tableState } = useFiretableContext(); + if (!tableState || !tableState.columns) return <>; + const { columns } = tableState; + + const needsMigration = Array.isArray(columns) && columns.length !== 0; + const tempColumns = needsMigration ? columns : Object.values(columns); return ( + + {mode === TableSettingsDialogModes.create && ( + + )} + {mode === TableSettingsDialogModes.update && ( + + )} + + +
+ ); +} diff --git a/www/src/constants/fields.tsx b/www/src/constants/fields.tsx index 8d5f9811..f13e3cb3 100644 --- a/www/src/constants/fields.tsx +++ b/www/src/constants/fields.tsx @@ -34,6 +34,7 @@ import RichTextIcon from "@material-ui/icons/TextFormat"; import ColorIcon from "@material-ui/icons/Colorize"; import SliderIcon from "assets/icons/Slider"; import CodeIcon from "@material-ui/icons/Code"; +import DerivativeIcon from "@material-ui/icons/Functions"; import UserIcon from "@material-ui/icons/Person"; export { @@ -94,6 +95,7 @@ export enum FieldType { json = "JSON", user = "USER", + derivative = "DERIVATIVE", last = "LAST", } @@ -147,6 +149,7 @@ export const FIELDS = [ { icon: , name: "JSON", type: FieldType.json }, { icon: , name: "User", type: FieldType.user }, { icon: , name: "Code", type: FieldType.code }, + { icon: , name: "Derivative", type: FieldType.derivative }, ]; /** diff --git a/www/src/contexts/firetableContext.tsx b/www/src/contexts/firetableContext.tsx index 16a139f9..14ed6532 100644 --- a/www/src/contexts/firetableContext.tsx +++ b/www/src/contexts/firetableContext.tsx @@ -1,8 +1,7 @@ import React, { useState, useContext, useEffect, useRef } from "react"; import _groupBy from "lodash/groupBy"; - -import { Column, DataGridHandle } from "react-data-grid"; -import { PopoverProps } from "@material-ui/core"; +import _sortBy from "lodash/sortBy"; +import { DataGridHandle } from "react-data-grid"; import firebase from "firebase/app"; import useFiretable, { FiretableActions, @@ -12,11 +11,7 @@ import useSettings from "hooks/useSettings"; import { useAppContext } from "./appContext"; import { useSnackContext } from "./snackContext"; import { SideDrawerRef } from "components/SideDrawer"; - -type SelectedColumnHeader = { - column: Column & { [key: string]: any }; - anchorEl: PopoverProps["anchorEl"]; -}; +import { ColumnMenuRef } from "components/Table/ColumnMenu"; export type Table = { collection: string; @@ -37,19 +32,32 @@ interface FiretableContextProps { fieldName: string, value: any ) => void; - createTable: Function; - userClaims: any; + settingsActions: { + createTable: (data: { + collection: string; + name: string; + description: string; + roles: string[]; + section: string; + }) => void; + updateTable: (data: { + collection: string; + name: string; + description: string; + roles: string[]; + section: string; + }) => Promise; + deleteTable: (collection: string) => void; + }; - // TODO: Investigate if this can be moved out of this context - selectedColumnHeader: SelectedColumnHeader | null; - setSelectedColumnHeader: React.Dispatch< - React.SetStateAction - >; + userClaims: any; // A ref to the data grid. Contains data grid functions dataGridRef: React.RefObject; // A ref to the side drawer state. Prevents unnecessary re-renders sideDrawerRef: React.MutableRefObject; + // A ref to the column menu. Prevents unnecessary re-renders + columnMenuRef: React.MutableRefObject; } const firetableContext = React.createContext>( @@ -83,36 +91,35 @@ export const FiretableContextProvider: React.FC = ({ children }) => { const { tableState, tableActions } = useFiretable(); const [tables, setTables] = useState(); const [sections, setSections] = useState(); - const [settings, createTable] = useSettings(); + const [settings, settingsActions] = useSettings(); const [userRoles, setUserRoles] = useState(); const [userClaims, setUserClaims] = useState(); - const [ - selectedColumnHeader, - setSelectedColumnHeader, - ] = useState(null); const { currentUser } = useAppContext(); useEffect(() => { const { tables } = settings; if (tables && userRoles && !sections) { - const filteredTables = tables.filter( - table => - !table.roles || table.roles.some(role => userRoles.includes(role)) - ); + const filteredTables = _sortBy(tables, "name") + .filter( + table => + !table.roles || table.roles.some(role => userRoles.includes(role)) + ) + .map(table => ({ + ...table, + section: table.section ? table.section.toUpperCase().trim() : "OTHER", + })); - const sections = _groupBy(filteredTables, "section"); - - setSections(sections); + const _sections = _groupBy(filteredTables, "section"); + setSections(_sections); setTables(filteredTables); } - }, [settings, userRoles]); + }, [settings, userRoles, sections]); useEffect(() => { if (currentUser && !userClaims) { currentUser.getIdTokenResult(true).then(results => { setUserRoles(results.claims.roles || []); setUserClaims(results.claims); - // setUserRegions(results.claims.regions || []); }); } }, [currentUser]); @@ -145,7 +152,7 @@ export const FiretableContextProvider: React.FC = ({ children }) => { open({ message: `You don't have permissions to make this change`, severity: "error", - duration: 3000, + duration: 2000, position: { horizontal: "center", vertical: "top" }, }); } @@ -156,6 +163,7 @@ export const FiretableContextProvider: React.FC = ({ children }) => { // A ref to the data grid. Contains data grid functions const dataGridRef = useRef(null); const sideDrawerRef = useRef(); + const columnMenuRef = useRef(); return ( { tableState, tableActions, updateCell, - createTable, + settingsActions, tables, sections, userClaims, - selectedColumnHeader, - setSelectedColumnHeader, dataGridRef, sideDrawerRef, + columnMenuRef, }} > {children} diff --git a/www/src/firebase/index.ts b/www/src/firebase/index.ts index d0b95db2..1c0ff00a 100644 --- a/www/src/firebase/index.ts +++ b/www/src/firebase/index.ts @@ -22,8 +22,12 @@ db.enablePersistence({ synchronizeTabs: true }); export const bucket = firebase.storage(); export const functions = firebase.functions(); + export const googleProvider = new firebase.auth.GoogleAuthProvider().setCustomParameters( { prompt: "select_account", } ); + + +export const deleteField = firebase.firestore.FieldValue.delete; diff --git a/www/src/hooks/useDoc.ts b/www/src/hooks/useDoc.ts index 309aa012..f7d27f10 100644 --- a/www/src/hooks/useDoc.ts +++ b/www/src/hooks/useDoc.ts @@ -13,13 +13,16 @@ const documentIntialState = { ref: null, loading: true, }; + const documentReducer = (prevState: any, newProps: any) => { switch (newProps.action) { case DocActions.clear: return documentIntialState; case DocActions.update: // takes data object form the dispatcher and updates doc + prevState.ref.set({ ...newProps.data }, { merge: true }); + return { ...prevState, doc: { ...prevState.doc, ...newProps.data } }; case DocActions.delete: prevState.ref.delete(); diff --git a/www/src/hooks/useFiretable/index.ts b/www/src/hooks/useFiretable/index.ts index c70be8af..0b023841 100644 --- a/www/src/hooks/useFiretable/index.ts +++ b/www/src/hooks/useFiretable/index.ts @@ -23,7 +23,7 @@ export type FiretableActions = { export type FiretableState = { orderBy: FiretableOrderBy; tablePath: string; - config: { rowHeight: number }; + config: { rowHeight: number; tableConfig: any }; columns: any[]; rows: { [key: string]: any }[]; queryLimit: number; @@ -72,7 +72,7 @@ const useFiretable = ( tablePath: tableState.path, filters: tableState.filters, columns: tableConfig.columns, - config: { rowHeight: tableConfig.rowHeight }, + config: { rowHeight: tableConfig.rowHeight, tableConfig }, rows: tableState.rows, queryLimit: tableState.limit, loadingRows: tableState.loading, diff --git a/www/src/hooks/useFiretable/useTable.tsx b/www/src/hooks/useFiretable/useTable.tsx index a4250be6..85f12602 100644 --- a/www/src/hooks/useFiretable/useTable.tsx +++ b/www/src/hooks/useFiretable/useTable.tsx @@ -106,7 +106,6 @@ const useTable = (initialOverrides: any) => { const ref = doc.ref; return { ...data, id, ref }; }); - console.log(tableState.path, { orderBy, rows }); tableDispatch({ rows, loading: false, @@ -221,6 +220,7 @@ const useTable = (initialOverrides: any) => { if (tableCollection !== tableState.path) { tableDispatch({ path: tableCollection, + rows: [], orderBy: [], }); } diff --git a/www/src/hooks/useFiretable/useTableConfig.ts b/www/src/hooks/useFiretable/useTableConfig.ts index 21ebb427..ba4f7828 100644 --- a/www/src/hooks/useFiretable/useTableConfig.ts +++ b/www/src/hooks/useFiretable/useTableConfig.ts @@ -4,8 +4,12 @@ import useDoc, { DocActions } from "../useDoc"; import { FieldType } from "constants/fields"; import _camelCase from "lodash/camelCase"; import _findIndex from "lodash/findIndex"; +import _find from "lodash/find"; +import _sortBy from "lodash/sortBy"; import { arrayMover } from "../../util/fns"; -import { db } from "../../firebase"; +import { db, deleteField } from "../../firebase"; + +//import const formatPathRegex = /\/[^\/]+\/([^\/]+)/g; @@ -45,13 +49,14 @@ const useTableConfig = (tablePath?: string) => { */ const add = (name: string, type: FieldType, data?: any) => { //TODO: validation - - //console.log("tableConfigState", tableConfigState); const { columns } = tableConfigState; + const newIndex = Object.keys(columns).length; + let updatedColumns = columns; const key = _camelCase(name); + updatedColumns[key] = { name, key, type, ...data, index: newIndex }; documentDispatch({ action: DocActions.update, - data: { columns: [...columns, { name, key, type, ...data }] }, + data: { columns: updatedColumns }, }); }; @@ -61,8 +66,21 @@ const useTableConfig = (tablePath?: string) => { */ const [resize] = useDebouncedCallback((index: number, width: number) => { const { columns } = tableConfigState; - columns[index].width = width; - documentDispatch({ action: DocActions.update, data: { columns } }); + const numberOfFixedColumns = Object.values(columns).filter( + (col: any) => col.fixed && !col.hidden + ).length; + const columnsArray = _sortBy( + Object.values(columns).filter((col: any) => !col.hidden && !col.fixed), + "index" + ); + let column: any = columnsArray[index - numberOfFixedColumns]; + column.width = width; + let updatedColumns = columns; + updatedColumns[column.key] = column; + documentDispatch({ + action: DocActions.update, + data: { columns: updatedColumns }, + }); }, 1000); type updatable = { field: string; value: unknown }; @@ -70,20 +88,30 @@ const useTableConfig = (tablePath?: string) => { * @param index of column. * @param {updatable[]} updatables properties to be updated */ - const updateColumn = (index: number, updatables: updatable[]) => { + const updateColumn = (key: string, updates: any) => { const { columns } = tableConfigState; - updatables.forEach((updatable: updatable) => { - columns[index][updatable.field] = updatable.value; + + const updatedColumns = { + ...columns, + [key]: { ...columns[key], ...updates }, + }; + + documentDispatch({ + action: DocActions.update, + data: { columns: updatedColumns }, }); - documentDispatch({ action: DocActions.update, data: { columns } }); }; /** remove column by index * @param index of column. */ - const remove = (index: number) => { + const remove = (key: string) => { const { columns } = tableConfigState; - columns.splice(index, 1); - documentDispatch({ action: DocActions.update, data: { columns } }); + let updatedColumns = columns; + updatedColumns[key] = deleteField(); + documentDispatch({ + action: DocActions.update, + data: { columns: updatedColumns }, + }); }; /** reorder columns by key * @param draggedColumnKey column being repositioned. @@ -91,13 +119,18 @@ const useTableConfig = (tablePath?: string) => { */ const reorder = (draggedColumnKey: string, droppedColumnKey: string) => { const { columns } = tableConfigState; - const draggedColumnIndex = _findIndex(columns, ["key", draggedColumnKey]); - const droppedColumnIndex = _findIndex(columns, ["key", droppedColumnKey]); - const reorderedColumns = [...columns]; - arrayMover(reorderedColumns, draggedColumnIndex, droppedColumnIndex); + console.log(columns[draggedColumnKey], columns[droppedColumnKey]); + const oldIndex = columns[draggedColumnKey].index; + const newIndex = columns[droppedColumnKey].index; + const columnsArray = _sortBy(Object.values(columns), "index"); + arrayMover(columnsArray, oldIndex, newIndex); + let updatedColumns = columns; + columnsArray.forEach((column: any, index) => { + updatedColumns[column.key] = { ...column, index }; + }); documentDispatch({ action: DocActions.update, - data: { columns: reorderedColumns }, + data: { columns: updatedColumns }, }); }; /** changing table configuration used for things such as row height diff --git a/www/src/hooks/useSettings.ts b/www/src/hooks/useSettings.ts index cd2fb9c8..b24db039 100644 --- a/www/src/hooks/useSettings.ts +++ b/www/src/hooks/useSettings.ts @@ -2,6 +2,7 @@ import { useEffect } from "react"; import useDoc, { DocActions } from "./useDoc"; import { db } from "../firebase"; import _groupBy from "lodash/groupBy"; +import _find from "lodash/find"; const useSettings = () => { const [settingsState, documentDispatch] = useDoc({ @@ -22,26 +23,65 @@ const useSettings = () => { } }, [settingsState]); - const createTable = (name: string, collection: string) => { + const createTable = (data: { + name: string; + collection: string; + description: string; + roles: string[]; + }) => { const { tables } = settingsState; // updates the setting doc - if (tables) { - documentDispatch({ - action: DocActions.update, - data: { tables: [...tables, { name, collection }] }, - }); - } else { - db.doc("_FIRETABLE_/settings").set( - { tables: [{ name, collection }] }, - { merge: true } - ); - } + + db.doc("_FIRETABLE_/settings").set( + { tables: tables ? [...tables, data] : [data] }, + { merge: true } + ); + //create the firetable collection doc with empty columns db.collection("_FIRETABLE_/settings/schema") - .doc(collection) - .set({ name, columns: [] }); + .doc(data.collection) + .set({ ...data, columns: [] }, { merge: true }); }; - return [settingsState, createTable]; + + const updateTable = (data: { + name: string; + collection: string; + description: string; + roles: string[]; + }) => { + const { tables } = settingsState; + const table = tables.filter(t => t.collection === data.collection)[0]; + return Promise.all([ + db.doc("_FIRETABLE_/settings").set( + { + tables: tables + ? [ + ...tables.filter(table => table.collection !== data.collection), + { table, ...data }, + ] + : [data], + }, + { merge: true } + ), + //update the firetable collection doc with empty columns + db + .collection("_FIRETABLE_/settings/schema") + .doc(data.collection) + .set({ ...data }, { merge: true }), + ]); + }; + const deleteTable = (collection: string) => { + const { tables } = settingsState; + + db.doc("_FIRETABLE_/settings").update({ + tables: tables.filter(table => table.collection !== collection), + }); + db.collection("_FIRETABLE_/settings/schema") + .doc(collection) + .delete(); + }; + const settingsActions = { createTable, updateTable, deleteTable }; + return [settingsState, settingsActions]; }; export default useSettings; diff --git a/www/src/views/TablesView.tsx b/www/src/views/TablesView.tsx index aed60eb9..14ac64f0 100644 --- a/www/src/views/TablesView.tsx +++ b/www/src/views/TablesView.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, { useState } from "react"; import _groupBy from "lodash/groupBy"; import _find from "lodash/find"; import { @@ -9,8 +9,13 @@ import { Link, Typography, Divider, + Fab, Checkbox, + Tooltip, + IconButton, } from "@material-ui/core"; +import AddIcon from "@material-ui/icons/Add"; +import EditIcon from "@material-ui/icons/Edit"; import Favorite from "@material-ui/icons/Favorite"; import FavoriteBorder from "@material-ui/icons/FavoriteBorder"; import SecurityIcon from "@material-ui/icons/Security"; @@ -18,11 +23,14 @@ import AppBar from "components/AppBar"; import Loading from "components/Loading"; import EmptyState from "components/EmptyState"; import StyledCard from "components/StyledCard"; -import CreateTableDialog from "components/CreateTableDialog"; + import routes from "constants/routes"; import { useFiretableContext } from "contexts/firetableContext"; import { useAppContext } from "contexts/appContext"; import { DocActions } from "hooks/useDoc"; +import TableSettingsDialog, { + TableSettingsDialogModes, +} from "components/TableSettingsDialog"; const useStyles = makeStyles(theme => createStyles({ root: { minHeight: "100vh", paddingBottom: theme.spacing(8) }, @@ -55,30 +63,50 @@ const useStyles = makeStyles(theme => [theme.breakpoints.down("md")]: { minHeight: 180 }, }, - createTableContainer: { - alignSelf: "flex-end", - marginLeft: "auto", + createTable: { + padding: theme.spacing(2), }, - createTableFab: { + fab: { width: 80, height: 80, + borderRadius: theme.shape.borderRadius * 2, "& svg": { width: "2em", height: "2em" }, }, }) ); +const regionalFilter = (regional, userClaims) => + regional && userClaims?.regions && !userClaims?.regions?.includes("GLOBAL") + ? `?filters=%5B%7B%22key%22%3A%22region%22%2C%22operator%22%3A%22%3D%3D%22%2C%22value%22%3A%22${userClaims?.regions[0]}%22%7D%5D` + : ""; const TablesView = () => { + const [settingsDialogState, setSettingsDialogState] = useState<{ + mode: null | TableSettingsDialogModes; + data: null | { + collection: string; + description: string; + roles: string[]; + name: string; + section: string; + }; + }>({ + mode: null, + data: null, + }); + const clearDialog = () => + setSettingsDialogState({ + mode: null, + data: null, + }); const classes = useStyles(); - const { sections, createTable, userClaims } = useFiretableContext(); + const { sections, userClaims } = useFiretableContext(); const { userDoc } = useAppContext(); const favs = userDoc.state.doc?.favoriteTables ? userDoc.state.doc.favoriteTables : []; - - const TableCard = props => { - const { table } = props; + const TableCard = ({ table }) => { const checked = Boolean(_find(favs, table)); return ( @@ -86,18 +114,7 @@ const TablesView = () => { className={classes.card} overline={table.section} title={table.name} - bodyContent={table.description} - primaryLink={{ - to: `${routes.table}/${table.collection}${ - table.regional && - userClaims?.regions && - !userClaims?.regions.includes("GLOBAL") - ? `?filters=%5B%7B%22key%22%3A%22region%22%2C%22operator%22%3A%22%3D%3D%22%2C%22value%22%3A%22${userClaims?.regions[0]}%22%7D%5D` - : "" - }`, - label: "Open", - }} - secondaryAction={ + headerAction={ { userDoc.dispatch({ @@ -115,59 +132,61 @@ const TablesView = () => { name="checkedH" /> } + bodyContent={table.description} + primaryLink={{ + to: `${routes.table}/${table.collection}${regionalFilter( + table?.regional ?? false, + userClaims + )}`, + label: "Open", + }} + secondaryAction={ + + setSettingsDialogState({ + mode: TableSettingsDialogModes.update, + data: table, + }) + } + > + + + } /> ); }; return ( -
- - - {(!userClaims?.roles || userClaims.roles.length === 0) && ( - - Please contact the Assistant to the Regional Manager of - your branch then{" "} - { - window.location.reload(); - }} - variant="body2" - style={{ verticalAlign: "baseline" }} - > - refresh this page - - . - - } - /> - )} - {favs.length !== 0 && ( -
- favorites - - - {favs.map(table => ( - - ))} - -
- )} - {sections ? ( - Object.keys(sections).map(sectionName => ( -
- - {sectionName == "undefined" ? "Other" : sectionName} - + <> +
+ + + {(!userClaims?.roles || userClaims.roles.length === 0) && ( + + Please contact the Assistant to the Regional Manager + of your branch then{" "} + { + window.location.reload(); + }} + variant="body2" + style={{ verticalAlign: "baseline" }} + > + refresh this page + + . + + } + /> + )} + {favs.length !== 0 && ( +
+ favorites { justify="flex-start" className={classes.cardGrid} > - {sections[sectionName].map(table => ( - + {favs.map(table => ( + ))}
- )) - ) : ( - - )} + )} + {sections ? ( + Object.keys(sections).map(sectionName => ( +
+ + {sectionName === "undefined" ? "Other" : sectionName} + + + + {sections[sectionName].map(table => ( + + ))} + +
+ )) + ) : ( + + )} +
+ + { + setSettingsDialogState({ + mode: TableSettingsDialogModes.create, + data: null, + }); + }} + > + + + +
+
+
- - { - if (createTable) { - createTable(tableName, collectionName); - } - }} - classes={{ fab: classes.createTableFab }} - /> - - -
+ + ); };