snapshot of https://github.com/makeplane/plane.git @ d868349a55de79979eb8b98701bf1702c67ad634 (2026-09-15)
Some checks failed
CodeQL / Analyze (javascript) (push) Failing after 16s
CodeQL / Analyze (python) (push) Failing after 3s
i18n sync check / check:sync (push) Failing after 3s
React Doctor / react-doctor (push) Failing after 16s
Branch Build CE / Build-Push Admin Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge Admin Manifest (push) Has been cancelled
Branch Build CE / Build-Push Web Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge Web Manifest (push) Has been cancelled
Branch Build CE / Build-Push Space Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge Proxy Manifest (push) Has been cancelled
Branch Build CE / Merge Space Manifest (push) Has been cancelled
Branch Build CE / Prepare AIO Assets (push) Has been cancelled
Branch Build CE / Build Setup (push) Has been cancelled
Branch Build CE / Build-Push Live Collaboration Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge Live Collaboration Manifest (push) Has been cancelled
Branch Build CE / Build-Push API Server Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge API Server Manifest (push) Has been cancelled
Branch Build CE / Build-Push Proxy Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Build-Push AIO Docker Image (${{ matrix.arch }}) (push) Has been cancelled
Branch Build CE / Merge AIO Manifest (push) Has been cancelled
Branch Build CE / Upload Build Assets (push) Has been cancelled
Branch Build CE / Build Release (push) Has been cancelled

This commit is contained in:
upstream-mirror
2026-09-15 15:14:58 +05:00
commit 23059a1de3
5252 changed files with 573834 additions and 0 deletions

View File

@@ -0,0 +1,118 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import { DownloadOutline } from "@makeplane/propel/icons";
import type { 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 (
<div className="flex items-center gap-x-2">
<div>
{avatar_url && avatar_url.trim() !== "" ? (
<span className="relative flex h-4 w-4 items-center justify-center rounded-full text-on-color capitalize">
<img
src={getFileURL(avatar_url)}
className="absolute top-0 left-0 h-full w-full rounded-full object-cover"
alt={display_name || email}
/>
</span>
) : (
<span className="bg-gray-700 relative flex h-4 w-4 items-center justify-center rounded-full text-11 text-on-color capitalize">
{(email ?? display_name ?? "?")[0]}
</span>
)}
</div>
<div>{display_name}</div>
</div>
);
},
},
{
key: "Exported On",
content: "Exported On",
tdRender: (rowData: RowData) => <span>{renderFormattedDate(rowData.created_at)}</span>,
},
{
key: "Exported projects",
content: "Exported projects",
tdRender: (rowData: RowData) => <div className="text-13">{rowData.project.length} project(s)</div>,
},
{
key: "Format",
content: "Format",
tdRender: (rowData: RowData) => (
<span className="text-13">
{rowData.provider === "csv"
? "CSV"
: rowData.provider === "xlsx"
? "Excel"
: rowData.provider === "json"
? "JSON"
: ""}
</span>
),
},
{
key: "Status",
content: "Status",
tdRender: (rowData: RowData) => (
<span
className={`rounded-sm px-2 py-1 text-11 capitalize ${
rowData.status === "completed"
? "bg-success-subtle text-success-primary"
: rowData.status === "processing"
? "bg-yellow-500/20 text-yellow-500"
: rowData.status === "failed"
? "bg-danger-subtle text-danger-primary"
: rowData.status === "expired"
? "bg-orange-500/20 text-orange-500"
: "bg-gray-500/20 text-gray-500"
}`}
>
{rowData.status}
</span>
),
},
{
key: "Download",
content: "Download",
tdRender: (rowData: RowData) =>
checkExpiry(rowData.created_at) ? (
<>
{rowData.status == "completed" ? (
<a target="_blank" href={rowData?.url} rel="noopener noreferrer">
<button className="flex w-full items-center gap-1 font-medium text-accent-primary">
<DownloadOutline className="h-4 w-4" />
<div>Download</div>
</button>
</a>
) : (
"-"
)}
</>
) : (
<div className="text-11 text-danger-primary">Expired</div>
),
},
];
return columns;
};

View File

