diff --git a/packages/i18n/src/locales/en/translations.json b/packages/i18n/src/locales/en/translations.json index 0630f87d4a..64b62e2f46 100644 --- a/packages/i18n/src/locales/en/translations.json +++ b/packages/i18n/src/locales/en/translations.json @@ -1354,6 +1354,8 @@ "exporting": "Exporting", "previous_exports": "Previous exports", "export_separate_files": "Export the data into separate files", + "exporting_projects": "Exporting project", + "format": "Format", "modal": { "title": "Export to", "toasts": { diff --git a/web/app/[workspaceSlug]/(settings)/settings/(workspace)/layout.tsx b/web/app/[workspaceSlug]/(settings)/settings/(workspace)/layout.tsx index c0d1d0becc..b3306e2c2c 100644 --- a/web/app/[workspaceSlug]/(settings)/settings/(workspace)/layout.tsx +++ b/web/app/[workspaceSlug]/(settings)/settings/(workspace)/layout.tsx @@ -7,10 +7,10 @@ import { usePathname } from "next/navigation"; import { EUserWorkspaceRoles, WORKSPACE_SETTINGS_ACCESS } from "@plane/constants"; // hooks import { NotAuthorizedView } from "@/components/auth-screens"; +import { CommandPalette } from "@/components/command-palette"; import { SettingsContentWrapper } from "@/components/settings"; import { useUserPermissions } from "@/hooks/store"; // local components -import { MobileWorkspaceSettingsTabs } from "./mobile-header-tabs"; import { WorkspaceSettingsSidebar } from "./sidebar"; export interface IWorkspaceSettingLayout { @@ -42,7 +42,7 @@ const WorkspaceSettingLayout: FC = observer((props) => return ( <> - +
{workspaceUserInfo && !isAuthorized ? ( diff --git a/web/core/components/exporter/column.tsx b/web/core/components/exporter/column.tsx new file mode 100644 index 0000000000..113478c9db --- /dev/null +++ b/web/core/components/exporter/column.tsx @@ -0,0 +1,112 @@ +import { Download } from "lucide-react"; +import { IExportData } from "@plane/types"; +import { getDate, getFileURL, renderFormattedDate } from "@plane/utils"; + +type RowData = IExportData; +const checkExpiry = (inputDateString: string) => { + const currentDate = new Date(); + const expiryDate = getDate(inputDateString); + if (!expiryDate) return false; + expiryDate.setDate(expiryDate.getDate() + 7); + return expiryDate > currentDate; +}; +export const useExportColumns = () => { + const columns = [ + { + key: "Exported By", + content: "Exported By", + tdRender: (rowData: RowData) => { + const { avatar_url, display_name, email } = rowData.initiated_by_detail; + return ( +
+
+ {avatar_url && avatar_url.trim() !== "" ? ( + + {display_name + + ) : ( + + {(email ?? display_name ?? "?")[0]} + + )} +
+
{display_name}
+
+ ); + }, + }, + { + key: "Exported On", + content: "Exported On", + tdRender: (rowData: RowData) => {renderFormattedDate(rowData.created_at)}, + }, + + { + key: "Exported projects", + content: "Exported projects", + tdRender: (rowData: RowData) =>
{rowData.project.length} project(s)
, + }, + { + key: "Format", + content: "Format", + tdRender: (rowData: RowData) => ( + + {rowData.provider === "csv" + ? "CSV" + : rowData.provider === "xlsx" + ? "Excel" + : rowData.provider === "json" + ? "JSON" + : ""} + + ), + }, + { + key: "Status", + content: "Status", + tdRender: (rowData: RowData) => ( + + {rowData.status} + + ), + }, + { + key: "Download", + content: "Download", + tdRender: (rowData: RowData) => + checkExpiry(rowData.created_at) ? ( + <> + {rowData.status == "completed" ? ( + + + + ) : ( + "-" + )} + + ) : ( +
Expired
+ ), + }, + ]; + return columns; +}; diff --git a/web/core/components/exporter/export-form.tsx b/web/core/components/exporter/export-form.tsx new file mode 100644 index 0000000000..b108d2e4c5 --- /dev/null +++ b/web/core/components/exporter/export-form.tsx @@ -0,0 +1,172 @@ +import { useState } from "react"; +import { intersection } from "lodash"; +import { Controller, useForm } from "react-hook-form"; +import { EUserPermissions, EUserPermissionsLevel, EXPORTERS_LIST } from "@plane/constants"; +import { useTranslation } from "@plane/i18n"; +import { Button, CustomSearchSelect, CustomSelect, TOAST_TYPE, setToast } from "@plane/ui"; +import { useProject, useUser, useUserPermissions } from "@/hooks/store"; +import { ProjectExportService } from "@/services/project/project-export.service"; + +type Props = { + workspaceSlug: string; + provider: string | null; + mutateServices: () => void; +}; +type FormData = { + provider: (typeof EXPORTERS_LIST)[0]; + project: string[]; + multiple: boolean; +}; +const projectExportService = new ProjectExportService(); + +export const ExportForm = (props: Props) => { + // props + const { workspaceSlug, mutateServices } = props; + // states + const [exportLoading, setExportLoading] = useState(false); + // store hooks + const { allowPermissions } = useUserPermissions(); + const { data: user, canPerformAnyCreateAction, projectsWithCreatePermissions } = useUser(); + const { workspaceProjectIds, getProjectById } = useProject(); + const { t } = useTranslation(); + // form + const { handleSubmit, control } = useForm({ + defaultValues: { + provider: EXPORTERS_LIST[0], + project: [], + multiple: false, + }, + }); + // derived values + const hasProjects = workspaceProjectIds && workspaceProjectIds.length > 0; + const isAdmin = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.WORKSPACE); + const wsProjectIdsWithCreatePermisisons = projectsWithCreatePermissions + ? intersection(workspaceProjectIds, Object.keys(projectsWithCreatePermissions)) + : []; + const options = wsProjectIdsWithCreatePermisisons?.map((projectId) => { + const projectDetails = getProjectById(projectId); + + return { + value: projectDetails?.id, + query: `${projectDetails?.name} ${projectDetails?.identifier}`, + content: ( +
+ {projectDetails?.identifier} + {projectDetails?.name} +
+ ), + }; + }); + + // handlers + const ExportCSVToMail = async (formData: FormData) => { + console.log(formData); + setExportLoading(true); + if (workspaceSlug && user) { + const payload = { + provider: formData.provider.provider, + project: formData.project, + multiple: formData.project.length > 1, + }; + await projectExportService + .csvExport(workspaceSlug as string, payload) + .then(() => { + mutateServices(); + setExportLoading(false); + setToast({ + type: TOAST_TYPE.SUCCESS, + title: t("workspace_settings.settings.exports.modal.toasts.success.title"), + message: t("workspace_settings.settings.exports.modal.toasts.success.message", { + entity: + formData.provider.provider === "csv" + ? "CSV" + : formData.provider.provider === "xlsx" + ? "Excel" + : formData.provider.provider === "json" + ? "JSON" + : "", + }), + }); + }) + .catch(() => { + setExportLoading(false); + setToast({ + type: TOAST_TYPE.ERROR, + title: t("error"), + message: t("workspace_settings.settings.exports.modal.toasts.error.message"), + }); + }); + } + }; + return ( +
+
+ {/* Project Selector */} +
+
+ {t("workspace_settings.settings.exports.exporting_projects")} +
+ ( + onChange(val)} + options={options} + input + label={ + value && value.length > 0 + ? value + .map((projectId) => { + const projectDetails = getProjectById(projectId); + + return projectDetails?.identifier; + }) + .join(", ") + : "All projects" + } + optionsClassName="max-w-48 sm:max-w-[532px]" + placement="bottom-end" + multiple + /> + )} + /> +
+ {/* Format Selector */} +
+
+ {t("workspace_settings.settings.exports.format")} +
+ ( + + {EXPORTERS_LIST.map((service) => ( + + {t(service.i18n_title)} + + ))} + + )} + /> +
+
+
+ +
+
+ ); +}; diff --git a/web/core/components/exporter/guide.tsx b/web/core/components/exporter/guide.tsx index 1dfd900107..024cd059eb 100644 --- a/web/core/components/exporter/guide.tsx +++ b/web/core/components/exporter/guide.tsx @@ -1,221 +1,38 @@ "use client"; -import { useEffect, useState } from "react"; +import { useState } from "react"; import { observer } from "mobx-react"; -import Image from "next/image"; -import Link from "next/link"; import { useParams, useSearchParams } from "next/navigation"; -import useSWR, { mutate } from "swr"; -// icons -import { MoveLeft, MoveRight, RefreshCw } from "lucide-react"; -// plane imports -import { EXPORTERS_LIST, EUserPermissions, EUserPermissionsLevel } from "@plane/constants"; -import { useTranslation } from "@plane/i18n"; -import { Button } from "@plane/ui"; -// components -import { DetailedEmptyState } from "@/components/empty-state"; -import { Exporter, SingleExport } from "@/components/exporter"; -import { ImportExportSettingsLoader } from "@/components/ui"; -// constants +import { mutate } from "swr"; import { EXPORT_SERVICES_LIST } from "@/constants/fetch-keys"; -// hooks -import { useProject, useUser, useUserPermissions } from "@/hooks/store"; -import { useAppRouter } from "@/hooks/use-app-router"; -import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; -// services images -import CSVLogo from "@/public/services/csv.svg"; -import ExcelLogo from "@/public/services/excel.svg"; -import JSONLogo from "@/public/services/json.svg"; -// services -import { IntegrationService } from "@/services/integrations"; - -const integrationService = new IntegrationService(); - -const getExporterLogo = (provider: string) => { - switch (provider) { - case "csv": - return CSVLogo; - case "excel": - return ExcelLogo; - case "xlsx": - return ExcelLogo; - case "json": - return JSONLogo; - default: - return ""; - } -}; +import { ExportForm } from "./export-form"; +import { PrevExports } from "./prev-exports"; const IntegrationGuide = observer(() => { - // states - const [refreshing, setRefreshing] = useState(false); - const per_page = 10; - const [cursor, setCursor] = useState(`10:0:0`); // router - const router = useAppRouter(); const { workspaceSlug } = useParams(); const searchParams = useSearchParams(); const provider = searchParams.get("provider"); - // plane hooks - const { t } = useTranslation(); - // store hooks - const { data: currentUser, canPerformAnyCreateAction } = useUser(); - const { allowPermissions } = useUserPermissions(); - const { workspaceProjectIds } = useProject(); - // derived values - const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/workspace-settings/exports" }); - - const { data: exporterServices } = useSWR( - workspaceSlug && cursor ? EXPORT_SERVICES_LIST(workspaceSlug as string, cursor, `${per_page}`) : null, - workspaceSlug && cursor - ? () => integrationService.getExportsServicesList(workspaceSlug as string, cursor, per_page) - : null - ); - - const handleRefresh = () => { - setRefreshing(true); - mutate(EXPORT_SERVICES_LIST(workspaceSlug as string, `${cursor}`, `${per_page}`)).then(() => setRefreshing(false)); - }; - - const handleCsvClose = () => { - router.replace(`/${workspaceSlug?.toString()}/settings/exports`); - }; - - const hasProjects = workspaceProjectIds && workspaceProjectIds.length > 0; - const isAdmin = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.WORKSPACE); - - useEffect(() => { - const interval = setInterval(() => { - if (exporterServices?.results?.some((service) => service.status === "processing")) { - handleRefresh(); - } else { - clearInterval(interval); - } - }, 3000); - - return () => clearInterval(interval); - }, [exporterServices]); + // state + const per_page = 10; + const [cursor, setCursor] = useState(`10:0:0`); return ( <>
<> -
- {EXPORTERS_LIST.map((service) => ( -
-
-
-
- {`${t(service.i18n_title)} -
-
-

{t(service.i18n_title)}

-

{t(service.i18n_description)}

-
-
-
- - - - - -
-
-
- ))} -
-
-
-
-

- {t("workspace_settings.settings.exports.previous_exports")} -

- - -
-
- - -
-
-
- {exporterServices && exporterServices?.results ? ( - exporterServices?.results?.length > 0 ? ( -
-
- {exporterServices?.results.map((service) => ( - - ))} -
-
- ) : ( -
- -
- ) - ) : ( - - )} -
-
- - {provider && ( - handleCsvClose()} - data={null} - user={currentUser || null} + mutate(EXPORT_SERVICES_LIST(workspaceSlug as string, `${cursor}`, `${per_page}`))} /> - )} + +
); diff --git a/web/core/components/exporter/prev-exports.tsx b/web/core/components/exporter/prev-exports.tsx new file mode 100644 index 0000000000..ce246a3fe5 --- /dev/null +++ b/web/core/components/exporter/prev-exports.tsx @@ -0,0 +1,135 @@ +import { useEffect, useState } from "react"; +import { observer } from "mobx-react"; +import useSWR, { mutate } from "swr"; +import { MoveLeft, MoveRight, RefreshCw } from "lucide-react"; +import { useTranslation } from "@plane/i18n"; +import { IExportData } from "@plane/types"; +import { Table } from "@plane/ui"; +import { EXPORT_SERVICES_LIST } from "@/constants/fetch-keys"; +import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; +import { IntegrationService } from "@/services/integrations"; +import { DetailedEmptyState } from "../empty-state"; +import { ImportExportSettingsLoader } from "../ui"; +import { useExportColumns } from "./column"; + +const integrationService = new IntegrationService(); + +type Props = { + workspaceSlug: string; + cursor: string | undefined; + per_page: number; + setCursor: (cursor: string) => void; +}; +type RowData = IExportData; +export const PrevExports = observer((props: Props) => { + // props + const { workspaceSlug, cursor, per_page, setCursor } = props; + // state + const [refreshing, setRefreshing] = useState(false); + // hooks + const { t } = useTranslation(); + const columns = useExportColumns(); + const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/workspace-settings/exports" }); + + const { data: exporterServices } = useSWR( + workspaceSlug && cursor ? EXPORT_SERVICES_LIST(workspaceSlug as string, cursor, `${per_page}`) : null, + workspaceSlug && cursor + ? () => integrationService.getExportsServicesList(workspaceSlug as string, cursor, per_page) + : null + ); + + const handleRefresh = () => { + setRefreshing(true); + mutate(EXPORT_SERVICES_LIST(workspaceSlug as string, `${cursor}`, `${per_page}`)).then(() => setRefreshing(false)); + }; + + useEffect(() => { + const interval = setInterval(() => { + if (exporterServices?.results?.some((service) => service.status === "processing")) { + handleRefresh(); + } else { + clearInterval(interval); + } + }, 3000); + + return () => clearInterval(interval); + }, [exporterServices]); + + return ( +
+
+
+

+ {t("workspace_settings.settings.exports.previous_exports")} +

+ + +
+
+ + +
+
+ +
+ {exporterServices && exporterServices?.results ? ( + exporterServices?.results?.length > 0 ? ( +
+
+ rowData?.id ?? ""} + tHeadClassName="border-b border-custom-border-100" + thClassName="text-left font-medium divide-x-0 text-custom-text-400" + tBodyClassName="divide-y-0" + tBodyTrClassName="divide-x-0 p-4 h-[50px] text-custom-text-200" + tHeadTrClassName="divide-x-0" + /> + + + ) : ( +
+ +
+ ) + ) : ( + + )} + + + ); +});