mirror of
https://github.com/formbricks/formbricks.git
synced 2025-12-30 10:19:51 -06:00
Compare commits
12 Commits
formbricks
...
action-env
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
41bc62fb3b | ||
|
|
041f26ab2d | ||
|
|
d03b01cb67 | ||
|
|
c7981fc4d9 | ||
|
|
a1719c2258 | ||
|
|
a5275c2ead | ||
|
|
eec71d9a98 | ||
|
|
d547a70013 | ||
|
|
8870b7d710 | ||
|
|
3c8f6945ba | ||
|
|
e1bd66b009 | ||
|
|
abbd69e355 |
@@ -1,12 +1,16 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { createActionClassAction } from "@/app/(app)/(survey-editor)/environments/[environmentId]/surveys/[surveyId]/edit/actions";
|
||||||
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
||||||
import { Code2Icon, MousePointerClickIcon, SparklesIcon } from "lucide-react";
|
import { Code2Icon, MousePointerClickIcon, SparklesIcon } from "lucide-react";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
|
import toast from "react-hot-toast";
|
||||||
import { convertDateTimeStringShort } from "@formbricks/lib/time";
|
import { convertDateTimeStringShort } from "@formbricks/lib/time";
|
||||||
import { capitalizeFirstLetter } from "@formbricks/lib/utils/strings";
|
import { capitalizeFirstLetter } from "@formbricks/lib/utils/strings";
|
||||||
import { TActionClass } from "@formbricks/types/action-classes";
|
import { TActionClass, TActionClassInput, TActionClassInputCode } from "@formbricks/types/action-classes";
|
||||||
|
import { TEnvironment } from "@formbricks/types/environment";
|
||||||
|
import { Button } from "@formbricks/ui/components/Button";
|
||||||
import { ErrorComponent } from "@formbricks/ui/components/ErrorComponent";
|
import { ErrorComponent } from "@formbricks/ui/components/ErrorComponent";
|
||||||
import { Label } from "@formbricks/ui/components/Label";
|
import { Label } from "@formbricks/ui/components/Label";
|
||||||
import { LoadingSpinner } from "@formbricks/ui/components/LoadingSpinner";
|
import { LoadingSpinner } from "@formbricks/ui/components/LoadingSpinner";
|
||||||
@@ -15,15 +19,25 @@ import { getActiveInactiveSurveysAction } from "../actions";
|
|||||||
interface ActivityTabProps {
|
interface ActivityTabProps {
|
||||||
actionClass: TActionClass;
|
actionClass: TActionClass;
|
||||||
environmentId: string;
|
environmentId: string;
|
||||||
|
environment: TEnvironment;
|
||||||
|
otherEnvActionClasses: TActionClass[];
|
||||||
|
otherEnvironment: TEnvironment;
|
||||||
|
isReadOnly: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ActionActivityTab = ({ actionClass, environmentId }: ActivityTabProps) => {
|
export const ActionActivityTab = ({
|
||||||
|
actionClass,
|
||||||
|
otherEnvActionClasses,
|
||||||
|
otherEnvironment,
|
||||||
|
environmentId,
|
||||||
|
environment,
|
||||||
|
isReadOnly,
|
||||||
|
}: ActivityTabProps) => {
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
const [activeSurveys, setActiveSurveys] = useState<string[] | undefined>();
|
const [activeSurveys, setActiveSurveys] = useState<string[] | undefined>();
|
||||||
const [inactiveSurveys, setInactiveSurveys] = useState<string[] | undefined>();
|
const [inactiveSurveys, setInactiveSurveys] = useState<string[] | undefined>();
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState<Error | null>(null);
|
const [error, setError] = useState<Error | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
@@ -45,6 +59,57 @@ export const ActionActivityTab = ({ actionClass, environmentId }: ActivityTabPro
|
|||||||
updateState();
|
updateState();
|
||||||
}, [actionClass.id, environmentId]);
|
}, [actionClass.id, environmentId]);
|
||||||
|
|
||||||
|
const actionClassNames = useMemo(
|
||||||
|
() => otherEnvActionClasses.map((actionClass) => actionClass.name),
|
||||||
|
[otherEnvActionClasses]
|
||||||
|
);
|
||||||
|
|
||||||
|
const actionClassKeys = useMemo(() => {
|
||||||
|
const codeActionClasses: TActionClassInputCode[] = otherEnvActionClasses.filter(
|
||||||
|
(actionClass) => actionClass.type === "code"
|
||||||
|
) as TActionClassInputCode[];
|
||||||
|
|
||||||
|
return codeActionClasses.map((actionClass) => actionClass.key);
|
||||||
|
}, [otherEnvActionClasses]);
|
||||||
|
|
||||||
|
const copyAction = async (data: TActionClassInput) => {
|
||||||
|
const { type } = data;
|
||||||
|
let copyName = data.name;
|
||||||
|
try {
|
||||||
|
if (isReadOnly) {
|
||||||
|
throw new Error(t("common.you_are_not_authorised_to_perform_this_action"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (copyName && actionClassNames.includes(copyName)) {
|
||||||
|
while (actionClassNames.includes(copyName)) {
|
||||||
|
copyName += " (copy)";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === "code" && data.key && actionClassKeys.includes(data.key)) {
|
||||||
|
throw new Error(t("environments.actions.action_with_key_already_exists", { key: data.key }));
|
||||||
|
}
|
||||||
|
|
||||||
|
let updatedAction = {
|
||||||
|
...data,
|
||||||
|
name: copyName.trim(),
|
||||||
|
environmentId: otherEnvironment.id,
|
||||||
|
};
|
||||||
|
|
||||||
|
const createActionClassResponse = await createActionClassAction({
|
||||||
|
action: updatedAction as TActionClassInput,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!createActionClassResponse?.data) {
|
||||||
|
throw new Error(t("environments.actions.action_copy_failed", {}));
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.success(t("environments.actions.action_copied_successfully"));
|
||||||
|
} catch (e: any) {
|
||||||
|
toast.error(e.message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (loading) return <LoadingSpinner />;
|
if (loading) return <LoadingSpinner />;
|
||||||
if (error) return <ErrorComponent />;
|
if (error) return <ErrorComponent />;
|
||||||
|
|
||||||
@@ -98,6 +163,22 @@ export const ActionActivityTab = ({ actionClass, environmentId }: ActivityTabPro
|
|||||||
<p className="text-sm text-slate-700">{capitalizeFirstLetter(actionClass.type)}</p>
|
<p className="text-sm text-slate-700">{capitalizeFirstLetter(actionClass.type)}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="">
|
||||||
|
<Label className="text-xs font-normal text-slate-500">Environment</Label>
|
||||||
|
<div className="items-center-center flex gap-2">
|
||||||
|
<p className="text-xs text-slate-700">
|
||||||
|
{environment.type === "development" ? "Development" : "Production"}
|
||||||
|
</p>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
copyAction(actionClass);
|
||||||
|
}}
|
||||||
|
className="m-0 p-0 text-xs font-medium text-black underline underline-offset-4 focus:ring-0 focus:ring-offset-0"
|
||||||
|
variant="minimal">
|
||||||
|
{environment.type === "development" ? "Copy to Production" : "Copy to Development"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,20 +2,27 @@
|
|||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { TActionClass } from "@formbricks/types/action-classes";
|
import { TActionClass } from "@formbricks/types/action-classes";
|
||||||
|
import { TEnvironment } from "@formbricks/types/environment";
|
||||||
import { ActionDetailModal } from "./ActionDetailModal";
|
import { ActionDetailModal } from "./ActionDetailModal";
|
||||||
|
|
||||||
interface ActionClassesTableProps {
|
interface ActionClassesTableProps {
|
||||||
environmentId: string;
|
environmentId: string;
|
||||||
actionClasses: TActionClass[];
|
actionClasses: TActionClass[];
|
||||||
|
environment: TEnvironment;
|
||||||
children: [JSX.Element, JSX.Element[]];
|
children: [JSX.Element, JSX.Element[]];
|
||||||
isReadOnly: boolean;
|
isReadOnly: boolean;
|
||||||
|
otherEnvironment: TEnvironment;
|
||||||
|
otherEnvActionClasses: TActionClass[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ActionClassesTable = ({
|
export const ActionClassesTable = ({
|
||||||
environmentId,
|
environmentId,
|
||||||
actionClasses,
|
actionClasses,
|
||||||
|
environment,
|
||||||
children: [TableHeading, actionRows],
|
children: [TableHeading, actionRows],
|
||||||
isReadOnly,
|
isReadOnly,
|
||||||
|
otherEnvActionClasses,
|
||||||
|
otherEnvironment,
|
||||||
}: ActionClassesTableProps) => {
|
}: ActionClassesTableProps) => {
|
||||||
const [isActionDetailModalOpen, setActionDetailModalOpen] = useState(false);
|
const [isActionDetailModalOpen, setActionDetailModalOpen] = useState(false);
|
||||||
|
|
||||||
@@ -48,11 +55,14 @@ export const ActionClassesTable = ({
|
|||||||
{activeActionClass && (
|
{activeActionClass && (
|
||||||
<ActionDetailModal
|
<ActionDetailModal
|
||||||
environmentId={environmentId}
|
environmentId={environmentId}
|
||||||
|
environment={environment}
|
||||||
open={isActionDetailModalOpen}
|
open={isActionDetailModalOpen}
|
||||||
setOpen={setActionDetailModalOpen}
|
setOpen={setActionDetailModalOpen}
|
||||||
actionClasses={actionClasses}
|
actionClasses={actionClasses}
|
||||||
actionClass={activeActionClass}
|
actionClass={activeActionClass}
|
||||||
isReadOnly={isReadOnly}
|
isReadOnly={isReadOnly}
|
||||||
|
otherEnvActionClasses={otherEnvActionClasses}
|
||||||
|
otherEnvironment={otherEnvironment}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -1,17 +1,21 @@
|
|||||||
import { Code2Icon, MousePointerClickIcon, SparklesIcon } from "lucide-react";
|
import { Code2Icon, MousePointerClickIcon, SparklesIcon } from "lucide-react";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { TActionClass } from "@formbricks/types/action-classes";
|
import { TActionClass } from "@formbricks/types/action-classes";
|
||||||
|
import { TEnvironment } from "@formbricks/types/environment";
|
||||||
import { ModalWithTabs } from "@formbricks/ui/components/ModalWithTabs";
|
import { ModalWithTabs } from "@formbricks/ui/components/ModalWithTabs";
|
||||||
import { ActionActivityTab } from "./ActionActivityTab";
|
import { ActionActivityTab } from "./ActionActivityTab";
|
||||||
import { ActionSettingsTab } from "./ActionSettingsTab";
|
import { ActionSettingsTab } from "./ActionSettingsTab";
|
||||||
|
|
||||||
interface ActionDetailModalProps {
|
interface ActionDetailModalProps {
|
||||||
environmentId: string;
|
environmentId: string;
|
||||||
|
environment: TEnvironment;
|
||||||
open: boolean;
|
open: boolean;
|
||||||
setOpen: (v: boolean) => void;
|
setOpen: (v: boolean) => void;
|
||||||
actionClass: TActionClass;
|
actionClass: TActionClass;
|
||||||
actionClasses: TActionClass[];
|
actionClasses: TActionClass[];
|
||||||
isReadOnly: boolean;
|
isReadOnly: boolean;
|
||||||
|
otherEnvironment: TEnvironment;
|
||||||
|
otherEnvActionClasses: TActionClass[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ActionDetailModal = ({
|
export const ActionDetailModal = ({
|
||||||
@@ -20,13 +24,25 @@ export const ActionDetailModal = ({
|
|||||||
setOpen,
|
setOpen,
|
||||||
actionClass,
|
actionClass,
|
||||||
actionClasses,
|
actionClasses,
|
||||||
|
environment,
|
||||||
isReadOnly,
|
isReadOnly,
|
||||||
|
otherEnvActionClasses,
|
||||||
|
otherEnvironment,
|
||||||
}: ActionDetailModalProps) => {
|
}: ActionDetailModalProps) => {
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
const tabs = [
|
const tabs = [
|
||||||
{
|
{
|
||||||
title: t("common.activity"),
|
title: t("common.activity"),
|
||||||
children: <ActionActivityTab actionClass={actionClass} environmentId={environmentId} />,
|
children: (
|
||||||
|
<ActionActivityTab
|
||||||
|
otherEnvActionClasses={otherEnvActionClasses}
|
||||||
|
otherEnvironment={otherEnvironment}
|
||||||
|
isReadOnly={isReadOnly}
|
||||||
|
environment={environment}
|
||||||
|
actionClass={actionClass}
|
||||||
|
environmentId={environmentId}
|
||||||
|
/>
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t("common.settings"),
|
title: t("common.settings"),
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { getTranslations } from "next-intl/server";
|
|||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { getActionClasses } from "@formbricks/lib/actionClass/service";
|
import { getActionClasses } from "@formbricks/lib/actionClass/service";
|
||||||
import { authOptions } from "@formbricks/lib/authOptions";
|
import { authOptions } from "@formbricks/lib/authOptions";
|
||||||
|
import { getEnvironments } from "@formbricks/lib/environment/service";
|
||||||
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
|
import { getMembershipByUserIdOrganizationId } from "@formbricks/lib/membership/service";
|
||||||
import { getAccessFlags } from "@formbricks/lib/membership/utils";
|
import { getAccessFlags } from "@formbricks/lib/membership/utils";
|
||||||
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
|
import { getOrganizationByEnvironmentId } from "@formbricks/lib/organization/service";
|
||||||
@@ -44,6 +45,17 @@ const Page = async ({ params }) => {
|
|||||||
throw new Error(t("common.product_not_found"));
|
throw new Error(t("common.product_not_found"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const environments = await getEnvironments(product.id);
|
||||||
|
const currentEnvironment = environments.find((env) => env.id === params.environmentId);
|
||||||
|
|
||||||
|
if (!currentEnvironment) {
|
||||||
|
throw new Error(t("common.environment_not_found"));
|
||||||
|
}
|
||||||
|
|
||||||
|
const otherEnvironment = environments.filter((env) => env.id !== params.environmentId)[0];
|
||||||
|
|
||||||
|
const otherEnvActionClasses = await getActionClasses(otherEnvironment.id);
|
||||||
|
|
||||||
const currentUserMembership = await getMembershipByUserIdOrganizationId(session?.user.id, organization.id);
|
const currentUserMembership = await getMembershipByUserIdOrganizationId(session?.user.id, organization.id);
|
||||||
const { isMember, isBilling } = getAccessFlags(currentUserMembership?.role);
|
const { isMember, isBilling } = getAccessFlags(currentUserMembership?.role);
|
||||||
|
|
||||||
@@ -69,6 +81,9 @@ const Page = async ({ params }) => {
|
|||||||
<PageContentWrapper>
|
<PageContentWrapper>
|
||||||
<PageHeader pageTitle={t("common.actions")} cta={!isReadOnly ? renderAddActionButton() : undefined} />
|
<PageHeader pageTitle={t("common.actions")} cta={!isReadOnly ? renderAddActionButton() : undefined} />
|
||||||
<ActionClassesTable
|
<ActionClassesTable
|
||||||
|
environment={currentEnvironment}
|
||||||
|
otherEnvironment={otherEnvironment}
|
||||||
|
otherEnvActionClasses={otherEnvActionClasses}
|
||||||
environmentId={params.environmentId}
|
environmentId={params.environmentId}
|
||||||
actionClasses={actionClasses}
|
actionClasses={actionClasses}
|
||||||
isReadOnly={isReadOnly}>
|
isReadOnly={isReadOnly}>
|
||||||
|
|||||||
@@ -488,6 +488,8 @@
|
|||||||
},
|
},
|
||||||
"environments": {
|
"environments": {
|
||||||
"actions": {
|
"actions": {
|
||||||
|
"action_copied_successfully": "Aktion erfolgreich kopiert",
|
||||||
|
"action_copy_failed": "Aktion konnte nicht kopiert werden",
|
||||||
"action_created_successfully": "Aktion erfolgreich erstellt",
|
"action_created_successfully": "Aktion erfolgreich erstellt",
|
||||||
"action_deleted_successfully": "Aktion erfolgreich gelöscht",
|
"action_deleted_successfully": "Aktion erfolgreich gelöscht",
|
||||||
"action_type": "Aktionstyp",
|
"action_type": "Aktionstyp",
|
||||||
|
|||||||
@@ -488,6 +488,8 @@
|
|||||||
},
|
},
|
||||||
"environments": {
|
"environments": {
|
||||||
"actions": {
|
"actions": {
|
||||||
|
"action_copied_successfully": "Action copied successfully",
|
||||||
|
"action_copy_failed": "Action copy failed",
|
||||||
"action_created_successfully": "Action created successfully",
|
"action_created_successfully": "Action created successfully",
|
||||||
"action_deleted_successfully": "Action deleted successfully",
|
"action_deleted_successfully": "Action deleted successfully",
|
||||||
"action_type": "Action Type",
|
"action_type": "Action Type",
|
||||||
|
|||||||
@@ -488,6 +488,8 @@
|
|||||||
},
|
},
|
||||||
"environments": {
|
"environments": {
|
||||||
"actions": {
|
"actions": {
|
||||||
|
"action_copied_successfully": "Ação copiada com sucesso",
|
||||||
|
"action_copy_failed": "Falha ao copiar a ação",
|
||||||
"action_created_successfully": "Ação criada com sucesso",
|
"action_created_successfully": "Ação criada com sucesso",
|
||||||
"action_deleted_successfully": "Ação deletada com sucesso",
|
"action_deleted_successfully": "Ação deletada com sucesso",
|
||||||
"action_type": "Tipo de Ação",
|
"action_type": "Tipo de Ação",
|
||||||
|
|||||||
Reference in New Issue
Block a user