import React, { useState, useEffect, useContext, memo } from "react"; import { toast } from 'react-toastify'; import { IconButton, List, ListItem, ListItemText, ListItemAvatar, ListItemSecondaryAction, Tooltip, Button, FormControl, InputLabel, TextField, Divider, Select, MenuItem, Dialog, DialogTitle, DialogContent, DialogActions, Typography, Skeleton, Checkbox, Chip, Menu, } from "@mui/material"; import { Link as LinkIcon, OpenInNew as OpenInNewIcon, Edit as EditIcon, CloudDownload as CloudDownloadIcon, Delete as DeleteIcon, FileCopy as FileCopyIcon, Cached as CachedIcon, Publish as PublishIcon, Clear as ClearIcon, Add as AddIcon, SelectAll, } from "@mui/icons-material"; import Dropzone from "../components/Dropzone.jsx"; import ShuffleCodeEditor from "../components/ShuffleCodeEditor1.jsx"; import theme from "../theme.jsx"; import { Context } from "../context/ContextApi.jsx"; const Files = memo((props) => { const { globalUrl, userdata, serverside, selectedOrganization, isCloud,isSelectedFiles } = props; const [files, setFiles] = React.useState([]); const [showLoader, setShowLoader] = useState(true) const [selectedCategory, setSelectedCategory] = React.useState("default"); const [openFileId, setOpenFileId] = React.useState(false); const [fileCategories, setFileCategories] = React.useState([]); const [fileContent, setFileContent] = React.useState(""); const [openEditor, setOpenEditor] = React.useState(false); const [renderTextBox, setRenderTextBox] = React.useState(false); const [loadFileModalOpen, setLoadFileModalOpen] = React.useState(false); const [field1, setField1] = React.useState(""); const [field2, setField2] = React.useState(""); const [downloadUrl, setDownloadUrl] = React.useState("https://github.com/shuffle/standards") const [downloadBranch, setDownloadBranch] = React.useState("main"); const [downloadFolder, setDownloadFolder] = React.useState("translation_standards"); const [contentLoading, setContentLoading] = React.useState(false) const [selectAllChecked, setSelectAllChecked] = React.useState(false) const [selectedFiles, setSelectedFiles] = useState([]); const [selectedFileId, setSelectedFileId] = useState([]) const [showFileCategoryPopup, setShowFileCategoryPopup] = useState(false) const [updateToThisCategory, setUpdateToThisCategory] = useState("") const [showDistributionPopup, setShowDistributionPopup] = useState(false) const [selectedSubOrg, setSelectedSubOrg] = useState([]) const [fileIdSelectedForDistribution, setFileIdSelectedForDistribution] = useState("") //const alert = useAlert(); const allowedFileTypes = ["txt", "py", "yaml", "yml","json", "html", "js", "csv", "log", "eml", "msg", "md", "xml", "sh", "bat", "ps1", "psm1", "psd1", "ps1xml", "pssc", "psc1", "response"] var upload = ""; const handleKeyDown = (event) => { if (event.key === 'Enter') { fileCategories.push(event.target.value); setSelectedCategory(event.target.value); setRenderTextBox(false); } if (event.key === 'Escape'){ // not working for some reasons console.log('escape pressed') setRenderTextBox(false); } } const changeDistribution = (id, selectedSubOrg) => { editFileConfig(id, "suborg_distribute", [...new Set(selectedSubOrg)]) } const editFileConfig = (id, parentAction, selectedSubOrg) => { const data = { id: id, action: parentAction !== undefined && parentAction !== null ? parentAction : "change_category", selected_suborgs: selectedSubOrg, } const url = globalUrl + "/api/v1/files/" + id + "/config"; fetch(url, { mode: "cors", method: "POST", body: JSON.stringify(data), credentials: "include", crossDomain: true, withCredentials: true, headers: { "Content-Type": "application/json; charset=utf-8", }, }) .then((response) => response.json().then((responseJson) => { if (responseJson["success"] === false) { toast("Failed overwriting files"); } else { toast("Successfully updated file!"); setTimeout(() => { getFiles(); }, 1000); } }) ) .catch((error) => { toast("Err: " + error.toString()); }); }; const handleFileCheckboxChange = (index) => { setSelectedFiles((prevSelected) => { const updatedSelected = [...prevSelected]; updatedSelected[index] = !updatedSelected[index]; return updatedSelected; }); }; const runUpdateText = (text) =>{ fetch(`${globalUrl}/api/v1/files/${openFileId}/edit`, { method: "PUT", headers: { "Content-Type": "application/json", Accept: "application/json", }, body:text, credentials: "include", }) .then((response) => { if (response.status !== 200) { console.log("Can't update file"); } return response.json(); }) .then((responseJson) => { if (responseJson.success === true) { toast("Successfully updated file"); } }) .catch((error) => { toast("Error updating file: " + error.toString()); }) } const getFiles = (namespace) => { var parsedurl = `${globalUrl}/api/v1/files` if (namespace === undefined || namespace === null || namespace === "default") { } else if (namespace !== undefined && namespace !== null && namespace !== "") { parsedurl = `${globalUrl}/api/v1/files/namespaces/${namespace}?ids=true` } else if (selectedCategory !== undefined && selectedCategory !== null && selectedCategory !== "default" && selectedCategory !== "") { parsedurl = `${globalUrl}/api/v1/files/namespaces/${selectedCategory}?ids=true` } fetch(parsedurl, { method: "GET", headers: { "Content-Type": "application/json", Accept: "application/json", }, credentials: "include", }) .then((response) => { if (response.status !== 200) { console.log("Status not 200 for apps :O!"); return; } return response.json(); }) .then((responseJson) => { if (responseJson.files !== undefined && responseJson.files !== null) { setFiles(responseJson.files); setShowLoader(false) setShowDistributionPopup(false) } else if (responseJson.list !== undefined && responseJson.list !== null) { // Set the "namespace" field in all items if (namespace !== undefined && namespace !== null) { responseJson.list.forEach((item) => { item.namespace = namespace item.filename = item.name item.workflow_id = "global" }) } setFiles(responseJson.list); setShowLoader(false) } else { setFiles([]); setShowLoader(false) setShowDistributionPopup(false) } if (namespace === undefined || namespace === null || namespace === "default") { if (responseJson.namespaces !== undefined && responseJson.namespaces !== null && (fileCategories.length === 0 || responseJson.namespaces.length > fileCategories.length)) { setFileCategories(responseJson.namespaces) } } }) .catch((error) => { toast(error.toString()); }); }; useEffect(() => { getFiles("default") setTimeout(() => { var category = selectedCategory if (window.location.search.includes("category=")) { const urlParams = new URLSearchParams(window.location.search) category = urlParams.get("category") } if (category !== undefined && category !== null && category.length > 0 && category !== "default") { setSelectedCategory(category) } getFiles(category) }, 1000) }, []); const importStandardsFromUrl = (url, folder) => { if (url === undefined || url === null || url.length < 5) { toast("Please enter a valid URL"); return; } if (folder === undefined || folder === null || folder.length < 2) { toast("Please enter a valid folder name") return } const parsedData = { url: url, path: folder, field_3: downloadBranch || "master", }; if (field1.length > 0) { parsedData["field_1"] = field1; } if (field2.length > 0) { parsedData["field_2"] = field2; } toast(`Getting files from url ${url}. This may take a while if the repository is large. Please wait...`); fetch(globalUrl + "/api/v1/files/download_remote", { method: "POST", mode: "cors", headers: { Accept: "application/json", }, body: JSON.stringify(parsedData), credentials: "include", }) .then((response) => { if (response.status === 200) { toast("Successfully loaded files from " + downloadUrl); setLoadFileModalOpen(false); } return response.json(); }) .then((responseJson) => { if (!responseJson.success) { if (responseJson.reason !== undefined) { toast("Failed loading: " + responseJson.reason); } else { toast("Failed loading"); } } }) .catch((error) => { toast(error.toString()); }); } const handleGithubValidation = () => { importStandardsFromUrl(downloadUrl, downloadFolder); } const fileDownloadModal = loadFileModalOpen ?
Load Files from Github
Files will be loaded from the repository and branch you specify, with the focus on files in one folder at a time. This is NOT recursive.
Repository URL (supported: github, gitlab, bitbucket) setDownloadUrl(e.target.value)} placeholder="https://github.com/shuffle/standards" fullWidth />
Branch (default value is "main"): setDownloadBranch(e.target.value)} placeholder="master" fullWidth /> Folder (can use / for subfolders): setDownloadFolder(e.target.value)} placeholder="translation_standards" fullWidth />
Authentication (optional - private repos etc):
setField1(e.target.value)} type="username" placeholder="Username / APIkey (optional)" fullWidth /> setField2(e.target.value)} type="password" placeholder="Password (optional)" fullWidth />
: null const handleSelectSubOrg = (id, action) => { if (action === "all") { const childOrgs = userdata.orgs.filter( (data) => data.creator_org === userdata.active_org.id ); setSelectedSubOrg((prev) => { if (prev.length === childOrgs.length) { // If all child orgs are already selected, clear the selection return []; } else { // Otherwise, select all child org IDs return childOrgs.map((data) => data.id); } }); } else if (action === "none") { setSelectedSubOrg([]); } else { setSelectedSubOrg((prev) => { if (prev.includes(id)) { return prev.filter((data) => data !== id); } else { return [...prev, id]; } }); } }; const fileDistributionModal = showDistributionPopup ? ( setShowDistributionPopup(false)} PaperProps={{ sx: { borderRadius: theme?.palette?.DialogStyle?.borderRadius, border: theme?.palette?.DialogStyle?.border, fontFamily: theme?.typography?.fontFamily, backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, zIndex: 1000, minWidth: "600px", minHeight: "320px", overflow: "auto", '& .MuiDialogContent-root': { backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, }, '& .MuiDialogTitle-root': { backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, }, '& .MuiDialogActions-root': { backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, }, }, }} >
Select sub-org to distribute files
{handleSelectSubOrg(null, "none")}}>None {handleSelectSubOrg(null, "all")}}>All {userdata.orgs.map((data, index) => { if (data.creator_org !== userdata.active_org.id) { return null; } const imagesize = 22; const imageStyle = { width: imagesize, height: imagesize, pointerEvents: "none", marginRight: 10, marginLeft: data.id === userdata.active_org.id ? 0 : 20, }; const image = data.image === "" ? ( {data.name} ) : ( {data.name} ); return ( handleSelectSubOrg(data.id)} style={{ display: "flex", alignItems: "center" }} > {image} {data.name} ); })}
): null const deleteFile = (file) => { fetch(globalUrl + "/api/v1/files/" + file.id, { method: "DELETE", headers: { "Content-Type": "application/json", Accept: "application/json", }, credentials: "include", }) .then((response) => { if (response.status !== 200) { console.log("Status not 200 for file delete :O!"); } return response.json(); }) .then((responseJson) => { if (responseJson.success) { toast("Successfully deleted file") } else if ( responseJson.reason !== undefined && responseJson.reason !== null ) { toast("Failed to delete file: " + responseJson.reason); } setTimeout(() => { getFiles(selectedCategory) }, 1500); }) .catch((error) => { toast(error.toString()); }); }; const readFileData = (file) => { setContentLoading(true) fetch(globalUrl + "/api/v1/files/" + file.id + "/content", { method: "GET", headers: { "Content-Type": "application/json", Accept: "application/json", }, credentials: "include", }) .then((response) => { setContentLoading(false) if (response.status !== 200) { console.log("Status not 200 for file :O!"); return ""; } return response.text(); }) .then((respdata) => { // console.log("respdata ->", respdata); // console.log("respdata type ->", typeof(respdata)); if (respdata.length === 0) { toast("Failed getting file. Is it deleted?"); return; } return respdata }) .then((responseData) => { setFileContent(responseData); //console.log("filecontent state ",fileContent); }) .catch((error) => { setContentLoading(false) toast(error.toString()) }); }; const downloadFile = (file) => { fetch(globalUrl + "/api/v1/files/" + file.id + "/content", { method: "GET", credentials: "include", }) .then((response) => { if (response.status !== 200) { console.log("Status not 200 for apps :O!"); return ""; } console.log("Resp: ", response) return response.blob() }) .then((respdata) => { if (respdata.length === 0) { toast("Failed getting file. Is it deleted?"); return; } var blob = new Blob([respdata], { type: "application/octet-stream", }); var url = URL.createObjectURL(blob); var link = document.createElement("a"); link.setAttribute("href", url); link.setAttribute("download", `${file.filename}`); var event = document.createEvent("MouseEvents"); event.initMouseEvent( "click", true, true, window, 1, 0, 0, 0, 0, false, false, false, false, 0, null ); link.dispatchEvent(event); //return response.json() }) .then((responseJson) => { //console.log(responseJson) //setSchedules(responseJson) }) .catch((error) => { toast(error.toString()); }); }; const handleCreateFile = (filename, file) => { var data = { filename: filename, org_id: selectedOrganization.id, workflow_id: "global", }; if ( selectedCategory !== undefined && selectedCategory !== null && selectedCategory.length > 0 && selectedCategory !== "default" ) { data.namespace = selectedCategory; } fetch(globalUrl + "/api/v1/files/create", { method: "POST", headers: { "Content-Type": "application/json", Accept: "application/json", }, credentials: "include", body: JSON.stringify(data), }) .then((response) => { if (response.status !== 200) { console.log("Status not 200 for apps :O!"); return; } return response.json(); }) .then((responseJson) => { //console.log("RESP: ", responseJson) if (responseJson.success === true) { handleFileUpload(responseJson.id, file); } else { toast("Failed to upload file ", filename); } }) .catch((error) => { toast("Failed to upload file ", filename); console.log(error.toString()); }); }; const handleFileUpload = (file_id, file) => { //console.log("FILE: ", file_id, file) fetch(`${globalUrl}/api/v1/files/${file_id}/upload`, { method: "POST", credentials: "include", body: file, }) .then((response) => { if (response.status !== 200 && response.status !== 201) { console.log("Status not 200 for apps :O!"); toast("File was created, but failed to upload."); return; } return response.json(); }) .then((responseJson) => { //console.log("RESPONSE: ", responseJson) //setFiles(responseJson) }) .catch((error) => { toast(error.toString()); }); }; const uploadFiles = (files) => { for (var key in files) { try { const filename = files[key].name; var filedata = new FormData(); filedata.append("shuffle_file", files[key]); if (typeof files[key] === "object") { handleCreateFile(filename, filedata); } /* reader.addEventListener('load', (e) => { var data = e.target.result; setIsDropzone(false) console.log(filename) console.log(data) console.log(files[key]) }) reader.readAsText(files[key]) */ } catch (e) { console.log("Error in dropzone: ", e); } } setTimeout(() => { getFiles() }, 3000); }; const uploadFile = (e) => { const isDropzone = e.dataTransfer === undefined ? false : e.dataTransfer.files.length > 0; const files = isDropzone ? e.dataTransfer.files : e.target.files; //const reader = new FileReader(); //toast("Starting fileupload") uploadFiles(files); }; const handleUpdateFileCategory = (namespace) => { if (selectedFiles.length === 0 && !selectAllChecked) { toast("Please select files to update category") return } if (namespace === undefined || namespace === null || namespace === "") { toast("Please select a category to update files to") return } const url = globalUrl + `/api/v1/files/namespaces/${namespace}/share` const data = { SelectedFiles: selectedFileId, } setShowLoader(true) fetch(url, { mode: "cors", method: "POST", body: JSON.stringify(data), credentials: "include", crossDomain: true, withCredentials: true, headers: { "Content-Type": "application/json; charset=utf-8", }, }) .then((response) => response.json().then((responseJson) => { if (responseJson["success"] === false) { toast("Failed overwriting files"); } else { setSelectAllChecked(false) setSelectedFiles([]) setSelectedFileId([]) setShowFileCategoryPopup(false) setSelectedCategory(namespace) setTimeout(() => { getFiles(); toast("Successfully updated file!"); if (window.location.search.includes("category=")) { const newurl = window.location.href.replace(/category=[^&]+/, `category=${namespace}`) window.history.pushState({ path: newurl }, "", newurl) } else { window.history.pushState({ path: window.location.href }, "", `${window.location.href}&category=${namespace}`) } }, 1000); } } )) .catch((error) => { toast("Err: " + error.toString()); }); } return ( {fileDistributionModal}
{fileDownloadModal}

Files

Files from Workflows are a way to store as well as edit files.{" "} Learn more
{/* */} (upload = ref)} onChange={(event) => { //const file = event.target.value //const fileObject = URL.createObjectURL(actualFile) //setFile(fileObject) //const files = event.target.files[0] uploadFiles(event.target.files); }} /> {/*
*/} {fileCategories !== undefined && fileCategories !== null && fileCategories.length > 1 ? ( setShowFileCategoryPopup(false)}> File Categories Please note that your selected files ({selectedFileId?.length}) will be moved to the {updateToThisCategory} category. ) : null}
{renderTextBox ? : } {renderTextBox && { handleKeyDown(event); if(event.key === 'Enter' && selectedFileId.length > 0){ setShowFileCategoryPopup(true) setUpdateToThisCategory(event.target.value) } }} style={{ height: 35, width: 200, marginTop: 0, }} InputProps={{ style: { color: "white", height: 35, fontSize: 16, borderRadius: 4, paddingTop: 0, }, }} color="primary" placeholder="File category name" required margin="dense" defaultValue={""} autoFocus />}
{isSelectedFiles?null: }
{[ { setSelectAllChecked((prev) => !prev); setSelectedFiles((prev) => { if (prev.length === files.length) { return [] } else { return files.map((_, index) => !prev.includes(index)) } }) if (selectAllChecked) { setSelectedFileId([]) } else { setSelectedFileId( files .filter((file) => file.namespace === selectedCategory) .map((file) => file.id) ); } }} /> , "Name", "Workflow", "Md5", "Status", "Filesize", "Actions", "Distribution" ] .filter(Boolean) .map((header, index) => ( ))} {showLoader ? [...Array(6)].map((_, rowIndex) => ( {Array(8) .fill() .map((_, colIndex) => ( ))} )): files.length === 0 ? (
No files found
):( files?.map((file, index) => { if (file.namespace === "") { file.namespace = "default"; } if (file.namespace !== selectedCategory) { return null; } var bgColor = isSelectedFiles ? "#212121":"#27292d"; if (index % 2 === 0) { bgColor = isSelectedFiles ? "#1A1A1A":"#1f2023"; } const isDistributed = file?.suborg_distribution?.length > 0 ? true : false; const filenamesplit = file.filename.split(".") const iseditable = file.filesize < 2000000 && file.status === "active" && (allowedFileTypes.includes(filenamesplit[filenamesplit.length-1]) || !file?.filename.includes(".")) return ( {/* */} {handleFileCheckboxChange(index); setSelectedFileId(prev => { if (prev.includes(file.id)) { return prev.filter((item) => item !== file.id) } else { return [...prev, file.id] } })}} /> : ( ) } style={{ display: 'table-cell', overflow: "hidden", }} /> {file.md5_sum} )} primaryTypographyProps={{ style:{ display: 'table-cell', marginLeft:isSelectedFiles? 15:null, overflow: "hidden", whiteSpace: 'nowrap', textOverflow: 'ellipsis', maxWidth: 200, } }} /> { setOpenEditor(true) setOpenFileId(file.id) readFileData(file) }} > edit icon {/* { // Open the file, without downloading it window.open(`${globalUrl}/api/v1/files/${file.id}/content?type=text&authorization=${file.public_authorization}`, "_blank noreferrer noopener") }} > */} { downloadFile(file); }} > download icon { navigator.clipboard.writeText(file.id); document.execCommand("copy"); toast(file.id + " copied to clipboard"); }} > copy icon { deleteFile(file); }} > style={{ display: 'table-cell', textAlign:'center' // overflow: "hidden", }} /> {selectedOrganization.id !== undefined && file?.org_id !== selectedOrganization.id ? : { setShowDistributionPopup(true) if(file?.suborg_distribution?.length > 0){ setSelectedSubOrg(file.suborg_distribution) }else{ setSelectedSubOrg([]) } setFileIdSelectedForDistribution(file.id) }} /> } ); }) ) }
) }) export default memo(Files); const DownloadFileIcon = memo(({ setLoadFileModalOpen, isSelectedFiles }) => { return ( setLoadFileModalOpen(true)} sx={{ position: "absolute", right: 0, top: isSelectedFiles ? null : 0, left: isSelectedFiles ? "93%" : null, transition: "left 0.3s ease", width: 48, height: 48, borderRadius: "50%", }} > ); });