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
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:
118
apps/web/core/components/exporter/column.tsx
Normal file
118
apps/web/core/components/exporter/column.tsx
Normal 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;
|
||||
};
|
||||
251
apps/web/core/components/exporter/export-form.tsx
Normal file
251
apps/web/core/components/exporter/export-form.tsx
Normal 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>
|
||||
);
|
||||
});
|
||||
168
apps/web/core/components/exporter/export-modal.tsx
Normal file
168
apps/web/core/components/exporter/export-modal.tsx
Normal 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>
|
||||
);
|
||||
});
|
||||
38
apps/web/core/components/exporter/guide.tsx
Normal file
38
apps/web/core/components/exporter/guide.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
});
|
||||
141
apps/web/core/components/exporter/prev-exports.tsx
Normal file
141
apps/web/core/components/exporter/prev-exports.tsx
Normal 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>
|
||||
);
|
||||
});
|
||||
81
apps/web/core/components/exporter/single-export.tsx
Normal file
81
apps/web/core/components/exporter/single-export.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user