From edc3fa8304e078371a55f6717146423a7d6e7cb4 Mon Sep 17 00:00:00 2001 From: Pavel Denisjuk Date: Fri, 13 Mar 2026 19:51:42 +0100 Subject: [PATCH 1/6] chore: delete unused code and files --- extensions/_wip/idp/MyIdpConfig.ts | 65 ------------------------ extensions/_wip/idp/MyIdpExtension.tsx | 10 ---- extensions/_wip/myOnEntryBeforeCreate.ts | 14 ----- 3 files changed, 89 deletions(-) delete mode 100644 extensions/_wip/idp/MyIdpConfig.ts delete mode 100644 extensions/_wip/idp/MyIdpExtension.tsx delete mode 100644 extensions/_wip/myOnEntryBeforeCreate.ts diff --git a/extensions/_wip/idp/MyIdpConfig.ts b/extensions/_wip/idp/MyIdpConfig.ts deleted file mode 100644 index cd57a45f2c1..00000000000 --- a/extensions/_wip/idp/MyIdpConfig.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { Okta } from "webiny/idp/okta"; -import jwt from "jsonwebtoken"; -import type { Identity } from "@webiny/app-admin/src/domain/Identity.js"; -import type { Tenant } from "@webiny/api-core/src/types/tenancy.js"; - -interface IIdpConfig { - getIdentity(token: jwt.JwtPayload): Promise | Identity.Data; - getDefaultTenantId(identity: Identity.Data): string; - canAccessTenant(identity: Identity, tenant: Tenant): boolean; - verifyToken?(jwt: string): Promise | jwt.JwtPayload; - verifyTokenClaims?(jwt: jwt.JwtPayload): Promise | jwt.JwtPayload; -} - -class MyOktaConfig implements Okta.Api.Interface { - getIdentity(token: jwt.JwtPayload) { - return { - id: token["sub"], - type: "admin", - displayName: token["name"], - // User profile on this tenant - profile: { - roles: [token["role"]], - teams: [token["team"]], - firstName: token["given_name"], - lastName: token["family_name"], - email: token["email"] - }, - // For all other custom runtime data. This is not stored in the database. - context: { - clientId: token["iss"], - defaultTenant: token["tenantId"] - } - }; - } - - getDefaultTenantId(identity) { - // Return the default tenant ID for this identity. - return identity.context.defaultTenant; - } - - canAccessTenant(identity, tenant) { - // Implement logic that determines if the user can access the tenant. - return identity.context.tenantId === tenant.id; - } - - /** - * Optional - */ - async verifyToken(jwt: string): jwt.JwtPayload { - // If not using JWKeys, implement your own verification logic. - // The moment you implement this method, JWK verification is disabled. - } - - /** - * Optional - */ - async verifyTokenClaims(jwt: jwt.JwtPayload): jwt.JwtPayload { - // If using standard JWKeys, this callback will allow you to verify additional claims. - } -} - -export default Okta.Api.createImplementation({ - implementation: MyOktaConfig, - dependencies: [] -}); diff --git a/extensions/_wip/idp/MyIdpExtension.tsx b/extensions/_wip/idp/MyIdpExtension.tsx deleted file mode 100644 index 8bcec271140..00000000000 --- a/extensions/_wip/idp/MyIdpExtension.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import React from "react"; -import { Api } from "webiny/extensions"; - -export const MyIdpExtension = () => { - return ( - <> - - - ); -}; diff --git a/extensions/_wip/myOnEntryBeforeCreate.ts b/extensions/_wip/myOnEntryBeforeCreate.ts deleted file mode 100644 index c18ca0a8ec7..00000000000 --- a/extensions/_wip/myOnEntryBeforeCreate.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { createImplementation } from "@webiny/di"; -import { OnEntryBeforeCreate } from "webiny/api/cms/features"; - -class MyOnEntryBeforeCreate implements OnEntryBeforeCreate.Interface { - execute({ model, entry }: OnEntryBeforeCreate.Params) { - // Do something. - } -} - -export default createImplementation({ - abstraction: OnEntryBeforeCreate, - implementation: MyOnEntryBeforeCreate, - dependencies: [GetCmsEntry, GetCmsModel] -}); From f5225d0b18f6e268d69e6132eded121bce141870 Mon Sep 17 00:00:00 2001 From: Pavel Denisjuk Date: Fri, 13 Mar 2026 19:56:54 +0100 Subject: [PATCH 2/6] feat: add useEnvConfig hook --- packages/app/src/exports/admin/env-config.ts | 1 + packages/app/src/features/envConfig/EnvConfig.ts | 4 ++++ packages/app/src/features/envConfig/abstractions.ts | 1 + packages/app/src/index.ts | 1 + .../app/src/presentation/envConfig/useEnvConfig.ts | 13 +++++++++++++ 5 files changed, 20 insertions(+) create mode 100644 packages/app/src/presentation/envConfig/useEnvConfig.ts diff --git a/packages/app/src/exports/admin/env-config.ts b/packages/app/src/exports/admin/env-config.ts index 56d66ac5133..65712c8f03a 100644 --- a/packages/app/src/exports/admin/env-config.ts +++ b/packages/app/src/exports/admin/env-config.ts @@ -1 +1,2 @@ export { EnvConfig } from "~/features/envConfig/index.js"; +export { useEnvConfig } from "~/presentation/envConfig/useEnvConfig.js"; diff --git a/packages/app/src/features/envConfig/EnvConfig.ts b/packages/app/src/features/envConfig/EnvConfig.ts index 4fd80172219..2ddac559212 100644 --- a/packages/app/src/features/envConfig/EnvConfig.ts +++ b/packages/app/src/features/envConfig/EnvConfig.ts @@ -19,4 +19,8 @@ export class DefaultEnvConfig implements Abstraction.Interface { return rawValue; } + + getAll(): Abstraction.Config { + return this.env; + } } diff --git a/packages/app/src/features/envConfig/abstractions.ts b/packages/app/src/features/envConfig/abstractions.ts index 8b08aefb2bf..95afb3a1815 100644 --- a/packages/app/src/features/envConfig/abstractions.ts +++ b/packages/app/src/features/envConfig/abstractions.ts @@ -20,6 +20,7 @@ type Env = { export interface IEnvConfig { get(key: K, defaultValue?: Env[K]): Env[K]; + getAll(): Env; } export const EnvConfig = new Abstraction("EnvConfig"); diff --git a/packages/app/src/index.ts b/packages/app/src/index.ts index e89c4f62825..c6986447231 100644 --- a/packages/app/src/index.ts +++ b/packages/app/src/index.ts @@ -28,6 +28,7 @@ export { Route } from "./features/router/Route.js"; export { RouteLink, type RouteLinkProps } from "./presentation/router/components/RouteLink.js"; export { SimpleLink, type SimpleLinkProps } from "./presentation/router/components/SimpleLink.js"; +export { useEnvConfig } from "./presentation/envConfig/useEnvConfig.js"; export { useRouter, useRoute } from "./presentation/router/index.js"; export { useLocalStorage, diff --git a/packages/app/src/presentation/envConfig/useEnvConfig.ts b/packages/app/src/presentation/envConfig/useEnvConfig.ts new file mode 100644 index 00000000000..5b00a347d54 --- /dev/null +++ b/packages/app/src/presentation/envConfig/useEnvConfig.ts @@ -0,0 +1,13 @@ +import { useFeature } from "~/shared/di/useFeature.js"; +import { EnvConfig } from "~/features/envConfig/index.js"; +import { EnvConfigFeature } from "~/features/envConfig/feature.js"; + +/** + * Returns the EnvConfig instance from DI. + * Useful when you want to access EnvConfig inside components and hooks. + */ +export function useEnvConfig(): EnvConfig.Config { + const envConfig = useFeature(EnvConfigFeature); + + return envConfig.getAll(); +} From 28991ff79c04d2187e04bb22580c1d60274ec09d Mon Sep 17 00:00:00 2001 From: Pavel Denisjuk Date: Fri, 13 Mar 2026 19:57:15 +0100 Subject: [PATCH 3/6] feat: fill-grid utility class for grid background --- packages/admin-ui/src/theme.css | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/admin-ui/src/theme.css b/packages/admin-ui/src/theme.css index 9c38b3c6d93..5dfc231ab3c 100644 --- a/packages/admin-ui/src/theme.css +++ b/packages/admin-ui/src/theme.css @@ -346,6 +346,12 @@ @apply pr-xxl pl-xxl mx-auto w-full; } +@utility fill-grid { + background-image: radial-gradient(var(--color-neutral-strong) 1px, transparent 0px); + background-size: 15px 15px; + @apply bg-neutral-light; +} + @layer base { a { @apply text-accent-primary hover:underline focus-visible:outline-none focus-visible:ring-lg focus-visible:ring-primary-dimmed rounded-xs; From 576f8199b3e0f6247bc1eca300fd2c8cd86c60cc Mon Sep 17 00:00:00 2001 From: Pavel Denisjuk Date: Fri, 13 Mar 2026 19:58:33 +0100 Subject: [PATCH 4/6] feat: add readonly mode and improve UI resize handling --- .../src/BaseEditor/config/EditorConfig.tsx | 10 +++- .../src/BaseEditor/config/IsNotReadOnly.tsx | 16 ++++++ .../src/BaseEditor/config/IsReadOnly.tsx | 16 ++++++ .../src/BaseEditor/config/Layout.tsx | 21 ++++++- .../src/BaseEditor/config/Sidebar/Layout.tsx | 6 +- .../src/BaseEditor/config/Toolbar/Layout.tsx | 2 +- .../src/BaseEditor/config/Toolbar/Toolbar.tsx | 2 +- .../src/BaseEditor/config/TopBar/Layout.tsx | 1 + .../Content/Breadcrumbs/Breadcrumbs.tsx | 7 ++- .../Content/Preview/AddressBar.tsx | 5 +- .../defaultConfig/Content/Preview/Iframe.tsx | 52 ++++++++++++------ .../Content/Preview/PreviewContainer.tsx | 20 +++++++ .../Content/Preview/useResponsiveContainer.ts | 29 +++++----- .../defaultConfig/DefaultEditorConfig.tsx | 9 ++- .../BaseEditor/hooks/useReservedUISpace.ts | 55 +++++++++++++++++++ .../src/editorSdk/Editor.ts | 5 ++ .../PageEditor/DefaultPageEditorConfig.tsx | 9 ++- .../modules/pages/PageEditor/PageAutoSave.tsx | 6 ++ .../pages/PageEditor/PageEditorConfig.tsx | 4 +- .../modules/pages/PageEditor/TopBar/Title.tsx | 12 ++++ 20 files changed, 242 insertions(+), 45 deletions(-) create mode 100644 packages/app-website-builder/src/BaseEditor/config/IsNotReadOnly.tsx create mode 100644 packages/app-website-builder/src/BaseEditor/config/IsReadOnly.tsx create mode 100644 packages/app-website-builder/src/BaseEditor/defaultConfig/Content/Preview/PreviewContainer.tsx create mode 100644 packages/app-website-builder/src/BaseEditor/hooks/useReservedUISpace.ts diff --git a/packages/app-website-builder/src/BaseEditor/config/EditorConfig.tsx b/packages/app-website-builder/src/BaseEditor/config/EditorConfig.tsx index c5e46a58cce..e5e437c1ab3 100644 --- a/packages/app-website-builder/src/BaseEditor/config/EditorConfig.tsx +++ b/packages/app-website-builder/src/BaseEditor/config/EditorConfig.tsx @@ -13,6 +13,8 @@ import { ElementProperties, ElementProperty } from "./ElementProperty.js"; import { ElementAction, ElementActions } from "./ElementAction.js"; import type { ElementInputConfig } from "./ElementInput.js"; import { ElementInput } from "./ElementInput.js"; +import { IsNotReadOnly } from "~/BaseEditor/config/IsNotReadOnly.js"; +import { IsReadOnly } from "~/BaseEditor/config/IsReadOnly.js"; interface EditorConfig { elements: ElementConfig[]; @@ -21,7 +23,7 @@ interface EditorConfig { const base = createConfigurableComponent("DocumentEditor"); -export const EditorConfig = Object.assign(base.Config, { +export const EditorConfigComponents = { /** * Components to configure editor UI. */ @@ -33,6 +35,8 @@ export const EditorConfig = Object.assign(base.Config, { TopBar, Toolbar, Sidebar, + IsReadOnly, + IsNotReadOnly, OnActiveElement, NoActiveElement }, @@ -57,7 +61,9 @@ export const EditorConfig = Object.assign(base.Config, { * Access full editor config. WARNING: very low-level, we don't recommend using this directly! */ useEditorConfig -}); +}; + +export const EditorConfig = Object.assign(base.Config, EditorConfigComponents); export const EditorWithConfig = Object.assign(base.WithConfig, { displayName: "EditorWithConfig" }); diff --git a/packages/app-website-builder/src/BaseEditor/config/IsNotReadOnly.tsx b/packages/app-website-builder/src/BaseEditor/config/IsNotReadOnly.tsx new file mode 100644 index 00000000000..8b79574db72 --- /dev/null +++ b/packages/app-website-builder/src/BaseEditor/config/IsNotReadOnly.tsx @@ -0,0 +1,16 @@ +import React from "react"; +import { useSelectFromEditor } from "~/BaseEditor/hooks/useSelectFromEditor.js"; + +export interface IsNotReadOnlyProps { + children: React.ReactNode; +} + +export const IsNotReadOnly = ({ children }: IsNotReadOnlyProps) => { + const isEditorReadOnly = useSelectFromEditor(state => state.isReadOnly); + + if (isEditorReadOnly) { + return null; + } + + return <>{children}; +}; diff --git a/packages/app-website-builder/src/BaseEditor/config/IsReadOnly.tsx b/packages/app-website-builder/src/BaseEditor/config/IsReadOnly.tsx new file mode 100644 index 00000000000..3091fbcf030 --- /dev/null +++ b/packages/app-website-builder/src/BaseEditor/config/IsReadOnly.tsx @@ -0,0 +1,16 @@ +import React from "react"; +import { useSelectFromEditor } from "~/BaseEditor/hooks/useSelectFromEditor.js"; + +export interface IsNotReadOnlyProps { + children: React.ReactNode; +} + +export const IsReadOnly = ({ children }: IsNotReadOnlyProps) => { + const isEditorReadOnly = useSelectFromEditor(state => state.isReadOnly); + + if (isEditorReadOnly) { + return null; + } + + return <>{children}; +}; diff --git a/packages/app-website-builder/src/BaseEditor/config/Layout.tsx b/packages/app-website-builder/src/BaseEditor/config/Layout.tsx index 7a80a396942..cd582d8370e 100644 --- a/packages/app-website-builder/src/BaseEditor/config/Layout.tsx +++ b/packages/app-website-builder/src/BaseEditor/config/Layout.tsx @@ -2,6 +2,9 @@ import React, { useEffect } from "react"; import { makeDecoratable } from "@webiny/react-composition"; import { EditorConfig } from "./EditorConfig.js"; import styled from "@emotion/styled"; +import { IsNotReadOnly } from "~/BaseEditor/config/IsNotReadOnly.js"; +import { useReservedUISpace } from "~/BaseEditor/hooks/useReservedUISpace.js"; +import { useDocumentEditor } from "~/DocumentEditor/index.js"; const EditorLayoutContainer = styled.div` height: 100%; @@ -9,6 +12,14 @@ const EditorLayoutContainer = styled.div` `; export const Layout = makeDecoratable("EditorLayout", () => { + const editor = useDocumentEditor(); + + useReservedUISpace(dimensions => { + editor.updateEditor(state => { + state.uiReservedSpace = dimensions; + }); + }); + useEffect(() => { const currentOverflow = document.body.style.overflow; document.body.style.overflow = "hidden"; @@ -20,12 +31,16 @@ export const Layout = makeDecoratable("EditorLayout", () => { return ( -
- +
+ + +
- + + +
diff --git a/packages/app-website-builder/src/BaseEditor/config/Sidebar/Layout.tsx b/packages/app-website-builder/src/BaseEditor/config/Sidebar/Layout.tsx index 4ab2944cac9..b5893112551 100644 --- a/packages/app-website-builder/src/BaseEditor/config/Sidebar/Layout.tsx +++ b/packages/app-website-builder/src/BaseEditor/config/Sidebar/Layout.tsx @@ -5,7 +5,11 @@ import { Sidebar } from "./Sidebar.js"; export const Layout = makeDecoratable("SidebarLayout", () => { return ( -
+
{ return (
{ return ( -
+
); diff --git a/packages/app-website-builder/src/BaseEditor/config/TopBar/Layout.tsx b/packages/app-website-builder/src/BaseEditor/config/TopBar/Layout.tsx index 13676f63811..24b605cf61b 100644 --- a/packages/app-website-builder/src/BaseEditor/config/TopBar/Layout.tsx +++ b/packages/app-website-builder/src/BaseEditor/config/TopBar/Layout.tsx @@ -7,6 +7,7 @@ export const Layout = makeDecoratable("TopBarLayout", () => { return ( } middle={} end={ diff --git a/packages/app-website-builder/src/BaseEditor/defaultConfig/Content/Breadcrumbs/Breadcrumbs.tsx b/packages/app-website-builder/src/BaseEditor/defaultConfig/Content/Breadcrumbs/Breadcrumbs.tsx index 6fb6e0941db..224bd67a269 100644 --- a/packages/app-website-builder/src/BaseEditor/defaultConfig/Content/Breadcrumbs/Breadcrumbs.tsx +++ b/packages/app-website-builder/src/BaseEditor/defaultConfig/Content/Breadcrumbs/Breadcrumbs.tsx @@ -51,7 +51,12 @@ export const Breadcrumbs = () => { }, []); return ( -
    +
      {items.map(({ id, label }, index) => (
    • { }, [path, previewDomain]); return ( -
      +
      { const iframeRef = useRef(null); - const previewSize = useResponsiveContainer(props.viewportManager); + const previewBodyRef = useRef(null); + const previewWidth = useResponsiveContainer(props.viewportManager); const { viewport } = usePreviewData(); + const editor = useSelectFromEditor(state => ({ + isReadOnly: state.isReadOnly, + reservedHeight: state.uiReservedSpace.height + })); + + const isReadOnly = editor.isReadOnly; + + // When preview width changes, we need to force calculation of the iframe scrollHeight + // by reducing the height of its parent div to the current viewport height. + // Otherwise, the iframe grows to fill the parent div, and will not report the correct content height. + useEffect(() => { + if (previewBodyRef.current) { + const minHeight = `calc(100vh - ${editor.reservedHeight}px)`; + previewBodyRef.current.style.minHeight = minHeight; + } + }, [previewWidth]); const iframeUrl = useMemo(() => { const localUrl = new URL(url); @@ -28,15 +47,7 @@ export const Iframe = observer(({ url, timestamp, ...props }: IframeProps) => { }, [url, timestamp]); return ( -
      + {props.showLoading ? ( { {/* Content wrapper - sized by iframe content */}
      - + {!isReadOnly ? : null}