diff --git a/src/course-unit/preview-changes/index.tsx b/src/course-unit/preview-changes/index.tsx index 8e2e0530f..4f74765bc 100644 --- a/src/course-unit/preview-changes/index.tsx +++ b/src/course-unit/preview-changes/index.tsx @@ -4,7 +4,7 @@ import { import { ActionRow, Button, Icon, ModalDialog, useToggle, } from '@openedx/paragon'; -import { Info, Warning } from '@openedx/paragon/icons'; +import { Info } from '@openedx/paragon/icons'; import { useIntl, FormattedMessage } from '@edx/frontend-platform/i18n'; import { ToastContext } from '@src/generic/toast-context'; @@ -150,7 +150,7 @@ export const PreviewLibraryXBlockChanges = ({ return ( - {isTextWithLocalChanges ? ( + {isTextWithLocalChanges && ( - ) : (!blockData.isContainer && ( - - ))} + )} {getBody()} diff --git a/src/course-unit/preview-changes/messages.ts b/src/course-unit/preview-changes/messages.ts index 244b89d15..c1573fd82 100644 --- a/src/course-unit/preview-changes/messages.ts +++ b/src/course-unit/preview-changes/messages.ts @@ -51,11 +51,6 @@ const messages = defineMessages({ defaultMessage: 'Ignore', description: 'Preview changes confirmation dialog confirm button text when user clicks on ignore changes.', }, - olderVersionPreviewAlert: { - id: 'course-authoring.review-tab.preview.old-version-alert', - defaultMessage: 'The old version preview is the previous library version', - description: 'Alert message stating that older version in preview is of library block', - }, localEditsAlert: { id: 'course-authoring.review-tab.preview.loal-edits-alert', defaultMessage: 'This library content has local edits.', @@ -73,7 +68,7 @@ const messages = defineMessages({ }, updateToPublishedLibraryContentBody: { id: 'course-authoring.review-tab.preview.update-to-published.modal.body', - defaultMessage: 'Updating this block will discard local changes. Any eidts made within this course will be discarted, and cannot be recovered', + defaultMessage: 'Updating this block will discard local changes. Any edits made within this course will be discarded, and cannot be recovered', description: 'Body of the modal to update a content to the published library content', }, updateToPublishedLibraryContentConfirm: { @@ -93,7 +88,7 @@ const messages = defineMessages({ }, keepCourseContentBody: { id: 'course-authoring.review-tab.preview.keep-course-content.modal.body', - defaultMessage: 'This will keep the locally edited course content. if the component is published again in its library, you can choose to update to published library content', + defaultMessage: 'This will keep the locally edited course content. If the component is published again in its library, you can choose to update to published library content', description: 'Body of the modal to keep the content of a course component', }, }); diff --git a/src/legacy-libraries-migration/LegacyLibMigrationPage.test.tsx b/src/legacy-libraries-migration/LegacyLibMigrationPage.test.tsx index 17d4cbbbb..5a9a4a149 100644 --- a/src/legacy-libraries-migration/LegacyLibMigrationPage.test.tsx +++ b/src/legacy-libraries-migration/LegacyLibMigrationPage.test.tsx @@ -82,6 +82,7 @@ describe('', () => { }); it('should select legacy libraries', async () => { + const user = userEvent.setup(); renderPage(); expect(await screen.findByText('Migrate Legacy Libraries')).toBeInTheDocument(); @@ -89,6 +90,15 @@ describe('', () => { // The next button is disabled expect(nextButton).toBeDisabled(); + // The filter is Unmigrated by default + const filterButton = await screen.findByRole('button', { name: /unmigrated/i }); + expect(filterButton).toBeInTheDocument(); + + // Clear filter to show all + await user.click(filterButton); + const clearButton = await screen.findByRole('button', { name: /clear filter/i }); + await user.click(clearButton); + expect(await screen.findByText('MBA')).toBeInTheDocument(); expect(await screen.findByText('Legacy library 1')).toBeInTheDocument(); expect(await screen.findByText('MBA 1')).toBeInTheDocument(); @@ -116,6 +126,37 @@ describe('', () => { expect(nextButton).not.toBeDisabled(); }); + it('should select all legacy libraries', async () => { + const user = userEvent.setup(); + renderPage(); + expect(await screen.findByText('Migrate Legacy Libraries')).toBeInTheDocument(); + + // The filter is Unmigrated by default + const filterButton = await screen.findByRole('button', { name: /unmigrated/i }); + expect(filterButton).toBeInTheDocument(); + + // Clear filter to show all + await user.click(filterButton); + const clearButton = await screen.findByRole('button', { name: /clear filter/i }); + await user.click(clearButton); + + const selectAll = screen.getByRole('checkbox', { name: /select all/i }); + await user.click(selectAll); + + const library1 = screen.getByRole('checkbox', { name: 'MBA' }); + const library2 = screen.getByRole('checkbox', { name: /legacy library 1 imported library/i }); + const library3 = screen.getByRole('checkbox', { name: 'MBA 1' }); + + expect(library1).toBeChecked(); + expect(library2).toBeChecked(); + expect(library3).toBeChecked(); + + await user.click(selectAll); + expect(library1).not.toBeChecked(); + expect(library2).not.toBeChecked(); + expect(library3).not.toBeChecked(); + }); + it('should back to select legacy libraries', async () => { renderPage(); expect(await screen.findByText('Migrate Legacy Libraries')).toBeInTheDocument(); @@ -250,10 +291,18 @@ describe('', () => { }); it('should confirm migration', async () => { + const user = userEvent.setup(); renderPage(); expect(await screen.findByText('Migrate Legacy Libraries')).toBeInTheDocument(); expect(await screen.findByText('MBA')).toBeInTheDocument(); + // The filter is 'unmigrated' by default. + // Clear the filter to select all libraries + const filterButton = screen.getByRole('button', { name: /unmigrated/i }); + await user.click(filterButton); + const clearButton = await screen.findByRole('button', { name: /clear filter/i }); + await user.click(clearButton); + const legacyLibrary1 = screen.getByRole('checkbox', { name: 'MBA' }); const legacyLibrary2 = screen.getByRole('checkbox', { name: /legacy library 1 imported library/i }); const legacyLibrary3 = screen.getByRole('checkbox', { name: 'MBA 1' }); diff --git a/src/legacy-libraries-migration/LegacyLibMigrationPage.tsx b/src/legacy-libraries-migration/LegacyLibMigrationPage.tsx index c24504d5f..0d8a7a7a9 100644 --- a/src/legacy-libraries-migration/LegacyLibMigrationPage.tsx +++ b/src/legacy-libraries-migration/LegacyLibMigrationPage.tsx @@ -16,7 +16,7 @@ import Header from '@src/header'; import SubHeader from '@src/generic/sub-header/SubHeader'; import type { ContentLibrary } from '@src/library-authoring/data/api'; import type { LibraryV1Data } from '@src/studio-home/data/api'; -import LibrariesList from '@src/studio-home/tabs-section/libraries-tab'; +import { Filter, LibrariesList } from '@src/studio-home/tabs-section/libraries-tab'; import messages from './messages'; import { SelectDestinationView } from './SelectDestinationView'; @@ -157,6 +157,8 @@ export const LegacyLibMigrationPage = () => { selectedIds={legacyLibrariesIds} handleCheck={handleUpdateLegacyLibraries} hideMigationAlert + initialFilter={[Filter.unmigrated]} + setSelectedLibraries={setLegacyLibraries} /> { -
- - {currentStep !== 'confirmation-view' ? ( - - ) : ( - - )} -
+
+ + {currentStep !== 'confirmation-view' ? ( + + ) : ( + + )} +
void; legacyLibCount: number; } diff --git a/src/legacy-libraries-migration/index.scss b/src/legacy-libraries-migration/index.scss index 9af18213a..92d63cb7a 100644 --- a/src/legacy-libraries-migration/index.scss +++ b/src/legacy-libraries-migration/index.scss @@ -1,8 +1,5 @@ .legacy-library-migration-page { .migration-container { - // Calculate all the screen size subtracting the height of the header and top/bottom margins - min-height: calc(calc(100vh - 60px) - calc(var(--pgn-spacing-spacer-base) * 6)); - .courses-tab-container { min-height: auto; } @@ -10,6 +7,14 @@ .migration-content { flex: 1; } + + .card-item { + margin: 0 0 16px !important; + + &.selected { + box-shadow: 0 0 0 2px var(--pgn-color-primary-700); + } + } } .confirmation-view { @@ -17,4 +22,10 @@ margin-top: calc(var(--pgn-spacing-spacer-base)) !important;; } } + + .content-buttons { + width: 100%; + position: fixed; + bottom: 0; + } } diff --git a/src/library-authoring/component-comparison/CompareChangesWidget.tsx b/src/library-authoring/component-comparison/CompareChangesWidget.tsx index 26b686efc..e7dd66eb1 100644 --- a/src/library-authoring/component-comparison/CompareChangesWidget.tsx +++ b/src/library-authoring/component-comparison/CompareChangesWidget.tsx @@ -52,7 +52,7 @@ const CompareChangesWidget = ({ {oldTitle} )} -
+
= ({ isLibraries = false, courseKey = '', selectMode, + isSelected = false, itemId = '', path, url, @@ -174,6 +178,7 @@ const CardItem: React.FC = ({ migratedToKey, migratedToTitle, migratedToCollectionKey, + scrollIntoView = false, }) => { const intl = useIntl(); const { @@ -182,6 +187,7 @@ const CardItem: React.FC = ({ rerunCreatorStatus, } = useSelector(getStudioHomeData); const waffleFlags = useWaffleFlags(); + const cardRef = useRef(null); const destinationUrl: string = path ?? ( waffleFlags.useNewCourseOutlinePage && !isLibraries @@ -217,66 +223,78 @@ const CardItem: React.FC = ({ return libUrl; }; + useEffect(() => { + /* istanbul ignore next */ + if (scrollIntoView && cardRef.current && 'scrollIntoView' in cardRef.current) { + cardRef.current.scrollIntoView({ behavior: 'smooth' }); + } + }, [scrollIntoView]); + return ( - - - )} - subtitle={getSubtitle()} - actions={showActions && ( - - - - {isShowRerunLink && ( - - {messages.btnReRunText.defaultMessage} +
+ + + )} + subtitle={getSubtitle()} + actions={showActions && ( + + + + {isShowRerunLink && ( + + {messages.btnReRunText.defaultMessage} + + )} + + {intl.formatMessage(messages.viewLiveBtnText)} - )} - - {intl.formatMessage(messages.viewLiveBtnText)} - - - - )} - /> - {isMigrated && migratedToKey - && ( - - - - {intl.formatMessage(messages.libraryMigrationStatusText)} - - - {migratedToTitle} - - - - - )} - + + + )} + /> + {isMigrated && migratedToKey + && ( + + + + {intl.formatMessage(messages.libraryMigrationStatusText)} + + + {migratedToTitle} + + + + + )} + +
); }; diff --git a/src/studio-home/tabs-section/index.tsx b/src/studio-home/tabs-section/index.tsx index 9ada160ff..4dd575065 100644 --- a/src/studio-home/tabs-section/index.tsx +++ b/src/studio-home/tabs-section/index.tsx @@ -14,7 +14,7 @@ import { useNavigate, useLocation } from 'react-router-dom'; import { RequestStatus } from '@src/data/constants'; import { getLoadingStatuses, getStudioHomeData } from '../data/selectors'; import messages from './messages'; -import LibrariesList from './libraries-tab'; +import { LibrariesList } from './libraries-tab'; import LibrariesV2List from './libraries-v2-tab/index'; import CoursesTab from './courses-tab'; import { WelcomeLibrariesV2Alert } from './libraries-v2-tab/WelcomeLibrariesV2Alert'; diff --git a/src/studio-home/tabs-section/libraries-tab/index.tsx b/src/studio-home/tabs-section/libraries-tab/index.tsx index 952946bb9..856ca24b5 100644 --- a/src/studio-home/tabs-section/libraries-tab/index.tsx +++ b/src/studio-home/tabs-section/libraries-tab/index.tsx @@ -1,5 +1,5 @@ import { useCallback, useState } from 'react'; -import { useIntl } from '@edx/frontend-platform/i18n'; +import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n'; import { ActionRow, Form, Icon, Menu, MenuItem, Pagination, Row, SearchField, } from '@openedx/paragon'; @@ -19,9 +19,11 @@ import { MigrateLegacyLibrariesAlert } from './MigrateLegacyLibrariesAlert'; const CardList = ({ data, inSelectMode, + selectedIds, }: { data: LibraryV1Data[], inSelectMode: boolean, + selectedIds?: string[]; }) => ( // eslint-disable-next-line react/jsx-no-useless-fragment <> @@ -46,6 +48,7 @@ const CardList = ({ url={url} itemId={libraryKey} selectMode={inSelectMode ? 'multiple' : undefined} + isSelected={selectedIds?.includes(libraryKey)} isMigrated={isMigrated} migratedToKey={migratedToKey} migratedToTitle={migratedToTitle} @@ -62,7 +65,7 @@ function findInValues(arr: T[] | undefined, searchValue: string) { ))); } -enum Filter { +export enum Filter { migrated = 'migrated', unmigrated = 'unmigrated', } @@ -141,19 +144,23 @@ const MigrationFilter = ({ filters, setFilters }: MigrationFilterProps) => { interface LibrariesListProps { selectedIds?: string[]; handleCheck?: (library: LibraryV1Data, action: 'add' | 'remove') => void; + setSelectedLibraries?: (libraries: LibraryV1Data[]) => void; hideMigationAlert?: boolean; + initialFilter?: Filter[]; } -const LibrariesList = ({ +export const LibrariesList = ({ selectedIds, handleCheck, + setSelectedLibraries, hideMigationAlert = false, + initialFilter = BaseFilterState, }: LibrariesListProps) => { const intl = useIntl(); const { isPending, data, isError } = useLibrariesV1Data(); const [currentPage, setCurrentPage] = useState(1); const [search, setSearch] = useState(''); - const [migrationFilter, setMigrationFilter] = useState(BaseFilterState); + const [migrationFilter, setMigrationFilter] = useState(initialFilter); let filteredData = findInValues(data?.libraries, search || '') || []; if (migrationFilter.length === 1) { @@ -165,6 +172,10 @@ const LibrariesList = ({ const currentPageData = filteredData.slice((currentPage - 1) * perPage, currentPage * perPage); const inSelectMode = handleCheck !== undefined; + const allChecked = filteredData.every(value => selectedIds?.includes(value.libraryKey)); + const someChecked = filteredData.some(value => selectedIds?.includes(value.libraryKey)); + const checkboxIsIndeterminate = someChecked && !allChecked; + const handleChangeCheckboxSet = useCallback((event) => { if (handleCheck) { const libraryId = event.target.value; @@ -179,6 +190,14 @@ const LibrariesList = ({ } }, [handleCheck, currentPageData]); + const handleSelectAll = useCallback(() => { + if (checkboxIsIndeterminate || selectedIds?.length === 0) { + setSelectedLibraries?.(filteredData); + } else { + setSelectedLibraries?.([]); + } + }, [checkboxIsIndeterminate, selectedIds, filteredData]); + if (isPending) { return ( @@ -206,6 +225,16 @@ const LibrariesList = ({ {!hideMigationAlert && getConfig().ENABLE_LEGACY_LIBRARY_MIGRATOR === 'true' && ()}
+ {inSelectMode && ( + + + + )} {}} @@ -235,6 +264,7 @@ const LibrariesList = ({ ) : ( @@ -259,5 +289,3 @@ const LibrariesList = ({ ); }; - -export default LibrariesList; diff --git a/src/studio-home/tabs-section/libraries-v2-tab/index.tsx b/src/studio-home/tabs-section/libraries-v2-tab/index.tsx index dd348139b..f09e9115b 100644 --- a/src/studio-home/tabs-section/libraries-v2-tab/index.tsx +++ b/src/studio-home/tabs-section/libraries-v2-tab/index.tsx @@ -24,19 +24,23 @@ import LibrariesV2Filters from './libraries-v2-filters'; interface CardListProps { hasV2Libraries: boolean; selectMode?: 'single' | 'multiple'; + selectedLibraryId?: string; isFiltered: boolean; isLoading: boolean; data: LibrariesV2Response; handleClearFilters: () => void; + scrollIntoView?: boolean; } const CardList: React.FC = ({ hasV2Libraries, selectMode, + selectedLibraryId, isFiltered, isLoading, data, handleClearFilters, + scrollIntoView = false, }) => { if (hasV2Libraries) { return ( @@ -53,7 +57,9 @@ const CardList: React.FC = ({ number={slug} path={`/library/${id}`} selectMode={selectMode} + isSelected={selectedLibraryId === id} itemId={id} + scrollIntoView={scrollIntoView && selectedLibraryId === id} /> )) } @@ -96,6 +102,7 @@ const LibrariesV2List: React.FC = ({ const [currentPage, setCurrentPage] = useState(1); const [filterParams, setFilterParams] = useState({}); const [isCreateLibraryOpen, openCreateLibrary, closeCreateLibrary] = useToggle(false); + const [scrollIntoCard, setScrollIntoCard] = useState(false); const isFiltered = Object.keys(filterParams).length > 0; const inSelectMode = handleSelect !== undefined; @@ -119,17 +126,19 @@ const LibrariesV2List: React.FC = ({ if (handleSelect) { handleSelect(library); closeCreateLibrary(); + setScrollIntoCard(true); } - }, [handleSelect, closeCreateLibrary]); + }, [handleSelect, closeCreateLibrary, setScrollIntoCard]); const handleOnChangeRadioSet = useCallback((libraryId: string) => { + setScrollIntoCard(false); if (handleSelect && data) { const library = data.results.find((item) => item.id === libraryId); if (library) { handleSelect(library); } } - }, [data, handleSelect]); + }, [data, handleSelect, setScrollIntoCard]); if (isPending && !isFiltered) { return ( @@ -194,16 +203,17 @@ const LibrariesV2List: React.FC = ({ ) : (