mirror of
https://github.com/rowyio/rowy.git
synced 2026-09-01 19:50:46 +02:00
@@ -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() {
|
||||
<RequireAuth>
|
||||
<Navigation>
|
||||
<TableSettingsDialog />
|
||||
<ProjectSettingsDialog />
|
||||
</Navigation>
|
||||
</RequireAuth>
|
||||
}
|
||||
|
||||
@@ -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<ProjectSettingsDialogState>) => {
|
||||
set(projectSettingsDialogAtom, {
|
||||
open: true,
|
||||
tab: "secrets",
|
||||
...update,
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Store the current ID of the table being edited in tableSettingsDialog
|
||||
* to derive tableSettingsDialogSchemaAtom
|
||||
|
||||
@@ -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);
|
||||
|
||||
11
src/components/CodeEditor/extensions.d.ts
vendored
11
src/components/CodeEditor/extensions.d.ts
vendored
@@ -128,4 +128,15 @@ type PushNotificationRequest = {
|
||||
type PushNotificationBody = (
|
||||
context: ExtensionContext
|
||||
) => Message | Message[] | Promise<Message | Message[]>;
|
||||
|
||||
type TaskBody = (context: ExtensionContext) => Promise<any>;
|
||||
|
||||
type BuildshipAuthenticatedTriggerBody = (
|
||||
context: ExtensionContext
|
||||
) => Promise<{
|
||||
buildshipConfig: {
|
||||
projectId: string;
|
||||
workflowId: string;
|
||||
};
|
||||
body: string;
|
||||
}>;
|
||||
|
||||
282
src/components/ProjectSettingsDialog/ProjectSettingsDialog.tsx
Normal file
282
src/components/ProjectSettingsDialog/ProjectSettingsDialog.tsx
Normal file
@@ -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 (
|
||||
<>
|
||||
<Modal
|
||||
onClose={handleClose}
|
||||
open={open}
|
||||
maxWidth="sm"
|
||||
fullWidth
|
||||
title={"Project settings"}
|
||||
sx={{
|
||||
".MuiDialogContent-root": {
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
height: "100%",
|
||||
},
|
||||
}}
|
||||
children={
|
||||
<>
|
||||
<TabContext value={tab}>
|
||||
<Box
|
||||
sx={{
|
||||
borderBottom: 1,
|
||||
borderColor: "divider",
|
||||
}}
|
||||
>
|
||||
<TabList value={tab} onChange={handleTabChange}>
|
||||
<Tab label="Secret keys" value={"secrets"} />
|
||||
</TabList>
|
||||
</Box>
|
||||
<TabPanel
|
||||
value={tab}
|
||||
sx={{
|
||||
overflowY: "scroll",
|
||||
}}
|
||||
>
|
||||
<Paper elevation={1} variant={"outlined"}>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
padding: 3,
|
||||
}}
|
||||
>
|
||||
<Typography variant="h6" sx={{ fontWeight: "bold" }}>
|
||||
Secrets
|
||||
</Typography>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={() => {
|
||||
setSecretDetailsModal({
|
||||
open: true,
|
||||
mode: "add",
|
||||
});
|
||||
}}
|
||||
>
|
||||
Add secret key
|
||||
</Button>
|
||||
</Box>
|
||||
{secretNames.secretNames?.map((secretName) => (
|
||||
<Box
|
||||
key={secretName}
|
||||
sx={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
padding: 3,
|
||||
borderTop: 1,
|
||||
borderColor: "divider",
|
||||
}}
|
||||
>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{secretName}
|
||||
</Typography>
|
||||
<Box>
|
||||
<Tooltip title={"Edit"}>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
style={{
|
||||
minWidth: "40px",
|
||||
paddingLeft: 0,
|
||||
paddingRight: 0,
|
||||
marginRight: "8px",
|
||||
}}
|
||||
onClick={() => {
|
||||
setSecretDetailsModal({
|
||||
open: true,
|
||||
mode: "edit",
|
||||
secretName,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<EditIcon color={"secondary"} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Tooltip title={"Delete"}>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
style={{
|
||||
minWidth: "40px",
|
||||
paddingLeft: 0,
|
||||
paddingRight: 0,
|
||||
}}
|
||||
onClick={() => {
|
||||
console.log("setting", {
|
||||
open: true,
|
||||
mode: "delete",
|
||||
secretName,
|
||||
});
|
||||
setSecretDetailsModal({
|
||||
open: true,
|
||||
mode: "delete",
|
||||
secretName,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<DeleteOutlineIcon color={"secondary"} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
</Box>
|
||||
))}
|
||||
</Paper>
|
||||
</TabPanel>
|
||||
</TabContext>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<SecretDetailsModal
|
||||
open={secretDetailsModal.open}
|
||||
mode={secretDetailsModal.mode}
|
||||
error={secretDetailsModal.error}
|
||||
loading={secretDetailsModal.loading}
|
||||
secretName={secretDetailsModal.secretName}
|
||||
handleClose={() => {
|
||||
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,
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
157
src/components/ProjectSettingsDialog/SecretDetailsModal.tsx
Normal file
157
src/components/ProjectSettingsDialog/SecretDetailsModal.tsx
Normal file
@@ -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 (
|
||||
<Modal
|
||||
onClose={handleClose}
|
||||
open={open}
|
||||
maxWidth="xs"
|
||||
fullWidth
|
||||
title={`${capitalize(mode)} secret key`}
|
||||
sx={{
|
||||
".MuiDialogContent-root": {
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
height: "100%",
|
||||
},
|
||||
}}
|
||||
children={
|
||||
<Box
|
||||
sx={{
|
||||
marginTop: 1,
|
||||
}}
|
||||
>
|
||||
{mode === "add" && (
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "flex-start",
|
||||
gap: 1,
|
||||
}}
|
||||
>
|
||||
<Typography variant="subtitle2">Secret Name</Typography>
|
||||
<TextField
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={newSecretName}
|
||||
onChange={(e) => setNewSecretName(e.target.value)}
|
||||
/>
|
||||
<Typography
|
||||
variant={"body2"}
|
||||
color={"text.secondary"}
|
||||
fontSize={"12px"}
|
||||
>
|
||||
This will create a secret key on Google Cloud.
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
{mode === "delete" ? (
|
||||
<Typography>
|
||||
Are you sure you want to delete this secret key {secretName}?
|
||||
</Typography>
|
||||
) : (
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "flex-start",
|
||||
gap: 1,
|
||||
marginTop: 2,
|
||||
}}
|
||||
>
|
||||
<Typography variant="subtitle2">Secret Value</Typography>
|
||||
<TextField
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={secretValue}
|
||||
onChange={(e) => setSecretValue(e.target.value)}
|
||||
/>
|
||||
<Typography
|
||||
variant={"body2"}
|
||||
color={"text.secondary"}
|
||||
fontSize={"12px"}
|
||||
>
|
||||
Paste your secret key here.
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
{error?.length && (
|
||||
<Typography color={"error"} marginTop={2}>
|
||||
{error}
|
||||
</Typography>
|
||||
)}
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
justifyContent: "flex-start",
|
||||
gap: 1,
|
||||
marginTop: 4,
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={handleClose}
|
||||
sx={{ textTransform: "none" }}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<LoadingButton
|
||||
variant="contained"
|
||||
color={"primary"}
|
||||
loading={loading}
|
||||
disabled={
|
||||
(mode === "add" && (!newSecretName || !secretValue)) ||
|
||||
(mode === "edit" && !secretValue)
|
||||
}
|
||||
onClick={() => {
|
||||
switch (mode) {
|
||||
case "add":
|
||||
handleAdd(newSecretName, secretValue);
|
||||
break;
|
||||
case "edit":
|
||||
handleEdit(secretName ?? "", secretValue);
|
||||
break;
|
||||
case "delete":
|
||||
handleDelete(secretName ?? "");
|
||||
break;
|
||||
}
|
||||
}}
|
||||
>
|
||||
{mode === "delete" ? "Delete" : "Save"}
|
||||
</LoadingButton>
|
||||
</Box>
|
||||
</Box>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
2
src/components/ProjectSettingsDialog/index.ts
Normal file
2
src/components/ProjectSettingsDialog/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from "./ProjectSettingsDialog";
|
||||
export { default } from "./ProjectSettingsDialog";
|
||||
103
src/components/Settings/UserSettings/TableSettings.tsx
Normal file
103
src/components/Settings/UserSettings/TableSettings.tsx
Normal file
@@ -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 (
|
||||
<>
|
||||
<FormControl sx={{ my: -10 / 10, display: "flex" }}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={Boolean(
|
||||
settings.defaultTableSettings?.saveSortsPopupDisabled
|
||||
)}
|
||||
onChange={(e) => {
|
||||
updateSettings({
|
||||
defaultTableSettings: merge(settings.defaultTableSettings, {
|
||||
saveSortsPopupDisabled: e.target.checked,
|
||||
}),
|
||||
});
|
||||
}}
|
||||
/>
|
||||
}
|
||||
label="Disable popup - to save sorting changes to the team"
|
||||
style={{ marginLeft: -11, marginBottom: 13 }}
|
||||
/>
|
||||
<Collapse in={settings.defaultTableSettings?.saveSortsPopupDisabled}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={Boolean(
|
||||
settings.defaultTableSettings?.automaticallyApplySorts
|
||||
)}
|
||||
onChange={(e) => {
|
||||
updateSettings({
|
||||
defaultTableSettings: merge(settings.defaultTableSettings, {
|
||||
automaticallyApplySorts: e.target.checked,
|
||||
}),
|
||||
});
|
||||
}}
|
||||
/>
|
||||
}
|
||||
label="Automatically apply sorting changes to all users"
|
||||
style={{ marginLeft: 20, marginBottom: 10, marginTop: -13 }}
|
||||
/>
|
||||
</Collapse>
|
||||
|
||||
<Divider />
|
||||
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={Boolean(
|
||||
settings.defaultTableSettings?.saveColumnSizingPopupDisabled
|
||||
)}
|
||||
onChange={(e) => {
|
||||
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 }}
|
||||
/>
|
||||
<Collapse
|
||||
in={settings.defaultTableSettings?.saveColumnSizingPopupDisabled}
|
||||
>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={Boolean(
|
||||
settings.defaultTableSettings?.automaticallyApplyColumnSizing
|
||||
)}
|
||||
onChange={(e) => {
|
||||
updateSettings({
|
||||
defaultTableSettings: merge(settings.defaultTableSettings, {
|
||||
automaticallyApplyColumnSizing: e.target.checked,
|
||||
}),
|
||||
});
|
||||
}}
|
||||
/>
|
||||
}
|
||||
label="Automatically apply column width changes to all users"
|
||||
style={{ marginLeft: 20 }}
|
||||
/>
|
||||
</Collapse>
|
||||
</FormControl>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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<SnackbarKey | null>(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,
|
||||
]
|
||||
);
|
||||
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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_);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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`,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
IconButton,
|
||||
Stack,
|
||||
TextField,
|
||||
Tooltip,
|
||||
Typography,
|
||||
useTheme,
|
||||
} from "@mui/material";
|
||||
@@ -98,15 +99,17 @@ export default function Details() {
|
||||
Description
|
||||
</Typography>
|
||||
{isAdmin && (
|
||||
<IconButton
|
||||
aria-label="Edit description"
|
||||
onClick={() => {
|
||||
setEditDescription(!editDescription);
|
||||
}}
|
||||
sx={{ top: 4 }}
|
||||
>
|
||||
{editDescription ? <EditOffIcon /> : <EditIcon />}
|
||||
</IconButton>
|
||||
<Tooltip title="Edit">
|
||||
<IconButton
|
||||
aria-label="Edit description"
|
||||
onClick={() => {
|
||||
setEditDescription(!editDescription);
|
||||
}}
|
||||
sx={{ top: 4 }}
|
||||
>
|
||||
{editDescription ? <EditOffIcon /> : <EditIcon />}
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Stack>
|
||||
{editDescription ? (
|
||||
@@ -145,15 +148,17 @@ export default function Details() {
|
||||
Details
|
||||
</Typography>
|
||||
{isAdmin && (
|
||||
<IconButton
|
||||
aria-label="Edit details"
|
||||
onClick={() => {
|
||||
setEditDetails(!editDetails);
|
||||
}}
|
||||
sx={{ top: 4 }}
|
||||
>
|
||||
{editDetails ? <EditOffIcon /> : <EditIcon />}
|
||||
</IconButton>
|
||||
<Tooltip title="Edit">
|
||||
<IconButton
|
||||
aria-label="Edit details"
|
||||
onClick={() => {
|
||||
setEditDetails(!editDetails);
|
||||
}}
|
||||
sx={{ top: 4 }}
|
||||
>
|
||||
{editDetails ? <EditOffIcon /> : <EditIcon />}
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Stack>
|
||||
<Box
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
IconButton,
|
||||
Stack,
|
||||
styled,
|
||||
Tooltip,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import CloseIcon from "@mui/icons-material/Close";
|
||||
@@ -111,12 +112,14 @@ export default function SideDrawer() {
|
||||
Information
|
||||
</Typography>
|
||||
</Stack>
|
||||
<IconButton
|
||||
onClick={() => setSideDrawer(RESET)}
|
||||
aria-label="Close"
|
||||
>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
<Tooltip title="Close">
|
||||
<IconButton
|
||||
onClick={() => setSideDrawer(RESET)}
|
||||
aria-label="Close"
|
||||
>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Stack>
|
||||
<Box
|
||||
sx={{
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export const extensionTypes = [
|
||||
"buildshipAuthenticatedTrigger",
|
||||
"task",
|
||||
"docSync",
|
||||
"historySnapshot",
|
||||
@@ -15,6 +16,7 @@ export const extensionTypes = [
|
||||
export type ExtensionType = typeof extensionTypes[number];
|
||||
|
||||
export const extensionNames: Record<ExtensionType, string> = {
|
||||
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")
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -257,8 +257,16 @@ export default function Step1Columns({
|
||||
const isNewColumn = !!find(config.newColumns, { key: columnKey });
|
||||
|
||||
return (
|
||||
<Grid container key={field} component="li" wrap="nowrap">
|
||||
<Grid item xs>
|
||||
<Grid
|
||||
container
|
||||
key={field}
|
||||
component="li"
|
||||
wrap="nowrap"
|
||||
sx={{
|
||||
marginTop: "36px !important",
|
||||
}}
|
||||
>
|
||||
<Grid container item xs alignItems={"center"}>
|
||||
<FormControlLabel
|
||||
key={field}
|
||||
control={
|
||||
@@ -291,88 +299,145 @@ export default function Step1Columns({
|
||||
<ArrowIcon color="disabled" sx={{ color: "secondary.main" }} />
|
||||
</Grid>
|
||||
|
||||
<Grid item xs>
|
||||
<Grid item container spacing={4} xs alignItems={"center"}>
|
||||
{selected && (
|
||||
<ColumnSelect
|
||||
multiple={false}
|
||||
value={columnKey}
|
||||
onChange={handleChange(field) as any}
|
||||
TextFieldProps={{
|
||||
hiddenLabel: true,
|
||||
SelectProps: {
|
||||
renderValue: () => {
|
||||
if (!columnKey) return "Select or add column";
|
||||
else
|
||||
return (
|
||||
<Stack
|
||||
direction="row"
|
||||
gap={1}
|
||||
alignItems="center"
|
||||
>
|
||||
<Box sx={{ width: 24, height: 24 }}>
|
||||
{!isNewColumn ? (
|
||||
getFieldProp("icon", matchingColumn?.type)
|
||||
) : (
|
||||
<TableColumnIcon color="disabled" />
|
||||
)}
|
||||
</Box>
|
||||
{matchingColumn?.name}
|
||||
{isNewColumn && (
|
||||
<Chip
|
||||
label="New"
|
||||
color="primary"
|
||||
size="small"
|
||||
variant="outlined"
|
||||
style={{
|
||||
marginLeft: "auto",
|
||||
pointerEvents: "none",
|
||||
height: 24,
|
||||
fontWeight: "normal",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
},
|
||||
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",
|
||||
<>
|
||||
<Grid item xs>
|
||||
<ColumnSelect
|
||||
multiple={false}
|
||||
value={columnKey}
|
||||
onChange={handleChange(field) as any}
|
||||
TextFieldProps={{
|
||||
hiddenLabel: true,
|
||||
SelectProps: {
|
||||
renderValue: () => {
|
||||
if (!columnKey) return "Select or add column";
|
||||
else
|
||||
return (
|
||||
<Stack
|
||||
direction="row"
|
||||
gap={1}
|
||||
alignItems="center"
|
||||
>
|
||||
<Box sx={{ width: 24, height: 24 }}>
|
||||
{!isNewColumn ? (
|
||||
getFieldProp(
|
||||
"icon",
|
||||
matchingColumn?.type
|
||||
)
|
||||
) : (
|
||||
<TableColumnIcon color="disabled" />
|
||||
)}
|
||||
</Box>
|
||||
{matchingColumn?.name}
|
||||
{isNewColumn && (
|
||||
<Chip
|
||||
label="New"
|
||||
color="primary"
|
||||
size="small"
|
||||
variant="outlined"
|
||||
style={{
|
||||
marginLeft: "auto",
|
||||
pointerEvents: "none",
|
||||
height: 24,
|
||||
fontWeight: "normal",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
},
|
||||
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",
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<TextField
|
||||
label="Field key"
|
||||
value={
|
||||
config.pairs.find(
|
||||
(pair) => 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,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
</>
|
||||
)}
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
@@ -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 = {
|
||||
})}
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
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
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
<IconButton
|
||||
aria-label="Actions…"
|
||||
id="table-settings-actions-button"
|
||||
aria-controls="table-settings-actions-menu"
|
||||
aria-haspopup="true"
|
||||
aria-expanded={open ? "true" : undefined}
|
||||
onClick={handleOpen}
|
||||
>
|
||||
{mode === "create" ? <ImportIcon /> : <ExportIcon />}
|
||||
</IconButton>
|
||||
<Tooltip title="Actions menu">
|
||||
<IconButton
|
||||
aria-label="Actions…"
|
||||
id="table-settings-actions-button"
|
||||
aria-controls="table-settings-actions-menu"
|
||||
aria-haspopup="true"
|
||||
aria-expanded={open ? "true" : undefined}
|
||||
onClick={handleOpen}
|
||||
>
|
||||
{mode === "create" ? <ImportIcon /> : <ExportIcon />}
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
|
||||
<Menu
|
||||
id="table-settings-actions-menu"
|
||||
|
||||
@@ -3,7 +3,13 @@ import { useAtom, useSetAtom } from "jotai";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useSnackbar } from "notistack";
|
||||
|
||||
import { IconButton, Menu, MenuItem, DialogContentText } from "@mui/material";
|
||||
import {
|
||||
IconButton,
|
||||
Menu,
|
||||
MenuItem,
|
||||
DialogContentText,
|
||||
Tooltip,
|
||||
} from "@mui/material";
|
||||
import DeleteIcon from "@mui/icons-material/DeleteOutlined";
|
||||
|
||||
import {
|
||||
@@ -55,16 +61,18 @@ export default function DeleteMenu({ clearDialog, data }: IDeleteMenuProps) {
|
||||
|
||||
return (
|
||||
<>
|
||||
<IconButton
|
||||
aria-label="Delete table…"
|
||||
id="table-settings-delete-button"
|
||||
aria-controls="table-settings-delete-menu"
|
||||
aria-haspopup="true"
|
||||
aria-expanded={open ? "true" : undefined}
|
||||
onClick={(e) => setAnchorEl(e.currentTarget)}
|
||||
>
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
<Tooltip title="Delete menu">
|
||||
<IconButton
|
||||
aria-label="Delete table…"
|
||||
id="table-settings-delete-button"
|
||||
aria-controls="table-settings-delete-menu"
|
||||
aria-haspopup="true"
|
||||
aria-expanded={open ? "true" : undefined}
|
||||
onClick={(e) => setAnchorEl(e.currentTarget)}
|
||||
>
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
|
||||
<Menu
|
||||
id="table-settings-delete-menu"
|
||||
|
||||
@@ -27,7 +27,7 @@ export default function TableName({ watchedField, ...props }: ITableNameProps) {
|
||||
onChange(startCase(watchedValue));
|
||||
} else if (typeof value === "string") {
|
||||
// otherwise if table name is valid, set watched value to table name
|
||||
onChange(value.trim());
|
||||
onChange(startCase(value.trim()));
|
||||
}
|
||||
}
|
||||
}, [watchedValue, disabled, onChange, value]);
|
||||
|
||||
@@ -411,7 +411,7 @@ export default function TableSettingsDialog() {
|
||||
},
|
||||
/*
|
||||
* TODO: Figure out where to store this settings
|
||||
|
||||
|
||||
{
|
||||
id: "function",
|
||||
title: "Cloud Function",
|
||||
|
||||
@@ -213,7 +213,7 @@ export const tableSettings = (
|
||||
name: "name",
|
||||
label: "Table name",
|
||||
required: true,
|
||||
watchedField: "collection",
|
||||
watchedField: "name",
|
||||
assistiveText: "User-facing name for this table",
|
||||
autoFocus: true,
|
||||
gridCols: { xs: 12, sm: 6 },
|
||||
|
||||
@@ -37,6 +37,9 @@ export const runRoutes = {
|
||||
setFirestoreRules: { path: "/setFirestoreRules", method: "POST" } as RunRoute,
|
||||
listCollections: { path: "/listCollections", method: "GET" } as RunRoute,
|
||||
listSecrets: { path: "/listSecrets", method: "GET" } as RunRoute,
|
||||
addSecret: { path: "/addSecret", method: "POST" } as RunRoute,
|
||||
editSecret: { path: "/editSecret", method: "POST" } as RunRoute,
|
||||
deleteSecret: { path: "/deleteSecret", method: "POST" } as RunRoute,
|
||||
serviceAccountAccess: {
|
||||
path: "/serviceAccountAccess",
|
||||
method: "GET",
|
||||
|
||||
@@ -9,6 +9,7 @@ import SettingsSkeleton from "@src/components/Settings/SettingsSkeleton";
|
||||
import SettingsSection from "@src/components/Settings/SettingsSection";
|
||||
import Account from "@src/components/Settings/UserSettings/Account";
|
||||
import Theme from "@src/components/Settings/UserSettings/Theme";
|
||||
import TableSettings from "@src/components/Settings/UserSettings/TableSettings";
|
||||
import Personalization from "@src/components/Settings/UserSettings/Personalization";
|
||||
|
||||
import {
|
||||
@@ -57,6 +58,7 @@ export default function UserSettingsPage() {
|
||||
const sections = [
|
||||
{ title: "Account", Component: Account, props: childProps },
|
||||
{ title: "Theme", Component: Theme, props: childProps },
|
||||
{ title: "Table Settings", Component: TableSettings, props: childProps },
|
||||
{ title: "Personalization", Component: Personalization, props: childProps },
|
||||
];
|
||||
|
||||
|
||||
@@ -137,39 +137,45 @@ export default function TablesPage() {
|
||||
const getActions = (table: TableSettings) => (
|
||||
<>
|
||||
{userRoles.includes("ADMIN") && (
|
||||
<IconButton
|
||||
aria-label="Edit table"
|
||||
onClick={() =>
|
||||
openTableSettingsDialog({ mode: "update", data: table })
|
||||
}
|
||||
size={view === "list" ? "large" : undefined}
|
||||
>
|
||||
<EditIcon />
|
||||
</IconButton>
|
||||
<Tooltip title="Edit Table">
|
||||
<IconButton
|
||||
aria-label="Edit table"
|
||||
onClick={() =>
|
||||
openTableSettingsDialog({ mode: "update", data: table })
|
||||
}
|
||||
size={view === "list" ? "large" : undefined}
|
||||
>
|
||||
<EditIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Checkbox
|
||||
onChange={handleFavorite(table.id)}
|
||||
checked={favorites.includes(table.id)}
|
||||
icon={<FavoriteBorderIcon />}
|
||||
checkedIcon={
|
||||
<Zoom in>
|
||||
<FavoriteIcon />
|
||||
</Zoom>
|
||||
}
|
||||
name={`favorite-${table.id}`}
|
||||
inputProps={{ "aria-label": "Favorite" }}
|
||||
sx={view === "list" ? { p: 1.5 } : undefined}
|
||||
color="secondary"
|
||||
/>
|
||||
<IconButton
|
||||
aria-label="Table information"
|
||||
size={view === "list" ? "large" : undefined}
|
||||
component={Link}
|
||||
to={`${getLink(table)}#sideDrawer="table-information"`}
|
||||
style={{ marginLeft: 0 }}
|
||||
>
|
||||
<InfoIcon />
|
||||
</IconButton>
|
||||
<Tooltip title="Favorite">
|
||||
<Checkbox
|
||||
onChange={handleFavorite(table.id)}
|
||||
checked={favorites.includes(table.id)}
|
||||
icon={<FavoriteBorderIcon />}
|
||||
checkedIcon={
|
||||
<Zoom in>
|
||||
<FavoriteIcon />
|
||||
</Zoom>
|
||||
}
|
||||
name={`favorite-${table.id}`}
|
||||
inputProps={{ "aria-label": "Favorite" }}
|
||||
sx={view === "list" ? { p: 1.5 } : undefined}
|
||||
color="secondary"
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip title="Table information">
|
||||
<IconButton
|
||||
aria-label="Table information"
|
||||
size={view === "list" ? "large" : undefined}
|
||||
component={Link}
|
||||
to={`${getLink(table)}#sideDrawer="table-information"`}
|
||||
style={{ marginLeft: 0 }}
|
||||
>
|
||||
<InfoIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</>
|
||||
);
|
||||
|
||||
|
||||
9
src/types/settings.d.ts
vendored
9
src/types/settings.d.ts
vendored
@@ -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<{
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user