From 6ce83d0fb7c73608d75148dbdcba2df179aab1fe Mon Sep 17 00:00:00 2001 From: Rebecca Tinchon <58392030+WolfyWin@users.noreply.github.com> Date: Wed, 4 Sep 2024 10:39:15 +0200 Subject: [PATCH] [Training] Improved cursus (#2834) * New editor cursus * archive and restore actions * Creation types for trainings * hidden archived session and event * PR review * pr review 2 --- .../cursus/Controller/CourseController.php | 93 ++++++++++- src/plugin/cursus/Entity/Course.php | 2 + src/plugin/cursus/Finder/EventFinder.php | 1 + src/plugin/cursus/Finder/SessionFinder.php | 1 + .../Migrations/Version20240821130015.php | 30 ++++ .../modules/actions/course/archive.js | 53 +++++++ .../modules/actions/course/restore.js | 34 ++++ .../modules/course/components/about.jsx | 7 + .../modules/course/components/edit.jsx | 50 ------ .../modules/course/components/empty.jsx | 26 ++-- .../modules/course/components/list.jsx | 11 +- .../modules/course/components/type.jsx | 131 ++++++++++++++++ .../course/editor/components/actions.jsx | 45 ++++++ .../course/editor/components/appearance.jsx | 67 ++++++++ .../course/editor/components/history.jsx | 14 ++ .../modules/course/editor/components/main.jsx | 73 +++++++++ .../course/editor/components/overview.jsx | 75 +++++++++ .../course/editor/components/permissions.jsx | 44 ++++++ .../course/editor/components/registration.jsx | 145 ++++++++++++++++++ .../course/editor/components/workspaces.jsx | 95 ++++++++++++ .../modules/course/editor/containers/main.jsx | 28 ++++ .../modals/creation/components/modal.jsx | 39 +++++ .../modals/creation/containers/modal.jsx | 29 ++++ .../modules/course/modals/creation/index.js | 13 ++ .../course/modals/creation/store/index.js | 7 + .../course/modals/creation/store/reducer.js | 10 ++ .../course/modals/creation/store/selectors.js | 5 + .../Resources/modules/course/store/actions.js | 32 ++-- src/plugin/cursus/Resources/modules/plugin.js | 4 +- .../modules/session/components/list.jsx | 2 +- .../modules/tools/events/components/tool.jsx | 20 ++- .../modules/tools/events/containers/tool.jsx | 1 + .../trainings/catalog/components/list.jsx | 23 ++- .../trainings/catalog/components/main.jsx | 13 +- .../trainings/catalog/containers/main.jsx | 5 +- .../trainings/catalog/store/selectors.js | 1 + .../tools/trainings/components/tool.jsx | 8 +- .../tools/trainings/containers/menu.jsx | 20 --- .../trainings/editor/components/list.jsx | 23 +++ .../trainings/editor/components/main.jsx | 31 ++++ .../trainings/editor/containers/main.jsx | 18 +++ .../tools/trainings/editor/store/index.js | 7 + .../tools/trainings/editor/store/reducer.js | 10 ++ .../tools/trainings/editor/store/selectors.js | 5 + .../Resources/translations/actions.en.json | 13 +- .../Resources/translations/actions.fr.json | 13 +- .../Resources/translations/cursus.en.json | 36 ++++- .../Resources/translations/cursus.fr.json | 37 ++++- .../cursus/Serializer/CourseSerializer.php | 4 +- 49 files changed, 1316 insertions(+), 138 deletions(-) create mode 100644 src/plugin/cursus/Installation/Migrations/Version20240821130015.php create mode 100644 src/plugin/cursus/Resources/modules/actions/course/archive.js create mode 100644 src/plugin/cursus/Resources/modules/actions/course/restore.js delete mode 100644 src/plugin/cursus/Resources/modules/course/components/edit.jsx create mode 100644 src/plugin/cursus/Resources/modules/course/components/type.jsx create mode 100644 src/plugin/cursus/Resources/modules/course/editor/components/actions.jsx create mode 100644 src/plugin/cursus/Resources/modules/course/editor/components/appearance.jsx create mode 100644 src/plugin/cursus/Resources/modules/course/editor/components/history.jsx create mode 100644 src/plugin/cursus/Resources/modules/course/editor/components/main.jsx create mode 100644 src/plugin/cursus/Resources/modules/course/editor/components/overview.jsx create mode 100644 src/plugin/cursus/Resources/modules/course/editor/components/permissions.jsx create mode 100644 src/plugin/cursus/Resources/modules/course/editor/components/registration.jsx create mode 100644 src/plugin/cursus/Resources/modules/course/editor/components/workspaces.jsx create mode 100644 src/plugin/cursus/Resources/modules/course/editor/containers/main.jsx create mode 100644 src/plugin/cursus/Resources/modules/course/modals/creation/components/modal.jsx create mode 100644 src/plugin/cursus/Resources/modules/course/modals/creation/containers/modal.jsx create mode 100644 src/plugin/cursus/Resources/modules/course/modals/creation/index.js create mode 100644 src/plugin/cursus/Resources/modules/course/modals/creation/store/index.js create mode 100644 src/plugin/cursus/Resources/modules/course/modals/creation/store/reducer.js create mode 100644 src/plugin/cursus/Resources/modules/course/modals/creation/store/selectors.js delete mode 100644 src/plugin/cursus/Resources/modules/tools/trainings/containers/menu.jsx create mode 100644 src/plugin/cursus/Resources/modules/tools/trainings/editor/components/list.jsx create mode 100644 src/plugin/cursus/Resources/modules/tools/trainings/editor/components/main.jsx create mode 100644 src/plugin/cursus/Resources/modules/tools/trainings/editor/containers/main.jsx create mode 100644 src/plugin/cursus/Resources/modules/tools/trainings/editor/store/index.js create mode 100644 src/plugin/cursus/Resources/modules/tools/trainings/editor/store/reducer.js create mode 100644 src/plugin/cursus/Resources/modules/tools/trainings/editor/store/selectors.js diff --git a/src/plugin/cursus/Controller/CourseController.php b/src/plugin/cursus/Controller/CourseController.php index f793e62f6a6..5a3d7bb69b2 100644 --- a/src/plugin/cursus/Controller/CourseController.php +++ b/src/plugin/cursus/Controller/CourseController.php @@ -55,7 +55,7 @@ public function __construct( RoutingHelper $routing, ToolManager $toolManager, CourseManager $manager, - PdfManager $pdfManager + PdfManager $pdfManager, ) { $this->authorization = $authorization; $this->tokenStorage = $tokenStorage; @@ -106,6 +106,8 @@ protected function getDefaultHiddenFilters(): array } } + $filters['archived'] = false; + return $filters; } @@ -114,14 +116,89 @@ protected function getDefaultHiddenFilters(): array */ public function listPublicAction(Request $request): JsonResponse { - $params = $request->query->all(); - $params['hiddenFilters'] = [ - 'public' => true, - ]; + return new JsonResponse($this->crud->list( + Course::class, + array_merge($request->query->all(), ['hiddenFilters' => array_merge($this->getDefaultHiddenFilters(), [ + 'public' => true, + ])]), + $this->getOptions()['list'] + )); + } - return new JsonResponse( - $this->crud->list(Course::class, $params) - ); + /** + * @Route("/list/archived", name="apiv2_cursus_course_list_archived", methods={"GET"}) + */ + public function listArchivedAction(Request $request): JsonResponse + { + $this->checkPermission('IS_AUTHENTICATED_FULLY', null, [], true); + + return new JsonResponse($this->crud->list( + Course::class, + array_merge($request->query->all(), ['hiddenFilters' => array_merge($this->getDefaultHiddenFilters(), [ + 'archived' => true, + ])]), + $this->getOptions()['list'] + )); + } + + /** + * @Route("/archive", name="apiv2_cursus_course_archive", methods={"POST"}) + */ + public function archiveAction(Request $request): JsonResponse + { + $processed = []; + + $this->om->startFlushSuite(); + + $data = json_decode($request->getContent(), true); + + /** @var Course[] $courses */ + $courses = $this->om->getRepository(Course::class)->findBy([ + 'uuid' => $data['ids'], + ]); + + foreach ($courses as $course) { + if ($this->authorization->isGranted('ADMINISTRATE', $course) && !$course->isArchived()) { + $course->setArchived(true); + $processed[] = $course; + } + } + + $this->om->endFlushSuite(); + + return new JsonResponse(array_map(function (Course $course) { + return $this->serializer->serialize($course); + }, $processed)); + } + + /** + * @Route("/restore", name="apiv2_cursus_course_restore", methods={"POST"}) + */ + public function restoreAction(Request $request): JsonResponse + { + $processed = []; + + $this->om->startFlushSuite(); + + $data = json_decode($request->getContent(), true); + + /** @var Course[] $courses */ + $courses = $this->om->getRepository(Course::class)->findBy([ + 'uuid' => $data['ids'], + ]); + + foreach ($courses as $course) { + if ($this->authorization->isGranted('ADMINISTRATE', $course) && $course->isArchived()) { + $course->setArchived(false); + $processed[] = $course; + } + } + + $this->om->endFlushSuite(); + + return new JsonResponse(array_map(function (Course $course) { + return $this->serializer->serialize($course); + }, $processed)); } /** diff --git a/src/plugin/cursus/Entity/Course.php b/src/plugin/cursus/Entity/Course.php index 05dbcec7aa3..eb508ff8100 100644 --- a/src/plugin/cursus/Entity/Course.php +++ b/src/plugin/cursus/Entity/Course.php @@ -11,6 +11,7 @@ namespace Claroline\CursusBundle\Entity; +use Claroline\AppBundle\Entity\Meta\Archived; use Claroline\AppBundle\Entity\Meta\IsPublic; use Claroline\CommunityBundle\Model\HasOrganizations; use Claroline\CoreBundle\Entity\Facet\PanelFacet; @@ -31,6 +32,7 @@ class Course extends AbstractTraining { use HasOrganizations; use IsPublic; + use Archived; /** * @Gedmo\Slug(fields={"name"}) diff --git a/src/plugin/cursus/Finder/EventFinder.php b/src/plugin/cursus/Finder/EventFinder.php index 15c71007d7a..23d35423d9a 100644 --- a/src/plugin/cursus/Finder/EventFinder.php +++ b/src/plugin/cursus/Finder/EventFinder.php @@ -27,6 +27,7 @@ public function configureQueryBuilder(QueryBuilder $qb, array $searches = [], ar $qb->join('obj.plannedObject', 'po'); $qb->join('obj.session', 's'); $qb->join('s.course', 'c'); + $qb->andWhere('c.archived = 0'); foreach ($searches as $filterName => $filterValue) { switch ($filterName) { diff --git a/src/plugin/cursus/Finder/SessionFinder.php b/src/plugin/cursus/Finder/SessionFinder.php index 9450f918c36..20eba1a3eb5 100644 --- a/src/plugin/cursus/Finder/SessionFinder.php +++ b/src/plugin/cursus/Finder/SessionFinder.php @@ -27,6 +27,7 @@ public static function getClass(): string public function configureQueryBuilder(QueryBuilder $qb, array $searches = [], array $sortBy = null, ?int $page = 0, ?int $limit = -1): QueryBuilder { $qb->join('obj.course', 'c'); + $qb->andWhere('c.archived = 0'); foreach ($searches as $filterName => $filterValue) { switch ($filterName) { diff --git a/src/plugin/cursus/Installation/Migrations/Version20240821130015.php b/src/plugin/cursus/Installation/Migrations/Version20240821130015.php new file mode 100644 index 00000000000..ffab3c787db --- /dev/null +++ b/src/plugin/cursus/Installation/Migrations/Version20240821130015.php @@ -0,0 +1,30 @@ +addSql(' + ALTER TABLE claro_cursusbundle_course + ADD archived TINYINT(1) NOT NULL + '); + } + + public function down(Schema $schema): void + { + $this->addSql(' + ALTER TABLE claro_cursusbundle_course + DROP archived + '); + } +} diff --git a/src/plugin/cursus/Resources/modules/actions/course/archive.js b/src/plugin/cursus/Resources/modules/actions/course/archive.js new file mode 100644 index 00000000000..893a50d844b --- /dev/null +++ b/src/plugin/cursus/Resources/modules/actions/course/archive.js @@ -0,0 +1,53 @@ +import {createElement} from 'react' +import get from 'lodash/get' + +import {url} from '#/main/app/api' +import {ASYNC_BUTTON} from '#/main/app/buttons' +import {hasPermission} from '#/main/app/security' +import {trans, transChoice} from '#/main/app/intl/translation' + +import {CourseCard} from '#/plugin/cursus/course/components/card' + +/** + * Archive action. + */ +export default (courses, refresher) => { + const processable = courses.filter(course => hasPermission('administrate', course) && !get(course, 'meta.archived')) + + return { + name: 'archive', + type: ASYNC_BUTTON, + icon: 'fa fa-fw fa-box', + label: trans('archive', {}, 'actions'), + displayed: 0 !== processable.length, + dangerous: true, + confirm: { + title: transChoice('archive_training_confirm_title', processable.length, {}, 'actions'), + message: transChoice('archive_training_confirm_message', processable.length, {count: processable.length}, 'actions'), + additional: [ + createElement('div', { + key: 'additional', + className: 'modal-body' + }, processable.map(course => createElement(CourseCard, { + key: course.id, + orientation: 'row', + className: 'mb-2', + size: 'xs', + data: course + }))) + ] + }, + request: { + url: url(['apiv2_cursus_course_archive']), + request: { + method: 'POST', + body: JSON.stringify({ + ids: processable.map(course => course.id) + }) + }, + success: (response) => refresher.update(response) + }, + group: trans('management'), + scope: ['object', 'collection'] + } +} diff --git a/src/plugin/cursus/Resources/modules/actions/course/restore.js b/src/plugin/cursus/Resources/modules/actions/course/restore.js new file mode 100644 index 00000000000..1952600fcda --- /dev/null +++ b/src/plugin/cursus/Resources/modules/actions/course/restore.js @@ -0,0 +1,34 @@ +import get from 'lodash/get' + +import {url} from '#/main/app/api' +import {ASYNC_BUTTON} from '#/main/app/buttons' +import {trans} from '#/main/app/intl/translation' +import {hasPermission} from '#/main/app/security' + +/** + * Restore action. + */ +export default (courses, refresher) => { + const processable = courses.filter(course => hasPermission('administrate', course) && get(course, 'meta.archived')) + + return { + name: 'restore', + type: ASYNC_BUTTON, + icon: 'fa fa-fw fa-box-open', + label: trans('restore', {}, 'actions'), + displayed: 0 !== processable.length, + request: { + url: url(['apiv2_cursus_course_restore']), + request: { + method: 'POST', + body: JSON.stringify({ + ids: processable.map(course => course.id) + }) + }, + success: (response) => refresher.update(response) + }, + group: trans('management'), + scope: ['object', 'collection'], + dangerous: true + } +} diff --git a/src/plugin/cursus/Resources/modules/course/components/about.jsx b/src/plugin/cursus/Resources/modules/course/components/about.jsx index f7ade47f25b..ce2087bd088 100644 --- a/src/plugin/cursus/Resources/modules/course/components/about.jsx +++ b/src/plugin/cursus/Resources/modules/course/components/about.jsx @@ -178,6 +178,13 @@ const CourseAbout = (props) => {
+ + {get(props.course, 'meta.archived') === true && + + {trans('course_archived_info_help', {}, 'cursus')} + + } + {!isEmpty(props.activeSession) &&
diff --git a/src/plugin/cursus/Resources/modules/course/components/edit.jsx b/src/plugin/cursus/Resources/modules/course/components/edit.jsx deleted file mode 100644 index b12b7720397..00000000000 --- a/src/plugin/cursus/Resources/modules/course/components/edit.jsx +++ /dev/null @@ -1,50 +0,0 @@ -import React from 'react' -import {connect} from 'react-redux' -import {PropTypes as T} from 'prop-types' - -import {trans} from '#/main/app/intl/translation' -import {LINK_BUTTON} from '#/main/app/buttons' -import {Course as CourseTypes} from '#/plugin/cursus/prop-types' -import {CoursePage} from '#/plugin/cursus/course/components/page' -import {CourseForm} from '#/plugin/cursus/course/containers/form' - -import {selectors} from '#/plugin/cursus/course/store' -import {selectors as toolSelectors} from '#/main/core/tool/store' -import {selectors as formSelectors} from '#/main/app/content/form/store' - -const CourseEditComponent = (props) => - - - - -CourseEditComponent.propTypes = { - path: T.string.isRequired, - contextType: T.string.isRequired, - course: T.shape( - CourseTypes.propTypes - ) -} - -const CourseEdit = connect( - (state) => ({ - path: toolSelectors.path(state), - course: formSelectors.data(formSelectors.form(state, selectors.FORM_NAME)), - contextType: toolSelectors.contextType(state) - }) -)(CourseEditComponent) - -export { - CourseEdit -} diff --git a/src/plugin/cursus/Resources/modules/course/components/empty.jsx b/src/plugin/cursus/Resources/modules/course/components/empty.jsx index 1a364c7810b..1ca58315aa3 100644 --- a/src/plugin/cursus/Resources/modules/course/components/empty.jsx +++ b/src/plugin/cursus/Resources/modules/course/components/empty.jsx @@ -1,12 +1,14 @@ import React from 'react' import {PropTypes as T} from 'prop-types' -import {trans} from '#/main/app/intl/translation' -import {LINK_BUTTON} from '#/main/app/buttons' import {ToolPage} from '#/main/core/tool' -import {Button} from '#/main/app/action/components/button' +import {MODAL_BUTTON} from '#/main/app/buttons' +import {trans} from '#/main/app/intl/translation' + import {ContentSizing} from '#/main/app/content/components/sizing' +import {CreationType} from '#/plugin/cursus/course/components/type' import {ContentPlaceholder} from '#/main/app/content/components/placeholder' +import {MODAL_COURSE_TYPE_CREATION} from '#/plugin/cursus/course/modals/creation' const EmptyCourse = (props) => actions={[ { name: 'add', - type: LINK_BUTTON, + type: MODAL_BUTTON, icon: 'fa fa-fw fa-plus', label: trans('add_course', {}, 'cursus'), - target: `${props.path}/new`, + modal: [MODAL_COURSE_TYPE_CREATION, { + path: props.path + }], group: trans('management'), displayed: props.canEdit, primary: true @@ -30,14 +34,12 @@ const EmptyCourse = (props) => title={trans('no_course', {}, 'cursus')} help={trans('no_course_help', {}, 'cursus')} /> -