mirror of
https://github.com/rowyio/rowy.git
synced 2026-07-12 21:39:28 +02:00
Merge pull request #155 from AntlerVC/group-collections-table
Group collections table
This commit is contained in:
13
www/app.yaml
Normal file
13
www/app.yaml
Normal file
@@ -0,0 +1,13 @@
|
||||
# [START runtime]
|
||||
runtime: nodejs
|
||||
env: flex
|
||||
# [END runtime]
|
||||
|
||||
# [START handlers]
|
||||
handlers:
|
||||
- url: /
|
||||
static_files: build/index.html
|
||||
upload: build/index.html
|
||||
- url: /(.*)$
|
||||
static_files: build/\1
|
||||
upload: build/(.*)
|
||||
5
www/dispatch.yaml
Normal file
5
www/dispatch.yaml
Normal file
@@ -0,0 +1,5 @@
|
||||
dispatch:
|
||||
- url: "firetable-magic.uc.r.appspot.com/"
|
||||
service: default
|
||||
- url: "*.firetable.io/"
|
||||
service: default
|
||||
@@ -63,9 +63,11 @@
|
||||
"yup": "^0.27.0"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"start": "serve -s build",
|
||||
"prestart": "npm install -g serve",
|
||||
"local": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"test": "react-scripts test --env=jsdom",
|
||||
"eject": "react-scripts eject",
|
||||
"env": "node createDotEnv",
|
||||
"target": "firebase target:apply hosting firetable",
|
||||
|
||||
@@ -72,7 +72,12 @@ const App: React.FC = () => {
|
||||
|
||||
<PrivateRoute
|
||||
exact
|
||||
path={[routes.home, routes.tableWithId, routes.gridWithId]}
|
||||
path={[
|
||||
routes.home,
|
||||
routes.tableWithId,
|
||||
routes.tableGroupWithId,
|
||||
routes.gridWithId,
|
||||
]}
|
||||
render={() => (
|
||||
<FiretableContextProvider>
|
||||
<Switch>
|
||||
@@ -86,8 +91,8 @@ const App: React.FC = () => {
|
||||
render={() => <TableView />}
|
||||
/>
|
||||
<PrivateRoute
|
||||
path={routes.gridWithId}
|
||||
render={() => <GridView />}
|
||||
path={routes.tableGroupWithId}
|
||||
render={() => <TableView />}
|
||||
/>
|
||||
</Switch>
|
||||
</FiretableContextProvider>
|
||||
|
||||
@@ -28,7 +28,6 @@ const searchClient = algoliasearch(
|
||||
process.env.REACT_APP_ALGOLIA_APP_ID ?? "",
|
||||
process.env.REACT_APP_ALGOLIA_SEARCH_API_KEY ?? ""
|
||||
);
|
||||
console.log("SEARCH CLIENT", searchClient);
|
||||
|
||||
export interface IPopupContentsProps
|
||||
extends Omit<IConnectTableSelectProps, "className" | "TextFieldProps"> {}
|
||||
@@ -60,8 +59,6 @@ export default function PopupContents({
|
||||
const [search] = useDebouncedCallback(
|
||||
async (query: string) => {
|
||||
if (!algoliaIndex) return;
|
||||
console.log("SEARCH", query, algoliaIndex, row);
|
||||
|
||||
const data = { ...userClaims, ...row };
|
||||
const filters = config?.filters
|
||||
? config?.filters.replace(/\{\{(.*?)\}\}/g, (m, k) => data[k])
|
||||
|
||||
@@ -75,8 +75,9 @@ export default function Navigation({
|
||||
// Get the table path, including filtering for user permissions
|
||||
const getTablePath = (table: Table): LinkProps["to"] => {
|
||||
if (!table || !userClaims) return "";
|
||||
|
||||
return table.collection;
|
||||
return table.isCollectionGroup
|
||||
? `/tableGroup/${table.collection}`
|
||||
: `/table/${table.collection}`;
|
||||
};
|
||||
|
||||
const currentCollection = tableCollection.split("/")[0];
|
||||
@@ -160,6 +161,7 @@ export default function Navigation({
|
||||
disabled={sectionName === section}
|
||||
// onClick={handleSectionClick(sectionName)}
|
||||
component={Link}
|
||||
replace={true}
|
||||
to={getTablePath(tables[0])}
|
||||
color="inherit"
|
||||
className={classes.sectionButton}
|
||||
|
||||
@@ -54,8 +54,6 @@ function Action({
|
||||
const snack = useContext(SnackContext);
|
||||
const handleRun = () => {
|
||||
setIsRunning(true);
|
||||
console.log("RUN");
|
||||
|
||||
cloudFunction(
|
||||
config.callableName,
|
||||
{
|
||||
|
||||
@@ -37,9 +37,9 @@ const useStyles = makeStyles(theme =>
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
export default function CodeEditor(props: any) {
|
||||
const { handleChange, script } = props;
|
||||
console.log({ script });
|
||||
const classes = useStyles();
|
||||
const editor = useRef<AceEditor>(null);
|
||||
const handleResize = () => {
|
||||
|
||||
@@ -28,7 +28,6 @@ const ColumnSelector = ({
|
||||
};
|
||||
useEffect(() => {
|
||||
if (table) {
|
||||
console.log({ table });
|
||||
getColumns(table);
|
||||
}
|
||||
}, [table]);
|
||||
|
||||
@@ -219,7 +219,6 @@ export default function FormDialog({
|
||||
<ConfigForm
|
||||
type={type}
|
||||
handleChange={key => update => {
|
||||
console.log(key, update);
|
||||
setNewConfig({ ...newConfig, [key]: update });
|
||||
}}
|
||||
config={newConfig}
|
||||
|
||||
@@ -21,6 +21,7 @@ import { SnackContext } from "contexts/snackContext";
|
||||
import { useFiretableContext } from "contexts/firetableContext";
|
||||
import { db } from "../../firebase";
|
||||
import { FieldType } from "constants/fields";
|
||||
import { isCollectionGroup } from "util/fns";
|
||||
|
||||
const ITEM_HEIGHT = 48;
|
||||
const ITEM_PADDING_TOP = 8;
|
||||
@@ -159,7 +160,9 @@ export default function ExportCSV() {
|
||||
duration: 5000,
|
||||
});
|
||||
|
||||
let query: any = db.collection(tableState?.tablePath!);
|
||||
let query: any = isCollectionGroup()
|
||||
? db.collectionGroup(tableState?.tablePath!)
|
||||
: db.collection(tableState?.tablePath!);
|
||||
// add filters
|
||||
tableState?.filters.forEach(filter => {
|
||||
query = query.where(filter.key, filter.operator, filter.value);
|
||||
|
||||
@@ -13,7 +13,6 @@ const AlgoliaSelect = (props: any) => {
|
||||
} = props;
|
||||
const [searchState] = useAlgolia(algoliaIndex, algoliaKey, filters);
|
||||
|
||||
console.log(filters);
|
||||
const [options, setOptions] = useState<any[]>([]);
|
||||
useEffect(() => {
|
||||
if (Array.isArray(searchState.results?.hits)) {
|
||||
|
||||
@@ -9,8 +9,6 @@ const MigrateButton = ({ columns, needsMigration }) => {
|
||||
|
||||
const configDocPath = tableState?.config.tableConfig.path;
|
||||
const handleColumnMigration = async () => {
|
||||
console.log({ columns });
|
||||
|
||||
const newColumns = columns.reduce((acc, currCol, currIndex) => {
|
||||
const baseCol = {
|
||||
...currCol,
|
||||
|
||||
@@ -9,6 +9,8 @@ import {
|
||||
Typography,
|
||||
Button,
|
||||
} from "@material-ui/core";
|
||||
|
||||
import { isCollectionGroup } from "../../util/fns";
|
||||
import AddIcon from "@material-ui/icons/Add";
|
||||
|
||||
import Filters from "./Filters";
|
||||
@@ -84,25 +86,27 @@ export default function TableHeader({
|
||||
className={classes.root}
|
||||
>
|
||||
<MigrateButton needsMigration={needsMigration} columns={tempColumns} />
|
||||
<Grid item>
|
||||
<Button
|
||||
onClick={() => {
|
||||
const initialVal = tempColumns.reduce((acc, currCol) => {
|
||||
if (currCol.type === FieldType.checkbox) {
|
||||
return { ...acc, [currCol.key]: false };
|
||||
} else {
|
||||
return acc;
|
||||
}
|
||||
}, {});
|
||||
tableActions?.row.add(initialVal);
|
||||
}}
|
||||
variant="contained"
|
||||
color="primary"
|
||||
startIcon={<AddIcon />}
|
||||
>
|
||||
Add Row
|
||||
</Button>
|
||||
</Grid>
|
||||
{!isCollectionGroup() && (
|
||||
<Grid item>
|
||||
<Button
|
||||
onClick={() => {
|
||||
const initialVal = tempColumns.reduce((acc, currCol) => {
|
||||
if (currCol.type === FieldType.checkbox) {
|
||||
return { ...acc, [currCol.key]: false };
|
||||
} else {
|
||||
return acc;
|
||||
}
|
||||
}, {});
|
||||
tableActions?.row.add(initialVal);
|
||||
}}
|
||||
variant="contained"
|
||||
color="primary"
|
||||
startIcon={<AddIcon />}
|
||||
>
|
||||
Add Row
|
||||
</Button>
|
||||
</Grid>
|
||||
)}
|
||||
|
||||
<Grid item>{/* <HiddenFields /> */}</Grid>
|
||||
<Grid item>
|
||||
@@ -150,9 +154,11 @@ export default function TableHeader({
|
||||
|
||||
<Grid item />
|
||||
|
||||
<Grid item>
|
||||
<ImportCSV />
|
||||
</Grid>
|
||||
{!isCollectionGroup() && (
|
||||
<Grid item>
|
||||
<ImportCSV />
|
||||
</Grid>
|
||||
)}
|
||||
|
||||
<Grid item>
|
||||
<ExportCSV />
|
||||
|
||||
@@ -44,12 +44,7 @@ export type FiretableColumn = Column<any> & {
|
||||
[key: string]: any;
|
||||
};
|
||||
|
||||
interface ITableProps {
|
||||
collection: string;
|
||||
filters: FireTableFilter[];
|
||||
}
|
||||
|
||||
export default function Table({ collection, filters }: ITableProps) {
|
||||
export default function Table() {
|
||||
const classes = useStyles();
|
||||
const theme = useTheme();
|
||||
const finalColumnClasses = useFinalColumnStyles();
|
||||
@@ -64,12 +59,6 @@ export default function Table({ collection, filters }: ITableProps) {
|
||||
const { userDoc } = useAppContext();
|
||||
const userDocHiddenFields =
|
||||
userDoc.state.doc?.tables[`${tableState?.tablePath}`]?.hiddenFields ?? [];
|
||||
useEffect(() => {
|
||||
if (tableActions && tableState && tableState.tablePath !== collection) {
|
||||
tableActions.table.set(collection, filters);
|
||||
if (sideDrawerRef?.current) sideDrawerRef.current.setCell!(null);
|
||||
}
|
||||
}, [collection]);
|
||||
|
||||
const rowsContainerRef = useRef<HTMLDivElement>(null);
|
||||
// Gets more rows when scrolled down.
|
||||
@@ -138,7 +127,7 @@ export default function Table({ collection, filters }: ITableProps) {
|
||||
const rows = tableState.rows;
|
||||
const rowGetter = (rowIdx: number) => rows[rowIdx];
|
||||
|
||||
const inSubTable = collection.split("/").length > 1;
|
||||
const inSubTable = tableState.tablePath.split("/").length > 1;
|
||||
|
||||
let tableWidth: any = `calc(100% - ${
|
||||
DRAWER_COLLAPSED_WIDTH
|
||||
@@ -152,7 +141,7 @@ export default function Table({ collection, filters }: ITableProps) {
|
||||
<Hotkeys selectedCell={selectedCell} />
|
||||
</Suspense> */}
|
||||
|
||||
{inSubTable && <SubTableBreadcrumbs collection={collection} />}
|
||||
{inSubTable && <SubTableBreadcrumbs collection={tableState.tablePath} />}
|
||||
|
||||
<TableHeader
|
||||
rowHeight={rowHeight}
|
||||
|
||||
@@ -11,11 +11,12 @@ import {
|
||||
DialogTitle,
|
||||
TextField,
|
||||
Button,
|
||||
Select,
|
||||
Typography,
|
||||
Switch,
|
||||
FormControlLabel,
|
||||
} from "@material-ui/core";
|
||||
import { useFiretableContext } from "../contexts/firetableContext";
|
||||
import OptionsInput from "./Table/ColumnMenu/ConfigFields/OptionsInput";
|
||||
|
||||
export enum TableSettingsDialogModes {
|
||||
create,
|
||||
update,
|
||||
@@ -31,6 +32,7 @@ export interface ICreateTableDialogProps {
|
||||
section: string;
|
||||
description: string;
|
||||
name: string;
|
||||
isCollectionGroup: boolean;
|
||||
} | null;
|
||||
}
|
||||
|
||||
@@ -40,6 +42,7 @@ const FORM_EMPTY_STATE = {
|
||||
collection: "",
|
||||
description: "",
|
||||
roles: ["ADMIN"],
|
||||
isCollectionGroup: false,
|
||||
};
|
||||
export default function TableSettingsDialog({
|
||||
mode,
|
||||
@@ -66,7 +69,11 @@ export default function TableSettingsDialog({
|
||||
settingsActions?.createTable(formState);
|
||||
|
||||
if (router.location.pathname === "/") {
|
||||
router.history.push(`table/${formState.collection}`);
|
||||
router.history.push(
|
||||
`${formState.isCollectionGroup ? "tableGroup" : "table"}/${
|
||||
formState.collection
|
||||
}`
|
||||
);
|
||||
} else {
|
||||
router.history.push(formState.collection);
|
||||
}
|
||||
@@ -123,6 +130,16 @@ export default function TableSettingsDialog({
|
||||
/>
|
||||
)}
|
||||
|
||||
<FormControlLabel
|
||||
control={<Switch />}
|
||||
label={"isCollectionGroup"}
|
||||
labelPlacement="start"
|
||||
value={formState.isCollectionGroup}
|
||||
onChange={e =>
|
||||
handleChange("isCollectionGroup", !formState.isCollectionGroup)
|
||||
}
|
||||
// classes={{ root: classes.formControlLabel, label: classes.label }}
|
||||
/>
|
||||
<TextField
|
||||
value={formState.section}
|
||||
onChange={e => handleChange("section", e.target.value)}
|
||||
|
||||
@@ -5,7 +5,10 @@ export enum routes {
|
||||
signOut = "/signOut",
|
||||
|
||||
table = "/table",
|
||||
tableGroup = "/tableGroup",
|
||||
|
||||
tableWithId = "/table/:id",
|
||||
tableGroupWithId = "/tableGroup/:id",
|
||||
grid = "/grid",
|
||||
gridWithId = "/grid/:id",
|
||||
editor = "/editor",
|
||||
|
||||
@@ -19,6 +19,7 @@ export type Table = {
|
||||
roles: string[];
|
||||
description: string;
|
||||
section: string;
|
||||
isCollectionGroup: boolean;
|
||||
};
|
||||
|
||||
interface FiretableContextProps {
|
||||
|
||||
43
www/src/firebase/experiment.ts
Normal file
43
www/src/firebase/experiment.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import firebase from "firebase/app";
|
||||
import "firebase/auth";
|
||||
import "firebase/firestore";
|
||||
import "firebase/functions";
|
||||
import "firebase/storage";
|
||||
|
||||
export let auth: any = false;
|
||||
|
||||
export let db: any = false;
|
||||
|
||||
export let bucket: any = false;
|
||||
export let functions: any = false;
|
||||
|
||||
export let googleProvider: any = false;
|
||||
|
||||
console.log(`fetching config for ${window.location.hostname.split(".")[0]}`);
|
||||
fetch(
|
||||
`https://us-central1-firetable-magic.cloudfunctions.net/getWebAppConfig?projectId=${window.location.hostname.split(
|
||||
"."
|
||||
)[0] ?? "antler-vc"}`
|
||||
)
|
||||
.then(async response => {
|
||||
const config = await response.json();
|
||||
console.log({ config });
|
||||
firebase.initializeApp(config);
|
||||
auth = firebase.auth();
|
||||
|
||||
db = firebase.firestore();
|
||||
// db.settings({ cacheSizeBytes: firebase.firestore.CACHE_SIZE_UNLIMITED });
|
||||
// db.enablePersistence({ synchronizeTabs: true });
|
||||
|
||||
bucket = firebase.storage();
|
||||
functions = firebase.functions();
|
||||
|
||||
googleProvider = new firebase.auth.GoogleAuthProvider().setCustomParameters(
|
||||
{
|
||||
prompt: "select_account",
|
||||
}
|
||||
);
|
||||
})
|
||||
.catch(err => console.log(err));
|
||||
|
||||
export const deleteField = firebase.firestore.FieldValue.delete;
|
||||
@@ -49,11 +49,6 @@ const useFiretable = (
|
||||
orderBy,
|
||||
});
|
||||
|
||||
/**Subscribes to _FIRETABLE_/settings/schema to have table configuration pre-cached */
|
||||
const [settingsState, setttingsDispatch] = useTable({
|
||||
path: "_FIRETABLE_/settings/schema",
|
||||
});
|
||||
|
||||
/** set collection path of table */
|
||||
const setTable = (collectionName: string, filters: FireTableFilter[]) => {
|
||||
if (collectionName !== tableState.path || filters !== tableState.filters) {
|
||||
|
||||
@@ -7,6 +7,7 @@ import firebase from "firebase/app";
|
||||
import { FireTableFilter, FiretableOrderBy } from ".";
|
||||
import { SnackContext } from "../../contexts/snackContext";
|
||||
import { cloudFunction } from "../../firebase/callables";
|
||||
import { isCollectionGroup } from "util/fns";
|
||||
const CAP = 1000; // safety paramter sets the upper limit of number of docs fetched by this hook
|
||||
const serverTimestamp = firebase.firestore.FieldValue.serverTimestamp;
|
||||
var characters =
|
||||
@@ -87,7 +88,9 @@ const useTable = (initialOverrides: any) => {
|
||||
});
|
||||
let query:
|
||||
| firebase.firestore.CollectionReference
|
||||
| firebase.firestore.Query = db.collection(tableState.path);
|
||||
| firebase.firestore.Query = isCollectionGroup()
|
||||
? db.collectionGroup(tableState.path)
|
||||
: db.collection(tableState.path);
|
||||
|
||||
filters.forEach(filter => {
|
||||
query = query.where(filter.key, filter.operator, filter.value);
|
||||
|
||||
@@ -6,7 +6,7 @@ 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 { arrayMover, isCollectionGroup } from "../../util/fns";
|
||||
import { db, deleteField } from "../../firebase";
|
||||
|
||||
//import
|
||||
@@ -14,10 +14,9 @@ import { db, deleteField } from "../../firebase";
|
||||
const formatPathRegex = /\/[^\/]+\/([^\/]+)/g;
|
||||
|
||||
const formatPath = (tablePath: string) => {
|
||||
return `_FIRETABLE_/settings/schema/${tablePath.replace(
|
||||
formatPathRegex,
|
||||
"/subTables/$1"
|
||||
)}`;
|
||||
return `_FIRETABLE_/settings/${
|
||||
isCollectionGroup() ? "groupSchema" : "schema"
|
||||
}/${tablePath.replace(formatPathRegex, "/subTables/$1")}`;
|
||||
};
|
||||
const useTableConfig = (tablePath?: string) => {
|
||||
const [tableConfigState, documentDispatch] = useDoc({
|
||||
@@ -50,7 +49,6 @@ const useTableConfig = (tablePath?: string) => {
|
||||
const add = (name: string, type: FieldType, data?: any) => {
|
||||
//TODO: validation
|
||||
const { columns } = tableConfigState;
|
||||
console.log({ columns });
|
||||
const newIndex = Object.keys(columns).length;
|
||||
let updatedColumns = columns;
|
||||
const key = _camelCase(name);
|
||||
|
||||
@@ -53,3 +53,8 @@ export const sanitiseRowData = (rowData: any) => {
|
||||
});
|
||||
return rowData;
|
||||
};
|
||||
|
||||
export const isCollectionGroup = () => {
|
||||
const pathName = window.location.pathname.split("/")[1];
|
||||
return pathName === "tableGroup";
|
||||
};
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from "react";
|
||||
import React, { useEffect } from "react";
|
||||
import queryString from "query-string";
|
||||
import { useFiretableContext } from "contexts/firetableContext";
|
||||
|
||||
import { Hidden } from "@material-ui/core";
|
||||
|
||||
@@ -13,7 +14,7 @@ import useRouter from "hooks/useRouter";
|
||||
export default function TableView() {
|
||||
const router = useRouter();
|
||||
const tableCollection = decodeURIComponent(router.match.params.id);
|
||||
|
||||
const { tableState, tableActions, sideDrawerRef } = useFiretableContext();
|
||||
let filters: FireTableFilter[] = [];
|
||||
const parsed = queryString.parse(router.location.search);
|
||||
if (typeof parsed.filters === "string") {
|
||||
@@ -23,14 +24,20 @@ export default function TableView() {
|
||||
//TODO: json schema validator
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
tableActions &&
|
||||
tableState &&
|
||||
tableState.tablePath !== tableCollection
|
||||
) {
|
||||
tableActions.table.set(tableCollection, filters);
|
||||
if (sideDrawerRef?.current) sideDrawerRef.current.setCell!(null);
|
||||
}
|
||||
}, [tableCollection]);
|
||||
if (!tableState?.tablePath) return <></>;
|
||||
return (
|
||||
<Navigation tableCollection={tableCollection}>
|
||||
<Table
|
||||
key={tableCollection}
|
||||
collection={tableCollection}
|
||||
filters={filters}
|
||||
/>
|
||||
|
||||
<Table key={tableCollection} />
|
||||
<Hidden smDown>
|
||||
<SideDrawer />
|
||||
</Hidden>
|
||||
|
||||
@@ -89,6 +89,7 @@ const TablesView = () => {
|
||||
roles: string[];
|
||||
name: string;
|
||||
section: string;
|
||||
isCollectionGroup: boolean;
|
||||
};
|
||||
}>({
|
||||
mode: null,
|
||||
@@ -137,7 +138,9 @@ const TablesView = () => {
|
||||
}
|
||||
bodyContent={table.description}
|
||||
primaryLink={{
|
||||
to: `${routes.table}/${table.collection}`,
|
||||
to: `${
|
||||
table.isCollectionGroup ? routes.tableGroup : routes.table
|
||||
}/${table.collection}`,
|
||||
label: "Open",
|
||||
}}
|
||||
secondaryAction={
|
||||
|
||||
Reference in New Issue
Block a user