diff --git a/src/App.tsx b/src/App.tsx index 653b6a88..9612ff03 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -41,6 +41,12 @@ const SetupPage = lazy(() => import("@src/pages/SetupPage" /* webpackChunkName: const Navigation = lazy(() => import("@src/layouts/Navigation" /* webpackChunkName: "Navigation" */)); // prettier-ignore const TableSettingsDialog = lazy(() => import("@src/components/TableSettingsDialog" /* webpackChunkName: "TableSettingsDialog" */)); +const ProjectSettingsDialog = lazy( + () => + import( + "@src/components/ProjectSettingsDialog" /* webpackChunkName: "ProjectSettingsDialog" */ + ) +); // prettier-ignore const TablesPage = lazy(() => import("@src/pages/TablesPage" /* webpackChunkName: "TablesPage" */)); @@ -99,6 +105,7 @@ export default function App() { + } diff --git a/src/atoms/projectScope/ui.ts b/src/atoms/projectScope/ui.ts index 48af64b0..01fc1499 100644 --- a/src/atoms/projectScope/ui.ts +++ b/src/atoms/projectScope/ui.ts @@ -131,6 +131,26 @@ export const tableSettingsDialogAtom = atom( } ); +export type ProjectSettingsDialogTab = + | "general" + | "rowy-run" + | "services" + | "secrets"; +export type ProjectSettingsDialogState = { + open: boolean; + tab: ProjectSettingsDialogTab; +}; +export const projectSettingsDialogAtom = atom( + { open: false, tab: "secrets" } as ProjectSettingsDialogState, + (_, set, update?: Partial) => { + set(projectSettingsDialogAtom, { + open: true, + tab: "secrets", + ...update, + }); + } +); + /** * Store the current ID of the table being edited in tableSettingsDialog * to derive tableSettingsDialogSchemaAtom diff --git a/src/atoms/projectScope/user.ts b/src/atoms/projectScope/user.ts index 62bcc175..5c03281e 100644 --- a/src/atoms/projectScope/user.ts +++ b/src/atoms/projectScope/user.ts @@ -30,6 +30,12 @@ export const themeOverriddenAtom = atomWithStorage( false ); +/** User's default table settings (affecting saving and popup behavior) */ +export const defaultTableSettingsAtom = atom((get) => { + const userSettings = get(userSettingsAtom); + return userSettings.defaultTableSettings; +}); + /** Customized base theme based on project and user settings */ export const customizedThemesAtom = atom((get) => { const publicSettings = get(publicSettingsAtom); diff --git a/src/components/CodeEditor/extensions.d.ts b/src/components/CodeEditor/extensions.d.ts index 22af5756..d57c5e21 100644 --- a/src/components/CodeEditor/extensions.d.ts +++ b/src/components/CodeEditor/extensions.d.ts @@ -128,4 +128,15 @@ type PushNotificationRequest = { type PushNotificationBody = ( context: ExtensionContext ) => Message | Message[] | Promise; + type TaskBody = (context: ExtensionContext) => Promise; + +type BuildshipAuthenticatedTriggerBody = ( + context: ExtensionContext +) => Promise<{ + buildshipConfig: { + projectId: string; + workflowId: string; + }; + body: string; +}>; diff --git a/src/components/ProjectSettingsDialog/ProjectSettingsDialog.tsx b/src/components/ProjectSettingsDialog/ProjectSettingsDialog.tsx new file mode 100644 index 00000000..49e9c1c8 --- /dev/null +++ b/src/components/ProjectSettingsDialog/ProjectSettingsDialog.tsx @@ -0,0 +1,282 @@ +import React from "react"; +import { useAtom } from "jotai"; +import { + projectScope, + projectSettingsDialogAtom, + ProjectSettingsDialogTab, + rowyRunAtom, + secretNamesAtom, + updateSecretNamesAtom, +} from "@src/atoms/projectScope"; +import Modal from "@src/components/Modal"; +import { Box, Button, Paper, Tab, Tooltip, Typography } from "@mui/material"; +import { TabContext, TabPanel, TabList } from "@mui/lab"; +import DeleteOutlineIcon from "@mui/icons-material/DeleteOutline"; +import EditIcon from "@mui/icons-material/Edit"; +import SecretDetailsModal from "./SecretDetailsModal"; +import { runRoutes } from "@src/constants/runRoutes"; + +export default function ProjectSettingsDialog() { + const [{ open, tab }, setProjectSettingsDialog] = useAtom( + projectSettingsDialogAtom, + projectScope + ); + const [secretNames] = useAtom(secretNamesAtom, projectScope); + const [secretDetailsModal, setSecretDetailsModal] = React.useState<{ + open: boolean; + loading?: boolean; + mode?: "add" | "edit" | "delete"; + secretName?: string; + error?: string; + }>({ + open: false, + }); + const [rowyRun] = useAtom(rowyRunAtom, projectScope); + const [updateSecretNames] = useAtom(updateSecretNamesAtom, projectScope); + + if (!open) return null; + + const handleClose = () => { + setProjectSettingsDialog({ open: false }); + }; + + const handleTabChange = ( + event: React.SyntheticEvent, + newTab: ProjectSettingsDialogTab + ) => { + setProjectSettingsDialog({ tab: newTab }); + }; + + console.log(secretDetailsModal); + + return ( + <> + + + + + + + + + + + + Secrets + + + + {secretNames.secretNames?.map((secretName) => ( + + + {secretName} + + + + + + + + + + + ))} + + + + + } + /> + { + setSecretDetailsModal({ ...secretDetailsModal, open: false }); + }} + handleAdd={async (newSecretName, secretValue) => { + setSecretDetailsModal({ + ...secretDetailsModal, + loading: true, + }); + try { + await rowyRun({ + route: runRoutes.addSecret, + body: { + name: newSecretName, + value: secretValue, + }, + }); + setSecretDetailsModal({ + ...secretDetailsModal, + open: false, + loading: false, + }); + // update secret name causes an unknown modal-related bug, to be fixed + // updateSecretNames?.(); + } catch (error: any) { + console.error(error); + setSecretDetailsModal({ + ...secretDetailsModal, + error: error.message, + }); + } + }} + handleEdit={async (secretName, secretValue) => { + setSecretDetailsModal({ + ...secretDetailsModal, + loading: true, + }); + try { + await rowyRun({ + route: runRoutes.editSecret, + body: { + name: secretName, + value: secretValue, + }, + }); + setSecretDetailsModal({ + ...secretDetailsModal, + open: false, + loading: false, + }); + // update secret name causes an unknown modal-related bug, to be fixed + // updateSecretNames?.(); + } catch (error: any) { + console.error(error); + setSecretDetailsModal({ + ...secretDetailsModal, + error: error.message, + }); + } + }} + handleDelete={async (secretName) => { + setSecretDetailsModal({ + ...secretDetailsModal, + loading: true, + }); + try { + await rowyRun({ + route: runRoutes.deleteSecret, + body: { + name: secretName, + }, + }); + console.log("Setting", { + ...secretDetailsModal, + open: false, + loading: false, + }); + setSecretDetailsModal({ + ...secretDetailsModal, + open: false, + loading: false, + }); + // update secret name causes an unknown modal-related bug, to be fixed + // updateSecretNames?.(); + } catch (error: any) { + console.error(error); + setSecretDetailsModal({ + ...secretDetailsModal, + error: error.message, + }); + } + }} + /> + + ); +} diff --git a/src/components/ProjectSettingsDialog/SecretDetailsModal.tsx b/src/components/ProjectSettingsDialog/SecretDetailsModal.tsx new file mode 100644 index 00000000..1e156429 --- /dev/null +++ b/src/components/ProjectSettingsDialog/SecretDetailsModal.tsx @@ -0,0 +1,157 @@ +import React, { useState } from "react"; +import Modal from "@src/components/Modal"; +import { Box, Button, TextField, Typography } from "@mui/material"; +import { capitalize } from "lodash-es"; +import LoadingButton from "@mui/lab/LoadingButton"; + +export interface ISecretDetailsModalProps { + open: boolean; + loading?: boolean; + mode?: "add" | "edit" | "delete"; + error?: string; + secretName?: string; + handleClose: () => void; + handleAdd: (secretName: string, secretValue: string) => void; + handleEdit: (secretName: string, secretValue: string) => void; + handleDelete: (secretName: string) => void; +} + +export default function SecretDetailsModal({ + open, + loading, + mode, + error, + secretName, + handleClose, + handleAdd, + handleEdit, + handleDelete, +}: ISecretDetailsModalProps) { + const [newSecretName, setNewSecretName] = useState(""); + const [secretValue, setSecretValue] = useState(""); + + return ( + + {mode === "add" && ( + + Secret Name + setNewSecretName(e.target.value)} + /> + + This will create a secret key on Google Cloud. + + + )} + {mode === "delete" ? ( + + Are you sure you want to delete this secret key {secretName}? + + ) : ( + + Secret Value + setSecretValue(e.target.value)} + /> + + Paste your secret key here. + + + )} + {error?.length && ( + + {error} + + )} + + + { + switch (mode) { + case "add": + handleAdd(newSecretName, secretValue); + break; + case "edit": + handleEdit(secretName ?? "", secretValue); + break; + case "delete": + handleDelete(secretName ?? ""); + break; + } + }} + > + {mode === "delete" ? "Delete" : "Save"} + + + + } + /> + ); +} diff --git a/src/components/ProjectSettingsDialog/index.ts b/src/components/ProjectSettingsDialog/index.ts new file mode 100644 index 00000000..e57386dd --- /dev/null +++ b/src/components/ProjectSettingsDialog/index.ts @@ -0,0 +1,2 @@ +export * from "./ProjectSettingsDialog"; +export { default } from "./ProjectSettingsDialog"; diff --git a/src/components/Settings/UserSettings/TableSettings.tsx b/src/components/Settings/UserSettings/TableSettings.tsx new file mode 100644 index 00000000..6c4bfcb9 --- /dev/null +++ b/src/components/Settings/UserSettings/TableSettings.tsx @@ -0,0 +1,103 @@ +import { merge } from "lodash-es"; +import { IUserSettingsChildProps } from "@src/pages/Settings/UserSettingsPage"; + +import { + FormControl, + FormControlLabel, + Divider, + Checkbox, + Collapse, +} from "@mui/material"; + +export default function TableSettings({ + settings, + updateSettings, +}: IUserSettingsChildProps) { + return ( + <> + + { + updateSettings({ + defaultTableSettings: merge(settings.defaultTableSettings, { + saveSortsPopupDisabled: e.target.checked, + }), + }); + }} + /> + } + label="Disable popup - to save sorting changes to the team" + style={{ marginLeft: -11, marginBottom: 13 }} + /> + + { + updateSettings({ + defaultTableSettings: merge(settings.defaultTableSettings, { + automaticallyApplySorts: e.target.checked, + }), + }); + }} + /> + } + label="Automatically apply sorting changes to all users" + style={{ marginLeft: 20, marginBottom: 10, marginTop: -13 }} + /> + + + + + { + updateSettings({ + defaultTableSettings: merge(settings.defaultTableSettings, { + saveColumnSizingPopupDisabled: e.target.checked, + }), + }); + }} + /> + } + label="Disable popup - to save column width changes to the team" + style={{ marginLeft: -11, marginTop: 13 }} + /> + + { + updateSettings({ + defaultTableSettings: merge(settings.defaultTableSettings, { + automaticallyApplyColumnSizing: e.target.checked, + }), + }); + }} + /> + } + label="Automatically apply column width changes to all users" + style={{ marginLeft: 20 }} + /> + + + + ); +} diff --git a/src/components/Table/ColumnHeader/useSaveTableSorts.tsx b/src/components/Table/ColumnHeader/useSaveTableSorts.tsx index c0c239ed..76efdbef 100644 --- a/src/components/Table/ColumnHeader/useSaveTableSorts.tsx +++ b/src/components/Table/ColumnHeader/useSaveTableSorts.tsx @@ -1,5 +1,5 @@ import { useCallback, useState } from "react"; -import { useAtom } from "jotai"; +import { useAtom, useAtomValue } from "jotai"; import { SnackbarKey, useSnackbar } from "notistack"; import LoadingButton from "@mui/lab/LoadingButton"; @@ -11,17 +11,25 @@ import { tableScope, updateTableSchemaAtom, } from "@src/atoms/tableScope"; -import { projectScope, updateUserSettingsAtom } from "@src/atoms/projectScope"; +import { + defaultTableSettingsAtom, + projectScope, + updateUserSettingsAtom, +} from "@src/atoms/projectScope"; import { TableSort } from "@src/types/table"; function useSaveTableSorts(canEditColumns: boolean) { const [updateTableSchema] = useAtom(updateTableSchemaAtom, tableScope); const [updateUserSettings] = useAtom(updateUserSettingsAtom, projectScope); const [tableId] = useAtom(tableIdAtom, tableScope); + const defaultTableSettings = useAtomValue( + defaultTableSettingsAtom, + projectScope + ); const { enqueueSnackbar, closeSnackbar } = useSnackbar(); const [snackbarId, setSnackbarId] = useState(null); - // Offer to save when table sorts changes + // Offer to save when table sorts changes, depending on user settings const trigger = useCallback( (sorts: TableSort[]) => { if (!updateTableSchema) throw new Error("Cannot update table schema"); @@ -33,6 +41,15 @@ function useSaveTableSorts(canEditColumns: boolean) { }); } if (!canEditColumns) return; + // If the user has disabled the popup, return early + if (defaultTableSettings?.saveSortsPopupDisabled) { + // If the user has `automaticallyApplySorts` set to true, apply the sorting before returning + if (defaultTableSettings?.automaticallyApplySorts) { + const updateTable = async () => await updateTableSchema({ sorts }); + updateTable(); + } + return; + } if (snackbarId) { closeSnackbar(snackbarId); } @@ -43,7 +60,7 @@ function useSaveTableSorts(canEditColumns: boolean) { updateTable={async () => await updateTableSchema({ sorts })} /> ), - anchorOrigin: { horizontal: "center", vertical: "top" }, + anchorOrigin: { horizontal: "left", vertical: "bottom" }, }) ); @@ -57,6 +74,7 @@ function useSaveTableSorts(canEditColumns: boolean) { tableId, closeSnackbar, updateTableSchema, + defaultTableSettings, ] ); diff --git a/src/components/Table/Table.tsx b/src/components/Table/Table.tsx index a4b85957..ba17786b 100644 --- a/src/components/Table/Table.tsx +++ b/src/components/Table/Table.tsx @@ -285,7 +285,7 @@ export default function Table({ const { handler: hotKeysHandler } = useHotKeys([ ["mod+C", handleCopy], ["mod+X", handleCut], - ["mod+V", handlePaste], + ["mod+V", (e) => handlePaste], // So the event isn't passed to the handler ]); // Handle prompt to save local column sizes if user `canEditColumns` @@ -324,6 +324,14 @@ export default function Table({ fetchMoreOnBottomReached(containerRef.current); }, [fetchMoreOnBottomReached, tableNextPage.loading, containerRef]); + useEffect(() => { + document.addEventListener("paste", handlePaste); + + return () => { + document.removeEventListener("paste", handlePaste); + }; + }, [handlePaste]); + // apply user default sort on first render const [applySort, setApplySort] = useState(true); useEffect(() => { diff --git a/src/components/Table/useHotKey.tsx b/src/components/Table/useHotKey.tsx index cf74808e..e57d9c84 100644 --- a/src/components/Table/useHotKey.tsx +++ b/src/components/Table/useHotKey.tsx @@ -12,7 +12,6 @@ export default function useHotKeys(actions: HotKeysAction[]) { const event_ = "nativeEvent" in event ? event.nativeEvent : event; actions.forEach(([hotkey, handler_]) => { if (getHotkeyMatcher(hotkey)(event_)) { - event.preventDefault(); handler_(event_); } }); diff --git a/src/components/Table/useMenuAction.tsx b/src/components/Table/useMenuAction.tsx index dbe55907..3a1d4599 100644 --- a/src/components/Table/useMenuAction.tsx +++ b/src/components/Table/useMenuAction.tsx @@ -161,71 +161,96 @@ export function useMenuAction( handleClose, ]); - const handlePaste = useCallback(async () => { - try { - if (!selectedCell || !selectedCol) return; - let text; + const handlePaste = useCallback( + async (e?: ClipboardEvent) => { try { - text = await navigator.clipboard.readText(); - } catch (e) { - enqueueSnackbar(`Read clipboard permission denied.`, { - variant: "error", - }); - return; - } - const cellDataType = getFieldProp("dataType", getFieldType(selectedCol)); - let parsed; - switch (cellDataType) { - case "number": - parsed = Number(text); - if (isNaN(parsed)) throw new Error(`${text} is not a number`); - break; - case "string": - parsed = text; - break; - case "reference": - try { - parsed = doc(firebaseDb, text); - } catch (e: any) { - enqueueSnackbar(`Invalid reference.`, { variant: "error" }); + if (!selectedCell || !selectedCol) return; + let text: string; + // Firefox doesn't allow for reading clipboard data, hence the workaround + if (navigator.userAgent.includes("Firefox")) { + if (!e || !e.clipboardData) { + enqueueSnackbar( + `If you're on Firefox, please use the hotkey instead (Ctrl + V / Cmd + V).`, + { + variant: "info", + autoHideDuration: 7000, + } + ); + enqueueSnackbar(`Cannot read clipboard data.`, { + variant: "error", + }); + return; } - break; - default: - parsed = JSON.parse(text); - break; - } + text = e.clipboardData.getData("text/plain") || ""; + } else { + try { + text = await navigator.clipboard.readText(); + } catch (e) { + enqueueSnackbar(`Read clipboard permission denied.`, { + variant: "error", + }); + return; + } + } + const cellDataType = getFieldProp( + "dataType", + getFieldType(selectedCol) + ); + let parsed; + switch (cellDataType) { + case "number": + parsed = Number(text); + if (isNaN(parsed)) throw new Error(`${text} is not a number`); + break; + case "string": + parsed = text; + break; + case "reference": + try { + parsed = doc(firebaseDb, text); + } catch (e: any) { + enqueueSnackbar(`Invalid reference.`, { variant: "error" }); + } + break; + default: + parsed = JSON.parse(text); + break; + } - if (selectedCol.type === FieldType.slider) { - if (parsed < selectedCol.config?.min) parsed = selectedCol.config?.min; - else if (parsed > selectedCol.config?.max) - parsed = selectedCol.config?.max; - } + if (selectedCol.type === FieldType.slider) { + if (parsed < selectedCol.config?.min) + parsed = selectedCol.config?.min; + else if (parsed > selectedCol.config?.max) + parsed = selectedCol.config?.max; + } - if (selectedCol.type === FieldType.rating) { - if (parsed < 0) parsed = 0; - if (parsed > (selectedCol.config?.max || 5)) - parsed = selectedCol.config?.max || 5; - } + if (selectedCol.type === FieldType.rating) { + if (parsed < 0) parsed = 0; + if (parsed > (selectedCol.config?.max || 5)) + parsed = selectedCol.config?.max || 5; + } - if (selectedCol.type === FieldType.percentage) { - parsed = parsed / 100; + if (selectedCol.type === FieldType.percentage) { + parsed = parsed / 100; + } + updateField({ + path: selectedCell.path, + fieldName: selectedCol.fieldName, + value: parsed, + arrayTableData: { + index: selectedCell.arrayIndex, + }, + }); + } catch (error) { + enqueueSnackbar( + `${selectedCol?.type} field does not support the data type being pasted`, + { variant: "error" } + ); } - updateField({ - path: selectedCell.path, - fieldName: selectedCol.fieldName, - value: parsed, - arrayTableData: { - index: selectedCell.arrayIndex, - }, - }); - } catch (error) { - enqueueSnackbar( - `${selectedCol?.type} field does not support the data type being pasted`, - { variant: "error" } - ); - } - if (handleClose) handleClose(); - }, [selectedCell, selectedCol, updateField, enqueueSnackbar, handleClose]); + if (handleClose) handleClose(); + }, + [selectedCell, selectedCol, updateField, enqueueSnackbar, handleClose] + ); useEffect(() => { if (!selectedCell) return setCellValue(""); @@ -276,9 +301,9 @@ export function useMenuAction( }; } const fieldType = getFieldType(selectedCol); - return function () { + return function (e?: ClipboardEvent) { if (SUPPORTED_TYPES_PASTE.has(fieldType)) { - return func(); + return func(e); } else { enqueueSnackbar( `${fieldType} field does not support paste functionality`, diff --git a/src/components/Table/useSaveColumnSizing.tsx b/src/components/Table/useSaveColumnSizing.tsx index af1a6e37..b0c1d55c 100644 --- a/src/components/Table/useSaveColumnSizing.tsx +++ b/src/components/Table/useSaveColumnSizing.tsx @@ -1,5 +1,5 @@ import { useEffect, useState } from "react"; -import { useSetAtom } from "jotai"; +import { useAtomValue, useSetAtom } from "jotai"; import { useSnackbar } from "notistack"; import { useDebounce } from "use-debounce"; import { isEqual, isEmpty } from "lodash-es"; @@ -13,6 +13,10 @@ import { updateColumnAtom, IUpdateColumnOptions, } from "@src/atoms/tableScope"; +import { + defaultTableSettingsAtom, + projectScope, +} from "@src/atoms/projectScope"; import { DEBOUNCE_DELAY } from "./Table"; import { ColumnSizingState } from "@tanstack/react-table"; @@ -26,14 +30,31 @@ export function useSaveColumnSizing( ) { const { enqueueSnackbar, closeSnackbar } = useSnackbar(); const updateColumn = useSetAtom(updateColumnAtom, tableScope); + const defaultTableSettings = useAtomValue( + defaultTableSettingsAtom, + projectScope + ); // Debounce for saving to schema const [debouncedColumnSizing] = useDebounce(columnSizing, DEBOUNCE_DELAY, { equalityFn: isEqual, }); - // Offer to save when column sizing changes + // Offer to save when column sizing changes, depending on user settings useEffect(() => { if (!canEditColumns || isEmpty(debouncedColumnSizing)) return; + // If the user has disabled the popup, return early + if (defaultTableSettings?.saveColumnSizingPopupDisabled) { + // If the user has `automaticallyApplyColumnSizing` set to true, apply the column width before returning + if (defaultTableSettings?.automaticallyApplyColumnSizing) { + const updateTable = async () => { + for (const [key, value] of Object.entries(debouncedColumnSizing)) { + await updateColumn({ key, config: { width: value } }); + } + }; + updateTable(); + } + return; + } const snackbarId = enqueueSnackbar("Save column sizes for all users?", { action: ( @@ -42,7 +63,7 @@ export function useSaveColumnSizing( updateColumn={updateColumn} /> ), - anchorOrigin: { horizontal: "center", vertical: "top" }, + anchorOrigin: { horizontal: "left", vertical: "bottom" }, }); return () => closeSnackbar(snackbarId); @@ -52,6 +73,7 @@ export function useSaveColumnSizing( enqueueSnackbar, closeSnackbar, updateColumn, + defaultTableSettings, ]); return null; diff --git a/src/components/TableInformationDrawer/Details.tsx b/src/components/TableInformationDrawer/Details.tsx index 78dd0236..b097198a 100644 --- a/src/components/TableInformationDrawer/Details.tsx +++ b/src/components/TableInformationDrawer/Details.tsx @@ -8,6 +8,7 @@ import { IconButton, Stack, TextField, + Tooltip, Typography, useTheme, } from "@mui/material"; @@ -98,15 +99,17 @@ export default function Details() { Description {isAdmin && ( - { - setEditDescription(!editDescription); - }} - sx={{ top: 4 }} - > - {editDescription ? : } - + + { + setEditDescription(!editDescription); + }} + sx={{ top: 4 }} + > + {editDescription ? : } + + )} {editDescription ? ( @@ -145,15 +148,17 @@ export default function Details() { Details {isAdmin && ( - { - setEditDetails(!editDetails); - }} - sx={{ top: 4 }} - > - {editDetails ? : } - + + { + setEditDetails(!editDetails); + }} + sx={{ top: 4 }} + > + {editDetails ? : } + + )} - setSideDrawer(RESET)} - aria-label="Close" - > - - + + setSideDrawer(RESET)} + aria-label="Close" + > + + + = { + buildshipAuthenticatedTrigger: "BuildShip Authenticated Trigger", task: "Task", docSync: "Doc Sync", historySnapshot: "History Snapshot", @@ -61,6 +63,30 @@ export interface IRuntimeOptions { export const triggerTypes: ExtensionTrigger[] = ["create", "update", "delete"]; const extensionBodyTemplate = { + buildshipAuthenticatedTrigger: `const extensionBody: BuildshipAuthenticatedTriggerBody = async({row, db, change, ref, logging}) => { + logging.log("extensionBody started") + + // Put your endpoint URL and request body below. + // It will trigger your endpoint with the request body. + return ({ + buildshipConfig: { + projectId: "", + workflowId: "" + }, + body: JSON.stringify({ + row, + ref: { + id: ref.id, + path: ref.path + }, + change: { + before: change.before.get(), + after: change.after.get(), + }, + // Add your own payload here + }) + }) +}`, task: `const extensionBody: TaskBody = async({row, db, change, ref, logging}) => { logging.log("extensionBody started") diff --git a/src/components/TableModals/ImportCsvWizard/ImportCsvWizard.tsx b/src/components/TableModals/ImportCsvWizard/ImportCsvWizard.tsx index 4f3f9a9e..f7989d26 100644 --- a/src/components/TableModals/ImportCsvWizard/ImportCsvWizard.tsx +++ b/src/components/TableModals/ImportCsvWizard/ImportCsvWizard.tsx @@ -323,7 +323,9 @@ export default function ImportCsvWizard({ onClose }: ITableModalProps) { disableNext: config.pairs.length === 0 || !validRows || - (config.documentId === "column" && !config.documentIdCsvKey), + (config.documentId === "column" && !config.documentIdCsvKey) || + config.pairs.some((pair) => !pair.columnKey) || + config.newColumns.some((col) => !col.key), }, config.newColumns.length > 0 && { title: "Set column types", diff --git a/src/components/TableModals/ImportCsvWizard/Step1Columns.tsx b/src/components/TableModals/ImportCsvWizard/Step1Columns.tsx index 888b87e5..5baa8616 100644 --- a/src/components/TableModals/ImportCsvWizard/Step1Columns.tsx +++ b/src/components/TableModals/ImportCsvWizard/Step1Columns.tsx @@ -257,8 +257,16 @@ export default function Step1Columns({ const isNewColumn = !!find(config.newColumns, { key: columnKey }); return ( - - + + - + {selected && ( - { - if (!columnKey) return "Select or add column"; - else - return ( - - - {!isNewColumn ? ( - getFieldProp("icon", matchingColumn?.type) - ) : ( - - )} - - {matchingColumn?.name} - {isNewColumn && ( - - )} - - ); - }, - sx: [ - { - backgroundColor: "background.default", - border: (theme) => - `1px solid ${theme.palette.divider}`, - borderRadius: 0, - boxShadow: "none", - "& .MuiSelect-select": { - boxSizing: "border-box", - height: COLUMN_HEADER_HEIGHT - 2, - typography: "caption", - fontWeight: "medium", - lineHeight: "28px", + <> + + { + if (!columnKey) return "Select or add column"; + else + return ( + + + {!isNewColumn ? ( + getFieldProp( + "icon", + matchingColumn?.type + ) + ) : ( + + )} + + {matchingColumn?.name} + {isNewColumn && ( + + )} + + ); }, + sx: [ + { + backgroundColor: "background.default", + border: (theme) => + `1px solid ${theme.palette.divider}`, + borderRadius: 0, + boxShadow: "none", + "& .MuiSelect-select": { + boxSizing: "border-box", + height: COLUMN_HEADER_HEIGHT - 2, + typography: "caption", + fontWeight: "medium", + lineHeight: "28px", + }, - color: "text.secondary", - "&:hover": { - backgroundColor: "background.default", - color: "text.primary", - boxShadow: "none", - }, + color: "text.secondary", + "&:hover": { + backgroundColor: "background.default", + color: "text.primary", + boxShadow: "none", + }, - "&::before": { content: "none" }, - "&::after": { pointerEvents: "none" }, + "&::before": { content: "none" }, + "&::after": { pointerEvents: "none" }, + }, + !columnKey && { color: "text.disabled" }, + ], }, - !columnKey && { color: "text.disabled" }, - ], - }, - sx: { "& .MuiInputLabel-root": { display: "none" } }, - }} - clearable={false} - displayEmpty - freeText - AddButtonProps={{ children: "Create column…" }} - AddDialogProps={{ - title: "Create column", - textFieldLabel: "Column name", - }} - /> + sx: { "& .MuiInputLabel-root": { display: "none" } }, + }} + clearable={false} + displayEmpty + freeText + AddButtonProps={{ children: "Create column…" }} + AddDialogProps={{ + title: "Create column", + textFieldLabel: "Column name", + }} + /> + + + pair.columnKey === columnKey + )?.columnKey ?? + config.newColumns.find( + (pair) => pair.key === columnKey + )?.key + } + onChange={(e) => { + const newKey = e.target.value; + const newPairs = config.pairs.map((pair) => { + if (pair.columnKey === columnKey) { + return { ...pair, columnKey: newKey }; + } else { + return pair; + } + }); + + const newColumns = config.newColumns.map((column) => { + if (column.key === columnKey) { + return { + ...column, + key: newKey, + fieldName: newKey, + }; + } else { + return column; + } + }); + + setConfig((config) => ({ + ...config, + pairs: newPairs, + newColumns, + })); + }} + sx={{ + "& .MuiInputLabel-root": { + position: "absolute", + transform: "translateY(-100%)", + }, + "& .MuiInputBase-root": { + height: 40, + }, + }} + /> + + )} diff --git a/src/components/TableModals/WebhooksModal/Schemas/stripe.tsx b/src/components/TableModals/WebhooksModal/Schemas/stripe.tsx index 21d0deb7..806efd3c 100644 --- a/src/components/TableModals/WebhooksModal/Schemas/stripe.tsx +++ b/src/components/TableModals/WebhooksModal/Schemas/stripe.tsx @@ -7,6 +7,7 @@ import { projectScope, secretNamesAtom, updateSecretNamesAtom, + projectSettingsDialogAtom, } from "@src/atoms/projectScope"; import InputLabel from "@mui/material/InputLabel"; import MenuItem from "@mui/material/MenuItem"; @@ -56,6 +57,10 @@ export const webhookStripe = { Auth: (webhookObject: IWebhook, setWebhookObject: (w: IWebhook) => void) => { const [secretNames] = useAtom(secretNamesAtom, projectScope); const [updateSecretNames] = useAtom(updateSecretNamesAtom, projectScope); + const [{ open, tab }, setProjectSettingsDialog] = useAtom( + projectSettingsDialogAtom, + projectScope + ); return ( <> @@ -118,8 +123,9 @@ export const webhookStripe = { })} { - const secretManagerLink = `https://console.cloud.google.com/security/secret-manager/create`; - window?.open?.(secretManagerLink, "_blank")?.focus(); + setProjectSettingsDialog({ + open: true, + }); }} > Add a key in Secret Manager diff --git a/src/components/TableSettingsDialog/ActionsMenu/ActionsMenu.tsx b/src/components/TableSettingsDialog/ActionsMenu/ActionsMenu.tsx index 3dc853a4..a014727f 100644 --- a/src/components/TableSettingsDialog/ActionsMenu/ActionsMenu.tsx +++ b/src/components/TableSettingsDialog/ActionsMenu/ActionsMenu.tsx @@ -3,7 +3,7 @@ import { Control } from "react-hook-form"; import { useSetAtom } from "jotai"; import type { UseFormReturn, FieldValues } from "react-hook-form"; -import { IconButton, Menu, MenuItem } from "@mui/material"; +import { IconButton, Menu, MenuItem, Tooltip } from "@mui/material"; import { Export as ExportIcon, Import as ImportIcon } from "@src/assets/icons"; import ImportSettings from "./ImportSettings"; @@ -50,16 +50,18 @@ export default function ActionsMenu({ return ( <> - - {mode === "create" ? : } - + + + {mode === "create" ? : } + + - setAnchorEl(e.currentTarget)} - > - - + + setAnchorEl(e.currentTarget)} + > + + + ( <> {userRoles.includes("ADMIN") && ( - - openTableSettingsDialog({ mode: "update", data: table }) - } - size={view === "list" ? "large" : undefined} - > - - + + + openTableSettingsDialog({ mode: "update", data: table }) + } + size={view === "list" ? "large" : undefined} + > + + + )} - } - checkedIcon={ - - - - } - name={`favorite-${table.id}`} - inputProps={{ "aria-label": "Favorite" }} - sx={view === "list" ? { p: 1.5 } : undefined} - color="secondary" - /> - - - + + } + checkedIcon={ + + + + } + name={`favorite-${table.id}`} + inputProps={{ "aria-label": "Favorite" }} + sx={view === "list" ? { p: 1.5 } : undefined} + color="secondary" + /> + + + + + + ); diff --git a/src/types/settings.d.ts b/src/types/settings.d.ts index 334df8b9..0c5321d1 100644 --- a/src/types/settings.d.ts +++ b/src/types/settings.d.ts @@ -50,7 +50,14 @@ export type UserSettings = Partial<{ theme: Record<"base" | "light" | "dark", ThemeOptions>; favoriteTables: string[]; - /** Stores user overrides */ + /** Stores default user settings for all tables */ + defaultTableSettings: Partial<{ + saveSortsPopupDisabled: boolean; + automaticallyApplySorts: boolean; + saveColumnSizingPopupDisabled: boolean; + automaticallyApplyColumnSizing: boolean; + }>; + /** Stores table-specific user overrides */ tables: Record< string, Partial<{ diff --git a/yarn.lock b/yarn.lock index 46894875..66b5f669 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8739,9 +8739,9 @@ tinybench@^2.5.0: integrity sha512-kRwSG8Zx4tjF9ZiyH4bhaebu+EDz1BOx9hOigYHlUW4xxI/wKIUQUqo018UlU4ar6ATPBsaMrdbKZ+tmPdohFA== tinymce@^5, tinymce@^5.5.1: - version "5.10.7" - resolved "https://registry.yarnpkg.com/tinymce/-/tinymce-5.10.7.tgz#d89d446f1962f2a1df6b2b70018ce475ec7ffb80" - integrity sha512-9UUjaO0R7FxcFo0oxnd1lMs7H+D0Eh+dDVo5hKbVe1a+VB0nit97vOqlinj+YwgoBDt6/DSCUoWqAYlLI8BLYA== + version "5.10.8" + resolved "https://registry.yarnpkg.com/tinymce/-/tinymce-5.10.8.tgz#c85758fa3cca2cbb4b14dd037a0b315b6462c50e" + integrity sha512-iyoo3VGMAJhLMDdblAefKvYgBRk9kQi58GTwAmoieqsyggGsKZWlQl/YY6nTILFHUCA1FhYu0HdmM5YYjs17UQ== tinypool@^0.5.0: version "0.5.0"