Removed unneeded files from lib-content-components

This commit is contained in:
Braden MacDonald
2024-08-09 11:47:35 -07:00
parent b088a8fe3d
commit d3d5fe0e1b
81 changed files with 3 additions and 40606 deletions

View File

@@ -1,13 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<Placeholder /> renders correctly 1`] = `
<div
className="Placeholder"
>
<h1>
Under Construction
<br />
Coming Soon
</h1>
</div>
`;

View File

@@ -4,7 +4,7 @@ import TestRenderer from 'react-test-renderer';
import { AppContext } from '@edx/frontend-platform/react';
import { Context as ResponsiveContext } from 'react-responsive';
import Placeholder from './index';
import Placeholder from '../index';
describe('<Placeholder />', () => {
it('renders correctly', () => {

View File

@@ -0,0 +1,10 @@
This library is meant for high-level components related to "content" types, which sometimes correspond to xblocks --- so videos, html, problems, etc.
- This is a higher-level layer on top of paragon.
The components may be shared across multiple frontends.
- For example, components may be used by both the learning MFE, the course authoring MFE, and the shared content MFE.
- Video-playing components may be used for previewing in editing contexts, etc.
The components cover many types of content, not just a single one.
- For example, HTML editing frontend might share code and style with video editing or problem editing.
- This will also help keep style and accessibility consistent across different content types.

View File

@@ -0,0 +1,69 @@
# Status
Accepted
# Context
We seek to hoist the legacy text (HTML) text XBlock editor out of the monolith into a decoupled React application
without reducing editor capabilities in the process.
The legacy editor delegates the bulk of its operations to the [TinyMCE](https://www.tiny.cloud/docs/tinymce/6/) editor.
Per the link, TinyMCE is "a rich-text editor that allows users to create formatted content within
a user-friendly interface".
# Decision set (each decision small)
1. Rely on cross-editor XBlock network access support, per "Network Request Layer" ADR in this repo
2. Continue using the tinyMCE editor
3. Wrap the tinyMCE editor so as to be able to handle it as a vanilla React component
4. Use a mix of configuration and tinyMCE API calls to customize the editor as needed
* (e.g., offer image upload and image gallery selection)
# Consequences
## Rely on editors-wide XBlock API (network access) decision
No complexity associated with CRUD operations on XBlock content, irrespective of where the XBlocks are stored.
## Continue using the tinyMCE editor
Future customizations might not be possible if look-and-feel configurations offered by tinyMCE prove inadequate, or if the tinyMCE API falls short. For current requirements this was not a showstopper.
# Wrap the tinyMCE editor for React use
Wrapping of the tinyMCE editor to behave like a react component is done by the simple expedient of having `TextEditor.jsx` import the [tinymce/tinymce-react](https://github.com/tinymce/tinymce-react) repo and then interacting with the Editor component defined there.
Interaction with the tinyMCE editor is via a reference, saved at editor initialization.
When wrapping the tinyMCE component we assign a `ref` to the created component so that we can access its state and apis to update the content and draw it back out.
* initialize with html content from xblock
# Editor customization
Because the tinyMCE instance is the core of the TextEditor experience (pretty much the whole thing), and it is not a custom-configuratble UI,
the configuration hook provides much of our control of the UI.
However, because TinyMCE provides extensible controls, we have a few internal-written widgets we have embedded into the workflow,
specifically including the image upload/settings modal.
Much of the editor configuration is actually defined in `TextEditor/pluginConfig.js`, and the editorHook mostly draws from this configuration to populate the editor."
## Image upload/gallery selection
The tinyMCE editor is extended to offer an image upload/select button. When the end-user pushes this button, the editor's window is occluded by a modal dialog capable of uploading an image. The end-user can also interact with this modal window to toggle between uploading a new image, or selecting from among previously uploaded images (aka, the image gallery), to save, or to cancel out.
On its initialization, the tinyMCE editor is provided with an icon and a callback to associate with the image upload button.
* Tiny MCE needs a "configured" button to open the modal dialog to select/upload an image.
* This verbiage is somewhat important distinction, because this is a configuration, not a coding feature
Later, on invocation of the modal dialog, this window is initialized with a reference to the tinyMCE editor.
If the modal window is driven to a save operation on an uploaded or a selected image, the window uses this reference to provide an image tag and image metadata to the tinyMCE editor, for inclusion at the current cursor location.
The wrapping modal around upload/settings has a hook that calls the editor execCommand to insert the image tag on a button click.
The hook runs before everything renders and produces a button callback that will save a passed image to the (_sic_ tinyMCE) editor context
* [on image upload or gallery selection] insert image tag at cursor location with source, dimensions, and alt text
* [on image update] Update and replace a given image tag selection with a new one, updating source, dimensions, and/or alt-text
* Update and replace are utilizing exposed tinyMCE editor api accessed from the ref associated with the created component.
* Modal must have a "Save" option that inserts appropriately formatted tags into the tinyMCE editor context.
* Does not always update on relinquishing control, and communicates nothing on cancel

View File

@@ -0,0 +1,68 @@
V2 Content Editors
Synopsis
--------
We have created a framework for creating improved editor experiences for existing xblocks. We call these new editors V2 Content Editors.
V2 Content Editors replace existing xblock editing experiences using redirection.
The V2 Editor framework allows for the easy creation and configuration of new editors through automated boilerplate generation, simple networking and state abstractions, and premade components for basic editing views.
Decisions
------
I. All V2 content editors shall live in this repository. This choice was made as the existing xblock framework is not amenable to running modern React applications for studio views, and cannot be upgraded to do so without herculanean effort.
II. These editors will be served to the user as if an overlay on the existing editing experience for a particular xblock. This shall occur by this library's editor comoponent being served from a learning context MFE (Library or Course Authoring).
III. The Editor component is loaded into a learning context authoring tool (eg. Course or Library Authoring) from this JS library. This component then serves the correct editor type based on the xblock id it is provided.
IV. Editors for a specific xblock are then provided with the relevant data and metadata of that xblock instance, and their code is run to provide the experience.
V. The following process was implemented to inject this flow into Studio.
For entering an editor page: Users click on the "edit xblock" button, are redirected to the course authoring MFE, where they are presented with the relevant editor.
.. image:: https://user-images.githubusercontent.com/49422820/166940630-51dfc25e-c760-4118-b4dd-ae1fa7fa73b9.png
For saving content: Once inside the editor, clicking save saves the content to the xblock api and returns the user to the course authoring context.
.. image:: https://user-images.githubusercontent.com/49422820/166940624-068e8446-0c86-4c24-a2dd-3eb474984f08.png
For exiting without saving: The user is simply transported back to the course authoring context.
.. image:: https://user-images.githubusercontent.com/49422820/166940617-80455ade-0a5e-4e61-94b0-b9e2d7a0531e.png
VI. The library provides prebuilt components and features to accomplish common editor tasks.
- The EditorContainer component makes for easy saving, canceling changes, and xblock title editing.
- An app-level abstraction for network requests and handling thier states. This is the /Requests layer in the redux store. More information will be contained in ADR 0004 Network Request Layer
VII. There are several patterns and principles along which the V2 editors are built. Additional editors are not required to follow these, but it is strongly encouraged. Theses are:
- Following the Testing and Implementation ADR.
- Generalize components for reuse when possible.
- Use Redux for global state management.
VIII. How to create, configure, and enable a new editor experience will exist in other documentation, but should rely on automated configuration.
Status
------
Adopted
Context
-------
We need self-contained xblock editing and configuration experiences. Changing requirements require that that experience be modernized to use Paragon, work across authoring for different learning contexts (course authoring and library authoring), and be flexible, extensible and repeatable.
Carving experiences out of Studio is an architectural imperative. Editing, as xblocks are discrete pieces of content, can exist in a context independent of the learning context, so having a learning-context agnostic environment for editing makes sense.
Consequences
------------
This design has several consequences. These consequences are the result of the favoring of incremental changes, which can be iterated upon as other improvements in the openedx ecosystem occur.
The majority of the impactful consequences have to do with the architectural choice to NOT simply upgrade the capabilities of xblock rendering, and instead serve the new experiences from a separate library. The fallout of these design choices leads to architectural complexity, but also the ability to deliver value in new ways.
For example, locating the V2 editor in frontend-lib-content-components outside of the xblock code leaves no clear solution for easy open-source extension of V2 editors. This choice, however, also allows us to easily serve library and course contexts and leads to the easier creation of common content react components.
In addition, this also allows developers to add value to editors, without having to rewrite the course-authoring experience to leverage React. Indeed, even when course authoring moves into an MFE, it will be trivial to place the editor inside the editor.
This choice, however, is not intended to be final. Instead, this library can become merely a set of tools and common components, and once xblock editor views are Reactified, we can very easily restore the abstraction that all xblock code lives with the xblock. It is in this spirit of providing incremental value that we provided this choice.

View File

@@ -0,0 +1,51 @@
Network and Requests Layer
Synopsis
--------
For V2 Content Editors, we have defined a general abstraction for basic editor actions and content retrieval. This abstraction is twofold: a defined set of general “app” actions for basic editor actions, and a Requests Layer to track the status of ALL network requests.
This will be a powerful tool to speed up the creation of new editors.
Decision
------
The common actions required for any V2 content editor are as follows:
Retrieve an xblock
Save an xblock to the xblock api in the CMS.
Return to your learning context (Studio, Course Authoring, Library Authoring)
Obtain content (video, files, images) from the contentstore associated with a learning context.
We have implemented actions to perform those tasks in src/editors/data/redux/thunkActions/app.js. These actions are decoupled from the code of a specific editor, and are easily portable across editors.
We have also defined an atomic method to track the lifecycle of a network action. This abstraction applies to these common actions, as well as any actions defined in the data layer of a particular V2 editor.
The lifecycle of the acquisition of data from network and the updating of the global state with that data is termed to be a "request." The "states" of the lifecycle associated with a request are [inactive, pending, completed, failed]. This lifecycle provides information to the Redux consumer as to the status of their data.
Each unique request instance is given a key in `src/editors/data/constants/requests`. This key can be queried to ascertain the status of the request using a Redux selector by a consumer of the redux state. This allows for easy conditional rendering. By following this pattern, additional async actions will be easy to write.
The individual api methods are all defined in `data/services/cms/api`. The goal of the `requests` thunkActions is to first route the appropriate store data to the api request based on how they are being called.
The actual chain the an example request to save an xblock code is:
`thunkActions/app:saveBlock` -> `thunkActions/requests:saveBlock` `services/cms/api:saveBlock`
* The "app" thunk action updates the local block content, and then dispatches the request thunkAction
* The "request" thunkAction then loads relevant redux data for the save event and calls the api.saveBlock method, wrapped such that the UI can track the request state
* The "api" method provides the specifics for the actual network request, including prop format and url."
Status
------
Adopted
Context
-------
In building React Redux applications, asynchronous actions require a set of "Thunk" actions dispatched at relevant points. A common standard around the lifecycle helps prevent the boilerplate for these actions to spiral. In addition, it allows for the faster development of new V2 editors, as developers have easily usable Redux actions to dispatch, as well as Redux selectors to track the status of their requests.
Consequences
------------
Network-based CRUD actions have a common language of lifecycle, as well as a common pattern to implement, allowing developers to use ready-made requests without issue for common actions, like xblock saving, content store retrieval, and even outside api access. This improves ease of use, as well as readability and uniformity.

View File

@@ -0,0 +1,136 @@
# Internal editor testability decision
# Increased complexity for the sake of testability
The internally-managed editors in this repo (as of now planned to include text, video, and problem types) follow a number of patterns that increase the complexity of parts of the code slightly, in favor of providing increased testability around their behavior.
## Note - Strict Dictionaries
Javacript is generally fairly lackadaisical with regards to dictionary access of missing/invalid keys. This is fine and expected in many cases, but also prevents us from using dictionary access on something like a key store to ensure we are calling something that actually exists.
For this purpose, there are a pair of utilities in this repo called `StrictDict` and `keyStore`.
`StrictDict` takes an object and returns a version that will complain (throw an error) if called with an invalid key.
`keyStore` takes an object and returns a StrictDict of just the keys of that object. (this is useful particularly for mocking and spying on specific methods and fields)
## Note - Self imports
Javascript webpack imports can be problematic around the specific issue of attempting to mock a single method being used in another method in the same file.
Problem: File A includes methodA and methodB (which calls methodA). We want to be able to test methodA and then test methodB *without* having to re-test methodA as part of that test. We want to be able to mock methodA *only* while we are testing methodB.
Solution: Self-imports. By importing the module into itself (which webpack handles nicely, don't worry), we provide tests the ability to spy on and mock individual methods from that module separately on a per-test basis.
Ex:
```javascript
// myFile.js
import * as module from './myFile';
export const methodA = (val) => // do something complex with val and return a number
export const methodB = (val) => module.methodA(val) * 2;
// myFile.test.js
import * as module from './myFile';
import { keyStore } from './utils';
cosnt moduleKeys = keyStore(module);
describe('myFile', () => {
describe('methodA', () => ...);
describe('methodB', () => {
const mockMethodA = (val) => val + 3
const testValue = 23;
beforeEach(() => {
jest.spyOn(module, moduleKeys).mockImplementationValueOnce(mockMethodA);
});
it('returns double the output of methodA with the given value', () => {
expect(module.methodB(testValue)).toEqual(mockMethodA(testValue) + 3);
});
});
});
```
## Hooks and Snapshots - Separation from components for increased viability of snapshots
As part of the testing of these internal editors, we are relying on snapshot testing to ensure stability of the display of the components themselves. This can be a fragile solution in certain situations where components are too large or complex to adequately snapshot and verify.
For this purpose, we have opted for a general pattern of separating all of the behavior of components withing these editors into separate `hooks` files.
These hook files contain methods that utilize both `react` and `react-redux` hooks, along with arguments passed directly into the component, in order to generate the resulting desired behaviors.
From there, components are tested by mocking out the behavior of these hooks to return verifyable data in the snapshots.
As part of this separation, there are a number of additional patterns that are followed
### Snapshot considerations
#### Callbacks
Any callback that is included in render in a component should be separated such that is is either passed in as a prop or derived from a hook, and should be mocked with a `mockName` using jest, to ensure that they are uniquely identifyable in the snapshots.
Ex:
```javascript
const props = {
onClick: jest.fn().mockName('props.onClick');
}
expect(shallow(<MyElement {...props} />)).toMatchSnapshot();
```
#### Imported components
Imported compoents are mocked to return simple string components based on their existing name. This results in shallow renders that display the components by name, with passed props, but do not attempt to render *any* logic from those components.
This is a bit more complex for components with sub-components, but we have provided a test utility in `src/testUtils` called `mockNestedComponent` that will allow you to easily mock these for your snapshots as well.
Ex:
```javascript
jest.mock('componentModule', () => {
const { mockNestedComponent } = jest.requireActual('./testUtils');
return {
SimpleComponents: () => 'SimpleComponent',
NestedComponent: mockNestedComponent('NestedComponent', {
NestedChild1: 'NestedComponent.NestedChild1',
NestedChild2: 'NestedComponent.NestedChild2',
}),
});
```
#### Top-level mocked imports
We have mocked out all paragon components and icons being used in the repo, as well as a number of other common methods, hooks, and components in our module's `setupTests` file, which will ensure that those components show up reasonably in snapshots.
### Hook Considerations
#### useState and mockUseState
React's useState hook is a very powerful alternative to class components, but is also somewhat problematic to test, as it returns different values based on where it is called in a hook, as well as based on previous runs of that hook.
To resolve this, we are using a custom test utility to mock a hook modules state values for easy testing.
This requires a particular structure to hook modules that use the useState, for the sake of enabling the mock process (which is documented with the utility).
Ex:
```javascript
import * as module from './hooks';
const state = {
myStateValue: (val) => useState(val),
};
const myHook = () => {
const [myStateValue, setMyStateValue] = module.state.myStateValue('initialValue');
};
```
Examples on how to use this for testing are included with the mock class in `src/testUtils`
#### useCallback, useEffect
These hooks provide behavior that calls a method based on given prerequisite behaviors.
For this reason, we use general-purpose mocks for these hooks that return an object containing the passed callback and prerequisites for easy test access.
#### Additional Considrations
*useIntl not available*
We are using react-intl under the hood for our i18n support, but do not have access to some of the more recent features in that library due to the pinned version in frontend-platform. Specifically, this includes a `useIntl` hook available in later versions that is still unavailable to us, requiring us to use the older `injectIntl` pattern.
*useDispatch*
React-redux's `useDispatch` hook does not play nicely with being called in a method called by a component, and really wants to be called *in* the component. For this reason, the dispatch method is generated in components and passed through to hook components.
## Notes for integration testing
Because of the top-level mocks in setupTest, any integration tests will need to be sure to unmock most of these.
Ex:
```javascript
jest.unmock('@edx/frontend-platform/i18n');
jest.unmock('@openedx/paragon');
jest.unmock('@openedx/paragon/icons');
jest.unmock('react-redux');
```

View File

@@ -0,0 +1,57 @@
Test Names
Synopsis
--------
Tests with descriptive names are a great way to document what a function does and what can go wrong.
On the other hand, unclear test names can pose a risk, because if a test is faulty, you cannot see from the test name
what the test intends, so it is difficult to identify faulty tests.
We are setting up some conventions for naming of tests.
Decisions
---------
1. The name of your test should consist of three parts:
- The name of the unit and/or method being tested.
- The scenario / context under which it's being tested.
- The expected behavior when the scenario is invoked.
2. Use nested `describe` blocks to describe, unit, method, and scenario under test.
3. Use a `test` statement for the expected behavior.
4. A good test statement tests a single behavior in a single scenario for a single method.
5. Avoid the word "test" in your test name.
6. A test name describes an expectation. Use either an expectational statement using the "should" keyword or a factual statement. Do not use patterns like imperatives.
- Good: "function add() should calculate the sum of two numbers".
- Good: "function add() calculcates the sum of two numbers".
- Bad: "test function add() for two numbers". (Imperative voice)
7. Aim to write test names as full meaningful sentences. A test name consists of a few pieces: some describe statements and a test statements.
When running tests, they will be concatenated to the test name. An example: ::
// name of the unit under test
describe('calculator', () => {
...
// name of the method under test
describe('add() function', () => {
...
// The scenario / context under which it's being tested
describe('with invalid settings', () => {
...
// The expected behavior when the scenario is invoked
it('should throw a descriptive error', () => {
...
}
}
}
}
This results in the full meaningful sentence: "calculator add() function with invalid settings should throw a descriptive error".
Further reading:
----------------
https://learn.microsoft.com/en-us/dotnet/core/testing/unit-testing-best-practices

View File

@@ -0,0 +1,212 @@
7. Feature-based Application Organization
-----------------------------------------
Status
------
Accepted
Context
-------
The common, naive approach to organizing React/Redux applications says that code should be grouped into folders by type, putting like-types of code together. This means having directories like:
- components
- actions
- reducers
- constants
- selectors
- sagas
- services
This is often referred to as a "Ruby on Rails" approach, which organizes applications similarly.
As applications grow, it's acknowledged by the community that this organization starts to fall down and become difficult to maintain. It does nothing to help engineers keep their code modular and decoupled, as it groups code by how it looks and not how it's used. Code that functions as part of a unit is spread out over 7+ directories.
This ADR documents an approach and rules of thumb for organizing code modularly by feature, informed by articles and prior art.
Note on terminology: "feature" and "module" are used interchangeably in this ADR. In general, the feature refers to the semantically significant thing, whereas the module refers to the directory of code pertaining to that feature.
Decision
--------
**Following the spirit of these principles is more important than following them to the letter.**
These rules are guidelines. It won't always be reasonable or necessary to follow them to the letter. They provide a set of tools for dealing with complexity. It follows, then, that if you don't have complexity, then you may not need the tools.
Primary guiding principles
==========================
**1. Code is organized into feature directories.**
A feature is a logical or semantically significant grouping of code; what comprises a feature is subjective. It also may not be obvious at first - if code tends to be related or change together, then it's probably part of the same feature.
It's unlikely to be worth agonizing over your feature breakdown; time will tell what's correct moreso than overthinking it. That said, a sufficiently complex set of features will need a similarly robust taxonomy and organizational hierarchy. (This document endeavors to help inform that hierarchy.) A nice rule of thumb is that a feature should conceptually be able to be extracted into its own npm package with minimal effort.
**2. Create strict module boundaries**
A module should have a public interface exposed via an index.js file in the module directory. Consumers of a feature should limit themselves to importing only from the public exports.
::
import { MyComponent, reducer as myComponentReducer } from './submodule'; // Good
import MyComponent from './submodule/MyComponent'; // Bad
import reducer from './submodule/data/reducers'; // Bad
Modules are configured by their parent. Generally a module will expose a few things which need to be configured make use of them in the consuming code. The reason for doing this is so that the module doesn't make assumptions about it's context (effectively dependency injection).
Examples:
* For React components, this involves including them in JSX and giving them props.
* For services, this is calling their "configure" method and providing them apiClient/configuration, etc.
* For reducers, this is mounting the reducer at an agreed-upon place in the redux store's state tree.
**3. Avoid circular dependencies**
Circular dependencies are unresolvable in webpack, and will result in something being imported as 'undefined'. They're also incredibly difficult and frustrating to track down. Properly factoring your features and supporting modules should help avoid these sorts of issues. In general, a feature should never need to import from its parent or grandparents, and a more "general purpose" module should never be importing from a more specific one. If you find yourself importing from a domain-specific feature in your general utility module, then something is probably ill-factored.
File and directory naming
=========================
This section details a specific taxonomy and hierarchy to help make code modular, approachable and maintainable.
**A. Separate data management from components.**
In order to isolate our view layer (React) from the management of our data, global state, APIs, and side effects, we want to adopt the "ducks" organization (see references). This involves isolating data management into a
sub directory of a feature. We'll use the directory name "data" rather than the traditional "ducks".
**C. React components will be named semantically.**
The convention for React components is for the file to be named for what the component does, so we will preserve this. A given feature may break up its view layer into multiple sub-components without a sub-feature being present.
**B. Files in a module's data directory are named by function.**
In the data sub-directory, the file names describe what each piece of code does. Unlike React components, all of the data handlers (actions, reducers, sagas, selectors, services, etc.) are generally short functions, and so we put them all in the same file together with others of their kind.
::
/profile
/index.js // public interface
/ProfilePage.jsx // view
/ProfilePhotoUploader.jsx // supporting view
/data // Note: most files here are named with a plural, as they contain many of the things in question.
/actions.js
/constants.js
/reducers.js
/sagas.js
/selectors.js
/service.js // Note: singular - there's one 'service' here that provides many methods.
If you find yourself desiring to have multiple files of a particular type in the data directory, this is a strong sign that you actually need a sub-feature instead.
**C. Sub-features follow the same naming scheme.**
Sub-features should follow the same rules as any other module.
A module with a sub-module:
::
/profile
/index.js // public interface
/ProfilePage.jsx
/Avatar.jsx // additional components for a feature reside here at the top level, not in a "components" subdirectory.
/data
/actions.js
/reducers.js
/sagas.js
/service.js
/profile-photo
/index.js // public interface
/ProfilePhoto.jsx
/data
/actions.js
/reducers.js
/selectors.js
/education // Sparse sub-module
/index.js // public interface
/Education.jsx
/site-language // No view layer sub-module
/index.js // public interface
/data
/actions.js
/reducers.js
Note that a given feature need not contain files of all types, nor is having files of all types a prerequisite for having a feature. A feature may not contain a view (Component) layer, or in contrast to that, may not need a data directory at all!
Importing rules of thumb
========================
It can be difficult to figure out where it's okay to import from. Following these rules of thumb will help maintain a healthy code organization and should prevent the possibility of circular dependencies.
**I. A feature may not import from its parentage.**
As described above in "Avoid circular dependencies", features should not import from their parent, grandparent, etc. A feature should be agnostic to the context in which it is used. If a module is importing from its parent or grandparent, that implies something is ill-factored.
**II. A feature may import from its children, but not its grandchildren.**
The feature may only import from the exports of its child, which may include exports of the grandchildren. Importing directly from grandchildren (or great grandchildren, etc.) would violate the strict module boundary of the child.
**II. Features may import from their siblings.**
It's acceptable to import from a module's siblings, or the siblings of their parents, grandparents, etc. This is necessary to support code re-use. As an example, assume we have a sub-module with common code to support our web forms.
::
/feature1
/sub-form-1
/sub-form-2
/forms-common-code
The sub-form modules can import from forms-common-code. The latter has its own strict module boundary and could conceptually be extracted into its own repository/completely independent module as far as they're concerned. They're unaware, conceptually, that it's a child of feature1, and they don't care.
**III. Features may import from the siblings of their parentage.**
This is less intuitive, but is not really any different than the above.
If another feature (feature2) also needs forms-common-code, it should be brought up a level so it's available to feature2, as feature2 cannot "reach into" feature1:
::
/feature1
/sub-form-1
/sub-form-2
/forms-common-code
/feature2 // can now use forms-common-code
In a complex app, you could imagine that forms-common-code needs to be brought up several levels, in which case our imports might look like:
::
import { formStuff } from '../../../forms-common-code';
This is okay. Conceptually it's no different than importing from a third party npm package, we just happen to know the code we want is up a few directories nearby, rather than using the syntactic sugar of a pathless import from node_modules.
At some point, if forms-common-code is general purpose enough, we may want to extract it from this repository/set of features all together.
Consequences
------------
This organization has been implemented in several of our micro-frontends so far (frontend-app-account and frontend-app-payment most significantly) and we feel it has improved the organization and approachability of the apps. When converting frontend-app-account to use this organization, it took 2-3 days to refactor the code.
It's worth noting that to get this right, it may actually involve changing the way the modules interact with each other. It isn't as simple as just moving files around and copy/pasting code. For instance, in frontend-app-account, it became obvious very quickly that to create strict module boundaries, we had to change the way that our service layers (server requests) were configured to keep them from importing their own configuration from their parent/grandparent. Similarly, our redux store tree of reducers became more complex and deeply nested.
References
----------
Articles on react/redux application organization:
* Primary reference:
- https://jaysoo.ca/2016/02/28/organizing-redux-application/
* Ducks references:
- https://github.com/erikras/ducks-modular-redux
- https://medium.freecodecamp.org/scaling-your-redux-app-with-ducks-6115955638be
* Other reading:
- https://hackernoon.com/fractal-a-react-app-structure-for-infinite-scale-4dab943092af
- https://marmelab.com/blog/2015/12/17/react-directory-structure.html
- https://redux.js.org/faq/code-structure

View File

@@ -16,7 +16,7 @@ import { Spinner } from '@openedx/paragon';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import EditorContainer from '../EditorContainer';
import * as module from '.';
import * as module from '..';
import { actions, selectors } from '../../data/redux';
import { RequestKeys } from '../../data/constants/requests';

View File

@@ -1,6 +1,6 @@
/* istanbul ignore file */
import react from 'react';
import { StrictDict } from './editors/utils';
import { StrictDict } from './utils';
/**
* Mocked formatMessage provided by react-intl
*/

View File

@@ -1,18 +0,0 @@
import arMessages from './messages/ar.json';
// no need to import en messages-- they are in the defaultMessage field
import es419Messages from './messages/es_419.json';
import frMessages from './messages/fr.json';
import kokrMessages from './messages/ko_KR.json';
import ptbrMessages from './messages/pt_BR.json';
import zhcnMessages from './messages/zh_CN.json';
const messages = {
ar: arMessages,
'es-419': es419Messages,
fr: frMessages,
'zh-cn': zhcnMessages,
'ko-kr': kokrMessages,
'pt-br': ptbrMessages,
};
export default messages;

View File

@@ -1,2 +0,0 @@
{
}

View File

@@ -1,2 +0,0 @@
{
}

View File

@@ -1,2 +0,0 @@
{
}

View File

@@ -1,2 +0,0 @@
{
}

View File

@@ -1,2 +0,0 @@
{
}

View File

@@ -1,2 +0,0 @@
{
}

View File

@@ -1,24 +0,0 @@
import Placeholder from './Placeholder';
import messages from './i18n/index';
import EditorPage from './editors/EditorPage';
import VideoSelectorPage from './editors/VideoSelectorPage';
import DraggableList, { SortableItem } from './editors/sharedComponents/DraggableList';
import ErrorAlert from './editors/sharedComponents/ErrorAlerts/ErrorAlert';
import { TinyMceWidget } from './editors/sharedComponents/TinyMceWidget';
import { prepareEditorRef } from './editors/sharedComponents/TinyMceWidget/hooks';
import TypeaheadDropdown from './editors/sharedComponents/TypeaheadDropdown';
import SelectableBox from './editors/sharedComponents/SelectableBox';
export {
messages,
EditorPage,
VideoSelectorPage,
DraggableList,
SortableItem,
ErrorAlert,
TinyMceWidget,
prepareEditorRef,
TypeaheadDropdown,
SelectableBox,
};
export default Placeholder;

View File

View File

@@ -1,156 +0,0 @@
/* eslint-disable import/no-extraneous-dependencies */
import 'babel-polyfill';
import 'jest-canvas-mock';
/* need to mock window for tinymce on import, as it is JSDOM incompatible */
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: jest.fn().mockImplementation(query => ({
matches: false,
media: query,
onchange: null,
addListener: jest.fn(), // Deprecated
removeListener: jest.fn(), // Deprecated
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
dispatchEvent: jest.fn(),
})),
});
// These configuration values are usually set in webpack's EnvironmentPlugin however
// Jest does not use webpack so we need to set these so for testing
process.env.ACCESS_TOKEN_COOKIE_NAME = 'edx-jwt-cookie-header-payload';
process.env.BASE_URL = 'localhost:1995';
process.env.CREDENTIALS_BASE_URL = 'http://localhost:18150';
process.env.CSRF_TOKEN_API_PATH = '/csrf/api/v1/token';
process.env.ECOMMERCE_BASE_URL = 'http://localhost:18130';
process.env.LANGUAGE_PREFERENCE_COOKIE_NAME = 'openedx-language-preference';
process.env.LMS_BASE_URL = 'http://localhost:18000';
process.env.LOGIN_URL = 'http://localhost:18000/login';
process.env.LOGOUT_URL = 'http://localhost:18000/login';
process.env.MARKETING_SITE_BASE_URL = 'http://localhost:18000';
process.env.ORDER_HISTORY_URL = 'localhost:1996/orders';
process.env.REFRESH_ACCESS_TOKEN_ENDPOINT = 'http://localhost:18000/login_refresh';
process.env.SEGMENT_KEY = 'segment_whoa';
process.env.SITE_NAME = 'edX';
process.env.USER_INFO_COOKIE_NAME = 'edx-user-info';
process.env.LOGO_URL = 'https://edx-cdn.org/v3/default/logo.svg';
process.env.LOGO_TRADEMARK_URL = 'https://edx-cdn.org/v3/default/logo-trademark.svg';
process.env.LOGO_WHITE_URL = 'https://edx-cdn.org/v3/default/logo-white.svg';
process.env.FAVICON_URL = 'https://edx-cdn.org/v3/default/favicon.ico';
jest.mock('@edx/frontend-platform/i18n', () => {
const i18n = jest.requireActual('@edx/frontend-platform/i18n');
const PropTypes = jest.requireActual('prop-types');
return {
...i18n,
intlShape: PropTypes.shape({
formatMessage: PropTypes.func,
}),
defineMessages: m => m,
getLocale: () => 'getLocale',
FormattedDate: () => 'FormattedDate',
FormattedMessage: () => 'FormattedMessage',
FormattedTime: () => 'FormattedTime',
};
});
jest.mock('@openedx/paragon', () => jest.requireActual('testUtils').mockNestedComponents({
Alert: {
Heading: 'Alert.Heading',
},
ActionRow: {
Spacer: 'ActionRow.Spacer',
},
Button: 'Button',
ButtonGroup: 'ButtonGroup',
Collapsible: {
Advanced: 'Advanced',
Body: 'Body',
Trigger: 'Trigger',
Visible: 'Visible',
},
Card: {
Header: 'Card.Header',
Section: 'Card.Section',
Footer: 'Card.Footer',
Body: 'Card.Body',
},
Col: 'Col',
Container: 'Container',
Dropdown: {
Item: 'Dropdown.Item',
Menu: 'Dropdown.Menu',
Toggle: 'Dropdown.Toggle',
},
ErrorContext: {
Provider: 'ErrorContext.Provider',
},
Hyperlink: 'Hyperlink',
Icon: 'Icon',
IconButton: 'IconButton',
IconButtonWithTooltip: 'IconButtonWithTooltip',
Image: 'Image',
MailtoLink: 'MailtoLink',
ModalDialog: {
Footer: 'ModalDialog.Footer',
Header: 'ModalDialog.Header',
Title: 'ModalDialog.Title',
Body: 'ModalDialog.Body',
CloseButton: 'ModalDialog.CloseButton',
},
Form: {
Checkbox: 'Form.Checkbox',
Control: {
Feedback: 'Form.Control.Feedback',
},
Group: 'Form.Group',
Label: 'Form.Label',
Text: 'Form.Text',
Row: 'Form.Row',
Radio: 'Radio',
RadioSet: 'RadioSet',
},
OverlayTrigger: 'OverlayTrigger',
Tooltip: 'Tooltip',
FullscreenModal: 'FullscreenModal',
Row: 'Row',
Scrollable: 'Scrollable',
SelectableBox: {
Set: 'SelectableBox.Set',
},
Spinner: 'Spinner',
Stack: 'Stack',
Toast: 'Toast',
Truncate: 'Truncate',
useWindowSize: { height: '500px' },
}));
jest.mock('@openedx/paragon/icons', () => ({
Close: jest.fn().mockName('icons.Close'),
Edit: jest.fn().mockName('icons.Edit'),
Locked: jest.fn().mockName('icons.Locked'),
Unlocked: jest.fn().mockName('icons.Unlocked'),
}));
// Mock react-redux hooks
// unmock for integration tests
jest.mock('react-redux', () => {
const dispatch = jest.fn((...args) => ({ dispatch: args })).mockName('react-redux.dispatch');
return {
connect: (mapStateToProps, mapDispatchToProps) => (component) => ({
mapStateToProps,
mapDispatchToProps,
component,
}),
useDispatch: jest.fn(() => dispatch),
useSelector: jest.fn((selector) => ({ useSelector: selector })),
};
});
// Mock the plugins repo so jest will stop complaining about ES6 syntax
jest.mock('frontend-components-tinymce-advanced-plugins', () => ({
a11ycheckerCss: '',
}));