From 5b7efc61eb7bfe69ca983a683ee55a6dfa20e6bc Mon Sep 17 00:00:00 2001 From: Misha Holtz <36575242+mnholtz@users.noreply.github.com> Date: Fri, 26 Apr 2024 14:50:36 -0700 Subject: [PATCH 1/9] introduce empty sidebarPanelTheme spec file --- end-to-end-tests/tests/runtime/sidebarPanelTheme.spec.ts | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 end-to-end-tests/tests/runtime/sidebarPanelTheme.spec.ts diff --git a/end-to-end-tests/tests/runtime/sidebarPanelTheme.spec.ts b/end-to-end-tests/tests/runtime/sidebarPanelTheme.spec.ts new file mode 100644 index 0000000000..e69de29bb2 From 6df2f3265ce5d74742b24d4a7876c1baaa19fe2c Mon Sep 17 00:00:00 2001 From: Misha Holtz <36575242+mnholtz@users.noreply.github.com> Date: Fri, 26 Apr 2024 14:56:53 -0700 Subject: [PATCH 2/9] introduce sidebar theme test --- .../tests/runtime/sidebarPanelTheme.spec.ts | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/end-to-end-tests/tests/runtime/sidebarPanelTheme.spec.ts b/end-to-end-tests/tests/runtime/sidebarPanelTheme.spec.ts index e69de29bb2..65a26957fd 100644 --- a/end-to-end-tests/tests/runtime/sidebarPanelTheme.spec.ts +++ b/end-to-end-tests/tests/runtime/sidebarPanelTheme.spec.ts @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2024 PixieBrix, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import { test } from "../../fixtures/extensionBase"; +import { ActivateModPage } from "../../pageObjects/extensionConsole/modsPage"; + +test("custom sidebar theme css file is applied to all levels of sidebar document", async ({ + page, + extensionId, +}) => { + const modId = "@pixies/testing/panel-theme"; + + const modActivationPage = new ActivateModPage(page, extensionId, modId); + await modActivationPage.goto(); + + await modActivationPage.clickActivateAndWaitForModsPageRedirect(); +}); From b8b62aa854ac2446cae17bf3c6c9179508b822a1 Mon Sep 17 00:00:00 2001 From: Misha Holtz <36575242+mnholtz@users.noreply.github.com> Date: Fri, 26 Apr 2024 15:27:09 -0700 Subject: [PATCH 3/9] add steps for opening the sidebar --- end-to-end-tests/tests/runtime/sidebarPanelTheme.spec.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/end-to-end-tests/tests/runtime/sidebarPanelTheme.spec.ts b/end-to-end-tests/tests/runtime/sidebarPanelTheme.spec.ts index 65a26957fd..95e9a122d5 100644 --- a/end-to-end-tests/tests/runtime/sidebarPanelTheme.spec.ts +++ b/end-to-end-tests/tests/runtime/sidebarPanelTheme.spec.ts @@ -17,6 +17,7 @@ import { test } from "../../fixtures/extensionBase"; import { ActivateModPage } from "../../pageObjects/extensionConsole/modsPage"; +import { runModViaQuickBar } from "../../utils"; test("custom sidebar theme css file is applied to all levels of sidebar document", async ({ page, @@ -28,4 +29,12 @@ test("custom sidebar theme css file is applied to all levels of sidebar document await modActivationPage.goto(); await modActivationPage.clickActivateAndWaitForModsPageRedirect(); + + await page.goto("/"); + + // Ensure the page is focused by clicking on an element before running the keyboard shortcut, see runModViaQuickbar + await page.getByText("Index of /").click(); + await runModViaQuickBar(page, "Show Sidebar"); + + await page.pause(); }); From 2530c2cd13425d4dda705f4ebc7e50ecf6e1a387 Mon Sep 17 00:00:00 2001 From: Misha Holtz <36575242+mnholtz@users.noreply.github.com> Date: Fri, 26 Apr 2024 15:54:15 -0700 Subject: [PATCH 4/9] finish failing test with green assertions --- .../tests/runtime/sidebarPanelTheme.spec.ts | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/end-to-end-tests/tests/runtime/sidebarPanelTheme.spec.ts b/end-to-end-tests/tests/runtime/sidebarPanelTheme.spec.ts index 95e9a122d5..2c4ec6bbb2 100644 --- a/end-to-end-tests/tests/runtime/sidebarPanelTheme.spec.ts +++ b/end-to-end-tests/tests/runtime/sidebarPanelTheme.spec.ts @@ -15,9 +15,10 @@ * along with this program. If not, see . */ -import { test } from "../../fixtures/extensionBase"; +import { test, expect } from "../../fixtures/extensionBase"; import { ActivateModPage } from "../../pageObjects/extensionConsole/modsPage"; -import { runModViaQuickBar } from "../../utils"; +import { getSidebarPage, runModViaQuickBar } from "../../utils"; +import type { Page } from "@playwright/test"; test("custom sidebar theme css file is applied to all levels of sidebar document", async ({ page, @@ -36,5 +37,15 @@ test("custom sidebar theme css file is applied to all levels of sidebar document await page.getByText("Index of /").click(); await runModViaQuickBar(page, "Show Sidebar"); - await page.pause(); + const sidebarPage = (await getSidebarPage(page, extensionId)) as Page; + await expect( + sidebarPage.getByText("#8347: Theme Inheritance", { exact: true }), + ).toBeVisible(); + + const green = "rgb(0, 128, 0)"; + (await sidebarPage.getByText("This should be green").all()).map( + async (element) => { + await expect(element).toHaveCSS("color", green); + }, + ); }); From 6aa05cbf3b033de6ae34ba4fffc277237aac90af Mon Sep 17 00:00:00 2001 From: Misha Holtz <36575242+mnholtz@users.noreply.github.com> Date: Fri, 26 Apr 2024 16:49:56 -0700 Subject: [PATCH 5/9] replace StylesheetsContext --- src/components/StylesheetsContext.ts | 109 +++++++++++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100644 src/components/StylesheetsContext.ts diff --git a/src/components/StylesheetsContext.ts b/src/components/StylesheetsContext.ts new file mode 100644 index 0000000000..45839154e6 --- /dev/null +++ b/src/components/StylesheetsContext.ts @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2024 PixieBrix, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import React, { useContext } from "react"; +import bootstrap from "@/vendors/bootstrapWithoutRem.css?loadAsUrl"; +import bootstrapOverrides from "@/pageEditor/sidebar/sidebarBootstrapOverrides.scss?loadAsUrl"; +import custom from "@/bricks/renderers/customForm.css?loadAsUrl"; + +export type StylesheetsContextType = { + stylesheets: string[] | null; +}; + +const StylesheetsContext = React.createContext({ + stylesheets: null, +}); + +function useStylesheetsContextWithDefaultValues({ + newStylesheets, + defaultStylesheets, + disableParentStyles, +}: { + newStylesheets: string[] | undefined; + defaultStylesheets: string[]; + disableParentStyles: boolean; +}): { + stylesheets: string[]; +} { + const { stylesheets: inheritedStylesheets } = useContext(StylesheetsContext); + + const stylesheets: string[] = []; + + if (!disableParentStyles) { + if (inheritedStylesheets == null) { + stylesheets.push(...defaultStylesheets); + } else { + stylesheets.push(...inheritedStylesheets); + } + } + + if (newStylesheets != null) { + stylesheets.push(...newStylesheets); + } + + return { stylesheets }; +} + +export function useStylesheetsContextWithDocumentDefault({ + newStylesheets, + disableParentStyles, +}: { + newStylesheets: string[] | undefined; + disableParentStyles: boolean; +}): { + stylesheets: string[]; +} { + return useStylesheetsContextWithDefaultValues({ + newStylesheets, + defaultStylesheets: [ + bootstrap, + bootstrapOverrides, + // DocumentView.css is an artifact produced by webpack, see the DocumentView entrypoint included in + // `webpack.config.mjs`. We build styles needed to render documents separately from the rest of the sidebar + // in order to isolate the rendered document from the custom Bootstrap theme included in the Sidebar app + "/DocumentView.css", + // Required because it can be nested in the DocumentView. + "/CustomFormComponent.css", + ], + disableParentStyles, + }); +} + +export function useStylesheetsContextWithFormDefault({ + newStylesheets, + disableParentStyles, +}: { + newStylesheets: string[] | undefined; + disableParentStyles: boolean; +}): { + stylesheets: string[]; +} { + return useStylesheetsContextWithDefaultValues({ + newStylesheets, + defaultStylesheets: [ + bootstrap, + bootstrapOverrides, + // CustomFormComponent.css and EphemeralFormContent.css are artifacts produced by webpack, see the entrypoints. + "/EphemeralFormContent.css", + "/CustomFormComponent.css", + custom, + ], + disableParentStyles, + }); +} + +export default StylesheetsContext; From 3c6da8d8477499c579f24e0389384136d3f45d6f Mon Sep 17 00:00:00 2001 From: Misha Holtz <36575242+mnholtz@users.noreply.github.com> Date: Fri, 26 Apr 2024 17:27:46 -0700 Subject: [PATCH 6/9] replace StylesheetsContext provider and hooks usage --- src/bricks/renderers/CustomFormComponent.tsx | 10 ++++- .../renderers/documentView/DocumentView.tsx | 43 ++++++++++++------- .../documentView/DocumentViewProps.tsx | 4 ++ src/components/StylesheetsContext.ts | 2 +- 4 files changed, 41 insertions(+), 18 deletions(-) diff --git a/src/bricks/renderers/CustomFormComponent.tsx b/src/bricks/renderers/CustomFormComponent.tsx index 9d081cb6eb..66263fa042 100644 --- a/src/bricks/renderers/CustomFormComponent.tsx +++ b/src/bricks/renderers/CustomFormComponent.tsx @@ -34,6 +34,7 @@ import DescriptionField from "@/components/formBuilder/DescriptionField"; import TextAreaWidget from "@/components/formBuilder/TextAreaWidget"; import RjsfSubmitContext from "@/components/formBuilder/RjsfSubmitContext"; import { cloneDeep } from "lodash"; +import { useStylesheetsContextWithFormDefault } from "@/components/StylesheetsContext"; const FIELDS = { DescriptionField, @@ -65,6 +66,7 @@ export type CustomFormComponentProps = { resetOnSubmit?: boolean; className?: string; stylesheets?: string[]; + disableParentStyles?: boolean; }; const CustomFormComponent: React.FunctionComponent< @@ -78,7 +80,8 @@ const CustomFormComponent: React.FunctionComponent< className, onSubmit, resetOnSubmit = false, - stylesheets, + disableParentStyles = false, + stylesheets: newStylesheets, }) => { // Use useRef instead of useState because we don't need/want a re-render when count changes // This ref is used to track the onSubmit run number for runtime tracing @@ -99,6 +102,11 @@ const CustomFormComponent: React.FunctionComponent< setKey((prev) => prev + 1); }; + const { stylesheets } = useStylesheetsContextWithFormDefault({ + newStylesheets, + disableParentStyles, + }); + const submitData = async (data: UnknownObject): Promise => { submissionCountRef.current += 1; await onSubmit(data, { diff --git a/src/bricks/renderers/documentView/DocumentView.tsx b/src/bricks/renderers/documentView/DocumentView.tsx index d1f65c5d24..fdb7cc3cba 100644 --- a/src/bricks/renderers/documentView/DocumentView.tsx +++ b/src/bricks/renderers/documentView/DocumentView.tsx @@ -23,10 +23,14 @@ import { type DocumentViewProps } from "./DocumentViewProps"; import DocumentContext from "@/components/documentBuilder/render/DocumentContext"; import { Stylesheets } from "@/components/Stylesheets"; import { joinPathParts } from "@/utils/formUtils"; +import StylesheetsContext, { + useStylesheetsContextWithDocumentDefault, +} from "@/components/StylesheetsContext"; const DocumentView: React.FC = ({ body, - stylesheets, + stylesheets: newStylesheets, + disableParentStyles, options, meta, onAction, @@ -41,26 +45,33 @@ const DocumentView: React.FC = ({ throw new Error("meta.extensionId is required for DocumentView"); } + const { stylesheets } = useStylesheetsContextWithDocumentDefault({ + newStylesheets, + disableParentStyles, + }); + return ( // Wrap in a React context provider that passes BrickOptions down to any embedded bricks - - {body.map((documentElement, index) => { - const documentBranch = buildDocumentBranch(documentElement, { - staticId: joinPathParts("body", "children"), - // Root of the document, so no branches taken yet - branches: [], - }); + + + {body.map((documentElement, index) => { + const documentBranch = buildDocumentBranch(documentElement, { + staticId: joinPathParts("body", "children"), + // Root of the document, so no branches taken yet + branches: [], + }); - if (documentBranch == null) { - return null; - } + if (documentBranch == null) { + return null; + } - const { Component, props } = documentBranch; - // eslint-disable-next-line react/no-array-index-key -- They have no other unique identifier - return ; - })} - + const { Component, props } = documentBranch; + // eslint-disable-next-line react/no-array-index-key -- They have no other unique identifier + return ; + })} + + ); }; diff --git a/src/bricks/renderers/documentView/DocumentViewProps.tsx b/src/bricks/renderers/documentView/DocumentViewProps.tsx index 65fca3255c..f677846a6f 100644 --- a/src/bricks/renderers/documentView/DocumentViewProps.tsx +++ b/src/bricks/renderers/documentView/DocumentViewProps.tsx @@ -33,6 +33,10 @@ export type DocumentViewProps = { * Remote stylesheets (URLs) to include in the document. */ stylesheets?: string[]; + /** + * Whether to disable the base (bootstrap) styles, plus any inherited styles, on the document (and children). + */ + disableParentStyles?: boolean; options: BrickOptions; meta: { diff --git a/src/components/StylesheetsContext.ts b/src/components/StylesheetsContext.ts index 45839154e6..d5a7c831da 100644 --- a/src/components/StylesheetsContext.ts +++ b/src/components/StylesheetsContext.ts @@ -17,7 +17,7 @@ import React, { useContext } from "react"; import bootstrap from "@/vendors/bootstrapWithoutRem.css?loadAsUrl"; -import bootstrapOverrides from "@/pageEditor/sidebar/sidebarBootstrapOverrides.scss?loadAsUrl"; +import bootstrapOverrides from "@/sidebar/sidebarBootstrapOverrides.scss?loadAsUrl"; import custom from "@/bricks/renderers/customForm.css?loadAsUrl"; export type StylesheetsContextType = { From 0c4b204391ab938b1c160770a04346b06083a03f Mon Sep 17 00:00:00 2001 From: Misha Holtz <36575242+mnholtz@users.noreply.github.com> Date: Fri, 26 Apr 2024 17:38:21 -0700 Subject: [PATCH 7/9] fix lint errors and add StylesheetsContext to strictnull --- .../tests/runtime/sidebarPanelTheme.spec.ts | 11 +++++++---- src/tsconfig.strictNullChecks.json | 1 + 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/end-to-end-tests/tests/runtime/sidebarPanelTheme.spec.ts b/end-to-end-tests/tests/runtime/sidebarPanelTheme.spec.ts index 2c4ec6bbb2..74360a015b 100644 --- a/end-to-end-tests/tests/runtime/sidebarPanelTheme.spec.ts +++ b/end-to-end-tests/tests/runtime/sidebarPanelTheme.spec.ts @@ -43,9 +43,12 @@ test("custom sidebar theme css file is applied to all levels of sidebar document ).toBeVisible(); const green = "rgb(0, 128, 0)"; - (await sidebarPage.getByText("This should be green").all()).map( - async (element) => { - await expect(element).toHaveCSS("color", green); - }, + const elementsThatShouldBeGreen = await sidebarPage + .getByText("This should be green") + .all(); + await Promise.all( + elementsThatShouldBeGreen.map(async (element) => + expect(element).toHaveCSS("color", green), + ), ); }); diff --git a/src/tsconfig.strictNullChecks.json b/src/tsconfig.strictNullChecks.json index 182122cf49..2422965741 100644 --- a/src/tsconfig.strictNullChecks.json +++ b/src/tsconfig.strictNullChecks.json @@ -250,6 +250,7 @@ "./components/StopPropagation.tsx", "./components/Stylesheets.test.tsx", "./components/Stylesheets.tsx", + "./components/StylesheetsContext.ts", "./components/TooltipIconButton.tsx", "./components/UnstyledButton.tsx", "./components/addBlockModal/TagList.tsx", From 213f50c0a0f902030d0fe22d73edf91b7b3a6c15 Mon Sep 17 00:00:00 2001 From: Misha Holtz <36575242+mnholtz@users.noreply.github.com> Date: Mon, 29 Apr 2024 12:07:23 -0700 Subject: [PATCH 8/9] replace ephemeralformcontent logic --- .../ephemeralForm/EphemeralFormContent.tsx | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/bricks/transformers/ephemeralForm/EphemeralFormContent.tsx b/src/bricks/transformers/ephemeralForm/EphemeralFormContent.tsx index 689ba4c3e4..4e99bca5dc 100644 --- a/src/bricks/transformers/ephemeralForm/EphemeralFormContent.tsx +++ b/src/bricks/transformers/ephemeralForm/EphemeralFormContent.tsx @@ -32,6 +32,7 @@ import DescriptionField from "@/components/formBuilder/DescriptionField"; import RjsfSelectWidget from "@/components/formBuilder/RjsfSelectWidget"; import TextAreaWidget from "@/components/formBuilder/TextAreaWidget"; import { Stylesheets } from "@/components/Stylesheets"; +import { useStylesheetsContextWithFormDefault } from "@/components/StylesheetsContext"; export const fields = { DescriptionField, @@ -55,8 +56,21 @@ const EphemeralFormContent: React.FC = ({ nonce, isModal, }) => { - const { schema, uiSchema, cancelable, submitCaption, stylesheets } = - definition; + const { + schema, + uiSchema, + cancelable, + submitCaption, + stylesheets: newStylesheets, + disableParentStyles, + } = definition; + + // Ephemeral form can never be nested, but we use this to pull in + // the (boostrap) base themes + const { stylesheets } = useStylesheetsContextWithFormDefault({ + newStylesheets, + disableParentStyles, + }); return ( From 8340c87fc34c13bf08ba2cd9ac6b82172563c972 Mon Sep 17 00:00:00 2001 From: Misha Holtz <36575242+mnholtz@users.noreply.github.com> Date: Mon, 29 Apr 2024 12:08:40 -0700 Subject: [PATCH 9/9] fix strict null error --- src/bricks/transformers/ephemeralForm/EphemeralFormContent.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bricks/transformers/ephemeralForm/EphemeralFormContent.tsx b/src/bricks/transformers/ephemeralForm/EphemeralFormContent.tsx index 4e99bca5dc..d4e2a42748 100644 --- a/src/bricks/transformers/ephemeralForm/EphemeralFormContent.tsx +++ b/src/bricks/transformers/ephemeralForm/EphemeralFormContent.tsx @@ -69,7 +69,7 @@ const EphemeralFormContent: React.FC = ({ // the (boostrap) base themes const { stylesheets } = useStylesheetsContextWithFormDefault({ newStylesheets, - disableParentStyles, + disableParentStyles: disableParentStyles ?? false, }); return (