From 799e041024e57fa75bceb4642ec006474d1f0b4e Mon Sep 17 00:00:00 2001 From: Thor Arne Johansen Date: Tue, 5 Mar 2024 16:17:42 +0100 Subject: [PATCH 01/14] Added new workflow --- .github/workflows/verji-release.yaml | 57 ++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 .github/workflows/verji-release.yaml diff --git a/.github/workflows/verji-release.yaml b/.github/workflows/verji-release.yaml new file mode 100644 index 0000000..0949189 --- /dev/null +++ b/.github/workflows/verji-release.yaml @@ -0,0 +1,57 @@ +name: Release Automation +on: + workflow_dispatch: + inputs: + version-bump: + description: The scale of the version bump required for semver compatibility + required: true + default: patch + type: choice + options: + - patch + - minor + - major +concurrency: release +jobs: + release: + name: "Release & Publish" + runs-on: ubuntu-latest + steps: + - name: 🧮 Checkout code + uses: actions/checkout@v3 + with: + token: ${{ secrets.ELEMENT_BOT_TOKEN }} + + - name: 🔧 Set up node environment + uses: actions/setup-node@v3 + with: + cache: "yarn" + + - name: 🛠️ Setup + run: yarn install --pure-lockfile + + - name: 👊 Bump version + run: | + yarn version --no-git-tag-version --${{ github.event.inputs.version-bump }} + git config --global user.name 'ElementRobot' + git config --global user.email 'releases@riot.im' + git commit -am "${{ github.event.inputs.version-bump }} version bump" + git push + + - name: 🚀 Publish to npm + id: npm-publish + uses: JS-DevTools/npm-publish@v1 + with: + token: ${{ secrets.NPM_TOKEN }} + access: public + + - name: 🧬 Create release + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: v${{ steps.npm-publish.outputs.version }} + release_name: Release ${{ steps.npm-publish.outputs.version }} + body: ${{ steps.npm-publish.outputs.version }} Release + draft: false + prerelease: false From 7df67839b6133550feaea1afa1ea3637e7166b88 Mon Sep 17 00:00:00 2001 From: Thor Arne Johansen Date: Tue, 5 Mar 2024 16:20:43 +0100 Subject: [PATCH 02/14] Update verji-release.yaml --- .github/workflows/verji-release.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/verji-release.yaml b/.github/workflows/verji-release.yaml index 0949189..0d81478 100644 --- a/.github/workflows/verji-release.yaml +++ b/.github/workflows/verji-release.yaml @@ -1,4 +1,4 @@ -name: Release Automation +name: Verji Build package on: workflow_dispatch: inputs: From 7f88a1360ff10d254d0cd89c958fba91db412514 Mon Sep 17 00:00:00 2001 From: John Tore Simonsen Date: Fri, 5 Apr 2024 16:42:24 +0200 Subject: [PATCH 03/14] Added CustomComponentLifecycle, added tests for CustomComponentLifecycle --- README.md | 9 ++ src/lifecycles/CustomComponentLifecycle.ts | 66 ++++++++++ src/lifecycles/types.ts | 3 +- .../CustomComponentLifecycle.test.tsx | 118 ++++++++++++++++++ 4 files changed, 195 insertions(+), 1 deletion(-) create mode 100644 src/lifecycles/CustomComponentLifecycle.ts create mode 100644 test/lifecycles/CustomComponentLifecycle.test.tsx diff --git a/README.md b/README.md index 1fe76be..6f75807 100644 --- a/README.md +++ b/README.md @@ -103,6 +103,15 @@ The module can also change what room/user/entity the user is looking at, and joi From the `RuntimeModule` instance, modules can listen for `WrapperLifecycle.Wrapper` to provide a wrapper react component. It would wrap the `MatrixChat` component and let any consumer add a header, a footer. +### Custom components +From the `RuntimeModule` instance, modules can listen for different `CustomComponentLifecycle` events and swap the component +with a custom written component. In principle it works the same way as the `WrapperLifecycle`, but the usecase is different. +Instead of wrapping the element, you can intercept it, consume the state of the component including its children, and return +your own customly written component. + +It is possible to add `matrix-react-sdk`, `matrix-js-sdk` as a dependency into your custom module implementation to gain complete +functionality within your custom components, aswell as the ability to reuse sub-components and styles. + ## Contributing / developing Please see [CONTRIBUTING.md](./CONTRIBUTING.md) for the mechanics of the contribution process. diff --git a/src/lifecycles/CustomComponentLifecycle.ts b/src/lifecycles/CustomComponentLifecycle.ts new file mode 100644 index 0000000..dd5fdc8 --- /dev/null +++ b/src/lifecycles/CustomComponentLifecycle.ts @@ -0,0 +1,66 @@ +/* +Copyright 2024 Verji Tech AS + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/* + CustomComponentLifecycle is heavily inspired by the WrapperLifecycle.ts, but is intended for a different usecase. + Instead of appending something within the wrapper, this lifecycle should swap the contents of the wrapper with a custom component provided by a module implementation. +*/ + +import { ComponentType, PropsWithChildren } from "react"; + +/** + * UserMenu lifecycle events + */ +export enum CustomComponentLifecycle { + /** + * An event to request the component module. `ModuleRunner` should invoke an event matching the wrapped component. + * so any custom module can get the component provided by the module if any. + */ + AppsDrawer = "apps_drawer", + EntityTile = "entity_tile", + ErrorBoundary = "error_boundary", + Experimental = "experimental", + HelpUserSettingsTab = "help_user_settings_tab", + LegacyRoomHeader = "legacy_room_header", + LeftPanel = "left_panel", + LoggedInView = "logged_in_view", + MatrixChat = "matrix_chat", + MemberTile = "member_tile", + MessageContextMenu = "message_context_menu", + ReactionsRow = "reactions_row", + ReactionsRowButtonTooltip = "reactions_row_button_tooltip", + RolesRoomSettingsTab = "roles_room_settings_tab", + RoomHeader = "room_header", + RoomView = "room_view", + SessionManagerTab = "session_manage_tab", + SpacePanel = "space_panel", + UserMenu = "user_menu" +} + +/** + * Opts object that is populated with a Wrapper. + */ +export type CustomComponentOpts = { + /** + * A Wrapper React Component to be rendered around a component to swap. i.e the component to override. + */ + CustomComponent: ComponentType>; +}; + +/** + * Helper type that documents how to implement a UserMenu listener. + */ +export type CustomComponentListener = (opts: CustomComponentOpts) => void; diff --git a/src/lifecycles/types.ts b/src/lifecycles/types.ts index 8bcded1..51ece61 100644 --- a/src/lifecycles/types.ts +++ b/src/lifecycles/types.ts @@ -17,5 +17,6 @@ limitations under the License. import { RoomViewLifecycle } from "./RoomViewLifecycle"; import { WidgetLifecycle } from "./WidgetLifecycle"; import { WrapperLifecycle } from "./WrapperLifecycle"; +import { CustomComponentLifecycle } from "./CustomComponentLifecycle"; -export type AnyLifecycle = RoomViewLifecycle | WidgetLifecycle | WrapperLifecycle; +export type AnyLifecycle = RoomViewLifecycle | WidgetLifecycle | WrapperLifecycle | CustomComponentLifecycle; diff --git a/test/lifecycles/CustomComponentLifecycle.test.tsx b/test/lifecycles/CustomComponentLifecycle.test.tsx new file mode 100644 index 0000000..aec950e --- /dev/null +++ b/test/lifecycles/CustomComponentLifecycle.test.tsx @@ -0,0 +1,118 @@ +/* +Copyright 2024 Verji Tech AS + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from "react"; +import { render, screen } from "@testing-library/react"; + +import { RuntimeModule } from "../../src/RuntimeModule"; +import { CustomComponentLifecycle, CustomComponentListener, CustomComponentOpts } from "../../src/lifecycles/CustomComponentLifecycle"; + +//Mock a CustomUserMenu +class CustomUserMenu extends React.Component{ + render() { + return
{this.props.children}
+ } +} +//Mock a default UserMenu +class UserMenu extends React.Component { + render(){ + return
{this.props.children}
+ } +} +describe("CustomComponentLifecycle", () => { + let module: RuntimeModule; + + beforeAll(() => { + + module = new (class extends RuntimeModule { + constructor() { + super(undefined as any); + + this.on(CustomComponentLifecycle.UserMenu, this.customComponentListener); + } + + protected customComponentListener: CustomComponentListener = (customComponentOpts: CustomComponentOpts) => { + customComponentOpts.CustomComponent = ({ children }) => { + let usermenu: any = React.Children.toArray(children)[0] + return ( + <> + + {usermenu.props?.children} + + + ); + }; + }; + })(); + }); + + it("should swap the UserMenu with CustomUserMenu and keep it's children", () => { + const customComponentOpts: CustomComponentOpts = { CustomComponent: React.Fragment }; + module.emit(CustomComponentLifecycle.UserMenu, customComponentOpts); + + render( + + + Child1 + Child2 + + , + ); + + const customUserMenu = screen.getByTitle("CustomUserMenu"); + expect(customUserMenu).toBeInTheDocument(); + expect(customUserMenu.children.length).toEqual(2) + + const defaultUserMenu = screen.queryByTitle("UserMenu"); + expect(defaultUserMenu).toStrictEqual(null) + + const child1 = screen.getByText(/Child1/i); + const child2 = screen.getByText(/Child2/i); + expect(child1).toBeInTheDocument(); + expect(child2).toBeInTheDocument(); + + }); + + it("should NOT swap the UserMenu when module emits an event we are not listening to in the module", () => { + const customComponentOpts: CustomComponentOpts = { CustomComponent: React.Fragment }; + + // We emit a different lifecycle event than what our mock-module is listening to + module.emit(CustomComponentLifecycle.Experimental, customComponentOpts); + + render( + + + Child1 + Child2 + + , + ); + + // The document should not be affected at all + const defaultUserMenu = screen.getByTitle("UserMenu"); + expect(defaultUserMenu).toBeInTheDocument(); + expect(defaultUserMenu.children.length).toEqual(2) + + const customUserMenu = screen.queryByTitle("CustomUserMenu"); + expect(customUserMenu).toStrictEqual(null) + + const child1 = screen.getByText(/Child1/i); + const child2 = screen.getByText(/Child2/i); + expect(child1).toBeInTheDocument(); + expect(child2).toBeInTheDocument(); + + }); +}); From f12babad33de1b3d28221bb23354fe95fe4703f6 Mon Sep 17 00:00:00 2001 From: John Tore Simonsen Date: Mon, 8 Apr 2024 10:26:14 +0200 Subject: [PATCH 04/14] prettified changes --- src/lifecycles/CustomComponentLifecycle.ts | 2 +- src/lifecycles/types.ts | 44 ++-- .../CustomComponentLifecycle.test.tsx | 236 +++++++++--------- 3 files changed, 141 insertions(+), 141 deletions(-) diff --git a/src/lifecycles/CustomComponentLifecycle.ts b/src/lifecycles/CustomComponentLifecycle.ts index dd5fdc8..5ffab8c 100644 --- a/src/lifecycles/CustomComponentLifecycle.ts +++ b/src/lifecycles/CustomComponentLifecycle.ts @@ -47,7 +47,7 @@ export enum CustomComponentLifecycle { RoomView = "room_view", SessionManagerTab = "session_manage_tab", SpacePanel = "space_panel", - UserMenu = "user_menu" + UserMenu = "user_menu", } /** diff --git a/src/lifecycles/types.ts b/src/lifecycles/types.ts index 51ece61..3c09ffa 100644 --- a/src/lifecycles/types.ts +++ b/src/lifecycles/types.ts @@ -1,22 +1,22 @@ -/* -Copyright 2022 The Matrix.org Foundation C.I.C. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -import { RoomViewLifecycle } from "./RoomViewLifecycle"; -import { WidgetLifecycle } from "./WidgetLifecycle"; -import { WrapperLifecycle } from "./WrapperLifecycle"; -import { CustomComponentLifecycle } from "./CustomComponentLifecycle"; - -export type AnyLifecycle = RoomViewLifecycle | WidgetLifecycle | WrapperLifecycle | CustomComponentLifecycle; +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { RoomViewLifecycle } from "./RoomViewLifecycle"; +import { WidgetLifecycle } from "./WidgetLifecycle"; +import { WrapperLifecycle } from "./WrapperLifecycle"; +import { CustomComponentLifecycle } from "./CustomComponentLifecycle"; + +export type AnyLifecycle = RoomViewLifecycle | WidgetLifecycle | WrapperLifecycle | CustomComponentLifecycle; diff --git a/test/lifecycles/CustomComponentLifecycle.test.tsx b/test/lifecycles/CustomComponentLifecycle.test.tsx index aec950e..784bb8f 100644 --- a/test/lifecycles/CustomComponentLifecycle.test.tsx +++ b/test/lifecycles/CustomComponentLifecycle.test.tsx @@ -1,118 +1,118 @@ -/* -Copyright 2024 Verji Tech AS - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -import React from "react"; -import { render, screen } from "@testing-library/react"; - -import { RuntimeModule } from "../../src/RuntimeModule"; -import { CustomComponentLifecycle, CustomComponentListener, CustomComponentOpts } from "../../src/lifecycles/CustomComponentLifecycle"; - -//Mock a CustomUserMenu -class CustomUserMenu extends React.Component{ - render() { - return
{this.props.children}
- } -} -//Mock a default UserMenu -class UserMenu extends React.Component { - render(){ - return
{this.props.children}
- } -} -describe("CustomComponentLifecycle", () => { - let module: RuntimeModule; - - beforeAll(() => { - - module = new (class extends RuntimeModule { - constructor() { - super(undefined as any); - - this.on(CustomComponentLifecycle.UserMenu, this.customComponentListener); - } - - protected customComponentListener: CustomComponentListener = (customComponentOpts: CustomComponentOpts) => { - customComponentOpts.CustomComponent = ({ children }) => { - let usermenu: any = React.Children.toArray(children)[0] - return ( - <> - - {usermenu.props?.children} - - - ); - }; - }; - })(); - }); - - it("should swap the UserMenu with CustomUserMenu and keep it's children", () => { - const customComponentOpts: CustomComponentOpts = { CustomComponent: React.Fragment }; - module.emit(CustomComponentLifecycle.UserMenu, customComponentOpts); - - render( - - - Child1 - Child2 - - , - ); - - const customUserMenu = screen.getByTitle("CustomUserMenu"); - expect(customUserMenu).toBeInTheDocument(); - expect(customUserMenu.children.length).toEqual(2) - - const defaultUserMenu = screen.queryByTitle("UserMenu"); - expect(defaultUserMenu).toStrictEqual(null) - - const child1 = screen.getByText(/Child1/i); - const child2 = screen.getByText(/Child2/i); - expect(child1).toBeInTheDocument(); - expect(child2).toBeInTheDocument(); - - }); - - it("should NOT swap the UserMenu when module emits an event we are not listening to in the module", () => { - const customComponentOpts: CustomComponentOpts = { CustomComponent: React.Fragment }; - - // We emit a different lifecycle event than what our mock-module is listening to - module.emit(CustomComponentLifecycle.Experimental, customComponentOpts); - - render( - - - Child1 - Child2 - - , - ); - - // The document should not be affected at all - const defaultUserMenu = screen.getByTitle("UserMenu"); - expect(defaultUserMenu).toBeInTheDocument(); - expect(defaultUserMenu.children.length).toEqual(2) - - const customUserMenu = screen.queryByTitle("CustomUserMenu"); - expect(customUserMenu).toStrictEqual(null) - - const child1 = screen.getByText(/Child1/i); - const child2 = screen.getByText(/Child2/i); - expect(child1).toBeInTheDocument(); - expect(child2).toBeInTheDocument(); - - }); -}); +/* +Copyright 2024 Verji Tech AS + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from "react"; +import { render, screen } from "@testing-library/react"; + +import { RuntimeModule } from "../../src/RuntimeModule"; +import { CustomComponentLifecycle, CustomComponentListener, CustomComponentOpts } from "../../src/lifecycles/CustomComponentLifecycle"; + +//Mock a CustomUserMenu +class CustomUserMenu extends React.Component{ + render() { + return
{this.props.children}
+ } +} +//Mock a default UserMenu +class UserMenu extends React.Component { + render(){ + return
{this.props.children}
+ } +} +describe("CustomComponentLifecycle", () => { + let module: RuntimeModule; + + beforeAll(() => { + + module = new (class extends RuntimeModule { + constructor() { + super(undefined as any); + + this.on(CustomComponentLifecycle.UserMenu, this.customComponentListener); + } + + protected customComponentListener: CustomComponentListener = (customComponentOpts: CustomComponentOpts) => { + customComponentOpts.CustomComponent = ({ children }) => { + let usermenu: any = React.Children.toArray(children)[0] + return ( + <> + + {usermenu.props?.children} + + + ); + }; + }; + })(); + }); + + it("should swap the UserMenu with CustomUserMenu and keep it's children", () => { + const customComponentOpts: CustomComponentOpts = { CustomComponent: React.Fragment }; + module.emit(CustomComponentLifecycle.UserMenu, customComponentOpts); + + render( + + + Child1 + Child2 + + , + ); + + const customUserMenu = screen.getByTitle("CustomUserMenu"); + expect(customUserMenu).toBeInTheDocument(); + expect(customUserMenu.children.length).toEqual(2) + + const defaultUserMenu = screen.queryByTitle("UserMenu"); + expect(defaultUserMenu).toStrictEqual(null) + + const child1 = screen.getByText(/Child1/i); + const child2 = screen.getByText(/Child2/i); + expect(child1).toBeInTheDocument(); + expect(child2).toBeInTheDocument(); + + }); + + it("should NOT swap the UserMenu when module emits an event we are not listening to in the module", () => { + const customComponentOpts: CustomComponentOpts = { CustomComponent: React.Fragment }; + + // We emit a different lifecycle event than what our mock-module is listening to + module.emit(CustomComponentLifecycle.Experimental, customComponentOpts); + + render( + + + Child1 + Child2 + + , + ); + + // The document should not be affected at all + const defaultUserMenu = screen.getByTitle("UserMenu"); + expect(defaultUserMenu).toBeInTheDocument(); + expect(defaultUserMenu.children.length).toEqual(2) + + const customUserMenu = screen.queryByTitle("CustomUserMenu"); + expect(customUserMenu).toStrictEqual(null) + + const child1 = screen.getByText(/Child1/i); + const child2 = screen.getByText(/Child2/i); + expect(child1).toBeInTheDocument(); + expect(child2).toBeInTheDocument(); + + }); +}); From 7ea79229f6e9b71a0280d28d2a5026322624b096 Mon Sep 17 00:00:00 2001 From: John Tore Simonsen Date: Mon, 8 Apr 2024 15:26:19 +0200 Subject: [PATCH 05/14] update gitignore --- .gitignore | 219 +++++++++++++++++++++++++++-------------------------- 1 file changed, 110 insertions(+), 109 deletions(-) diff --git a/.gitignore b/.gitignore index 9b70dd4..c17e03a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,109 +1,110 @@ -# Custom -.idea/ -lib/ -.npmrc - -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -lerna-debug.log* - -# Diagnostic reports (https://nodejs.org/api/report.html) -report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json - -# Runtime data -pids -*.pid -*.seed -*.pid.lock - -# Directory for instrumented libs generated by jscoverage/JSCover -lib-cov - -# Coverage directory used by tools like istanbul -coverage -*.lcov - -# nyc test coverage -.nyc_output - -# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) -.grunt - -# Bower dependency directory (https://bower.io/) -bower_components - -# node-waf configuration -.lock-wscript - -# Compiled binary addons (https://nodejs.org/api/addons.html) -build/Release - -# Dependency directories -node_modules/ -jspm_packages/ - -# TypeScript v1 declaration files -typings/ - -# TypeScript cache -*.tsbuildinfo - -# Optional npm cache directory -.npm - -# Optional eslint cache -.eslintcache - -# Microbundle cache -.rpt2_cache/ -.rts2_cache_cjs/ -.rts2_cache_es/ -.rts2_cache_umd/ - -# Optional REPL history -.node_repl_history - -# Output of 'npm pack' -*.tgz - -# Yarn Integrity file -.yarn-integrity - -# dotenv environment variables file -.env -.env.test - -# parcel-bundler cache (https://parceljs.org/) -.cache - -# Next.js build output -.next - -# Nuxt.js build / generate output -.nuxt -dist - -# Gatsby files -.cache/ -# Comment in the public line in if your project uses Gatsby and *not* Next.js -# https://nextjs.org/blog/next-9-1#public-directory-support -# public - -# vuepress build output -.vuepress/dist - -# Serverless directories -.serverless/ - -# FuseBox cache -.fusebox/ - -# DynamoDB Local files -.dynamodb/ - -# TernJS port file -.tern-port +# Custom +.idea/ +lib/ +.npmrc +.github/ + +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# TypeScript v1 declaration files +typings/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env +.env.test + +# parcel-bundler cache (https://parceljs.org/) +.cache + +# Next.js build output +.next + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and *not* Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port From 0e45841426d88f13892888fde7a24a1825869067 Mon Sep 17 00:00:00 2001 From: John Tore Simonsen Date: Mon, 8 Apr 2024 15:28:02 +0200 Subject: [PATCH 06/14] update copyright --- src/lifecycles/CustomComponentLifecycle.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/lifecycles/CustomComponentLifecycle.ts b/src/lifecycles/CustomComponentLifecycle.ts index 5ffab8c..68b6b39 100644 --- a/src/lifecycles/CustomComponentLifecycle.ts +++ b/src/lifecycles/CustomComponentLifecycle.ts @@ -1,5 +1,7 @@ /* Copyright 2024 Verji Tech AS +Copyright 2023 Mikhail Aheichyk +Copyright 2023 Nordeck IT + Consulting GmbH. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. From 56fbb2023583a44b67c99358edb8e87c4284b383 Mon Sep 17 00:00:00 2001 From: John Tore Simonsen Date: Mon, 8 Apr 2024 16:01:16 +0200 Subject: [PATCH 07/14] Update tests, types.ts and update copyright --- src/lifecycles/CustomComponentLifecycle.ts | 4 +- src/lifecycles/types.ts | 44 ++-- .../CustomComponentLifecycle.test.tsx | 236 +++++++++--------- 3 files changed, 143 insertions(+), 141 deletions(-) diff --git a/src/lifecycles/CustomComponentLifecycle.ts b/src/lifecycles/CustomComponentLifecycle.ts index dd5fdc8..68b6b39 100644 --- a/src/lifecycles/CustomComponentLifecycle.ts +++ b/src/lifecycles/CustomComponentLifecycle.ts @@ -1,5 +1,7 @@ /* Copyright 2024 Verji Tech AS +Copyright 2023 Mikhail Aheichyk +Copyright 2023 Nordeck IT + Consulting GmbH. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -47,7 +49,7 @@ export enum CustomComponentLifecycle { RoomView = "room_view", SessionManagerTab = "session_manage_tab", SpacePanel = "space_panel", - UserMenu = "user_menu" + UserMenu = "user_menu", } /** diff --git a/src/lifecycles/types.ts b/src/lifecycles/types.ts index 51ece61..3c09ffa 100644 --- a/src/lifecycles/types.ts +++ b/src/lifecycles/types.ts @@ -1,22 +1,22 @@ -/* -Copyright 2022 The Matrix.org Foundation C.I.C. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -import { RoomViewLifecycle } from "./RoomViewLifecycle"; -import { WidgetLifecycle } from "./WidgetLifecycle"; -import { WrapperLifecycle } from "./WrapperLifecycle"; -import { CustomComponentLifecycle } from "./CustomComponentLifecycle"; - -export type AnyLifecycle = RoomViewLifecycle | WidgetLifecycle | WrapperLifecycle | CustomComponentLifecycle; +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { RoomViewLifecycle } from "./RoomViewLifecycle"; +import { WidgetLifecycle } from "./WidgetLifecycle"; +import { WrapperLifecycle } from "./WrapperLifecycle"; +import { CustomComponentLifecycle } from "./CustomComponentLifecycle"; + +export type AnyLifecycle = RoomViewLifecycle | WidgetLifecycle | WrapperLifecycle | CustomComponentLifecycle; diff --git a/test/lifecycles/CustomComponentLifecycle.test.tsx b/test/lifecycles/CustomComponentLifecycle.test.tsx index aec950e..784bb8f 100644 --- a/test/lifecycles/CustomComponentLifecycle.test.tsx +++ b/test/lifecycles/CustomComponentLifecycle.test.tsx @@ -1,118 +1,118 @@ -/* -Copyright 2024 Verji Tech AS - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -import React from "react"; -import { render, screen } from "@testing-library/react"; - -import { RuntimeModule } from "../../src/RuntimeModule"; -import { CustomComponentLifecycle, CustomComponentListener, CustomComponentOpts } from "../../src/lifecycles/CustomComponentLifecycle"; - -//Mock a CustomUserMenu -class CustomUserMenu extends React.Component{ - render() { - return
{this.props.children}
- } -} -//Mock a default UserMenu -class UserMenu extends React.Component { - render(){ - return
{this.props.children}
- } -} -describe("CustomComponentLifecycle", () => { - let module: RuntimeModule; - - beforeAll(() => { - - module = new (class extends RuntimeModule { - constructor() { - super(undefined as any); - - this.on(CustomComponentLifecycle.UserMenu, this.customComponentListener); - } - - protected customComponentListener: CustomComponentListener = (customComponentOpts: CustomComponentOpts) => { - customComponentOpts.CustomComponent = ({ children }) => { - let usermenu: any = React.Children.toArray(children)[0] - return ( - <> - - {usermenu.props?.children} - - - ); - }; - }; - })(); - }); - - it("should swap the UserMenu with CustomUserMenu and keep it's children", () => { - const customComponentOpts: CustomComponentOpts = { CustomComponent: React.Fragment }; - module.emit(CustomComponentLifecycle.UserMenu, customComponentOpts); - - render( - - - Child1 - Child2 - - , - ); - - const customUserMenu = screen.getByTitle("CustomUserMenu"); - expect(customUserMenu).toBeInTheDocument(); - expect(customUserMenu.children.length).toEqual(2) - - const defaultUserMenu = screen.queryByTitle("UserMenu"); - expect(defaultUserMenu).toStrictEqual(null) - - const child1 = screen.getByText(/Child1/i); - const child2 = screen.getByText(/Child2/i); - expect(child1).toBeInTheDocument(); - expect(child2).toBeInTheDocument(); - - }); - - it("should NOT swap the UserMenu when module emits an event we are not listening to in the module", () => { - const customComponentOpts: CustomComponentOpts = { CustomComponent: React.Fragment }; - - // We emit a different lifecycle event than what our mock-module is listening to - module.emit(CustomComponentLifecycle.Experimental, customComponentOpts); - - render( - - - Child1 - Child2 - - , - ); - - // The document should not be affected at all - const defaultUserMenu = screen.getByTitle("UserMenu"); - expect(defaultUserMenu).toBeInTheDocument(); - expect(defaultUserMenu.children.length).toEqual(2) - - const customUserMenu = screen.queryByTitle("CustomUserMenu"); - expect(customUserMenu).toStrictEqual(null) - - const child1 = screen.getByText(/Child1/i); - const child2 = screen.getByText(/Child2/i); - expect(child1).toBeInTheDocument(); - expect(child2).toBeInTheDocument(); - - }); -}); +/* +Copyright 2024 Verji Tech AS + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from "react"; +import { render, screen } from "@testing-library/react"; + +import { RuntimeModule } from "../../src/RuntimeModule"; +import { CustomComponentLifecycle, CustomComponentListener, CustomComponentOpts } from "../../src/lifecycles/CustomComponentLifecycle"; + +//Mock a CustomUserMenu +class CustomUserMenu extends React.Component{ + render() { + return
{this.props.children}
+ } +} +//Mock a default UserMenu +class UserMenu extends React.Component { + render(){ + return
{this.props.children}
+ } +} +describe("CustomComponentLifecycle", () => { + let module: RuntimeModule; + + beforeAll(() => { + + module = new (class extends RuntimeModule { + constructor() { + super(undefined as any); + + this.on(CustomComponentLifecycle.UserMenu, this.customComponentListener); + } + + protected customComponentListener: CustomComponentListener = (customComponentOpts: CustomComponentOpts) => { + customComponentOpts.CustomComponent = ({ children }) => { + let usermenu: any = React.Children.toArray(children)[0] + return ( + <> + + {usermenu.props?.children} + + + ); + }; + }; + })(); + }); + + it("should swap the UserMenu with CustomUserMenu and keep it's children", () => { + const customComponentOpts: CustomComponentOpts = { CustomComponent: React.Fragment }; + module.emit(CustomComponentLifecycle.UserMenu, customComponentOpts); + + render( + + + Child1 + Child2 + + , + ); + + const customUserMenu = screen.getByTitle("CustomUserMenu"); + expect(customUserMenu).toBeInTheDocument(); + expect(customUserMenu.children.length).toEqual(2) + + const defaultUserMenu = screen.queryByTitle("UserMenu"); + expect(defaultUserMenu).toStrictEqual(null) + + const child1 = screen.getByText(/Child1/i); + const child2 = screen.getByText(/Child2/i); + expect(child1).toBeInTheDocument(); + expect(child2).toBeInTheDocument(); + + }); + + it("should NOT swap the UserMenu when module emits an event we are not listening to in the module", () => { + const customComponentOpts: CustomComponentOpts = { CustomComponent: React.Fragment }; + + // We emit a different lifecycle event than what our mock-module is listening to + module.emit(CustomComponentLifecycle.Experimental, customComponentOpts); + + render( + + + Child1 + Child2 + + , + ); + + // The document should not be affected at all + const defaultUserMenu = screen.getByTitle("UserMenu"); + expect(defaultUserMenu).toBeInTheDocument(); + expect(defaultUserMenu.children.length).toEqual(2) + + const customUserMenu = screen.queryByTitle("CustomUserMenu"); + expect(customUserMenu).toStrictEqual(null) + + const child1 = screen.getByText(/Child1/i); + const child2 = screen.getByText(/Child2/i); + expect(child1).toBeInTheDocument(); + expect(child2).toBeInTheDocument(); + + }); +}); From 88a7c1a0c816926cdee4fdacac1fce727d04db1c Mon Sep 17 00:00:00 2001 From: John Tore Simonsen Date: Mon, 8 Apr 2024 16:05:52 +0200 Subject: [PATCH 08/14] remove verji-release.yaml --- .github/workflows/verji-release.yaml | 57 ---------------------------- 1 file changed, 57 deletions(-) delete mode 100644 .github/workflows/verji-release.yaml diff --git a/.github/workflows/verji-release.yaml b/.github/workflows/verji-release.yaml deleted file mode 100644 index 0d81478..0000000 --- a/.github/workflows/verji-release.yaml +++ /dev/null @@ -1,57 +0,0 @@ -name: Verji Build package -on: - workflow_dispatch: - inputs: - version-bump: - description: The scale of the version bump required for semver compatibility - required: true - default: patch - type: choice - options: - - patch - - minor - - major -concurrency: release -jobs: - release: - name: "Release & Publish" - runs-on: ubuntu-latest - steps: - - name: 🧮 Checkout code - uses: actions/checkout@v3 - with: - token: ${{ secrets.ELEMENT_BOT_TOKEN }} - - - name: 🔧 Set up node environment - uses: actions/setup-node@v3 - with: - cache: "yarn" - - - name: 🛠️ Setup - run: yarn install --pure-lockfile - - - name: 👊 Bump version - run: | - yarn version --no-git-tag-version --${{ github.event.inputs.version-bump }} - git config --global user.name 'ElementRobot' - git config --global user.email 'releases@riot.im' - git commit -am "${{ github.event.inputs.version-bump }} version bump" - git push - - - name: 🚀 Publish to npm - id: npm-publish - uses: JS-DevTools/npm-publish@v1 - with: - token: ${{ secrets.NPM_TOKEN }} - access: public - - - name: 🧬 Create release - uses: actions/create-release@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - tag_name: v${{ steps.npm-publish.outputs.version }} - release_name: Release ${{ steps.npm-publish.outputs.version }} - body: ${{ steps.npm-publish.outputs.version }} Release - draft: false - prerelease: false From 59ce15570940bf3c262554da965e308a69b23234 Mon Sep 17 00:00:00 2001 From: John Tore Simonsen Date: Mon, 8 Apr 2024 16:11:40 +0200 Subject: [PATCH 09/14] Revert "update gitignore" This reverts commit 7ea79229f6e9b71a0280d28d2a5026322624b096. From b1e775dfead5ec15e900c6043f74e96a837d6dc1 Mon Sep 17 00:00:00 2001 From: John Tore Simonsen Date: Mon, 8 Apr 2024 16:19:48 +0200 Subject: [PATCH 10/14] Revert "update gitignore" This reverts commit 7ea79229f6e9b71a0280d28d2a5026322624b096. --- .gitignore | 219 ++++++++++++++++++++++++++--------------------------- 1 file changed, 109 insertions(+), 110 deletions(-) diff --git a/.gitignore b/.gitignore index c17e03a..9b70dd4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,110 +1,109 @@ -# Custom -.idea/ -lib/ -.npmrc -.github/ - -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -lerna-debug.log* - -# Diagnostic reports (https://nodejs.org/api/report.html) -report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json - -# Runtime data -pids -*.pid -*.seed -*.pid.lock - -# Directory for instrumented libs generated by jscoverage/JSCover -lib-cov - -# Coverage directory used by tools like istanbul -coverage -*.lcov - -# nyc test coverage -.nyc_output - -# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) -.grunt - -# Bower dependency directory (https://bower.io/) -bower_components - -# node-waf configuration -.lock-wscript - -# Compiled binary addons (https://nodejs.org/api/addons.html) -build/Release - -# Dependency directories -node_modules/ -jspm_packages/ - -# TypeScript v1 declaration files -typings/ - -# TypeScript cache -*.tsbuildinfo - -# Optional npm cache directory -.npm - -# Optional eslint cache -.eslintcache - -# Microbundle cache -.rpt2_cache/ -.rts2_cache_cjs/ -.rts2_cache_es/ -.rts2_cache_umd/ - -# Optional REPL history -.node_repl_history - -# Output of 'npm pack' -*.tgz - -# Yarn Integrity file -.yarn-integrity - -# dotenv environment variables file -.env -.env.test - -# parcel-bundler cache (https://parceljs.org/) -.cache - -# Next.js build output -.next - -# Nuxt.js build / generate output -.nuxt -dist - -# Gatsby files -.cache/ -# Comment in the public line in if your project uses Gatsby and *not* Next.js -# https://nextjs.org/blog/next-9-1#public-directory-support -# public - -# vuepress build output -.vuepress/dist - -# Serverless directories -.serverless/ - -# FuseBox cache -.fusebox/ - -# DynamoDB Local files -.dynamodb/ - -# TernJS port file -.tern-port +# Custom +.idea/ +lib/ +.npmrc + +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# TypeScript v1 declaration files +typings/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env +.env.test + +# parcel-bundler cache (https://parceljs.org/) +.cache + +# Next.js build output +.next + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and *not* Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port From ebce9e6c45db19a3c6ae6999a9126c628464e65c Mon Sep 17 00:00:00 2001 From: John Tore Simonsen Date: Mon, 8 Apr 2024 16:24:21 +0200 Subject: [PATCH 11/14] update comment --- src/lifecycles/CustomComponentLifecycle.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/lifecycles/CustomComponentLifecycle.ts b/src/lifecycles/CustomComponentLifecycle.ts index 68b6b39..f33f479 100644 --- a/src/lifecycles/CustomComponentLifecycle.ts +++ b/src/lifecycles/CustomComponentLifecycle.ts @@ -17,8 +17,8 @@ limitations under the License. */ /* - CustomComponentLifecycle is heavily inspired by the WrapperLifecycle.ts, but is intended for a different usecase. - Instead of appending something within the wrapper, this lifecycle should swap the contents of the wrapper with a custom component provided by a module implementation. +CustomComponentLifecycle is heavily inspired by the WrapperLifecycle.ts, but is intended for a different usecase. +Instead of appending something within the wrapper, this lifecycle should swap the contents of the wrapper with a custom component provided by a module implementation. */ import { ComponentType, PropsWithChildren } from "react"; @@ -29,7 +29,7 @@ import { ComponentType, PropsWithChildren } from "react"; export enum CustomComponentLifecycle { /** * An event to request the component module. `ModuleRunner` should invoke an event matching the wrapped component. - * so any custom module can get the component provided by the module if any. + * So that any custom module can get the correct component based on Lifecycle event. */ AppsDrawer = "apps_drawer", EntityTile = "entity_tile", @@ -63,6 +63,6 @@ export type CustomComponentOpts = { }; /** - * Helper type that documents how to implement a UserMenu listener. + * Helper type that documents how to implement a CustomComponent listener. */ export type CustomComponentListener = (opts: CustomComponentOpts) => void; From bfc5be70bd81baaef4f44458e489435d636f6069 Mon Sep 17 00:00:00 2001 From: John Tore Simonsen Date: Tue, 16 Apr 2024 14:18:51 +0200 Subject: [PATCH 12/14] yarn lint:js-fix --- src/lifecycles/types.ts | 44 ++-- .../CustomComponentLifecycle.test.tsx | 243 +++++++++--------- 2 files changed, 147 insertions(+), 140 deletions(-) diff --git a/src/lifecycles/types.ts b/src/lifecycles/types.ts index 3c09ffa..51ece61 100644 --- a/src/lifecycles/types.ts +++ b/src/lifecycles/types.ts @@ -1,22 +1,22 @@ -/* -Copyright 2022 The Matrix.org Foundation C.I.C. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -import { RoomViewLifecycle } from "./RoomViewLifecycle"; -import { WidgetLifecycle } from "./WidgetLifecycle"; -import { WrapperLifecycle } from "./WrapperLifecycle"; -import { CustomComponentLifecycle } from "./CustomComponentLifecycle"; - -export type AnyLifecycle = RoomViewLifecycle | WidgetLifecycle | WrapperLifecycle | CustomComponentLifecycle; +/* +Copyright 2022 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { RoomViewLifecycle } from "./RoomViewLifecycle"; +import { WidgetLifecycle } from "./WidgetLifecycle"; +import { WrapperLifecycle } from "./WrapperLifecycle"; +import { CustomComponentLifecycle } from "./CustomComponentLifecycle"; + +export type AnyLifecycle = RoomViewLifecycle | WidgetLifecycle | WrapperLifecycle | CustomComponentLifecycle; diff --git a/test/lifecycles/CustomComponentLifecycle.test.tsx b/test/lifecycles/CustomComponentLifecycle.test.tsx index 784bb8f..70b153d 100644 --- a/test/lifecycles/CustomComponentLifecycle.test.tsx +++ b/test/lifecycles/CustomComponentLifecycle.test.tsx @@ -1,118 +1,125 @@ -/* -Copyright 2024 Verji Tech AS - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -import React from "react"; -import { render, screen } from "@testing-library/react"; - -import { RuntimeModule } from "../../src/RuntimeModule"; -import { CustomComponentLifecycle, CustomComponentListener, CustomComponentOpts } from "../../src/lifecycles/CustomComponentLifecycle"; - -//Mock a CustomUserMenu -class CustomUserMenu extends React.Component{ - render() { - return
{this.props.children}
- } -} -//Mock a default UserMenu -class UserMenu extends React.Component { - render(){ - return
{this.props.children}
- } -} -describe("CustomComponentLifecycle", () => { - let module: RuntimeModule; - - beforeAll(() => { - - module = new (class extends RuntimeModule { - constructor() { - super(undefined as any); - - this.on(CustomComponentLifecycle.UserMenu, this.customComponentListener); - } - - protected customComponentListener: CustomComponentListener = (customComponentOpts: CustomComponentOpts) => { - customComponentOpts.CustomComponent = ({ children }) => { - let usermenu: any = React.Children.toArray(children)[0] - return ( - <> - - {usermenu.props?.children} - - - ); - }; - }; - })(); - }); - - it("should swap the UserMenu with CustomUserMenu and keep it's children", () => { - const customComponentOpts: CustomComponentOpts = { CustomComponent: React.Fragment }; - module.emit(CustomComponentLifecycle.UserMenu, customComponentOpts); - - render( - - - Child1 - Child2 - - , - ); - - const customUserMenu = screen.getByTitle("CustomUserMenu"); - expect(customUserMenu).toBeInTheDocument(); - expect(customUserMenu.children.length).toEqual(2) - - const defaultUserMenu = screen.queryByTitle("UserMenu"); - expect(defaultUserMenu).toStrictEqual(null) - - const child1 = screen.getByText(/Child1/i); - const child2 = screen.getByText(/Child2/i); - expect(child1).toBeInTheDocument(); - expect(child2).toBeInTheDocument(); - - }); - - it("should NOT swap the UserMenu when module emits an event we are not listening to in the module", () => { - const customComponentOpts: CustomComponentOpts = { CustomComponent: React.Fragment }; - - // We emit a different lifecycle event than what our mock-module is listening to - module.emit(CustomComponentLifecycle.Experimental, customComponentOpts); - - render( - - - Child1 - Child2 - - , - ); - - // The document should not be affected at all - const defaultUserMenu = screen.getByTitle("UserMenu"); - expect(defaultUserMenu).toBeInTheDocument(); - expect(defaultUserMenu.children.length).toEqual(2) - - const customUserMenu = screen.queryByTitle("CustomUserMenu"); - expect(customUserMenu).toStrictEqual(null) - - const child1 = screen.getByText(/Child1/i); - const child2 = screen.getByText(/Child2/i); - expect(child1).toBeInTheDocument(); - expect(child2).toBeInTheDocument(); - - }); -}); +/* +Copyright 2024 Verji Tech AS + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from "react"; +import { render, screen } from "@testing-library/react"; + +import { RuntimeModule } from "../../src/RuntimeModule"; +import { + CustomComponentLifecycle, + CustomComponentListener, + CustomComponentOpts, +} from "../../src/lifecycles/CustomComponentLifecycle"; + +//Mock a CustomUserMenu +class CustomUserMenu extends React.Component { + render() { + return ( +
+ {this.props.children} +
+ ); + } +} +//Mock a default UserMenu +class UserMenu extends React.Component { + render() { + return ( +
+ {this.props.children} +
+ ); + } +} +describe("CustomComponentLifecycle", () => { + let module: RuntimeModule; + + beforeAll(() => { + module = new (class extends RuntimeModule { + constructor() { + super(undefined as any); + + this.on(CustomComponentLifecycle.UserMenu, this.customComponentListener); + } + + protected customComponentListener: CustomComponentListener = (customComponentOpts: CustomComponentOpts) => { + customComponentOpts.CustomComponent = ({ children }) => { + let usermenu: any = React.Children.toArray(children)[0]; + return ( + <> + {usermenu.props?.children} + + ); + }; + }; + })(); + }); + + it("should swap the UserMenu with CustomUserMenu and keep it's children", () => { + const customComponentOpts: CustomComponentOpts = { CustomComponent: React.Fragment }; + module.emit(CustomComponentLifecycle.UserMenu, customComponentOpts); + + render( + + + Child1 + Child2 + + , + ); + + const customUserMenu = screen.getByTitle("CustomUserMenu"); + expect(customUserMenu).toBeInTheDocument(); + expect(customUserMenu.children.length).toEqual(2); + + const defaultUserMenu = screen.queryByTitle("UserMenu"); + expect(defaultUserMenu).toStrictEqual(null); + + const child1 = screen.getByText(/Child1/i); + const child2 = screen.getByText(/Child2/i); + expect(child1).toBeInTheDocument(); + expect(child2).toBeInTheDocument(); + }); + + it("should NOT swap the UserMenu when module emits an event we are not listening to in the module", () => { + const customComponentOpts: CustomComponentOpts = { CustomComponent: React.Fragment }; + + // We emit a different lifecycle event than what our mock-module is listening to + module.emit(CustomComponentLifecycle.Experimental, customComponentOpts); + + render( + + + Child1 + Child2 + + , + ); + + // The document should not be affected at all + const defaultUserMenu = screen.getByTitle("UserMenu"); + expect(defaultUserMenu).toBeInTheDocument(); + expect(defaultUserMenu.children.length).toEqual(2); + + const customUserMenu = screen.queryByTitle("CustomUserMenu"); + expect(customUserMenu).toStrictEqual(null); + + const child1 = screen.getByText(/Child1/i); + const child2 = screen.getByText(/Child2/i); + expect(child1).toBeInTheDocument(); + expect(child2).toBeInTheDocument(); + }); +}); From a5e3ad4552f01946f3330be66d7f564b85b54567 Mon Sep 17 00:00:00 2001 From: John Tore Simonsen Date: Tue, 16 Apr 2024 14:26:36 +0200 Subject: [PATCH 13/14] fix linting readme --- README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 6f75807..89488e0 100644 --- a/README.md +++ b/README.md @@ -104,13 +104,14 @@ From the `RuntimeModule` instance, modules can listen for `WrapperLifecycle.Wrap It would wrap the `MatrixChat` component and let any consumer add a header, a footer. ### Custom components + From the `RuntimeModule` instance, modules can listen for different `CustomComponentLifecycle` events and swap the component -with a custom written component. In principle it works the same way as the `WrapperLifecycle`, but the usecase is different. +with a custom written component. In principle it works the same way as the `WrapperLifecycle`, but the usecase is different. Instead of wrapping the element, you can intercept it, consume the state of the component including its children, and return -your own customly written component. +your own customly written component. It is possible to add `matrix-react-sdk`, `matrix-js-sdk` as a dependency into your custom module implementation to gain complete -functionality within your custom components, aswell as the ability to reuse sub-components and styles. +functionality within your custom components, aswell as the ability to reuse sub-components and styles. ## Contributing / developing From 4074daa5771cc160733772c754fafbd296d0fc58 Mon Sep 17 00:00:00 2001 From: John Tore Simonsen Date: Fri, 26 Apr 2024 17:02:24 +0200 Subject: [PATCH 14/14] Add new tests for CustomComponentLifecycle to include FC components. --- .../CustomComponentLifecycle.test.tsx | 280 +++++++++++++----- 1 file changed, 204 insertions(+), 76 deletions(-) diff --git a/test/lifecycles/CustomComponentLifecycle.test.tsx b/test/lifecycles/CustomComponentLifecycle.test.tsx index 70b153d..49866a3 100644 --- a/test/lifecycles/CustomComponentLifecycle.test.tsx +++ b/test/lifecycles/CustomComponentLifecycle.test.tsx @@ -14,15 +14,15 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from "react"; +import React, { ReactPortal } from "react"; import { render, screen } from "@testing-library/react"; - import { RuntimeModule } from "../../src/RuntimeModule"; import { CustomComponentLifecycle, CustomComponentListener, CustomComponentOpts, } from "../../src/lifecycles/CustomComponentLifecycle"; +import { ModuleApi } from "../../src/ModuleApi"; //Mock a CustomUserMenu class CustomUserMenu extends React.Component { @@ -44,82 +44,210 @@ class UserMenu extends React.Component { ); } } -describe("CustomComponentLifecycle", () => { - let module: RuntimeModule; - - beforeAll(() => { - module = new (class extends RuntimeModule { - constructor() { - super(undefined as any); - - this.on(CustomComponentLifecycle.UserMenu, this.customComponentListener); - } - - protected customComponentListener: CustomComponentListener = (customComponentOpts: CustomComponentOpts) => { - customComponentOpts.CustomComponent = ({ children }) => { - let usermenu: any = React.Children.toArray(children)[0]; - return ( - <> - {usermenu.props?.children} - - ); - }; - }; - })(); - }); - - it("should swap the UserMenu with CustomUserMenu and keep it's children", () => { - const customComponentOpts: CustomComponentOpts = { CustomComponent: React.Fragment }; - module.emit(CustomComponentLifecycle.UserMenu, customComponentOpts); - - render( - - - Child1 - Child2 - - , - ); - - const customUserMenu = screen.getByTitle("CustomUserMenu"); - expect(customUserMenu).toBeInTheDocument(); - expect(customUserMenu.children.length).toEqual(2); +interface MyComponentProps { + children: React.ReactNode; +} - const defaultUserMenu = screen.queryByTitle("UserMenu"); - expect(defaultUserMenu).toStrictEqual(null); +// Mock a functional custom Component +const CustomFunctionalComponent: React.FC = ({ children }) => { + return
{children}
; +}; +// Mock a functional Component +const FunctionalComponent: React.FC = ({ children }) => { + return
{children}
; +}; - const child1 = screen.getByText(/Child1/i); - const child2 = screen.getByText(/Child2/i); - expect(child1).toBeInTheDocument(); - expect(child2).toBeInTheDocument(); +describe("CustomComponentLifecycle", () => { + describe("tests on Class components", () => { + let module: RuntimeModule; + let moduleApi: ModuleApi; + beforeAll(() => { + module = new (class extends RuntimeModule { + constructor() { + super(moduleApi); + + this.on(CustomComponentLifecycle.UserMenu, this.customComponentListener); + } + + protected customComponentListener: CustomComponentListener = ( + customComponentOpts: CustomComponentOpts, + ) => { + customComponentOpts.CustomComponent = ({ children }) => { + const usermenu = React.Children.toArray(children)[0] as UserMenu; + const usermenuChildren = usermenu?.props?.children ?? React.Fragment; + + return ( + <> + {usermenuChildren} + + ); + }; + }; + })(); + }); + + it("should swap the UserMenu with CustomUserMenu and keep it's children", () => { + const customComponentOpts: CustomComponentOpts = { CustomComponent: React.Fragment }; + module.emit(CustomComponentLifecycle.UserMenu, customComponentOpts); + + render( + + + Child1 + Child2 + + , + ); + + const customUserMenu = screen.getByTitle("CustomUserMenu"); + expect(customUserMenu).toBeInTheDocument(); + expect(customUserMenu.children.length).toEqual(2); + + const defaultUserMenu = screen.queryByTitle("UserMenu"); + expect(defaultUserMenu).toStrictEqual(null); + + const child1 = screen.getByText(/Child1/i); + const child2 = screen.getByText(/Child2/i); + expect(child1).toBeInTheDocument(); + expect(child2).toBeInTheDocument(); + }); + + it("should NOT swap the UserMenu when module emits an event we are not listening to in the module", () => { + const customComponentOpts: CustomComponentOpts = { CustomComponent: React.Fragment }; + + // We emit a different lifecycle event than what our mock-module is listening to + module.emit(CustomComponentLifecycle.Experimental, customComponentOpts); + + render( + + + Child1 + Child2 + + , + ); + + // The document should not be affected at all + const defaultUserMenu = screen.getByTitle("UserMenu"); + expect(defaultUserMenu).toBeInTheDocument(); + expect(defaultUserMenu.children.length).toEqual(2); + + const customUserMenu = screen.queryByTitle("CustomUserMenu"); + expect(customUserMenu).toStrictEqual(null); + + const child1 = screen.getByText(/Child1/i); + const child2 = screen.getByText(/Child2/i); + expect(child1).toBeInTheDocument(); + expect(child2).toBeInTheDocument(); + }); }); - - it("should NOT swap the UserMenu when module emits an event we are not listening to in the module", () => { - const customComponentOpts: CustomComponentOpts = { CustomComponent: React.Fragment }; - - // We emit a different lifecycle event than what our mock-module is listening to - module.emit(CustomComponentLifecycle.Experimental, customComponentOpts); - - render( - - - Child1 - Child2 - - , - ); - - // The document should not be affected at all - const defaultUserMenu = screen.getByTitle("UserMenu"); - expect(defaultUserMenu).toBeInTheDocument(); - expect(defaultUserMenu.children.length).toEqual(2); - - const customUserMenu = screen.queryByTitle("CustomUserMenu"); - expect(customUserMenu).toStrictEqual(null); - - const child1 = screen.getByText(/Child1/i); - const child2 = screen.getByText(/Child2/i); - expect(child1).toBeInTheDocument(); - expect(child2).toBeInTheDocument(); + describe("tests on FC components", () => { + let module: RuntimeModule; + let moduleApi: ModuleApi; + beforeAll(() => { + module = new (class extends RuntimeModule { + constructor() { + super(moduleApi); + // In this case we are reacting to the "Experimental" lifecyle, because we are testing a generic mocked functional component. + this.on(CustomComponentLifecycle.Experimental, this.customComponentListener); + } + + protected customComponentListener: CustomComponentListener = ( + customComponentOpts: CustomComponentOpts, + ) => { + customComponentOpts.CustomComponent = ({ children }) => { + // We extract the component wrapped in CustomComponentOpts.CustomComponent + const defaultFunctionalComponent: ReactPortal = React.Children.toArray( + children, + )[0] as ReactPortal; + return ( + <> + + {defaultFunctionalComponent.props.children} + + + ); + }; + }; + })(); + }); + + it("should swap the the wrapped FunctionalComponent with the CustomFunctionalComponent and keep it's children", () => { + const customComponentOpts: CustomComponentOpts = { CustomComponent: React.Fragment }; + module.emit(CustomComponentLifecycle.Experimental, customComponentOpts); + + render( + + + Child1 + Child2 + + , + ); + + const CustomFunctionalComponent = screen.getByTitle("CustomFunctionalComponent"); + + expect(CustomFunctionalComponent).toBeInTheDocument(); + expect(CustomFunctionalComponent.children.length).toEqual(2); + + const defaultFunctionalCompoonent = screen.queryByTitle("FunctionalComponent"); + expect(defaultFunctionalCompoonent).toStrictEqual(null); + + const child1 = screen.getByText(/Child1/i); + const child2 = screen.getByText(/Child2/i); + expect(child1).toBeInTheDocument(); + expect(child2).toBeInTheDocument(); + }); + it("should swap the the wrapped FunctionalComponent(alternative rendering style) with the CustomFunctionalComponent", () => { + const customComponentOpts: CustomComponentOpts = { CustomComponent: React.Fragment }; + module.emit(CustomComponentLifecycle.Experimental, customComponentOpts); + + const firstChild = Child1; + const secondChild = Child2; + + render( + + {FunctionalComponent({ children: [firstChild, secondChild] })} + , + ); + const CustomFunctionalComponent = screen.getByTitle("CustomFunctionalComponent"); + expect(CustomFunctionalComponent).toBeInTheDocument(); + + const defaultFunctionalCompoonent = screen.queryByTitle("FunctionalComponent"); + expect(defaultFunctionalCompoonent).toStrictEqual(null); + + const child1 = screen.getByText(/Child1/i); + const child2 = screen.getByText(/Child2/i); + expect(child1).toBeInTheDocument(); + expect(child2).toBeInTheDocument(); + }); + it("should NOT swap the FucntionalComponent when module emits an event we are not listening to in the module", () => { + const customComponentOpts: CustomComponentOpts = { CustomComponent: React.Fragment }; + + // We emit a different lifecycle event than what our mock-module is listening to + module.emit(CustomComponentLifecycle.ErrorBoundary, customComponentOpts); + + render( + + + Child1 + Child2 + + , + ); + + // The document should not be affected at all + const defaultFunctionalCompoonent = screen.getByTitle("FunctionalComponent"); + expect(defaultFunctionalCompoonent).toBeInTheDocument(); + expect(defaultFunctionalCompoonent.children.length).toEqual(2); + + const CustomFunctionalComponent = screen.queryByTitle("CustomFunctionalComponent"); + expect(CustomFunctionalComponent).toStrictEqual(null); + + const child1 = screen.getByText(/Child1/i); + const child2 = screen.getByText(/Child2/i); + expect(child1).toBeInTheDocument(); + expect(child2).toBeInTheDocument(); + }); }); });