diff --git a/backend/models/core/Setting.js b/backend/models/core/Setting.js index b71f45a6..ddc01ac5 100644 --- a/backend/models/core/Setting.js +++ b/backend/models/core/Setting.js @@ -1,21 +1,44 @@ -import DB from '@klaudsol/commons/lib/DB'; +import DB from "@klaudsol/commons/lib/DB"; class Resource { - static async get({ slug }) { + static async get() { const db = new DB(); - const getSettingSQL = `SELECT * FROM \`settings\` WHERE \`key\` = :key`; + const sql = `SELECT * FROM \`settings\``; + + const data = await db.executeStatement(sql); + const output = data.records.map( + ([ + { longValue: id }, + { stringValue: setting }, + { stringValue: value }, + ]) => ({ + id, + setting, + value, + }) + ); - const resource = await db.executeStatement(getSettingSQL, [ - { name: "key", value: { stringValue: slug } }, - ]); + return output; + } - return resource.records.map( - ([{ longValue: id }, { stringValue: key }, { stringValue: value }]) => ({ + static async getLogo() { + const db = new DB(); + const sql = `SELECT * FROM settings WHERE setting = "main_logo"`; + + const data = await db.executeStatement(sql); + const output = data.records.map( + ([ + { longValue: id }, + { stringValue: setting }, + { stringValue: value }, + ]) => ({ id, - key, + setting, value, }) ); + + return output; } static async create({ key, value }) { @@ -57,32 +80,22 @@ class Resource { } } - static async update({ key, value }) { + static async update(entry) { const db = new DB(); - const updateValuesBatchSQL = `UPDATE settings SET - value = :value - WHERE \`key\` = :key + const sql = ` + UPDATE settings SET value = :value + WHERE \`setting\` = :setting `; - const valueParams = [ - { name: "key", value: { stringValue: key } }, - { name: "value", value: { stringValue: value } }, - ]; - await db.executeStatement(updateValuesBatchSQL, valueParams); - - const getSettingSQL = `SELECT * FROM \`settings\` WHERE \`key\` = :key`; - const updatedResource = await db.executeStatement(getSettingSQL, [ - { name: "key", value: { stringValue: key } }, + const valueParams = Object.keys(entry).map((e) => [ + { name: "setting", value: { stringValue: e } }, + { name: "value", value: { stringValue: entry[e] } }, ]); - return updatedResource.records.map( - ([{ longValue: id }, { stringValue: key }, { stringValue: value }]) => ({ - id, - key, - value, - }) - ); + await db.batchExecuteStatement(sql, valueParams); + + return true; } static async delete({ slug }) { diff --git a/components/elements/frontPage/LoginForm.js b/components/elements/frontPage/LoginForm.js index b05f847b..37b60140 100644 --- a/components/elements/frontPage/LoginForm.js +++ b/components/elements/frontPage/LoginForm.js @@ -67,8 +67,8 @@ const LoginForm = ({ className, logo }) => {
cms-logo { @@ -100,6 +100,16 @@ const AppSidebar = () => { await loadEntityTypes({rootState, rootDispatch}); })(); }, [rootState]); + + useEffect(() => { + (async () => { + // We need the settings at rootState because it's used in multiple + // parts of the program. I decided to load the settings in the + // sidebar because this is the only 'common denominator' among all pages. + // We need to have a layout component that covers all pages. + await loadSettings({ rootState, rootDispatch }); + })() + }, []) return ( <> diff --git a/components/fields/FileField.js b/components/fields/FileField.js index 13e66a6b..c8834dda 100644 --- a/components/fields/FileField.js +++ b/components/fields/FileField.js @@ -6,6 +6,7 @@ import AppButtonSpinner from "@/components/klaudsolcms/AppButtonSpinner"; import { FaTrash } from "react-icons/fa"; import { useEffect } from "react"; import { SET_CHANGED } from "@/lib/actions" +import defaultImage from "@/public/default-image.svg"; const FileField = (props) => { const { setFieldValue, setTouched, touched } = useFormikContext(); @@ -46,6 +47,12 @@ const FileField = (props) => { document.body.onfocus = checkIfUnfocused; }; + const getImageSrc = () => { + if(value?.name === 'default') return "/logo-180x180.png"; + + return value?.link ?? staticLink ?? defaultImage; + } + return (
@@ -64,7 +71,7 @@ const FileField = (props) => { value={value?.name || ""} onClick={openUploadMenu} > - {value?.name} + {value?.name === 'default' ? 'Default Logo' : value.name} )} {!props.hideUpload && {
{(value || staticLink) && ( {value?.name} { + return { ...acc, [curr.setting]: curr.value }; + }, {}); + + rootDispatch({ + type: SET_SETTINGS, + payload: data + }); + } catch (ex) { + console.error(ex.stack); + } +} + const hashEqualTo = ({ rootState, typeSlug, hash }) => rootState.entityType[typeSlug]?.metadata?.hash === hash; diff --git a/components/reducers/contentManagerReducer.js b/components/reducers/contentManagerReducer.js index ea89e130..26125aa6 100644 --- a/components/reducers/contentManagerReducer.js +++ b/components/reducers/contentManagerReducer.js @@ -11,7 +11,8 @@ import { ROWS_SET, PAGE_SETS_RENDERER, SET_FIRST_FETCH, - TOGGLE_VIEW + TOGGLE_VIEW, + SET_VIEW } from "@/lib/actions"; export const initialState = { @@ -103,5 +104,10 @@ export const contentManagerReducer = (state, action) => { ...state, view: state.view === 'list' ? 'icon' : 'list' }; + case SET_VIEW: + return { + ...state, + view: action.payload + }; } }; diff --git a/components/reducers/rootReducer.js b/components/reducers/rootReducer.js index 2df0f7e8..43de9d50 100644 --- a/components/reducers/rootReducer.js +++ b/components/reducers/rootReducer.js @@ -3,10 +3,16 @@ import { RESET_CLIENT_SESSION, SET_ENTITY_TYPES, SET_COLLAPSE, - SET_CURRENT_ENTITY_TYPE + SET_CURRENT_ENTITY_TYPE, + SET_SETTINGS } from '@/lib/actions'; export const rootInitialState = { + settings: { + default_view: '', + cms_name: '', + main_logo: '' + }, entityTypes: [], entityType: {}, currentContentType:{} @@ -56,6 +62,12 @@ export const rootReducer = (state, action) => { ...state, currentContentType: action.payload } + + case SET_SETTINGS: + return { + ...state, + settings: action.payload + } default: return state; diff --git a/components/reducers/settingReducer.js b/components/reducers/settingReducer.js index c3cb5ec6..328f5416 100644 --- a/components/reducers/settingReducer.js +++ b/components/reducers/settingReducer.js @@ -6,7 +6,11 @@ export const initialState = { isDeleting: false, isSaving: false, isChanged: false, - values: {}, + values: { + default_view: '', + cms_name: '', + main_logo: {}, + }, errorMessage:'' }; @@ -57,4 +61,4 @@ export const settingReducer = (state, action) => { default: return state } -}; \ No newline at end of file +}; diff --git a/lib/actions.js b/lib/actions.js index a98dcb2c..011a7612 100644 --- a/lib/actions.js +++ b/lib/actions.js @@ -4,6 +4,7 @@ export const SET_CLIENT_SESSION = "SET_CLIENT_SESSION"; export const RESET_CLIENT_SESSION = "RESET_CLIENT_SESSION"; +export const SET_SETTINGS = "SET_SETTINGS"; export const SET_ENTITY_TYPES = "SET_ENTITY_TYPES"; export const SET_COLLAPSE = "SET_COLLAPSE"; export const LOADING = "LOADING"; @@ -35,4 +36,5 @@ export const SET_FORCE_CHANGE_PASSWORD= "SET_FORCE_CHANGE_PASSWORD"; export const SET_ERROR = 'SET_ERROR'; export const SET_ALL_VALIDATES = 'SET_ALL_VALIDATES'; export const TOGGLE_VIEW = 'TOGGLE_VIEW'; +export const SET_VIEW = 'SET_VIEW'; diff --git a/pages/admin.js b/pages/admin.js index bb029bcc..1c35ff0a 100644 --- a/pages/admin.js +++ b/pages/admin.js @@ -1,5 +1,6 @@ import BasicLayout from "@/components/layouts/BasicLayout"; import CacheContext from "@/components/contexts/CacheContext"; +import RootContext from "@/components/contexts/RootContext"; import { getSessionCache } from "@klaudsol/commons/lib/Session"; import { useState, useEffect, useContext } from "react"; @@ -10,13 +11,16 @@ import { FcVoicePresentation } from "react-icons/fc"; export default function Admin({cache}) { const { firstName = null, lastName = null } = cache ?? {}; + const { state } = useContext(RootContext); + const cmsName = state.settings.cms_name + return (
-

Hello, {firstName}.

Welcome!

+

Hello, {firstName}.

Welcome to {cmsName}!

@@ -31,4 +35,4 @@ export default function Admin({cache}) { ); } -export const getServerSideProps = getSessionCache(); \ No newline at end of file +export const getServerSideProps = getSessionCache(); diff --git a/pages/admin/content-manager/[entity_type_slug]/[id].js b/pages/admin/content-manager/[entity_type_slug]/[id].js index 5029980a..27c6f0a7 100644 --- a/pages/admin/content-manager/[entity_type_slug]/[id].js +++ b/pages/admin/content-manager/[entity_type_slug]/[id].js @@ -154,7 +154,7 @@ export default function Type({ cache }) { body: JSON.stringify(entry), }); - const { message, presignedUrls } = await response.json(); + const { presignedUrls } = await response.json(); if (files.length > 0) await uploadFilesToUrl(files, presignedUrls); diff --git a/pages/admin/content-manager/[entity_type_slug]/index.js b/pages/admin/content-manager/[entity_type_slug]/index.js index 24aedd80..2e07daba 100644 --- a/pages/admin/content-manager/[entity_type_slug]/index.js +++ b/pages/admin/content-manager/[entity_type_slug]/index.js @@ -41,7 +41,8 @@ import { SET_FIRST_FETCH, SET_PAGE, PAGE_SETS_RENDERER, - TOGGLE_VIEW + TOGGLE_VIEW, + SET_VIEW } from "@/lib/actions"; import AppContentPagination from "components/klaudsolcms/pagination/AppContentPagination"; import { defaultPageRender, maximumNumberOfPage, EntryValues, writeContents} from "lib/Constants" @@ -52,7 +53,7 @@ export default function ContentManager({ cache }) { const capabilities = cache?.capabilities; const { entity_type_slug } = router.query; const controllerRef = useRef(); - const { state: {currentContentType} } = useContext(RootContext); + const { state: {settings, currentContentType} } = useContext(RootContext); const [state, dispatch] = useReducer(contentManagerReducer, initialState); @@ -102,12 +103,15 @@ export default function ContentManager({ cache }) { })(); }, [entity_type_slug, state.page, state.entry, state.setsRenderer]); - useEffect(() => { dispatch({type: SET_PAGE,payload: defaultPageRender}); dispatch({type: PAGE_SETS_RENDERER,payload: defaultPageRender}); }, [entity_type_slug]); + useEffect(() => { + dispatch({ type: SET_VIEW, payload: settings.default_view }); + }, [state.firstFetch]) + const handleView = () => { dispatch({ type: TOGGLE_VIEW }) } diff --git a/pages/admin/settings.js b/pages/admin/settings.js index 0fbc82b6..af9dfe13 100644 --- a/pages/admin/settings.js +++ b/pages/admin/settings.js @@ -1,8 +1,16 @@ import InnerSingleLayout from "@/components/layouts/InnerSingleLayout"; import CacheContext from "@/components/contexts/CacheContext"; +import RootContext from "@/components/contexts/RootContext"; import { getSessionCache } from "@klaudsol/commons/lib/Session"; -import { Formik, Form } from "formik"; -import { useRef, useReducer, useEffect, useState, useCallback } from "react"; +import { Formik, Form, Field } from "formik"; +import { + useRef, + useReducer, + useContext, + useEffect, + useState, + useCallback, +} from "react"; import AppButtonLg from "@/components/klaudsolcms/buttons/AppButtonLg"; import AppButtonSpinner from "@/components/klaudsolcms/AppButtonSpinner"; @@ -16,103 +24,73 @@ import { settingReducer, initialState, } from "@/components/reducers/settingReducer"; -import { SAVING, LOADING, DELETING, CLEANUP, SET_VALUES, SET_CHANGED, SET_ERROR } from "@/lib/actions"; +import { + SAVING, + LOADING, + DELETING, + CLEANUP, + SET_VALUES, + SET_CHANGED, + SET_ERROR, +} from "@/lib/actions"; import { defaultLogo } from "@/constants/index"; -import { convertToFormData, getAllFiles } from "@/lib/s3FormController"; +import { getFilesToDelete, getBody } from "@/lib/s3FormController"; +import { uploadFilesToUrl } from "@/backend/data_access/S3"; import { validImageTypes } from "@/lib/Constants"; import { readSettings, modifyLogo } from "@/lib/Constants"; export default function Settings({ cache }) { const formRef = useRef(); const [state, dispatch] = useReducer(settingReducer, initialState); - const isValueExists = Object.keys(state.values).length !== 0 + const { state: rootState, dispatch: rootDispatch } = useContext(RootContext); + const isValueExists = Object.keys(state.values).length !== 0; const capabilities = cache?.capabilities; - const setInitialValues = (data) => { - const initialVal = Object.keys(data).length !== 0 - ? { mainlogo: { name: data.key, link: data.link, key: data.value } } - : {}; - return initialVal; - }; - - useEffect(() => { - (async () => { - try { - const response = await slsFetch("/api/settings/mainlogo"); - const { data } = await response.json(); - const newData = setInitialValues(data); - - dispatch({ type: SET_VALUES, payload: newData }); - } catch (error) { - dispatch({ type: SET_ERROR, payload: error.message }); - } finally { - dispatch({ type: CLEANUP }); - } - })(); - }, []); - - const onDelete = useCallback((setStaticLink) => { - (async () => { - try { - dispatch({ type: DELETING, payload: true }); - const response = await slsFetch(`/api/settings/mainlogo`, { - method: "DELETE", - headers: { - "Content-type": "application/json", - }, - }); - dispatch({ type: SET_VALUES, payload: {} }); - formRef.current.resetForm({ values: {} }); - setStaticLink(''); - dispatch({ type: SET_CHANGED, payload:false }) - } catch (ex) { - console.error(ex); - } finally { - dispatch({ type: DELETING, payload: false }); - } - })(); - }, []); - const onSubmit = (evt) => { evt.preventDefault(); formRef.current.handleSubmit(); }; - const getS3Keys = (files) => { - if(!files) return + const getFilesToDelete = (values) => { + const files = Object.keys(values).filter( + (value) => values[value] instanceof File + ); + const keys = files.map((file) => state.values[file].key); - const fileKeys = Object.keys(files); - const s3Keys = fileKeys.map((file) => state.values[file].key); - - return s3Keys; + return keys; }; const formikParams = { innerRef: formRef, - initialValues: state.values, + initialValues: rootState.settings, + enableReinitialize: true, onSubmit: (values) => { (async () => { try { dispatch({ type: SAVING }); - const isFile = Object.entries(values)[0][1] instanceof File; - const isCreateMode = !isValueExists && isFile; - - const filesToUpload = !isCreateMode && getAllFiles(values); - const s3Keys = getS3Keys(filesToUpload); - const newValues = isCreateMode ? values : {...values, toDeleteRaw: s3Keys} + const { files, data, fileNames } = await getBody(values); + const toDelete = getFilesToDelete(values); + + const entry = { + ...data, + fileNames, + toDelete, + }; + + const url = `/api/settings`; + const params = { + method: "PUT", + headers: { + "Content-type": "application/json", + }, + body: JSON.stringify(entry), + }; + + const response = await slsFetch(url, params); - const formattedEntries = convertToFormData(newValues); - - const response = await slsFetch(`/api/settings${isCreateMode ? '' : '/mainlogo'}`, { - method: `${isCreateMode ? "POST" : "PUT"}`, - body: formattedEntries, - }); - const { data } = await response.json() + const { presignedUrls } = await response.json(); - const newData = setInitialValues(data); - dispatch({ type: SET_VALUES, payload: newData }); - formRef.current.resetForm({ values: newData }); - dispatch({ type: SET_CHANGED, payload:false }) + if (files.length > 0) await uploadFilesToUrl(files, presignedUrls); } catch (ex) { console.error(ex); } finally { @@ -127,47 +105,43 @@ export default function Settings({ cache }) {
- {capabilities.includes(readSettings) ?
-
-
-

Settings

-
-
- {capabilities.includes(modifyLogo) && : } - onClick={onSubmit} - isDisabled={state.isLoading || state.isSaving || state.isDeleting || !state.isChanged} - />} -
+
+
+

Settings

+ : } + onClick={onSubmit} + />
- {!state.isLoading && ( - - {() => ( -
- - - )} -
- )} - {!isValueExists && !state.isLoading && "Logo is not set"} -
:

{state.errorMessage}

} + +
+
+
+

CMS Name

+ +
+
+

+ Default view +

+ + + + +
+
+
+

Logo

+ +
+
+
+
diff --git a/pages/api/settings.js b/pages/api/settings.js index 1deb080e..eee3aaa6 100644 --- a/pages/api/settings.js +++ b/pages/api/settings.js @@ -3,6 +3,7 @@ import { withSession } from '@klaudsol/commons/lib/Session'; import { defaultErrorHandler } from '@klaudsol/commons/lib/ErrorHandler'; import { generateResource, + generatePresignedUrls, addFileToBucket } from "@/backend/data_access/S3"; @@ -17,12 +18,6 @@ import { readSettings, writeSettings } from '@/lib/Constants'; export default withSession(handler); -export const config = { - api: { - bodyParser: false, - }, -} - async function handler(req, res) { try { @@ -32,6 +27,8 @@ async function handler(req, res) { case "POST": const { req: parsedReq, res: parsedRes } = await parseFormData(req, res); return await create(parsedReq, parsedRes); + case "PUT": + return update(req, res); default: throw new Error(`Unsupported method: ${req.method}`); } @@ -41,9 +38,33 @@ async function handler(req, res) { } async function get(req, res) { - + await assertUserCan(readSettings, req); + + const settings = await Setting.get(); + + // Formats the values for the `main_logo` property since its an image from S3 + const mainLogoIndex = settings.findIndex((item) => item.setting === 'main_logo'); + const mainLogo = settings[mainLogoIndex]; + const mainLogoValue = mainLogo.value; + mainLogo.value = { + name: mainLogoValue.substring(mainLogoValue.indexOf('_') + 1), + key: mainLogoValue, + link: `${process.env.KS_S3_BASE_URL}/${mainLogoValue}` + } + settings[mainLogoIndex] = mainLogo; + + const output = { + data: settings, + metadata: {} + } + + output.metadata.hash = createHash(output); + + setCORSHeaders({response: res, url: process.env.FRONTEND_URL}); + settings ? res.status(OK).json(output ?? []) : res.status(NOT_FOUND).json({}) } +// deprecated async function create(req, res) { try { await assert({ @@ -97,3 +118,19 @@ async function create(req, res) { } } + +async function update(req, res) { + await assert({ + loggedIn: true, + }, req); + + await assertUserCan(writeSettings, req); + + const { fileNames, toDelete, ...entry } = req.body; + await Setting.update(entry); + + const presignedUrls = fileNames.length > 0 && await generatePresignedUrls(fileNames); + + res.status(OK).json({ message: 'Successfully updated the settings', presignedUrls }) +} + diff --git a/pages/index.js b/pages/index.js index d70ce767..e64ef0fd 100644 --- a/pages/index.js +++ b/pages/index.js @@ -13,33 +13,35 @@ export default function Index(props) { export const getServerSideProps = withIronSessionSsr( async function getServerSideProps({ req, res }) { - try { - var rawData = await Setting.get({ slug: mainlogo }); - var data = { ...rawData[0], link:`${process.env.KS_S3_BASE_URL}/${rawData[0].value}` }; - // To ensure maximum efficiency and avoid any unnecessary involvement with the assertUserCan() function, - // it is recommended to directly fetch our logo from the core setting. - } catch (err) {} + const rawData = await Setting.getLogo(); + const data = rawData[0]; - try{ - if(req.session?.cache?.forcePasswordChange){ - await serverSideLogout(req) - } - } catch(err){} + if (req.session?.cache?.forcePasswordChange) { + await serverSideLogout(req); + } + + const homepage = req.session?.cache?.homepage; + if (homepage && !req.session?.cache?.forcePasswordChange) { + return { + redirect: { + permanent: false, + destination: `/${homepage}`, + }, + }; + } else { + const logoLink = `${process.env.KS_S3_BASE_URL}/${data.value}`; + + return { + props: { + logo: data.value !== "default" ? logoLink : "/logo-180x180.png", + }, + }; + } + } catch (err) { + console.error(err); - let homepage = req.session?.cache?.homepage; - if ((homepage && !req.session?.cache?.forcePasswordChange)) { - return { - redirect: { - permanent: false, - destination: `/${homepage}`, - }, - }; - } - else { - return { - props: { logo: (process.env.KS_S3_BASE_URL && rawData?.length) ? data : {} }, - }; + return { props: {} }; } }, sessionOptions