diff --git a/package-lock.json b/package-lock.json
index 7e1e4d3..8e19aaa 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -32,6 +32,7 @@
"react-hook-form": "^7.52.0",
"react-icons": "^5.2.1",
"react-json-view-lite": "^1.4.0",
+ "react-player": "^2.16.0",
"recharts": "^2.12.7",
"recoil": "^0.7.7",
"sonner": "^1.5.0",
@@ -5856,6 +5857,12 @@
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="
},
+ "node_modules/load-script": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/load-script/-/load-script-1.0.0.tgz",
+ "integrity": "sha512-kPEjMFtZvwL9TaZo0uZ2ml+Ye9HUMmPwbYRJ324qF9tqMejwykJ5ggTyvzmrbBeapCAbk98BSbTeovHEEP1uCA==",
+ "license": "MIT"
+ },
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
@@ -5934,6 +5941,12 @@
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
+ "node_modules/memoize-one": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz",
+ "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==",
+ "license": "MIT"
+ },
"node_modules/merge2": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
@@ -6429,6 +6442,12 @@
"react": "^18.3.1"
}
},
+ "node_modules/react-fast-compare": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz",
+ "integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==",
+ "license": "MIT"
+ },
"node_modules/react-firebase-hooks": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/react-firebase-hooks/-/react-firebase-hooks-5.1.1.tgz",
@@ -6477,6 +6496,22 @@
"react": "^16.13.1 || ^17.0.0 || ^18.0.0"
}
},
+ "node_modules/react-player": {
+ "version": "2.16.0",
+ "resolved": "https://registry.npmjs.org/react-player/-/react-player-2.16.0.tgz",
+ "integrity": "sha512-mAIPHfioD7yxO0GNYVFD1303QFtI3lyyQZLY229UEAp/a10cSW+hPcakg0Keq8uWJxT2OiT/4Gt+Lc9bD6bJmQ==",
+ "license": "MIT",
+ "dependencies": {
+ "deepmerge": "^4.0.0",
+ "load-script": "^1.0.0",
+ "memoize-one": "^5.1.1",
+ "prop-types": "^15.7.2",
+ "react-fast-compare": "^3.0.1"
+ },
+ "peerDependencies": {
+ "react": ">=16.6.0"
+ }
+ },
"node_modules/react-remove-scroll": {
"version": "2.5.10",
"resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.5.10.tgz",
diff --git a/package.json b/package.json
index ff21043..cc6c820 100644
--- a/package.json
+++ b/package.json
@@ -33,6 +33,7 @@
"react-hook-form": "^7.52.0",
"react-icons": "^5.2.1",
"react-json-view-lite": "^1.4.0",
+ "react-player": "^2.16.0",
"recharts": "^2.12.7",
"recoil": "^0.7.7",
"sonner": "^1.5.0",
diff --git a/src/components/editor/fastboard-components/card/FastboardCard.tsx b/src/components/editor/fastboard-components/card/FastboardCard.tsx
new file mode 100644
index 0000000..ccea3b2
--- /dev/null
+++ b/src/components/editor/fastboard-components/card/FastboardCard.tsx
@@ -0,0 +1,210 @@
+import { ComponentId, ComponentType } from "@/types/editor";
+import {
+ CardComponentProperties,
+ CardComponentType,
+ CardProperties,
+ ImageComponentProperties,
+ LinkComponentProperties,
+ SpacerComponentProperties,
+ TextComponentProperties,
+ VideoComponentProperties,
+} from "@/types/editor/card-types";
+import { Spinner } from "@nextui-org/react";
+import scrollbarStyles from "@/styles/scrollbar.module.css";
+import useData from "@/hooks/useData";
+import { toast } from "sonner";
+import { useEffect, useMemo } from "react";
+import useNavigation from "@/hooks/useNavigation";
+import { useSetRecoilState } from "recoil";
+import { propertiesDrawerState } from "@/atoms/editor";
+import useDashboard from "@/hooks/dashboards/useDashboard";
+import TextComponent from "./TextComponent";
+import ImageComponent from "./ImageComponent";
+import VideoComponent from "./VideoComponent";
+import LinkComponent from "./LinkComponent";
+import SpacerComponent from "./SpacerComponent";
+import { useTheme } from "next-themes";
+
+export default function FastboardCard({
+ id,
+ properties,
+}: {
+ id: ComponentId;
+ properties: CardProperties;
+}) {
+ const { theme } = useTheme();
+ const {
+ sourceQueryData,
+ queryParameters,
+ components,
+ spacing,
+ showShadow,
+ backgroundColor,
+ } = properties;
+ const { updateComponentProperties } = useDashboard();
+ const { getQueryParam } = useNavigation();
+ const traducedQueryParameters = useMemo(() => {
+ if (!queryParameters) return {};
+
+ return Object.entries(queryParameters).reduce((acc, parameter) => {
+ // Test if the value of the parametes has the synstax {{URL.queryValue}}
+ const pattern = /\{\{\s*URL\.([^\s]+)\s*\}\}/;
+ const match = parameter[1].match(pattern);
+ if (match) {
+ const queryValue = getQueryParam(match[1]);
+ return { ...acc, [parameter[0]]: queryValue };
+ }
+ return { ...acc, [parameter[0]]: parameter[1] };
+ }, {});
+ }, [queryParameters]);
+ const {
+ data,
+ keys,
+ isFetching: dataFetching,
+ isError: isDataError,
+ error: dataError,
+ refetch,
+ } = useData(
+ `${ComponentType.Card}-${id}`,
+ sourceQueryData,
+ traducedQueryParameters
+ );
+ const setPropertiesState = useSetRecoilState(propertiesDrawerState);
+
+ useEffect(() => {
+ updateComponentProperties(id, {
+ ...properties,
+ dataKeys: keys,
+ });
+ setPropertiesState((previous) => {
+ if (previous.selectedComponentId !== id) {
+ return previous;
+ }
+ return {
+ ...previous,
+ properties: {
+ ...previous.properties,
+ dataKeys: keys,
+ },
+ };
+ });
+ }, [keys]);
+
+ useEffect(() => {
+ refetch();
+ }, [queryParameters]);
+
+ useEffect(() => {
+ if (isDataError) {
+ toast.error(dataError?.message);
+ }
+ }, [isDataError]);
+
+ function isValidData(data: any[]) {
+ if (data.length > 1) {
+ return false;
+ }
+ if (data.length === 0) {
+ return false;
+ }
+ return typeof data[0] === "object";
+ }
+
+ function renderComponent(component: CardComponentProperties, data: any[]) {
+ const item = data[0];
+
+ switch (component.type) {
+ case CardComponentType.Text: {
+ return (
+
+ );
+ }
+ case CardComponentType.Image:
+ return (
+
+ );
+ case CardComponentType.Link:
+ return (
+
+ );
+ case CardComponentType.Video:
+ return (
+
+ );
+ case CardComponentType.Spacer:
+ return (
+
+ );
+ default:
+ return
Component not found
;
+ }
+ }
+
+ return (
+
+ {dataFetching &&
}
+ {!dataFetching && isDataError && (
+
+ {dataError?.message}
+
+ )}
+
+ {!dataFetching && !isDataError && (
+ <>
+ {!sourceQueryData && (
+
+ Nothing to show. There is no query selected
+
+ )}
+ {sourceQueryData && isValidData(data) && (
+ <>
+ {components.length === 0 && (
+
+ Add components
+
+ )}
+
+ {components.length > 0 && (
+
+ {components.map((component) =>
+ renderComponent(component, data)
+ )}
+
+ )}
+ >
+ )}
+ {sourceQueryData && !isValidData(data) && (
+
+ Seems like the data is not an object or is empty.
+
+ )}
+ >
+ )}
+
+ );
+}
diff --git a/src/components/editor/fastboard-components/card/FastboardCardDraggable.tsx b/src/components/editor/fastboard-components/card/FastboardCardDraggable.tsx
new file mode 100644
index 0000000..d5bce51
--- /dev/null
+++ b/src/components/editor/fastboard-components/card/FastboardCardDraggable.tsx
@@ -0,0 +1,20 @@
+import Draggable from "../Draggable";
+import { ComponentType } from "@/types/editor";
+import DraggableImage from "@/components/shared/DraggableImage";
+import { CardProperties } from "@/types/editor/card-types";
+
+export default function FastboardCardDraggable() {
+ return (
+
+
+
+ );
+}
diff --git a/src/components/editor/fastboard-components/card/ImageComponent.tsx b/src/components/editor/fastboard-components/card/ImageComponent.tsx
new file mode 100644
index 0000000..70b92e0
--- /dev/null
+++ b/src/components/editor/fastboard-components/card/ImageComponent.tsx
@@ -0,0 +1,66 @@
+import { Alignment } from "@/components/shared/AlignmentProperty";
+import { ImageComponentProperties } from "@/types/editor/card-types";
+import { FastboardHeaderPhotoSize } from "@/types/editor/header-types";
+import { Image } from "@nextui-org/react";
+import { useEffect, useState } from "react";
+
+export default function ImageComponent({
+ properties,
+ item,
+}: {
+ properties: ImageComponentProperties;
+ item: any;
+}) {
+ const { dataKey, alignment, border, size } = properties;
+ const [imageError, setImageError] = useState(false);
+
+ useEffect(() => {
+ setImageError(false);
+ }, [dataKey]);
+
+ function getSize(size: FastboardHeaderPhotoSize) {
+ switch (size) {
+ case FastboardHeaderPhotoSize.Small:
+ return "100px";
+ case FastboardHeaderPhotoSize.Medium:
+ return "150px";
+ case FastboardHeaderPhotoSize.Large:
+ return "300px";
+ }
+ }
+
+ return (
+
+ setImageError(true)}
+ />
+
+ );
+}
diff --git a/src/components/editor/fastboard-components/card/LinkComponent.tsx b/src/components/editor/fastboard-components/card/LinkComponent.tsx
new file mode 100644
index 0000000..9c14378
--- /dev/null
+++ b/src/components/editor/fastboard-components/card/LinkComponent.tsx
@@ -0,0 +1,76 @@
+import { Alignment } from "@/components/shared/AlignmentProperty";
+import { Icon } from "@/components/shared/IconPicker";
+import { LinkComponentProperties } from "@/types/editor/card-types";
+import { Link } from "@nextui-org/react";
+import { useTheme } from "next-themes";
+
+export default function LinkComponent({
+ properties,
+ item,
+}: {
+ properties: LinkComponentProperties;
+ item: any;
+}) {
+ const { theme } = useTheme();
+ const {
+ dataKey,
+ alignment,
+ label,
+ defaultText,
+ isExternal,
+ externalIcon,
+ showExternalIcon,
+ fontSize,
+ textColor,
+ labelColor,
+ } = properties;
+
+ function addPrefix(url: string) {
+ if (!url.startsWith("http://") && !url.startsWith("https://")) {
+ return `https://${url}`;
+ }
+ return url;
+ }
+
+ return (
+
+
+ {label}
+
+
+ ) : (
+
+ )
+ }
+ style={{
+ color: theme === "dark" ? textColor.dark : textColor.light,
+ fontSize: `${fontSize}px`,
+ }}
+ >
+ {dataKey !== "" ? item[dataKey] : defaultText}
+
+
+ );
+}
diff --git a/src/components/editor/fastboard-components/card/SpacerComponent.tsx b/src/components/editor/fastboard-components/card/SpacerComponent.tsx
new file mode 100644
index 0000000..4eac58f
--- /dev/null
+++ b/src/components/editor/fastboard-components/card/SpacerComponent.tsx
@@ -0,0 +1,25 @@
+import {
+ SpacerComponentProperties,
+ VideoComponentProperties,
+} from "@/types/editor/card-types";
+import { Spacer } from "@nextui-org/react";
+import React from "react";
+import ReactPlayer from "react-player/lazy";
+
+export default function SpacerComponent({
+ properties,
+ item,
+}: {
+ properties: SpacerComponentProperties;
+ item: any;
+}) {
+ const { height } = properties;
+
+ return (
+
+ );
+}
diff --git a/src/components/editor/fastboard-components/card/TextComponent.tsx b/src/components/editor/fastboard-components/card/TextComponent.tsx
new file mode 100644
index 0000000..d77cd8a
--- /dev/null
+++ b/src/components/editor/fastboard-components/card/TextComponent.tsx
@@ -0,0 +1,63 @@
+import { Alignment } from "@/components/shared/AlignmentProperty";
+import { FontType } from "@/components/shared/FontTypeProperty";
+import { TextComponentProperties } from "@/types/editor/card-types";
+import { useTheme } from "next-themes";
+
+export default function TextComponent({
+ properties,
+ item,
+}: {
+ properties: TextComponentProperties;
+ item: any;
+}) {
+ const { theme } = useTheme();
+ const {
+ dataKey,
+ label,
+ defaultText,
+ alignment,
+ fontSize,
+ fontTypes,
+ textColor,
+ labelColor,
+ } = properties;
+
+ return (
+
+ {label !== "" && (
+
+ {label}
+
+ )}
+
+
+ {dataKey !== "" ? item[dataKey] : defaultText}
+
+
+ );
+}
diff --git a/src/components/editor/fastboard-components/card/VideoComponent.tsx b/src/components/editor/fastboard-components/card/VideoComponent.tsx
new file mode 100644
index 0000000..afb62a7
--- /dev/null
+++ b/src/components/editor/fastboard-components/card/VideoComponent.tsx
@@ -0,0 +1,37 @@
+import { VideoComponentProperties } from "@/types/editor/card-types";
+import React from "react";
+import ReactPlayer from "react-player/lazy";
+
+export default function VideoComponent({
+ properties,
+ item,
+}: {
+ properties: VideoComponentProperties;
+ item: any;
+}) {
+ const { dataKey } = properties;
+
+ return (
+
+ {
+ console.error(e.target.error);
+ }}
+ />
+
+ );
+}
diff --git a/src/components/editor/fastboard-components/card/properties/CardComponent.tsx b/src/components/editor/fastboard-components/card/properties/CardComponent.tsx
new file mode 100644
index 0000000..96e62c2
--- /dev/null
+++ b/src/components/editor/fastboard-components/card/properties/CardComponent.tsx
@@ -0,0 +1,109 @@
+import {
+ CardComponentProperties,
+ CardComponentType,
+ DefaultCardComponentProperties,
+ ImageComponentProperties,
+ LinkComponentProperties,
+ SpacerComponentProperties,
+ TextComponentProperties,
+ VideoComponentProperties,
+} from "@/types/editor/card-types";
+import CardTextComponentProperties from "./CardTextComponentProperties";
+import { Select, SelectItem } from "@nextui-org/react";
+import CardComponentIcon from "./CardComponentIcon";
+import CardImageComponentProperties from "./CardImageComponentProperties";
+import CardLinkComponentProperties from "./CardLinkComponentProperties";
+import CardVideoComponentProperties from "./CardVideoComponentProperties";
+import CardSpacerComponentProperties from "./CardSpacerComponentProperties";
+
+export default function CardComponent({
+ component,
+ dataKeys,
+ onComponentChange,
+}: {
+ component: CardComponentProperties;
+ dataKeys: string[];
+ onComponentChange: (componentProperties: CardComponentProperties) => void;
+}) {
+ const { type } = component;
+
+ function changeComponentType(
+ type: CardComponentType
+ ): CardComponentProperties {
+ const newProperties = DefaultCardComponentProperties.of(type);
+ return {
+ ...newProperties,
+ };
+ }
+
+ return (
+
+ }
+ onChange={(e) => {
+ onComponentChange(
+ changeComponentType(e.target.value as CardComponentType)
+ );
+ }}
+ >
+ {Object.values(CardComponentType).map((type) => (
+ }
+ >
+ {type}
+
+ ))}
+
+
+ {type === CardComponentType.Text && (
+ {
+ onComponentChange(component);
+ }}
+ />
+ )}
+ {type === CardComponentType.Image && (
+ {
+ onComponentChange(component);
+ }}
+ />
+ )}
+ {type === CardComponentType.Link && (
+ {
+ onComponentChange(component);
+ }}
+ />
+ )}
+ {type === CardComponentType.Video && (
+ {
+ onComponentChange(component);
+ }}
+ />
+ )}
+ {type === CardComponentType.Spacer && (
+ {
+ onComponentChange(component);
+ }}
+ />
+ )}
+
+ );
+}
diff --git a/src/components/editor/fastboard-components/card/properties/CardComponentIcon.tsx b/src/components/editor/fastboard-components/card/properties/CardComponentIcon.tsx
new file mode 100644
index 0000000..d86e866
--- /dev/null
+++ b/src/components/editor/fastboard-components/card/properties/CardComponentIcon.tsx
@@ -0,0 +1,36 @@
+import { CardComponentType } from "@/types/editor/card-types";
+import {
+ IconProps,
+ Image,
+ Link,
+ Pharagraphspacing,
+ Text,
+ VideoSquare,
+} from "iconsax-react";
+
+export default function CardComponentIcon({
+ type,
+ size,
+ variant,
+ className,
+}: {
+ type: CardComponentType;
+ size: number;
+ variant?: IconProps["variant"];
+ className?: string;
+}) {
+ switch (type) {
+ case CardComponentType.Text:
+ return ;
+ case CardComponentType.Image:
+ return ;
+ case CardComponentType.Link:
+ return ;
+ case CardComponentType.Video:
+ return ;
+ case CardComponentType.Spacer:
+ return ;
+ default:
+ return null;
+ }
+}
diff --git a/src/components/editor/fastboard-components/card/properties/CardComponentsList.tsx b/src/components/editor/fastboard-components/card/properties/CardComponentsList.tsx
new file mode 100644
index 0000000..7fc5e41
--- /dev/null
+++ b/src/components/editor/fastboard-components/card/properties/CardComponentsList.tsx
@@ -0,0 +1,74 @@
+import {
+ Button,
+ Dropdown,
+ DropdownItem,
+ DropdownMenu,
+ DropdownTrigger,
+} from "@nextui-org/react";
+import { Add } from "iconsax-react";
+
+import Option from "@/components/shared/Option";
+import {
+ CardComponentProperties,
+ CardComponentType,
+ DefaultCardComponentProperties,
+} from "@/types/editor/card-types";
+import CardComponentIcon from "./CardComponentIcon";
+
+export default function CardComponentsList({
+ components,
+ onSelectComponent,
+ onChange,
+}: {
+ components: CardComponentProperties[];
+ onSelectComponent?: (component: CardComponentProperties) => void;
+ onChange?: (newComponents: CardComponentProperties[]) => void;
+}) {
+ return (
+
+
+
+
+ } variant={"flat"}>
+ Add
+
+
+
+ {Object.values(CardComponentType).map((type) => (
+ {
+ onChange?.([
+ ...components,
+ DefaultCardComponentProperties.of(type),
+ ]);
+ }}
+ startContent={}
+ >
+ {type}
+
+ ))}
+
+
+
+
+
+ {components.map((component, index) => (
+ }
+ onPress={() => {
+ onSelectComponent?.(component);
+ }}
+ onDelete={() => {
+ const newComponents = [...components];
+ newComponents.splice(index, 1);
+ onChange?.(newComponents);
+ }}
+ />
+ ))}
+
+
+ );
+}
diff --git a/src/components/editor/fastboard-components/card/properties/CardImageComponentProperties.tsx b/src/components/editor/fastboard-components/card/properties/CardImageComponentProperties.tsx
new file mode 100644
index 0000000..5aaa2f6
--- /dev/null
+++ b/src/components/editor/fastboard-components/card/properties/CardImageComponentProperties.tsx
@@ -0,0 +1,93 @@
+import AlignmentProperty from "@/components/shared/AlignmentProperty";
+import ImageBoderProperty from "@/components/shared/ImageBorderProperty";
+import { ImageComponentProperties } from "@/types/editor/card-types";
+import { FastboardHeaderPhotoSize } from "@/types/editor/header-types";
+import { Select, SelectItem, Slider } from "@nextui-org/react";
+
+export default function CardImageComponentProperties({
+ properties,
+ dataKeys,
+ onValueChange,
+}: {
+ properties: ImageComponentProperties;
+ dataKeys: string[];
+ onValueChange: (properties: ImageComponentProperties) => void;
+}) {
+ const { dataKey, alignment, border, size } = properties;
+
+ return (
+
+
+
+ onValueChange({ ...properties, alignment: position })
+ }
+ />
+ onValueChange({ ...properties, border })}
+ />
+ {
+ const selectedValue = e as number;
+ const size =
+ selectedValue === 0
+ ? FastboardHeaderPhotoSize.Small
+ : selectedValue === 1
+ ? FastboardHeaderPhotoSize.Medium
+ : FastboardHeaderPhotoSize.Large;
+ onValueChange({
+ ...properties,
+ size,
+ });
+ }}
+ />
+
+ );
+}
diff --git a/src/components/editor/fastboard-components/card/properties/CardLinkComponentProperties.tsx b/src/components/editor/fastboard-components/card/properties/CardLinkComponentProperties.tsx
new file mode 100644
index 0000000..5eb5dba
--- /dev/null
+++ b/src/components/editor/fastboard-components/card/properties/CardLinkComponentProperties.tsx
@@ -0,0 +1,163 @@
+import AlignmentProperty from "@/components/shared/AlignmentProperty";
+import ColorPicker from "@/components/shared/ColorPicker";
+import IconPicker from "@/components/shared/IconPicker";
+import { LinkComponentProperties } from "@/types/editor/card-types";
+import { Checkbox, Input, Select, SelectItem } from "@nextui-org/react";
+import { useTheme } from "next-themes";
+
+export default function CardLinkComponentProperties({
+ properties,
+ dataKeys,
+ onValueChange,
+}: {
+ properties: LinkComponentProperties;
+ dataKeys: string[];
+ onValueChange: (properties: LinkComponentProperties) => void;
+}) {
+ const { theme } = useTheme();
+ const {
+ dataKey,
+ label,
+ defaultText,
+ isExternal,
+ externalIcon,
+ showExternalIcon,
+ alignment,
+ fontSize,
+ textColor,
+ labelColor,
+ } = properties;
+
+ return (
+
+
+
{
+ onValueChange({ ...properties, label: value });
+ }}
+ />
+
{
+ onValueChange({ ...properties, defaultText: value });
+ }}
+ />
+
+
Text size
+ {
+ onValueChange({ ...properties, fontSize: Number(value) });
+ }}
+ />
+
+
+
+ onValueChange({ ...properties, alignment: position })
+ }
+ />
+
+
+ Is external
+ {
+ onValueChange({
+ ...properties,
+ isExternal: value,
+ });
+ }}
+ />
+
+
+ Show anchow icon
+ {
+ onValueChange({
+ ...properties,
+ showExternalIcon: value,
+ });
+ }}
+ />
+
+
+ Anchor icon
+ {
+ onValueChange({
+ ...properties,
+ externalIcon: icon,
+ });
+ }}
+ />
+
+
+ {
+ if (theme === "dark") {
+ onValueChange({
+ ...properties,
+ textColor: { dark: color, light: textColor.light },
+ });
+ } else {
+ onValueChange({
+ ...properties,
+ textColor: { dark: textColor.dark, light: color },
+ });
+ }
+ }}
+ />
+ {
+ if (theme === "dark") {
+ onValueChange({
+ ...properties,
+ labelColor: { dark: color, light: labelColor.light },
+ });
+ } else {
+ onValueChange({
+ ...properties,
+ labelColor: { dark: labelColor.dark, light: color },
+ });
+ }
+ }}
+ />
+
+ );
+}
diff --git a/src/components/editor/fastboard-components/card/properties/CardSpacerComponentProperties.tsx b/src/components/editor/fastboard-components/card/properties/CardSpacerComponentProperties.tsx
new file mode 100644
index 0000000..dd64359
--- /dev/null
+++ b/src/components/editor/fastboard-components/card/properties/CardSpacerComponentProperties.tsx
@@ -0,0 +1,35 @@
+import { SpacerComponentProperties } from "@/types/editor/card-types";
+import { Input } from "@nextui-org/react";
+
+export default function CardSpacerComponentProperties({
+ properties,
+ dataKeys,
+ onValueChange,
+}: {
+ properties: SpacerComponentProperties;
+ dataKeys: string[];
+ onValueChange: (properties: SpacerComponentProperties) => void;
+}) {
+ const { height } = properties;
+
+ return (
+
+
+
Spacing
+ {
+ onValueChange({
+ ...properties,
+ height: parseInt(e.target.value),
+ });
+ }}
+ />
+
+
+ );
+}
diff --git a/src/components/editor/fastboard-components/card/properties/CardStyle.tsx b/src/components/editor/fastboard-components/card/properties/CardStyle.tsx
new file mode 100644
index 0000000..918e8d5
--- /dev/null
+++ b/src/components/editor/fastboard-components/card/properties/CardStyle.tsx
@@ -0,0 +1,57 @@
+import ColorPicker from "@/components/shared/ColorPicker";
+import { CardProperties } from "@/types/editor/card-types";
+import { Checkbox, Input } from "@nextui-org/react";
+import { useTheme } from "next-themes";
+
+export default function CardStyle({
+ properties,
+ onValueChange,
+}: {
+ properties: CardProperties;
+ onValueChange: (properties: CardProperties) => void;
+}) {
+ const { theme } = useTheme();
+ const { spacing, backgroundColor, showShadow } = properties;
+
+ return (
+
+
+
Show border
+ {
+ onValueChange({
+ ...properties,
+ showShadow: isSelected,
+ });
+ }}
+ />
+
+
{
+ if (theme === "light") {
+ onValueChange({
+ ...properties,
+ backgroundColor: {
+ light: color,
+ dark: backgroundColor.dark,
+ },
+ });
+ } else {
+ onValueChange({
+ ...properties,
+ backgroundColor: {
+ light: backgroundColor.light,
+ dark: color,
+ },
+ });
+ }
+ }}
+ />
+
+ );
+}
diff --git a/src/components/editor/fastboard-components/card/properties/CardTextComponentProperties.tsx b/src/components/editor/fastboard-components/card/properties/CardTextComponentProperties.tsx
new file mode 100644
index 0000000..6679691
--- /dev/null
+++ b/src/components/editor/fastboard-components/card/properties/CardTextComponentProperties.tsx
@@ -0,0 +1,129 @@
+import AlignmentProperty from "@/components/shared/AlignmentProperty";
+import ColorPicker from "@/components/shared/ColorPicker";
+import FontTypeProperty from "@/components/shared/FontTypeProperty";
+import { TextComponentProperties } from "@/types/editor/card-types";
+import { Input, Select, SelectItem } from "@nextui-org/react";
+import { useTheme } from "next-themes";
+
+export default function CardTextComponentProperties({
+ properties,
+ dataKeys,
+ onValueChange,
+}: {
+ properties: TextComponentProperties;
+ dataKeys: string[];
+ onValueChange: (properties: TextComponentProperties) => void;
+}) {
+ const { theme } = useTheme();
+ const {
+ dataKey,
+ label,
+ defaultText,
+ alignment,
+ fontSize,
+ fontTypes,
+ textColor,
+ labelColor,
+ } = properties;
+
+ return (
+
+
+
{
+ onValueChange({ ...properties, label: value });
+ }}
+ />
+
{
+ onValueChange({ ...properties, defaultText: value });
+ }}
+ />
+
+
Text size
+ {
+ onValueChange({ ...properties, fontSize: Number(value) });
+ }}
+ />
+
+
+
+ onValueChange({ ...properties, alignment: position })
+ }
+ />
+ {
+ onValueChange({ ...properties, fontTypes: newFontTypes });
+ }}
+ />
+ {
+ if (theme === "dark") {
+ onValueChange({
+ ...properties,
+ textColor: { dark: color, light: textColor.light },
+ });
+ } else {
+ onValueChange({
+ ...properties,
+ textColor: { dark: textColor.dark, light: color },
+ });
+ }
+ }}
+ />
+ {
+ if (theme === "dark") {
+ onValueChange({
+ ...properties,
+ labelColor: { dark: color, light: labelColor.light },
+ });
+ } else {
+ onValueChange({
+ ...properties,
+ labelColor: { dark: labelColor.dark, light: color },
+ });
+ }
+ }}
+ />
+
+ );
+}
diff --git a/src/components/editor/fastboard-components/card/properties/CardVideoComponentProperties.tsx b/src/components/editor/fastboard-components/card/properties/CardVideoComponentProperties.tsx
new file mode 100644
index 0000000..a242ddf
--- /dev/null
+++ b/src/components/editor/fastboard-components/card/properties/CardVideoComponentProperties.tsx
@@ -0,0 +1,37 @@
+import { VideoComponentProperties } from "@/types/editor/card-types";
+import { Select, SelectItem } from "@nextui-org/react";
+
+export default function CardVideoComponentProperties({
+ properties,
+ dataKeys,
+ onValueChange,
+}: {
+ properties: VideoComponentProperties;
+ dataKeys: string[];
+ onValueChange: (properties: VideoComponentProperties) => void;
+}) {
+ const { dataKey } = properties;
+
+ return (
+
+
+
+ );
+}
diff --git a/src/components/editor/fastboard-components/card/properties/FastboardCardProperties.tsx b/src/components/editor/fastboard-components/card/properties/FastboardCardProperties.tsx
new file mode 100644
index 0000000..ff91390
--- /dev/null
+++ b/src/components/editor/fastboard-components/card/properties/FastboardCardProperties.tsx
@@ -0,0 +1,220 @@
+import QuerySelection from "@/components/editor/QuerySelection";
+import {
+ CardComponentProperties,
+ CardProperties,
+} from "@/types/editor/card-types";
+import {
+ Accordion,
+ AccordionItem,
+ BreadcrumbItem,
+ Breadcrumbs,
+ Code,
+ Input,
+ Spacer,
+ Tooltip,
+} from "@nextui-org/react";
+import { RiQuestionLine } from "react-icons/ri";
+import CardComponentsList from "./CardComponentsList";
+import { useEffect, useState } from "react";
+import { useRecoilValue } from "recoil";
+import { propertiesDrawerState } from "@/atoms/editor";
+import CardComponent from "./CardComponent";
+import { QueryType } from "@/types/connections";
+import CardStyle from "./CardStyle";
+import DebounceInput from "@/components/shared/DebounceInput";
+
+export default function FastboardCardProperties({
+ properties,
+ onValueChange,
+}: {
+ properties: CardProperties;
+ onValueChange: (properties: CardProperties) => void;
+}) {
+ const { sourceQueryData, queryParameters, components, dataKeys } = properties;
+ const { selectedComponentId } = useRecoilValue(propertiesDrawerState);
+ const [componentSelectedIndex, setComponentSelectedIndex] = useState<
+ number | null
+ >(null);
+
+ useEffect(() => {
+ // Reset selectinos when component is changed
+ setComponentSelectedIndex(null);
+ }, [selectedComponentId]);
+
+ function onComponentChange(component: CardComponentProperties) {
+ if (componentSelectedIndex === null) {
+ return;
+ }
+ const newComponents = [...components];
+ newComponents[componentSelectedIndex] = component;
+
+ onValueChange({
+ ...properties,
+ components: newComponents,
+ });
+ }
+
+ return (
+
+
+ {
+ setComponentSelectedIndex(null);
+ }}
+ >
+ Card
+
+
+ {componentSelectedIndex != null && (
+ Component
+ )}
+
+
+
+ {componentSelectedIndex === null && (
+
+
+ {
+ if (newQuery.id === sourceQueryData?.queryId) {
+ return;
+ }
+ onValueChange({
+ ...properties,
+ sourceQueryData: {
+ queryId: newQuery.id,
+ connectionId: newQuery.connection_id,
+ method: newQuery.metadata?.method,
+ },
+ queryParameters: newQuery.metadata?.parameters?.reduce(
+ (acc: Record, param: any) => {
+ acc[param.name] = param.preview;
+ return acc;
+ },
+ {}
+ ),
+ dataKeys: [],
+ components: components.map((component) => ({
+ ...component,
+ dataKey: "",
+ })),
+ });
+ }}
+ />
+ {sourceQueryData && queryParameters && (
+ <>
+
+
Parameters
+
+ Use{" "}
+
+ {"{{URL.queryValue}}"}
+ {" "}
+ syntax to access url query strings.
+
+ }
+ className={"p-3 w-[275px] -translate-x-[35px] text-xs"}
+ placement={"bottom"}
+ offset={10}
+ closeDelay={0}
+ >
+
+
+
+
+
+
+
+ {Object.entries(queryParameters).map((parameter, index) => (
+
+
{parameter[0]}
+ {
+ onValueChange({
+ ...properties,
+ queryParameters: {
+ ...queryParameters,
+ [parameter[0]]: value,
+ },
+ });
+ }}
+ />
+
+ ))}
+
+ >
+ )}
+
+
+ {(!sourceQueryData || dataKeys.length === 0) && (
+
+ Select a query to enable components
+
+ )}
+ {sourceQueryData && dataKeys.length > 0 && (
+ {
+ setComponentSelectedIndex(components.indexOf(component));
+ }}
+ onChange={(newComponents) =>
+ onValueChange({ ...properties, components: newComponents })
+ }
+ />
+ )}
+
+
+
+
+
+ )}
+
+ {componentSelectedIndex !== null && (
+
+ )}
+
+ );
+}
diff --git a/src/components/editor/fastboard-components/header/FastboardHeader.tsx b/src/components/editor/fastboard-components/header/FastboardHeader.tsx
index 7a78bf5..b460d57 100644
--- a/src/components/editor/fastboard-components/header/FastboardHeader.tsx
+++ b/src/components/editor/fastboard-components/header/FastboardHeader.tsx
@@ -1,10 +1,10 @@
import { ComponentId } from "@/types/editor";
import { FastboardHeaderProperties } from "@/types/editor/header-types";
import { Image, Navbar, NavbarContent, NavbarItem } from "@nextui-org/react";
-import { FastboardHeaderPosition } from "@/types/editor/header-types";
import { useEffect, useState } from "react";
import { ThemeSwitcher } from "@/components/layout/ThemeSwitcher";
import { useTheme } from "next-themes";
+import { Alignment } from "@/components/shared/AlignmentProperty";
export default function FastboardHeader({
id,
@@ -43,17 +43,20 @@ export default function FastboardHeader({
theme === "light" ? backgroundColor.light : backgroundColor.dark,
}}
>
-
+
{photo.url && (
{title.size && (
- {showThemeSwitcher && position === FastboardHeaderPosition.Right && (
+ {showThemeSwitcher && position === Alignment.Right && (
)}
- {showThemeSwitcher && position !== FastboardHeaderPosition.Right && (
+ {showThemeSwitcher && position !== Alignment.Right && (
-
Position
-
- {Object.entries(FastboardHeaderPosition).map(([key, value]) => (
-
- }
- onClick={() => {
- onValueChange({
- ...properties,
- position: value,
- });
- }}
- >
- {key}
-
- ))}
-
+
+ onValueChange({
+ ...properties,
+ position: position,
+ })
+ }
+ />
Photo Border Radius
diff --git a/src/components/editor/fastboard-components/sidebar/FastboardSidebar.tsx b/src/components/editor/fastboard-components/sidebar/FastboardSidebar.tsx
index a6aeac8..52a4780 100644
--- a/src/components/editor/fastboard-components/sidebar/FastboardSidebar.tsx
+++ b/src/components/editor/fastboard-components/sidebar/FastboardSidebar.tsx
@@ -13,7 +13,6 @@ export default function FastboardSidebar({
const { theme } = useTheme();
const { currentPage, changePage } = useNavigation();
const { menuItems, backgroundColor, textColor, selectedColor } = properties;
- const cursorClassName = `bg-[${selectedColor.light}]`;
function handleSelectionChange(key: Key) {
changePage(key.toString());
diff --git a/src/components/editor/fastboard-components/table/FastboardTable.tsx b/src/components/editor/fastboard-components/table/FastboardTable.tsx
index 5b57acf..5b0ae00 100644
--- a/src/components/editor/fastboard-components/table/FastboardTable.tsx
+++ b/src/components/editor/fastboard-components/table/FastboardTable.tsx
@@ -1,4 +1,3 @@
-import CustomSkeleton from "@/components/shared/CustomSkeleton";
import useExecuteQuery from "@/hooks/adapter/useExecuteQuery";
import {
FastboardTableProperties,
@@ -53,15 +52,16 @@ import {
Table,
useReactTable,
} from "@tanstack/react-table";
-import { HTTP_METHOD } from "@/types/connections";
import {
fillParameters,
getExportData,
getFilterFunction,
getFinalColumns,
sortFunction,
+ traduceQueryStrings,
} from "@/lib/table.utils";
-import Filters, { NumberFilter, StringFilter } from "./Filters";
+import Filters from "./Filters";
+import useNavigation from "@/hooks/useNavigation";
export default function FastboardTable({
id,
@@ -73,6 +73,7 @@ export default function FastboardTable({
const { theme } = useTheme();
const { id: dashboardId } = useParams();
const { updateComponentProperties } = useDashboard();
+ const { changePage } = useNavigation();
const { openModal } = useModalFrame();
const {
sourceQueryData,
@@ -345,16 +346,29 @@ export default function FastboardTable({
}
onPress={() => {
setSelectedRowAction({ action, item });
+ updateComponentProperties(id, {
+ ...properties,
+ selectedRow: item,
+ });
if (action.type === "view") {
- executeAction({ action, item });
- setViewModalOpen(action.query ? true : false);
+ if (action.mode === "modal") {
+ executeAction({ action, item });
+ setViewModalOpen(action.query ? true : false);
+ } else if (action.mode === "page") {
+ if (!action.pageId) {
+ toast.warning(
+ "No page found for this action, please add one in action properties"
+ );
+ return;
+ }
+ changePage(
+ action.pageId,
+ traduceQueryStrings(action.queryStrings, item)
+ );
+ }
} else if (action.type === "delete") {
setDeleteModalOpen(true);
} else if (action.type === "edit") {
- updateComponentProperties(id, {
- ...properties,
- selectedRow: item,
- });
openModal(action.modalId ?? "");
}
}}
diff --git a/src/components/editor/fastboard-components/table/properties/FastboardTableProperties.tsx b/src/components/editor/fastboard-components/table/properties/FastboardTableProperties.tsx
index d7a08e8..8217404 100644
--- a/src/components/editor/fastboard-components/table/properties/FastboardTableProperties.tsx
+++ b/src/components/editor/fastboard-components/table/properties/FastboardTableProperties.tsx
@@ -31,6 +31,7 @@ import { QueryType } from "@/types/connections";
import FiltersList from "./filters/FiltersList";
import TableStringFilterProperties from "./filters/TableStringFilterProperties";
import TableNumberFilterProperties from "./filters/TableNumberFilterProperties";
+import { ViewActionProperties } from "./ViewActionProperties";
import { queryToQueryData } from "@/lib/rest-queries";
const FastboardTablePropertiesComponent = ({
@@ -285,7 +286,7 @@ const FastboardTablePropertiesComponent = ({
)}
- {actionSelected && (
+ {actionSelected && actionSelected.type == "delete" && (
c.column)}
@@ -298,6 +299,19 @@ const FastboardTablePropertiesComponent = ({
}}
/>
)}
+ {actionSelected && actionSelected.type == "view" && (
+ c.column)}
+ onChange={(action) => {
+ setActionSelected(action);
+ onValueChange({
+ ...properties,
+ actions: actions.map((a) => (a.key === action.key ? action : a)),
+ });
+ }}
+ />
+ )}
{filterIndexSelected !== null &&
filters[filterIndexSelected]?.type === FilterType.StringFilter && (
diff --git a/src/components/editor/fastboard-components/table/properties/TableActionsList.tsx b/src/components/editor/fastboard-components/table/properties/TableActionsList.tsx
index 9eedefa..2f58dec 100644
--- a/src/components/editor/fastboard-components/table/properties/TableActionsList.tsx
+++ b/src/components/editor/fastboard-components/table/properties/TableActionsList.tsx
@@ -24,7 +24,7 @@ export default function TableActionsList({
onActionSelect: (action: TableActionProperty) => void;
onChange?: (actions: TableActionProperty[]) => void;
}) {
- const { createModalFrame, deleteModalFrame } = useDashboard();
+ const { createModalFrame, deleteModalFrame, deletePage } = useDashboard();
const [actions, setActions] = useState(actionsProperties);
useEffect(() => {
@@ -38,6 +38,7 @@ export default function TableActionsList({
type,
query: null,
parameters: [],
+ queryStrings: [],
};
setActions((previous) => [...previous, newAction]);
@@ -66,6 +67,7 @@ export default function TableActionsList({
query: null,
parameters: [],
modalId,
+ queryStrings: [],
};
setActions((previous) => [...previous, newAction]);
@@ -78,6 +80,9 @@ export default function TableActionsList({
if (actions[index].modalId) {
deleteModalFrame(actions[index].modalId as string);
}
+ if (actions[index].pageId) {
+ deletePage(actions[index].pageId);
+ }
const newActions = actions.filter((action) => action.key !== key);
setActions(newActions);
if (onChange) {
diff --git a/src/components/editor/fastboard-components/table/properties/ViewActionProperties.tsx b/src/components/editor/fastboard-components/table/properties/ViewActionProperties.tsx
new file mode 100644
index 0000000..cf11053
--- /dev/null
+++ b/src/components/editor/fastboard-components/table/properties/ViewActionProperties.tsx
@@ -0,0 +1,249 @@
+import {
+ Button,
+ Code,
+ Input,
+ Select,
+ SelectItem,
+ Tab,
+ Tabs,
+ Tooltip,
+} from "@nextui-org/react";
+import { RiQuestionLine } from "react-icons/ri";
+import { Column, TableActionProperty } from "@/types/editor/table-types";
+import QuerySelection from "@/components/editor/QuerySelection";
+import useDashboard from "@/hooks/dashboards/useDashboard";
+import { Add } from "iconsax-react";
+import useNavigation from "@/hooks/useNavigation";
+import { IoIosClose } from "react-icons/io";
+import { QueryType } from "@/types/connections";
+import { queryToQueryData } from "@/lib/rest-queries";
+
+export function ViewActionProperties({
+ action,
+ columns,
+ onChange,
+}: {
+ action: TableActionProperty;
+ columns: Column[];
+ onChange: (action: TableActionProperty) => void;
+}) {
+ const { label, mode, query, parameters, pageId, queryStrings } = action;
+ const { addPage } = useDashboard();
+ const { currentPage } = useNavigation();
+
+ function createViewPage() {
+ const pageId = addPage(currentPage);
+ if (!pageId) return;
+
+ onChange({
+ ...action,
+ pageId: pageId,
+ });
+ }
+
+ return (
+
+
{
+ onChange({
+ ...action,
+ label: newValue,
+ });
+ }}
+ />
+
+
{
+ onChange({
+ ...action,
+ mode: key.toString() as "modal" | "page",
+ });
+ }}
+ >
+
+ {
+ onChange({
+ ...action,
+ query: queryToQueryData(query),
+ parameters: query.metadata.parameters?.map(
+ (p: { name: string; preview: string }) => ({
+ name: p.name,
+ columnKey: "",
+ value: p.preview,
+ })
+ ),
+ });
+ }}
+ />
+ {parameters?.length > 0 && (
+
+
Parameters
+
+ If no column is selected for a parameter we use{" "}
+ {"preview value"} setting
+ on queries editor.
+
+ }
+ className={"p-3 w-[275px] -translate-x-[35px] text-xs"}
+ placement={"bottom"}
+ offset={10}
+ closeDelay={0}
+ >
+
+
+
+
+
+ )}
+
+ {parameters?.map((parameter, index) => (
+
+
{parameter.name}
+
+
+ ))}
+
+
+
+
+ {!pageId && (
+
+ )}
+ {pageId && (
+
+
+ }
+ variant={"flat"}
+ onPress={() => {
+ onChange({
+ ...action,
+ queryStrings: queryStrings
+ ? [
+ ...queryStrings,
+ {
+ name: "",
+ columnKey: "",
+ },
+ ]
+ : [{ name: "", columnKey: "" }],
+ });
+ }}
+ >
+ Add Query String
+
+
+
+ {queryStrings?.map((queryString, index) => (
+
+ {
+ const newQueryStrings = queryStrings.map((qs, i) => {
+ if (i === index) {
+ return {
+ ...qs,
+ name: value,
+ };
+ }
+ return qs;
+ });
+ onChange({
+ ...action,
+ queryStrings: newQueryStrings,
+ });
+ }}
+ />
+
+
+
+
+
+ ))}
+
+ )}
+
+
+
+ );
+}
diff --git a/src/components/editor/fastboard-components/utils.tsx b/src/components/editor/fastboard-components/utils.tsx
index 0823c87..3a5f76e 100644
--- a/src/components/editor/fastboard-components/utils.tsx
+++ b/src/components/editor/fastboard-components/utils.tsx
@@ -44,6 +44,10 @@ import { SidebarProperties } from "@/types/editor/sidebar-types";
import FastboardHeader from "./header/FastboardHeader";
import FastboardHeaderPropertiesComponent from "./header/FastboardHeaderProperties";
import { FastboardHeaderProperties } from "@/types/editor/header-types";
+import FastboardCard from "./card/FastboardCard";
+import { CardProperties } from "@/types/editor/card-types";
+import FastboardCardProperties from "./card/properties/FastboardCardProperties";
+import FastboardCardDraggable from "./card/FastboardCardDraggable";
export function getDraggableComponent(id: ComponentType) {
const components = {
@@ -54,6 +58,7 @@ export function getDraggableComponent(id: ComponentType) {
),
[ComponentType.Cards]:
,
+ [ComponentType.Card]:
,
[ComponentType.Sidebar]: null,
[ComponentType.Header]: null,
};
@@ -104,7 +109,20 @@ export function getComponent(
editable: null,
view: null,
},
-
+ [ComponentType.Card]: {
+ editable: (
+
+ ),
+ view: (
+
+ ),
+ },
[ComponentType.Cards]: {
editable: (
),
+ [ComponentType.Card]: (
+
{
+ if (onValueChange) {
+ onValueChange(properties);
+ }
+ }}
+ />
+ ),
[ComponentType.Cards]: (
{!(hasSidebar && sidebarVisible) && (
{
changeLayout("home", 0, layoutType);
}}
diff --git a/src/components/shared/AlignmentProperty.tsx b/src/components/shared/AlignmentProperty.tsx
new file mode 100644
index 0000000..7165f86
--- /dev/null
+++ b/src/components/shared/AlignmentProperty.tsx
@@ -0,0 +1,48 @@
+import { Button, ButtonGroup } from "@nextui-org/react";
+import AlignIcon from "./icons/AlignIcon";
+
+export enum Alignment {
+ Left = "Start",
+ Center = "Center",
+ Right = "End",
+}
+
+export default function AlignmentProperty({
+ label,
+ position,
+ onPositionChange,
+}: {
+ label?: string;
+ position: Alignment;
+ onPositionChange: (position: Alignment) => void;
+}) {
+ return (
+
+
{label}
+
+ {Object.entries(Alignment).map(([key, value]) => (
+
+ }
+ onClick={() => {
+ onPositionChange(value);
+ }}
+ >
+ {key}
+
+ ))}
+
+
+ );
+}
diff --git a/src/components/shared/DebounceInput.tsx b/src/components/shared/DebounceInput.tsx
new file mode 100644
index 0000000..4d3cf89
--- /dev/null
+++ b/src/components/shared/DebounceInput.tsx
@@ -0,0 +1,37 @@
+import { Input } from "@nextui-org/react";
+import React, { useEffect, useState } from "react";
+
+export default function DebounceInput({
+ value,
+ onValueChange,
+ delay = 500,
+ className,
+ type,
+}: {
+ value: string;
+ onValueChange: (value: string) => void;
+ delay?: number;
+ className?: string;
+ type?: string;
+}) {
+ const [inputValue, setInputValue] = useState(value);
+
+ useEffect(() => {
+ const timeoutId = setTimeout(() => {
+ onValueChange(inputValue);
+ }, delay);
+ return () => clearTimeout(timeoutId);
+ }, [inputValue, delay]);
+
+ return (
+ {
+ setInputValue(value);
+ }}
+ width={"100%"}
+ />
+ );
+}
diff --git a/src/components/shared/FontTypeProperty.tsx b/src/components/shared/FontTypeProperty.tsx
new file mode 100644
index 0000000..966a439
--- /dev/null
+++ b/src/components/shared/FontTypeProperty.tsx
@@ -0,0 +1,68 @@
+import { Button, ButtonGroup } from "@nextui-org/react";
+
+export enum FontType {
+ Bold = "Bold",
+ Italic = "Italic",
+ Underline = "Underline",
+}
+
+export default function FontTypeProperty({
+ fontTypes,
+ onFontTypesChange,
+}: {
+ fontTypes: FontType[];
+ onFontTypesChange: (fontTypes: FontType[]) => void;
+}) {
+ return (
+
+
+
+
+
+ );
+}
diff --git a/src/components/shared/IconPicker.tsx b/src/components/shared/IconPicker.tsx
index 1d539d1..5f9ac77 100644
--- a/src/components/shared/IconPicker.tsx
+++ b/src/components/shared/IconPicker.tsx
@@ -51,11 +51,11 @@ import {
Heart,
Hierarchy,
Home,
+ Link,
Folder,
Trash,
User,
} from "iconsax-react";
-import { IoIosClose } from "react-icons/io";
export function Icon({
icon,
@@ -113,6 +113,7 @@ export function Icon({
[IconType.Heart]: ,
[IconType.Hierarchy]: ,
[IconType.Home]: ,
+ [IconType.Link]: ,
[IconType.Folder]: ,
[IconType.Trash]: ,
[IconType.User]: ,
diff --git a/src/components/shared/ImageBorderProperty.tsx b/src/components/shared/ImageBorderProperty.tsx
new file mode 100644
index 0000000..2574574
--- /dev/null
+++ b/src/components/shared/ImageBorderProperty.tsx
@@ -0,0 +1,59 @@
+import { Button, ButtonGroup } from "@nextui-org/react";
+
+export enum ImageBorder {
+ None = "none",
+ Round = "lg",
+ Circle = "full",
+}
+
+export default function ImageBoderProperty({
+ label,
+ border,
+ onBorderChange,
+}: {
+ label?: string;
+ border: ImageBorder;
+ onBorderChange: (border: ImageBorder) => void;
+}) {
+ return (
+
+
{label}
+
+ {Object.entries(ImageBorder).map(([key, value]) => (
+
+ }
+ onPress={() => {
+ onBorderChange(value);
+ }}
+ >
+ {key}
+
+ ))}
+
+
+ );
+}
diff --git a/src/components/shared/Viewport.tsx b/src/components/shared/Viewport.tsx
index bfce694..ebcca47 100644
--- a/src/components/shared/Viewport.tsx
+++ b/src/components/shared/Viewport.tsx
@@ -6,9 +6,12 @@ import { previewAccessTokenState } from "@/atoms/editor";
import useNavigation from "@/hooks/useNavigation";
import { useEffect } from "react";
import { getLayout } from "../editor/fastboard-components/utils";
-import { Spinner } from "@nextui-org/react";
+import { Button, Spinner } from "@nextui-org/react";
import { AxiosError } from "axios";
import { notFound } from "next/navigation";
+import { Back } from "iconsax-react";
+
+const RETURN_BUTTON_HEIGHT = 48;
export default function Viewport({
mode,
@@ -19,7 +22,7 @@ export default function Viewport({
mode === "editor" || mode === "preview" ? "editor" : "published"
);
const setPreviewAccessToken = useSetRecoilState(previewAccessTokenState);
- const { currentPage } = useNavigation();
+ const { currentPage, changePage } = useNavigation();
useEffect(() => {
if (mode === "editor" && dashboard?.metadata?.auth?.previewAccessToken) {
@@ -39,9 +42,7 @@ export default function Viewport({
const sidebar = dashboard?.metadata?.sidebar?.id
? getComponent(dashboard.metadata.sidebar?.id)
: null;
- const selectedPage = dashboard?.metadata?.pages[currentPage]
- ? currentPage
- : "home";
+ const selectedPage = dashboard?.metadata?.pages[currentPage];
if (loading) {
return (
@@ -103,15 +104,48 @@ export default function Viewport({
/>
)}
-
- {dashboard?.metadata?.pages[selectedPage].map((layout, index) =>
- getLayout(
- layout,
- currentPage,
- index,
- mode === "editor" ? "editable" : "view"
- )
+
+ {selectedPage && selectedPage.returnPage && (
+
+
+
)}
+
+ {selectedPage &&
+ selectedPage.layouts.map((layout, index) =>
+ getLayout(
+ layout,
+ currentPage,
+ index,
+ mode === "editor" ? "editable" : "view"
+ )
+ )}
+
diff --git a/src/hooks/dashboards/useDashboard.ts b/src/hooks/dashboards/useDashboard.ts
index a23c325..ccd9850 100644
--- a/src/hooks/dashboards/useDashboard.ts
+++ b/src/hooks/dashboards/useDashboard.ts
@@ -146,10 +146,13 @@ const useDashboard = (mode: "editor" | "published" = "editor") => {
changePage("home");
};
- const addPage = (): string | null => {
+ const addPage = (returnPage?: string): string | null => {
if (!dashboard) return null;
- const [newMetadata, pageId] = editorUtils.addPage(dashboard.metadata);
+ const [newMetadata, pageId] = editorUtils.addPage(
+ dashboard.metadata,
+ returnPage
+ );
updateDashboard((prev) => ({
...prev,
metadata: newMetadata,
@@ -167,7 +170,7 @@ const useDashboard = (mode: "editor" | "published" = "editor") => {
const getBaseLayout = (): LayoutType | null => {
if (!dashboard) return null;
- return dashboard.metadata.pages["home"][0].type;
+ return dashboard.metadata.pages["home"].layouts[0].type;
};
const changeLayout = (
diff --git a/src/hooks/useData.ts b/src/hooks/useData.ts
index 7d1c2a4..0c09269 100644
--- a/src/hooks/useData.ts
+++ b/src/hooks/useData.ts
@@ -6,7 +6,11 @@ import { useRecoilValue } from "recoil";
import { previewAccessTokenState } from "@/atoms/editor";
import { useParams } from "next/navigation";
-const useData = (componentId: string, queryData: QueryData | null) => {
+const useData = (
+ componentId: string,
+ queryData: QueryData | null,
+ queryParameters?: Record
+) => {
const { queryId, connectionId } = queryData || {};
const { id: dashboardId } = useParams();
const previewAccessToken = useRecoilValue(previewAccessTokenState);
@@ -37,7 +41,7 @@ const useData = (componentId: string, queryData: QueryData | null) => {
const response = await adapterService.executeQuery(
queryId ?? null,
dashboardId as string,
- {},
+ queryParameters ?? {},
previewAccessToken,
{
headers: {
diff --git a/src/hooks/useNavigation.ts b/src/hooks/useNavigation.ts
index 37447f8..347a88d 100644
--- a/src/hooks/useNavigation.ts
+++ b/src/hooks/useNavigation.ts
@@ -1,10 +1,13 @@
+import { isPropertiesDrawerOpen } from "@/atoms/editor";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
import { useCallback, useMemo } from "react";
+import { useSetRecoilState } from "recoil";
const useNavigation = () => {
const router = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams();
+ const setIsPropertiesDrawerOpen = useSetRecoilState(isPropertiesDrawerOpen);
// Get a new searchParams string by merging the current
// searchParams with a provided key/value pair
@@ -22,13 +25,29 @@ const useNavigation = () => {
return searchParams.get("page") || "home";
}, [searchParams]);
- function changePage(pageId: string) {
- router.push(pathname + "?" + createQueryString("page", pageId));
+ function changePage(
+ pageId: string,
+ queryStrings?: { name: string; value: string }[]
+ ) {
+ const newParams = new URLSearchParams();
+ newParams.set("page", pageId);
+ if (queryStrings) {
+ queryStrings.forEach((qs) => {
+ newParams.set(qs.name, qs.value);
+ });
+ }
+ router.push(pathname + "?" + newParams.toString());
+ setIsPropertiesDrawerOpen(false);
+ }
+
+ function getQueryParam(key: string) {
+ return searchParams.get(key);
}
return {
currentPage,
changePage,
+ getQueryParam,
};
};
diff --git a/src/lib/editor.utils.ts b/src/lib/editor.utils.ts
index 6405c3c..89a16b5 100644
--- a/src/lib/editor.utils.ts
+++ b/src/lib/editor.utils.ts
@@ -43,14 +43,19 @@ function deleteTableModalsFrame(
component: FastboardComponent,
dashboardMetadata: DashboardMetadata
): DashboardMetadata {
+ const viewModalId = component.properties?.actions?.find(
+ (action: TableActionProperty) => action.type === "view"
+ )?.modalId;
const editModalId = component.properties?.actions?.find(
(action: TableActionProperty) => action.type === "edit"
)?.modalId;
const addRowModalId = component.properties?.addOns?.addRowForm?.modalId;
+ if (viewModalId) {
+ dashboardMetadata = removeModalFrame(viewModalId, dashboardMetadata);
+ }
if (editModalId) {
dashboardMetadata = removeModalFrame(editModalId, dashboardMetadata);
}
-
if (addRowModalId) {
dashboardMetadata = removeModalFrame(addRowModalId, dashboardMetadata);
}
@@ -58,6 +63,20 @@ function deleteTableModalsFrame(
return dashboardMetadata;
}
+function deleteTablePageView(
+ component: FastboardComponent,
+ dashboardMetadata: DashboardMetadata
+): DashboardMetadata {
+ const pageId = component.properties?.actions?.find(
+ (action: TableActionProperty) => action.type === "view"
+ )?.pageId;
+
+ if (pageId) {
+ dashboardMetadata = deletePage(pageId, dashboardMetadata);
+ }
+ return dashboardMetadata;
+}
+
export function deleteComponent(
id: ComponentId,
dashboardMetadata: DashboardMetadata
@@ -67,8 +86,9 @@ export function deleteComponent(
return dashboardMetadata;
}
if (component.type === ComponentType.Table) {
- //If the component is a table, then we need to remove the modal frame that is associated with it
+ //If the component is a table, then we need to remove the modal frame that is associated with it and view pages also
dashboardMetadata = deleteTableModalsFrame(component, dashboardMetadata);
+ dashboardMetadata = deleteTablePageView(component, dashboardMetadata);
}
const { [id]: removedComponent, ...newComponents } =
dashboardMetadata.components;
@@ -111,7 +131,7 @@ export function addComponentToLayout(
container: containerIndex,
} = index;
// If there is already a component in the container, delete it
- const layout = dashboardMetadata.pages[pageIndex][layoutIndex];
+ const layout = dashboardMetadata.pages[pageIndex].layouts[layoutIndex];
const curretnComponentId: ComponentId | null =
layout[containerIndex as keyof Layout];
if (curretnComponentId) {
@@ -127,15 +147,18 @@ export function addComponentToLayout(
...newMetadata,
pages: {
...newMetadata.pages,
- [pageIndex]: newMetadata.pages[pageIndex].map((layout, index) => {
- if (index === layoutIndex) {
- return {
- ...layout,
- [containerIndex]: componentId,
- };
- }
- return layout;
- }),
+ [pageIndex]: {
+ ...newMetadata.pages[pageIndex],
+ layouts: newMetadata.pages[pageIndex].layouts.map((layout, index) => {
+ if (index === layoutIndex) {
+ return {
+ ...layout,
+ [containerIndex]: componentId,
+ };
+ }
+ return layout;
+ }),
+ },
},
};
}
@@ -145,7 +168,7 @@ export function deleteComponentFromLayout(
dashboardMetadata: DashboardMetadata
): DashboardMetadata {
const { page, layout: layoutIndex, container } = index;
- const layout = dashboardMetadata.pages[page][layoutIndex];
+ const layout = dashboardMetadata.pages[page].layouts[layoutIndex];
const componentId: ComponentId = layout[container as keyof Layout];
if (!componentId) {
return dashboardMetadata;
@@ -156,15 +179,18 @@ export function deleteComponentFromLayout(
...newMetadata,
pages: {
...newMetadata.pages,
- [page]: newMetadata.pages[page].map((layout, index) => {
- if (index === layoutIndex) {
- return {
- ...layout,
- [container]: null,
- };
- }
- return layout;
- }),
+ [page]: {
+ ...newMetadata.pages[page],
+ layouts: newMetadata.pages[page].layouts.map((layout, index) => {
+ if (index === layoutIndex) {
+ return {
+ ...layout,
+ [container]: null,
+ };
+ }
+ return layout;
+ }),
+ },
},
};
}
@@ -176,7 +202,7 @@ function changeLayout(
dashboardMetadata: DashboardMetadata
): DashboardMetadata {
let to = Layout.of(to_type);
- const from = dashboardMetadata.pages[pageIndex][layoutIndex];
+ const from = dashboardMetadata.pages[pageIndex].layouts[layoutIndex];
const keysFrom = Object.keys(from);
const keysTo = Object.keys(to);
@@ -192,12 +218,17 @@ function changeLayout(
...dashboardMetadata,
pages: {
...dashboardMetadata.pages,
- [pageIndex]: dashboardMetadata.pages[pageIndex].map((layout, index) => {
- if (index === layoutIndex) {
- return convertLayout(from, to_type);
- }
- return layout;
- }),
+ [pageIndex]: {
+ ...dashboardMetadata.pages[pageIndex],
+ layouts: dashboardMetadata.pages[pageIndex].layouts.map(
+ (layout, index) => {
+ if (index === layoutIndex) {
+ return convertLayout(from, to_type);
+ }
+ return layout;
+ }
+ ),
+ },
},
};
}
@@ -333,7 +364,8 @@ function deleteSidebar(
}
function addPage(
- dashboardMetadata: DashboardMetadata
+ dashboardMetadata: DashboardMetadata,
+ returnPage?: string
): [DashboardMetadata, string] {
const pageId = uuidv4();
return [
@@ -341,7 +373,10 @@ function addPage(
...dashboardMetadata,
pages: {
...dashboardMetadata.pages,
- [pageId]: [Layout.of(LayoutType.Full)],
+ [pageId]: {
+ layouts: [Layout.of(LayoutType.Full)],
+ returnPage: returnPage,
+ },
},
},
pageId,
@@ -353,7 +388,7 @@ function deletePage(
dashboardMetadata: DashboardMetadata
): DashboardMetadata {
//Remove all components in the page
- const layouts = dashboardMetadata.pages[pageId];
+ const layouts = dashboardMetadata.pages[pageId].layouts;
layouts.forEach((layout) => {
Object.keys(layout).forEach((key) => {
if (key === "type") {
diff --git a/src/lib/services/adapter.ts b/src/lib/services/adapter.ts
index 5601fe8..74b1da1 100644
--- a/src/lib/services/adapter.ts
+++ b/src/lib/services/adapter.ts
@@ -18,7 +18,7 @@ const previewQuery = async (
| RestQueryMetadata
| MongoVectorSearchMetadata,
parameters: QueryParameter[],
- config?: AxiosRequestConfig,
+ config?: AxiosRequestConfig
) => {
const contentType = (queryMetadata as RestQueryMetadata)?.contentType;
@@ -41,7 +41,7 @@ const previewQuery = async (
}
}
return param;
- }),
+ })
)
).reduce((acc: any, param: any) => {
return { ...acc, [param.name]: param.preview };
@@ -54,7 +54,7 @@ const previewQuery = async (
connection_metadata: queryMetadata,
parameters: transformedParameters,
},
- config,
+ config
);
return response.data;
@@ -65,7 +65,7 @@ async function executeQuery(
dashboardId: string,
parameters?: Record,
previewAccessToken?: string,
- config?: AxiosRequestConfig,
+ config?: AxiosRequestConfig
) {
try {
if (!queryId) {
@@ -73,19 +73,18 @@ async function executeQuery(
}
const token = localStorage.getItem(`auth-${dashboardId}`);
-
- const parametersToSend = parameters ?? {};
-
const viewMode = isPreviewPage() || isPublishPage();
-
- parametersToSend.token = viewMode ? token : previewAccessToken;
+ const parametersToSend = {
+ ...parameters,
+ token: viewMode ? token : previewAccessToken,
+ };
const response = await axiosInstance.post(
`/adapter/execute/${queryId}`,
{
parameters: parametersToSend,
},
- config,
+ config
);
if (response?.data.status_code && response?.data.status_code !== 200) {
@@ -98,7 +97,7 @@ async function executeQuery(
const error = response.data?.body?.error;
throw new Error(
- `Error ${response.data.status_code}: ${error?.description ?? ""}`,
+ `Error ${response.data.status_code}: ${error?.description ?? ""}`
);
}
return response.data;
@@ -109,7 +108,7 @@ async function executeQuery(
async function createEmbeddings(queryId: string, indexField: string) {
const response = await axiosInstance.post(
- `/embeddings/${queryId}?index_field=${indexField}`,
+ `/embeddings/${queryId}?index_field=${indexField}`
);
return mapQuery(response?.data?.body);
diff --git a/src/lib/table.utils.ts b/src/lib/table.utils.ts
index 040266f..e17b13d 100644
--- a/src/lib/table.utils.ts
+++ b/src/lib/table.utils.ts
@@ -84,6 +84,19 @@ export function fillParameters(
}, {});
}
+export function traduceQueryStrings(
+ queryStrings: { name: string; columnKey: string }[],
+ item: any
+) {
+ return queryStrings.reduce((acc, queryString) => {
+ const value = getKeyValue(item, queryString.columnKey);
+ if (queryString.name === "" || value === undefined) {
+ return [...acc];
+ }
+ return [...acc, { name: queryString.name, value }];
+ }, [] as { name: string; value: any }[]);
+}
+
export function getFilterFunction(
columnKey: string,
filters: FilterProperties[]
diff --git a/src/types/editor/card-types.ts b/src/types/editor/card-types.ts
new file mode 100644
index 0000000..71a0cc9
--- /dev/null
+++ b/src/types/editor/card-types.ts
@@ -0,0 +1,133 @@
+import { Alignment } from "@/components/shared/AlignmentProperty";
+import { Color } from "./style-types";
+import { FontType } from "@/components/shared/FontTypeProperty";
+import { ImageBorder } from "@/components/shared/ImageBorderProperty";
+import { IconType } from "./icon-types";
+import { QueryData } from "../connections";
+import { FastboardHeaderPhotoSize } from "./header-types";
+
+export enum CardComponentType {
+ Text = "Text",
+ Image = "Image",
+ Link = "Link",
+ Video = "Video",
+ Spacer = "Spacer",
+}
+
+interface BaseCardComponentProperties {
+ type: CardComponentType;
+ dataKey: string;
+}
+
+export interface TextComponentProperties extends BaseCardComponentProperties {
+ label: string;
+ defaultText: string;
+ alignment: Alignment;
+ fontSize: number;
+ fontTypes: FontType[];
+ textColor: Color;
+ labelColor: Color;
+}
+
+export interface ImageComponentProperties extends BaseCardComponentProperties {
+ alignment: Alignment;
+ border: ImageBorder;
+ size: FastboardHeaderPhotoSize;
+}
+
+export interface LinkComponentProperties extends BaseCardComponentProperties {
+ label: string;
+ defaultText: string;
+ isExternal: boolean;
+ externalIcon: IconType;
+ showExternalIcon: boolean;
+ alignment: Alignment;
+ fontSize: number;
+ textColor: Color;
+ labelColor: Color;
+}
+
+export interface VideoComponentProperties extends BaseCardComponentProperties {}
+
+export interface SpacerComponentProperties extends BaseCardComponentProperties {
+ height: number;
+}
+
+export type CardComponentProperties =
+ | TextComponentProperties
+ | ImageComponentProperties
+ | LinkComponentProperties
+ | VideoComponentProperties
+ | SpacerComponentProperties;
+
+export class DefaultCardComponentProperties {
+ static of(type: CardComponentType): CardComponentProperties {
+ const baseProperties = {
+ type,
+ dataKey: "",
+ };
+ switch (type) {
+ case CardComponentType.Text:
+ return {
+ ...baseProperties,
+ label: "",
+ defaultText: "Some text",
+ alignment: Alignment.Left,
+ fontSize: 18,
+ fontTypes: [],
+ textColor: new Color("#000000", "#ffffff"),
+ labelColor: new Color("#000000", "#ffffff"),
+ };
+ case CardComponentType.Image:
+ return {
+ ...baseProperties,
+ alignment: Alignment.Left,
+ border: ImageBorder.Round,
+ size: FastboardHeaderPhotoSize.Medium,
+ };
+ case CardComponentType.Link:
+ return {
+ ...baseProperties,
+ label: "",
+ defaultText: "https://fastboard-xgski.ondigitalocean.app/",
+ isExternal: true,
+ externalIcon: IconType.Link,
+ showExternalIcon: false,
+ alignment: Alignment.Left,
+ fontSize: 18,
+ textColor: Color.primary(),
+ labelColor: new Color("#000000", "#ffffff"),
+ };
+ case CardComponentType.Video:
+ return {
+ ...baseProperties,
+ };
+ case CardComponentType.Spacer:
+ return {
+ ...baseProperties,
+ height: 5,
+ };
+ default:
+ return {
+ ...baseProperties,
+ alignment: Alignment.Left,
+ border: ImageBorder.Round,
+ };
+ }
+ }
+}
+
+export class CardProperties {
+ sourceQueryData: QueryData | null = null;
+ queryParameters: Record = {};
+ components: CardComponentProperties[] = [];
+ dataKeys: string[] = [];
+
+ spacing: number = 2;
+ backgroundColor: Color = new Color("#ffffff", "#18181b");
+ showShadow: boolean = true;
+
+ static default(): CardProperties {
+ return new CardProperties();
+ }
+}
diff --git a/src/types/editor/header-types.ts b/src/types/editor/header-types.ts
index 1846c5a..2f11ab9 100644
--- a/src/types/editor/header-types.ts
+++ b/src/types/editor/header-types.ts
@@ -1,11 +1,6 @@
+import { Alignment } from "@/components/shared/AlignmentProperty";
import { Color } from "./style-types";
-export enum FastboardHeaderPosition {
- Left = "start",
- Center = "center",
- Right = "end",
-}
-
export enum FastboardHeaderFontSize {
Small = "x-large",
Medium = "xx-large",
@@ -39,7 +34,7 @@ export class FastboardHeaderProperties {
size: FastboardHeaderPhotoSize.Medium,
};
showThemeSwitcher: boolean = false;
- position: FastboardHeaderPosition = FastboardHeaderPosition.Center;
+ position: Alignment = Alignment.Center;
divider: boolean = false;
backgroundColor: Color = new Color("#ffffff", "#000000");
textColor: Color = new Color("#11181C", "#ECEDEE");
diff --git a/src/types/editor/icon-types.ts b/src/types/editor/icon-types.ts
index c2af540..98955bf 100644
--- a/src/types/editor/icon-types.ts
+++ b/src/types/editor/icon-types.ts
@@ -44,6 +44,7 @@ export enum IconType {
Heart = "Heart",
Hierarchy = "Hierarchy",
Home = "Home",
+ Link = "Link",
Folder = "Folder",
Trash = "Trash",
User = "User",
diff --git a/src/types/editor/index.ts b/src/types/editor/index.ts
index c38c375..ab3f061 100644
--- a/src/types/editor/index.ts
+++ b/src/types/editor/index.ts
@@ -12,6 +12,7 @@ export enum ComponentType {
Image = "image",
GroupChart = "group-chart",
Form = "form",
+ Card = "card",
Cards = "cards",
Sidebar = "sidebar",
Header = "header",
@@ -49,6 +50,11 @@ export class DashboardAuth {
}
}
+export interface Page {
+ layouts: Layout[];
+ returnPage: string | undefined;
+}
+
export class DashboardMetadata {
components: Record = {};
header: { componentId: ComponentId | null; isVisible: boolean } = {
@@ -57,8 +63,11 @@ export class DashboardMetadata {
};
sidebar: { id: ComponentId; visible: boolean } | null = null;
modals: ModalFrame[] = [];
- pages: Record = {
- home: [Layout.of(LayoutType.Full)],
+ pages: Record = {
+ home: {
+ layouts: [Layout.of(LayoutType.Full)],
+ returnPage: undefined,
+ },
};
auth: DashboardAuth = DashboardAuth.default();
pageTitle: string = "";
diff --git a/src/types/editor/table-types.ts b/src/types/editor/table-types.ts
index 25aee7a..19f3b0d 100644
--- a/src/types/editor/table-types.ts
+++ b/src/types/editor/table-types.ts
@@ -15,9 +15,12 @@ export interface TableActionProperty {
key: string;
label: string;
type: "view" | "edit" | "delete";
+ mode?: "modal" | "page";
query: QueryData | null;
parameters: { name: string; columnKey: string; value: string }[];
modalId?: string;
+ pageId?: string;
+ queryStrings: { name: string; columnKey: string }[];
}
export interface AddRowFormProperties {