Merge pull request #168 from Hexastack/145-issue-missing-content-file-upload-input

fix: add content file upload input
This commit is contained in:
Med Marrouchi 2024-10-09 08:59:30 +01:00 committed by GitHub
commit 4a1d52ce0d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 146 additions and 3 deletions

View File

@ -99,6 +99,17 @@ const AttachmentUploader: FC<FileUploadProps> = ({
const file = event.target.files.item(0);
if (file) {
const acceptedTypes = accept.split(',');
const isValidType = acceptedTypes.some((type) =>
file.type === type || file.name.endsWith(type.replace('.*', ''))
);
if (!isValidType) {
toast.error(t("message.invalid_file_type"));
return;
}
uploadAttachment(file);
}
}

View File

@ -0,0 +1,99 @@
/*
* Copyright © 2024 Hexastack. All rights reserved.
*
* Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms:
* 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission.
* 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file).
*/
import CloseIcon from "@mui/icons-material/Close";
import { Button, Dialog, DialogActions, DialogContent } from "@mui/material";
import { FC, useState } from "react";
import { useQuery } from "react-query";
import AttachmentInput from "@/app-components/attachment/AttachmentInput";
import { DialogTitle } from "@/app-components/dialogs/DialogTitle";
import { ContentContainer } from "@/app-components/dialogs/layouts/ContentContainer";
import { ContentItem } from "@/app-components/dialogs/layouts/ContentItem";
import { useApiClient } from "@/hooks/useApiClient";
import { DialogControlProps } from "@/hooks/useDialog";
import { useToast } from "@/hooks/useToast";
import { useTranslate } from "@/hooks/useTranslate";
import { IContentType } from "@/types/content-type.types";
export type ContentImportDialogProps = DialogControlProps<{
contentType?: IContentType;
}>;
export const ContentImportDialog: FC<ContentImportDialogProps> = ({
open,
data,
closeDialog,
...rest
}) => {
const [attachmentId, setAttachementId] = useState<string | null>(null);
const { t } = useTranslate();
const { toast } = useToast();
const { apiClient } = useApiClient();
const { refetch, isFetching } = useQuery(
["importContent", data?.contentType?.id, attachmentId],
async () => {
await apiClient.importContent(data?.contentType?.id!, attachmentId!)},
{
enabled: false,
onSuccess: () => {
handleCloseDialog();
toast.success(t("message.success_save"));
},
onError: () => {
toast.error(t("message.internal_server_error"));
},
}
);
const handleCloseDialog = () => {
closeDialog();
setAttachementId(null);
};
const handleImportClick = () => {
if (attachmentId && data?.contentType?.id) {
refetch();
}
};
return (
<Dialog open={open} fullWidth onClose={handleCloseDialog} {...rest}>
<DialogTitle onClose={handleCloseDialog}>{t("title.import")}</DialogTitle>
<DialogContent>
<ContentContainer>
<ContentItem>
<AttachmentInput
format="basic"
accept="text/csv"
onChange={(id, _) => {
setAttachementId(id);
}}
label=""
value={attachmentId}
/>
</ContentItem>
</ContentContainer>
</DialogContent>
<DialogActions>
<Button
disabled={!attachmentId || isFetching}
onClick={handleImportClick}
>
{t("button.import")}
</Button>
<Button
startIcon={<CloseIcon />}
variant="outlined"
onClick={handleCloseDialog}
disabled={isFetching}
>
{t("button.cancel")}
</Button>
</DialogActions>
</Dialog>
);
};

View File

@ -9,6 +9,7 @@
import { faAlignLeft } from "@fortawesome/free-solid-svg-icons";
import AddIcon from "@mui/icons-material/Add";
import ArrowBackIcon from "@mui/icons-material/ArrowBack";
import UploadIcon from "@mui/icons-material/Upload";
import { Button, Chip, Grid, Paper, Switch, Typography } from "@mui/material";
import Link from "next/link";
import { useRouter } from "next/router";
@ -38,6 +39,7 @@ import { PermissionAction } from "@/types/permission.types";
import { getDateTimeFormatter } from "@/utils/date";
import { ContentDialog } from "./ContentDialog";
import { ContentImportDialog } from "./ContentImportDialog";
export const Contents = () => {
const { t } = useTranslate();
@ -58,6 +60,9 @@ export const Contents = () => {
$eq: [{ entity: String(query.id) }],
$iLike: ["title"],
});
const importDialogCtl = useDialog<{
contentType?: IContentType;
}>(false);
const hasPermission = useHasPermission();
const { data: contentType } = useGet(String(query.id), {
entity: EntityType.CONTENT_TYPE,
@ -137,6 +142,18 @@ export const Contents = () => {
</Button>
</Grid>
) : null}
{hasPermission(EntityType.CONTENT, PermissionAction.CREATE) ? (
<Grid item>
<Button
startIcon={<UploadIcon />}
variant="contained"
onClick={() => importDialogCtl.openDialog({ contentType })}
sx={{ float: "right" }}
>
{t("button.import")}
</Button>
</Grid>
) : null}
</Grid>
</PageHeader>
</Grid>
@ -144,6 +161,7 @@ export const Contents = () => {
<Paper>
<ContentDialog {...getDisplayDialogs(addDialogCtl)} />
<ContentDialog {...getDisplayDialogs(editDialogCtl)} />
<ContentImportDialog {...getDisplayDialogs(importDialogCtl)} />
<DeleteDialog
{...deleteDialogCtl}
callback={() => {

View File

@ -41,7 +41,11 @@ const TOAST_WARNING_STYLE = {
export const useToast = () => {
const { t } = useTranslation();
const extractErrorMessage = (error: any) => {
if (error?.statusCode == 409) {
if(typeof error === 'string') {
return error;
}
else if (error?.statusCode == 409) {
return t("message.duplicate_error");
}

View File

@ -104,7 +104,8 @@
"title_length_exceeded": "You have reached the maximum length",
"no_label_found": "No label found",
"code_is_required": "Language code is required",
"text_is_required": "Text is required"
"text_is_required": "Text is required",
"invalid_file_type": "Invalid file type"
},
"menu": {
"terms": "Terms of Use",

View File

@ -104,7 +104,8 @@
"title_length_exceeded": "Vous avez atteint la longueur maximale",
"no_label_found": "Aucune étiquette trouvée",
"code_is_required": "Le code est requis",
"text_is_required": "Texte requis"
"text_is_required": "Texte requis",
"invalid_file_type": "Type de fichier invalide"
},
"menu": {
"terms": "Conditions d'utilisation",

View File

@ -35,6 +35,7 @@ export const ROUTES = {
RESET: "/user/reset",
NLP_SAMPLE_IMPORT: "/nlpsample/import",
NLP_SAMPLE_PREDICT: "/nlpsample/message",
CONTENT_IMPORT: "/content/import",
// Entities
[EntityType.SUBSCRIBER]: "/subscriber",
[EntityType.LABEL]: "/label",
@ -209,6 +210,14 @@ export class ApiClient {
return data;
}
async importContent(contentTypeId: string, attachementId: string) {
const { data } = await this.request.get(
`${ROUTES.CONTENT_IMPORT}/${contentTypeId}/${attachementId}`,
);
return data;
}
async predictNlp(text: string) {
const { data } = await this.request.get<INlpDatasetSampleAttributes>(
`${ROUTES.NLP_SAMPLE_PREDICT}`,