diff --git a/.editorconfig b/.editorconfig index e2aa47d23..97f482d6c 100644 --- a/.editorconfig +++ b/.editorconfig @@ -34,6 +34,10 @@ trim_trailing_whitespace = false indent_style = space indent_size = 4 +[*.scss] +indent_style = space +indent_size = 2 + [*.sh] indent_style = tab indent_size = 4 diff --git a/assets/js/app/editor/Components/Collection.vue b/assets/js/app/editor/Components/Collection.vue index d69b700dc..a8d0e85d2 100644 --- a/assets/js/app/editor/Components/Collection.vue +++ b/assets/js/app/editor/Components/Collection.vue @@ -15,25 +15,28 @@ -
-
- -
- - -
- - {{ element.label }} -
- - -
+
+
+
+ + +
+ + {{ element.label }}
-
- + +
+
+ +
- +
@@ -145,8 +148,16 @@ export default { * This is a jQuery event listener, because Vue cannot handle an event emitted by a non-vue element. * The collection items are not Vue elements in order to initialise them correctly within their twig template. */ + window + .$(document) + .on('click', vueThis.selector.collectionContainer + ' .collection-item .summary', function(e) { + e.preventDefault(); + let thisCollectionItem = vueThis.getCollectionItemFromPressedButton(this); + thisCollectionItem.toggleClass('collapsed'); + }); window.$(document).on('click', vueThis.selector.collectionContainer + vueThis.selector.remove, function(e) { e.preventDefault(); + e.stopPropagation(); let collectionContainer = window.$(this).closest(vueThis.selector.collectionContainer); vueThis.getCollectionItemFromPressedButton(this).remove(); vueThis.setAllButtonsStates(collectionContainer); @@ -154,6 +165,7 @@ export default { }); window.$(document).on('click', vueThis.selector.collectionContainer + vueThis.selector.moveUp, function(e) { e.preventDefault(); + e.stopPropagation(); let thisCollectionItem = vueThis.getCollectionItemFromPressedButton(this); let prevCollectionitem = vueThis.getPreviousCollectionItem(thisCollectionItem); window.$(thisCollectionItem).after(prevCollectionitem); @@ -162,6 +174,7 @@ export default { }); window.$(document).on('click', vueThis.selector.collectionContainer + vueThis.selector.moveDown, function(e) { e.preventDefault(); + e.stopPropagation(); let thisCollectionItem = vueThis.getCollectionItemFromPressedButton(this); let nextCollectionItem = vueThis.getNextCollectionItem(thisCollectionItem); window.$(thisCollectionItem).before(nextCollectionItem); @@ -171,14 +184,14 @@ export default { window.$(document).on('click', vueThis.selector.collectionContainer + vueThis.selector.expandAll, function(e) { e.preventDefault(); const collection = $(e.target).closest(vueThis.selector.collectionContainer); - collection.find('details').attr('open', ''); + collection.find('.collection-item').removeClass('collapsed'); }); window .$(document) .on('click', vueThis.selector.collectionContainer + vueThis.selector.collapseAll, function(e) { e.preventDefault(); const collection = $(e.target).closest(vueThis.selector.collectionContainer); - collection.find('details').removeAttr('open'); + collection.find('.collection-item').addClass('collapsed'); }); /** * Update the title dynamically. @@ -214,14 +227,14 @@ export default { /** * Open newly inserted collection items. */ - $(document).on('DOMNodeInserted', function(e) { - if ($(e.target).hasClass('collection-item')) { - $(e.target) - .find('details') - .first() - .attr('open', ''); - } - }); + // $(document).on('DOMNodeInserted', function(e) { + // if ($(e.target).hasClass('collection-item')) { + // $(e.target) + // .find('details') + // .first() + // .attr('open', ''); + // } + // }); }, updated() { this.setAllButtonsStates(window.$(this.$refs.collectionContainer)); diff --git a/assets/js/app/editor/Components/Select.vue b/assets/js/app/editor/Components/Select.vue index bec7c56e7..1a2e9fa14 100644 --- a/assets/js/app/editor/Components/Select.vue +++ b/assets/js/app/editor/Components/Select.vue @@ -87,7 +87,7 @@ export default { if (this.selected === null) { return JSON.stringify([]); - } else if (this.selected.map) { + } else if (this.selected.length > 0) { filtered = this.selected.map(item => item.key); return JSON.stringify(filtered); } else { diff --git a/assets/scss/init/_base.scss b/assets/scss/init/_base.scss index 2e13fb101..000674dd4 100644 --- a/assets/scss/init/_base.scss +++ b/assets/scss/init/_base.scss @@ -60,11 +60,6 @@ small { a:not(.btn):not(.dropdown-item) { text-decoration: underline; } - - ul, - ol { - padding-left: 1rem; - } } .custom-select { @@ -85,3 +80,9 @@ input.flatpickr-input[disabled] { input::placeholder { font-style: italic; } + +.field-error { + color: $danger; + margin-top: -1.5rem; + margin-bottom: 2rem; +} diff --git a/assets/scss/modules/admin/_widgets.scss b/assets/scss/modules/admin/_widgets.scss index 2d3349448..f7dec324b 100644 --- a/assets/scss/modules/admin/_widgets.scss +++ b/assets/scss/modules/admin/_widgets.scss @@ -1,3 +1,8 @@ .widget { margin-bottom: $spacer*1.625; + + ul, + ol { + padding-left: 1rem; + } } diff --git a/assets/scss/modules/editor/fields/_collection.scss b/assets/scss/modules/editor/fields/_collection.scss index 1bd2de0e2..c4c21128e 100644 --- a/assets/scss/modules/editor/fields/_collection.scss +++ b/assets/scss/modules/editor/fields/_collection.scss @@ -14,9 +14,10 @@ .collection-item { margin-bottom: 1rem; - summary { + .summary { padding: 0; background: transparent; + cursor: pointer; &::-webkit-details-marker { display: none; @@ -28,11 +29,14 @@ i.card-marker-caret { margin-right: 0.75rem; + transform: rotate(90deg); } } - details[open] summary i.card-marker-caret { - transform: rotate(90deg); + &.collapsed { + .summary i.card-marker-caret { + transform: rotate(0); + } } .collection-item-title { @@ -41,6 +45,11 @@ text-overflow: ellipsis; white-space: nowrap; overflow: hidden; + user-select: none; + } + + &.collapsed .details { + display: none; } .card-body { diff --git a/config/bolt/config.yaml b/config/bolt/config.yaml index 461e810e6..cfc8bf572 100644 --- a/config/bolt/config.yaml +++ b/config/bolt/config.yaml @@ -193,3 +193,9 @@ fixtures_seed: 87654 # Globally enable / disable the validator for Fields validator_options: enable: true + +# Options for user's avatar +user_avatar: + upload_path: avatars + extensions_allowed: ['png', 'jpeg', 'jpg', 'gif'] + default_avatar: '' # Put path of file locate in files directory diff --git a/config/services.yaml b/config/services.yaml index 64ca51b6a..6f5c52af6 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -67,6 +67,10 @@ services: tags: - { name: doctrine.event_listener, event: postLoad } + Bolt\Event\Listener\UserAvatarLoadListener: + tags: + - { name: doctrine.event_listener, event: postLoad } + Bolt\Extension\RoutesLoader: tags: [routing.loader] diff --git a/src/Controller/Backend/ProfileController.php b/src/Controller/Backend/ProfileController.php deleted file mode 100644 index dc093dfa6..000000000 --- a/src/Controller/Backend/ProfileController.php +++ /dev/null @@ -1,117 +0,0 @@ -em = $em; - $this->passwordEncoder = $passwordEncoder; - $this->csrfTokenManager = $csrfTokenManager; - } - - /** - * @Route("/profile-edit", methods={"GET"}, name="bolt_profile_edit") - */ - public function edit(): Response - { - $user = $this->getUser(); - - return $this->render('@bolt/users/profile.html.twig', [ - 'display_name' => $user->getDisplayName(), - 'user' => $user, - ]); - } - - /** - * @Route("/profile-edit", methods={"POST"}, name="bolt_profile_edit_post") - */ - public function save(ValidatorInterface $validator): Response - { - $this->validateCsrf('profileedit'); - - /** @var User $user */ - $user = $this->getUser(); - $displayName = $user->getDisplayName(); - $locale = Json::findScalar($this->getFromRequest('locale')); - - $user->setDisplayName((string) $this->getFromRequest('displayName')); - $user->setEmail((string) $this->getFromRequest('email')); - $user->setLocale($locale); - $user->setbackendTheme((string) $this->getFromRequest('backendTheme')); - $newPassword = (string) $this->getFromRequest('password'); - - // Set the plain password to check for validation - if (! empty($newPassword)) { - $user->setPlainPassword($newPassword); - } - - $errors = $validator->validate($user); - - if ($errors->count() > 0) { - $hasPasswordError = false; - - /** @var ConstraintViolationInterface $error */ - foreach ($errors as $error) { - $this->addFlash('danger', $error->getMessage()); - - if ($error->getPropertyPath() === 'plainPassword') { - $hasPasswordError = true; - } - } - - $suggestedPassword = $hasPasswordError ? Str::generatePassword() : null; - - return $this->render('@bolt/users/profile.html.twig', [ - 'display_name' => $displayName, - 'userEdit' => $user, - 'suggestedPassword' => $suggestedPassword, - ]); - } - - // Once validated, encode the password - if ($user->getPlainPassword()) { - $user->setPassword($this->passwordEncoder->encodePassword($user, $user->getPlainPassword())); - $user->eraseCredentials(); - } - - $this->em->flush(); - - $this->request->getSession()->set('_locale', $locale); - - $this->addFlash('success', 'user.updated_profile'); - - return $this->redirectToRoute('bolt_profile_edit'); - } -} diff --git a/src/Controller/Backend/UserEditController.php b/src/Controller/Backend/UserEditController.php index 2f9729471..faa87382a 100644 --- a/src/Controller/Backend/UserEditController.php +++ b/src/Controller/Backend/UserEditController.php @@ -4,29 +4,26 @@ namespace Bolt\Controller\Backend; -use Bolt\Common\Json; use Bolt\Common\Str; use Bolt\Controller\CsrfTrait; use Bolt\Controller\TwigAwareController; use Bolt\Entity\User; use Bolt\Enum\UserStatus; use Bolt\Event\UserEvent; +use Bolt\Form\UserType; use Bolt\Repository\UserRepository; use Doctrine\ORM\EntityManagerInterface; use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security; +use Symfony\Component\Form\FormInterface; use Symfony\Component\HttpFoundation\RedirectResponse; +use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface; use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface; -use Symfony\Component\Validator\ConstraintViolationInterface; -use Symfony\Component\Validator\Validator\ValidatorInterface; use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; -/** - * @Security("is_granted('ROLE_ADMIN')") - */ class UserEditController extends TwigAwareController implements BackendZoneInterface { use CsrfTrait; @@ -43,49 +40,153 @@ class UserEditController extends TwigAwareController implements BackendZoneInter /** @var EventDispatcherInterface */ private $dispatcher; + protected $defaultLocale; + public function __construct( UrlGeneratorInterface $urlGenerator, EntityManagerInterface $em, UserPasswordEncoderInterface $passwordEncoder, CsrfTokenManagerInterface $csrfTokenManager, - EventDispatcherInterface $dispatcher + EventDispatcherInterface $dispatcher, + string $defaultLocale ) { $this->urlGenerator = $urlGenerator; $this->em = $em; $this->passwordEncoder = $passwordEncoder; $this->csrfTokenManager = $csrfTokenManager; $this->dispatcher = $dispatcher; + $this->defaultLocale = $defaultLocale; } /** - * @Route("/user-edit/{id}", methods={"GET"}, name="bolt_user_edit", requirements={"id": "\d+"}) + * @Route("/user-edit/add", methods={"GET","POST"}, name="bolt_user_add") + * @Security("is_granted('ROLE_ADMIN')") */ - public function edit(?User $user): Response + public function add(Request $request): Response { - if (! $user instanceof User) { - $user = UserRepository::factory(); - $suggestedPassword = Str::generatePassword(); - } else { - $suggestedPassword = ''; + $user = UserRepository::factory(); + $submitted_data = $request->request->get('user'); + + $event = new UserEvent($user); + $this->dispatcher->dispatch($event, UserEvent::ON_ADD); + + $roles = array_merge($this->getParameter('security.role_hierarchy.roles'), $event->getRoleOptions()->toArray()); + + // These are the variables we have to pass into our FormType so we can build the fields correctly + $form_data = [ + 'suggested_password' => Str::generatePassword(), + 'roles' => $roles, + 'require_username' => true, + 'require_password' => true, + 'default_locale' => $this->defaultLocale, + ]; + $form = $this->createForm(UserType::class, $user, $form_data); + + // ON SUBMIT + if (! empty($submitted_data)) { + // We need to transform to JSON.stringify value for the field "roles" into + // an array so symfony forms validation works + $submitted_data['roles'] = json_decode($submitted_data['roles']); + + $submitted_data['locale'] = json_decode($submitted_data['locale'])[0]; + $submitted_data['status'] = json_decode($submitted_data['status'])[0]; + + // Transform media array to keep only filepath + $submitted_data['avatar'] = $submitted_data['avatar']['filename']; + + $form->submit($submitted_data); + } + + if ($form->isSubmitted() && $form->isValid()) { + $this->_handleValidFormSubmit($form); + + return $this->redirectToRoute('bolt_users'); + } + + return $this->render('@bolt/users/add.html.twig', [ + 'userForm' => $form->createView(), + ]); + } + + /** + * @Route("/user-edit/{id}", methods={"GET","POST"}, name="bolt_user_edit", requirements={"id": "\d+"}) + * @Route("/profile-edit", methods={"GET","POST"}, name="bolt_profile_edit") + */ + public function edit(?User $user, Request $request): Response + { + $submitted_data = $request->request->get('user'); + $is_profile_edit = false; + + // $user is null on /profile-edit but not on /user-edit/ + if ($user === null) { + $user = $this->getUser(); + $is_profile_edit = true; } $event = new UserEvent($user); $this->dispatcher->dispatch($event, UserEvent::ON_EDIT); $roles = array_merge($this->getParameter('security.role_hierarchy.roles'), $event->getRoleOptions()->toArray()); - $statuses = UserStatus::all(); - return $this->render('@bolt/users/edit.html.twig', [ - 'display_name' => $user->getDisplayName(), - 'userEdit' => $user, + // We don't require the user to set the password again on the "user edit" form + // If it is otherwise set use the given password normally + $require_password = false; + if (! empty($submitted_data['plainPassword'])) { + $require_password = true; + } + + // These are the variables we have to pass into our FormType so we can build the fields correctly + $form_data = [ + 'suggested_password' => Str::generatePassword(), 'roles' => $roles, - 'suggestedPassword' => $suggestedPassword, - 'statuses' => $statuses, + 'require_username' => false, + 'require_password' => $require_password, + 'default_locale' => $this->defaultLocale, + 'is_profile_edit' => $is_profile_edit, + ]; + $form = $this->createForm(UserType::class, $user, $form_data); + + // ON SUBMIT + if (! empty($submitted_data)) { + // Since the username is disabled on edit form we need to set it here so Symfony Forms doesn't throw an error + $submitted_data['username'] = $user->getUsername(); + + $submitted_data['locale'] = json_decode($submitted_data['locale'])[0]; + + // Status is not available for profile edit on non admin users + if (! empty($submitted_data['status'])) { + $submitted_data['status'] = json_decode($submitted_data['status'])[0]; + } + + // Roles is not available for profile edit on non admin users + if (! empty($submitted_data['roles'])) { + // We need to transform to JSON.stringify value for the field "roles" into + // an array so symfony forms validation works + $submitted_data['roles'] = json_decode($submitted_data['roles']); + } + + // Transform media array to keep only filepath + $submitted_data['avatar'] = $submitted_data['avatar']['filename']; + + $form->submit($submitted_data); + } + if ($form->isSubmitted() && $form->isValid()) { + $this->_handleValidFormSubmit($form); + if ($is_profile_edit) { + return $this->redirectToRoute('bolt_profile_edit'); + } + + return $this->redirectToRoute('bolt_users'); + } + + return $this->render('@bolt/users/edit.html.twig', [ + 'userForm' => $form->createView(), ]); } /** * @Route("/user-status/{id}", methods={"POST", "GET"}, name="bolt_user_update_status", requirements={"id": "\d+"}) + * @Security("is_granted('ROLE_ADMIN')") */ public function status(?User $user): Response { @@ -106,6 +207,7 @@ public function status(?User $user): Response /** * @Route("/user-delete/{id}", methods={"POST", "GET"}, name="bolt_user_delete", requirements={"id": "\d+"}) + * @Security("is_granted('ROLE_ADMIN')") */ public function delete(?User $user): Response { @@ -133,58 +235,14 @@ public function delete(?User $user): Response } /** - * @Route("/user-edit/{id}", methods={"POST"}, name="bolt_user_edit_post", requirements={"id": "\d+"}) + * This function is called by add and edit function if given form was submitted and validated correctly + * Here the User Object will be persisted to the DB */ - public function save(?User $user, ValidatorInterface $validator): Response + private function _handleValidFormSubmit(FormInterface $form): void { - $this->validateCsrf('useredit'); - - if (! $user instanceof User) { - $user = UserRepository::factory(); - } - - $displayName = $user->getDisplayName(); - $locale = Json::findScalar($this->getFromRequest('locale')); - $roles = Json::findArray($this->getFromRequest('roles')); - $status = Json::findScalar($this->getFromRequest('ustatus', UserStatus::ENABLED)); - - if (empty($user->getUsername())) { - $user->setUsername($this->getFromRequest('username')); - } - $user->setDisplayName($this->getFromRequest('displayName')); - $user->setEmail($this->getFromRequest('email')); - $user->setLocale($locale); - $user->setRoles($roles); - $user->setbackendTheme($this->getFromRequest('backendTheme')); - $user->setStatus($status); - - $newPassword = $this->getFromRequest('password'); - // Set the plain password to check for validation - if (! empty($newPassword)) { - $user->setPlainPassword($newPassword); - } - - $errors = $validator->validate($user); - if ($errors->count() > 0) { - $hasPasswordError = false; - - /** @var ConstraintViolationInterface $error */ - foreach ($errors as $error) { - $this->addFlash('danger', $error->getMessage()); - - if ($error->getPropertyPath() === 'plainPassword') { - $hasPasswordError = true; - } - } - - $suggestedPassword = $hasPasswordError ? Str::generatePassword() : null; - - return $this->render('@bolt/users/edit.html.twig', [ - 'display_name' => $displayName, - 'userEdit' => $user, - 'suggestedPassword' => $suggestedPassword, - ]); - } + // Get the adjusted User Entity from the form + /** @var User $user */ + $user = $form->getData(); // Once validated, encode the password if ($user->getPlainPassword()) { @@ -192,6 +250,10 @@ public function save(?User $user, ValidatorInterface $validator): Response $user->eraseCredentials(); } + $event = new UserEvent($user); + $this->dispatcher->dispatch($event, UserEvent::ON_PRE_SAVE); + + // Save the new user data into the DB $this->em->persist($user); $this->em->flush(); @@ -199,7 +261,5 @@ public function save(?User $user, ValidatorInterface $validator): Response $this->dispatcher->dispatch($event, UserEvent::ON_POST_SAVE); $this->addFlash('success', 'user.updated_profile'); - - return $this->redirectToRoute('bolt_users'); } } diff --git a/src/Controller/Frontend/ListingController.php b/src/Controller/Frontend/ListingController.php index 43f0fd6b2..8d2270af1 100644 --- a/src/Controller/Frontend/ListingController.php +++ b/src/Controller/Frontend/ListingController.php @@ -14,6 +14,7 @@ use Pagerfanta\Pagerfanta; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Symfony\Component\Routing\Annotation\Route; class ListingController extends TwigAwareController implements FrontendZoneInterface @@ -46,6 +47,11 @@ public function listing(ContentRepository $contentRepository, string $contentTyp $contentType = ContentType::factory($contentTypeSlug, $this->config->get('contenttypes')); + // If the ContentType is 'viewless' we throw a 404. + if ($contentType->get('viewless') === true) { + throw new NotFoundHttpException('Content is not viewable'); + } + // If the locale is the wrong locale if (! $this->validLocaleForContentType($contentType)) { return $this->redirectToDefaultLocale(); diff --git a/src/Entity/User.php b/src/Entity/User.php index 6288028cf..f302b69b4 100644 --- a/src/Entity/User.php +++ b/src/Entity/User.php @@ -15,8 +15,8 @@ /** * @ORM\Entity(repositoryClass="Bolt\Repository\UserRepository") - * @UniqueEntity("email", message="user.duplicate_email") - * @UniqueEntity("username", message="user.duplicate_username") + * @UniqueEntity("email", message="user.duplicate_email", groups={"add_user", "edit_user", "edit_user_without_pw"}) + * @UniqueEntity("username", message="user.duplicate_username", groups={"add_user", "edit_user", "edit_user_without_pw"}) */ class User implements UserInterface, \Serializable { @@ -34,8 +34,8 @@ class User implements UserInterface, \Serializable * @var string * * @ORM\Column(type="string") - * @Assert\NotBlank(normalizer="trim", message="user.not_valid_display_name") - * @Assert\Length(min=2, max=50, minMessage="user.not_valid_display_name") + * @Assert\NotBlank(normalizer="trim", message="user.not_valid_display_name", groups={"add_user", "edit_user", "edit_user_without_pw"}) + * @Assert\Length(min=2, max=50, minMessage="user.not_valid_display_name", groups={"add_user", "edit_user", "edit_user_without_pw"}) * @Groups({"get_content", "get_user"}) */ private $displayName; @@ -44,9 +44,9 @@ class User implements UserInterface, \Serializable * @var string * * @ORM\Column(type="string", unique=true, length=191) - * @Assert\NotBlank() - * @Assert\Length(min=2, max=50) - * @Assert\Regex(pattern="/^[a-z0-9_]+$/", message="user.username_invalid_characters") + * @Assert\NotBlank(groups={"add_user"}) + * @Assert\Length(min=2, max=50, groups={"add_user"}) + * @Assert\Regex(pattern="/^[a-z0-9_]+$/", message="user.username_invalid_characters", groups={"add_user"}) * @Groups("get_user") */ private $username; @@ -55,7 +55,7 @@ class User implements UserInterface, \Serializable * @var string * * @ORM\Column(type="string", unique=true, length=191) - * @Assert\Email(message="user.not_valid_email") + * @Assert\Email(message="user.not_valid_email", groups={"add_user", "edit_user", "edit_user_without_pw"}) * @Groups("get_user") */ private $email; @@ -69,7 +69,7 @@ class User implements UserInterface, \Serializable /** * @var string|null - * @Assert\Length(min="6", minMessage="user.not_valid_password") + * @Assert\Length(min="6", minMessage="user.not_valid_password", groups={"add_user", "edit_user"}) */ private $plainPassword; @@ -105,6 +105,9 @@ class User implements UserInterface, \Serializable /** @ORM\OneToOne(targetEntity="Bolt\Entity\UserAuthToken", mappedBy="user", cascade={"persist", "remove"}) */ private $userAuthToken; + /** @ORM\Column(type="string", length=250, nullable=true) */ + private $avatar; + public function __construct() { } @@ -114,7 +117,7 @@ public function getId(): int return $this->id; } - public function setDisplayName(string $displayName): void + public function setDisplayName(?string $displayName): void { $this->displayName = $displayName; } @@ -134,7 +137,7 @@ public function getUsername(): string return $this->username; } - public function setUsername(string $username): void + public function setUsername(?string $username): void { $slugify = new Slugify(['separator' => '_']); $cleanUsername = $slugify->slugify($username); @@ -146,7 +149,7 @@ public function getEmail(): string return $this->email; } - public function setEmail(string $email): void + public function setEmail(?string $email): void { $this->email = $email; } @@ -166,7 +169,7 @@ public function getPlainPassword(): ?string return $this->plainPassword; } - public function setPlainPassword(string $plainPassword): self + public function setPlainPassword(?string $plainPassword): self { $this->plainPassword = $plainPassword; @@ -314,4 +317,19 @@ public function toArray(): array { return get_object_vars($this); } + + public function isNewUser(): bool + { + return $this->id === null; + } + + public function getAvatar(): ?string + { + return $this->avatar; + } + + public function setAvatar(?string $avatar): void + { + $this->avatar = $avatar; + } } diff --git a/src/Event/Listener/UserAvatarLoadListener.php b/src/Event/Listener/UserAvatarLoadListener.php new file mode 100644 index 000000000..048f91028 --- /dev/null +++ b/src/Event/Listener/UserAvatarLoadListener.php @@ -0,0 +1,34 @@ +get('general'); + $this->avatarConfig = $config->get('user_avatar'); + } + + public function postLoad(LifecycleEventArgs $args): void + { + $entity = $args->getEntity(); + + if ($entity instanceof User) { + if (! $entity->getAvatar() && $this->avatarConfig->get('default_avatar') !== '') { + $entity->setAvatar($this->avatarConfig->get('default_avatar')); + } + } + } +} diff --git a/src/Event/UserEvent.php b/src/Event/UserEvent.php index 764f01dc2..3a3cbdd24 100644 --- a/src/Event/UserEvent.php +++ b/src/Event/UserEvent.php @@ -9,7 +9,9 @@ class UserEvent { + public const ON_ADD = 'bolt.users_pre_add'; public const ON_EDIT = 'bolt.users_pre_edit'; + public const ON_PRE_SAVE = 'bolt.users_post_save'; public const ON_POST_SAVE = 'bolt.users_post_save'; /** @var User */ diff --git a/src/Form/UserType.php b/src/Form/UserType.php index 5301ae44d..b35a37992 100644 --- a/src/Form/UserType.php +++ b/src/Form/UserType.php @@ -4,70 +4,150 @@ namespace Bolt\Form; +use Bolt\Collection\DeepCollection; +use Bolt\Configuration\Config; use Bolt\Entity\User; +use Bolt\Enum\UserStatus; +use Bolt\Utils\LocaleHelper; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\ChoiceType; use Symfony\Component\Form\Extension\Core\Type\EmailType; +use Symfony\Component\Form\Extension\Core\Type\PasswordType; use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\Form\FormInterface; use Symfony\Component\OptionsResolver\OptionsResolver; +use Twig\Environment; class UserType extends AbstractType { - /** - * {@inheritdoc} - */ + /** @var LocaleHelper */ + private $localeHelper; + + /** @var Environment */ + private $twig; + + /** @var DeepCollection */ + private $avatarConfig; + + public function __construct(LocaleHelper $localeHelper, Environment $twig, Config $config) + { + $this->localeHelper = $localeHelper; + $this->twig = $twig; + + /** @var DeepCollection $config */ + $config = $config->get('general'); + $this->avatarConfig = $config->get('user_avatar'); + } + public function buildForm(FormBuilderInterface $builder, array $options): void { - // For the full reference of options defined by each form field type - // see https://symfony.com/doc/current/reference/forms/types.html + // Create custom role options array + $roleOptions = []; + $custom_roles = $options['roles']; + foreach ($custom_roles as $roleName => $roleHierarchy) { + // For some reason these select arrays are built like + // array-key => Label + // array-value => Key which is used to save in the DB (and used for validation) + $label = mb_strtolower(implode(', ', $roleHierarchy)); + $roleOptions[$label] = $roleName; + } - // By default, form fields include the 'required' attribute, which enables - // the client-side form validation. This means that you can't test the - // server-side validation errors from the browser. To temporarily disable - // this validation, set the 'required' attribute to 'false': - // $builder->add('title', null, ['required' => false, ...]); + // Create custom location options array + $locationOptions = []; + $custom_locations = $this->localeHelper->getLocales($this->twig, null, true)->all(); + foreach ($custom_locations as $location) { + $data = $location->all(); + // Same strange array structure as above + $description = sprintf('%s %s (%s, %s)', $data['emoji'], $data['name'], $data['localizedname'], $data['code']); + $locationOptions[$description] = $data['code']; + } + /* + * These are the field that are available to all these actions + * + * - /bolt/user-edit/add + * - /bolt/user-edit/ + * - /bolt/profile-edit + */ $builder ->add('username', TextType::class, [ - 'label' => 'label.username', - 'disabled' => true, + 'required' => $options['require_username'], + // Always disable this field if it is not required aka not on add user page + 'disabled' => ! $options['require_username'], ]) - ->add('displayName', TextType::class, [ - 'label' => 'label.display_name', - ]) - ->add('email', EmailType::class, [ - 'label' => 'label.email', + ->add('displayName', TextType::class) + ->add('plainPassword', PasswordType::class, [ + // We need that, otherwise the suggested password won't be auto filled in + 'always_empty' => false, + 'attr' => [ + 'suggested_password' => $options['suggested_password'], + ], + 'required' => $options['require_password'], + 'empty_data' => '', ]) + ->add('email', EmailType::class) ->add('locale', ChoiceType::class, [ - 'label' => 'label.locale', - 'choices' => [ - 'English (en)' => 'en', - 'Nederlands (dutch, nl)' => 'nl', - 'Español (Spanish, es)' => 'es', - 'français (French, fr)' => 'fr', - 'Deutsch (German, de)' => 'de', - 'Polski (Polish, pl)' => 'pl', - 'Brasilian Portuguese (Brasilian Portuguese, pt_BR)' => 'pt_BR', - 'Italiano (Italian, it)' => 'it', + 'choices' => $locationOptions, + 'empty_data' => $options['default_locale'], + 'attr' => [ + 'default_locale' => $options['default_locale'], ], ]) - ->add('backendTheme', ChoiceType::class, [ - 'label' => 'label.backend_theme', - 'choices' => [ - 'Default Theme' => 'default', - 'Light Theme' => 'light', + ->add('avatar', TextType::class, [ + 'required' => false, + 'attr' => [ + 'upload_path' => $this->avatarConfig->get('upload_path'), + 'extensions_allowed' => $this->avatarConfig->get('extensions_allowed'), ], - ]); + ]) + ; + + /* + * Allow Roles and Status to be set if either + * - this form is used to add a new user + * - if the given user object (=logged in user in profile edit page) has ROLE_ADMIN + */ + if ($options['is_profile_edit'] === false || in_array('ROLE_ADMIN', $options['data']->getRoles(), true)) { + $builder + ->add('roles', ChoiceType::class, [ + 'choices' => $roleOptions, + 'multiple' => true, + ]) + ->add('status', ChoiceType::class, [ + 'choices' => UserStatus::all(), + ]); + } + +// ->add('lastseenAt') +// ->add('lastIp') +// ->add('backendTheme') +// ->add('userAuthToken') } - /** - * {@inheritdoc} - */ public function configureOptions(OptionsResolver $resolver): void { $resolver->setDefaults([ 'data_class' => User::class, + 'suggested_password' => '', + 'roles' => '', + 'require_username' => '', + 'require_password' => '', + 'default_locale' => '', + 'is_profile_edit' => false, + 'validation_groups' => function (FormInterface $form) { + /** @var User $entity */ + $entity = $form->getData(); + + $validation_group = 'edit_user'; + if ($entity->isNewUser()) { + $validation_group = 'add_user'; + } elseif ($entity->getPlainPassword() === '') { + $validation_group = 'edit_user_without_pw'; + } + + return $validation_group; + }, ]); } } diff --git a/src/Menu/BackendMenuBuilder.php b/src/Menu/BackendMenuBuilder.php index d4f24dda3..473f7dd72 100644 --- a/src/Menu/BackendMenuBuilder.php +++ b/src/Menu/BackendMenuBuilder.php @@ -42,7 +42,7 @@ final class BackendMenuBuilder implements BackendMenuBuilderInterface public function __construct( FactoryInterface $menuFactory, - iterable $extensionMenus, + iterable $extensionMenus = [], Config $config, ContentRepository $contentRepository, UrlGeneratorInterface $urlGenerator, diff --git a/src/Twig/ContentExtension.php b/src/Twig/ContentExtension.php index 62f32d210..0b7521465 100644 --- a/src/Twig/ContentExtension.php +++ b/src/Twig/ContentExtension.php @@ -461,6 +461,8 @@ public function getTaxonomies(?Content $content): Collection if (! $content instanceof Content) { $body = sprintf("You have called the |taxonomies filter with a parameter of type '%s', but |taxonomies accepts record (Content).", gettype($content)); $this->notifications->warning('Incorrect use of |taxonomies filter', $body); + + return new Collection(); } $taxonomies = []; diff --git a/templates/_partials/fields/simple_image.html.twig b/templates/_partials/fields/simple_image.html.twig new file mode 100644 index 000000000..6b880c68f --- /dev/null +++ b/templates/_partials/fields/simple_image.html.twig @@ -0,0 +1,83 @@ +{# {% extends '@bolt/_partials/fields/_base.html.twig' %} + +{% set extensions = field.definition.get('extensions')|default('') %} +{% set info %} + {{ 'upload.allow_file_types'|trans }}: {{ extensions|join(', ') }}
+ {{ 'upload.max_size'|trans }}: {{ config.maxupload|format_bytes }} +{% endset %} + +{% block field %} + {% set directory = path('bolt_async_upload', {'location': location|default('files'), 'path': upload_path}) %} + {% set filelist = path('bolt_async_filelisting', {'location': location|default('files') }) %} + {% set labels = { + 'button_upload': 'image.button_upload'|trans, + 'button_from_library': 'image.button_from_library'|trans, + 'button_remove': 'image.button_remove'|trans, + 'placeholder_filename': 'image.placeholder_filename'|trans, + 'placeholder_alt_text': 'image.placeholder_alt_text'|trans, + 'placeholder_title': 'image.placeholder_title'|trans, + 'button_edit_attributes': 'image.button_edit_attributes'|trans, + }|json_encode %} + + + +{% endblock %} + +#} + +{% extends '@bolt/_partials/fields/_base.html.twig' %} + +{% set extensions = extensions_allowed|default([]) %} +{% set info %} + {{ 'upload.allow_file_types'|trans }}: {{ extensions|join(', ') }}
+ {{ 'upload.max_size'|trans }}: {{ config.maxupload|format_bytes }} +{% endset %} + +{% block field %} + {% set directory = path('bolt_async_upload', {'location': location|default('files'), 'path': upload_path}) %} + {% set directoryurl = path('bolt_async_upload_url', {'location': location|default('files'), 'path': upload_path}) %} + {% set filelist = path('bolt_async_filelisting', {'location': location|default('files'), 'type': 'images' }) %} + {% set labels = { + 'button_upload': 'image.button_upload'|trans, + 'button_from_library': 'image.button_from_library'|trans, + 'button_remove': 'image.button_remove'|trans, + 'placeholder_filename': 'image.placeholder_filename'|trans, + 'placeholder_alt_text': 'image.placeholder_alt_text'|trans, + 'button_edit_attributes': 'image.button_edit_attributes'|trans, + 'button_from_url': 'image.button_from_url'|trans, + }|json_encode %} + + +{% endblock %} diff --git a/templates/users/_form.html.twig b/templates/users/_form.html.twig new file mode 100644 index 000000000..a1422ddbe --- /dev/null +++ b/templates/users/_form.html.twig @@ -0,0 +1,189 @@ +{% import '@bolt/_macro/_macro.html.twig' as macro %} + +{# DONT TOUCH THAT ID! #} +{{ form_start(userForm, {'attr': {'id': 'editcontent'}}) }} + + {# ===== USERNAME ===== #} + + {% include '@bolt/_partials/fields/text.html.twig' with { + 'id' : userForm.username.vars.id, + 'label' : 'label.username'|trans, + 'name' : userForm.vars.name ~ '[' ~ userForm.username.vars.name ~ ']', + 'value' : userForm.username.vars.value, + 'required': userForm.username.vars.required, + 'disabled': userForm.username.vars.disabled, + } %} + {% if form_errors(userForm.username) is not empty %} +
{{ form_errors(userForm.username) }}
+ {% endif %} + {% do userForm.username.setRendered() %} + {# Mark form field as manually rendered so it's not displayed Twice by the form_rest() Twig helper #} + + {# ===== DISPLAY NAME ===== #} + + {% include '@bolt/_partials/fields/text.html.twig' with { + 'id' : userForm.displayName.vars.id, + 'label' : 'label.display_name'|trans, + 'name' : userForm.vars.name ~ '[' ~ userForm.displayName.vars.name ~ ']', + 'value' : userForm.displayName.vars.value, + 'disabled' : false, + 'required': userForm.displayName.vars.required, + } %} + {% if form_errors(userForm.displayName) is not empty %} +
{{ form_errors(userForm.displayName) }}
+ {% endif %} + {% do userForm.displayName.setRendered() %} + + {# ===== PASSWORD ===== #} + + {% set use_suggested_pw = userForm.plainPassword.vars.required %} + {% set suggested_pw = userForm.plainPassword.vars.attr.suggested_password %} + + {% include '@bolt/_partials/fields/password.html.twig' with { + 'id' : userForm.plainPassword.vars.id, + 'label': 'label.password'|trans, + 'name' : userForm.vars.name ~ '[' ~ userForm.plainPassword.vars.name ~ ']', + 'value': use_suggested_pw ? suggested_pw : '', + 'hidden': 'true', + 'strength': true, + 'postfix': __('password.suggested', {'%password%': suggested_pw }), + 'required': userForm.plainPassword.vars.required, + } %} + {% if form_errors(userForm.plainPassword) is not empty %} +
{{ form_errors(userForm.plainPassword) }}
+ {% endif %} + {% do userForm.plainPassword.setRendered() %} + + {# ===== EMAIL ===== #} + + {% include '@bolt/_partials/fields/email.html.twig' with { + 'id' : userForm.email.vars.id, + 'label': 'label.email'|trans, + 'label_class': userForm.email.vars.required ? 'required' : '', + 'value': userForm.email.vars.value, + 'name' : userForm.vars.name ~ '[' ~ userForm.email.vars.name ~ ']', + 'class': 'form-control', + 'required': userForm.email.vars.required, + } %} + {% if form_errors(userForm.email) is not empty %} +
{{ form_errors(userForm.email) }}
+ {% endif %} + {% do userForm.email.setRendered() %} + + {# ===== LOCALE ===== #} + + {% set localeOptions = [] %} + + {% for locale in userForm.locale.vars.choices %} + {% set localeOptions = localeOptions|merge([ + { + 'key': locale.value, + 'value': locale.label + } + ]) %} + {% endfor %} + + {% include '@bolt/_partials/fields/select.html.twig' with { + 'id' : userForm.locale.vars.id, + 'label' : 'label.locale'|trans, + 'name' : userForm.vars.name ~ '[' ~ userForm.locale.vars.name ~ ']', + 'value' : userForm.locale.vars.value is empty ? userForm.locale.vars.attr.default_locale : userForm.locale.vars.value, + 'options' : localeOptions, + 'required': userForm.locale.vars.required, + 'multiple': userForm.locale.vars.multiple ? 'true' : 'false', + } %} + {% if form_errors(userForm.locale) is not empty %} +
{{ form_errors(userForm.locale) }}
+ {% endif %} + {% do userForm.locale.setRendered() %} + + {# ===== ROlES ===== #} + + {% if userForm.roles is defined %} + {% set roleOptions = [] %} + + {% for roleData in userForm.roles.vars.choices ?? [] %} + {% set roleOptions = roleOptions|merge([ + { + 'key': roleData.value, + 'value': roleData.value ~ ' (' ~ roleData.label ~ ')' + } + ]) %} + {% endfor %} + + {% include '@bolt/_partials/fields/select.html.twig' with { + 'id' : userForm.roles.vars.id, + 'label' : 'label.roles'|trans, + 'name' : userForm.vars.name ~ '[' ~ userForm.roles.vars.name ~ ']', + 'value' : userForm.roles.vars.value, + 'options' : roleOptions, + 'required': userForm.roles.vars.required, + 'multiple': userForm.roles.vars.multiple ? 'true' : 'false', + } %} + {% if form_errors(userForm.roles) is not empty %} +
{{ form_errors(userForm.roles) }}
+ {% endif %} + {% do userForm.roles.setRendered() %} + {% endif %} + + {# ===== STATUS ===== #} + + {% if userForm.status is defined %} + {% set statusOptions = [] %} + {% for option in userForm.status.vars.choices ?? [] %} + {% set statusOptions = statusOptions|merge([ + { + 'key': option.value, + 'value': option.value|capitalize + } + ]) %} + {% endfor %} + + {% include '@bolt/_partials/fields/select.html.twig' with { + 'id': userForm.status.vars.id, + 'label': 'field.status'|trans, + 'name' : userForm.vars.name ~ '[' ~ userForm.status.vars.name ~ ']', + 'value': userForm.status.vars.value, + 'options': statusOptions, + 'required': userForm.status.vars.required ? 'true' : 'false', + } %} + {% if form_errors(userForm.status) is not empty %} +
{{ form_errors(userForm.status) }}
+ {% endif %} + {% do userForm.status.setRendered() %} + {% endif %} + + {# ===== AVATAR ===== #} + {% include '@bolt/_partials/fields/simple_image.html.twig' with { + 'filepath': userForm.avatar.vars.value, + 'upload_path': userForm.avatar.vars.attr['upload_path'], + 'extensions_allowed': userForm.avatar.vars.attr['extensions_allowed'], + 'id' : userForm.avatar.vars.id, + 'label' : 'label.avatar'|trans, + 'name' : userForm.vars.name ~ '[' ~ userForm.avatar.vars.name ~ ']', + 'value' : userForm.avatar.vars.value, + 'required': userForm.avatar.vars.required, + } %} + {% if form_errors(userForm.avatar) is not empty %} +
{{ form_errors(userForm.avatar) }}
+ {% endif %} + {% do userForm.avatar.setRendered() %} + + +{# TODO: include form field with '@bolt/_partials/fields/select.html.twig' #} + {# Comment out for a while, until we work on this again +
+ + +
+ + #} + + {# DONT TOUCH THAT ID! #} + {{ macro.button('action.save', 'fa-save', 'primary', {'type': 'submit', 'form': 'editcontent', 'name': 'save'}) }} +{{ form_end(userForm) }} diff --git a/templates/users/add.html.twig b/templates/users/add.html.twig new file mode 100644 index 000000000..e0b10b048 --- /dev/null +++ b/templates/users/add.html.twig @@ -0,0 +1,37 @@ +{% extends '@bolt/_base/layout.html.twig' %} +{% import '@bolt/_macro/_macro.html.twig' as macro %} + +{# The 'title' and 'shoulder' blocks are the main heading of the page. #} +{% block shoulder %} + {{ 'action.add_user'|trans }} +{% endblock shoulder %} + +{% block title %} + {{ macro.icon('user') }} + {{ __('user.new_user') }} +{% endblock %} + +{% block vue_id 'editor' %} + +{% block main %} +
+ {% include('@bolt/users/_form.html.twig') with {'form_type': 'add'} %} +
+{% endblock %} + +{% block aside %} +
+
+
+
+ {{ macro.button('action.save', 'fa-save', 'primary', {'type': 'submit', 'form': 'editcontent'}) }} +
+
+
+
+{% endblock %} + +{% block javascripts %} + {{ encore_entry_script_tags('zxcvbn') }} + {{ parent() }} +{% endblock %} diff --git a/templates/users/edit.html.twig b/templates/users/edit.html.twig index 8097405c9..489fbec70 100644 --- a/templates/users/edit.html.twig +++ b/templates/users/edit.html.twig @@ -8,133 +8,14 @@ {% block title %} {{ macro.icon('user') }} - {{ display_name|default(__('user.new_user')) }} + {{ userForm.displayName.vars.value|default(__('user.new_user')) }} {% endblock %} {% block vue_id 'editor' %} {% block main %} -
-
- - - {% include '@bolt/_partials/fields/text.html.twig' with { - 'id' : 'username', - 'label' : 'label.username'|trans, - 'name' : 'username', - 'value' : userEdit.username, - 'disabled' : userEdit.username is empty ? false : true, - 'required': true, - } %} - - {% include '@bolt/_partials/fields/text.html.twig' with { - 'id' : 'display_name', - 'label' : 'label.display_name'|trans, - 'name' : 'displayName', - 'value' : userEdit.displayName, - 'disabled' : false, - 'required': true, - } %} - - {% include '@bolt/_partials/fields/password.html.twig' with { - 'id' : 'password', - 'label': 'label.password'|trans, - 'name': 'password', - 'value': suggestedPassword, - 'hidden': 'true', - 'strength': true, - 'postfix': suggestedPassword ? __('password.suggested', {'%password%': suggestedPassword }), - 'required': userEdit.username is empty ? true : false, - } %} - - {% include '@bolt/_partials/fields/email.html.twig' with { - 'id' : 'email', - 'label': 'label.email'|trans, - 'label_class': 'required', - 'value': userEdit.email, - 'name': 'email', - 'class': 'form-control', - 'required': true, - } %} - - {% set localeOptions = [] %} - - {% for locale in locales(all = true) %} - {% set localeOptions = localeOptions|merge([ - { - 'key': locale.code, - 'value': locale.emoji ~ ' ' ~ locale.name ~ ' (' ~ locale.localizedname ~ ', ' ~ locale.code ~ ')' - } - ]) %} - {% endfor %} - - {% include '@bolt/_partials/fields/select.html.twig' with { - 'id' : 'locale', - 'label' : 'label.locale'|trans, - 'name' : 'locale', - 'value' : userEdit.locale, - 'options' : localeOptions, - 'required': 'true', - } %} - - {% set roleOptions = [] %} - - {% for roleName, roleHierarchy in roles ?? [] %} - {% set roleOptions = roleOptions|merge([ - { - 'key': roleName, - 'value': roleName ~ ' (' ~ roleHierarchy|join(', ')|lower ~ ')' - } - ]) %} - {% endfor %} - - {% include '@bolt/_partials/fields/select.html.twig' with { - 'id' : 'roles', - 'label' : 'label.roles'|trans, - 'name' : 'roles', - 'value' : userEdit.roles, - 'options' : roleOptions, - 'required': 'true', - 'multiple': 'true', - } %} - - {% set statusOptions = [] %} - {% for option in statuses ?? [] %} - {% set statusOptions = statusOptions|merge([ - { - 'key': option, - 'value': option|capitalize - } - ]) %} - {% endfor %} - - {% include '@bolt/_partials/fields/select.html.twig' with { - 'id': 'ustatus', - 'label': 'field.status'|trans, - 'name': 'ustatus', - 'value': userEdit.status, - 'options': statusOptions, - 'required': true, - } %} - - {# TODO: include form field with '@bolt/_partials/fields/select.html.twig' #} - {# Comment out for a while, until we work on this again -
- - -
- - #} - - {{ macro.button('action.save', 'fa-save', 'primary', {'type': 'submit', 'form': 'editcontent', 'name': 'save'}) }} - -
+ {% include('@bolt/users/_form.html.twig') with {'form_type': 'edit'} %}
{% endblock %} diff --git a/templates/users/listing.html.twig b/templates/users/listing.html.twig index cdbabdf11..6382e40bd 100644 --- a/templates/users/listing.html.twig +++ b/templates/users/listing.html.twig @@ -59,7 +59,7 @@

- {{ macro.buttonlink('action.add_user', path('bolt_user_edit', {'id': 0}), 'user-plus', 'secondary') }} + {{ macro.buttonlink('action.add_user', path('bolt_user_add'), 'user-plus', 'secondary') }}

Current sessions

diff --git a/tests/e2e/users.feature b/tests/e2e/users.feature index cfbbd90d3..6ae2ab98a 100644 --- a/tests/e2e/users.feature +++ b/tests/e2e/users.feature @@ -57,22 +57,22 @@ Feature: Users & Permissions And I scroll "body > div.admin > div.admin__body > div.admin__body--container > main > p > a" into view And I follow "Add User" - Then I should be on "/bolt/user-edit/0" + Then I should be on "/bolt/user-edit/add" And I should see "New User" in the ".admin__header--title" element When I fill in the following: - | username | test_user | - | displayName | Test user | - | password | test%1 | - | email | test_user@example.org | - And I scroll "#multiselect-locale > div > div.multiselect__select" into view - And I click "#multiselect-locale > div > div.multiselect__select" - And I scroll "#multiselect-locale > div > div.multiselect__content-wrapper > ul > li:nth-child(1)" into view - And I click "#multiselect-locale > div > div.multiselect__content-wrapper > ul > li:nth-child(1)" - And I scroll "#multiselect-roles > div > div.multiselect__select" into view - And I click "#multiselect-roles > div > div.multiselect__select" - And I scroll "#multiselect-roles > div > div.multiselect__content-wrapper > ul > li:nth-child(1) > span" into view - And I click "#multiselect-roles > div > div.multiselect__content-wrapper > ul > li:nth-child(1) > span" + | user[username] | test_user | + | user[displayName] | Test user | + | user[plainPassword] | test%1 | + | user[email] | test_user@example.org | + And I scroll "#multiselect-user_locale > div > div.multiselect__select" into view + And I click "#multiselect-user_locale > div > div.multiselect__select" + And I scroll "#multiselect-user_locale > div > div.multiselect__content-wrapper > ul > li:nth-child(1)" into view + And I click "#multiselect-user_locale > div > div.multiselect__content-wrapper > ul > li:nth-child(1)" + And I scroll "#multiselect-user_roles > div > div.multiselect__select" into view + And I click "#multiselect-user_roles > div > div.multiselect__select" + And I scroll "#multiselect-user_roles > div > div.multiselect__content-wrapper > ul > li:nth-child(1) > span" into view + And I click "#multiselect-user_roles > div > div.multiselect__content-wrapper > ul > li:nth-child(1) > span" When I scroll "#editcontent > button" into view And I press "Save changes" @@ -103,8 +103,8 @@ Feature: Users & Permissions Then I should be on url matching "\/bolt\/user\-edit\/[0-9]+" When I fill in the following: - | displayName | Tom Doe CHANGED | - | email | tom_admin_changed@example.org | + | user[displayName] | Tom Doe CHANGED | + | user[email] | tom_admin_changed@example.org | And I scroll "#editcontent > button" into view And I wait 0.1 seconds And I press "Save changes" @@ -122,14 +122,13 @@ Feature: Users & Permissions Then I should be on "/bolt/user-edit/2" - When I fill "email" element with "admin@example.org" + When I fill "user[email]" element with "admin@example.org" And I scroll "Save changes" into view And I press "Save changes" Then I should be on "/bolt/user-edit/2" - And I should see "Notification" in the ".admin__notifications" element - And I should see "A user with \"admin@example.org\" email already exists." in the ".admin__notifications" element + And I should see "A user with \"admin@example.org\" email already exists." in the ".field-error" element @javascript Scenario: Edit user with incorrect display name, password and email @@ -137,9 +136,9 @@ Feature: Users & Permissions And I am on "/bolt/user-edit/2" When I fill in the following: - | displayName | x | - | password | short | - | email | smth@nth | + | user[displayName] | x | + | user[plainPassword] | short | + | user[email] | smth@nth | And I scroll "Save changes" into view And I press "Save changes" @@ -161,9 +160,9 @@ Feature: Users & Permissions Then I should be on "/bolt/profile-edit" And I should see "Jane Doe" in the "h1" element - And the field "username" should contain "jane_admin" + And the field "user[username]" should contain "jane_admin" - When I fill "displayName" element with " " + When I fill "user[displayName]" element with "a" And I scroll "Save changes" into view And I press "Save changes" @@ -175,12 +174,12 @@ Feature: Users & Permissions Given I am logged in as "jane_admin" with password "jane%1" And I am on "/bolt/profile-edit" - When I fill "displayName" element with "Administrator" + When I fill "user[displayName]" element with "Administrator" And I scroll "Save changes" into view And I press "Save changes" Then I should see "User Profile has been updated!" - And the field "displayName" should contain "Administrator" + And the field "user[displayName]" should contain "Administrator" And I logout @javascript diff --git a/translations/messages.en.xlf b/translations/messages.en.xlf index 2d6b70d46..b6ca20eaf 100644 --- a/translations/messages.en.xlf +++ b/translations/messages.en.xlf @@ -2439,5 +2439,17 @@ New folder + + + title.add_user + Add User + + + + + label.avatar + Avatar + +