forked from openedx/frontend-app-authoring
-
Notifications
You must be signed in to change notification settings - Fork 0
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: create library form (TEMP) #42
Closed
rpenido
wants to merge
104
commits into
rpenido/fal-3753-library-home-page-bare-bones
from
rpenido/fal-3768-create-new-library-form
Closed
Changes from 90 commits
Commits
Show all changes
104 commits
Select commit
Hold shift + click to select a range
4191ecb
feat: Add lib v2/legacy tabs in studio home
yusuf-musleh 15c678b
feat: Add `LIBRARY_MODE` config variable
yusuf-musleh 14b8b3a
feat: Add url paths/navigation for each tab
yusuf-musleh 515cc71
feat: LibraryV2 redirect to lib mfe or placeholder
yusuf-musleh 9c6e1e6
feat: Add pagination support for lib v2s
yusuf-musleh da189c1
fix: Redirect to placeholder create lib in v2/mixed disabled mfe
yusuf-musleh 85d9ff2
temp: This removes TS code to get tests to run
yusuf-musleh c6b7bf8
test: Update existing tests to support changes
yusuf-musleh 14933d2
temp: Rename .tsx -> .jsx & .ts -> .js for tests
yusuf-musleh 8b96268
fix: Fix lint issues
yusuf-musleh f8db853
feat: library home page bare bones
rpenido 7a8488d
test: Add tests for new functionality
yusuf-musleh 7842ce0
fix: update search modal for new library urls
rpenido 462cda9
feat: Add lib v2/legacy tabs in studio home
yusuf-musleh be8b2f4
feat: Add `LIBRARY_MODE` config variable
yusuf-musleh 27b2581
feat: Add url paths/navigation for each tab
yusuf-musleh 4ffd651
feat: LibraryV2 redirect to lib mfe or placeholder
yusuf-musleh 7f97243
feat: Add pagination support for lib v2s
yusuf-musleh c86b85a
fix: Redirect to placeholder create lib in v2/mixed disabled mfe
yusuf-musleh efbc625
temp: This removes TS code to get tests to run
yusuf-musleh 21da6f8
test: Update existing tests to support changes
yusuf-musleh 79e6516
temp: Rename .tsx -> .jsx & .ts -> .js for tests
yusuf-musleh 262cb3f
fix: Fix lint issues
yusuf-musleh 1ea229f
test: Add tests for new functionality
yusuf-musleh a0a30b7
refactor: Change /legacy-libraries -> /libraries-v1
yusuf-musleh 4deaea9
fix: add i18n messages
rpenido c2bdecf
fix: libraryAuthoring enabled check
rpenido 5213eff
Merge branch 'yusuf-musleh/lib-v2-tab-studio-home' into rpenido/fal-3…
rpenido a24b3ba
fix: add Create Library placeholder
rpenido 2859741
refactor: Remove hardcoded mfe path
yusuf-musleh d853f29
refactor: rename .ts files to .js
rpenido 567dcb4
feat: Add function to construct Lib Auth MFE URL
yusuf-musleh 094086e
feat: Make URL /library-v1 when referencing legacy
yusuf-musleh a95c990
fix: Add missing part of path
yusuf-musleh e3ebc55
fix: type and lint errors
rpenido 8ed168d
fix: Issue with destinationUrl
yusuf-musleh beda37f
test: Add checks for Tab.eventKey in tests
yusuf-musleh 2e3fa43
fix: Revert card item url changes to keep simple
yusuf-musleh 72edfac
fix: add tests
rpenido 8086ebf
Merge branch 'yusuf-musleh/lib-v2-tab-studio-home' into rpenido/fal-3…
rpenido 91443e9
fix: removing unused file
rpenido a29cf7e
fix: add ts-check
rpenido 4deab76
fix: removing deleted file references
rpenido e8bca34
chore: trigger CI
rpenido 388c40e
fix: fixes from review
rpenido b827976
feat: create library form
rpenido 7d6096e
fix: fix default parameter syntax
rpenido c63bc2f
fix: new library redirect
rpenido efe0bed
Revert "fix: new library redirect"
rpenido 942cbff
chore: trigger CI
rpenido a82a7bc
feat: add api call
rpenido 2d3be09
fix: new library redirect
rpenido e8f9f78
fix: remove warning on library home
rpenido 8724ac0
Merge branch 'rpenido/fal-3753-library-home-page-bare-bones' into rpe…
rpenido f9920f4
fix: remove unused import
rpenido 438b726
Merge branch 'rpenido/fal-3753-library-home-page-bare-bones' into rpe…
rpenido bccaedc
fix: add footer to create library form
rpenido 7fd6c27
Merge branch 'master' into rpenido/fal-3753-library-home-page-bare-bones
rpenido 94f483b
fix: merging nits
rpenido b455f97
fix: change version to slug in header
rpenido 3ae6fe8
fix: header outline link for libraries
rpenido e6b877d
Merge branch 'rpenido/fal-3753-library-home-page-bare-bones' into rpe…
rpenido 1045687
refactor: form refactor and tests
rpenido 38b578e
fix: set default for ts-check
rpenido e972fb0
Merge branch 'master' into rpenido/fal-3753-library-home-page-bare-bones
rpenido 55acb06
refactor: test renaming to ts
rpenido e3daf6a
refactor: test renaming to ts
rpenido 645b3de
refactor: migrating to typescript
rpenido a008837
fix: add description to api call
rpenido d131937
Merge branch 'rpenido/fal-3753-library-home-page-bare-bones' into rpe…
rpenido a1a0ad8
refactor: renaming files js -> ts
rpenido 87358b7
fix: update typescript code
rpenido 0b4e90e
Merge branch 'master' into rpenido/fal-3753-library-home-page-bare-bones
rpenido 529d76a
Merge branch 'rpenido/fal-3753-library-home-page-bare-bones' into rpe…
rpenido 037018b
feat: remove file
rpenido fabc561
refactor: renaming js -> ts
rpenido ae93c8e
fix: update typescript code
rpenido 61661a8
feat: better error handling and test
rpenido 708426c
fix: slug regex
rpenido 8d71297
fix: remove @ts-check and move CreateContentLibraryArgs type
rpenido 51d36ce
fix: remove @ts-check
rpenido fbd418d
fix: remove unused file
rpenido dee7c0b
Merge branch 'master' into rpenido/fal-3753-library-home-page-bare-bones
rpenido 2a5d42b
fix: optional parameters
rpenido 62fe7f5
chore: trigger CI
rpenido ca33c18
Merge branch 'master' into rpenido/fal-3753-library-home-page-bare-bones
rpenido a4c42b4
Merge branch 'rpenido/fal-3753-library-home-page-bare-bones' into rpe…
rpenido c447036
refactor: remove axios dependency
rpenido 9528bfd
chore(deps): update dependency meilisearch to ^0.41.0 (#1136)
renovate[bot] 8cf26e1
Version bump for Paragon to 22.6.1, with stricter typing (#1146)
bradenmacdonald 83489b0
feat: Add filters/sorting for the libraries v2 tab on studio home (#1…
yusuf-musleh d5fc6fc
Merge branch 'rpenido/fal-3753-library-home-page-bare-bones' into rpe…
rpenido 6a99b48
fix: merging errors
rpenido 63e7593
fix: rename contentId -> contextId and add typings
rpenido 5f4ebbd
refactor: renaming index.js -> index.ts
rpenido 01d4b85
perf: lockfile version check workflow file updated (#1107)
huniafatima-arbi 09822c2
chore: update browserslist DB (#443)
edx-requirements-bot f16c1df
Merge branch 'rpenido/fal-3753-library-home-page-bare-bones' into rpe…
rpenido a7fe999
feat: add types to createLibraryV2 api
rpenido 9a23204
refactor: change code to use useMutate values instead of states
rpenido 117b4f1
chore: remove core-js and regenerator-runtime (#1032)
bradenmacdonald f60ddb5
feat: library home page ("bare bones") (#1076)
rpenido 71fcf9f
fix: only show course blocks in the search modal (#1148)
rpenido fcef5ba
Merge branch 'master' into rpenido/fal-3768-create-new-library-form
rpenido File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
131 changes: 131 additions & 0 deletions
131
src/library-authoring/create-library/CreateLibrary.test.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,131 @@ | ||
import React from 'react'; | ||
import MockAdapter from 'axios-mock-adapter'; | ||
import { initializeMockApp } from '@edx/frontend-platform'; | ||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; | ||
import { IntlProvider } from '@edx/frontend-platform/i18n'; | ||
import { AppProvider } from '@edx/frontend-platform/react'; | ||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; | ||
import { fireEvent, render, waitFor } from '@testing-library/react'; | ||
import userEvent from '@testing-library/user-event'; | ||
|
||
import initializeStore from '../../store'; | ||
import CreateLibrary from './CreateLibrary'; | ||
import { getContentLibraryV2CreateApiUrl } from './data/api'; | ||
|
||
let store; | ||
const mockNavigate = jest.fn(); | ||
let axiosMock: MockAdapter; | ||
|
||
jest.mock('react-router-dom', () => ({ | ||
...jest.requireActual('react-router-dom'), | ||
useNavigate: () => mockNavigate, | ||
})); | ||
|
||
jest.mock('../../generic/data/apiHooks', () => ({ | ||
...jest.requireActual('../../generic/data/apiHooks'), | ||
useOrganizationListData: () => ({ | ||
data: ['org1', 'org2', 'org3', 'org4', 'org5'], | ||
isLoading: false, | ||
}), | ||
})); | ||
|
||
const queryClient = new QueryClient({ | ||
defaultOptions: { | ||
queries: { | ||
retry: false, | ||
}, | ||
}, | ||
}); | ||
|
||
const RootWrapper = () => ( | ||
<AppProvider store={store}> | ||
<IntlProvider locale="en" messages={{}}> | ||
<QueryClientProvider client={queryClient}> | ||
<CreateLibrary /> | ||
</QueryClientProvider> | ||
</IntlProvider> | ||
</AppProvider> | ||
); | ||
|
||
describe('<CreateLibrary />', () => { | ||
beforeEach(() => { | ||
initializeMockApp({ | ||
authenticatedUser: { | ||
userId: 3, | ||
username: 'abc123', | ||
administrator: true, | ||
roles: [], | ||
}, | ||
}); | ||
store = initializeStore(); | ||
|
||
axiosMock = new MockAdapter(getAuthenticatedHttpClient()); | ||
}); | ||
|
||
afterEach(() => { | ||
jest.clearAllMocks(); | ||
axiosMock.restore(); | ||
queryClient.clear(); | ||
}); | ||
|
||
test('call api data with correct data', async () => { | ||
axiosMock.onPost(getContentLibraryV2CreateApiUrl()).reply(200, { | ||
id: 'library-id', | ||
}); | ||
|
||
const { getByRole, getByTestId } = render(<RootWrapper />); | ||
|
||
const titleInput = getByRole('textbox', { name: /library name/i }); | ||
userEvent.click(titleInput); | ||
userEvent.type(titleInput, 'Test Library Name'); | ||
|
||
const orgInput = getByTestId('autosuggest-textbox-input'); | ||
userEvent.click(orgInput); | ||
userEvent.type(orgInput, 'org1'); | ||
userEvent.tab(); | ||
|
||
const slugInput = getByRole('textbox', { name: /library id/i }); | ||
userEvent.click(slugInput); | ||
userEvent.type(slugInput, 'test_library_slug'); | ||
|
||
fireEvent.click(getByRole('button', { name: /create/i })); | ||
await waitFor(() => { | ||
expect(axiosMock.history.post.length).toBe(1); | ||
expect(axiosMock.history.post[0].data).toBe( | ||
'{"description":"","title":"Test Library Name","org":"org1","slug":"test_library_slug"}', | ||
); | ||
expect(mockNavigate).toHaveBeenCalledWith('/library/library-id'); | ||
}); | ||
}); | ||
|
||
test('show api error', async () => { | ||
axiosMock.onPost(getContentLibraryV2CreateApiUrl()).reply(400, { | ||
field: 'Error message', | ||
}); | ||
const { getByRole, getByTestId } = render(<RootWrapper />); | ||
|
||
const titleInput = getByRole('textbox', { name: /library name/i }); | ||
userEvent.click(titleInput); | ||
userEvent.type(titleInput, 'Test Library Name'); | ||
|
||
const orgInput = getByTestId('autosuggest-textbox-input'); | ||
userEvent.click(orgInput); | ||
userEvent.type(orgInput, 'org1'); | ||
userEvent.tab(); | ||
|
||
const slugInput = getByRole('textbox', { name: /library id/i }); | ||
userEvent.click(slugInput); | ||
userEvent.type(slugInput, 'test_library_slug'); | ||
|
||
fireEvent.click(getByRole('button', { name: /create/i })); | ||
await waitFor(() => { | ||
expect(axiosMock.history.post.length).toBe(1); | ||
expect(axiosMock.history.post[0].data).toBe( | ||
'{"description":"","title":"Test Library Name","org":"org1","slug":"test_library_slug"}', | ||
); | ||
expect(mockNavigate).not.toHaveBeenCalled(); | ||
expect(getByRole('alert')).toHaveTextContent('Request failed with status code 400'); | ||
expect(getByRole('alert')).toHaveTextContent('{"field":"Error message"}'); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,147 @@ | ||
import React, { useState } from 'react'; | ||
import { StudioFooter } from '@edx/frontend-component-footer'; | ||
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n'; | ||
import { | ||
Alert, | ||
Container, | ||
Form, | ||
StatefulButton, | ||
} from '@openedx/paragon'; | ||
import { Formik } from 'formik'; | ||
import { useNavigate } from 'react-router-dom'; | ||
import * as Yup from 'yup'; | ||
|
||
import { REGEX_RULES } from '../../constants'; | ||
import Header from '../../header'; | ||
import FormikControl from '../../generic/FormikControl'; | ||
import FormikErrorFeedback from '../../generic/FormikErrorFeedback'; | ||
import { useOrganizationListData } from '../../generic/data/apiHooks'; | ||
import SubHeader from '../../generic/sub-header/SubHeader'; | ||
import type { CreateContentLibraryArgs } from './data/api'; | ||
import { useCreateLibraryV2 } from './data/apiHooks'; | ||
import messages from './messages'; | ||
|
||
const CreateLibrary = () => { | ||
const intl = useIntl(); | ||
const navigate = useNavigate(); | ||
|
||
const [apiError, setApiError] = useState<React.ReactNode>(); | ||
|
||
const { noSpaceRule, specialCharsRule } = REGEX_RULES; | ||
const validSlugIdRegex = /^[a-zA-Z\d]+(?:[\w-]*[a-zA-Z\d]+)*$/; | ||
|
||
const { | ||
mutateAsync, | ||
} = useCreateLibraryV2(); | ||
|
||
const { | ||
data: organizationListData, | ||
isLoading: isOrganizationListLoading, | ||
} = useOrganizationListData(); | ||
|
||
return ( | ||
<> | ||
<Header isHiddenMainMenu /> | ||
<Container size="xl" className="p-4 mt-3"> | ||
<SubHeader | ||
title={<FormattedMessage {...messages.createLibrary} />} | ||
/> | ||
<Formik | ||
initialValues={{ | ||
title: '', | ||
org: '', | ||
slug: '', | ||
}} | ||
validationSchema={ | ||
Yup.object().shape({ | ||
title: Yup.string() | ||
.required(intl.formatMessage(messages.requiredFieldError)), | ||
org: Yup.string() | ||
.required(intl.formatMessage(messages.requiredFieldError)) | ||
.matches( | ||
specialCharsRule, | ||
intl.formatMessage(messages.disallowedCharsError), | ||
) | ||
.matches(noSpaceRule, intl.formatMessage(messages.noSpaceError)), | ||
slug: Yup.string() | ||
.required(intl.formatMessage(messages.requiredFieldError)) | ||
.matches( | ||
validSlugIdRegex, | ||
intl.formatMessage(messages.invalidSlugError), | ||
), | ||
}) | ||
} | ||
onSubmit={async (values: CreateContentLibraryArgs) => { | ||
setApiError(undefined); | ||
try { | ||
const data = await mutateAsync(values); | ||
navigate(`/library/${data.id}`); | ||
} catch (error: any) { | ||
setApiError(( | ||
<> | ||
{error.message} | ||
<br /> | ||
{error.response?.data && JSON.stringify(error.response.data)} | ||
</> | ||
)); | ||
} | ||
}} | ||
> | ||
{(formikProps) => ( | ||
<Form onSubmit={formikProps.handleSubmit}> | ||
<FormikControl | ||
name="title" | ||
label={<Form.Label>{intl.formatMessage(messages.titleLabel)}</Form.Label>} | ||
value={formikProps.values.title} | ||
placeholder={intl.formatMessage(messages.titlePlaceholder)} | ||
help={intl.formatMessage(messages.titleHelp)} | ||
/> | ||
<Form.Group> | ||
<Form.Label>{intl.formatMessage(messages.orgLabel)}</Form.Label> | ||
<Form.Autosuggest | ||
name="org" | ||
isLoading={isOrganizationListLoading} | ||
onChange={(event) => formikProps.setFieldValue('org', event.selectionId)} | ||
placeholder={intl.formatMessage(messages.orgPlaceholder)} | ||
> | ||
{organizationListData ? organizationListData.map((org) => ( | ||
<Form.AutosuggestOption key={org} id={org}>{org}</Form.AutosuggestOption> | ||
)) : []} | ||
</Form.Autosuggest> | ||
<FormikErrorFeedback name="org"> | ||
<Form.Text>{intl.formatMessage(messages.orgHelp)}</Form.Text> | ||
</FormikErrorFeedback> | ||
</Form.Group> | ||
<FormikControl | ||
name="slug" | ||
label={<Form.Label>{intl.formatMessage(messages.slugLabel)}</Form.Label>} | ||
value={formikProps.values.slug} | ||
placeholder={intl.formatMessage(messages.slugPlaceholder)} | ||
help={intl.formatMessage(messages.slugHelp)} | ||
/> | ||
<StatefulButton | ||
type="submit" | ||
variant="primary" | ||
className="action btn-primary" | ||
state={formikProps.isSubmitting ? 'disabled' : 'enabled'} | ||
disabledStates={['disabled']} | ||
labels={{ | ||
enabled: intl.formatMessage(messages.createLibraryButton), | ||
disabled: intl.formatMessage(messages.createLibraryButtonPending), | ||
}} | ||
/> | ||
</Form> | ||
)} | ||
</Formik> | ||
{apiError && ( | ||
<Alert variant="danger" className="mt-3"> | ||
{apiError} | ||
</Alert> | ||
)} | ||
</Container> | ||
<StudioFooter /> | ||
</> | ||
); | ||
}; | ||
|
||
export default CreateLibrary; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
import { camelCaseObject, getConfig } from '@edx/frontend-platform'; | ||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; | ||
|
||
const getApiBaseUrl = () => getConfig().STUDIO_BASE_URL; | ||
|
||
/** | ||
* Get the URL for creating a new library. | ||
*/ | ||
export const getContentLibraryV2CreateApiUrl = () => `${getApiBaseUrl()}/api/libraries/v2/`; | ||
|
||
export interface CreateContentLibraryArgs { | ||
title: string, | ||
org: string, | ||
slug: string, | ||
} | ||
|
||
/** | ||
* Create a new library | ||
*/ | ||
export async function createLibraryV2(data: CreateContentLibraryArgs) { | ||
bradenmacdonald marked this conversation as resolved.
Show resolved
Hide resolved
|
||
const client = getAuthenticatedHttpClient(); | ||
const url = getContentLibraryV2CreateApiUrl(); | ||
|
||
// Description field cannot be null, but we don't have a input in the form for it | ||
const { data: newLibrary } = await client.post(url, { description: '', ...data }); | ||
|
||
return camelCaseObject(newLibrary); | ||
} |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You don't need to create your own
apiError
state. When you calluseCreateLibraryV2()
below, it returns anerror
andisError
state variables that you can use. It also returns anisLoading
which you can use to disable the UI and display a spinner while the API call happens. See https://tanstack.com/query/v4/docs/framework/react/reference/useMutationThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you @bradenmacdonald!
Didn't think this way the first time. I liked it: 9a23204