diff --git a/src/api.js b/src/api.js index 9973adfe6..83bbcad01 100644 --- a/src/api.js +++ b/src/api.js @@ -16,12 +16,8 @@ const setDirectoryFile = async (siteName, folderName, payload) => { return await axios.post(`${BACKEND_URL}/sites/${siteName}/collections/${folderName}/pages/collection.yml`, payload); } -const getFolderContents = async (siteName, folderName, subfolderName) => { - return await axios.get(`${BACKEND_URL}/sites/${siteName}/folders?path=_${folderName}${subfolderName ? `/${subfolderName}` : ''}`); -} - // EditPage -const getPageApiEndpoint = ({folderName, subfolderName, fileName, siteName, resourceName}) => { +const getPageApiEndpoint = ({folderName, subfolderName, fileName, siteName, resourceName, newFileName}) => { if (folderName) { return `${process.env.REACT_APP_BACKEND_URL}/sites/${siteName}/collections/${folderName}/pages/${encodeURIComponent(`${subfolderName ? `${subfolderName}/` : ''}${fileName}`)}` } @@ -31,6 +27,36 @@ const getPageApiEndpoint = ({folderName, subfolderName, fileName, siteName, reso return `${process.env.REACT_APP_BACKEND_URL}/sites/${siteName}/pages/${fileName}` } +const getCreatePageApiEndpoint = ({folderName, subfolderName, siteName, resourceName, newFileName}) => { + if (folderName) { + return `${process.env.REACT_APP_BACKEND_URL}/sites/${siteName}/collections/${folderName}/pages/new/${encodeURIComponent(`${subfolderName ? `${subfolderName}/` : ''}${newFileName}`)}` + } + if (resourceName) { + return `${process.env.REACT_APP_BACKEND_URL}/sites/${siteName}/resources/${resourceName}/pages/new/${newFileName}` + } + return `${process.env.REACT_APP_BACKEND_URL}/sites/${siteName}/pages/new/${newFileName}` +} + +const getRenamePageApiEndpoint = ({folderName, subfolderName, fileName, siteName, resourceName, newFileName}) => { + if (folderName) { + return `${process.env.REACT_APP_BACKEND_URL}/sites/${siteName}/collections/${folderName}/pages/${encodeURIComponent(`${subfolderName ? `${subfolderName}/` : ''}${fileName}`)}/rename/${encodeURIComponent(`${subfolderName ? `${subfolderName}/` : ''}${newFileName}`)}` + } + if (resourceName) { + return `${process.env.REACT_APP_BACKEND_URL}/sites/${siteName}/resources/${resourceName}/pages/${fileName}/rename/${newFileName}` + } + return `${process.env.REACT_APP_BACKEND_URL}/sites/${siteName}/pages/${fileName}/rename/${newFileName}` +} + +const getMovePageEndpoint = ({siteName, resourceName, folderName, subfolderName, newPath}) => { + if (folderName) { + return `${BACKEND_URL}/sites/${siteName}/collections/${encodeURIComponent(`${folderName ? `${folderName}`: ''}${subfolderName ? `/${subfolderName}` : ''}`)}/move/${encodeURIComponent(`${newPath}`)}` + } + if (resourceName) { + return `${BACKEND_URL}/sites/${siteName}/resources/${resourceName}/move/${encodeURIComponent(`${newPath}`)}` + } + return `${BACKEND_URL}/sites/${siteName}/pages/move/${encodeURIComponent(`${newPath}`)}` +} + const getEditPageData = async ({folderName, subfolderName, fileName, siteName, resourceName}) => { const apiEndpoint = getPageApiEndpoint({folderName, subfolderName, fileName, siteName, resourceName}) const resp = await axios.get(apiEndpoint); @@ -49,13 +75,38 @@ const getCsp = async (siteName) => { return await axios.get(`${process.env.REACT_APP_BACKEND_URL}/sites/${siteName}/netlify-toml`); } +const createPageData = async ({folderName, subfolderName, newFileName, siteName, resourceName}, content) => { + const apiEndpoint = getCreatePageApiEndpoint({folderName, subfolderName, newFileName, siteName, resourceName}) + const params = { content } + await axios.post(apiEndpoint, params) + + // redirect to new page upon successful creation + if (folderName) { + return `/sites/${siteName}/folder/${folderName}/${subfolderName ? `subfolder/${subfolderName}/` : ''}${newFileName}` + } + if (resourceName) { + return `/sites/${siteName}/resources/${resourceName}/${newFileName}` + } + return `/sites/${siteName}/pages/${newFileName}` +} + +const renamePageData = async ({folderName, subfolderName, fileName, siteName, resourceName, newFileName}, content, sha) => { + const apiEndpoint = getRenamePageApiEndpoint({folderName, subfolderName, fileName, siteName, resourceName, newFileName}) + const params = { + content, + sha, + }; + await axios.post(apiEndpoint, params) +} + const updatePageData = async ({folderName, subfolderName, fileName, siteName, resourceName}, content, sha) => { const apiEndpoint = getPageApiEndpoint({folderName, subfolderName, fileName, siteName, resourceName}) const params = { content, sha, }; - return await axios.post(apiEndpoint, params); + await axios.post(apiEndpoint, params); + return } const deletePageData = async ({folderName, subfolderName, fileName, siteName, resourceName}, sha) => { @@ -88,6 +139,19 @@ const renameResourceCategory = async ({ siteName, categoryName, newCategoryName} return await axios.post(apiUrl) } +const getAllCategoriesApiEndpoint = ({siteName, isResource}) => { + if (isResource) { + return `${BACKEND_URL}/sites/${siteName}/resources` + } + return `${BACKEND_URL}/sites/${siteName}/collections` +} + +const getAllCategories = async ({siteName, isResource}) => { + const apiEndpoint = getAllCategoriesApiEndpoint({siteName, isResource}) + const resp = await axios.get(apiEndpoint); + return resp.data +} + const getAllResourceCategories = async (siteName) => { return await axios.get(`${BACKEND_URL}/sites/${siteName}/resources`); } @@ -111,10 +175,8 @@ const getEditNavBarData = async(siteName) => { const { content, sha } = resp.data; navContent = content navSha = sha - const collectionResp = await axios.get(`${BACKEND_URL}/sites/${siteName}/collections`) - collectionContent = collectionResp.data - const resourceResp = await axios.get(`${BACKEND_URL}/sites/${siteName}/resources`) - resourceContent = resourceResp.data + collectionContent = await getAllCategories({siteName, isResource: false}) + resourceContent = await getAllCategories({siteName, isResource: true}) const foldersResp = await axios.get(`${BACKEND_URL}/sites/${siteName}/folders/all`) if (foldersResp.data && foldersResp.data.allFolderContent) { // parse directory files @@ -148,27 +210,6 @@ const updateNavBarData = async (siteName, originalNav, links, sha) => { return await axios.post(`${BACKEND_URL}/sites/${siteName}/navigation`, params); } -const createPage = async (endpointUrl, content) => { - return await axios.post(`${BACKEND_URL}/sites/${endpointUrl}`, { content }); -} - -const getPage = async (pageType, siteName, collectionName, pageName) => { - const endpointUrl = (pageType === 'collection') - ? `${siteName}/collections/${collectionName}/pages/${pageName}` - : `${siteName}/pages/${pageName}` - const resp = await axios.get(`${BACKEND_URL}/sites/${endpointUrl}`); - return resp.data -} - -const updatePage = async(endpointUrl, content, sha) => { - return await axios.post(`${BACKEND_URL}/sites/${endpointUrl}`, { content, sha }); -} - -const deletePage = async(endpointUrl, sha) => { - return - // return await axios.delete(`${BACKEND_URL}/sites/${endpointUrl}`, { content, sha }); -} - const moveFiles = async (siteName, selectedFiles, title, parentFolder) => { const baseApiUrl = `${process.env.REACT_APP_BACKEND_URL}/sites/${siteName}${parentFolder ? `/collections/${parentFolder}` : '/pages'}` const params = { @@ -178,10 +219,17 @@ const moveFiles = async (siteName, selectedFiles, title, parentFolder) => { return await axios.post(`${baseApiUrl}/move/${newPath}`, params) } +const moveFile = async ({selectedFile, siteName, resourceName, folderName, subfolderName, newPath}) => { + const apiEndpoint = getMovePageEndpoint({siteName, resourceName, folderName, subfolderName, newPath}) + const params = { + files: [selectedFile], + } + return await axios.post(apiEndpoint, params) +} + export { getDirectoryFile, setDirectoryFile, - getFolderContents, getEditPageData, getCsp, updatePageData, @@ -195,9 +243,9 @@ export { getResourcePages, getEditNavBarData, updateNavBarData, - createPage, - getPage, - updatePage, - deletePage, + createPageData, + renamePageData, + getAllCategories, moveFiles, + moveFile, } \ No newline at end of file diff --git a/src/components/CollectionPagesSection.jsx b/src/components/CollectionPagesSection.jsx index 427715f4e..c8d9f205b 100644 --- a/src/components/CollectionPagesSection.jsx +++ b/src/components/CollectionPagesSection.jsx @@ -1,101 +1,114 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState } from 'react'; import axios from 'axios'; +import { useQuery, useMutation } from 'react-query'; import PropTypes from 'prop-types'; import _ from 'lodash'; +import { + PAGE_CONTENT_KEY, + FOLDERS_CONTENT_KEY, + DIR_CONTENT_KEY, +} from '../constants' + +import { getEditPageData, deletePageData, getAllCategories, moveFile, getDirectoryFile } from '../api' + +import { DEFAULT_RETRY_MSG, parseDirectoryFile, convertFolderOrderToArray } from '../utils' + // Import components import OverviewCard from '../components/OverviewCard'; import ComponentSettingsModal from './ComponentSettingsModal' import PageSettingsModal from './PageSettingsModal' +import { errorToast, successToast } from '../utils/toasts'; +import DeleteWarningModal from '../components/DeleteWarningModal' +import GenericWarningModal from '../components/GenericWarningModal' // Import styles import elementStyles from '../styles/isomer-cms/Elements.module.scss'; import contentStyles from '../styles/isomer-cms/pages/Content.module.scss'; -// Import utils -import { retrieveThirdNavOptions } from '../utils/dropdownUtils' - -// Constants -const RADIX_PARSE_INT = 10; // axios settings axios.defaults.withCredentials = true +// Clean up note: Should be renamed, only used for resource pages and unlinked pages sections const CollectionPagesSection = ({ collectionName, pages, siteName, isResource }) => { const [isComponentSettingsActive, setIsComponentSettingsActive] = useState(false) const [selectedFile, setSelectedFile] = useState('') + const [selectedPath, setSelectedPath] = useState('') const [createNewPage, setCreateNewPage] = useState(false) - const [collectionPageData, setCollectionPageData] = useState(null) - const [thirdNavData, setThirdNavData] = useState(null) - const [allCategories, setAllCategories] = useState() - - useEffect(() => { - let _isMounted = true - const fetchData = async () => { - // Retrieve the list of all page/resource categories for use in the dropdown options. - if (isResource) { - const resourcesResp = await axios.get(`${process.env.REACT_APP_BACKEND_URL}/sites/${siteName}/resources`); - const { resources: allCategories } = resourcesResp.data; - if (_isMounted) setAllCategories(allCategories.map((category) => category.dirName)) - } else { - const collectionsResp = await axios.get(`${process.env.REACT_APP_BACKEND_URL}/sites/${siteName}/collections`); - const { collections: collectionCategories } = collectionsResp.data; - if (_isMounted) setAllCategories(collectionCategories) - } - } - fetchData() - - return () => { _isMounted = false } - }, []) - - const loadThirdNavOptions = async () => { - if (thirdNavData) { - return new Promise((resolve) => { - resolve(thirdNavData) - }); - } - - const { collectionPages, thirdNavOptions } = await retrieveThirdNavOptions(siteName, collectionName, true) - setCollectionPageData(collectionPages) - setThirdNavData(thirdNavOptions) - return thirdNavOptions - } + const [canShowDeleteWarningModal, setCanShowDeleteWarningModal] = useState(false) + const [canShowMoveModal, setCanShowMoveModal] = useState(false) + const [queryFolderName, setQueryFolderName] = useState('') + + const { data: pageData } = useQuery( + [PAGE_CONTENT_KEY, { siteName, fileName: selectedFile, resourceName: collectionName }], + () => getEditPageData({ siteName, fileName: selectedFile, resourceName: collectionName }), + { + enabled: selectedFile.length > 0, + retry: false, + onError: () => { + setSelectedFile('') + errorToast(`The page data could not be retrieved. ${DEFAULT_RETRY_MSG}`) + }, + }, + ) + // MOVE-TO Dropdown + // get all folders for move-to dropdown + const { data: allCategories } = useQuery( + [FOLDERS_CONTENT_KEY, { siteName, isResource }], + async () => getAllCategories({ siteName, isResource }), + { + enabled: selectedFile.length > 0, + onError: () => errorToast(`The folders data could not be retrieved. ${DEFAULT_RETRY_MSG}`), + }, + ) - const isCategoryDropdownDisabled = (isNewFile, category) => { - if (category) return true - if (isNewFile) return false - return true - } + // MOVE-TO Dropdown + // get subfolders of selected folder for move-to dropdown + const { data: querySubfolders } = useQuery( + [DIR_CONTENT_KEY, siteName, queryFolderName], + async () => getDirectoryFile(siteName, queryFolderName), + { + enabled: selectedFile.length > 0 && queryFolderName.length > 0, + onError: () => errorToast(`The folders data could not be retrieved. ${DEFAULT_RETRY_MSG}`), + }, + ) - const generateNewPageText = () => { - if (isResource) { - return `Add a new resource` - } else { - return `Add a new ${collectionName ? 'collection ' : ''}page` + // MOVE-TO Dropdown utils + // parse responses from move-to queries + const getCategories = (queryFolderName, allCategories, querySubfolders) => { + if (isResource && allCategories) { + return allCategories.resources.map(resource => resource.dirName).filter(dirName => dirName !== collectionName) + } + if (queryFolderName && querySubfolders) { + const parsedFolderContents = parseDirectoryFile(querySubfolders.data.content) + const parsedFolderArray = convertFolderOrderToArray(parsedFolderContents) + return parsedFolderArray.filter(file => file.type === 'dir').map(file => file.name) } + if (!queryFolderName && allCategories) { + return allCategories.collections + } + return null } - const settingsToggle = (event) => { - const { id } = event.target; - const idArray = id.split('-'); + const { mutateAsync: deleteHandler } = useMutation( + async () => deletePageData({ siteName, fileName: selectedFile, resourceName: collectionName }, pageData.pageSha), + { + onError: () => errorToast(`Your file could not be deleted successfully. ${DEFAULT_RETRY_MSG}`), + onSuccess: () => {successToast('Successfully deleted file'); window.location.reload();}, + onSettled: () => setCanShowDeleteWarningModal((prevState) => !prevState), + } + ) - // Create new page - if (idArray[1] === 'NEW') { - setIsComponentSettingsActive((prevState) => !prevState) - setSelectedFile('') - setCreateNewPage(true) - } else { - // Modify existing page frontmatter - const pageIndex = parseInt(idArray[1], RADIX_PARSE_INT); - - setIsComponentSettingsActive((prevState) => !prevState) - setSelectedFile(() => { - return isComponentSettingsActive ? null : pages[pageIndex] - }) - setCreateNewPage(false) + const { mutateAsync: moveHandler } = useMutation( + () => moveFile({siteName, selectedFile, newPath: selectedPath, resourceName: collectionName}), + { + onError: () => errorToast(`Your file could not be moved successfully. ${DEFAULT_RETRY_MSG}`), + onSuccess: () => {successToast('Successfully moved file'); window.location.reload();}, + onSettled: () => setCanShowMoveModal(prevState => !prevState), } - } + ) return ( <> @@ -103,76 +116,93 @@ const CollectionPagesSection = ({ collectionName, pages, siteName, isResource }) isComponentSettingsActive && ( isResource ? page.fileName) - .value() - } - collectionPageData={collectionPageData} - loadThirdNavOptions={loadThirdNavOptions} + fileName={selectedFile || ''} + isNewFile={!selectedFile} + pageData={pageData} + pageFileNames={pages?.map(page => page.name) || []} + setSelectedFile={setSelectedFile} setIsComponentSettingsActive={setIsComponentSettingsActive} /> - : ) } { - pages && _.isEmpty(pages) && - <> -
No files found.
-
- + canShowDeleteWarningModal + && ( + setCanShowDeleteWarningModal(false)} + onDelete={deleteHandler} + type={"page"} + /> + ) + } + { + canShowMoveModal + && ( + { + setCanShowMoveModal(false) + }} + proceedText="Continue" + cancelText="Cancel" + /> + ) }
- {/* Display loader if pages have not been retrieved from API call */} - { pages - ? ( + {
- { - _.isEmpty(pages) - ? null - : pages.map((page, pageIdx) => ( + { pages ? + pages.map((page, pageIdx) => ( )) + /* Display loader if pages have not been retrieved from API call */ + : 'Loading Pages...' }
- ) - : 'Loading Pages...' }
diff --git a/src/components/ComponentSettingsModal.jsx b/src/components/ComponentSettingsModal.jsx index 6d73316ac..8df920787 100644 --- a/src/components/ComponentSettingsModal.jsx +++ b/src/components/ComponentSettingsModal.jsx @@ -1,14 +1,11 @@ import React, { useState, useEffect } from 'react'; +import { useMutation } from 'react-query'; import axios from 'axios'; -import AsyncCreatableSelect from "react-select/async-creatable"; -import CreatableSelect from 'react-select/creatable'; -import { createFilter } from 'react-select'; -import { Base64 } from 'js-base64'; import PropTypes from 'prop-types'; import * as _ from 'lodash'; import FormField from './FormField'; -import DeleteWarningModal from './DeleteWarningModal'; +import FormFieldHorizontal from './FormFieldHorizontal'; import ResourceFormFields from './ResourceFormFields'; import SaveDeleteButtons from './SaveDeleteButtons'; @@ -17,58 +14,29 @@ import useRedirectHook from '../hooks/useRedirectHook'; import { DEFAULT_RETRY_MSG, frontMatterParser, - dequoteString, - generatePageFileName, - generateCollectionPageFileName, generateResourceFileName, - saveFileAndRetrieveUrl, retrieveResourceFileMetadata, + concatFrontMatterMdBody, } from '../utils'; -import { validatePageSettings, validateResourceSettings } from '../utils/validators'; + +import { createPageData, updatePageData, renamePageData } from '../api' + +import { validateResourceSettings } from '../utils/validators'; import { errorToast } from '../utils/toasts'; -import { retrieveThirdNavOptions } from '../utils/dropdownUtils' import elementStyles from '../styles/isomer-cms/Elements.module.scss'; // axios settings axios.defaults.withCredentials = true -// Helper functions -const generateInitialCategoryLabel = (originalCategory, isCategoryDisabled) => { - if (originalCategory) return originalCategory - // If category is disabled and no original category exists, it is an unlinked page - return isCategoryDisabled ? "Unlinked Page" : "Select a category or create a new category..." -} - -const generateInitialThirdNavLabel = (thirdNavTitle, originalCategory) => { - if (thirdNavTitle) return thirdNavTitle - if (originalCategory && !thirdNavTitle) return 'None' - return "Select a third nav section..." -} - -const generateCategoryFieldTitle = (type, isCategoryDisabled) => { - if (isCategoryDisabled) { - return `${type === 'resource' ? `Resource Category` : `Collection`}` - } else { - return `Add to ${type === 'resource' ? `Resource Category` : `Collection (optional)`}` - } -} - -// Global state -let thirdNavData = {} - const ComponentSettingsModal = ({ - category: originalCategory, + category, fileName, - isCategoryDisabled, isNewFile, - modalTitle, - collectionPageData, + pageData, pageFileNames, - settingsToggle, siteName, - type, - loadThirdNavOptions, + setSelectedFile, setIsComponentSettingsActive, }) => { const { setRedirectToPage } = useRedirectHook() @@ -78,14 +46,10 @@ const ComponentSettingsModal = ({ permalink: '', fileUrl: '', resourceDate: '', - category: '', - originalCategory: '', }) const [hasErrors, setHasErrors] = useState(false) // Base hooks - const [allCategories, setAllCategories] = useState(null); - const [category, setCategory] = useState(''); const [baseApiUrl, setBaseApiUrl] = useState(''); const [title, setTitle] = useState('') const [permalink, setPermalink] = useState('') @@ -97,10 +61,6 @@ const ComponentSettingsModal = ({ const [originalPermalink, setOriginalPermalink] = useState('') const [originalFileUrl, setOriginalFileUrl] = useState('') - // Collections-related - const [originalThirdNavTitle, setOriginalThirdNavTitle] = useState('') // this state is required for comparison purposes - const [thirdNavTitle, setThirdNavTitle] = useState('') - // Resource-related const [resourceDate, setResourceDate] = useState('') const [fileUrl, setFileUrl] = useState('') @@ -108,25 +68,10 @@ const ComponentSettingsModal = ({ // Page redirection modals const [canShowDeleteWarningModal, setCanShowDeleteWarningModal] = useState(false) - // Backup third nav option loader - const backupLoadThirdNavOptions = async () => { - if (thirdNavData[category]) { - return new Promise((resolve) => { - resolve(thirdNavData[category]) - }); - } - - const { thirdNavOptions } = await retrieveThirdNavOptions(siteName, category, allCategories.map(category => category.value).includes(category)) - thirdNavData[category] = thirdNavOptions - return thirdNavOptions - } - // Map element ID to setter functions const idToSetterFuncMap = { - category: setCategory, title: setTitle, permalink: setPermalink, - thirdNavTitle: setThirdNavTitle, date: setResourceDate, fileUrl: setFileUrl, } @@ -134,75 +79,47 @@ const ComponentSettingsModal = ({ useEffect(() => { let _isMounted = true - if (originalCategory) setCategory(originalCategory); - - const baseUrl = `${process.env.REACT_APP_BACKEND_URL}/sites/${siteName}${originalCategory ? type === "resource" ? `/resources/${originalCategory}` : `/collections/${originalCategory}` : ''}` - setBaseApiUrl(baseUrl) - - const fetchData = async () => { - // Retrieve the list of all page/resource categories for use in the dropdown options. Also sets the default category if none is specified. - if (type === 'resource') { - const resourcesResp = await axios.get(`${process.env.REACT_APP_BACKEND_URL}/sites/${siteName}/resources`); - const { resources } = resourcesResp.data; - if (_isMounted) setAllCategories(resources.map((category) => ({ - value: category.dirName, - label: category.dirName, - }))) - } else if (type === 'page') { - const collectionsResp = await axios.get(`${process.env.REACT_APP_BACKEND_URL}/sites/${siteName}/collections`); - const { collections } = collectionsResp.data; - if (_isMounted) setAllCategories(collections.map((category) => ( - { - value:category, - label:category, - } - ))) + const initializePageDetails = () => { + if (pageData !== undefined) { // is existing page + const { pageContent, pageSha } = pageData + const { frontMatter, pageMdBody } = frontMatterParser(pageContent) + const { file_url: originalFileUrl, permalink: originalPermalink } = frontMatter + const { title: originalTitle, type: originalType, date: originalDate } = retrieveResourceFileMetadata(fileName) + if (_isMounted) { + setSha(pageSha) + setMdBody(pageMdBody) + setIsPost(originalType === 'post') + + // Front matter properties + setTitle(originalTitle) + + setPermalink(originalPermalink) + setOriginalPermalink(originalPermalink) + setFileUrl(originalFileUrl) + setOriginalFileUrl(originalFileUrl) + + setResourceDate(originalDate) } - - // Set component form values - if (isNewFile) { - // Set default values for new file - if (_isMounted) { - setTitle('Title') - setPermalink(`${originalCategory ? `/${originalCategory}` : ''}/permalink`) - if (type === 'resource') setResourceDate(new Date().toISOString().split("T")[0]) - } - - } else { - // Retrieve data from an existing page/resource - const resp = await axios.get(`${baseUrl}/pages/${fileName}`); - const { content, sha: fileSha } = resp.data; - const base64DecodedContent = Base64.decode(content); - const { frontMatter, mdBody } = frontMatterParser(base64DecodedContent); - - if (_isMounted) { - // File properties - setSha(fileSha) - setMdBody(mdBody) - setIsPost(!frontMatter.file_url) - - // Front matter properties - setTitle(dequoteString(frontMatter.title)) - - setPermalink(frontMatter.permalink) - setOriginalPermalink(frontMatter.permalink) - setFileUrl(frontMatter.file_url) - setOriginalFileUrl(frontMatter.file_url) - - setResourceDate(type === 'resource' ? retrieveResourceFileMetadata(fileName).date : '') - setOriginalThirdNavTitle(frontMatter.third_nav_title) - setThirdNavTitle(frontMatter.third_nav_title) - } + } + if (isNewFile) { + const exampleDate = new Date().toISOString().split("T")[0] + const examplePermalink = `${category}/permalink` + let exampleTitle = 'Example Title' + while (_.find(pageFileNames, (v) => generateResourceFileName(exampleTitle, exampleDate, isPost) === v ) !== undefined) { + exampleTitle = exampleTitle+'_1' } + if (_isMounted) { + setTitle(exampleTitle) + setPermalink(examplePermalink) + setResourceDate(exampleDate) + } + } } - - fetchData().catch((err) => { - setIsComponentSettingsActive((prevState) => !prevState) - errorToast(`There was a problem retrieving data from your repo. ${DEFAULT_RETRY_MSG}`) - console.log(err) - }) - return () => { _isMounted = false } - }, []) + initializePageDetails() + return () => { + _isMounted = false + } + }, [pageData]) useEffect(() => { setHasErrors(_.some(errors, (field) => field.length > 0)); @@ -229,172 +146,35 @@ const ComponentSettingsModal = ({ } } - const saveHandler = async () => { - try { - const fileInfo = { - // state - title, - permalink, - fileUrl, - date: resourceDate, - mdBody, - sha, - category, - originalThirdNavTitle, - thirdNavTitle, - thirdNavOptions: thirdNavData[category] ? thirdNavData[category].map((thirdNavObj) => thirdNavObj.label): null, - // props - originalCategory, - collectionPageData, - type, - resourceType: isPost ? 'post' : 'file', - fileName, - isNewFile, - siteName, - isNewCollection: !allCategories.map(category => category.value).includes(category) - } - - const redirectUrl = await saveFileAndRetrieveUrl(fileInfo) - - // If editing details of existing file, refresh page - if (!isNewFile) window.location.reload(); - else { - // We refresh page if resource is being created from category folder - if (type === 'resource' && !isPost && originalCategory) { - window.location.reload(); - } else { - setRedirectToPage(redirectUrl) - } - } - - } catch (err) { - if (err?.response?.status === 409) { - // Error due to conflict in name - setErrors((prevState) => ({ - ...prevState, - title: 'This title is already in use. Please choose a different one.', - })); - errorToast(`Another ${type === 'resource' ? 'resource' : 'page'} with the same name exists. Please choose a different name.`) - } else { - errorToast(`There was a problem saving your page settings. ${DEFAULT_RETRY_MSG}`) - } - console.log(err); - } - } - - const deleteHandler = async () => { - try { - const params = { sha }; - await axios.delete(`${baseApiUrl}/pages/${fileName}`, { - data: params, - }); - - // Refresh page - window.location.reload(); - } catch (err) { - errorToast(`There was a problem trying to delete this file. ${DEFAULT_RETRY_MSG}.`) - console.log(err); - } - } + const { mutateAsync: saveHandler } = useMutation( + () => { + const frontMatter = isPost + ? { title, date: resourceDate, permalink } + : { title, date: resourceDate, file_url: fileUrl } + const newFileName = generateResourceFileName(title, resourceDate, isPost) + if (isNewFile) return createPageData({ siteName, resourceName: category, newFileName }, concatFrontMatterMdBody(frontMatter, mdBody)) + if (fileName !== newFileName) return renamePageData({ siteName, resourceName: category, fileName, newFileName }, concatFrontMatterMdBody(frontMatter, mdBody), sha) + return updatePageData({ siteName, resourceName: category, fileName }, concatFrontMatterMdBody(frontMatter, mdBody), sha) + }, + { + onSettled: () => {setSelectedFile(''); setIsComponentSettingsActive(false)}, + onSuccess: (redirectUrl) => redirectUrl ? setRedirectToPage(redirectUrl) : window.location.reload(), + onError: () => errorToast(`${isNewFile ? 'A new resource page could not be created.' : 'Your resource page settings could not be saved.'} ${DEFAULT_RETRY_MSG}`) + } + ) const changeHandler = (event) => { const { id, value } = event.target; - const currentFileName = fileName; - - const pageFileNamesExc = pageFileNames ? pageFileNames.filter((filename) => filename !== currentFileName) : null; - - let errorMessage, newFileName - if (type === 'resource') { - errorMessage = validateResourceSettings(id, value); - newFileName = generateResourceFileName(id === "title" ? value : title, id==="date" ? value : resourceDate) - } else if (type === 'page') { - errorMessage = validatePageSettings(id, value); - let groupIdentifier - if (originalCategory) { - groupIdentifier = fileName.split('-')[0] - newFileName = generateCollectionPageFileName(value, groupIdentifier) - } else { - newFileName = generatePageFileName(value); - } - } - - if (errorMessage === '' && pageFileNamesExc && pageFileNamesExc.includes(newFileName)) { - setErrors((prevState) => ({ - ...prevState, - title: 'This title is already in use. Please choose a different one.', - })); - idToSetterFuncMap[id](value); - } else { - setErrors((prevState) => ({ - ...prevState, - [id]: errorMessage, - })); - idToSetterFuncMap[id](value); - } - } - - const dropdownChangeHandler = (event) => { - const { id, value } = event.target; - let errorMessage - if (type === 'resource') { - errorMessage = validateResourceSettings(id, value); - } else if (type === 'page') { - errorMessage = validatePageSettings(id, value); - } + const { titleErrorMessage, errorMessage } = validateResourceSettings(id, value, title, resourceDate, isPost, pageFileNames.filter(file => file !== fileName)) + setErrors((prevState) => ({ ...prevState, [id]: errorMessage, + title: titleErrorMessage, })); idToSetterFuncMap[id](value); } - const categoryDropdownHandler = (newValue) => { - let event; - if (!newValue) { - // Field was cleared - event = { - target: { - id: 'category', - value: '', - } - } - } else { - const { value } = newValue - event = { - target: { - id: 'category', - value, - } - } - } - - dropdownChangeHandler(event); - }; - - // Artificially create event from dropdown action - const thirdNavDropdownHandler = (newValue) => { - let event; - if (!newValue) { - // Field was cleared - event = { - target: { - id: 'thirdNavTitle', - value: '', - }, - } - } else { - const { value } = newValue - event = { - target: { - id: 'thirdNavTitle', - value, - }, - } - } - - dropdownChangeHandler(event) - } - return ( <>
@@ -402,39 +182,18 @@ const ComponentSettingsModal = ({ && (
-

{modalTitle}

-
- {/* Category */} -

- {generateCategoryFieldTitle(type, isCategoryDisabled)} - { - type === 'resource' && !isCategoryDisabled && - (required) - } -

-
- -
- { errors.category && {errors.category} } + { isNewFile ? 'You may edit page details anytime. ' : ''} + To edit page content, simply click on the page title.
+ + My workspace > Resources > { category } > { title }

+
{/* Title */} - {/* Third Nav */} - { - ((type === "page" && originalCategory) || (type === 'page' && !originalCategory && category)) && - <> -

Add to Third Nav Section (optional)

-
- -
- - } +

{isPost? 'Page URL' : 'File URL'}

{/* Permalink */} - - { type === "resource" && - }
setCanShowDeleteWarningModal(true)} />
)}
- { - canShowDeleteWarningModal - && ( - setCanShowDeleteWarningModal(false)} - onDelete={deleteHandler} - type="resource" - /> - ) - } ); } diff --git a/src/components/MenuDropdown.jsx b/src/components/MenuDropdown.jsx index bd975e708..768f420a4 100644 --- a/src/components/MenuDropdown.jsx +++ b/src/components/MenuDropdown.jsx @@ -30,7 +30,7 @@ const MenuItem = ({ item, menuIndex, dropdownRef }) => { } } - const { type, handler } = item + const { type, handler, noBlur } = item const { itemName, itemId, iconClassName, children } = type ? getItemType(type) : item return (
{ onMouseDown={(e) => { e.stopPropagation() e.preventDefault() - dropdownRef.current.blur() - if (handler) handler(e) + if (!noBlur) dropdownRef.current.blur() + // if user clicks on nested button, don't run handler + if (handler && (e.target.id === `${itemId}-${menuIndex}` || !children || children.type !== 'button')) handler(e) }} className={`${elementStyles.dropdownItem}`} >
{ itemName }
- { children } +
{ children }
) } @@ -53,9 +54,9 @@ const MenuItem = ({ item, menuIndex, dropdownRef }) => { const MenuDropdown = ({ dropdownItems, menuIndex, dropdownRef, tabIndex, onBlur }) => { return (
- { dropdownItems.map(item => + { dropdownItems.filter(x => x).map(item => ( { const dropdownRef = useRef(null) const fileMoveDropdownRef = useRef(null) const [canShowDropdown, setCanShowDropdown] = useState(false) const [canShowFileMoveDropdown, setCanShowFileMoveDropdown] = useState(false) - const [canShowDeleteWarningModal, setCanShowDeleteWarningModal] = useState(false) - const [canShowGenericWarningModal, setCanShowGenericWarningModal] = useState(false) - const [chosenCategory, setChosenCategory] = useState() - const [isNewCollection, setIsNewCollection] = useState(false) - const baseApiUrl = `${process.env.REACT_APP_BACKEND_URL}/sites/${siteName}${category ? isResource ? `/resources/${category}` : `/collections/${category}` : ''}` useEffect(() => { if (canShowFileMoveDropdown) fileMoveDropdownRef.current.focus() if (canShowDropdown) dropdownRef.current.focus() }, [canShowFileMoveDropdown, canShowDropdown]) - const moveFile = async () => { - try { - // Retrieve data from existing page/resource - const resp = await axios.get(`${baseApiUrl}/pages/${fileName}`); - - const { content, sha } = resp.data; - const base64DecodedContent = Base64.decode(content); - const { frontMatter, mdBody } = frontMatterParser(base64DecodedContent); - const { - title, permalink, file_url: fileUrl, third_nav_title: thirdNavTitle, - } = frontMatter; - - let collectionPageData - if (!isResource && !isNewCollection && chosenCategory) { - // User selected an existing page collection - const { collectionPages } = await retrieveThirdNavOptions(siteName, chosenCategory, true) - collectionPageData = collectionPages - } - const fileInfo = { - title, - permalink, - fileUrl, - date, - mdBody, - sha, - category: chosenCategory, - originalCategory: category, - type: isResource ? 'resource' : 'page', - resourceType, - originalThirdNavTitle: thirdNavTitle, - fileName, - isNewFile: false, - siteName, - collectionPageData, - isNewCollection, - } - await saveFileAndRetrieveUrl(fileInfo) - - // Refresh page - window.location.reload(); - } catch (err) { - if (err?.response?.status === 409) { - // Error due to conflict in name - errorToast('This file name already exists in the category you are trying to move to. Please rename the file before proceeding.') - } else { - errorToast(`There was a problem trying to move this file. ${DEFAULT_RETRY_MSG}`) - } - setIsNewCollection(false) - setCanShowGenericWarningModal(false) - console.log(err); - } - } - - const deleteHandler = async () => { - try { - // Retrieve data from existing page/resource - const resp = await axios.get(`${baseApiUrl}/pages/${fileName}`); - - const { sha } = resp.data; - const params = { sha }; - await axios.delete(`${baseApiUrl}/pages/${fileName}`, { - data: params, - }); - - // Refresh page - window.location.reload(); - } catch (err) { - errorToast(`There was a problem trying to delete this file. ${DEFAULT_RETRY_MSG}`) - console.log(err); - } - } - const generateLink = () => { if (isResource) { return `/sites/${siteName}/resources/${category}/${fileName}` @@ -156,6 +79,7 @@ const OverviewCard = ({ // currentTarget is the parent element, relatedTarget is the clicked element if (!event.currentTarget.contains(event.relatedTarget)) { setCanShowFileMoveDropdown(false) + setQueryFolderName('') } } @@ -168,17 +92,17 @@ const OverviewCard = ({ <>
{category ? category : ''}
-

{generateTitle()}

+

{generateTitle()}

{`${date ? prettifyDate(date) : ''}${resourceType ? `/${resourceType.toUpperCase()}` : ''}`}

- {settingsToggle &&
- , + })) + : [{ + itemId: `loading`, + itemName:
, + }] + ), ]} setShowDropdown={canShowFileMoveDropdown} dropdownRef={fileMoveDropdownRef} - menuIndex={''} + menuIndex={itemIndex} tabIndex={1} - handleBlur={handleBlur} + onBlur={handleBlur} /> }
- } ) return ( <> { - resourceType !== 'file' + resourceType !== 'file' && !canShowFileMoveDropdown && !canShowDropdown // disables link while dropdown modals are open ? {CardContent} @@ -250,31 +194,6 @@ const OverviewCard = ({ {CardContent}
} - { - canShowGenericWarningModal && - { - setChosenCategory() - setIsNewCollection(false) - setCanShowGenericWarningModal(false) - }} - proceedText="Continue" - cancelText="Cancel" - /> - } - { - canShowDeleteWarningModal - && ( - setCanShowDeleteWarningModal(false)} - onDelete={deleteHandler} - type={isResource ? "resource" : "page"} - /> - ) - } ); }; @@ -283,7 +202,6 @@ const OverviewCard = ({ OverviewCard.propTypes = { date: PropTypes.string, category: PropTypes.string, - settingsToggle: PropTypes.func, itemIndex: PropTypes.number.isRequired, siteName: PropTypes.string.isRequired, fileName: PropTypes.string.isRequired, diff --git a/src/components/PageSettingsModal.jsx b/src/components/PageSettingsModal.jsx index 76944b3b9..d0aaea10e 100644 --- a/src/components/PageSettingsModal.jsx +++ b/src/components/PageSettingsModal.jsx @@ -1,25 +1,26 @@ import React, { useState, useEffect } from 'react'; -import { useQuery, useMutation } from 'react-query'; +import { useMutation } from 'react-query'; import axios from 'axios'; import * as _ from 'lodash'; -import FormField from './FormField'; + import { DEFAULT_RETRY_MSG, generatePageFileName, - generatePageContent, + concatFrontMatterMdBody, frontMatterParser, deslugifyPage, } from '../utils'; -import { getPage, createPage, updatePage } from '../api' +import { createPageData, updatePageData, renamePageData } from '../api' import elementStyles from '../styles/isomer-cms/Elements.module.scss'; -import contentStyles from '../styles/isomer-cms/pages/Content.module.scss'; import { validatePageSettings } from '../utils/validators'; -import SaveDeleteButtons from './SaveDeleteButtons'; import { errorToast } from '../utils/toasts'; + +import FormField from './FormField'; import FormFieldHorizontal from './FormFieldHorizontal'; +import SaveDeleteButtons from './SaveDeleteButtons'; import useRedirectHook from '../hooks/useRedirectHook'; @@ -27,12 +28,12 @@ import useRedirectHook from '../hooks/useRedirectHook'; axios.defaults.withCredentials = true const PageSettingsModal = ({ - pageType, folderName, subfolderName, originalPageName, isNewPage, pagesData, + pageData, siteName, setSelectedPage, setIsPageSettingsActive, @@ -43,6 +44,7 @@ const PageSettingsModal = ({ permalink: '', }) const [hasErrors, setHasErrors] = useState(false) + const [hasChanges, setHasChanges] = useState(false) // Base hooks const [title, setTitle] = useState('') @@ -57,36 +59,14 @@ const PageSettingsModal = ({ permalink: setPermalink, } - const {} = useQuery( - 'page', - async () => await getPage(pageType, siteName, folderName, originalPageName), - { - enabled: !isNewPage, - retry: false, - onSuccess: ({ content, sha }) => { - const { frontMatter, mdBody } = frontMatterParser(content) - setTitle(deslugifyPage(originalPageName)) - setPermalink(frontMatter.permalink) - setOriginalPermalink(frontMatter.permalink) - setSha(sha) - setMdBody(mdBody) - }, - onError: () => { - setSelectedPage('') - setIsPageSettingsActive(false) - errorToast(`The page data could not be retrieved. ${DEFAULT_RETRY_MSG}`) - } - } - ) - const { mutateAsync: saveHandler } = useMutation( - async () => { - if (originalPageName === generatePageFileName(title) && originalPermalink === permalink) return - const fileInfo = { siteName, title, permalink, mdBody, folderName, subfolderName, isNewPage, pageType, originalPageName } - const { endpointUrl, content, redirectUrl } = generatePageContent(fileInfo) - if (isNewPage) await createPage(endpointUrl, content) - if (!isNewPage) await updatePage(endpointUrl, content, sha) - return redirectUrl + () => { + const frontMatter = subfolderName + ? { title, permalink, third_nav_title: subfolderName } + : { title, permalink } + if (isNewPage) return createPageData({ siteName, folderName, subfolderName, newFileName: generatePageFileName(title) }, concatFrontMatterMdBody(frontMatter, mdBody)) + if (originalPageName !== generatePageFileName(title)) return renamePageData({ siteName, folderName, subfolderName, fileName: originalPageName, newFileName: generatePageFileName(title) }, concatFrontMatterMdBody(frontMatter, mdBody), sha) + return updatePageData({ siteName, folderName, subfolderName, fileName: originalPageName }, concatFrontMatterMdBody(frontMatter, mdBody), sha) }, { onSettled: () => {setSelectedPage(''); setIsPageSettingsActive(false)}, @@ -96,22 +76,51 @@ const PageSettingsModal = ({ ) useEffect(() => { - let exampleTitle = 'Example Title' - while (_.find(pagesData, function(v) { return v.type === 'file' && generatePageFileName(exampleTitle) === v.name }) !== undefined) { - exampleTitle = exampleTitle+'_1' + let _isMounted = true + + const initializePageDetails = () => { + if (pageData !== undefined) { // is existing page + const { pageContent, pageSha } = pageData + const { frontMatter, pageMdBody } = frontMatterParser(pageContent) + const { permalink: originalPermalink } = frontMatter + + if (_isMounted) { + setTitle(deslugifyPage(originalPageName)) + setPermalink(originalPermalink) + setOriginalPermalink(originalPermalink) + setSha(pageSha) + setMdBody(pageMdBody) + } + } + if (isNewPage) { + let exampleTitle = 'Example Title' + while (_.find(pagesData, function(v) { return v.type === 'file' && generatePageFileName(exampleTitle) === v.name }) !== undefined) { + exampleTitle = exampleTitle+'_1' + } + const examplePermalink = `/${folderName ? `${folderName}/` : ''}${subfolderName ? `${subfolderName}/` : ''}permalink` + if (_isMounted) { + setTitle(exampleTitle) + setPermalink(examplePermalink) + } + } + } + initializePageDetails() + return () => { + _isMounted = false } - const examplePermalink = `/${folderName ? `${folderName}/` : ''}${subfolderName ? `${subfolderName}/` : ''}permalink` - setTitle(exampleTitle) - setPermalink(examplePermalink) }, []) useEffect(() => { setHasErrors(_.some(errors, (field) => field.length > 0)); }, [errors]) + useEffect(() => { + setHasChanges(!isNewPage && !(originalPageName === generatePageFileName(title) && originalPermalink === permalink)) + }, [title, permalink]) + const changeHandler = (event) => { const { id, value } = event.target; - const errorMessage = validatePageSettings(id, value, pagesData) + const errorMessage = validatePageSettings(id, value, pagesData.filter(page => page.name !== originalPageName)) setErrors((prevState) => ({ ...prevState, [id]: errorMessage, @@ -131,10 +140,10 @@ const PageSettingsModal = ({
- { isNewPage ? 'You may edit page details anytime. ' : ''} - To edit page content, simply click on the page title. -
- +
+ { isNewPage ? 'You may edit page details anytime. ' : ''} + To edit page content, simply click on the page title.
+ My workspace > { folderName @@ -146,10 +155,8 @@ const PageSettingsModal = ({ ? {subfolderName} > : null } - { title } + { title }

-
-
{/* Title */}
diff --git a/src/components/folders/FolderContent.jsx b/src/components/folders/FolderContent.jsx index 876902cd3..a156fb928 100644 --- a/src/components/folders/FolderContent.jsx +++ b/src/components/folders/FolderContent.jsx @@ -4,8 +4,6 @@ import { Droppable, Draggable } from 'react-beautiful-dnd'; import { DragDropContext } from 'react-beautiful-dnd'; import update from 'immutability-helper'; -import FolderModal from '../FolderModal'; - import { deslugifyPage } from '../../utils' import MenuDropdown from '../MenuDropdown' @@ -21,17 +19,39 @@ const FolderContentItem = ({ numItems, link, itemIndex, + queryFolderName, + allCategories, + setQueryFolderName, setSelectedPage, + setSelectedFolder, setIsPageSettingsActive, setIsFolderModalOpen, + setIsMoveModalActive, setIsDeleteModalActive }) => { const [showDropdown, setShowDropdown] = useState(false) + const [showFileMoveDropdown, setShowFileMoveDropdown] = useState(false) const dropdownRef = useRef(null) + const fileMoveDropdownRef = useRef(null) useEffect(() => { if (showDropdown) dropdownRef.current.focus() - }, [showDropdown]) + if (showFileMoveDropdown) fileMoveDropdownRef.current.focus() + }, [showDropdown, showFileMoveDropdown]) + + const handleBlur = (event) => { + // if the blur was because of outside focus + // currentTarget is the parent element, relatedTarget is the clicked element + if (!event.currentTarget.contains(event.relatedTarget)) { + setShowFileMoveDropdown(false) + setQueryFolderName('') + } + } + + const toggleDropdownModals = () => { + setShowFileMoveDropdown(!showFileMoveDropdown) + setShowDropdown(!showDropdown) + } const generateDropdownItems = () => { const dropdownItems = [ @@ -46,7 +66,7 @@ const FolderContentItem = ({ }, { type: 'move', - handler: () => {}, // to be added in separate PR + handler: toggleDropdownModals }, { type: 'delete', @@ -56,47 +76,110 @@ const FolderContentItem = ({ if (isFile) return dropdownItems return dropdownItems.filter(item => item.itemId !== 'move') } - return ( - -
-
- { - isFile - ? - : + + const FolderItemContent = ( +
+
+ { + isFile + ? + : + } + {deslugifyPage(title)} + { + numItems !== null + ? {numItems} item{numItems === 1 ? '' : 's'} + : null + } +
+ + { showDropdown && + setShowDropdown(false)} + /> } - {deslugifyPage(title)} - { - numItems - ? {numItems} item{numItems === 1 ? '' : 's'} - : null + { showFileMoveDropdown && + { + if (queryFolderName) setQueryFolderName('') + else toggleDropdownModals() + }, + noBlur: true, + }, + queryFolderName === '' && { + itemName: 'Workspace', + itemId: `workspace`, + handler: () => { setSelectedFolder(`pages`); setIsMoveModalActive(true) }, + }, + ...(allCategories + ? allCategories.map(categoryName => ({ + itemName: categoryName, + itemId: categoryName, + handler: () => { + setSelectedFolder(`${queryFolderName ? `${queryFolderName}/` : ''}${categoryName}`) + setIsMoveModalActive(true) + }, + children: queryFolderName === '' && + , + })) + : [{ + itemId: `loading`, + itemName:
, + }] + ), + ]} + setShowDropdown={showFileMoveDropdown} + dropdownRef={fileMoveDropdownRef} + menuIndex={itemIndex} + tabIndex={1} + onBlur={handleBlur} + /> } -
- - { showDropdown && - setShowDropdown(false)} - /> - } -
- +
+ ) + + return ( + !showFileMoveDropdown && !showDropdown + ? + + {FolderItemContent} + + : +
+ {FolderItemContent} +
) } @@ -106,9 +189,14 @@ const FolderContent = ({ siteName, folderName, enableDragDrop, + allCategories, + queryFolderName, + setSelectedFolder, + setQueryFolderName, setSelectedPage, setIsPageSettingsActive, setIsFolderModalOpen, + setIsMoveModalActive, setIsDeleteModalActive, }) => { const generateLink = (folderContentItem) => { @@ -174,10 +262,15 @@ const FolderContent = ({ numItems={folderContentItem.type === 'dir' ? folderContentItem.children.filter(name => !name.includes('.keep')).length : null} isFile={folderContentItem.type === 'dir' ? false: true} link={generateLink(folderContentItem)} + allCategories={allCategories} itemIndex={folderContentIndex} + queryFolderName={queryFolderName} + setQueryFolderName={setQueryFolderName} setSelectedPage={setSelectedPage} + setSelectedFolder={setSelectedFolder} setIsPageSettingsActive={setIsPageSettingsActive} setIsFolderModalOpen={setIsFolderModalOpen} + setIsMoveModalActive={setIsMoveModalActive} setIsDeleteModalActive={setIsDeleteModalActive} />
diff --git a/src/constants.js b/src/constants.js index c161f9f9a..1238d64f4 100644 --- a/src/constants.js +++ b/src/constants.js @@ -6,4 +6,5 @@ export const DIR_CONTENT_KEY = 'dir-contents'; export const CSP_CONTENT_KEY = 'csp-contents'; export const NAVIGATION_CONTENT_KEY = 'navigation-contents'; export const RESOURCE_CATEGORY_CONTENT_KEY = 'resource-category-contents' -export const RESOURCE_ROOM_CONTENT_KEY = 'resource-room-contents' \ No newline at end of file +export const RESOURCE_ROOM_CONTENT_KEY = 'resource-room-contents' +export const FOLDERS_CONTENT_KEY = 'folders-contents' diff --git a/src/layouts/Folders.jsx b/src/layouts/Folders.jsx index 0e7307019..fbb8e698d 100644 --- a/src/layouts/Folders.jsx +++ b/src/layouts/Folders.jsx @@ -14,11 +14,16 @@ import FolderContent from '../components/folders/FolderContent'; import FolderModal from '../components/FolderModal'; import PageSettingsModal from '../components/PageSettingsModal' import DeleteWarningModal from '../components/DeleteWarningModal' +import GenericWarningModal from '../components/GenericWarningModal' import { errorToast, successToast } from '../utils/toasts'; import useRedirectHook from '../hooks/useRedirectHook'; - +import { + PAGE_CONTENT_KEY, + FOLDERS_CONTENT_KEY, + DIR_CONTENT_KEY, +} from '../constants' import { DEFAULT_RETRY_MSG, parseDirectoryFile, @@ -28,15 +33,9 @@ import { retrieveSubfolderContents, convertSubfolderArray, } from '../utils' -import { DIR_CONTENT_KEY } from '../constants' // Import API -import { - getDirectoryFile, - setDirectoryFile, - deletePage, - deleteSubfolder, -} from '../api'; +import { getDirectoryFile, setDirectoryFile, getEditPageData, deleteSubfolder, deletePageData, moveFile, getAllCategories } from '../api'; // Import styles import elementStyles from '../styles/isomer-cms/Elements.module.scss'; @@ -54,9 +53,12 @@ const Folders = ({ match, location }) => { const [parsedFolderContents, setParsedFolderContents] = useState([]) const [isFolderCreationActive, setIsFolderCreationActive] = useState(false) const [isDeleteModalActive, setIsDeleteModalActive] = useState(false) + const [isMoveModalActive, setIsMoveModalActive] = useState(false) const [isFolderModalOpen, setIsFolderModalOpen] = useState(false) const [selectedPage, setSelectedPage] = useState('') + const [selectedFolder, setSelectedFolder] = useState('') const [isSelectedItemPage, setIsSelectedItemPage] = useState(false) + const [queryFolderName, setQueryFolderName] = useState('') const { data: folderContents, error: queryError, isLoading: isLoadingDirectory, refetch: refetchFolderContents } = useQuery( [DIR_CONTENT_KEY, siteName, folderName, subfolderName], @@ -72,63 +74,138 @@ const Folders = ({ match, location }) => { } } }, - ); + ) - const { mutate: rearrangeFolder } = useMutation( - payload => setDirectoryFile(siteName, folderName, payload), + // parse contents of current folder directory + useEffect(() => { + if (folderContents && folderContents.data) { + const parsedFolderContents = parseDirectoryFile(folderContents.data.content) + setDirectoryFileSha(folderContents.data.sha) + setParsedFolderContents(parsedFolderContents) + + if (subfolderName) { + const subfolderFiles = retrieveSubfolderContents(parsedFolderContents, subfolderName) + if (subfolderFiles.length > 0) { + setFolderOrderArray(subfolderFiles.filter(item => item.name !== '.keep')) + } else { + // if subfolderName prop does not match directory file, it's not a valid subfolder + setRedirectToPage(`/sites/${siteName}/workspace`) + } + } else { + setFolderOrderArray(convertFolderOrderToArray(parsedFolderContents)) + } + } + }, [folderContents, subfolderName]) + + // set selected item type + useEffect(() => { + if (selectedPage) { + const selectedItem = folderOrderArray.find((item) => item.name === selectedPage) + setIsSelectedItemPage(selectedItem.type === 'file' ? true : false) + } + }, [selectedPage]) + + // get page settings details when page is selected (used for editing page settings and deleting) + const { data: pageData } = useQuery( + [PAGE_CONTENT_KEY, { siteName, folderName, subfolderName, fileName: selectedPage }], + () => getEditPageData({ siteName, folderName, subfolderName, fileName: selectedPage }), { - onError: () => errorToast(`Your file reordering could not be saved. Please try again. ${DEFAULT_RETRY_MSG}`), - onSuccess: () => { - successToast('Successfully updated page order') - refetchFolderContents() + enabled: selectedPage.length > 0 && isSelectedItemPage, + retry: false, + onError: () => { + setSelectedPage('') + errorToast(`The page data could not be retrieved. ${DEFAULT_RETRY_MSG}`) }, - onSettled: () => setIsRearrangeActive((prevState) => !prevState), - } + }, ) - + // delete file const { mutateAsync: deleteHandler } = useMutation( async () => { - if (isSelectedItemPage) await deletePage('collection', folderName, subfolderName, selectedPage) + if (isSelectedItemPage) await deletePageData({ siteName, folderName, subfolderName, fileName: selectedPage }, pageData.pageSha) else await deleteSubfolder({ siteName, folderName, subfolderName: selectedPage }) }, { - onError: () => errorToast(`Your file could not be deleted successfully. ${DEFAULT_RETRY_MSG}`), + onError: () => errorToast(`Your ${isSelectedItemPage ? 'file' : 'subfolder'} could not be deleted successfully. ${DEFAULT_RETRY_MSG}`), onSuccess: () => { successToast(`Successfully deleted ${isSelectedItemPage ? 'file' : 'subfolder'}`) refetchFolderContents() }, - onSettled: () => setIsDeleteModalActive((prevState) => !prevState), + onSettled: () => { + setIsDeleteModalActive((prevState) => !prevState) + setSelectedPage('') + setSelectedFolder('') + setQueryFolderName('') + }, } ) - useEffect(() => { - if (folderContents && folderContents.data) { - const parsedFolderContents = parseDirectoryFile(folderContents.data.content) - setDirectoryFileSha(folderContents.data.sha) - setParsedFolderContents(parsedFolderContents) + // move file + const { mutateAsync: moveHandler } = useMutation( + () => moveFile({siteName, selectedFile: selectedPage, folderName, subfolderName, newPath: selectedFolder}), + { + onError: () => errorToast(`Your file could not be moved successfully. ${DEFAULT_RETRY_MSG}`), + onSuccess: () => { + successToast('Successfully moved file') + refetchFolderContents() + }, + onSettled: () => { + setIsMoveModalActive((prevState) => !prevState) + setSelectedPage('') + setSelectedFolder('') + setQueryFolderName('') + }, + } + ) - if (subfolderName) { - const subfolderFiles = retrieveSubfolderContents(parsedFolderContents, subfolderName) - if (subfolderFiles.length > 0) { - setFolderOrderArray(subfolderFiles.filter(item => item.name !== '.keep')) - } else { - // if subfolderName prop does not match directory file, it's not a valid subfolder - setRedirectToPage(`/sites/${siteName}/workspace`) - } - } else { - setFolderOrderArray(convertFolderOrderToArray(parsedFolderContents)) - } - } - }, [folderContents, subfolderName]) + // MOVE-TO Dropdown + // get all folders for move-to dropdown + const { data: allFolders } = useQuery( + [FOLDERS_CONTENT_KEY, { siteName, folderName }], + async () => getAllCategories({ siteName }), + { + enabled: selectedPage.length > 0 && isSelectedItemPage, + onError: () => errorToast(`The folders data could not be retrieved. ${DEFAULT_RETRY_MSG}`) + }, + ) - useEffect(() => { - if (selectedPage) { - const selectedItem = folderOrderArray.find((item) => item.name === selectedPage) - setIsSelectedItemPage(selectedItem.type === 'file' ? true : false) + // MOVE-TO Dropdown + // get subfolders of selected folder for move-to dropdown + const { data: querySubfolders } = useQuery( + [DIR_CONTENT_KEY, siteName, queryFolderName], + async () => getDirectoryFile(siteName, queryFolderName), + { + enabled: selectedPage.length > 0 && queryFolderName.length > 0 && isSelectedItemPage, + onError: () => errorToast(`The folders data could not be retrieved. ${DEFAULT_RETRY_MSG}`) + }, + ) + + // MOVE-TO Dropdown utils + // parse responses from move-to queries + const getCategories = (queryFolderName, allFolders, querySubfolders) => { + if (queryFolderName && querySubfolders) { + const parsedFolderContents = parseDirectoryFile(querySubfolders.data.content) + const parsedFolderArray = convertFolderOrderToArray(parsedFolderContents) + return parsedFolderArray.filter(file => file.type === 'dir').map(file => file.name) } - }, [selectedPage]) + if (allFolders) { + return allFolders.collections + } + return [] + } + // REORDERING + // save file-reordering + const { mutate: rearrangeFolder } = useMutation( + payload => setDirectoryFile(siteName, folderName, payload), + { + onError: () => errorToast(`Your file reordering could not be saved. ${DEFAULT_RETRY_MSG}`), + onSuccess: () => successToast('Successfully updated page order'), + onSettled: () => setIsRearrangeActive((prevState) => !prevState), + } + ) + + // REORDERING utils const toggleRearrange = () => { if (isRearrangeActive) { // drag and drop complete, save new order @@ -168,16 +245,16 @@ const Folders = ({ match, location }) => { /> } { - isPageSettingsActive + isPageSettingsActive && (!selectedPage || pageData) && ( item.type === 'file')} + pageData={pageData} siteName={siteName} originalPageName={selectedPage || ''} - isNewPage={!selectedPage.length > 0} + isNewPage={!selectedPage} setIsPageSettingsActive={setIsPageSettingsActive} setSelectedPage={setSelectedPage} /> @@ -206,6 +283,21 @@ const Folders = ({ match, location }) => { /> ) } + { + isMoveModalActive + && ( + { + setIsMoveModalActive(false) + }} + proceedText="Continue" + cancelText="Cancel" + /> + ) + }
{ && }
diff --git a/src/styles/isomer-cms/elements/base.scss b/src/styles/isomer-cms/elements/base.scss index e997b4d5f..a2279bafe 100644 --- a/src/styles/isomer-cms/elements/base.scss +++ b/src/styles/isomer-cms/elements/base.scss @@ -69,4 +69,10 @@ i { .info { color: $isomer-blue; font-size: 14px; +} + +.infoGrey { + color: grey; + font-size: 14px; + margin-bottom: 1.5rem; } \ No newline at end of file diff --git a/src/styles/isomer-cms/elements/dropdown.scss b/src/styles/isomer-cms/elements/dropdown.scss index de5f60b39..713b5353f 100644 --- a/src/styles/isomer-cms/elements/dropdown.scss +++ b/src/styles/isomer-cms/elements/dropdown.scss @@ -1,6 +1,8 @@ .dropdown { position: absolute; width: 16rem; + max-height: 16.75rem; + overflow: scroll; background: white; box-shadow: 2px 2px 12px 0 rgba(43, 95, 206, 0.1); border-radius: 5px; @@ -16,7 +18,7 @@ .dropdownItem { width: 100%; height: 3.5rem; - padding: 0.75rem 2rem 0.75rem 2rem; + padding: 0.75rem 0.25rem 0.75rem 1.5rem; display:flex; align-items: center; @@ -43,6 +45,14 @@ } } + .dropdownItemChildButton { + background: rgba(0, 0, 0, 0); + + &:hover { + background: rgba(43, 95, 206, 0.3); + } + } + hr { margin: 0; } diff --git a/src/styles/isomer-cms/pages/Content.module.scss b/src/styles/isomer-cms/pages/Content.module.scss index 720ded266..dccf55c54 100644 --- a/src/styles/isomer-cms/pages/Content.module.scss +++ b/src/styles/isomer-cms/pages/Content.module.scss @@ -226,6 +226,11 @@ text-overflow: ellipsis; } + .componentTitleLink{ + @extend .componentTitle; + color: $isomer-blue; + } + .componentFolderName{ font-size: 16px; line-height: 24px; diff --git a/src/utils.js b/src/utils.js index 2c6c9a5a1..730587fbf 100644 --- a/src/utils.js +++ b/src/utils.js @@ -171,9 +171,9 @@ export function dequoteString(str) { return dequotedString; } -export function generateResourceFileName(title, date, resourceType) { - const safeTitle = slugify(title).replace(/[^a-zA-Z0-9-]/g, ''); - return `${date}-${resourceType}-${safeTitle}.md`; +export function generateResourceFileName(title, date, isPost) { + const safeTitle = slugify(title, {lower: true}).replace(/[^a-zA-Z0-9-]/g, ''); + return `${date}-${isPost ? 'post' : 'file'}-${safeTitle}.md`; } export function prettifyResourceCategory(category) { diff --git a/src/utils/validators.js b/src/utils/validators.js index 5e82f2406..e412d23ce 100644 --- a/src/utils/validators.js +++ b/src/utils/validators.js @@ -1,7 +1,7 @@ import { slugifyCategory } from '../utils'; import _ from 'lodash'; -import { generatePageFileName } from '../utils'; +import { generatePageFileName, generateResourceFileName } from '../utils'; // Common regexes and constants // ============== @@ -690,17 +690,18 @@ const validateDayOfMonth = (month, day) => { } }; -const validateResourceSettings = (id, value) => { - let errorMessage = ''; +const validateResourceSettings = (id, value, title, resourceDate, isPost, folderOrderArray) => { + let titleErrorMessage = '', errorMessage = ''; + const newFileName = generateResourceFileName(id === "title" ? value : title, id==="date" ? value : resourceDate, isPost) switch (id) { case 'title': { // Title is too short if (value.length < RESOURCE_SETTINGS_TITLE_MIN_LENGTH) { - errorMessage = `The title should be longer than ${RESOURCE_SETTINGS_TITLE_MIN_LENGTH} characters.`; + titleErrorMessage = `The title should be longer than ${RESOURCE_SETTINGS_TITLE_MIN_LENGTH} characters.`; } // Title is too long if (value.length > RESOURCE_SETTINGS_TITLE_MAX_LENGTH) { - errorMessage = `The title should be shorter than ${RESOURCE_SETTINGS_TITLE_MAX_LENGTH} characters.`; + titleErrorMessage = `The title should be shorter than ${RESOURCE_SETTINGS_TITLE_MAX_LENGTH} characters.`; } break; } @@ -757,7 +758,10 @@ const validateResourceSettings = (id, value) => { break; } } - return errorMessage; + if (folderOrderArray !== undefined && folderOrderArray.includes(newFileName)) { + titleErrorMessage = `This title is already in use. Please choose a different title.`; + } + return { titleErrorMessage, errorMessage }; }; // Resource room creation