mirror of
https://github.com/rowyio/rowy.git
synced 2026-09-01 19:50:46 +02:00
@@ -1,5 +1,5 @@
|
||||
import { useState } from "react";
|
||||
import Editor, { EditorProps } from "@monaco-editor/react";
|
||||
import Editor, { EditorProps, Monaco } from "@monaco-editor/react";
|
||||
import type { editor } from "monaco-editor/esm/vs/editor/editor.api";
|
||||
|
||||
import { useTheme, Box, BoxProps, AppBar, Toolbar } from "@mui/material";
|
||||
@@ -72,6 +72,36 @@ export default function CodeEditor({
|
||||
onValidate?.(markers);
|
||||
};
|
||||
|
||||
const validate = (monaco: Monaco, model: editor.ITextModel) => {
|
||||
const markers = [];
|
||||
for (let i = 1; i < model.getLineCount() + 1; i++) {
|
||||
const range = {
|
||||
startLineNumber: i,
|
||||
startColumn: 1,
|
||||
endLineNumber: i,
|
||||
endColumn: model.getLineLength(i) + 1,
|
||||
};
|
||||
const line = model.getValueInRange(range);
|
||||
for (const keyword of ["console.log", "console.warn", "console.error"]) {
|
||||
const consoleLogIndex = line.indexOf(keyword);
|
||||
if (consoleLogIndex >= 0) {
|
||||
markers.push({
|
||||
message: `Replace with ${keyword.replace(
|
||||
"console",
|
||||
"logging"
|
||||
)}: Rowy Cloud Logging provides a better experience to view logs. Simply replace 'console' with 'logging'. \n\nhttps://docs.rowy.io/cloud-logs`,
|
||||
severity: monaco.MarkerSeverity.Warning,
|
||||
startLineNumber: range.startLineNumber,
|
||||
endLineNumber: range.endLineNumber,
|
||||
startColumn: consoleLogIndex + 1,
|
||||
endColumn: consoleLogIndex + keyword.length + 1,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
monaco.editor.setModelMarkers(model, "owner", markers);
|
||||
};
|
||||
|
||||
return (
|
||||
<TrapFocus open={fullScreen}>
|
||||
<Box
|
||||
@@ -94,6 +124,12 @@ export default function CodeEditor({
|
||||
beforeMount={(monaco) => {
|
||||
monaco.editor.defineTheme("github-light", githubLightTheme as any);
|
||||
monaco.editor.defineTheme("github-dark", githubDarkTheme as any);
|
||||
monaco.editor.onDidCreateModel((model) => {
|
||||
validate(monaco, model);
|
||||
model.onDidChangeContent(() => {
|
||||
validate(monaco, model);
|
||||
});
|
||||
});
|
||||
}}
|
||||
onMount={(editor) => {
|
||||
if (onFocus) editor.onDidFocusEditorWidget(onFocus);
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
import { useEffect } from "react";
|
||||
// import {
|
||||
// quicktype,
|
||||
// InputData,
|
||||
// jsonInputForTargetLanguage,
|
||||
// } from "quicktype-core";
|
||||
import { useAtom } from "jotai";
|
||||
|
||||
import {
|
||||
@@ -13,15 +8,10 @@ import {
|
||||
} from "@src/atoms/tableScope";
|
||||
import { useMonaco } from "@monaco-editor/react";
|
||||
import type { languages } from "monaco-editor/esm/vs/editor/editor.api";
|
||||
import githubLightTheme from "./github-light-default.json";
|
||||
import githubDarkTheme from "./github-dark-default.json";
|
||||
|
||||
import { useTheme } from "@mui/material";
|
||||
import type { SystemStyleObject, Theme } from "@mui/system";
|
||||
|
||||
// TODO:
|
||||
// import { getFieldType, getFieldProp } from "@src/components/fields";
|
||||
|
||||
/* eslint-disable import/no-webpack-loader-syntax */
|
||||
import firestoreDefs from "!!raw-loader!./firestore.d.ts";
|
||||
import firebaseAuthDefs from "!!raw-loader!./firebaseAuth.d.ts";
|
||||
@@ -72,7 +62,6 @@ export default function useMonacoCustomizations({
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Initialize external libs & TypeScript compiler options
|
||||
useEffect(() => {
|
||||
if (!monaco) return;
|
||||
|
||||
@@ -95,6 +84,8 @@ export default function useMonacoCustomizations({
|
||||
"ts:filename/utils.d.ts"
|
||||
);
|
||||
monaco.languages.typescript.javascriptDefaults.addExtraLib(rowyUtilsDefs);
|
||||
|
||||
setLoggingReplacementActions();
|
||||
} catch (error) {
|
||||
console.error(
|
||||
"An error occurred during initialization of Monaco: ",
|
||||
@@ -135,6 +126,52 @@ export default function useMonacoCustomizations({
|
||||
}
|
||||
}, [monaco, stringifiedDiagnosticsOptions]);
|
||||
|
||||
const setLoggingReplacementActions = () => {
|
||||
if (!monaco) return;
|
||||
const { dispose } = monaco.languages.registerCodeActionProvider(
|
||||
"javascript",
|
||||
{
|
||||
provideCodeActions: (model, range, context, token) => {
|
||||
const actions = context.markers
|
||||
.filter((error) => {
|
||||
return error.message.includes("Rowy Cloud Logging");
|
||||
})
|
||||
.map((error) => {
|
||||
// first sentence of the message is "Replace with logging.[log/warn/error]"
|
||||
const firstSentence = error.message.split(":")[0];
|
||||
const replacement = firstSentence.split("with ")[1];
|
||||
return {
|
||||
title: firstSentence,
|
||||
diagnostics: [error],
|
||||
kind: "quickfix",
|
||||
edit: {
|
||||
edits: [
|
||||
{
|
||||
resource: model.uri,
|
||||
edit: {
|
||||
range: error,
|
||||
text: replacement,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
isPreferred: true,
|
||||
};
|
||||
});
|
||||
return {
|
||||
actions: actions,
|
||||
dispose: () => {},
|
||||
};
|
||||
},
|
||||
}
|
||||
);
|
||||
monaco.editor.onWillDisposeModel((model) => {
|
||||
// dispose code action provider when model is disposed
|
||||
// this makes sure code actions are not displayed multiple times
|
||||
dispose();
|
||||
});
|
||||
};
|
||||
|
||||
const addJsonFieldDefinition = async (
|
||||
columnKey: string,
|
||||
interfaceName: string
|
||||
|
||||
@@ -18,13 +18,13 @@ function useSaveTableSorts(canEditColumns: boolean) {
|
||||
const [updateTableSchema] = useAtom(updateTableSchemaAtom, tableScope);
|
||||
const [updateUserSettings] = useAtom(updateUserSettingsAtom, projectScope);
|
||||
const [tableId] = useAtom(tableIdAtom, tableScope);
|
||||
if (!updateTableSchema) throw new Error("Cannot update table schema");
|
||||
const { enqueueSnackbar, closeSnackbar } = useSnackbar();
|
||||
const [snackbarId, setSnackbarId] = useState<SnackbarKey | null>(null);
|
||||
|
||||
// Offer to save when table sorts changes
|
||||
const trigger = useCallback(
|
||||
(sorts: TableSort[]) => {
|
||||
if (!updateTableSchema) throw new Error("Cannot update table schema");
|
||||
if (updateUserSettings) {
|
||||
updateUserSettings({
|
||||
tables: {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useMemo, useRef, useState, useEffect, useCallback } from "react";
|
||||
import useStateRef from "react-usestateref";
|
||||
// import useStateRef from "react-usestateref"; // testing with useStateWithRef
|
||||
import { useAtom, useSetAtom } from "jotai";
|
||||
import { useThrottledCallback } from "use-debounce";
|
||||
import {
|
||||
@@ -41,6 +41,7 @@ import { useMenuAction } from "./useMenuAction";
|
||||
import { useSaveColumnSizing } from "./useSaveColumnSizing";
|
||||
import useHotKeys from "./useHotKey";
|
||||
import type { TableRow, ColumnConfig } from "@src/types/table";
|
||||
import useStateWithRef from "./useStateWithRef"; // testing with useStateWithRef
|
||||
|
||||
export const DEFAULT_ROW_HEIGHT = 41;
|
||||
export const DEFAULT_COL_WIDTH = 150;
|
||||
@@ -110,7 +111,9 @@ export default function Table({
|
||||
// so the state can re-render `TableBody`, preventing virtualization
|
||||
// not detecting scroll if the container element was initially `null`
|
||||
const [containerEl, setContainerEl, containerRef] =
|
||||
useStateRef<HTMLDivElement | null>(null);
|
||||
// useStateRef<HTMLDivElement | null>(null); // <-- older approach with useStateRef
|
||||
useStateWithRef<HTMLDivElement | null>(null); // <-- newer approach with custom hook
|
||||
|
||||
const gridRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Get column defs from table schema
|
||||
@@ -255,7 +258,10 @@ export default function Table({
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={(el) => setContainerEl(el)}
|
||||
ref={(el) => {
|
||||
if (!el) return;
|
||||
setContainerEl(el);
|
||||
}}
|
||||
onScroll={(e) => fetchMoreOnBottomReached(e.target as HTMLDivElement)}
|
||||
style={{ overflow: "auto", width: "100%", height: "100%" }}
|
||||
>
|
||||
|
||||
29
src/components/Table/useStateWithRef.ts
Normal file
29
src/components/Table/useStateWithRef.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import {
|
||||
MutableRefObject,
|
||||
useCallback,
|
||||
useRef,
|
||||
useSyncExternalStore,
|
||||
} from "react";
|
||||
|
||||
// NOTE: This is not the final solution. But is a potential solution for this problem.
|
||||
export default function useStateWithRef<T>(
|
||||
initialState: T
|
||||
): [T, (newValue: T) => void, MutableRefObject<T>] {
|
||||
const value = useRef<T>(initialState);
|
||||
const get = useCallback(() => value.current, []);
|
||||
const subscribers = useRef(new Set<() => void>());
|
||||
|
||||
const set = useCallback((newValue: T) => {
|
||||
value.current = newValue;
|
||||
subscribers.current.forEach((callback) => callback());
|
||||
}, []);
|
||||
|
||||
const subscribe = useCallback((callback: () => void) => {
|
||||
subscribers.current.add(callback);
|
||||
return () => subscribers.current.delete(callback);
|
||||
}, []);
|
||||
|
||||
const state = useSyncExternalStore(subscribe, get);
|
||||
|
||||
return [state, set, value];
|
||||
}
|
||||
22
src/components/Table/useTraceUpdates.ts
Normal file
22
src/components/Table/useTraceUpdates.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
// This hook is used to log changes to props in a component.
|
||||
export default function useTraceUpdates(
|
||||
props: { [key: string]: any },
|
||||
printMessage: string = "Changed props:"
|
||||
) {
|
||||
const prev = useRef(props);
|
||||
useEffect(() => {
|
||||
const changedProps = Object.entries(props).reduce((ps, [k, v]) => {
|
||||
if (prev.current[k] !== v) {
|
||||
// @ts-ignore
|
||||
ps[k] = [prev.current[k], v];
|
||||
}
|
||||
return ps;
|
||||
}, {});
|
||||
if (Object.keys(changedProps).length > 0) {
|
||||
console.log(printMessage, changedProps);
|
||||
}
|
||||
prev.current = props;
|
||||
});
|
||||
}
|
||||
@@ -23,14 +23,7 @@ const PreviewTable = ({ tableSchema }: { tableSchema: TableSchema }) => {
|
||||
scope={tableScope}
|
||||
initialValues={[
|
||||
[currentUserAtom, currentUser],
|
||||
[
|
||||
tableSettingsAtom,
|
||||
{
|
||||
...tableSettings,
|
||||
id: "preview-table",
|
||||
collection: "preview-collection",
|
||||
},
|
||||
],
|
||||
[tableSettingsAtom, tableSettings],
|
||||
[tableRowsDbAtom, []],
|
||||
]}
|
||||
>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useCallback, useEffect } from "react";
|
||||
import { useSetAtom } from "jotai";
|
||||
import { useAtom, useSetAtom } from "jotai";
|
||||
import { useAtomCallback } from "jotai/utils";
|
||||
import { cloneDeep, findIndex, sortBy } from "lodash-es";
|
||||
|
||||
@@ -10,39 +10,24 @@ import {
|
||||
tableRowsDbAtom,
|
||||
tableSchemaAtom,
|
||||
tableScope,
|
||||
tableSettingsAtom,
|
||||
} from "@src/atoms/tableScope";
|
||||
|
||||
import { TableRow, TableSchema } from "@src/types/table";
|
||||
import { updateRowData } from "@src/utils/table";
|
||||
|
||||
const initialRows = [
|
||||
{
|
||||
_rowy_ref: {
|
||||
id: "zzzzzzzzzzzzzzzzzzzw",
|
||||
path: "preview-collection/zzzzzzzzzzzzzzzzzzzw",
|
||||
},
|
||||
},
|
||||
{
|
||||
_rowy_ref: {
|
||||
id: "zzzzzzzzzzzzzzzzzzzx",
|
||||
path: "preview-collection/zzzzzzzzzzzzzzzzzzzx",
|
||||
},
|
||||
},
|
||||
{
|
||||
_rowy_ref: {
|
||||
id: "zzzzzzzzzzzzzzzzzzzy",
|
||||
path: "preview-collection/zzzzzzzzzzzzzzzzzzzy",
|
||||
},
|
||||
},
|
||||
];
|
||||
import { serializeRef } from "./util";
|
||||
|
||||
const TableSourcePreview = ({ tableSchema }: { tableSchema: TableSchema }) => {
|
||||
const [tableSettings] = useAtom(tableSettingsAtom, tableScope);
|
||||
const setTableSchemaAtom = useSetAtom(tableSchemaAtom, tableScope);
|
||||
const setRows = useSetAtom(tableRowsDbAtom, tableScope);
|
||||
|
||||
useEffect(() => {
|
||||
setRows(initialRows);
|
||||
}, [setRows]);
|
||||
setRows(
|
||||
["preview-doc-1", "preview-doc-2", "preview-doc-3"].map((docId) => ({
|
||||
_rowy_ref: serializeRef(`${tableSettings.collection}/${docId}`),
|
||||
}))
|
||||
);
|
||||
}, [setRows, tableSettings.collection]);
|
||||
|
||||
useEffect(() => {
|
||||
setTableSchemaAtom(() => ({
|
||||
@@ -52,7 +37,7 @@ const TableSourcePreview = ({ tableSchema }: { tableSchema: TableSchema }) => {
|
||||
}, [tableSchema, setTableSchemaAtom]);
|
||||
|
||||
const readRowsDb = useAtomCallback(
|
||||
useCallback((get) => get(tableRowsDbAtom) || initialRows, []),
|
||||
useCallback((get) => get(tableRowsDbAtom) || [], []),
|
||||
tableScope
|
||||
);
|
||||
|
||||
|
||||
7
src/components/fields/Formula/formula.d.ts
vendored
7
src/components/fields/Formula/formula.d.ts
vendored
@@ -1,10 +1,9 @@
|
||||
type RowRef = Pick<FirebaseFirestore.DocumentReference, "id", "path">;
|
||||
type RowRef<T> = { id: string; path: string; parent: T };
|
||||
interface Ref extends RowRef<Ref> {}
|
||||
|
||||
type FormulaContext = {
|
||||
row: Row;
|
||||
ref: RowRef;
|
||||
// storage: firebasestorage.Storage;
|
||||
// db: FirebaseFirestore.Firestore;
|
||||
ref: Ref;
|
||||
};
|
||||
|
||||
type Formula = (context: FormulaContext) => "PLACEHOLDER_OUTPUT_TYPE";
|
||||
|
||||
@@ -5,7 +5,11 @@ import { useAtom } from "jotai";
|
||||
import { TableRow, TableRowRef } from "@src/types/table";
|
||||
import { tableColumnsOrderedAtom, tableScope } from "@src/atoms/tableScope";
|
||||
|
||||
import { listenerFieldTypes, useDeepCompareMemoize } from "./util";
|
||||
import {
|
||||
listenerFieldTypes,
|
||||
serializeRef,
|
||||
useDeepCompareMemoize,
|
||||
} from "./util";
|
||||
|
||||
export const useFormula = ({
|
||||
row,
|
||||
@@ -60,11 +64,13 @@ export const useFormula = ({
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
worker.postMessage({
|
||||
formulaFn,
|
||||
row: JSON.stringify(availableFields),
|
||||
ref: { id: ref.id, path: ref.path },
|
||||
});
|
||||
worker.postMessage(
|
||||
JSON.stringify({
|
||||
formulaFn,
|
||||
row: availableFields,
|
||||
ref: serializeRef(ref.path),
|
||||
})
|
||||
);
|
||||
|
||||
return () => {
|
||||
worker.terminate();
|
||||
|
||||
@@ -24,6 +24,8 @@ import JsonDisplayCell from "@src/components/fields/Json/DisplayCell";
|
||||
import CodeDisplayCell from "@src/components/fields/Code/DisplayCell";
|
||||
import MarkdownDisplayCell from "@src/components/fields/Markdown/DisplayCell";
|
||||
import CreatedByDisplayCell from "@src/components/fields/CreatedBy/DisplayCell";
|
||||
import { TableRowRef } from "@src/types/table";
|
||||
import { DocumentData, DocumentReference } from "firebase/firestore";
|
||||
|
||||
export function useDeepCompareMemoize<T>(value: T) {
|
||||
const ref = useRef<T>(value);
|
||||
@@ -65,7 +67,7 @@ export const outputFieldTypes = Object.values(FieldType).filter(
|
||||
].includes(type)
|
||||
);
|
||||
|
||||
export const defaultFn = `const formula:Formula = async ({ row })=> {
|
||||
export const defaultFn = `const formula:Formula = async ({ row, ref })=> {
|
||||
// WRITE YOUR CODE ONLY BELOW THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY
|
||||
|
||||
// Example:
|
||||
@@ -120,3 +122,23 @@ export const getDisplayCell = (type: FieldType) => {
|
||||
return ShortTextDisplayCell;
|
||||
}
|
||||
};
|
||||
|
||||
export const serializeRef = (path: string, maxDepth = 20) => {
|
||||
const pathArr = path.split("/");
|
||||
const serializedRef = {
|
||||
path: pathArr.join("/"),
|
||||
id: pathArr.pop(),
|
||||
} as any;
|
||||
let curr: TableRowRef | Partial<DocumentReference<DocumentData>> =
|
||||
serializedRef;
|
||||
let depth = 0;
|
||||
while (pathArr.length > 0 && curr && depth < maxDepth) {
|
||||
(curr.parent as any) = {
|
||||
path: pathArr.join("/"),
|
||||
id: pathArr.pop(),
|
||||
} as Partial<DocumentReference<DocumentData>>;
|
||||
curr = curr.parent as any;
|
||||
maxDepth++;
|
||||
}
|
||||
return serializedRef;
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
onmessage = async ({ data }) => {
|
||||
try {
|
||||
const { formulaFn, row, ref } = data;
|
||||
const { formulaFn, row, ref } = JSON.parse(data);
|
||||
const AsyncFunction = async function () {}.constructor as any;
|
||||
const [_, fnBody] = formulaFn.match(/=>\s*({?[\s\S]*}?)$/);
|
||||
if (!fnBody) return;
|
||||
@@ -9,7 +9,7 @@ onmessage = async ({ data }) => {
|
||||
"ref",
|
||||
`const fn = async () => \n${fnBody}\n return fn();`
|
||||
);
|
||||
const result = await fn(JSON.parse(row), ref);
|
||||
const result = await fn(row, ref);
|
||||
postMessage({ result });
|
||||
} catch (error: any) {
|
||||
console.error("Error: ", error);
|
||||
|
||||
Reference in New Issue
Block a user