Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Library Content Block Editor #411

Closed
wants to merge 49 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
27b2c6f
feat: initial editor layout + ui
connorhaugh Oct 17, 2023
1a3bc9b
feat: wip
rayzhou-bit Oct 25, 2023
668c184
feat: wip2
rayzhou-bit Nov 1, 2023
d8a45fd
feat: wip3
rayzhou-bit Nov 6, 2023
3f1887c
feat: polish
rayzhou-bit Nov 8, 2023
d66c7e9
test: add reducers and selectors tests
connorhaugh Nov 8, 2023
3734e01
fix: proptypes
rayzhou-bit Nov 9, 2023
2b263ee
test: add blockselector test
connorhaugh Nov 9, 2023
ffd2e63
test: add three more JS tests
connorhaugh Nov 10, 2023
65eee08
fix: add tests for all but hooks.js
connorhaugh Nov 10, 2023
2674607
fix: initialize
rayzhou-bit Nov 13, 2023
e74c90f
fix: library api
rayzhou-bit Nov 13, 2023
3d8d5b0
feat: api tweaks mostly with index.js
rayzhou-bit Nov 15, 2023
5d2f783
feat: libraryselector.js api update
rayzhou-bit Nov 15, 2023
a9989da
feat: blockselector.js rewrite wip
rayzhou-bit Nov 16, 2023
56c4c39
feat: general changes to api usage
rayzhou-bit Nov 17, 2023
2b92174
feat: table saves candidates
rayzhou-bit Nov 22, 2023
21bd883
feat: load candidate works
rayzhou-bit Nov 28, 2023
ab3e541
feat: v1 library selection
rayzhou-bit Nov 29, 2023
fe65556
fix: loading issue and polish
rayzhou-bit Nov 30, 2023
45b9f40
fix: saving blocks
rayzhou-bit Nov 30, 2023
1339322
feat: sort library dropdown alphabetically
rayzhou-bit Nov 30, 2023
c92c35f
feat: lint and some fixes
rayzhou-bit Dec 1, 2023
48ce342
feat: lint and tests
rayzhou-bit Dec 5, 2023
8fb9644
feat: candidate tuples and more tests
rayzhou-bit Dec 5, 2023
9a56336
feat: more tests
rayzhou-bit Dec 6, 2023
4bdf570
feat: why are tests so hard
rayzhou-bit Dec 6, 2023
535bc14
feat: more tests
rayzhou-bit Dec 7, 2023
2699795
feat: lint
rayzhou-bit Dec 7, 2023
be6cbe8
feat: merge main
rayzhou-bit Dec 7, 2023
bcc0984
feat: lint
rayzhou-bit Dec 7, 2023
be615b6
feat: some more api tests
rayzhou-bit Dec 7, 2023
87fbfa7
feat: selectors test
rayzhou-bit Dec 7, 2023
6265f5a
feat: remove fetchV2LibraryMetadata
rayzhou-bit Dec 19, 2023
3a689a4
feat: v1 library api update
rayzhou-bit Dec 19, 2023
0d51711
feat: fix
rayzhou-bit Dec 19, 2023
b09ed11
feat: lint
rayzhou-bit Dec 19, 2023
32c4fb3
feat: library version should be string
rayzhou-bit Dec 19, 2023
208070a
feat: failure tests
rayzhou-bit Dec 20, 2023
02f6ac3
feat: merge conflict
rayzhou-bit Dec 20, 2023
3ea40d8
feat: LCB children
rayzhou-bit Dec 28, 2023
fd6d530
feat: new fetch children api
rayzhou-bit Jan 3, 2024
1c783d7
feat: test and lint
rayzhou-bit Jan 3, 2024
0bfa336
feat: use usage id and fetch v1 library block
rayzhou-bit Jan 5, 2024
8553ac6
feat: regex fix and v1 library version
rayzhou-bit Jan 5, 2024
9db63b4
feat: lint
rayzhou-bit Jan 6, 2024
0c68a92
feat: v1 api fix and candidate saving
rayzhou-bit Jan 16, 2024
01f6296
feat: test and lint
rayzhou-bit Jan 19, 2024
d94280c
Merge remote-tracking branch 'upstream/main' into feat--library-conte…
kdmccormick Feb 9, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
"@testing-library/dom": "^8.13.0",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^12.1.5",
"@testing-library/react-hooks": "^8.0.1",
"@testing-library/user-event": "^13.5.0",
"eslint-import-resolver-alias": "^1.1.2",
"eslint-plugin-import": "^2.26.0",
Expand Down
6 changes: 1 addition & 5 deletions src/editors/Editor.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,6 @@ jest.mock('./hooks', () => ({
initializeApp: jest.fn(),
}));

