From 552260e94c14b76796729b098f8c0ce71c12873a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Fern=C3=A1ndez=20de=20Alba?= Date: Wed, 15 Jan 2025 17:16:57 +0100 Subject: [PATCH 1/5] Export the getContent bare fetcher (#6594) --- apps/rr7/app/content.tsx | 61 ++++++++----------------------- apps/rr7/app/root.tsx | 38 +++++++++++++++++-- apps/rr7/app/routes.ts | 2 +- packages/client/news/6594.feature | 1 + packages/client/src/client.ts | 2 + 5 files changed, 55 insertions(+), 49 deletions(-) create mode 100644 packages/client/news/6594.feature diff --git a/apps/rr7/app/content.tsx b/apps/rr7/app/content.tsx index a8483813d9..cfc6134824 100644 --- a/apps/rr7/app/content.tsx +++ b/apps/rr7/app/content.tsx @@ -1,39 +1,20 @@ -import type { LoaderArgs } from './routes/+types.home'; -import { - dehydrate, - QueryClient, - HydrationBoundary, - useQuery, - useQueryClient, -} from '@tanstack/react-query'; -import { useLoaderData, useLocation } from 'react-router'; -import type { MetaFunction } from 'react-router'; -import { usePloneClient } from '@plone/providers'; +import type { Route } from './+types/content'; +import { data, useLoaderData, useLocation } from 'react-router'; import PloneClient from '@plone/client'; import App from '@plone/slots/components/App'; import config from '@plone/registry'; -export const meta: MetaFunction = () => { +export const meta: Route.MetaFunction = ({ data }) => { return [ - { title: 'Plone on React Router 7' }, - { name: 'description', content: 'Welcome to Plone!' }, + { title: data?.title }, + { name: 'description', content: data?.description }, ]; }; const expand = ['navroot', 'breadcrumbs', 'navigation']; // eslint-disable-next-line @typescript-eslint/no-unused-vars -export async function loader({ params, request }: LoaderArgs) { - const queryClient = new QueryClient({ - defaultOptions: { - queries: { - // With SSR, we usually want to set some default staleTime - // above 0 to avoid refetching immediately on the client - staleTime: 60 * 1000, - }, - }, - }); - +export async function loader({ params, request }: Route.LoaderArgs) { const ploneClient = config .getUtility({ name: 'ploneClient', @@ -41,7 +22,7 @@ export async function loader({ params, request }: LoaderArgs) { }) .method(); - const { getContentQuery } = ploneClient as PloneClient; + const { getContent } = ploneClient as PloneClient; const path = new URL(request.url).pathname; @@ -57,30 +38,20 @@ export async function loader({ params, request }: LoaderArgs) { ) ) { console.log('prefetching', path); - await queryClient.prefetchQuery(getContentQuery({ path, expand })); + try { + return await getContent({ path, expand }); + } catch (error) { + throw data('Content Not Found', { status: 404 }); + } } else { console.log('path not prefetched', path); + throw data('Content Not Found', { status: 404 }); } - - return { dehydratedState: dehydrate(queryClient) }; -} - -function Page() { - const { getContentQuery } = usePloneClient(); - const pathname = useLocation().pathname; - const { data } = useQuery(getContentQuery({ path: pathname, expand })); - - if (!data) return 'Loading...'; - return ; } export default function Content() { - const { dehydratedState } = useLoaderData(); - const queryClient = useQueryClient(); + const data = useLoaderData(); + const pathname = useLocation().pathname; - return ( - - - - ); + return ; } diff --git a/apps/rr7/app/root.tsx b/apps/rr7/app/root.tsx index d1c4bb5bae..7340c7627a 100644 --- a/apps/rr7/app/root.tsx +++ b/apps/rr7/app/root.tsx @@ -1,3 +1,5 @@ +import type { LinksFunction } from 'react-router'; +import type { Route } from './+types/root'; import { useState } from 'react'; import { Links, @@ -10,8 +12,8 @@ import { useNavigate as useRRNavigate, useParams, useLoaderData, + isRouteErrorResponse, } from 'react-router'; -import type { LinksFunction } from 'react-router'; import { QueryClient } from '@tanstack/react-query'; import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; @@ -24,8 +26,8 @@ import installSSR from './config.server'; install(); -import '@plone/theming/styles/main.css'; -import '@plone/slots/main.css'; +import themingMain from '@plone/theming/styles/main.css?url'; +import slotsMain from '@plone/slots/main.css?url'; function useNavigate() { const navigate = useRRNavigate(); @@ -37,6 +39,8 @@ function useHrefLocal(to: string) { } export const links: LinksFunction = () => [ + { rel: 'stylesheet', href: themingMain }, + { rel: 'stylesheet', href: slotsMain }, { rel: 'preconnect', href: 'https://fonts.googleapis.com' }, { rel: 'preconnect', @@ -85,6 +89,34 @@ export function Layout({ children }: { children: React.ReactNode }) { ); } +export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) { + let message = 'Oops!'; + let details = 'An unexpected error occurred.'; + let stack: string | undefined; + if (isRouteErrorResponse(error)) { + message = error.status === 404 ? '404' : 'Error'; + details = + error.status === 404 + ? 'The requested page could not be found.' + : error.statusText || details; + } else if (import.meta.env.DEV && error && error instanceof Error) { + details = error.message; + stack = error.stack; + } + + return ( +
+

{message}

+

{details}

+ {stack && ( +
+          {stack}
+        
+ )} +
+ ); +} + export default function App() { if (!import.meta.env.SSR) { config.settings.apiPath = window.env.PLONE_API_PATH; diff --git a/apps/rr7/app/routes.ts b/apps/rr7/app/routes.ts index a4981be3dc..4d6b9ec32f 100644 --- a/apps/rr7/app/routes.ts +++ b/apps/rr7/app/routes.ts @@ -4,7 +4,7 @@ import { index, route } from '@react-router/dev/routes'; const routes: RouteConfig = [ index('content.tsx', { id: 'index' }), route('ok', 'okroute.tsx', { id: 'ok' }), - route('*', 'content.tsx', { id: 'splat' }), + route('*', 'content.tsx', { id: 'content' }), ]; export default routes; diff --git a/packages/client/news/6594.feature b/packages/client/news/6594.feature new file mode 100644 index 0000000000..7ba9fa94c3 --- /dev/null +++ b/packages/client/news/6594.feature @@ -0,0 +1 @@ +Import `getContent` bare fetcher. @sneridagh diff --git a/packages/client/src/client.ts b/packages/client/src/client.ts index b0d5e5289a..a14033d7f2 100644 --- a/packages/client/src/client.ts +++ b/packages/client/src/client.ts @@ -4,6 +4,7 @@ import { } from './restapi/login/post'; import type { LoginArgs } from './restapi/login/post'; +import { getContent as _getContent } from './restapi/content/get'; import { getContentQuery as _getContentQuery } from './restapi/content/get'; import { createContentMutation as _createContentMutation } from './restapi/content/add'; import { updateContentMutation as _updateContentMutation } from './restapi/content/update'; @@ -150,6 +151,7 @@ export default class PloneClient { /* Content queries */ + getContent = queryWithConfig(_getContent, this.getConfig); getContentQuery = queryWithConfig(_getContentQuery, this.getConfig); createContentMutation = mutationWithConfig( _createContentMutation, From 5dc98e1d2d604dd84da13c5b1ab76db2b2e842db Mon Sep 17 00:00:00 2001 From: Victor Fernandez de Alba Date: Wed, 15 Jan 2025 18:48:08 +0100 Subject: [PATCH 2/5] Release @plone/client 1.0.0-alpha.21 --- packages/client/CHANGELOG.md | 6 ++++++ packages/client/news/6594.feature | 1 - packages/client/package.json | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) delete mode 100644 packages/client/news/6594.feature diff --git a/packages/client/CHANGELOG.md b/packages/client/CHANGELOG.md index 57c72746b1..5e2d83708e 100644 --- a/packages/client/CHANGELOG.md +++ b/packages/client/CHANGELOG.md @@ -8,6 +8,12 @@ +## 1.0.0-alpha.21 (2025-01-15) + +### Feature + +- Import `getContent` bare fetcher. @sneridagh [#6594](https://github.com/plone/volto/pull/6594) + ## 1.0.0-alpha.20 (2024-11-05) ### Internal diff --git a/packages/client/news/6594.feature b/packages/client/news/6594.feature deleted file mode 100644 index 7ba9fa94c3..0000000000 --- a/packages/client/news/6594.feature +++ /dev/null @@ -1 +0,0 @@ -Import `getContent` bare fetcher. @sneridagh diff --git a/packages/client/package.json b/packages/client/package.json index 327f15c87c..e4bce7a30d 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -8,7 +8,7 @@ } ], "license": "MIT", - "version": "1.0.0-alpha.20", + "version": "1.0.0-alpha.21", "repository": { "type": "git", "url": "git@github.com:plone/volto.git" From 5c71a35d1c91bc38aadd629c3e92cf40fb42aa4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Fern=C3=A1ndez=20de=20Alba?= Date: Fri, 17 Jan 2025 15:51:50 +0100 Subject: [PATCH 3/5] Route registry (#6600) --- packages/coresandbox/src/index.ts | 10 +++ packages/registry/news/6600.feature | 1 + packages/registry/src/index.ts | 16 ++++ packages/registry/src/registry.test.tsx | 97 +++++++++++++++++++++++++ packages/types/news/6600.feature | 1 + packages/types/src/config/index.d.ts | 18 +++++ packages/types/src/utils.d.ts | 8 ++ 7 files changed, 151 insertions(+) create mode 100644 packages/registry/news/6600.feature create mode 100644 packages/types/news/6600.feature diff --git a/packages/coresandbox/src/index.ts b/packages/coresandbox/src/index.ts index a27e22cd2d..ec4979d483 100644 --- a/packages/coresandbox/src/index.ts +++ b/packages/coresandbox/src/index.ts @@ -235,6 +235,16 @@ const applyConfig = (config: ConfigType) => { predicates: [ContentTypeCondition(['Document']), RouteCondition('/hello')], }); + config.registerRoute({ + type: 'route', + path: '/hello', + file: 'src/components/Views/NewsAndEvents/asd.tsx', + options: { + id: 'hello', + index: true, + }, + }); + return config; }; diff --git a/packages/registry/news/6600.feature b/packages/registry/news/6600.feature new file mode 100644 index 0000000000..946395b56f --- /dev/null +++ b/packages/registry/news/6600.feature @@ -0,0 +1 @@ +Added route registry. @sneridagh diff --git a/packages/registry/src/index.ts b/packages/registry/src/index.ts index 5394254281..2254fa42b6 100644 --- a/packages/registry/src/index.ts +++ b/packages/registry/src/index.ts @@ -13,6 +13,7 @@ import type { UtilitiesConfig, ViewsConfig, WidgetsConfig, + ReactRouterRouteEntry, } from '@plone/types'; export type ConfigData = { @@ -22,6 +23,7 @@ export type ConfigData = { widgets: WidgetsConfig | Record; addonReducers?: AddonReducersConfig; addonRoutes?: AddonRoutesConfig; + routes?: Array; slots: SlotsConfig | Record; components: ComponentsConfig | Record; utilities: UtilitiesConfig | Record; @@ -126,6 +128,14 @@ class Config { this._data.addonRoutes = addonRoutes; } + get routes() { + return this._data.routes; + } + + set routes(routes) { + this._data.routes = routes; + } + get slots() { return this._data.slots; } @@ -494,6 +504,12 @@ class Config { return utilities; } + + registerRoute(options: ReactRouterRouteEntry) { + const route = this._data.routes || []; + route.push(options); + this._data.routes = route; + } } const instance = new Config(); diff --git a/packages/registry/src/registry.test.tsx b/packages/registry/src/registry.test.tsx index 90ad418249..02df01d9d6 100644 --- a/packages/registry/src/registry.test.tsx +++ b/packages/registry/src/registry.test.tsx @@ -1073,3 +1073,100 @@ describe('Utilities registry', () => { ).toEqual([]); }); }); + +describe('Routes registry', () => { + afterEach(() => { + config.set('routes', []); + }); + + it('registers a simple route', () => { + config.registerRoute({ + type: 'route', + path: '/login', + file: 'login.tsx', + }); + + expect(config.routes).toEqual([ + { + type: 'route', + path: '/login', + file: 'login.tsx', + }, + ]); + }); + + it('registers a simple route with options', () => { + config.registerRoute({ + type: 'route', + path: '/login', + file: 'login.tsx', + options: { id: 'login', caseSensitive: true }, + }); + + expect(config.routes).toEqual([ + { + type: 'route', + path: '/login', + file: 'login.tsx', + options: { id: 'login', caseSensitive: true }, + }, + ]); + }); + + it('registers a nested route', () => { + config.registerRoute({ + type: 'route', + path: '/login', + file: 'login.tsx', + children: [ + { + type: 'route', + path: '/login/ok', + file: 'ok.tsx', + }, + ], + }); + + expect(config.routes).toEqual([ + { + type: 'route', + path: '/login', + file: 'login.tsx', + children: [ + { + type: 'route', + path: '/login/ok', + file: 'ok.tsx', + }, + ], + }, + ]); + }); + + it('registers a couple of routes', () => { + config.registerRoute({ + type: 'route', + path: '/login', + file: 'login.tsx', + }); + + config.registerRoute({ + type: 'route', + path: '/logout', + file: 'logout.tsx', + }); + + expect(config.routes).toEqual([ + { + type: 'route', + path: '/login', + file: 'login.tsx', + }, + { + type: 'route', + path: '/logout', + file: 'logout.tsx', + }, + ]); + }); +}); diff --git a/packages/types/news/6600.feature b/packages/types/news/6600.feature new file mode 100644 index 0000000000..cfa4d841ba --- /dev/null +++ b/packages/types/news/6600.feature @@ -0,0 +1 @@ +Added typings for the route registry. @sneridagh diff --git a/packages/types/src/config/index.d.ts b/packages/types/src/config/index.d.ts index 58be498702..1ae06e1fdc 100644 --- a/packages/types/src/config/index.d.ts +++ b/packages/types/src/config/index.d.ts @@ -13,6 +13,24 @@ export type AddonRoutesConfig = { component: React.ComponentType; }[]; +export type AddonRoutesEntry = { + path: string; + exact: boolean; + component: React.ComponentType; +}; + +export type ReactRouterRouteEntry = { + type: 'route' | 'index' | 'layout' | 'prefix'; + path: string; + file: string; + options?: { + id?: string; + index?: boolean; + caseSensitive?: boolean; + }; + children?: ReactRouterRouteEntry[]; +}; + export type ComponentsConfig = Record< string, { component: React.ComponentType } diff --git a/packages/types/src/utils.d.ts b/packages/types/src/utils.d.ts index 220a792ceb..a564760a68 100644 --- a/packages/types/src/utils.d.ts +++ b/packages/types/src/utils.d.ts @@ -2,3 +2,11 @@ * Get the type of the elements in an array */ export type ArrayElement = A extends readonly (infer T)[] ? T : never; + +export type RequireAtLeastOne = Pick< + T, + Exclude +> & + { + [K in Keys]-?: Required> & Partial>>; + }[Keys]; From ae58c675db1c9fb06ec32e98e00c663da6a886c5 Mon Sep 17 00:00:00 2001 From: Shyam Raghuwanshi Date: Sat, 18 Jan 2025 04:40:36 +0530 Subject: [PATCH 4/5] fix site logo issue (#6591) Co-authored-by: Steve Piercy --- packages/volto/news/6591.feature | 1 + .../manage/Widgets/RegistryImageWidget.jsx | 31 +++++++++---------- 2 files changed, 16 insertions(+), 16 deletions(-) create mode 100644 packages/volto/news/6591.feature diff --git a/packages/volto/news/6591.feature b/packages/volto/news/6591.feature new file mode 100644 index 0000000000..f662901e21 --- /dev/null +++ b/packages/volto/news/6591.feature @@ -0,0 +1 @@ +- Fixed handling of the site logo preview to appear after upload. @Shyam-Raghuwanshi diff --git a/packages/volto/src/components/manage/Widgets/RegistryImageWidget.jsx b/packages/volto/src/components/manage/Widgets/RegistryImageWidget.jsx index 0ec4367a36..7ab9238486 100644 --- a/packages/volto/src/components/manage/Widgets/RegistryImageWidget.jsx +++ b/packages/volto/src/components/manage/Widgets/RegistryImageWidget.jsx @@ -3,7 +3,7 @@ * @module components/manage/Widgets/RegistryImageWidget */ -import React from 'react'; +import React, { useState } from 'react'; import PropTypes from 'prop-types'; import { Button, Image, Dimmer } from 'semantic-ui-react'; import { readAsDataURL } from 'promise-file-reader'; @@ -76,12 +76,15 @@ const RegistryImageWidget = (props) => { const { id, value, onChange, isDisabled } = props; const intl = useIntl(); - const fileName = value?.split(';')[0]; - const imgsrc = fileName - ? `${toPublicURL('/')}@@site-logo/${atob( - fileName.replace('filenameb64:', ''), - )}` - : ''; + // State to manage the preview image source + const [previewSrc, setPreviewSrc] = useState(() => { + const fileName = value?.split(';')[0]; + return fileName + ? `${toPublicURL('/')}@@site-logo/${atob( + fileName.replace('filenameb64:', ''), + )}` + : ''; + }); /** * Drop handler @@ -102,8 +105,7 @@ const RegistryImageWidget = (props) => { reader.onload = function () { const fields = reader.result.match(/^data:(.*);(.*),(.*)$/); if (imageMimetypes.includes(fields[1])) { - let imagePreview = document.getElementById(`field-${id}-image`); - imagePreview.src = reader.result; + setPreviewSrc(reader.result); } }; reader.readAsDataURL(files[0]); @@ -115,12 +117,12 @@ const RegistryImageWidget = (props) => { {({ getRootProps, getInputProps, isDragActive }) => (
{isDragActive && } - {imgsrc ? ( + {previewSrc ? ( ) : (
@@ -139,7 +141,6 @@ const RegistryImageWidget = (props) => { )}
)} -