diff --git a/.sqlx/query-14d6f116e96d1a14e4f9012505c65b4fb5054857d28d6c91efe581a96394cd95.json b/.sqlx/query-14d6f116e96d1a14e4f9012505c65b4fb5054857d28d6c91efe581a96394cd95.json
deleted file mode 100644
index e7d63f41..00000000
--- a/.sqlx/query-14d6f116e96d1a14e4f9012505c65b4fb5054857d28d6c91efe581a96394cd95.json
+++ /dev/null
@@ -1,15 +0,0 @@
-{
- "db_name": "PostgreSQL",
- "query": "\n UPDATE organizations\n SET current_subscription = $2\n WHERE id = $1\n ",
- "describe": {
- "columns": [],
- "parameters": {
- "Left": [
- "Uuid",
- "Jsonb"
- ]
- },
- "nullable": []
- },
- "hash": "14d6f116e96d1a14e4f9012505c65b4fb5054857d28d6c91efe581a96394cd95"
-}
diff --git a/.sqlx/query-1a243b9b6d1530826d837de97574ca2d9346c3905b109bbc30bb5a6431091377.json b/.sqlx/query-1a243b9b6d1530826d837de97574ca2d9346c3905b109bbc30bb5a6431091377.json
new file mode 100644
index 00000000..3b5ba2c4
--- /dev/null
+++ b/.sqlx/query-1a243b9b6d1530826d837de97574ca2d9346c3905b109bbc30bb5a6431091377.json
@@ -0,0 +1,22 @@
+{
+ "db_name": "PostgreSQL",
+ "query": "\n SELECT current_subscription\n FROM organizations\n WHERE id = $1\n ",
+ "describe": {
+ "columns": [
+ {
+ "ordinal": 0,
+ "name": "current_subscription",
+ "type_info": "Jsonb"
+ }
+ ],
+ "parameters": {
+ "Left": [
+ "Uuid"
+ ]
+ },
+ "nullable": [
+ false
+ ]
+ },
+ "hash": "1a243b9b6d1530826d837de97574ca2d9346c3905b109bbc30bb5a6431091377"
+}
diff --git a/.sqlx/query-57d9a1e644c9243ceab7d33cb983480e0b8701de3f83c79d4959fc6d70df9790.json b/.sqlx/query-2591893c48c8aae12c9face136b5981a18ab9369f7164d41120876e2cc0448ad.json
similarity index 79%
rename from .sqlx/query-57d9a1e644c9243ceab7d33cb983480e0b8701de3f83c79d4959fc6d70df9790.json
rename to .sqlx/query-2591893c48c8aae12c9face136b5981a18ab9369f7164d41120876e2cc0448ad.json
index 00bf5b98..874d57d7 100644
--- a/.sqlx/query-57d9a1e644c9243ceab7d33cb983480e0b8701de3f83c79d4959fc6d70df9790.json
+++ b/.sqlx/query-2591893c48c8aae12c9face136b5981a18ab9369f7164d41120876e2cc0448ad.json
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
- "query": "\n INSERT INTO projects (id, organization_id, name)\n VALUES (gen_random_uuid(), $1, $2)\n RETURNING *\n ",
+ "query": "\n INSERT INTO projects (id, organization_id, name, retention_period_days)\n VALUES (gen_random_uuid(), $1, $2, $3)\n RETURNING *\n ",
"describe": {
"columns": [
{
@@ -37,7 +37,8 @@
"parameters": {
"Left": [
"Uuid",
- "Varchar"
+ "Varchar",
+ "Int4"
]
},
"nullable": [
@@ -49,5 +50,5 @@
false
]
},
- "hash": "57d9a1e644c9243ceab7d33cb983480e0b8701de3f83c79d4959fc6d70df9790"
+ "hash": "2591893c48c8aae12c9face136b5981a18ab9369f7164d41120876e2cc0448ad"
}
diff --git a/.sqlx/query-3a98c1af6bc82460788f2ac4f420d20a20f9d66f96d155c002541950755d949e.json b/.sqlx/query-3a98c1af6bc82460788f2ac4f420d20a20f9d66f96d155c002541950755d949e.json
new file mode 100644
index 00000000..87c106ed
--- /dev/null
+++ b/.sqlx/query-3a98c1af6bc82460788f2ac4f420d20a20f9d66f96d155c002541950755d949e.json
@@ -0,0 +1,12 @@
+{
+ "db_name": "PostgreSQL",
+ "query": "\n UPDATE messages m\n SET raw_data = '',\n message_data = NULL,\n recipients = '{}',\n delivery_details = '{}'\n FROM projects p\n WHERE m.project_id = p.id\n AND m.created_at < NOW() - (p.retention_period_days * INTERVAL '1 day')\n AND octet_length(m.raw_data) > 0;\n ",
+ "describe": {
+ "columns": [],
+ "parameters": {
+ "Left": []
+ },
+ "nullable": []
+ },
+ "hash": "3a98c1af6bc82460788f2ac4f420d20a20f9d66f96d155c002541950755d949e"
+}
diff --git a/.sqlx/query-d1a0a2eac92e8b211cbbefdd214c2142b6ebdf5171fbaab6b66185475fb1703c.json b/.sqlx/query-6019031dbd6be02050e34e453b1a324c429aafe2b26fe7d82fe20da2f96b7a1a.json
similarity index 77%
rename from .sqlx/query-d1a0a2eac92e8b211cbbefdd214c2142b6ebdf5171fbaab6b66185475fb1703c.json
rename to .sqlx/query-6019031dbd6be02050e34e453b1a324c429aafe2b26fe7d82fe20da2f96b7a1a.json
index d3d7db33..ce850c0e 100644
--- a/.sqlx/query-d1a0a2eac92e8b211cbbefdd214c2142b6ebdf5171fbaab6b66185475fb1703c.json
+++ b/.sqlx/query-6019031dbd6be02050e34e453b1a324c429aafe2b26fe7d82fe20da2f96b7a1a.json
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
- "query": "\n UPDATE projects \n SET name = $3 \n WHERE id = $2\n AND organization_id = $1\n RETURNING *\n ",
+ "query": "\n UPDATE projects \n SET name = $3,\n retention_period_days = $4\n WHERE id = $2\n AND organization_id = $1\n RETURNING *\n ",
"describe": {
"columns": [
{
@@ -38,7 +38,8 @@
"Left": [
"Uuid",
"Uuid",
- "Varchar"
+ "Varchar",
+ "Int4"
]
},
"nullable": [
@@ -50,5 +51,5 @@
false
]
},
- "hash": "d1a0a2eac92e8b211cbbefdd214c2142b6ebdf5171fbaab6b66185475fb1703c"
+ "hash": "6019031dbd6be02050e34e453b1a324c429aafe2b26fe7d82fe20da2f96b7a1a"
}
diff --git a/.sqlx/query-81ab44acb4330019bdf4dacd374367d9b9b9157a31db88a2c99aaf02df9dc647.json b/.sqlx/query-81ab44acb4330019bdf4dacd374367d9b9b9157a31db88a2c99aaf02df9dc647.json
new file mode 100644
index 00000000..8b4ed078
--- /dev/null
+++ b/.sqlx/query-81ab44acb4330019bdf4dacd374367d9b9b9157a31db88a2c99aaf02df9dc647.json
@@ -0,0 +1,15 @@
+{
+ "db_name": "PostgreSQL",
+ "query": "\n UPDATE projects\n SET retention_period_days = $2\n WHERE organization_id = $1\n AND retention_period_days > $2;\n ",
+ "describe": {
+ "columns": [],
+ "parameters": {
+ "Left": [
+ "Uuid",
+ "Int4"
+ ]
+ },
+ "nullable": []
+ },
+ "hash": "81ab44acb4330019bdf4dacd374367d9b9b9157a31db88a2c99aaf02df9dc647"
+}
diff --git a/.sqlx/query-a6d00789a29b8924a9b4030d357d89e4acebeba4f6eb83631f16f1d5b5088a7d.json b/.sqlx/query-a6d00789a29b8924a9b4030d357d89e4acebeba4f6eb83631f16f1d5b5088a7d.json
deleted file mode 100644
index 56d74b6e..00000000
--- a/.sqlx/query-a6d00789a29b8924a9b4030d357d89e4acebeba4f6eb83631f16f1d5b5088a7d.json
+++ /dev/null
@@ -1,12 +0,0 @@
-{
- "db_name": "PostgreSQL",
- "query": "\n UPDATE messages\n SET raw_data = '',\n message_data = NULL,\n recipients = '{}',\n delivery_details = '{}'\n WHERE created_at < NOW() - INTERVAL '30 days'\n AND octet_length(raw_data) > 0;\n ",
- "describe": {
- "columns": [],
- "parameters": {
- "Left": []
- },
- "nullable": []
- },
- "hash": "a6d00789a29b8924a9b4030d357d89e4acebeba4f6eb83631f16f1d5b5088a7d"
-}
diff --git a/.sqlx/query-eee5c2bd32d661206ef59b22224541c8ed0643b246101d0b91d54ce18474a488.json b/.sqlx/query-eee5c2bd32d661206ef59b22224541c8ed0643b246101d0b91d54ce18474a488.json
new file mode 100644
index 00000000..7d5bf17e
--- /dev/null
+++ b/.sqlx/query-eee5c2bd32d661206ef59b22224541c8ed0643b246101d0b91d54ce18474a488.json
@@ -0,0 +1,15 @@
+{
+ "db_name": "PostgreSQL",
+ "query": "\n UPDATE organizations\n SET current_subscription = $2\n WHERE id = $1\n ",
+ "describe": {
+ "columns": [],
+ "parameters": {
+ "Left": [
+ "Uuid",
+ "Jsonb"
+ ]
+ },
+ "nullable": []
+ },
+ "hash": "eee5c2bd32d661206ef59b22224541c8ed0643b246101d0b91d54ce18474a488"
+}
diff --git a/frontend/src/Pages.tsx b/frontend/src/Pages.tsx
index 77c02c33..146f9447 100644
--- a/frontend/src/Pages.tsx
+++ b/frontend/src/Pages.tsx
@@ -57,7 +57,7 @@ function Page() {
const {
state: { organizations, routerState },
} = useRemails();
- const { subscription } = useSubscription();
+ const { currentSubscription } = useSubscription();
if (organizations?.length === 0 && routerState.name != "invite") {
return ;
@@ -67,8 +67,8 @@ function Page() {
!(routerState.name === "settings") &&
!routerState.name.startsWith("organizations") &&
!routerState.name.startsWith("admin") &&
- subscription &&
- subscription.status !== "active"
+ currentSubscription &&
+ currentSubscription.status !== "active"
) {
return ;
}
diff --git a/frontend/src/components/InfoTooltip.tsx b/frontend/src/components/InfoTooltip.tsx
new file mode 100644
index 00000000..52386af8
--- /dev/null
+++ b/frontend/src/components/InfoTooltip.tsx
@@ -0,0 +1,25 @@
+import { MantineSize, ThemeIcon, Tooltip } from "@mantine/core";
+import { IconInfoCircle } from "@tabler/icons-react";
+
+interface InfoTooltipProps {
+ text: string;
+ size?: MantineSize | number;
+}
+
+export default function InfoTooltip({ text, size }: InfoTooltipProps) {
+ return (
+
+
+
+
+
+ );
+}
diff --git a/frontend/src/components/SetupSubscription.tsx b/frontend/src/components/SetupSubscription.tsx
index 6cbc9e4d..9a7990bd 100644
--- a/frontend/src/components/SetupSubscription.tsx
+++ b/frontend/src/components/SetupSubscription.tsx
@@ -5,10 +5,10 @@ import { IconReload } from "@tabler/icons-react";
import { useOrganizations } from "../hooks/useOrganizations.ts";
export default function SetupSubscription() {
- const { subscription, reloadSubscription, navigateToSales } = useSubscription();
+ const { currentSubscription, reloadSubscription, navigateToSales } = useSubscription();
const { currentOrganization } = useOrganizations();
- if (!currentOrganization || !subscription) {
+ if (!currentOrganization || !currentSubscription) {
return null;
}
diff --git a/frontend/src/components/domains/DomainVerification.tsx b/frontend/src/components/domains/DomainVerification.tsx
index b4cb7fb5..6133a848 100644
--- a/frontend/src/components/domains/DomainVerification.tsx
+++ b/frontend/src/components/domains/DomainVerification.tsx
@@ -2,13 +2,13 @@ import { useOrganizations } from "../../hooks/useOrganizations.ts";
import { useRemails } from "../../hooks/useRemails.ts";
import { useDomains } from "../../hooks/useDomains.ts";
import { VerifyResult } from "../../types.ts";
-import { IconInfoCircle } from "@tabler/icons-react";
-import { Badge, Button, Code, Group, Loader, Paper, Popover, Table, Text, ThemeIcon, Tooltip } from "@mantine/core";
+import { Badge, Button, Code, Group, Loader, Paper, Popover, Table, Text } from "@mantine/core";
import { dkimRecord, dmarcValue, spfRecord } from "./DnsRecords.tsx";
import { useVerifyDomain } from "../../hooks/useVerifyDomain.ts";
import { formatDateTime } from "../../util.ts";
import { CopyableCode } from "../CopyableCode.tsx";
import React, { useState } from "react";
+import InfoTooltip from "../InfoTooltip.tsx";
const badgeColors: { [key in VerifyResult["status"]]: string } = {
Success: "green",
@@ -157,11 +157,7 @@ export default function DomainVerification() {
recordValue: (
any
-
-
-
-
-
+
),
verifyResult: verificationResult?.a,
diff --git a/frontend/src/components/emails/EmailOverview.tsx b/frontend/src/components/emails/EmailOverview.tsx
index 4dcea78b..0004260f 100644
--- a/frontend/src/components/emails/EmailOverview.tsx
+++ b/frontend/src/components/emails/EmailOverview.tsx
@@ -1,31 +1,9 @@
-import {
- Accordion,
- ActionIcon,
- Badge,
- Box,
- Button,
- Code,
- Group,
- MultiSelect,
- NativeSelect,
- Text,
- ThemeIcon,
- Tooltip,
-} from "@mantine/core";
+import { Accordion, ActionIcon, Badge, Box, Button, Code, Group, MultiSelect, NativeSelect, Text } from "@mantine/core";
import { useEmails } from "../../hooks/useEmails.ts";
import { Loader } from "../../Loader.tsx";
import { formatDateTime } from "../../util.ts";
import { useRemails } from "../../hooks/useRemails.ts";
-import {
- IconArrowLeft,
- IconArrowRight,
- IconCheck,
- IconClock,
- IconEye,
- IconInfoCircle,
- IconRefresh,
- IconX,
-} from "@tabler/icons-react";
+import { IconArrowLeft, IconArrowRight, IconCheck, IconClock, IconEye, IconRefresh, IconX } from "@tabler/icons-react";
import { getFullStatusDescription } from "./EmailDetails.tsx";
import { DateTimePicker } from "@mantine/dates";
import dayjs from "dayjs";
@@ -38,6 +16,7 @@ import Label from "./Label.tsx";
import { EmailStatus } from "../../types.ts";
import OrganizationHeader from "../organizations/OrganizationHeader.tsx";
import ProjectLink from "../ProjectLink.tsx";
+import InfoTooltip from "../InfoTooltip.tsx";
function statusIcons(status: EmailStatus) {
if (status == "processing" || status == "accepted") {
@@ -184,18 +163,12 @@ export function EmailOverview() {
<>
{routerState.name == "emails" && }
- {routerState.name == "emails" ? (
-
- This page shows a list of all emails sent in this organization. Use it to check delivery status, inspect
- metadata, and troubleshoot issues. You’ll see timestamps, recipient addresses, and SMTP-level details for each
- message.
-
- ) : (
-
- This page shows a list of all emails sent in this project. Use it to check delivery status, inspect metadata,
- and troubleshoot issues. You’ll see timestamps, recipient addresses, and SMTP-level details for each message.
-
- )}
+
+ This page shows a list of all emails sent in this {routerState.name == "emails" ? "organization" : "project"}.
+ Use it to check delivery status, inspect metadata, and troubleshoot issues. You’ll see timestamps, recipient
+ addresses, and SMTP-level details for each message. Messages are automatically deleted after the rentention
+ period set in the project settings.
+
@@ -203,11 +176,10 @@ export function EmailOverview() {
label={
Label
-
-
-
-
-
+
}
placeholder="Pick labels"
diff --git a/frontend/src/components/organizations/Members.tsx b/frontend/src/components/organizations/Members.tsx
index 9fe28169..e27d42cd 100644
--- a/frontend/src/components/organizations/Members.tsx
+++ b/frontend/src/components/organizations/Members.tsx
@@ -19,7 +19,7 @@ import UpdateRole from "./UpdateRole";
export default function Members() {
const { currentOrganization } = useOrganizations();
const { isAdmin } = useOrgRole();
- const { subscription } = useSubscription();
+ const { currentSubscription } = useSubscription();
const { invites, setInvites } = useInvites();
const { members, setMembers } = useMembers();
const user = useSelector((state) => state.user);
@@ -27,7 +27,7 @@ export default function Members() {
const [opened, { open, close }] = useDisclosure(false);
- if (!currentOrganization || !subscription) {
+ if (!currentOrganization || !currentSubscription) {
return null;
}
@@ -217,7 +217,7 @@ export default function Members() {
diff --git a/frontend/src/components/organizations/SubscriptionCard.tsx b/frontend/src/components/organizations/SubscriptionCard.tsx
index 28b1b33f..878b3713 100644
--- a/frontend/src/components/organizations/SubscriptionCard.tsx
+++ b/frontend/src/components/organizations/SubscriptionCard.tsx
@@ -6,10 +6,10 @@ import { formatDate } from "../../util.ts";
import { useOrgRole } from "../../hooks/useOrganizations.ts";
export default function SubscriptionCard() {
- const { subscription, navigateToSales, navigateToCustomerPortal } = useSubscription();
+ const { currentSubscription, navigateToSales, navigateToCustomerPortal } = useSubscription();
const { isAdmin } = useOrgRole();
- if (!subscription) {
+ if (!currentSubscription) {
return null;
}
@@ -77,7 +77,7 @@ export default function SubscriptionCard() {
Your subscription
- {details(subscription)}
+ {details(currentSubscription)}
);
diff --git a/frontend/src/components/projects/NewProject.tsx b/frontend/src/components/projects/NewProject.tsx
index de71919b..939f0627 100644
--- a/frontend/src/components/projects/NewProject.tsx
+++ b/frontend/src/components/projects/NewProject.tsx
@@ -1,12 +1,16 @@
-import { Button, Group, Modal, Stack, TextInput } from "@mantine/core";
+import { Button, Group, Modal, Slider, Stack, TextInput } from "@mantine/core";
import { useForm } from "@mantine/form";
import { useOrganizations } from "../../hooks/useOrganizations.ts";
import { useRemails } from "../../hooks/useRemails.ts";
import { notifications } from "@mantine/notifications";
import { errorNotification } from "../../notify.tsx";
+import InfoTooltip from "../InfoTooltip.tsx";
+import { MAX_RETENTION } from "./ProjectSettings.tsx";
+import { useSubscription } from "../../hooks/useSubscription.ts";
interface FormValues {
name: string;
+ retention_period_days: number;
}
interface NewProjectProps {
@@ -16,11 +20,13 @@ interface NewProjectProps {
export function NewProject({ opened, close }: NewProjectProps) {
const { currentOrganization } = useOrganizations();
+ const { currentProduct } = useSubscription();
const { navigate, dispatch } = useRemails();
const form = useForm({
initialValues: {
name: "",
+ retention_period_days: 1,
},
validate: {
name: (value) => (value.length < 3 ? "Name must have at least 3 letters" : null),
@@ -65,11 +71,13 @@ export function NewProject({ opened, close }: NewProjectProps) {
});
};
+ const max_retention = currentProduct ? MAX_RETENTION[currentProduct] : 1;
+
return (
<>
>
diff --git a/frontend/src/components/projects/ProjectSettings.tsx b/frontend/src/components/projects/ProjectSettings.tsx
index 3e8c7ff0..d90e8f69 100644
--- a/frontend/src/components/projects/ProjectSettings.tsx
+++ b/frontend/src/components/projects/ProjectSettings.tsx
@@ -1,6 +1,6 @@
import { useForm } from "@mantine/form";
-import { Group, List, Stack, Text, TextInput } from "@mantine/core";
-import { Project } from "../../types.ts";
+import { Group, List, Slider, Stack, Text, TextInput } from "@mantine/core";
+import { ProductIdentifier, Project } from "../../types.ts";
import { modals } from "@mantine/modals";
import { notifications } from "@mantine/notifications";
import { IconTrash } from "@tabler/icons-react";
@@ -11,21 +11,39 @@ import { Loader } from "../../Loader.tsx";
import { useDomains } from "../../hooks/useDomains.ts";
import { errorNotification } from "../../notify.tsx";
import { MaintainerButton } from "../RoleButtons.tsx";
+import InfoTooltip from "../InfoTooltip.tsx";
+import { useSubscription } from "../../hooks/useSubscription.ts";
interface FormValues {
name: string;
+ retention_period_days: number;
}
+// Values should match `max_retention_period` in `src/moneybird/model.rs`
+export const MAX_RETENTION: Record = {
+ "RMLS-FREE": 1,
+ "RMLS-TINY-MONTHLY": 3,
+ "RMLS-SMALL-MONTHLY": 7,
+ "RMLS-MEDIUM-MONTHLY": 14,
+ "RMLS-LARGE-MONTHLY": 30,
+ "RMLS-TINY-YEARLY": 3,
+ "RMLS-SMALL-YEARLY": 7,
+ "RMLS-MEDIUM-YEARLY": 14,
+ "RMLS-LARGE-YEARLY": 30,
+};
+
export default function ProjectSettings() {
const { dispatch, navigate } = useRemails();
const { currentOrganization } = useOrganizations();
+ const { currentProduct } = useSubscription();
const { currentProject } = useProjects();
const { domains } = useDomains();
const form = useForm({
initialValues: {
name: currentProject?.name || "",
+ retention_period_days: currentProject?.retention_period_days || 1,
},
validate: {
name: (value) => {
@@ -119,11 +137,13 @@ export default function ProjectSettings() {
form.resetDirty();
};
+ const max_retention = currentProduct ? MAX_RETENTION[currentProduct] : 1;
+
return (
<>
Project Settings