diff --git a/bin/cmd.ts b/bin/cmd.ts
index e87de8ae..66ba2feb 100755
--- a/bin/cmd.ts
+++ b/bin/cmd.ts
@@ -5,7 +5,7 @@ import importAreaSet from "@/server/commands/importAreaSet";
import importPostcodes from "@/server/commands/importPostcodes";
import regeocode from "@/server/commands/regeocode";
import removeDevWebhooks from "@/server/commands/removeDevWebhooks";
-import Invite from "@/server/emails/invite";
+import Invite from "@/server/emails/Invite";
import enrichDataSource from "@/server/jobs/enrichDataSource";
import importDataSource from "@/server/jobs/importDataSource";
import { createInvitation } from "@/server/repositories/Invitation";
@@ -140,7 +140,7 @@ program
const orgs = await findOrganisationsByUserId(user.id);
if (!orgs.length) {
- logger.warning(`No organisation found for user ${user.email}`);
+ logger.warn(`No organisation found for user ${user.email}`);
continue;
}
diff --git a/src/app/map/[id]/components/table/MapTable.tsx b/src/app/map/[id]/components/table/MapTable.tsx
index dab7445f..2e2d252b 100644
--- a/src/app/map/[id]/components/table/MapTable.tsx
+++ b/src/app/map/[id]/components/table/MapTable.tsx
@@ -7,6 +7,7 @@ import { useDataSources } from "@/app/map/[id]/hooks/useDataSources";
import { useInspector } from "@/app/map/[id]/hooks/useInspector";
import { useMapViews } from "@/app/map/[id]/hooks/useMapViews";
import { useTable } from "@/app/map/[id]/hooks/useTable";
+import { DataSourceFeatures } from "@/features";
import { useFeatureFlagEnabled } from "@/hooks";
import { DataSourceTypeLabels } from "@/labels";
import { FilterType } from "@/server/models/MapView";
@@ -30,7 +31,6 @@ export default function MapTable() {
const { view, updateView } = useMapViews();
const { getDataSourceById } = useDataSources();
const { focusedRecord, setFocusedRecord } = useInspector();
- const enableSyncToCRM = useFeatureFlagEnabled("sync-to-crm");
const [lookingUpPage, setLookingUpPage] = useState(false);
const {
@@ -53,7 +53,9 @@ export default function MapTable() {
const { mutate: tagRecords } = useMutation(
trpc.mapView.tagRecordsWithViewName.mutationOptions({
onSuccess: () => {
- toast.success("Tagging records in the background");
+ toast.success(
+ "Tagging records in the background - we will email you when the process completes.",
+ );
},
onError: () => {
toast.error("Failed to tag records with view name");
@@ -136,13 +138,13 @@ export default function MapTable() {
view,
]);
- if (!selectedDataSourceId || !view) {
- return null;
- }
-
const dataSource = getDataSourceById(selectedDataSourceId);
+ const enableSyncToCRM =
+ useFeatureFlagEnabled("sync-to-crm") &&
+ dataSource &&
+ DataSourceFeatures[dataSource.config.type].syncToCrm;
- if (!dataSource) {
+ if (!dataSource || !view) {
return null;
}
diff --git a/src/features.ts b/src/features.ts
index bff7fa9e..db9cf88c 100644
--- a/src/features.ts
+++ b/src/features.ts
@@ -2,31 +2,41 @@ import { DataSourceType } from "./server/models/DataSource";
export const DataSourceFeatures: Record<
DataSourceType,
- { autoEnrich: boolean; autoImport: boolean; enrichment: boolean }
+ {
+ autoEnrich: boolean;
+ autoImport: boolean;
+ enrichment: boolean;
+ syncToCrm: boolean;
+ }
> = {
[DataSourceType.ActionNetwork]: {
autoEnrich: true,
autoImport: true,
enrichment: false,
+ syncToCrm: true,
},
[DataSourceType.Airtable]: {
autoEnrich: true,
autoImport: true,
enrichment: false,
+ syncToCrm: true,
},
[DataSourceType.CSV]: {
autoEnrich: false,
autoImport: false,
enrichment: false,
+ syncToCrm: false,
},
[DataSourceType.GoogleSheets]: {
autoEnrich: true,
autoImport: true,
enrichment: false,
+ syncToCrm: true,
},
[DataSourceType.Mailchimp]: {
autoEnrich: true,
autoImport: true,
enrichment: false,
+ syncToCrm: true,
},
};
diff --git a/src/server/emails/forgot-password.tsx b/src/server/emails/ForgotPassword.tsx
similarity index 97%
rename from src/server/emails/forgot-password.tsx
rename to src/server/emails/ForgotPassword.tsx
index b26fa81a..7a7811a6 100644
--- a/src/server/emails/forgot-password.tsx
+++ b/src/server/emails/ForgotPassword.tsx
@@ -9,7 +9,7 @@ import {
} from "@react-email/components";
import * as React from "react";
import { getAbsoluteUrl } from "@/utils/appUrl";
-import { EmailLayout } from "./layout";
+import { EmailLayout } from "./Layout";
export default function ForgotPassword({ token = "123" }: { token: string }) {
const baseUrl = getAbsoluteUrl();
diff --git a/src/server/emails/invite.tsx b/src/server/emails/Invite.tsx
similarity index 98%
rename from src/server/emails/invite.tsx
rename to src/server/emails/Invite.tsx
index 4d23a7ec..e0ed1e1c 100644
--- a/src/server/emails/invite.tsx
+++ b/src/server/emails/Invite.tsx
@@ -9,7 +9,7 @@ import {
} from "@react-email/components";
import * as React from "react";
import { getAbsoluteUrl } from "@/utils/appUrl";
-import { EmailLayout } from "./layout";
+import { EmailLayout } from "./Layout";
export default function Invite({ token }: { token: string }) {
const baseUrl = getAbsoluteUrl();
diff --git a/src/server/emails/layout.tsx b/src/server/emails/Layout.tsx
similarity index 100%
rename from src/server/emails/layout.tsx
rename to src/server/emails/Layout.tsx
diff --git a/src/server/emails/TaggingComplete.tsx b/src/server/emails/TaggingComplete.tsx
new file mode 100644
index 00000000..eff6a5fb
--- /dev/null
+++ b/src/server/emails/TaggingComplete.tsx
@@ -0,0 +1,50 @@
+import {
+ Container,
+ Img,
+ Preview,
+ Section,
+ Text,
+} from "@react-email/components";
+import * as React from "react";
+import { getAbsoluteUrl } from "@/utils/appUrl";
+import { EmailLayout } from "./Layout";
+
+export default function TaggingComplete({
+ dataSourceName,
+ viewName,
+}: {
+ dataSourceName: string;
+ viewName: string;
+}) {
+ const baseUrl = getAbsoluteUrl();
+
+ return (
+
+ Tagging complete for {dataSourceName}
+
+
+
+
+
+
+ Hello,
+
+
+
+ The tagging job for data source {dataSourceName} with
+ view {viewName} has completed successfully.
+
+
+
+ All records have been tagged accordingly.
+
+
+
+ );
+}
diff --git a/src/server/emails/TaggingFailed.tsx b/src/server/emails/TaggingFailed.tsx
new file mode 100644
index 00000000..fe63952d
--- /dev/null
+++ b/src/server/emails/TaggingFailed.tsx
@@ -0,0 +1,51 @@
+import {
+ Container,
+ Img,
+ Preview,
+ Section,
+ Text,
+} from "@react-email/components";
+import * as React from "react";
+
+import { getAbsoluteUrl } from "@/utils/appUrl";
+
+import { EmailLayout } from "./Layout";
+
+export default function TaggingFailed({
+ dataSourceName,
+ viewName,
+ reason,
+}: {
+ dataSourceName: string;
+ viewName: string;
+ reason: string;
+}) {
+ const baseUrl = getAbsoluteUrl();
+
+ return (
+
+ Tagging failed for {dataSourceName}
+
+
+
+
+
+ Hello,
+
+
+ The tagging job for data source {dataSourceName} with
+ view {viewName} has failed.
+
+
+ Reason: {reason}
+
+
+
+ );
+}
diff --git a/src/server/jobs/tagDataSource.ts b/src/server/jobs/tagDataSource.ts
index b4fe8d37..b69d3e0c 100644
--- a/src/server/jobs/tagDataSource.ts
+++ b/src/server/jobs/tagDataSource.ts
@@ -1,5 +1,7 @@
import { DATA_SOURCE_JOB_BATCH_SIZE } from "@/constants";
import { getDataSourceAdaptor } from "@/server/adaptors";
+import TaggingComplete from "@/server/emails/TaggingComplete";
+import TaggingFailed from "@/server/emails/TaggingFailed";
import {
countDataRecordsForDataSource,
streamDataRecordsByDataSource,
@@ -7,32 +9,62 @@ import {
import { findDataSourceById } from "@/server/repositories/DataSource";
import { findMapViewById } from "@/server/repositories/MapView";
import logger from "@/server/services/logger";
+import { sendEmail } from "@/server/services/mailer";
import { batchAsync } from "@/server/utils";
import { findMapById } from "../repositories/Map";
import type { TaggedRecord } from "@/types";
+const sendFailureEmail = async (
+ userEmail: string,
+ dataSourceName: string,
+ viewName: string,
+ reason: string,
+) => {
+ try {
+ await sendEmail(
+ userEmail,
+ "Tagging failed",
+ TaggingFailed({ dataSourceName, viewName, reason }),
+ );
+ } catch (error) {
+ logger.error("Failed to send tagging failure email", { error });
+ }
+};
+
const tagDataSource = async (args: object | null): Promise => {
- if (!args || !("dataSourceId" in args) || !("viewId" in args)) {
+ if (
+ !args ||
+ !("dataSourceId" in args) ||
+ !("viewId" in args) ||
+ !("userEmail" in args)
+ ) {
return false;
}
const dataSourceId = String(args.dataSourceId);
const viewId = String(args.viewId);
+ const userEmail = String(args.userEmail);
const dataSource = await findDataSourceById(dataSourceId);
if (!dataSource) {
- logger.info(`Data source ${dataSourceId} not found.`);
+ const reason = `Failed to tag data source: ${dataSourceId} not found.`;
+ logger.warn(reason);
+ await sendFailureEmail(userEmail, dataSourceId, viewId, reason);
return false;
}
const view = await findMapViewById(viewId);
if (!view) {
- logger.info(`View ${viewId} not found.`);
+ const reason = `View ${viewId} not found.`;
+ logger.warn(`Failed to tag data source ${dataSourceId}: ${reason}`);
+ await sendFailureEmail(userEmail, dataSource.name, viewId, reason);
return false;
}
const map = await findMapById(view.mapId);
if (!map) {
- logger.info(`Map ${view.mapId} not found.`);
+ const reason = `Map ${view.mapId} not found.`;
+ logger.warn(`Failed to tag data source ${dataSourceId}: ${reason}`);
+ await sendFailureEmail(userEmail, dataSource.name, view.name, reason);
return false;
}
@@ -46,9 +78,9 @@ const tagDataSource = async (args: object | null): Promise => {
const adaptor = getDataSourceAdaptor(dataSource);
if (!adaptor) {
- logger.error(
- `Could not get data source adaptor for source ${dataSourceId}, type ${dataSource.config.type}`,
- );
+ const reason = `Could not get data source adaptor for source ${dataSourceId}, type ${dataSource.config.type}`;
+ logger.error(reason);
+ await sendFailureEmail(userEmail, dataSource.name, view.name, reason);
return false;
}
@@ -90,15 +122,27 @@ const tagDataSource = async (args: object | null): Promise => {
logger.info(
`Tagged data source ${dataSourceId} with view ${view.name} (${view.id})`,
);
+
+ try {
+ await sendEmail(
+ userEmail,
+ "Tagging complete",
+ TaggingComplete({
+ dataSourceName: dataSource.name,
+ viewName: view.name,
+ }),
+ );
+ } catch (error) {
+ logger.error("Failed to send tagging success email", { error });
+ }
+
return true;
} catch (error) {
- logger.error(
- `Failed to tag records for ${dataSource.config.type} ${dataSourceId}`,
- { error },
- );
+ const reason = `Failed to tag records for ${dataSource.config.type} ${dataSourceId}`;
+ logger.error(reason, { error });
+ await sendFailureEmail(userEmail, dataSource.name, view.name, reason);
+ throw error;
}
-
- return false;
};
export default tagDataSource;
diff --git a/src/server/trpc/routers/auth.ts b/src/server/trpc/routers/auth.ts
index 220e01c1..0d654402 100644
--- a/src/server/trpc/routers/auth.ts
+++ b/src/server/trpc/routers/auth.ts
@@ -5,7 +5,7 @@ import { NoResultError } from "kysely";
import { cookies } from "next/headers";
import z from "zod";
import ensureOrganisationMap from "@/server/commands/ensureOrganisationMap";
-import ForgotPassword from "@/server/emails/forgot-password";
+import ForgotPassword from "@/server/emails/ForgotPassword";
import {
findAndUseInvitation,
updateInvitation,
diff --git a/src/server/trpc/routers/invitation.ts b/src/server/trpc/routers/invitation.ts
index 71aefda8..1db9c1bd 100644
--- a/src/server/trpc/routers/invitation.ts
+++ b/src/server/trpc/routers/invitation.ts
@@ -2,7 +2,7 @@ import { TRPCError } from "@trpc/server";
import { SignJWT } from "jose";
import z from "zod";
import ensureOrganisationMap from "@/server/commands/ensureOrganisationMap";
-import Invite from "@/server/emails/invite";
+import Invite from "@/server/emails/Invite";
import {
createInvitation,
listPendingInvitations,
diff --git a/src/server/trpc/routers/mapView.ts b/src/server/trpc/routers/mapView.ts
index b8a1149a..44f8bad9 100644
--- a/src/server/trpc/routers/mapView.ts
+++ b/src/server/trpc/routers/mapView.ts
@@ -16,6 +16,7 @@ export const mapViewRouter = router({
await enqueue("tagDataSource", ctx.dataSource.id, {
dataSourceId: ctx.dataSource.id,
viewId: input.viewId,
+ userEmail: ctx.user.email,
});
return true;
}),