@@ -0,0 +1,251 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import { useState } from "react";
import { intersection } from "lodash-es";
import { observer } from "mobx-react";
import { Controller, useForm } from "react-hook-form";
// import { Info } from "lucide-react";
import {
EUserPermissions,
EUserPermissionsLevel,
EXPORTERS_LIST,
// ISSUE_DISPLAY_FILTERS_BY_PAGE,
} from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { Button } from "@plane/propel/button";
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
// import { Tooltip } from "@makeplane/propel/components/tooltip";
// import { EIssuesStoreType } from "@plane/types";
import type { TWorkItemFilterExpression } from "@plane/types";
import { CustomSearchSelect, CustomSelect } from "@plane/ui";
// import { WorkspaceLevelWorkItemFiltersHOC } from "@/components/work-item-filters/filters-hoc/workspace-level";
// import { WorkItemFiltersRow } from "@/components/work-item-filters/filters-row";
import { useProject } from "@/hooks/store/use-project";
import { useUser, useUserPermissions } from "@/hooks/store/user";
import { ProjectExportService } from "@/services/project/project-export.service";
// local imports
import { SettingsBoxedControlItem } from "../settings/boxed-control-item";
type Props = {
workspaceSlug: string;
provider: string | null;
mutateServices: () => void;
};
type FormData = {
provider: (typeof EXPORTERS_LIST)[0];
project: string[];
multiple: boolean;
filters: TWorkItemFilterExpression;
};
// const initialWorkItemFilters = {
// richFilters: {},
// displayFilters: {},
// displayProperties: {},
// kanbanFilters: {
// group_by: [],
// sub_group_by: [],
// },
// };
const projectExportService = new ProjectExportService();
export const ExportForm = observer(function 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<FormData>({
defaultValues: {
provider: EXPORTERS_LIST[0],
project: [],
multiple: false,
filters: {},
},
});
// derived values
const hasProjects = workspaceProjectIds && workspaceProjectIds.length > 0;
const isMember = allowPermissions([EUserPermissions.ADMIN, EUserPermissions.MEMBER], 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: (
<div className="flex items-center gap-2">
<span className="flex-shrink-0 text-10 text-secondary">{projectDetails?.identifier}</span>
<span className="truncate">{projectDetails?.name}</span>
</div>
),
};
});
// handlers
async function ExportCSVToMail(formData: FormData) {
setExportLoading(true);
if (workspaceSlug && user) {
const payload = {
provider: formData.provider.provider,
project: formData.project,
multiple: formData.project.length > 1,
rich_filters: formData.filters,
};
try {
await projectExportService.csvExport(workspaceSlug, payload);
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 (_error) {
setExportLoading(false);
setToast({
type: TOAST_TYPE.ERROR,
title: t("error"),
message: t("workspace_settings.settings.exports.modal.toasts.error.message"),
});
}
} else {
setExportLoading(false);
}
}
return (
<form
onSubmit={(e) => {
void handleSubmit(ExportCSVToMail)(e);
}}
className="flex flex-col gap-5"
>
<div className="rounded-lg border border-subtle bg-layer-2">
{/* Project Selector */}
<SettingsBoxedControlItem
className="rounded-none border-0 border-b"
title={t("workspace_settings.settings.exports.exporting_projects")}
control={
<Controller
control={control}
name="project"
disabled={!isMember && (!hasProjects || !canPerformAnyCreateAction)}
render={({ field: { value, onChange } }) => (
<CustomSearchSelect
value={value ?? []}
onChange={(val: string[]) => 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 */}
<SettingsBoxedControlItem
className="rounded-none border-0 border-b"
title={t("workspace_settings.settings.exports.format")}
control={
<Controller
control={control}
name="provider"
disabled={!isMember && (!hasProjects || !canPerformAnyCreateAction)}
render={({ field: { value, onChange } }) => (
<CustomSelect
value={value}
onChange={onChange}
label={t(value.i18n_title)}
optionsClassName="max-w-48 sm:max-w-[532px]"
placement="bottom-end"
buttonClassName="py-2 text-13"
>
{EXPORTERS_LIST.map((service) => (
<CustomSelect.Option key={service.provider} className="flex items-center gap-2" value={service}>
<span className="truncate">{t(service.i18n_title)}</span>
</CustomSelect.Option>
))}
</CustomSelect>
)}
/>
}
/>
<div className="px-4 py-3">
<Button variant="primary" size="lg" type="submit" loading={exportLoading}>
{exportLoading ? `${t("workspace_settings.settings.exports.exporting")}...` : t("export")}
</Button>
</div>
</div>
{/* Rich Filters */}
{/* <div className="w-full">
<div className="flex items-center gap-2 mb-2">
<div className="text-13 font-medium text-secondary leading-tight">{t("common.filters")}</div>
<Tooltip label={t("workspace_settings.settings.exports.filters_info")} layout="stacked">
<button type="button" className="flex items-center justify-center">
<Info className="h-3 w-3 text-tertiary" />
</button>
</Tooltip>
</div>
<Controller
control={control}
name="filters"
render={({ field: { onChange } }) => (
<WorkspaceLevelWorkItemFiltersHOC
entityId={workspaceSlug}
entityType={EIssuesStoreType.GLOBAL}
filtersToShowByLayout={ISSUE_DISPLAY_FILTERS_BY_PAGE.my_issues.filters}
initialWorkItemFilters={initialWorkItemFilters}
isTemporary
updateFilters={(updatedFilters) => onChange(updatedFilters)}
showOnMount
workspaceSlug={workspaceSlug}
>
{({ filter: workspaceExportWorkItemsFilter }) =>
workspaceExportWorkItemsFilter && (
<WorkItemFiltersRow filter={workspaceExportWorkItemsFilter} variant="modal" />
)
}
</WorkspaceLevelWorkItemFiltersHOC>
)}
/>
</div> */}
</form>
);
});

View File

@@ -0,0 +1,168 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import React, { useState } from "react";
import { intersection } from "lodash-es";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
// types
import { useTranslation } from "@plane/i18n";
import { Button } from "@plane/propel/button";
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import type { IUser, IImporterService } from "@plane/types";
// ui
import { Checkbox } from "@makeplane/propel/components/checkbox";
import { CustomSearchSelect, EModalPosition, EModalWidth, ModalCore } from "@plane/ui";
// hooks
import { useProject } from "@/hooks/store/use-project";
import { useUser } from "@/hooks/store/user";
import { useAppRouter } from "@/hooks/use-app-router";
// services
import { ProjectExportService } from "@/services/project";
type Props = {
isOpen: boolean;
handleClose: () => void;
data: IImporterService | null;
user: IUser | null;
provider: string | string[];
mutateServices: () => void;
};
const projectExportService = new ProjectExportService();
export const Exporter = observer(function Exporter(props: Props) {
const { isOpen, handleClose, user, provider, mutateServices } = props;
// states
const [exportLoading, setExportLoading] = useState(false);
const [isSelectOpen, setIsSelectOpen] = useState(false);
// router
const router = useAppRouter();
const { workspaceSlug } = useParams();
// store hooks
const { workspaceProjectIds, getProjectById } = useProject();
const { projectsWithCreatePermissions } = useUser();
const { t } = useTranslation();
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: (
<div className="flex items-center gap-2">
<span className="flex-shrink-0 text-10 text-secondary">{projectDetails?.identifier}</span>
<span className="truncate">{projectDetails?.name}</span>
</div>
),
};
});
const [value, setValue] = React.useState<string[]>([]);
const [multiple, setMultiple] = React.useState<boolean>(false);
const onChange = (val: any) => {
setValue(val);
};
async function ExportCSVToMail() {
setExportLoading(true);
if (workspaceSlug && user && typeof provider === "string") {
const payload = {
provider: provider,
project: value,
multiple: multiple,
};
await projectExportService
.csvExport(workspaceSlug, payload)
.then(() => {
mutateServices();
router.push(`/${workspaceSlug}/settings/exports`);
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: provider === "csv" ? "CSV" : provider === "xlsx" ? "Excel" : 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 (
<ModalCore
isOpen={isOpen}
handleClose={() => {
if (!isSelectOpen) handleClose();
}}
position={EModalPosition.CENTER}
width={EModalWidth.XL}
>
<div className="flex flex-col gap-6 gap-y-4 p-6">
<div className="flex w-full items-center justify-start gap-6">
<span className="flex items-center justify-start">
<h3 className="text-18 font-medium 2xl:text-20">
{t("workspace_settings.settings.exports.modal.title")}{" "}
{provider === "csv" ? "CSV" : provider === "xlsx" ? "Excel" : provider === "json" ? "JSON" : ""}
</h3>
</span>
</div>
<div>
<CustomSearchSelect
value={value ?? []}
onChange={(val: string[]) => onChange(val)}
options={options}
input
label={
value && value.length > 0
? value
.map((projectId) => {
const projectDetails = getProjectById(projectId);
return projectDetails?.identifier;
})
.join(", ")
: "All projects"
}
onOpen={() => setIsSelectOpen(true)}
onClose={() => setIsSelectOpen(false)}
optionsClassName="max-w-48 sm:max-w-[532px]"
placement="bottom-end"
multiple
/>
</div>
<Checkbox
label={t("workspace_settings.settings.exports.export_separate_files")}
stretch="auto"
checked={multiple}
onCheckedChange={setMultiple}
/>
<div className="flex justify-end gap-2">
<Button variant="secondary" onClick={handleClose}>
{t("cancel")}
</Button>
<Button variant="primary" onClick={ExportCSVToMail} disabled={exportLoading} loading={exportLoading}>
{exportLoading
? `${t("workspace_settings.settings.exports.exporting")}...`
: t("workspace_settings.settings.exports.title")}
</Button>
</div>
</div>
</ModalCore>
);
});

View File

@@ -0,0 +1,38 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import { useState } from "react";
import { observer } from "mobx-react";
import { useParams, useSearchParams } from "next/navigation";
import { mutate } from "swr";
// constants
import { EXPORT_SERVICES_LIST } from "@plane/constants";
// local imports
import { ExportForm } from "./export-form";
import { PrevExports } from "./prev-exports";
export const ExportGuide = observer(function ExportGuide() {
// router
const { workspaceSlug } = useParams();
const searchParams = useSearchParams();
const provider = searchParams.get("provider");
// state
const per_page = 10;
const [cursor, setCursor] = useState<string | undefined>(`10:0:0`);
return (
<>
<div className="flex size-full flex-col gap-y-13">
<ExportForm
workspaceSlug={workspaceSlug}
provider={provider}
mutateServices={() => mutate(EXPORT_SERVICES_LIST(workspaceSlug, `${cursor}`, `${per_page}`))}
/>
<PrevExports workspaceSlug={workspaceSlug} cursor={cursor} per_page={per_page} setCursor={setCursor} />
</div>
</>
);
});

View File

@@ -0,0 +1,141 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import { useCallback, useEffect, useState } from "react";
import { observer } from "mobx-react";
import useSWR, { mutate } from "swr";
import { ArrowNarrowLeftOutline, ArrowNarrowRightOutline, RefreshOutline } from "@makeplane/propel/icons";
// plane imports
import { useTranslation } from "@plane/i18n";
import { Button } from "@plane/propel/button";
import { EmptyStateCompact } from "@plane/propel/empty-state";
import type { IExportData } from "@plane/types";
import { Table } from "@plane/ui";
// components
import { ImportExportSettingsLoader } from "@/components/ui/loader/settings/import-and-export";
// constants
import { EXPORT_SERVICES_LIST } from "@plane/constants";
// services
import { IntegrationService } from "@/services/integrations";
// local imports
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(function PrevExports(props: Props) {
// props
const { workspaceSlug, cursor, per_page, setCursor } = props;
// state
const [refreshing, setRefreshing] = useState(false);
// hooks
const { t } = useTranslation();
const columns = useExportColumns();
const { data: exporterServices } = useSWR(
workspaceSlug && cursor ? EXPORT_SERVICES_LIST(workspaceSlug, cursor, `${per_page}`) : null,
workspaceSlug && cursor ? () => integrationService.getExportsServicesList(workspaceSlug, cursor, per_page) : null
);
const handleRefresh = useCallback(async () => {
setRefreshing(true);
try {
await mutate(EXPORT_SERVICES_LIST(workspaceSlug, `${cursor}`, `${per_page}`));
} catch (error) {
// eslint-disable-next-line no-console
console.error("Failed to refresh export services list", error);
} finally {
setRefreshing(false);
}
}, [workspaceSlug, cursor, per_page]);
useEffect(() => {
const interval = setInterval(() => {
if (
Array.isArray(exporterServices?.results) &&
exporterServices.results.some((service) => service.status === "processing")
) {
handleRefresh();
} else {
clearInterval(interval);
}
}, 3000);
return () => clearInterval(interval);
}, [exporterServices, handleRefresh]);
return (
<div>
<div className="flex items-center justify-between border-b border-subtle pb-3.5">
<div className="flex items-center gap-2">
<h3 className="text-h6-medium text-primary">{t("workspace_settings.settings.exports.previous_exports")}</h3>
<Button variant="tertiary" className="shrink-0" onClick={handleRefresh}>
<RefreshOutline className={`h-3 w-3 ${refreshing ? "animate-spin" : ""}`} />
{refreshing ? t("refreshing") : t("refresh_status")}
</Button>
</div>
{Array.isArray(exporterServices?.results) && exporterServices.results.length > 0 && (
<div className="flex items-center gap-2 text-11">
<Button
variant="secondary"
size="sm"
disabled={!exporterServices?.prev_page_results}
onClick={() => exporterServices?.prev_page_results && setCursor(exporterServices?.prev_cursor)}
prependIcon={<ArrowNarrowLeftOutline />}
>
{t("prev")}
</Button>
<Button
variant="secondary"
size="sm"
disabled={!exporterServices?.next_page_results}
onClick={() => exporterServices?.next_page_results && setCursor(exporterServices?.next_cursor)}
appendIcon={<ArrowNarrowRightOutline />}
>
{t("next")}
</Button>
</div>
)}
</div>
<div className="flex flex-col">
{!exporterServices ? (
<ImportExportSettingsLoader />
) : Array.isArray(exporterServices.results) && exporterServices.results.length > 0 ? (
<div>
<div className="divide-y divide-subtle-1">
<Table
columns={columns}
data={exporterServices.results}
keyExtractor={(rowData: RowData) => rowData?.id ?? ""}
tHeadClassName="border-b border-subtle"
thClassName="text-left font-medium divide-x-0 text-placeholder"
tBodyClassName="divide-y-0"
tBodyTrClassName="divide-x-0 p-4 h-[40px] text-secondary"
tHeadTrClassName="divide-x-0"
/>
</div>
</div>
) : (
<div className="flex h-full w-full items-center justify-center">
<EmptyStateCompact
assetKey="export"
title={t("settings_empty_state.exports.title")}
description={t("settings_empty_state.exports.description")}
align="start"
rootClassName="py-20"
/>
</div>
)}
</div>
</div>
);
});

View File

@@ -0,0 +1,81 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import { useState } from "react";
// ui
import { Button } from "@plane/propel/button";
import type { IExportData } from "@plane/types";
// helpers
import { getDate, renderFormattedDate } from "@plane/utils";
// types
type Props = {
service: IExportData;
refreshing: boolean;
};
export function SingleExport({ service, refreshing }: Props) {
const provider = service.provider;
const [isLoading] = useState(false);
const checkExpiry = (inputDateString: string) => {
const currentDate = new Date();
const expiryDate = getDate(inputDateString);
if (!expiryDate) return false;
expiryDate.setDate(expiryDate.getDate() + 7);
return expiryDate > currentDate;
};
return (
<div className="flex items-center justify-between gap-2 px-4 py-3">
<div>
<h4 className="flex items-center gap-2 text-13">
<span>
Export to{" "}
<span className="font-medium">
{provider === "csv" ? "CSV" : provider === "xlsx" ? "Excel" : provider === "json" ? "JSON" : ""}
</span>{" "}
</span>
<span
className={`rounded-sm px-2 py-0.5 text-11 capitalize ${
service.status === "completed"
? "bg-success-subtle text-success-primary"
: service.status === "processing"
? "bg-yellow-500/20 text-yellow-500"
: service.status === "failed"
? "bg-danger-subtle text-danger-primary"
: service.status === "expired"
? "bg-orange-500/20 text-orange-500"
: ""
}`}
>
{refreshing ? "Refreshing..." : service.status}
</span>
</h4>
<div className="mt-2 flex items-center gap-2 text-11 text-secondary">
<span>{renderFormattedDate(service.created_at)}</span>|
<span>Exported by {service?.initiated_by_detail?.display_name}</span>
</div>
</div>
{checkExpiry(service.created_at) ? (
<>
{service.status == "completed" && (
<div>
<a target="_blank" href={service?.url} rel="noopener noreferrer">
<Button variant="primary" className="w-full">
{isLoading ? "Downloading..." : "Download"}
</Button>
</a>
</div>
)}
</>
) : (
<div className="text-11 text-danger-primary">Expired</div>
)}
</div>
);
}