jest.mock('./containers/TextEditor', () => 'TextEditor');
jest.mock('./containers/VideoEditor', () => 'VideoEditor');
jest.mock('./containers/ProblemEditor', () => 'ProblemEditor');

const initData = {
blockId: 'block-v1:edX+DemoX+Demo_Course+type@html+block@030e35c4756a4ddc8d40b95fbbfff4d4',
blockType: blockTypes.html,
Expand All @@ -39,7 +35,7 @@ describe('Editor', () => {
});
test.each(Object.values(blockTypes))('renders %p editor when ref is ready', (blockType) => {
el = shallow(<Editor {...props} blockType={blockType} />);
expect(el.shallowWrapper.props.children.props.children.type).toBe(supportedEditors[blockType]);
expect(el.children().children().at(0).name()).toBe(supportedEditors[blockType].displayName);
});
});
describe('behavior', () => {
Expand Down
2 changes: 1 addition & 1 deletion src/editors/__snapshots__/Editor.test.jsx.snap
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ exports[`Editor render snapshot: renders correct editor given blockType (html ->
className="pgn__modal-fullscreen h-100"
role="dialog"
>
<TextEditor
<injectIntl(ShimmedIntlComponent)
onClose={[MockFunction props.onClose]}
returnFunction={null}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ exports[`EditorContainer component render snapshot: initialized. enable save and
</div>
</ModalDialog.Header>
<ModalDialog.Body
className="pb-0 mb-6"
className="pb-6 min-vh-100"
>
<h1>
My test content
Expand Down Expand Up @@ -139,7 +139,7 @@ exports[`EditorContainer component render snapshot: not initialized. disable sav
</div>
</ModalDialog.Header>
<ModalDialog.Body
className="pb-0 mb-6"
className="pb-6 min-vh-100"
/>
<injectIntl(ShimmedIntlComponent)
disableSave={true}
Expand Down
2 changes: 1 addition & 1 deletion src/editors/containers/EditorContainer/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ export const EditorContainer = ({
/>
</div>
</ModalDialog.Header>
<ModalDialog.Body className="pb-0 mb-6">
<ModalDialog.Body className="pb-6 min-vh-100">
rayzhou-bit marked this conversation as resolved.
Show resolved Hide resolved
{isInitialized && children}
</ModalDialog.Body>
<EditorFooter
Expand Down
149 changes: 149 additions & 0 deletions src/editors/containers/LibraryContentEditor/BlocksSelector.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import React from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import { FormattedMessage, injectIntl } from '@edx/frontend-platform/i18n';
import { CheckboxControl, DataTable, Form } from '@edx/paragon';

import { modes } from './constants';
import { selectors } from '../../data/redux';
import { useBlocksSelectorHook } from './hooks';
import messages from './messages';

export const SELECT_ONE_TEST_ID = 'selectOne';

export const RowCheckbox = ({ row }) => {
const {
indeterminate,
checked,
...toggleRowSelectedProps
} = row.getToggleRowSelectedProps();

return (
<div className="text-center">
<CheckboxControl
{...toggleRowSelectedProps}
title="Toggle row selected"
checked={checked}
isIndeterminate={false}
data-testid={SELECT_ONE_TEST_ID}
/>
</div>
);
};

export const BlocksSelector = ({
// redux
blocks,
mode,
candidates,
libraries,
savedLibraryId,
selectedLibraryId,
v1BlockRequests,
}) => {
const {
tableDataLoaded,
data,
initialRows,
onSelectedRowsChanged,
} = useBlocksSelectorHook({
blocks,
candidates,
libraries,
savedLibraryId,
selectedLibraryId,
v1BlockRequests,
});

const columns = [
{
Header: 'Name',
accessor: 'display_name',
},
{
Header: 'Block Type',
accessor: 'block_type',
},
];

const selectColumn = {
id: 'selection',
Header: () => null,
Cell: RowCheckbox,
disableSortBy: true,
};

if (selectedLibraryId === null || mode !== modes.selected.value || !tableDataLoaded) {
return null;
}

return (
<div className="mb-5 pt-3 border-top">
<Form.Label>
<FormattedMessage {...messages.tableInstructionLabel} />
</Form.Label>
<DataTable
key={selectedLibraryId}
columns={columns}
data={data}
itemCount={data.length}
isSelectable
isPaginated
isSortable
initialState={{ selectedRowIds: initialRows }}
manualSelectColumn={selectColumn}
onSelectedRowsChanged={onSelectedRowsChanged}
>
<DataTable.TableControlBar />
<DataTable.Table />
<DataTable.EmptyTable content="No blocks found." />
<DataTable.TableFooter />
</DataTable>
</div>
);
};

RowCheckbox.defaultProps = {
row: {},
};

RowCheckbox.propTypes = {
row: PropTypes.shape({
getToggleRowSelectedProps: PropTypes.func.isRequired,
connorhaugh marked this conversation as resolved.
Show resolved Hide resolved
}),
};

BlocksSelector.defaultProps = {
blocks: [],
candidates: [],
libraries: [],
mode: '',
savedLibraryId: null,
selectedLibraryId: null,
v1BlockRequests: [],
};

BlocksSelector.propTypes = {
// redux
blocks: PropTypes.arrayOf(PropTypes.shape({})),
candidates: PropTypes.shape([]),
libraries: PropTypes.shape([]),
mode: PropTypes.string,
savedLibraryId: PropTypes.string,
selectedLibraryId: PropTypes.string,
v1BlockRequests: PropTypes.shape({}),
};

export const mapStateToProps = (state) => ({
blocks: selectors.library.blocks(state),
candidates: selectors.library.candidates(state),
libraries: selectors.library.libraries(state),
mode: selectors.library.mode(state),
savedLibraryId: selectors.library.savedLibraryId(state),
selectedLibraryId: selectors.library.selectedLibraryId(state),
v1BlockRequests: selectors.library.v1BlockRequests(state),
});

export const mapDispatchToProps = {};

export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(BlocksSelector));
111 changes: 111 additions & 0 deletions src/editors/containers/LibraryContentEditor/BlocksSelector.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import React from 'react';
import { screen, render } from '@testing-library/react';
import '@testing-library/jest-dom/extend-expect';
import userEvent from '@testing-library/user-event';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { BlocksSelector, RowCheckbox, SELECT_ONE_TEST_ID } from './BlocksSelector';

jest.unmock('@edx/paragon');
jest.unmock('@edx/paragon/icons');

jest.mock('./hooks', () => ({
useBlocksSelectorHook: jest.fn().mockReturnValue({
tableDataLoaded: true,
data: [
{ id: 1, display_name: 'Block 1', block_type: 'Type A' },
{ id: 2, display_name: 'Block 2', block_type: 'Type B' },
],
initialRows: {},
onSelectedRowsChanged: jest.fn(),
}),
}));

function renderComponent(props) {
return render(
<IntlProvider locale="en">
<BlocksSelector {...props} />
</IntlProvider>,
);
}

const mockProps = {
initialRows: {},
mode: 'selected',
setCandidatesForLibrary: jest.fn(),
selectedLibraryId: 'exampleLibraryId',
};
const mockOnChange = jest.fn();
const defaultToggleRowSelectedProps = {
indeterminate: false,
checked: false,
onChange: mockOnChange,
};
const mockToggleRowSelectedProps = jest.fn(() => defaultToggleRowSelectedProps);
const defaultRow = {
id: 'foo',
getToggleRowSelectedProps: mockToggleRowSelectedProps,
};
const checkedRow = {
...defaultRow,
getToggleRowSelectedProps: jest.fn(() => ({
...defaultToggleRowSelectedProps,
checked: true,
})),
};

describe('BlocksSelector', () => {
it('renders when selectedLibraryId is not null', () => {
const { queryByText } = renderComponent(mockProps);

// make sure that the relevant columns are there
expect(queryByText('Name')).toBeTruthy();
expect(queryByText('Block Type')).toBeTruthy();
// make sure that the relevant rows are there
});

it('does not render when selectedLibraryId is null', () => {
const { container } = renderComponent({ ...mockProps, selectedLibraryId: null });
expect(container.firstChild).toBeFalsy();
});

it('renders when mode is selected', () => {
const { queryByText } = renderComponent(mockProps);
expect(queryByText('Name')).toBeTruthy();
});

it('does not render when mode is not selected', () => {
const { container } = renderComponent({ ...mockProps, mode: 'soMeThingElse' });
expect(container.firstChild).toBeFalsy();
});
});

describe('RowCheckbox', () => {
beforeEach(() => {
jest.clearAllMocks();
});

it('renders a checkbox', () => {
render(<RowCheckbox contextKey="emails" row={defaultRow} />);
const checkbox = screen.getByTestId(SELECT_ONE_TEST_ID);
expect(checkbox).toBeInTheDocument();
expect(checkbox).toHaveProperty('checked', false);
});

it('renders a selected checkbox', () => {
render(
<RowCheckbox contextKey="emails" row={checkedRow} />,
);
const checkbox = screen.getByTestId(SELECT_ONE_TEST_ID);
expect(checkbox).toBeInTheDocument();
expect(checkbox).toHaveProperty('checked', true);
});

it('deselects the row when selected checkbox is checked', () => {
render(
<RowCheckbox contextKey="emails" row={defaultRow} />,
);
const checkbox = screen.getByTestId(SELECT_ONE_TEST_ID);
userEvent.click(checkbox);
expect(mockOnChange).toHaveBeenCalledTimes(1);
});
});
Loading
Loading