From 60f7927def3f9577fdf1f20fcb993146215c0a5d Mon Sep 17 00:00:00 2001 From: nael Date: Mon, 20 May 2024 23:38:43 +0200 Subject: [PATCH 1/2] :sparkles: Field mapping updates --- .../app/(Dashboard)/configuration/page.tsx | 100 ++++--- .../FieldMappings/FieldMappingsTable.tsx | 100 +++++-- .../Configuration/FieldMappings/columns.tsx | 266 +++++++++--------- .../FieldMappings/defineForm.tsx | 217 ++++++++++++++ .../Configuration/FieldMappings/mapForm.tsx | 263 +++++++++++++++++ .../Configuration/LinkedUsers/columns.tsx | 25 -- .../Configuration/Webhooks/columns.tsx | 4 +- .../src/components/Connection/columns.tsx | 27 -- .../src/components/Events/columns.tsx | 27 -- .../shared/data-table-faceted-filter.tsx | 5 +- .../shared/data-table-webhook-scopes.tsx | 25 ++ .../src/hooks/get/useProviderProperties.tsx | 2 +- .../field-mapping/field-mapping.service.ts | 23 +- packages/shared/src/utils.ts | 2 +- 14 files changed, 796 insertions(+), 290 deletions(-) create mode 100644 apps/client-ts/src/components/Configuration/FieldMappings/defineForm.tsx create mode 100644 apps/client-ts/src/components/Configuration/FieldMappings/mapForm.tsx diff --git a/apps/client-ts/src/app/(Dashboard)/configuration/page.tsx b/apps/client-ts/src/app/(Dashboard)/configuration/page.tsx index 17183a098..005a1bef4 100644 --- a/apps/client-ts/src/app/(Dashboard)/configuration/page.tsx +++ b/apps/client-ts/src/app/(Dashboard)/configuration/page.tsx @@ -1,5 +1,4 @@ 'use client' - import { Card, CardContent, @@ -7,11 +6,6 @@ import { CardHeader, CardTitle, } from "@/components/ui/card" -import { - Dialog, - DialogContent, - DialogTrigger, -} from "@/components/ui/dialog" import { Tabs, TabsContent, @@ -19,9 +13,6 @@ import { TabsTrigger, } from "@/components/ui/tabs" import { LinkedUsersPage } from "@/components/Configuration/LinkedUsers/LinkedUsersPage"; -import { Button } from "@/components/ui/button"; -import { PlusCircledIcon } from "@radix-ui/react-icons"; -import { FModal } from "@/components/Configuration/FieldMappings/FieldMappingModal" import { Separator } from "@/components/ui/separator"; import FieldMappingsTable from "@/components/Configuration/FieldMappings/FieldMappingsTable"; import AddLinkedAccount from "@/components/Configuration/LinkedUsers/AddLinkedAccount"; @@ -32,16 +23,15 @@ import { useState } from "react"; import AddWebhook from "@/components/Configuration/Webhooks/AddWebhook"; import { WebhooksPage } from "@/components/Configuration/Webhooks/WebhooksPage"; import useWebhooks from "@/hooks/get/useWebhooks"; -import { usePostHog } from 'posthog-js/react' -import config from "@/lib/config"; -import useProjectStore from "@/state/projectStore"; import useConnectionStrategies from "@/hooks/get/useConnectionStrategies"; -import { extractAuthMode,extractProvider,extractVertical} from '@panora/shared' import { Heading } from "@/components/ui/heading"; import CustomConnectorPage from "@/components/Configuration/Connector/CustomConnectorPage"; +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; +import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; +import { HelpCircle } from "lucide-react"; +import { Button } from "@/components/ui/button"; export default function Page() { - const {idProject} = useProjectStore(); const { data: linkedUsers, isLoading, error } = useLinkedUsers(); const { data: webhooks, isLoading: isWebhooksLoading, error: isWebhooksError } = useWebhooks(); @@ -49,11 +39,6 @@ export default function Page() { const { data: mappings, isLoading: isFieldMappingsLoading, error: isFieldMappingsError } = useFieldMappings(); const [open, setOpen] = useState(false); - const handleClose = () => { - setOpen(false); - }; - - const posthog = usePostHog() if(error){ console.log("error linked users.."); @@ -87,7 +72,7 @@ export default function Page() { standard_object: mapping.ressource_owner_type, source_app: mapping.source, status: mapping.status, - category: mapping.ressource_owner_type, + category: mapping.ressource_owner_type.split('.')[0], source_field: mapping.remote_id, destination_field: mapping.slug, data_type: mapping.data_type, @@ -121,7 +106,31 @@ export default function Page() { - Your Linked Accounts + + Your Linked Accounts + + + + + + + + + +
+
+

What are linked accounts ?

+
+
+
+
+ Help +
+
+
You connected {linkedUsers ? linkedUsers.length : } linked accounts. @@ -136,33 +145,36 @@ export default function Page() {
- - - - - - - - - Your Fields Mapping + + Your Fields Mappings + + + + + + + + + +
+
+

What are field mappings ?

+
+
+
+
+ Help +
+
+
You built {mappings ? mappings.length : } fields mappings. - Learn more about custom field mappings + Learn more about custom field mappings in our docs !
diff --git a/apps/client-ts/src/components/Configuration/FieldMappings/FieldMappingsTable.tsx b/apps/client-ts/src/components/Configuration/FieldMappings/FieldMappingsTable.tsx index 6a62af78a..21658ec99 100644 --- a/apps/client-ts/src/components/Configuration/FieldMappings/FieldMappingsTable.tsx +++ b/apps/client-ts/src/components/Configuration/FieldMappings/FieldMappingsTable.tsx @@ -1,13 +1,25 @@ import { - Card, - CardContent, - CardHeader, - CardTitle, - } from "@/components/ui/card" + Card, + CardContent, + CardHeader, + CardTitle, +} from "@/components/ui/card" import { DataTable } from "@/components/shared/data-table"; -import { columns } from "./columns"; +import { useColumns } from "./columns"; import { DataTableLoading } from "@/components/shared/data-table-loading"; - +import { Button } from "@/components/ui/button"; +import { PlusCircledIcon } from "@radix-ui/react-icons"; +import { usePostHog } from "posthog-js/react"; +import useProjectStore from "@/state/projectStore"; +import config from "@/lib/config"; +import { + Dialog, + DialogContent, + DialogTrigger, +} from "@/components/ui/dialog" +import { DefineForm } from "./defineForm"; +import { useState } from "react"; +import { MapForm } from "./mapForm"; export interface Mapping { standard_object: string; source_app: string; @@ -20,8 +32,19 @@ export interface Mapping { export default function FieldMappingsTable({ mappings, - isLoading + isLoading }: { mappings: Mapping[] | undefined; isLoading: boolean }) { + const [defineOpen, setDefineOpen] = useState(false); + const [mapOpen, setMapOpen] = useState(false); + const columns = useColumns(); + const handleDefineClose = () => { + setDefineOpen(false); + }; + const handleMapClose = () => { + setMapOpen(false); + }; + const posthog = usePostHog() + const {idProject} = useProjectStore(); const countDefined = mappings?.filter(mapping => mapping.status === "defined").length; const countMapped = mappings?.filter(mapping => mapping.status === "mapped").length; @@ -32,22 +55,65 @@ export default function FieldMappingsTable({ <>
- - - Defined - - -

{countDefined}

-
- + + + Defined + + +

{countDefined}

+
+ + + + + + + +
- + Mapped

{countMapped}

+ + + + + + + +
{mappings && } diff --git a/apps/client-ts/src/components/Configuration/FieldMappings/columns.tsx b/apps/client-ts/src/components/Configuration/FieldMappings/columns.tsx index a2b432687..ad1b59911 100644 --- a/apps/client-ts/src/components/Configuration/FieldMappings/columns.tsx +++ b/apps/client-ts/src/components/Configuration/FieldMappings/columns.tsx @@ -1,146 +1,156 @@ "use client" import { ColumnDef } from "@tanstack/react-table" - import { Badge } from "@/components/ui/badge" -import { Checkbox } from "@/components/ui/checkbox" - import { DataTableColumnHeader } from "../../shared/data-table-column-header" import { DataTableRowActions } from "../../shared/data-table-row-actions" import { Mapping } from "./schema" +import { useState } from "react" +import { Button } from "@/components/ui/button" +import { Dialog, DialogContent } from "@/components/ui/dialog" +import { MapForm } from "./mapForm" -export const columns: ColumnDef[] = [ - { - id: "select", - header: ({ table }) => ( - table.toggleAllPageRowsSelected(!!value)} - aria-label="Select all" - className="translate-y-[2px]" - /> - ), - cell: ({ row }) => ( - row.toggleSelected(!!value)} - aria-label="Select row" - className="translate-y-[2px]" - /> - ), - enableSorting: false, - enableHiding: false, - }, - { - accessorKey: "standard_object", - header: ({ column }) => ( - - ), - cell: ({ row }) =>{ +export function useColumns() { + const [mapOpen, setMapOpen] = useState(false); + const [currentRow, setCurrentRow] = useState(null); - return ( -
- {row.getValue("standard_object")} -
- ) - }, - enableSorting: false, - enableHiding: false, - }, - { - accessorKey: "source_app", - header: ({ column }) => ( - - ), - cell: ({ row }) => { - return ( -
- {row.getValue("source_app")} -
- ) + const handleMapClick = (row: Mapping) => { + setCurrentRow(row); + setMapOpen(true); + }; + + const handleClose = () => { + setMapOpen(false); + setCurrentRow(null); + }; + + return [ + { + accessorKey: "_", + cell: ({ row }) => ( + !row.getValue("source_app") && !row.getValue("source_field") && <> + + + + {currentRow && } + + + + ), }, - }, - { - accessorKey: "status", - header: ({ column }) => ( - - ), - cell: ({ row }) => { - return ( -
- {row.getValue("status")} -
- ) + { + accessorKey: "standard_object", + header: ({ column }) => ( + + ), + cell: ({ row }) =>{ + + return ( +
+ {row.getValue("standard_object")} +
+ ) + }, + enableSorting: false, + enableHiding: false, }, - filterFn: (row, id, value) => { - return value.includes(row.getValue(id)) + { + accessorKey: "source_app", + header: ({ column }) => ( + + ), + cell: ({ row }) => { + return ( +
+ {(row.getValue("source_app") as string) && {row.getValue("source_app")}} +
+ ) + }, }, - }, - { - accessorKey: "category", - header: ({ column }) => ( - - ), - cell: ({ row }) => { - return ( -
- {row.getValue("category")} -
- ) + { + accessorKey: "status", + header: ({ column }) => ( + + ), + cell: ({ row }) => { + return ( +
+ {row.getValue("status")} +
+ ) + }, + filterFn: (row, id, value) => { + return value.includes(row.getValue(id)) + }, }, - filterFn: (row, id, value) => { - return value.includes(row.getValue(id)) + { + accessorKey: "category", + header: ({ column }) => ( + + ), + cell: ({ row }) => { + return ( +
+ {row.getValue("category")} +
+ ) + }, + filterFn: (row, id, value) => { + return value.includes(row.getValue(id)) + }, }, - }, - { - accessorKey: "source_field", - header: ({ column }) => ( - - ), - cell: ({ row }) => { - return ( -
- {row.getValue("source_field")} -
- ) + { + accessorKey: "source_field", + header: ({ column }) => ( + + ), + cell: ({ row }) => { + return ( +
+ {(row.getValue("source_field") as string) && {row.getValue("source_field")}} +
+ ) + }, + filterFn: (row, id, value) => { + return value.includes(row.getValue(id)) + }, }, - filterFn: (row, id, value) => { - return value.includes(row.getValue(id)) + { + accessorKey: "destination_field", + header: ({ column }) => ( + + ), + cell: ({ row }) => { + + return ( +
+ {row.getValue("destination_field")} +
+ ) + }, }, - }, - { - accessorKey: "destination_field", - header: ({ column }) => ( - - ), - cell: ({ row }) => { - - return ( -
- {row.getValue("destination_field")} -
- ) + { + accessorKey: "data_type", + header: ({ column }) => ( + + ), + cell: ({ row }) => { + return ( +
+ {row.getValue("data_type")} +
+ ) + }, }, - }, - { - accessorKey: "data_type", - header: ({ column }) => ( - - ), - cell: ({ row }) => { - - return ( -
- {row.getValue("data_type")} -
- ) + { + id: "actions", + cell: ({ row }) => , }, - }, - { - id: "actions", - cell: ({ row }) => , - }, -] \ No newline at end of file + ] as ColumnDef[]; +} \ No newline at end of file diff --git a/apps/client-ts/src/components/Configuration/FieldMappings/defineForm.tsx b/apps/client-ts/src/components/Configuration/FieldMappings/defineForm.tsx new file mode 100644 index 000000000..3a3f344e9 --- /dev/null +++ b/apps/client-ts/src/components/Configuration/FieldMappings/defineForm.tsx @@ -0,0 +1,217 @@ +/* eslint-disable react/no-unescaped-entities */ +'use client' + +import { Button } from "@/components/ui/button" +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "@/components/ui/card" +import { Input } from "@/components/ui/input" +import { + Tabs, + TabsContent, + TabsList, + TabsTrigger, +} from "@/components/ui/tabs" +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form" +import { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" +import useMapField from "@/hooks/create/useMapField" +import { useEffect, useState } from "react" +import useFieldMappings from "@/hooks/get/useFieldMappings" +import useProviderProperties from "@/hooks/get/useProviderProperties" +import { standardObjects } from "@panora/shared/src/standardObjects" +import useProjectStore from "@/state/projectStore" +import useLinkedUsers from "@/hooks/get/useLinkedUsers" +import { zodResolver } from "@hookform/resolvers/zod" +import { useForm } from "react-hook-form" +import * as z from "zod" +import { usePostHog } from 'posthog-js/react' +import config from "@/lib/config" +import { CRM_PROVIDERS } from "@panora/shared" +import useDefineField from "@/hooks/create/useDefineField" + + +const defineFormSchema = z.object({ + standardModel: z.string().min(2, { + message: "standardModel must be at least 2 characters.", + }), + fieldName: z.string().min(2, { + message: "fieldName must be at least 2 characters.", + }), + fieldDescription: z.string().min(2, { + message: "fieldDescription must be at least 2 characters.", + }), + fieldType: z.string().min(2, { + message: "fieldType must be at least 2 characters.", + }), +}) + + +export function DefineForm({ onClose }: {onClose: () => void}) { + + const defineForm = useForm>({ + resolver: zodResolver(defineFormSchema), + defaultValues: { + standardModel: "", + fieldName: "", + fieldDescription: "", + fieldType: "", + }, + }) + + const {idProject} = useProjectStore(); + + const { data: mappings } = useFieldMappings(); + const { mutate: mutateDefineField } = useDefineField(); + + + const posthog = usePostHog() + + + function onDefineSubmit(values: z.infer) { + console.log(values) + mutateDefineField({ + object_type_owner: values.standardModel, + name: values.fieldName, + description: values.fieldDescription, + data_type: values.fieldType, + }); + posthog?.capture("field_defined", { + id_project: idProject, + mode: config.DISTRIBUTION + }) + onClose(); + } + return ( +
+ + + Define a custom field + + Create a custom field in Panora to extend our unified objects. Once done, you can map this field to existing fields in your end-user's software. Find details in + documentation. + + + +
+ ( + + What object to you want to extend? + + + + + + )} + /> +
+
+ ( + + Give your Custom Field an identifier + + + + + + )} + /> +
+
+ ( + + Short Description + + + + + + )} + /> +
+
+ ( + + Data Type + + + + + + )} + /> +
+
+ + + +
+ + ) +} + \ No newline at end of file diff --git a/apps/client-ts/src/components/Configuration/FieldMappings/mapForm.tsx b/apps/client-ts/src/components/Configuration/FieldMappings/mapForm.tsx new file mode 100644 index 000000000..3075b470b --- /dev/null +++ b/apps/client-ts/src/components/Configuration/FieldMappings/mapForm.tsx @@ -0,0 +1,263 @@ +/* eslint-disable react/no-unescaped-entities */ +'use client' + +import { Button } from "@/components/ui/button" +import { + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "@/components/ui/card" +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form" +import { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" +import useMapField from "@/hooks/create/useMapField" +import { useEffect, useState } from "react" +import useFieldMappings from "@/hooks/get/useFieldMappings" +import useProviderProperties from "@/hooks/get/useProviderProperties" +import useProjectStore from "@/state/projectStore" +import useLinkedUsers from "@/hooks/get/useLinkedUsers" +import { zodResolver } from "@hookform/resolvers/zod" +import { useForm } from "react-hook-form" +import * as z from "zod" +import { usePostHog } from 'posthog-js/react' +import config from "@/lib/config" +import { CRM_PROVIDERS, providersArray } from "@panora/shared" + +const mapFormSchema = z.object({ + attributeId: z.string().min(2, { + message: "attributeId must be at least 2 characters.", + }), + sourceCustomFieldId: z.string().min(2, { + message: "sourceCustomFieldId must be at least 2 characters.", + }), + sourceProvider: z.string().min(2, { + message: "sourceProvider must be at least 2 characters.", + }), + linkedUserId: z.string().min(2, { + message: "linkedUserId must be at least 2 characters.", + }), +}) + +export function MapForm({ onClose, fieldToMap }: {onClose: () => void; fieldToMap?: string}) { + const mapForm = useForm>({ + resolver: zodResolver(mapFormSchema), + defaultValues: { + attributeId: "", + sourceCustomFieldId: fieldToMap || "", + sourceProvider: "", + linkedUserId: "" + }, + }) + + const [sourceCustomFieldsData, setSourceCustomFieldsData] = useState[]>([]); + const [linkedUserId, sourceProvider] = mapForm.watch(['linkedUserId', 'sourceProvider']); + const [connectorVertical, setConnectorVertical] = useState(""); + + const {idProject} = useProjectStore(); + + const { data: mappings } = useFieldMappings(); + const { mutate: mutateMapField } = useMapField(); + const { data: linkedUsers } = useLinkedUsers(); + const { data: sourceCustomFields, error, isLoading } = useProviderProperties(linkedUserId, sourceProvider, connectorVertical); + const connectors = providersArray(); + const posthog = usePostHog() + + useEffect(() => { + if (sourceCustomFields && sourceCustomFields.data.length > 0 && !isLoading && !error) { + setSourceCustomFieldsData(sourceCustomFields.data); + } + }, [sourceCustomFields, isLoading, error]); + + const handleProviderChange = (provider: string, vertical: string) => { + mapForm.setValue("sourceProvider", provider); + setConnectorVertical(vertical); + } + + function onMapSubmit(values: z.infer) { + mutateMapField({ + attributeId: values.attributeId.trim(), + source_custom_field_id: values.sourceCustomFieldId, + source_provider: values.sourceProvider, + linked_user_id: values.linkedUserId, + }); + posthog?.capture("field_mapped", { + id_project: idProject, + mode: config.DISTRIBUTION + }) + onClose(); + } + + return ( +
+ + + Map Field + + Field Mapping allows you to map data from your users' platforms to custom fields on your Panora Unified Models. + + + +
+ ( + + Panora Custom Field + + + + + + )} + /> +
+
+ ( + + Linked User Id + + + + + This is the id of the user in your system. + + + + )} + /> +
+
+ ( + + Provider + + + + + This is the source provider where the field exists. + + + + )} + /> +
+
+ ( + + Origin Source Field + + + + + These are all the fields we found in your customer's software. + + + + )} + /> +
+
+ + + +
+ + ) +} \ No newline at end of file diff --git a/apps/client-ts/src/components/Configuration/LinkedUsers/columns.tsx b/apps/client-ts/src/components/Configuration/LinkedUsers/columns.tsx index 4af3a2ec7..36528c49d 100644 --- a/apps/client-ts/src/components/Configuration/LinkedUsers/columns.tsx +++ b/apps/client-ts/src/components/Configuration/LinkedUsers/columns.tsx @@ -2,33 +2,8 @@ import { ColumnDef } from "@tanstack/react-table"; import { ColumnLU } from "./schema"; import { DataTableColumnHeader } from "@/components/shared/data-table-column-header"; import { Badge } from "@/components/ui/badge"; -import { Checkbox } from "@/components/ui/checkbox"; export const columns: ColumnDef[] = [ - { - id: "select", - header: ({ table }) => ( - table.toggleAllPageRowsSelected(!!value)} - aria-label="Select all" - className="translate-y-[2px]" - /> - ), - cell: ({ row }) => ( - row.toggleSelected(!!value)} - aria-label="Select row" - className="translate-y-[2px]" - /> - ), - enableSorting: false, - enableHiding: false, - }, { accessorKey: "linked_user_id", header: ({ column }) => ( diff --git a/apps/client-ts/src/components/Configuration/Webhooks/columns.tsx b/apps/client-ts/src/components/Configuration/Webhooks/columns.tsx index f343ccaf3..8ea70fe30 100644 --- a/apps/client-ts/src/components/Configuration/Webhooks/columns.tsx +++ b/apps/client-ts/src/components/Configuration/Webhooks/columns.tsx @@ -58,7 +58,7 @@ export function useColumns(webhooks: Webhook[] | undefined, setWebhooks: React.D {row.getValue("url")} , @@ -120,7 +120,7 @@ export function useColumns(webhooks: Webhook[] | undefined, setWebhooks: React.D {row.getValue("endpoint_description")} , diff --git a/apps/client-ts/src/components/Connection/columns.tsx b/apps/client-ts/src/components/Connection/columns.tsx index a5d6f9fdb..ffc3be2ac 100644 --- a/apps/client-ts/src/components/Connection/columns.tsx +++ b/apps/client-ts/src/components/Connection/columns.tsx @@ -1,10 +1,7 @@ "use client" import { ColumnDef } from "@tanstack/react-table" - import { Badge } from "@/components/ui/badge" -import { Checkbox } from "@/components/ui/checkbox" - import { Connection } from "./schema" import { DataTableColumnHeader } from "./../shared/data-table-column-header" import React from "react" @@ -70,30 +67,6 @@ const connectionTokenComponent = ({row}:{row:any}) => { } export const columns: ColumnDef[] = [ - { - id: "select", - header: ({ table }) => ( - table.toggleAllPageRowsSelected(!!value)} - aria-label="Select all" - className="translate-y-[2px]" - /> - ), - cell: ({ row }) => ( - row.toggleSelected(!!value)} - aria-label="Select row" - className="translate-y-[2px]" - /> - ), - enableSorting: false, - enableHiding: false, - }, /*{ accessorKey: "organisation", header: ({ column }) => ( diff --git a/apps/client-ts/src/components/Events/columns.tsx b/apps/client-ts/src/components/Events/columns.tsx index 27b8f8d06..1cf6f8072 100644 --- a/apps/client-ts/src/components/Events/columns.tsx +++ b/apps/client-ts/src/components/Events/columns.tsx @@ -1,39 +1,12 @@ "use client" import { ColumnDef } from "@tanstack/react-table" - import { Badge } from "@/components/ui/badge" -import { Checkbox } from "@/components/ui/checkbox" - import { DataTableColumnHeader } from "../shared/data-table-column-header" import { Event } from "./schema" import { getLogoURL } from "@panora/shared" export const columns: ColumnDef[] = [ - { - id: "select", - header: ({ table }) => ( - table.toggleAllPageRowsSelected(!!value)} - aria-label="Select all" - className="translate-y-[2px]" - /> - ), - cell: ({ row }) => ( - row.toggleSelected(!!value)} - aria-label="Select row" - className="translate-y-[2px]" - /> - ), - enableSorting: false, - enableHiding: false, - }, { accessorKey: "method", header: ({ column }) => ( diff --git a/apps/client-ts/src/components/shared/data-table-faceted-filter.tsx b/apps/client-ts/src/components/shared/data-table-faceted-filter.tsx index d3a11c9ef..af4a2b246 100644 --- a/apps/client-ts/src/components/shared/data-table-faceted-filter.tsx +++ b/apps/client-ts/src/components/shared/data-table-faceted-filter.tsx @@ -24,11 +24,12 @@ export function DataTableFacetedFilter({ title, field }: { title?: string, field const [inputValue, setInputValue] = React.useState(""); const [selectedValues, setSelectedValues] = React.useState>(new Set()); useEffect(() => { - if(field.value !== " "){ + if (field.value && field.value.trim()!== "") { setSelectedValues(new Set(field.value.split(' '))); + } else { + setSelectedValues(new Set()); } }, [field.value]); - const handleAddScope = () => { if (inputValue && !selectedValues.has(inputValue)) { setSelectedValues(new Set([...selectedValues, inputValue])); diff --git a/apps/client-ts/src/components/shared/data-table-webhook-scopes.tsx b/apps/client-ts/src/components/shared/data-table-webhook-scopes.tsx index 1eede3f74..0bb57e399 100644 --- a/apps/client-ts/src/components/shared/data-table-webhook-scopes.tsx +++ b/apps/client-ts/src/components/shared/data-table-webhook-scopes.tsx @@ -24,6 +24,7 @@ import { scopes } from "@panora/shared" export function DataTableFacetedFilterWebhook({ title, field }: { title?: string, field: any }) { const [inputValue, setInputValue] = React.useState(""); const [selectedValues, setSelectedValues] = React.useState>(new Set()); + const [selectedAll, setSelectedAll] = React.useState(false); useEffect(() => { if (field.value && field.value.trim() !== "") { @@ -52,6 +53,18 @@ export function DataTableFacetedFilterWebhook({ title, field }: { title?: string field.onChange(''); }; + const handleSelectAll = () => { + if (selectedValues.size === scopes.length) { + setSelectedAll(false); + handleClearScopes(); + } else { + const allScopes = new Set(scopes); + setSelectedValues(allScopes); + field.onChange(Array.from(allScopes).join(' ')); + setSelectedAll(true); + } + }; + const filteredScopes = scopes.filter(scope => scope.toLowerCase().includes(inputValue.toLowerCase()) && !selectedValues.has(scope) ); @@ -99,6 +112,18 @@ export function DataTableFacetedFilterWebhook({ title, field }: { title?: string + +
+ {selectedAll && } +
+ Select All +
{Array.from(selectedValues).map((value) => ( Date: Tue, 21 May 2024 14:06:40 +0200 Subject: [PATCH 2/2] :sparkles: Added widget handling --- .../src/app/(Dashboard)/api-keys/page.tsx | 2 - .../app/(Dashboard)/configuration/page.tsx | 56 ++++- .../src/components/ApiKeys/columns.tsx | 10 - .../src/components/ApiKeys/schema.ts | 1 - .../Configuration/Catalog/CatalogWidget.tsx | 130 ++++++++++++ .../Configuration/Catalog/CopySnippet.tsx | 199 ++++++++++++++++++ .../Connector/CustomConnectorPage.tsx | 2 +- .../Connector/VerticalSelector.tsx | 23 +- .../Configuration/FieldMappings/mapForm.tsx | 3 +- .../LinkedUsers/AddLinkedAccount.tsx | 2 - .../Configuration/Webhooks/columns.tsx | 4 +- .../src/components/Connection/columns.tsx | 38 +--- .../src/components/Events/columns.tsx | 53 +---- .../src/components/ui/loading-spinner.tsx | 19 ++ apps/client-ts/src/lib/utils.ts | 37 ++++ apps/client-ts/src/state/verticalStore.ts | 2 +- 16 files changed, 466 insertions(+), 115 deletions(-) create mode 100644 apps/client-ts/src/components/Configuration/Catalog/CatalogWidget.tsx create mode 100644 apps/client-ts/src/components/Configuration/Catalog/CopySnippet.tsx create mode 100644 apps/client-ts/src/components/ui/loading-spinner.tsx diff --git a/apps/client-ts/src/app/(Dashboard)/api-keys/page.tsx b/apps/client-ts/src/app/(Dashboard)/api-keys/page.tsx index 3b6613cdd..def0960d4 100644 --- a/apps/client-ts/src/app/(Dashboard)/api-keys/page.tsx +++ b/apps/client-ts/src/app/(Dashboard)/api-keys/page.tsx @@ -47,7 +47,6 @@ interface TSApiKeys { id_api_key: string; name : string; token : string; - created : string; } export default function Page() { @@ -66,7 +65,6 @@ export default function Page() { id_api_key: key.id_api_key, name: key.name || "", token: key.api_key_hash, - created: new Date().toISOString() })) setTSApiKeys(temp_tsApiKeys) },[apiKeys]) diff --git a/apps/client-ts/src/app/(Dashboard)/configuration/page.tsx b/apps/client-ts/src/app/(Dashboard)/configuration/page.tsx index 005a1bef4..33066589c 100644 --- a/apps/client-ts/src/app/(Dashboard)/configuration/page.tsx +++ b/apps/client-ts/src/app/(Dashboard)/configuration/page.tsx @@ -30,6 +30,9 @@ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/comp import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import { HelpCircle } from "lucide-react"; import { Button } from "@/components/ui/button"; +import { LoadingSpinner } from "@/components/ui/loading-spinner"; +import { CatalogWidget } from "@/components/Configuration/Catalog/CatalogWidget"; +import { CopySnippet } from "@/components/Configuration/Catalog/CopySnippet"; export default function Page() { @@ -97,6 +100,9 @@ export default function Page() { Webhooks + + Manage Catalog Widget + Manage Connectors @@ -121,8 +127,10 @@ export default function Page() {
-
-

What are linked accounts ?

+
+

What are linked accounts ?

+

The linked-user object represents your end-user entity inside our system.

+

It is a mirror of the end-user that exist in your backend. It helps Panora have the same source of truth about your user’s information.

@@ -131,8 +139,8 @@ export default function Page() { - - You connected {linkedUsers ? linkedUsers.length : } linked accounts. + + You connected {linkedUsers ? linkedUsers.length : } linked accounts. @@ -162,8 +170,18 @@ export default function Page() {
-
-

What are field mappings ?

+
+

What are field mappings ?

+

+ By default, our unified models are predefined as you can see in the API reference.
+

+

Now with field mappings, you have the option to map your custom fields (that may exist in your end-customer's tools) to our unified model !

+

+ It is done in 2 steps. First you must define your custom field so it is recognized by Panora. Lastly, you must map this field to your remote field that exist in a 3rd party. +

+

+
That way, Panora can retrieve the newly created custom field directly within the unified model. +

@@ -172,8 +190,8 @@ export default function Page() { - - You built {mappings ? mappings.length : } fields mappings. + + You built {mappings ? mappings.length : } fields mappings. Learn more about custom field mappings in our docs ! @@ -191,8 +209,8 @@ export default function Page() { Your Webhooks - - You enabled {webhooks ? webhooks.length : } webhooks. + + You enabled {webhooks ? webhooks.length : } webhooks. Read more about webhooks from our documentation @@ -207,6 +225,24 @@ export default function Page() { + + +
+ + + + Customize Your Embedded Widget + + Select connectors you would like to have in the UI widget catalog. By default, they are all displayed. + + + + + + + +
+
diff --git a/apps/client-ts/src/components/ApiKeys/columns.tsx b/apps/client-ts/src/components/ApiKeys/columns.tsx index cb399e906..00183c51d 100644 --- a/apps/client-ts/src/components/ApiKeys/columns.tsx +++ b/apps/client-ts/src/components/ApiKeys/columns.tsx @@ -82,16 +82,6 @@ export function useColumns() { ); }, }, - { - accessorKey: "created", - header: ({ column }) => ( - - ), - cell: ({ row }) =>
{row.getValue("created")}
, - filterFn: (row, id, value) => { - return value.includes(row.getValue(id)) - }, - }, { id: "actions", cell: ({ row }) => , diff --git a/apps/client-ts/src/components/ApiKeys/schema.ts b/apps/client-ts/src/components/ApiKeys/schema.ts index b7fcd2442..dfb2acf32 100644 --- a/apps/client-ts/src/components/ApiKeys/schema.ts +++ b/apps/client-ts/src/components/ApiKeys/schema.ts @@ -4,7 +4,6 @@ export const apiKeySchema = z.object({ id_api_key: z.string(), name: z.string(), token: z.string(), - created: z.string(), }) export type ApiKey = z.infer \ No newline at end of file diff --git a/apps/client-ts/src/components/Configuration/Catalog/CatalogWidget.tsx b/apps/client-ts/src/components/Configuration/Catalog/CatalogWidget.tsx new file mode 100644 index 000000000..6d17b6062 --- /dev/null +++ b/apps/client-ts/src/components/Configuration/Catalog/CatalogWidget.tsx @@ -0,0 +1,130 @@ +import { ComponentProps, useState } from "react" +import { cn } from "@/lib/utils" +import { Badge } from "@/components/ui/badge" +import { ScrollArea } from "@/components/ui/scroll-area" +import { AuthStrategy, categoriesVerticals, Provider, providersArray } from "@panora/shared" +import { DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from "@/components/ui/dropdown-menu" +import { Button } from "@/components/ui/button" +import { ListFilter } from "lucide-react" +import { Card } from "@/components/ui/card" +import { Switch } from "@/components/ui/switch" +import { Label } from "@/components/ui/label" + +export const verticals = categoriesVerticals as string[]; + +export function CatalogWidget() { + const [vertical, setVertical] = useState("All") + const filteredConnectors = vertical === "All" + ? providersArray() + : providersArray(vertical); + + const handleCheckboxChange = (vertical: string) => { + setVertical(vertical); + }; + return ( + <> +
+ +
+ + + + + + Filter by + + handleCheckboxChange("All")} + > + All + + + {verticals.map((v) => ( + handleCheckboxChange(v)} + > + {v} + + ))} + + +
+ + + +
+ + +
+ {filteredConnectors.map((item) => ( + +
+
+
+ +
{`${item.name.substring(0, 1).toUpperCase()}${item.name.substring(1)}`}
+
+
+ {item.description!.substring(0, 300)} +
+
+ {item.vertical && + + {item.vertical} + + } + {item.authStrategy && + + {item.authStrategy} + + } +
+
+
+ disableWebhook(row.original.id_webhook_endpoint, !row.getValue('active')) } + /> +
+
+
+ + ))} +
+
+ + ) +} + +function getBadgeVariantFromLabel( + label?: string +): ComponentProps["variant"] { + if (label === AuthStrategy.oauth2) { + return "secondary" + } + return "default" +} diff --git a/apps/client-ts/src/components/Configuration/Catalog/CopySnippet.tsx b/apps/client-ts/src/components/Configuration/Catalog/CopySnippet.tsx new file mode 100644 index 000000000..084b9d49a --- /dev/null +++ b/apps/client-ts/src/components/Configuration/Catalog/CopySnippet.tsx @@ -0,0 +1,199 @@ +'use client' + +import { Button } from "@/components/ui/button"; +import { Card } from "@/components/ui/card"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + } from "@/components/ui/dialog" +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; +import { Copy } from "lucide-react"; +import { useState } from "react"; + +export const CopySnippet = () => { + const [open,setOpen] = useState(false); + const [copiedLeft, setCopiedLeft] = useState(false); + const [copiedRight, setCopiedRight] = useState(false); + + const handleClick = () => { + setOpen(true); + } + + const handleCopyLeft = () => { + navigator.clipboard.writeText( + ` + projectId={'c9a1b1f8-466d-442d-a95e-11cdd00baf49'} + returnUrl={'https://acme.inc'} + linkedUserId={'b860d6c1-28f9-485c-86cd-fb09e60f10a2'} + ` + ); + setCopiedLeft(true); + setTimeout(() => { + setCopiedLeft(false); + }, 2000); + }; + + const handleCopyRight = () => { + navigator.clipboard.writeText( + ` + name={'hubspot'} + vertical={'crm'} + projectId={'c9a1b1f8-466d-442d-a95e-11cdd00baf49'} + returnUrl={'https://acme.inc'} + linkedUserId={'b860d6c1-28f9-485c-86cd-fb09e60f10a2'} + ` + ); + setCopiedRight(true); + setTimeout(() => { + setCopiedRight(false); + }, 2000); + }; + + return ( + <> + + + + + Import UI catalog components + + You can either import the whole catalog or import a specific connector! More info in our docs. + + + +
+
+
Import the whole catalog
+
+ +
+ + + + + + +

Copy

+
+
+
+
+ + + + {``} + + + + projectId{`={'c9a1b1f8-466d-442d-a95e-11cdd00baf49'}`} + + + returnUrl{`={'https://acme.inc'}`} + + + linkedUserId{`={'b860d6c1-28f9-485c-86cd-fb09e60f10a2'}`} + + + + {``} + + + +
+
+
+
+
Import Specific Connector
+
+ +
+ + + + + + +

Copy

+
+
+
+
+ + + + {``} + + + + name{`={'hubspot'}`} + + + vertical{`={'crm'}`} + + + projectId{`={'c9a1b1f8-466d-442d-a95e-11cdd00baf49'}`} + + + returnUrl{`={'https://acme.inc'}`} + + + linkedUserId{`={'b860d6c1-28f9-485c-86cd-fb09e60f10a2'}`} + + + + {``} + + + +
+
+
+
+
+
+
+ + + + ) +} \ No newline at end of file diff --git a/apps/client-ts/src/components/Configuration/Connector/CustomConnectorPage.tsx b/apps/client-ts/src/components/Configuration/Connector/CustomConnectorPage.tsx index 74f81e840..5f3628174 100644 --- a/apps/client-ts/src/components/Configuration/Connector/CustomConnectorPage.tsx +++ b/apps/client-ts/src/components/Configuration/Connector/CustomConnectorPage.tsx @@ -12,7 +12,7 @@ export default function CustomConnectorPage() { setSearchQuery(query) } - const filteredConnectors = vertical === "" + const filteredConnectors = vertical === "All" ? providersArray() : providersArray(vertical); diff --git a/apps/client-ts/src/components/Configuration/Connector/VerticalSelector.tsx b/apps/client-ts/src/components/Configuration/Connector/VerticalSelector.tsx index 9745d639a..b709fbd59 100644 --- a/apps/client-ts/src/components/Configuration/Connector/VerticalSelector.tsx +++ b/apps/client-ts/src/components/Configuration/Connector/VerticalSelector.tsx @@ -10,7 +10,7 @@ import { CommandGroup, CommandInput, CommandItem, -} from "@/components/ui/command" +} from "@/components/ui/command" import { Popover, PopoverContent, @@ -18,6 +18,7 @@ import { } from "@/components/ui/popover" import useVerticalStore from "@/state/verticalStore" import { categoriesVerticals } from '@panora/shared'; +import { Separator } from "@/components/ui/separator" export const verticals = categoriesVerticals as string[]; @@ -45,6 +46,26 @@ export function VerticalSelector({ onSelectVertical }: { onSelectVertical: (vert No verticals found. + { + setVertical("All") + setSelectedVertical("All") + setOpen(false) + onSelectVertical("All") + }} + > + All + + + {verticals.map((vertical) => ( void; fieldToMa - {isLoading ? "Loading..." : error ? "Error fetching properties" : sourceCustomFieldsData.map(field => ( + {isLoading ?

Loading...

: error ?

Error fetching properties

: sourceCustomFieldsData.map(field => ( {field.name} ))}
diff --git a/apps/client-ts/src/components/Configuration/LinkedUsers/AddLinkedAccount.tsx b/apps/client-ts/src/components/Configuration/LinkedUsers/AddLinkedAccount.tsx index e2772bb28..a5c388f82 100644 --- a/apps/client-ts/src/components/Configuration/LinkedUsers/AddLinkedAccount.tsx +++ b/apps/client-ts/src/components/Configuration/LinkedUsers/AddLinkedAccount.tsx @@ -94,8 +94,6 @@ const AddLinkedAccount = () => { }) form.reset() } - - return ( diff --git a/apps/client-ts/src/components/Configuration/Webhooks/columns.tsx b/apps/client-ts/src/components/Configuration/Webhooks/columns.tsx index 8ea70fe30..3f12bae8a 100644 --- a/apps/client-ts/src/components/Configuration/Webhooks/columns.tsx +++ b/apps/client-ts/src/components/Configuration/Webhooks/columns.tsx @@ -58,7 +58,7 @@ export function useColumns(webhooks: Webhook[] | undefined, setWebhooks: React.D {row.getValue("url")} , @@ -120,7 +120,7 @@ export function useColumns(webhooks: Webhook[] | undefined, setWebhooks: React.D {row.getValue("endpoint_description")} , diff --git a/apps/client-ts/src/components/Connection/columns.tsx b/apps/client-ts/src/components/Connection/columns.tsx index ffc3be2ac..b331c636d 100644 --- a/apps/client-ts/src/components/Connection/columns.tsx +++ b/apps/client-ts/src/components/Connection/columns.tsx @@ -8,43 +8,7 @@ import React from "react" import { ClipboardIcon } from '@radix-ui/react-icons' import { toast } from "sonner" import { getLogoURL } from "@panora/shared" - - -function truncateMiddle(str: string, maxLength: number) { - if (str.length <= maxLength) { - return str; - } - - const start = str.substring(0, maxLength / 2); - const end = str.substring(str.length - maxLength / 2); - return `${start}...${end}`; -} - -function insertDots(originalString: string): string { - if(!originalString) return ""; - // if (originalString.length <= 50) { - // return originalString; - // } - return originalString.substring(0, 7) + '...'; -} - -function formatISODate(ISOString: string): string { - const date = new Date(ISOString); - const options: Intl.DateTimeFormatOptions = { - weekday: 'long', // "Monday" - year: 'numeric', // "2024" - month: 'long', // "April" - day: 'numeric', // "27" - hour: '2-digit', // "02" - minute: '2-digit', // "58" - second: '2-digit', // "59" - timeZoneName: 'short' // "GMT" - }; - - // Create a formatter (using US English locale as an example) - const formatter = new Intl.DateTimeFormat('en-US', options); - return formatter.format(date); -} +import { formatISODate, truncateMiddle } from "@/lib/utils" const connectionTokenComponent = ({row}:{row:any}) => { const handleCopy = async () => { diff --git a/apps/client-ts/src/components/Events/columns.tsx b/apps/client-ts/src/components/Events/columns.tsx index 1cf6f8072..2c494ad5e 100644 --- a/apps/client-ts/src/components/Events/columns.tsx +++ b/apps/client-ts/src/components/Events/columns.tsx @@ -5,6 +5,7 @@ import { Badge } from "@/components/ui/badge" import { DataTableColumnHeader } from "../shared/data-table-column-header" import { Event } from "./schema" import { getLogoURL } from "@panora/shared" +import { formatISODate } from "@/lib/utils" export const columns: ColumnDef[] = [ { @@ -13,13 +14,9 @@ export const columns: ColumnDef[] = [ ), cell: ({ row }) =>{ - //const label = labels2.find((label) => label.value === row.original.method) - return (
- {row.getValue("method") ? {row.getValue("method")} - : _null_ - } + {row.getValue("method")}
) }, @@ -32,13 +29,9 @@ export const columns: ColumnDef[] = [ ), cell: ({ row }) => { - //const label = labels.find((label) => label.value === row.original.url) - return (
- {row.getValue("url") ? {row.getValue("url")} - : _null_ - } + {row.getValue("url")}
) }, @@ -49,19 +42,9 @@ export const columns: ColumnDef[] = [ ), cell: ({ row }) => { - /*const status = statuses.find( - (status) => status.value === row.getValue("status") - ) - - if (!status) { - return null - }*/ - return (
- {row.getValue("status") ? {row.getValue("status")} - : _null_ - } + {row.getValue("status")}
) }, @@ -75,19 +58,9 @@ export const columns: ColumnDef[] = [ ), cell: ({ row }) => { - /*const direction = priorities.find( - (direction) => direction.value === row.getValue("direction") - ) - - if (!direction) { - return null - }*/ - return (
- {row.getValue("direction") ? {row.getValue("direction")} - : _null_ - } + {row.getValue("direction")}
) }, @@ -101,25 +74,15 @@ export const columns: ColumnDef[] = [ ), cell: ({ row }) => { - /*const status = statuses.find( - (status) => status.value === row.getValue("integration") - ) - - if (!status) { - return null - }*/ const provider = (row.getValue("integration") as string).toLowerCase(); return (
- {row.getValue("integration") ? {provider} - : _null_ - }
) }, @@ -133,13 +96,9 @@ export const columns: ColumnDef[] = [ ), cell: ({ row }) => { - //const label = labels.find((label) => label.value === row.original.date) - return (
- {row.getValue("date") ? {row.getValue("date")} - : _null_ - } + {formatISODate(row.getValue("date"))}
) }, diff --git a/apps/client-ts/src/components/ui/loading-spinner.tsx b/apps/client-ts/src/components/ui/loading-spinner.tsx new file mode 100644 index 000000000..db4a2b4ac --- /dev/null +++ b/apps/client-ts/src/components/ui/loading-spinner.tsx @@ -0,0 +1,19 @@ +import { cn } from "@/lib/utils" + +export const LoadingSpinner = ({className} : {className?: string}) => { + return ( + + + ) +} \ No newline at end of file diff --git a/apps/client-ts/src/lib/utils.ts b/apps/client-ts/src/lib/utils.ts index fd581b930..596760aa2 100644 --- a/apps/client-ts/src/lib/utils.ts +++ b/apps/client-ts/src/lib/utils.ts @@ -8,3 +8,40 @@ export function cn(...inputs: ClassValue[]) { export function toDomain(email: string): string { return email.split("@")[1]; } + +export function formatISODate(ISOString: string): string { + const date = new Date(ISOString); + const options: Intl.DateTimeFormatOptions = { + weekday: 'long', // "Monday" + year: 'numeric', // "2024" + month: 'long', // "April" + day: 'numeric', // "27" + hour: '2-digit', // "02" + minute: '2-digit', // "58" + second: '2-digit', // "59" + timeZoneName: 'short' // "GMT" + }; + + // Create a formatter (using US English locale as an example) + const formatter = new Intl.DateTimeFormat('en-US', options); + return formatter.format(date); +} + + +export function truncateMiddle(str: string, maxLength: number) { + if (str.length <= maxLength) { + return str; + } + + const start = str.substring(0, maxLength / 2); + const end = str.substring(str.length - maxLength / 2); + return `${start}...${end}`; +} + +export function insertDots(originalString: string): string { + if(!originalString) return ""; + // if (originalString.length <= 50) { + // return originalString; + // } + return originalString.substring(0, 7) + '...'; +} \ No newline at end of file diff --git a/apps/client-ts/src/state/verticalStore.ts b/apps/client-ts/src/state/verticalStore.ts index 399e5d3ad..9c56d874a 100644 --- a/apps/client-ts/src/state/verticalStore.ts +++ b/apps/client-ts/src/state/verticalStore.ts @@ -6,7 +6,7 @@ interface VerticalState { } const useVerticalStore = create()((set) => ({ - vertical: "", + vertical: "All", setVertical: (name) => set({ vertical: name }), }));