Compare commits

...

280 Commits

Author SHA1 Message Date
Tim Donohue
d8ed267e5f Update version tag for release 2023-11-15 14:33:16 -06:00
Tim Donohue
fdcaeb592c Merge pull request #2639 from tdonohue/port_2610_to_dspace-7_x
[Port dspace-7_x] Fixed menu not updating when a new sub section is added after rendering has already completed
2023-11-13 17:00:20 -06:00
Tim Donohue
8872fd3340 Merge pull request #2638 from DSpace/backport-2633-to-dspace-7_x
[Port dspace-7_x] Edit-item view: random order of buttons in status tab
2023-11-13 16:51:21 -06:00
Tim Donohue
959d592394 Merge pull request #2637 from DSpace/backport-2632-to-dspace-7_x
[Port dspace-7_x] Fixes "some item edit pages are accessible by anonymous users"
2023-11-13 16:40:19 -06:00
Alexandre Vryghem
c98e8f6504 107902: Created test case for 2f26e686cc 2023-11-13 16:36:05 -06:00
Kristof De Langhe
e293f3db52 107685: menu-component re-render section on store update 2023-11-13 16:35:16 -06:00
Vlad Nouski
651b6a7d76 refactor: code
(cherry picked from commit fbbbc18844)
2023-11-13 22:05:45 +00:00
Vlad Nouski
dd554590b1 fix: random order of buttons in status tab
(cherry picked from commit 35f8b55f58)
2023-11-13 22:05:45 +00:00
Tim Donohue
0a3502e9cc Merge pull request #2636 from DSpace/backport-2562-to-dspace-7_x
[Port dspace-7_x] Fix match theme by handle with canonical prefix https://hdl.handle.net/ not working
2023-11-13 16:00:02 -06:00
Vlad Nouski
94866cab45 [DURACOM-202] refactor: code
(cherry picked from commit 6f64db1645)
2023-11-13 20:34:42 +00:00
Vlad Nouski
a5a59dcf8b [DURACOM-202] refactor: code
(cherry picked from commit b6d515ff09)
2023-11-13 20:34:42 +00:00
Vlad Nouski
8f9a358afb [DURACOM-202] feature: item edit pages are accessible by administrator
(cherry picked from commit ccf1cc4547)
2023-11-13 20:34:42 +00:00
Alexandre Vryghem
0cd72e4917 107671: Fixed theme matching by handle not working in production mode
(cherry picked from commit 7529ed8b35)
2023-11-13 20:21:10 +00:00
Alexandre Vryghem
1222ed45ca 107671: Fixed bug where config property would still sometimes be undefined whey calling the ngOnDestroy in the ThemedComponent
(cherry picked from commit 4e54cca600)
2023-11-13 20:21:10 +00:00
Alexandre Vryghem
27f3fc310f 107671: Split Theme model & ThemeConfig classes in separate files to prevent circular dependencies
(cherry picked from commit da8880e5ba)
2023-11-13 20:21:10 +00:00
Alexandre Vryghem
d3fdfebde1 107671: Fix handle theme not working with canonical prefix https://hdl.handle.net/
(cherry picked from commit a7faf7d449)
2023-11-13 20:21:10 +00:00
Tim Donohue
626cc30738 Merge pull request #2635 from DSpace/backport-2579-to-dspace-7_x
[Port dspace-7_x] adding new access-status-list-element-badge css classes
2023-11-13 13:19:23 -06:00
Tim Donohue
64364c9ddb Merge pull request #2634 from DSpace/backport-2630-to-dspace-7_x
[Port dspace-7_x] Fix handle redirect not working with custom nameSpace
2023-11-13 13:18:52 -06:00
Paulo Graça
a276f415a8 adding ngOnDestroy for dealing with unsubscribe
(cherry picked from commit 75b788d05b)
2023-11-13 17:30:28 +00:00
Paulo Graça
59c4d59e45 remove replaceAll and use an object property
(cherry picked from commit c7eae9242a)
2023-11-13 17:30:28 +00:00
Paulo Graça
a12488c827 new accessStatusClass atribute
(cherry picked from commit 6378dbec4a)
2023-11-13 17:30:28 +00:00
Paulo Graça
ff03243298 Create new access-status-badge.component.scss
(cherry picked from commit 3bf2eb1997)
2023-11-13 17:30:28 +00:00
Paulo Graça
116bfbded1 adding new access-status-list-element-badge css classes
(cherry picked from commit e847e4ef51)
2023-11-13 17:30:28 +00:00
Alexandre Vryghem
68cdd120c9 Fix handle redirect not working with custom nameSpace
(cherry picked from commit b894dce3b0)
2023-11-13 17:22:53 +00:00
Tim Donohue
7d5c4560cd Merge pull request #2629 from tdonohue/port_2620_to_dspace-7_x
[Port dspace-7_x] Fix for repeatable date field labels
2023-11-10 17:20:40 -06:00
Tim Donohue
755e89dffa Merge pull request #2628 from DSpace/backport-2625-to-dspace-7_x
[Port dspace-7_x] Bump axios from 0.27.2 to 1.6.0
2023-11-10 17:15:22 -06:00
lotte
8458d589b2 Fixed test 2023-11-10 16:40:31 -06:00
lotte
30ce8440e1 108045: Fix for repeatable date field labels 2023-11-10 16:39:38 -06:00
dependabot[bot]
c8d98ec0b1 Bump axios from 0.27.2 to 1.6.0
Bumps [axios](https://github.com/axios/axios) from 0.27.2 to 1.6.0.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v0.27.2...v1.6.0)

---
updated-dependencies:
- dependency-name: axios
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
(cherry picked from commit ef9f31d3c6)
2023-11-10 22:23:44 +00:00
Tim Donohue
6975fd15d5 Merge pull request #2627 from DSpace/backport-2545-to-dspace-7_x
[Port dspace-7_x] Fix "Edit Group" page always requests all member Subgroups & EPersons
2023-11-10 15:14:27 -06:00
Tim Donohue
d305e6096a Address feedback. Run empty search on init. Reorder sections to list current members before add members (for both eperson and groups)
(cherry picked from commit 9117ac005f)
2023-11-10 19:03:47 +00:00
Tim Donohue
37ae09acd1 Remove seemingly unnecessary page reload after new search.
(cherry picked from commit d163db13f2)
2023-11-10 19:03:47 +00:00
Tim Donohue
8cc36d7056 Refactor subgroups-list component's "search()" to act same as member-list component's "search()". Avoids reloading the page as frequently.
(cherry picked from commit 2eb1a17e4e)
2023-11-10 19:03:47 +00:00
Tim Donohue
4a1f2a1b75 Refactor members-list and subgroups-list components to use new isNotMemberOf endpoints (via services)
(cherry picked from commit 8a10888d2a)
2023-11-10 19:03:47 +00:00
Tim Donohue
1f1dc59f8b Fix subgroups-list specs so they align with new members-list specs
(cherry picked from commit 64f968b246)
2023-11-10 19:03:47 +00:00
Tim Donohue
f0b4239df9 Also remove unnecessary EpersonDtoModel from extending ReviewersListComponent. Remove "memberOfGroup" from EpersonDtoModel as it is no longer used
(cherry picked from commit b598f1b5ca)
2023-11-10 19:03:47 +00:00
Tim Donohue
753a31f7f4 Remove unnecessary EpersonDtoModel. Rework code and tests to use EPerson instead.
(cherry picked from commit bffae54b10)
2023-11-10 19:03:47 +00:00
Tim Donohue
ac6a7be7aa Remove "isMemberOfGroup()" from members-list component.
(cherry picked from commit 43d37196fb)
2023-11-10 19:03:47 +00:00
Tim Donohue
c02cfff8da Fix bug where linked Community/Collection info was sometimes listed many times in a row
(cherry picked from commit 229236634a)
2023-11-10 19:03:47 +00:00
Tim Donohue
d7ccce1f8f Remove isSubgroupOfGroup() functionality as it loads every subgroup at once. Bad peformance for large groups
(cherry picked from commit 97479a2945)
2023-11-10 19:03:47 +00:00
Tim Donohue
139446118b Limit getMembers() and getSubgroups() to only fetching one object. These lists are only used to find the size of each
(cherry picked from commit 0da7c15f2e)
2023-11-10 19:03:47 +00:00
Tim Donohue
787feae631 Merge pull request #2626 from tdonohue/port_2607_to_dspace-7_x
[Port dspace-7_x] Added skip to main content button
2023-11-10 11:43:15 -06:00
Alexandre Vryghem
e545c42aae Added skip to main content button 2023-11-10 10:59:57 -06:00
Tim Donohue
d166b5e37a Merge pull request #2623 from DSpace/backport-2611-to-dspace-7_x
[Port dspace-7_x] Communities & Collections tree browser updates - Replaced #2597
2023-11-10 09:55:38 -06:00
Tim Donohue
61ded72183 Merge pull request #2616 from DSpace/backport-2574-to-dspace-7_x
[Port dspace-7_x] Support type-bind of elements based on repeatable list type-bound element (CHECKBOX_GROUP)
2023-11-10 09:15:59 -06:00
William Welling
b6d8c7d18e Filter expanded nodes by id
Co-Authored-By: Art Lowel <1567693+artlowel@users.noreply.github.com>
(cherry picked from commit dc2ef989e6)
2023-11-10 15:14:28 +00:00
William Welling
1d0ca04992 Update condition to render show more node
`loadingNode` ends up being the current `node` after clicking it preventing it from rendering when more pages available.

Update community list component spec

Make the show more flat node id unique

The nodes with same id are conflicting when added to the tree. Clicking on the second with same id places the show more button under the wrong branch and expands the wrong page.

(cherry picked from commit 11d3771e72)
2023-11-10 15:14:28 +00:00
Tim Donohue
34b91a7dea Merge pull request #2617 from DSpace/backport-2594-to-dspace-7_x
[Port dspace-7_x] Media viewer controls rendered behind DSpace header
2023-11-10 06:49:12 -06:00
Tim Donohue
203dcbebda Merge pull request #2612 from DSpace/backport-2596-to-dspace-7_x
[Port dspace-7_x] Fix cache issue when depositing a submission
2023-11-09 16:53:28 -06:00
Davide Negretti
c6ade09e4a [DURACOM-180] Prevent header from covering media viewer controls (base theme)
(cherry picked from commit c042cd8d11)
2023-11-09 22:44:28 +00:00
Davide Negretti
5f46b638e4 [DURACOM-180] Prevent header from covering media viewer controls (dspace theme)
(cherry picked from commit 0208a78437)
2023-11-09 22:44:28 +00:00
Davide Negretti
0d0c2dac17 [DURACOM-195] Simplify vertical spacing in header and breadcrumbs
(cherry picked from commit a3e6d9b09a)
2023-11-09 22:44:28 +00:00
Andreas Mahnke
bc21085398 Support type-bind of elements based on repeatable list type-bound element (CHECKBOX_GROUP)
(cherry picked from commit 09aaa46875)
2023-11-09 22:04:12 +00:00
Tim Donohue
137a83e7f1 Merge pull request #2614 from DSpace/backport-2595-to-dspace-7_x
[Port dspace-7_x] Add UI nameSpace context path to Mirador viewer path
2023-11-09 13:40:03 -06:00
Alan Orth
31ee580047 Merge pull request #2615 from DSpace/backport-2602-to-dspace-7_x
[Port dspace-7_x] Support for freetext values in submission for vocabulary controlled `onebox` and `tag` types
2023-11-09 22:39:53 +03:00
Jens Vannerum
e815b1d938 108055: add user input to tag list
(cherry picked from commit aac58e612d)
2023-11-09 18:56:37 +00:00
Jens Vannerum
a758848146 108055: fix issue 8686: unable to enter freetext values in the submission form for vocabulary
(cherry picked from commit 0dcf6cb885)
2023-11-09 18:56:37 +00:00
William Welling
fba30781de Add UI nameSpace context path to Mirador viewer path
(cherry picked from commit 3228c457a3)
2023-11-09 17:45:22 +00:00
Giuseppe Digilio
4b4c1dc08a [DURACOM-197] Fix cache issue when depositing a submission
(cherry picked from commit f992ff6671)
2023-11-09 17:31:52 +00:00
Tim Donohue
5eb62e22eb Merge pull request #2605 from DSpace/backport-2364-to-dspace-7_x
[Port dspace-7_x] Utility gap-* classes for flexbox
2023-11-08 16:57:59 -06:00
Tim Donohue
fbe4732450 Merge pull request #2590 from marcoaureliocardoso/dspace-7_x
fix(pt-BR.json5): fix and update the language file
2023-11-08 16:49:38 -06:00
Davide Negretti
c5f22ab959 [DURACOM-177] Use gap-* classes on navbar buttons
(cherry picked from commit a35629536e)
2023-11-08 22:15:29 +00:00
Davide Negretti
75e45cc8c2 [DURACOM-177] gap-* classes
(cherry picked from commit 930a381e4a)
2023-11-08 22:15:29 +00:00
Tim Donohue
5bb451a649 Merge pull request #2604 from DSpace/backport-2603-to-dspace-7_x
[Port dspace-7_x] Fix e2e tests by running in production mode & using a user-agent for statistics
2023-11-08 13:13:21 -06:00
Tim Donohue
042c0f06f1 Specify user agent to avoid being detected as a "bot" by backend
(cherry picked from commit 72cda41731)
2023-11-08 18:25:13 +00:00
Tim Donohue
f3f87dc928 Ensure e2e tests run in production mode
(cherry picked from commit 7dcaae8465)
2023-11-08 18:25:13 +00:00
Tim Donohue
4a10d37d0d Merge pull request #2598 from DSpace/backport-2593-to-dspace-7_x
[Port dspace-7_x] Fix "Submission input type date doesn't work properly once a value has been set"
2023-11-03 15:56:55 -05:00
Alisa Ismailati
c99487babc [DURACOM-194] fixed year input value on input type date
(cherry picked from commit c412c1fa13)
2023-11-03 20:15:37 +00:00
Alisa Ismailati
e54723aa85 Merged in DSC-106 (pull request #643)
[DSC-106] Date input usable via keyboard using tab

Approved-by: Vincenzo Mecca
(cherry picked from commit 543b4ad576)
2023-11-03 20:15:37 +00:00
Marco Aurelio Cardoso
8b48a0b118 fix(pt-BR.json5): fix and update the language file and previous errors
Fix and update the pt-BR language file and the previous errors
2023-10-29 07:18:04 -03:00
Marco Aurelio Cardoso
e1494c0518 fix(pt-BR.json5): fix and update the language file
Fix and update the pt-BR language file
2023-10-29 06:49:20 -03:00
Tim Donohue
701c6e36b0 Merge pull request #2589 from DSpace/backport-2580-to-dspace-7_x
[Port dspace-7_x]   Check cssRules before css variables are read from stylesheet (again)
2023-10-27 13:51:12 -05:00
Gantner, Florian Klaus
c6b66b62e3 more error-prone check of cssRules existence before css variables are get from stylesheet
check the existence off cssRules property before the variables are readed from this stylesheet
https://github.com/DSpace/dspace-angular/issues/2450

(cherry picked from commit 4dd334f2e7)
2023-10-27 15:28:08 +00:00
Tim Donohue
6a182c32e1 Merge pull request #2585 from tdonohue/port_2442_to_7x
[Port dspace-7_x] New themed components & minor CSS fixes
2023-10-26 16:56:03 -05:00
Alexandre Vryghem
b8079a350c New themed components & minor CSS fixes (#2442)
* 100839: Created themeable BrowseByComponent

* 100839: Added themed BrowseByComponent to custom theme

* 100839: Added themed BrowseEntryListElementComponent to custom theme

* Added PersonComponent to custom theme

* Themed LogInComponent

* Fix focus on navbar using different color

* Fix ccLicense checkbox margin

* Fix long search facets name not displaying correctly

* Removed RecentItemListComponent's unnecessary float causing alignment issues when adding components underneath it

* Themed RegisterEmailFormComponent
2023-10-26 16:07:24 -05:00
Tim Donohue
3e9f3aba92 Merge pull request #2584 from DSpace/backport-2441-to-dspace-7_x
[Port dspace-7_x] Minor css variables fixes for header & navbar
2023-10-26 15:02:26 -05:00
Alexandre Vryghem
e548ebcb5a Added new variables for the expandable navbar section
(cherry picked from commit 2ca2a3881f)
2023-10-26 19:03:08 +00:00
Alexandre Vryghem
166444fc50 Fixed breadcrumb padding using incorrect syntax
(cherry picked from commit 6c48238fa2)
2023-10-26 19:03:08 +00:00
Alexandre Vryghem
a40e26985d Fixed header bg color not being set in default (no) theme
(cherry picked from commit 14b1ce5e50)
2023-10-26 19:03:08 +00:00
Alexandre Vryghem
72dcfddff1 Added support for changing the color of the navbar
(cherry picked from commit f6649e1c38)
2023-10-26 19:03:08 +00:00
Tim Donohue
b9f60fa627 Merge pull request #2583 from DSpace/backport-2506-to-dspace-7_x
[Port dspace-7_x] allow insertion of multi-line scope notes in MD field registry
2023-10-26 13:25:32 -05:00
Tim Donohue
97b22c63f8 Merge pull request #2391 from alexandrevryghem/split-eperson-administration-page_contribute-maintenance-7.6
Created separate ePerson pages for create/edit
2023-10-26 13:23:15 -05:00
Sascha Szott
163661a956 allow to insert multi-line scope notes in MD field registry
(cherry picked from commit 5bc5dd859e)
2023-10-26 16:56:44 +00:00
Tim Donohue
f8671e7d4b Merge pull request #2581 from DSpace/backport-2542-to-dspace-7_x
[Port dspace-7_x] Fix i18n labels and alignment in vocabulary-treeview
2023-10-26 11:24:11 -05:00
Tim Donohue
673f81759e Merge pull request #2464 from alexandrevryghem/i18n-cache-busting_contribute-7.6
i18n production improvements
2023-10-26 11:22:36 -05:00
Davide Negretti
e10a08ecfa [DURACOM-190] Fix alignment in vocabulary-treeview
(cherry picked from commit feb2b2be53)
2023-10-26 15:35:38 +00:00
Davide Negretti
00eb24c39d [DURACOM-190] Fix i18n labels in vocabulary-treeview
(cherry picked from commit b321d6f727)
2023-10-26 15:35:38 +00:00
Tim Donohue
3b6dd66680 Merge pull request #2458 from atmire/backport-2423-to-dspace-7_x
[Port to dspace-7_x] Fix inherit policies at item move (Angular)
2023-10-25 16:38:44 -05:00
Tim Donohue
d6951dc8e3 Merge pull request #2576 from tdonohue/port_2362_to_7x
[Port dspace-7_x] Making user menu component themeable
2023-10-25 16:06:50 -05:00
Tim Donohue
1d2cdf75e6 Merge pull request #2575 from tdonohue/port_2530_to_7x
[Port to dspace-7_x] Fix display LogInComponent turning blank when entering wrong username/password combination
2023-10-25 15:18:20 -05:00
Eike Martin Löhden
4945460382 Removed default value from inExpandableNavbar. 2023-10-25 15:16:01 -05:00
Eike Martin Löhden
2ec90b8273 Included user-menu component in custom theme. 2023-10-25 15:15:54 -05:00
Eike Martin Löhden
a4aecce865 Replaced tags for ds-user-menu. 2023-10-25 15:15:48 -05:00
Eike Martin Löhden
de826634c8 Corrected missing semicolon. 2023-10-25 15:15:41 -05:00
Eike Martin Löhden
d04d9fd250 Added themed-user-menu component. 2023-10-25 15:15:25 -05:00
Alexandre Vryghem
86657108dd Merge branch 'fix-display-order-authentication-methods_contribute-7.4' into fix-display-order-authentication-methods_contribute-7.6
# Conflicts:
#	src/app/shared/log-in/log-in.component.html
#	src/app/shared/log-in/log-in.component.ts
2023-10-25 14:27:09 -05:00
Alan Orth
73f21f21e7 Merge pull request #2571 from DSpace/backport-2553-to-dspace-7_x
[Port dspace-7_x] Added Serbian Cyrillic translation and corrected Serbian Latin translаtion
2023-10-24 22:47:50 +03:00
imilos
5ab69af71e Added Serbian cyrilic translation and corrected Serbian latin translation.
(cherry picked from commit aa9e12dcfe)
2023-10-24 18:02:41 +00:00
imilos
884e113168 Added Serbian cyrilic translation and corrected Serbian latin translation.
(cherry picked from commit ad12e5a7f2)
2023-10-24 18:02:41 +00:00
Alan Orth
a92aa05049 Merge pull request #2566 from DSpace/backport-2527-to-dspace-7_x
[Port dspace-7_x] Minor header button improvements
2023-10-23 21:57:44 +03:00
Alexandre Vryghem
d7dba4bfcf Fixed invalid html structure the ExpandableNavbarSectionComponent had an ul tag containing non-li tags
(cherry picked from commit fa56d5dfb7)
2023-10-23 17:50:43 +00:00
Alexandre Vryghem
e82a1ebedb Applied same gap between header icons in the dspace theme and made the search field non-focusable when collapsed
(cherry picked from commit 58d31dd73f)
2023-10-23 17:50:43 +00:00
Alexandre Vryghem
d7f1d37e41 Themed LangSwitchComponent
(cherry picked from commit f9b4460e70)
2023-10-23 17:50:43 +00:00
Alexandre Vryghem
d6de6fee6c Use gap instead of individual paddings for header icons
(cherry picked from commit 9f2a1d048b)
2023-10-23 17:50:43 +00:00
Tim Donohue
7dd9156375 Merge pull request #2564 from DSpace/backport-2561-to-dspace-7_x
[Port dspace-7_x] Fix RequestService test sometimes failing in CICD
2023-10-23 10:00:35 -05:00
Alexandre Vryghem
8428b0549b Fix RequestService test failing because of different lastUpdated time
(cherry picked from commit fb315335c9)
2023-10-23 13:56:07 +00:00
Tim Donohue
33c2c98757 Merge pull request #2560 from DSpace/backport-2439-to-dspace-7_x
[Port dspace-7_x] Fix Request-a-Copy grant form optional message not optional in form validation
2023-10-20 15:14:08 -05:00
Agustina Martinez
e9b70e34d5 Update email-request-copy.component.html
Message is optional: remove req in [disabled]

(cherry picked from commit 94c756d52d)
2023-10-20 20:13:00 +00:00
Tim Donohue
c3ee2ca6c1 Merge pull request #2559 from DSpace/backport-2558-to-dspace-7_x
[Port dspace-7_x] Revert 2454 "Check cssRules before css variables are read from stylesheet"
2023-10-20 12:07:43 -05:00
Tim Donohue
2834ac33a4 Revert "Check cssRules before css variables are read from stylesheet (#2454)"
This reverts commit fa79c358c0.

(cherry picked from commit 6f73b65d53)
2023-10-20 17:07:13 +00:00
Tim Donohue
1eeed36036 Merge pull request #2552 from DSpace/backport-2549-to-dspace-7_x
[Port dspace-7_x] Bump @babel/traverse from 7.21.4 to 7.23.2
2023-10-20 09:57:08 -05:00
dependabot[bot]
bc0629e004 Bump @babel/traverse from 7.21.4 to 7.23.2
Bumps [@babel/traverse](https://github.com/babel/babel/tree/HEAD/packages/babel-traverse) from 7.21.4 to 7.23.2.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.23.2/packages/babel-traverse)

---
updated-dependencies:
- dependency-name: "@babel/traverse"
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
(cherry picked from commit 97f7a5e82a)
2023-10-19 21:45:57 +00:00
Alexandre Vryghem
d473fcdf16 Merge branch 'split-eperson-administration-page_contribute-7.6' into split-eperson-administration-page_contribute-maintenance-7.6 2023-10-19 23:01:48 +02:00
Alexandre Vryghem
2e571767ea 107664: Normalized ePerson & group edit url and moved error message to translation file 2023-10-19 23:01:24 +02:00
Tim Donohue
c25b80abdd Merge pull request #2504 from atmire/Angular-SRR-menu-issues
Fix Angular SSR menu issues
2023-10-19 10:19:27 -05:00
Alan Orth
b5f942b71d Merge pull request #2547 from DSpace/backport-2454-to-dspace-7_x
[Port dspace-7_x] Check cssRules before css variables are read from stylesheet
2023-10-17 22:50:14 +03:00
Gantner, Florian Klaus
21dcef0a42 checkstyle remove unused extra lines
(cherry picked from commit 5f8a9dea34)
2023-10-17 19:17:51 +00:00
Gantner, Florian Klaus
c4a60abd65 check cssRules existence before css variables are get from stylesheet
(cherry picked from commit 367cda2de0)
2023-10-17 19:17:51 +00:00
Alan Orth
fd850164f5 Merge pull request #2546 from DSpace/backport-2534-to-dspace-7_x
[Port dspace-7_x] Bump postcss from 8.4.23 to 8.4.31
2023-10-16 14:20:07 +03:00
dependabot[bot]
8f881dbb52 Bump postcss from 8.4.23 to 8.4.31
Bumps [postcss](https://github.com/postcss/postcss) from 8.4.23 to 8.4.31.
- [Release notes](https://github.com/postcss/postcss/releases)
- [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/postcss/postcss/compare/8.4.23...8.4.31)

---
updated-dependencies:
- dependency-name: postcss
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
(cherry picked from commit 3c5079e9ce)
2023-10-16 07:36:28 +00:00
Alexandre Vryghem
a419956e2a Merge remote-tracking branch 'upstream/dspace-7_x' into split-eperson-administration-page_contribute-maintenance-7.6
# Conflicts:
#	src/app/access-control/epeople-registry/eperson-form/eperson-form.component.html
2023-10-14 17:38:26 +02:00
Alan Orth
3cb23c18e7 Merge pull request #2536 from atmire/issue-2535_hide-add-more-button-submission-if-no-disabled-sections-7.x
Issue 2535 hide add more button submission if no disabled sections 7.x
2023-10-12 09:46:14 +03:00
Alan Orth
59be2ae907 Merge pull request #2543 from DSpace/backport-2531-to-dspace-7_x
[Port dspace-7_x] Fix browse by visual bug
2023-10-12 09:17:48 +03:00
Jens Vannerum
15d2880ca4 Fix browse by visual bug
(cherry picked from commit d0b4e15db4)
2023-10-11 19:04:20 +00:00
Pascal-Nicolas Becker
c03cd03274 Merge pull request #2541 from DSpace/backport-2537-to-dspace-7_x
[Port dspace-7_x] Translate community as 'Bereich' in de.json5
2023-10-11 05:33:28 +02:00
Janne Jensen
ec86bc12bd translate community as 'Bereich' in de.json5
(cherry picked from commit 0139670371)
2023-10-10 20:40:03 +00:00
Marie Verdonck
77dd72b6ef Merge branch 'issue-2535_hide-add-more-button-submission-if-no-disabled-sections-7.4' into issue-2535_hide-add-more-button-submission-if-no-disabled-sections-7.x 2023-10-04 17:30:45 +02:00
Marie Verdonck
709848ee25 Issue#2535: Hide add more button in submission if no disabled sections 2023-10-04 17:27:04 +02:00
Alan Orth
980e254d9a Merge pull request #2528 from DSpace/backport-2385-to-dspace-7_x
[Port dspace-7_x] Move subscription button to DSO edit menu
2023-09-27 10:56:27 +03:00
Yury Bondarenko
6d195f5ffa Update DSO edit menu resolver tests
- Abstract away the different "subsections" ~ DSO type (the tests should not care about this)
  Instead, retrieve sections of interest by ID & assert whether they're there & how they should look
- Test separately for Communities, Collections & Items
- Test newly added menu section

(cherry picked from commit 18b7a9c7de)
2023-09-27 07:02:24 +00:00
Yury Bondarenko
578a427f46 Move subscription button to DSO edit menu
(cherry picked from commit c9558167b2)
2023-09-27 07:02:24 +00:00
Alan Orth
506579cd23 Merge pull request #2510 from atmire/fix-mutliple-api-calls-on-route-change
Fix multiple api calls on route change
2023-09-27 08:12:47 +03:00
Alan Orth
19eec6ac31 Merge pull request #2526 from DSpace/backport-2525-to-dspace-7_x
[Port dspace-7_x] Fix mouse cursor on language dropdown menu
2023-09-26 22:09:17 +03:00
Davide Negretti
2aab4265a5 [DURACOM-185] Fix pointer on language dropdown menu
(cherry picked from commit 6b5708cffd)
2023-09-26 18:40:11 +00:00
Art Lowel
67a6f58865 Merge branch 'fix-mutliple-api-calls-on-route-change-7.6' into fix-mutliple-api-calls-on-route-change 2023-09-26 16:56:17 +02:00
Art Lowel
6a99185214 roll back unintended change to the responseMsToLive for RootDataservice 2023-09-26 16:56:07 +02:00
Art Lowel
d02c5397f8 Merge branch 'fix-mutliple-api-calls-on-route-change-7.6' into fix-mutliple-api-calls-on-route-change 2023-09-26 14:44:39 +02:00
Art Lowel
32fc28ec54 fix dev mode issue where retrieving the login options fails 2023-09-26 14:44:13 +02:00
DSpace Bot
77efe52f4a [Port dspace-7_x] Fix missing or wrong Italian translations (#2522)
[DURACOM-184] fix missing or wrong Italian translations

---------

Co-authored-by: Andrea Barbasso <´andrea.barbasso@4science.com´>
2023-09-26 11:04:49 +03:00
Alan Orth
83beb5474e Merge pull request #2521 from DSpace/backport-2473-to-dspace-7_x
[Port dspace-7_x] Fix some routes not using the correct baseHref
2023-09-26 10:51:51 +03:00
DSpace Bot
1c38d9259a [Port dspace-7_x] Serbian (Latin) translation (#2520)
* Serbian (latin) translation added.

---------

Co-authored-by: imilos <imilos@gmail.com>
2023-09-26 10:08:07 +03:00
Alexandre Vryghem
d6d5a2891c Fix routes not working with baseHref
(cherry picked from commit 18febff7a6)
2023-09-26 07:07:20 +00:00
Hrafn Malmquist
abd6c01a98 Merge pull request #2502 from alanorth/undo-text-capitalize
Don't capitalize metadata values in search
2023-09-26 00:19:07 +01:00
DSpace Bot
f77d01c01f [Port dspace-7_x] Update fi.json5 (#2516)
* Update fi.json5

Two last translations to the Finnish file.

(cherry picked from commit c3a908bccb)
2023-09-25 12:53:45 +03:00
Tim Donohue
fd3f1628ee Merge pull request #2514 from DSpace/backport-2505-to-dspace-7_x
[Port dspace-7_x] remove obsolete label element in metadata-schema.component.html
2023-09-22 14:53:04 -05:00
Sascha Szott
c7fe310d81 removed trailing whitespaces as suggested by reviewer
(cherry picked from commit 43f19e7d91)
2023-09-22 17:57:54 +00:00
Sascha Szott
742b2d920a remove obsolete label element in metadata-schema.component.html
(cherry picked from commit 6847c30e58)
2023-09-22 17:57:54 +00:00
Art Lowel
7a8e2206ae Merge branch 'fix-mutliple-api-calls-on-route-change-7.6' into fix-mutliple-api-calls-on-route-change 2023-09-22 12:01:24 +02:00
Art Lowel
8fbe8c16dc fix issue where invalidateRootCache didn't happen when the page first loaded 2023-09-22 12:00:59 +02:00
Art Lowel
0104f81d54 Merge branch 'fix-mutliple-api-calls-on-route-change-7.6' into fix-mutliple-api-calls-on-route-change 2023-09-22 10:48:46 +02:00
Art Lowel
5ad621b27e fix issue where more than one api call was made on every route change 2023-09-22 10:23:07 +02:00
Alan Orth
47029c0a78 src/app/shared/search: don't capitalize metadata values
Don't capitalize metadata values for display purposes.
2023-09-19 07:42:23 +03:00
Yana De Pauw
5a5f71a3d9 Merge remote-tracking branch 'upstream/dspace-7_x' into Angular-SRR-menu-issues 2023-09-18 16:49:36 +02:00
Yana De Pauw
3e8d180f1b 106974: Angular SSR menu issues 2023-09-18 16:43:32 +02:00
Tim Donohue
2d733732f6 Merge pull request #2498 from DSpace/backport-2451-to-dspace-7_x
[Port dspace-7_x] Graceful shutdown on SIGINT (e.g. from 'pm2 stop').
2023-09-14 12:29:23 -05:00
Mark H. Wood
d6cabd1d01 Document a modified method as required by PR guidelines.
(cherry picked from commit bf9b2b82e1)
2023-09-14 16:07:36 +00:00
Mark H. Wood
46e2f4e22c Properly await termination.
(cherry picked from commit 4449737aed)
2023-09-14 16:07:36 +00:00
Mark H. Wood
15c2af5fbf Graceful shutdown on SIGINT (e.g. from 'pm2 stop').
(cherry picked from commit 6709c3bb5f)
2023-09-14 16:07:36 +00:00
Tim Donohue
a37e0f29b7 Merge pull request #2497 from DSpace/backport-2493-to-dspace-7_x
[Port dspace-7_x] Correct text of help info on edit group page
2023-09-14 11:01:38 -05:00
Tim Donohue
b423b49cac Correct text of help info on edit group page
(cherry picked from commit 49247430e5)
2023-09-14 15:07:21 +00:00
Tim Donohue
bdf7414392 Merge pull request #2488 from alexandrevryghem/fix-display-order-authentication-methods_contribute-7.6
[Port dspace-7_x] Made it possible to reorder the login methods
2023-09-11 16:40:43 -05:00
Tim Donohue
459a43184a Merge pull request #2486 from DSpace/backport-2421-to-dspace-7_x
[Port dspace-7_x] 🚸remove thumbnail from file-upload section and show bitstream format …
2023-09-08 16:13:14 -05:00
Alexandre Vryghem
0905a53db5 Fix innerText still being undefined in ssr mode 2023-09-08 22:35:27 +02:00
Alan Orth
cd93c6eecd Merge pull request #2487 from DSpace/backport-2445-to-dspace-7_x
[Port dspace-7_x] Fix to Metadata Registry create new metadata schema doesn't appear without reload #1081
2023-09-08 23:29:50 +03:00
Hugo Dominguez
161d7e069b 🎨 revert format
(cherry picked from commit 3e5524de69)
2023-09-08 19:50:25 +00:00
Hugo Dominguez
e3ea2cb2b0 🐛 fix bug of caching when add new schema
(cherry picked from commit 9fb9e5848c)
2023-09-08 19:50:25 +00:00
Hugo Dominguez
8d295419c7 ♻️ refactor chain of observables to avoid async issues
(cherry picked from commit 2dc9fd44d7)
2023-09-08 19:50:25 +00:00
Tim Donohue
22538f30dc Update workspaceitem-section-upload-file.model.ts
Fix code comment

(cherry picked from commit 01c8a4d9c3)
2023-09-08 19:33:55 +00:00
Hugo Dominguez
5daf993451 🎨revert unnecessary format
(cherry picked from commit 13e4052c4d)
2023-09-08 19:33:55 +00:00
Hugo Dominguez
b7b3db5ba8 🚸remove thumbnail from file-upload section and show bitstream format and checksum
(cherry picked from commit 4c8ec8a4f2)
2023-09-08 19:33:55 +00:00
Tim Donohue
a0a8607628 Merge pull request #2484 from DSpace/backport-2412-to-dspace-7_x
[Port dspace-7_x] remove redundant cache default values from server.ts
2023-09-08 11:40:43 -05:00
Sascha Szott
3a465ac452 remove redundant cache default values from server.ts
(cherry picked from commit e53abcb69e)
2023-09-08 15:44:21 +00:00
Tim Donohue
97b2eb7a7c Merge pull request #2404 from alexandrevryghem/fix-setStaleByHrefSubtring-not-emitting-after-all-requests-were-stale_contribute-maintenance-7.6
[Port dspace-7_x] Fix `setStaleByHrefSubstring` not emitting true when all requests are stale
2023-09-07 14:39:33 -05:00
Tim Donohue
b46390c315 Merge pull request #2396 from alexandrevryghem/w2p-104312_pass-query-to-external-search-tabs_contribute-maintenance-7.6
[Port dspace-7_x] Search query is not set by default when opening an external sources tab
2023-09-07 12:49:47 -05:00
Tim Donohue
d14e258b5b Merge pull request #2483 from DSpace/backport-2470-to-dspace-7_x
[Port dspace-7_x] Minor pt-PT translation fixes
2023-09-07 11:15:21 -05:00
José Carvalho
1622b25aac Minor pt-PT translation fixes
(cherry picked from commit a6c1120700)
2023-09-07 15:35:02 +00:00
Tim Donohue
d95fa43c6b Merge pull request #2479 from DSpace/backport-2366-to-dspace-7_x
[Port dspace-7_x] CSV export fixes
2023-09-06 16:08:21 -05:00
Kristof De Langhe
4918ff212c 104189: CSV export add fixedFilter
(cherry picked from commit 45ad5f7316)
2023-09-06 19:53:01 +00:00
Kristof De Langhe
c2790584bd 104189: Allow CSV export on related entity search
(cherry picked from commit cac1407f08)
2023-09-06 19:53:01 +00:00
Tim Donohue
162cf94772 Merge pull request #2478 from DSpace/backport-2361-to-dspace-7_x
[Port dspace-7_x] Fix tree not updating when switching between "Browse by Vocabulary" pages
2023-09-06 09:22:00 -05:00
Nona Luypaert
92e0b6dddf Fix VocabularyTreeview not updating + i18n for nsi
(cherry picked from commit b5a70e8f95)
2023-09-05 22:14:44 +00:00
Tim Donohue
5b646af818 Merge pull request #2477 from DSpace/backport-2457-to-dspace-7_x
[Port dspace-7_x] config/config.example.yml: fix example syntax
2023-09-05 16:25:23 -05:00
Alan Orth
8363273f58 config/config.example.yml: fix example syntax
As of DSpace Angular 7.2 the syntax has changed from TypeScript to
YAML.

(cherry picked from commit 9e46b5310b)
2023-09-05 21:24:21 +00:00
Alan Orth
5f5d11cc0b Merge pull request #2476 from DSpace/backport-2432-to-dspace-7_x
[Port dspace-7_x] Fix to Value of dropdown changes automatically on item submission page
2023-09-05 20:16:37 +03:00
Hugo Dominguez
eb38b5877e change test event, click by mousedown on dynamic-scrollable-dropdown.component.spec.ts
(cherry picked from commit 25479e1794)
2023-09-05 17:15:51 +00:00
Hugo Dominguez
f88638e9fe 🐛 Fix Value of dropdown changes automatically on item submission page
(cherry picked from commit 651305952d)
2023-09-05 17:15:51 +00:00
Tim Donohue
cd350ddf5f Merge pull request #2472 from DSpace/backport-2448-to-dspace-7_x
[Port dspace-7_x] Spanish translation updated to 7.6
2023-09-05 09:48:32 -05:00
Alan Orth
2987ad05be Merge pull request #2474 from DSpace/backport-2444-to-dspace-7_x
[Port dspace-7_x] Fix to Mobile navbar hamburger menu doesn't work in Firefox/Safari #2372
2023-09-05 12:01:31 +03:00
Hugo Dominguez
9afbd8d746 🐛 fix when navbar expands on firefox
(cherry picked from commit 60706720e4)
2023-09-05 09:00:34 +00:00
Sergio Fernández Celorio
a4eaf02a47 Some lint errors fixed
(cherry picked from commit 1885638ba6)
2023-09-01 19:05:02 +00:00
Sergio Fernández Celorio
d1ebf07456 Spanish translation updated to 7.6
(cherry picked from commit 4cc4192e93)
2023-09-01 19:05:02 +00:00
Tim Donohue
02c47c3234 Merge pull request #2469 from DSpace/backport-2468-to-dspace-7_x
[Port dspace-7_x] Minor Accessibility Fixes & Enable accessibility scan on more pages
2023-08-30 10:31:36 -05:00
Tim Donohue
63c752b3f4 Fix heading order accessibility issue in search filters/facets
(cherry picked from commit 276d80895e)
2023-08-30 14:40:51 +00:00
Tim Donohue
6df76515ba Minor fixes to cypress tests
(cherry picked from commit 70a7bbe3cb)
2023-08-30 14:40:51 +00:00
Tim Donohue
3cdcdaf475 Fix accessibility of date sliders by adding aria-labels
(cherry picked from commit 2a881791ba)
2023-08-30 14:40:51 +00:00
Tim Donohue
b90d102e5e Update ng2-nouislider and nouislider to latest versions
(cherry picked from commit 91d8b7e4f7)
2023-08-30 14:40:51 +00:00
Tim Donohue
13ead8174a Fix heading order issue with item page & update accessibility tests to prove it now passes
(cherry picked from commit ba244bf6b1)
2023-08-30 14:40:51 +00:00
Tim Donohue
a7a807c0bb Enable excessibility checking of login menu, and remove unnecessary exclusion from header
(cherry picked from commit 339ed63734)
2023-08-30 14:40:51 +00:00
Tim Donohue
baecf2ac11 Reenable accessibility check fixed in #2251
(cherry picked from commit 158ebb0e32)
2023-08-30 14:40:51 +00:00
Tim Donohue
7ebdc43ca2 Update to latest axe-core
(cherry picked from commit 50899f1d1b)
2023-08-30 14:40:51 +00:00
Tim Donohue
13c0cb48ed Update to latest cypress
(cherry picked from commit 68a3323fca)
2023-08-30 14:40:51 +00:00
Alan Orth
6639594f7e Merge pull request #2465 from DSpace/backport-2463-to-dspace-7_x
[Port dspace-7_x] Fix to Pagination position is retained between searches #2159
2023-08-29 11:27:09 +03:00
Alexandre Vryghem
07a2e333ca Implemented i18n cache busting 2023-08-28 21:24:03 +02:00
Tim Donohue
af8c599497 Merge pull request #2466 from DSpace/backport-2447-to-dspace-7_x
[Port dspace-7_x] 2251 accessibility issues on the community list page
2023-08-28 14:13:02 -05:00
Alan Orth
0542e9b2fd Merge pull request #2467 from DSpace/backport-2399-to-dspace-7_x
[Port dspace-7_x] fix(i18n): curation-task.task.registerdoi.label
2023-08-28 21:07:04 +03:00
Mirko Scherf
2a55e36082 fix(i18m): curation-task.task.registerdoi.label
changed curation-task.task.register-doi.label back to
curation-task.task.registerdoi.label and added German translation

(cherry picked from commit 0ec72f679b)
2023-08-28 18:02:28 +00:00
Hrafn Malmquist
8a5d6897c4 Reorder instance method to come after member declaration
(cherry picked from commit e59913acab)
2023-08-28 14:44:18 +00:00
Hrafn Malmquist
e89a277702 Improve documentation
(cherry picked from commit d9b6e9d81f)
2023-08-28 14:44:18 +00:00
Hrafn Malmquist
9fc4e213df Add trackby function so cdktree can differentiate between new and old nodes
(cherry picked from commit 5f71de885b)
2023-08-28 14:44:18 +00:00
Hrafn Malmquist
46ac61dcac Replace h2 with a h1
(cherry picked from commit 05c53ad1d4)
2023-08-28 14:44:18 +00:00
Hrafn Malmquist
eef98d70c3 Replace h5 with a span
(cherry picked from commit 5ef4a827f5)
2023-08-28 14:44:18 +00:00
Hugo Dominguez
5c669fb1b7 add first page condition when search submit
(cherry picked from commit 044230209c)
2023-08-28 14:33:34 +00:00
Hugo Dominguez
a343991e74 🐛 go to first page after submit search
(cherry picked from commit 88a7088b47)
2023-08-28 14:33:34 +00:00
Tim Donohue
83de2c5769 Merge pull request #2462 from tdonohue/port_2395
[Port dspace-7_x] Fix themed components duplicating themself when switching themes
2023-08-25 13:28:03 -05:00
Alexandre Vryghem
8b57a2f6af Merge branch 'fix-ngonchanges-not-working-for-themed-components_contribute-7.2' into fix-ngonchanges-not-working-for-themed-components_contribute-7.4
# Conflicts:
#	src/app/app.component.ts
2023-08-25 12:34:10 -05:00
Alexandre Vryghem
7352d9e273 Reset to base theme when no default theme is set and leaving UUID/handle theme 2023-08-25 12:22:51 -05:00
Alexandre Vryghem
4e14bc0b78 101577: Ensure the component is always destroyed before rendering the new component 2023-08-25 12:22:32 -05:00
Alexandre Vryghem
0e289b3f39 103176: Fix vcr not being defined yet in OnInit hook 2023-08-25 12:19:30 -05:00
Koen Pauwels
815425c101 Fix lint issue. 2023-08-25 10:18:09 +02:00
Alan Orth
6ad641f4e2 Merge pull request #2460 from DSpace/backport-2434-to-dspace-7_x
[Port dspace-7_x] Input type list doesn't work correctly if multiple value-pairs have the same pair value
2023-08-25 10:37:33 +03:00
Alexandre Vryghem
7f00253d3d 105265: Made the DsDynamicListComponent's checkbox & radio buttons ids unique for each metadata field to prevent other value-pairs having the same id
(cherry picked from commit 4c419e1eee)
2023-08-25 06:17:16 +00:00
Tim Donohue
03d17678e2 Merge pull request #2459 from DSpace/backport-2449-to-dspace-7_x
[Port dspace-7_x] Update pl.json5
2023-08-24 16:03:53 -05:00
Michał Dykas
0efb95825d Update pl.json5 fix test 2 spaces instead of 1
(cherry picked from commit f58a7a2e9b)
2023-08-24 19:58:39 +00:00
Michał Dykas
95cde220e6 Update pl.json5 fix issues from tests
(cherry picked from commit 93299ec83d)
2023-08-24 19:58:39 +00:00
Michał Dykas
964066056c Update pl.json5
Translation update of 2 spaces instead of 3

(cherry picked from commit cfd753f928)
2023-08-24 19:58:39 +00:00
Michał Dykas
22db36f938 Update pl.json5
Update from 7.4 to 7.6 version

(cherry picked from commit b439ab4484)
2023-08-24 19:58:39 +00:00
Tim Donohue
9df4d660e7 Merge pull request #2453 from tdonohue/fix_demo_urls_7_x
[Port dspace-7_x] Replace mentions of demo7.dspace.org and api7.dspace.org with new demo URLs
2023-08-24 12:41:02 -05:00
Koen Pauwels
a5b30ea3c2 103818 Adjusted "Inherit policies" tooltip 2023-08-24 15:27:52 +02:00
Koen Pauwels
2078b7593a 103818 Add warning tooltip to "Inherit policies" checkbox on item move page 2023-08-24 15:27:52 +02:00
Tim Donohue
fe8429ebbe Enable new skip merge commit feature 2023-08-23 17:08:44 -05:00
Tim Donohue
8feeedfc3a Merge pull request #2455 from DSpace/backport-2438-to-dspace-7_x
[Port dspace-7_x] 2437 Correct and clarify commented-out code in several custom components
2023-08-23 16:07:17 -05:00
Hardy Pottinger
3292222e47 fix lint error in workflow-item-send-back.component.ts
(cherry picked from commit 518cc714f2)
2023-08-23 19:50:22 +00:00
Hardy Pottinger
36868c06f0 2437 Correct and clarify commented-out code in several custom components
- Correct the path in commented-out code in the custom theme's
  components
- Clarify that the workflow-item-sen-back.component.scss file is a stub
- It has no corresponding SCSS file in the base theme

(cherry picked from commit 0906234a29)
2023-08-23 19:50:22 +00:00
Tim Donohue
5853e49bd0 Update default configs to use https://demo.dspace.org/server/ 2023-08-22 16:44:10 -05:00
Tim Donohue
2fd53c7ad2 Replace mentions of demo7.dspace.org and api7.dspace.org with demo or sandbox 2023-08-22 16:38:00 -05:00
Tim Donohue
63345a335a Merge pull request #2428 from DSpace/backport-2409-to-dspace-7_x
[Port dspace-7_x] Bump word-wrap from 1.2.3 to 1.2.5
2023-08-09 10:07:14 -05:00
dependabot[bot]
bbb50f2858 Bump word-wrap from 1.2.3 to 1.2.5
Bumps [word-wrap](https://github.com/jonschlinkert/word-wrap) from 1.2.3 to 1.2.5.
- [Release notes](https://github.com/jonschlinkert/word-wrap/releases)
- [Commits](https://github.com/jonschlinkert/word-wrap/compare/1.2.3...1.2.5)

---
updated-dependencies:
- dependency-name: word-wrap
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
(cherry picked from commit 2fec33e70a)
2023-08-08 21:42:41 +00:00
Tim Donohue
3e31c1eee3 Merge pull request #2427 from tdonohue/port_2048
[Port dspace-7_x] ProcessDetailComponent test improvement
2023-08-08 16:25:21 -05:00
Kristof De Langhe
cfcf93ecf8 104126: ProcessDetailComponent test improvement 2023-08-08 15:08:49 -05:00
Tim Donohue
74c2f3d9bb Merge pull request #2405 from alexandrevryghem/fix-collection-form-bugs_contribute-maintenance-7.6
[Port dspace-7_x] Fix minor collection form bugs
2023-08-04 12:48:34 -05:00
Tim Donohue
f22fcc7b3c Merge pull request #2420 from tdonohue/port_2388
[Port dspace-7_x] fix(i18n): add and update missing status strings
2023-08-04 12:25:44 -05:00
Mirko Scherf
c3b9a1d5c6 fix(i18n): add and update missing status strings
New strings for status filter entries:
search.filters.namedresourcetype.*
Refactored strings introduced with #2068 (refactor badged),
e.g. mydspace.status.archived -> mydspace.status.mydspaceArchived
2023-08-04 11:19:55 -05:00
Alan Orth
d072ae7027 Merge pull request #2419 from DSpace/backport-2378-to-dspace-7_x
[Port dspace-7_x] Show error message from the password validation response
2023-08-04 16:04:15 +03:00
milanmajchrak
f746d45ac1 Show error message from the error response
(cherry picked from commit e6546b4499)
2023-08-04 12:21:30 +00:00
Tim Donohue
1fd917dd4a Merge pull request #2418 from DSpace/backport-2344-to-dspace-7_x
[Port dspace-7_x] Catch and handle unsuccessful "convert rels to items" responses
2023-08-03 16:38:44 -05:00
Tim Donohue
99e349b91f Merge pull request #2392 from alexandrevryghem/fix-mathjax-displaying-twiced-in-ui_contribute-maintenance-7.6
[Port dspace-7_x] Fixed MathJax code being displayed twice by `dsMarkdown` pipe
2023-08-03 16:09:56 -05:00
Kim Shepherd
a7ed053d15 catch and handle unsuccessful "convert rels to items" responses
(cherry picked from commit a35b7d8356)
2023-08-03 20:03:27 +00:00
Tim Donohue
99c6dd1829 Merge pull request #2347 from alanorth/finnish-language-strings
src/assets/i18n: update Finnish language strings
2023-08-03 12:56:22 -05:00
Tim Donohue
0a48b09bd7 Merge pull request #2416 from tdonohue/port_of_2381
[Port dspace-7_x] refactor: rename aletr-type.ts to alert-type.ts
2023-08-03 12:37:18 -05:00
Mirko Scherf
0dc74165dc refactor: rename aletr-type.ts to alert-type.ts 2023-08-03 11:51:48 -05:00
Tim Donohue
9cbb634245 Merge pull request #2415 from DSpace/backport-2363-to-dspace-7_x
[Port dspace-7_x] Bump semver from 5.7.1 to 5.7.2
2023-08-02 15:55:32 -05:00
dependabot[bot]
7c379db7ee Bump semver from 5.7.1 to 5.7.2
Bumps [semver](https://github.com/npm/node-semver) from 5.7.1 to 5.7.2.
- [Release notes](https://github.com/npm/node-semver/releases)
- [Changelog](https://github.com/npm/node-semver/blob/v5.7.2/CHANGELOG.md)
- [Commits](https://github.com/npm/node-semver/compare/v5.7.1...v5.7.2)

---
updated-dependencies:
- dependency-name: semver
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
(cherry picked from commit 0b0c60e38c)
2023-08-02 20:06:40 +00:00
Alexandre Vryghem
3dc73f9021 Properly handle AuthMethod subscription in LogInComponent 2023-08-02 21:25:58 +02:00
Alexandre Vryghem
94ceee9080 Merge remote-tracking branch 'templates/dspace-7.6' into fix-display-order-authentication-methods_contribute-7.6
# Conflicts:
#	src/app/shared/log-in/log-in.component.html
#	src/app/shared/log-in/methods/log-in-external-provider/log-in-external-provider.component.spec.ts
#	src/app/shared/log-in/methods/oidc/log-in-oidc.component.html
#	src/app/shared/log-in/methods/oidc/log-in-oidc.component.spec.ts
#	src/app/shared/log-in/methods/orcid/log-in-orcid.component.html
#	src/app/shared/log-in/methods/password/log-in-password.component.ts
#	src/app/shared/log-in/methods/shibboleth/log-in-shibboleth.component.html
#	src/app/shared/log-in/methods/shibboleth/log-in-shibboleth.component.spec.ts
2023-08-02 21:01:36 +02:00
Alexandre Vryghem
71cf66ecf4 Fix display order of authentication methods 2023-08-02 10:00:43 +02:00
Alan Orth
1b9656b135 src/assets/i18n: update Finnish language strings
Contributed by Reeta Kuukoski from the National Library of Finland.
2023-07-31 12:32:14 +03:00
Tim Donohue
85acdcb9c5 Merge pull request #2407 from DSpace/backport-2406-to-dspace-7_x
[Port dspace-7_x] Add GitHub action to automatically create a port PR (based on label) & minor bug fixes
2023-07-28 15:25:02 -05:00
Tim Donohue
867ae9c341 Minor update to label_merge_conflicts to ignore any errors (seem random at this time)
(cherry picked from commit d75d12b423)
2023-07-28 20:11:29 +00:00
Tim Donohue
4965bdee5f Add action to automatically create a port PR when specified
(cherry picked from commit 338b63ebb8)
2023-07-28 20:11:29 +00:00
Alexandre Vryghem
ebaccc055e Merge branch 'fix-collection-form-bugs_contribute-7.4' into fix-collection-form-bugs_contribute-7.6
# Conflicts:
#	src/app/access-control/epeople-registry/eperson-form/eperson-form.component.html
2023-07-28 00:14:58 +02:00
Alexandre Vryghem
273be5bd81 Merge remote-tracking branch 'contributions/fix-setStaleByHrefSubtring-not-emitting-after-all-requests-were-stale_contribute-7.4' into fix-setStaleByHrefSubtring-not-emitting-after-all-requests-were-stale_contribute-7.6
# Conflicts:
#	src/app/core/data/request.service.ts
2023-07-27 23:57:18 +02:00
Alexandre Vryghem
5062e46433 104312: Add missing query @Input() to ThemedDynamicLookupRelationExternalSourceTabComponent 2023-07-25 18:26:47 +02:00
Alexandre Vryghem
9b1d18bd32 Merge tag 'dspace-7.6' into w2p-104312_pass-query-to-external-search-tabs_contribute-7.6 2023-07-25 18:26:18 +02:00
Alexandre Vryghem
15656b03ce Fix mathjax code being displayed twice
This was due to sanitizeHtml rendering the mjx-assistive-mml tag as text. This tag is used by screen readers and therefor we should allow it to be rendered
2023-07-21 23:18:40 +02:00
Alexandre Vryghem
75ec046bba Hide the delete EPerson/Group buttons when the user can't delete the EPerson/Group 2023-07-21 18:34:42 +02:00
Alexandre Vryghem
998e1fac8d Fix spacing issues for EPerson form buttons 2023-07-21 18:34:42 +02:00
Alexandre Vryghem
9ac19d40fc Cleanup access-control components
- Use the same methods to retrieve the access-control urls
- Fix EPersonDataService.startEditingNewEPerson returning the incorrect link
2023-07-21 18:34:41 +02:00
Alexandre Vryghem
2a35180a1b Created separate pages for edit & create EPersons 2023-07-21 17:15:10 +02:00
Alexandre Vryghem
648925f3e1 104312: DsDynamicLookupRelationExternalSourceTabComponent should have the form value already filled in the search input 2023-07-19 14:04:34 +02:00
Tim Donohue
4f0e1d6de1 Merge pull request #2369 from tdonohue/port_2358_2359
Port recent GitHub Actions changes to dspace-7_x branch
2023-07-14 16:56:32 -05:00
Tim Donohue
1809f0585c Split docker images into separate jobs to run in parallel. Ensure 'main' codebase is tagged as 'latest' 2023-07-14 15:41:57 -05:00
Tim Donohue
a484379f69 Ensure codescan and label_merge_conflicts run on maintenance branches 2023-07-14 15:41:41 -05:00
Tim Donohue
7bf4da55cf Enable Pull Request Opened action to assign PRs to their creator 2023-07-14 15:41:26 -05:00
Alan Orth
a079ed729c Merge pull request #2354 from alanorth/request-copy-component-path
src/app: fix path to deny-request-copy component
2023-07-06 20:45:56 +03:00
Alan Orth
3a48ed390b src/app: fix path to deny-request-copy component
The themed-deny-request-copy.component erroneously includes the cus-
tom theme's deny-request-copy component instead of its own.

Closes: https://github.com/DSpace/dspace-angular/issues/2351
2023-07-06 20:41:52 +03:00
Alexandre Vryghem
cf77726866 Fix enter not submitting collection form correctly
Fixed it for communities, collections, ePersons & groups
2023-07-01 01:01:57 +02:00
Alexandre Vryghem
b2b1782cd8 Hide entity field in collection form when entities aren't initialized 2023-06-30 22:43:46 +02:00
Alexandre Vryghem
02a20c8862 103236: Added tests for setStaleByHrefSubstring 2023-06-30 17:01:25 +02:00
Art Lowel
ae6b183fae 103236: fix issue where setStaleByHrefSubtring wouldn't emit after all requests were stale 2023-06-30 16:56:15 +02:00
Tim Donohue
884aa07430 Update version tag for development of next release 2023-06-23 13:04:19 -05:00
337 changed files with 15176 additions and 4584 deletions

View File

@@ -1,26 +0,0 @@
# This workflow runs whenever a new pull request is created
# TEMPORARILY DISABLED. Unfortunately this doesn't work for PRs created from forked repositories (which is how we tend to create PRs).
# There is no known workaround yet. See https://github.community/t/how-to-use-github-token-for-prs-from-forks/16818
name: Pull Request opened
# Only run for newly opened PRs against the "main" branch
on:
pull_request:
types: [opened]
branches:
- main
jobs:
automation:
runs-on: ubuntu-latest
steps:
# Assign the PR to whomever created it. This is useful for visualizing assignments on project boards
# See https://github.com/marketplace/actions/pull-request-assigner
- name: Assign PR to creator
uses: thomaseizinger/assign-pr-creator-action@v1.0.0
# Note, this authentication token is created automatically
# See: https://docs.github.com/en/actions/configuring-and-managing-workflows/authenticating-with-the-github_token
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
# Ignore errors. It is possible the PR was created by someone who cannot be assigned
continue-on-error: true

View File

@@ -5,12 +5,16 @@
# because CodeQL requires a fresh build with all tests *disabled*.
name: "Code Scanning"
# Run this code scan for all pushes / PRs to main branch. Also run once a week.
# Run this code scan for all pushes / PRs to main or maintenance branches. Also run once a week.
on:
push:
branches: [ main ]
branches:
- main
- 'dspace-**'
pull_request:
branches: [ main ]
branches:
- main
- 'dspace-**'
# Don't run if PR is only updating static documentation
paths-ignore:
- '**/*.md'

View File

@@ -15,29 +15,35 @@ on:
permissions:
contents: read # to fetch code (actions/checkout)
env:
# Define tags to use for Docker images based on Git tags/branches (for docker/metadata-action)
# For a new commit on default branch (main), use the literal tag 'latest' on Docker image.
# For a new commit on other branches, use the branch name as the tag for Docker image.
# For a new tag, copy that tag name as the tag for Docker image.
IMAGE_TAGS: |
type=raw,value=latest,enable=${{ endsWith(github.ref, github.event.repository.default_branch) }}
type=ref,event=branch,enable=${{ !endsWith(github.ref, github.event.repository.default_branch) }}
type=ref,event=tag
# Define default tag "flavor" for docker/metadata-action per
# https://github.com/docker/metadata-action#flavor-input
# We manage the 'latest' tag ourselves to the 'main' branch (see settings above)
TAGS_FLAVOR: |
latest=false
# Architectures / Platforms for which we will build Docker images
# If this is a PR, we ONLY build for AMD64. For PRs we only do a sanity check test to ensure Docker builds work.
# If this is NOT a PR (e.g. a tag or merge commit), also build for ARM64.
PLATFORMS: linux/amd64${{ github.event_name != 'pull_request' && ', linux/arm64' || '' }}
jobs:
docker:
###############################################
# Build/Push the 'dspace/dspace-angular' image
###############################################
dspace-angular:
# Ensure this job never runs on forked repos. It's only executed for 'dspace/dspace-angular'
if: github.repository == 'dspace/dspace-angular'
runs-on: ubuntu-latest
env:
# Define tags to use for Docker images based on Git tags/branches (for docker/metadata-action)
# For a new commit on default branch (main), use the literal tag 'dspace-7_x' on Docker image.
# For a new commit on other branches, use the branch name as the tag for Docker image.
# For a new tag, copy that tag name as the tag for Docker image.
IMAGE_TAGS: |
type=raw,value=dspace-7_x,enable=${{ endsWith(github.ref, github.event.repository.default_branch) }}
type=ref,event=branch,enable=${{ !endsWith(github.ref, github.event.repository.default_branch) }}
type=ref,event=tag
# Define default tag "flavor" for docker/metadata-action per
# https://github.com/docker/metadata-action#flavor-input
# We turn off 'latest' tag by default.
TAGS_FLAVOR: |
latest=false
# Architectures / Platforms for which we will build Docker images
# If this is a PR, we ONLY build for AMD64. For PRs we only do a sanity check test to ensure Docker builds work.
# If this is NOT a PR (e.g. a tag or merge commit), also build for ARM64.
PLATFORMS: linux/amd64${{ github.event_name != 'pull_request' && ', linux/arm64' || '' }}
steps:
# https://github.com/actions/checkout
@@ -61,9 +67,6 @@ jobs:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_ACCESS_TOKEN }}
###############################################
# Build/Push the 'dspace/dspace-angular' image
###############################################
# https://github.com/docker/metadata-action
# Get Metadata for docker_build step below
- name: Sync metadata (tags, labels) from GitHub to Docker for 'dspace-angular' image
@@ -77,7 +80,7 @@ jobs:
# https://github.com/docker/build-push-action
- name: Build and push 'dspace-angular' image
id: docker_build
uses: docker/build-push-action@v3
uses: docker/build-push-action@v4
with:
context: .
file: ./Dockerfile
@@ -89,9 +92,36 @@ jobs:
tags: ${{ steps.meta_build.outputs.tags }}
labels: ${{ steps.meta_build.outputs.labels }}
#####################################################
# Build/Push the 'dspace/dspace-angular' image ('-dist' tag)
#####################################################
#############################################################
# Build/Push the 'dspace/dspace-angular' image ('-dist' tag)
#############################################################
dspace-angular-dist:
# Ensure this job never runs on forked repos. It's only executed for 'dspace/dspace-angular'
if: github.repository == 'dspace/dspace-angular'
runs-on: ubuntu-latest
steps:
# https://github.com/actions/checkout
- name: Checkout codebase
uses: actions/checkout@v3
# https://github.com/docker/setup-buildx-action
- name: Setup Docker Buildx
uses: docker/setup-buildx-action@v2
# https://github.com/docker/setup-qemu-action
- name: Set up QEMU emulation to build for multiple architectures
uses: docker/setup-qemu-action@v2
# https://github.com/docker/login-action
- name: Login to DockerHub
# Only login if not a PR, as PRs only trigger a Docker build and not a push
if: github.event_name != 'pull_request'
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_ACCESS_TOKEN }}
# https://github.com/docker/metadata-action
# Get Metadata for docker_build_dist step below
- name: Sync metadata (tags, labels) from GitHub to Docker for 'dspace-angular-dist' image
@@ -107,7 +137,7 @@ jobs:
- name: Build and push 'dspace-angular-dist' image
id: docker_build_dist
uses: docker/build-push-action@v3
uses: docker/build-push-action@v4
with:
context: .
file: ./Dockerfile.dist

View File

@@ -1,11 +1,12 @@
# This workflow checks open PRs for merge conflicts and labels them when conflicts are found
name: Check for merge conflicts
# Run whenever the "main" branch is updated
# NOTE: This means merge conflicts are only checked for when a PR is merged to main.
# Run this for all pushes (i.e. merges) to 'main' or maintenance branches
on:
push:
branches: [ main ]
branches:
- main
- 'dspace-**'
# So that the `conflict_label_name` is removed if conflicts are resolved,
# we allow this to run for `pull_request_target` so that github secrets are available.
pull_request_target:
@@ -24,6 +25,8 @@ jobs:
# See: https://github.com/prince-chrismc/label-merge-conflicts-action
- name: Auto-label PRs with merge conflicts
uses: prince-chrismc/label-merge-conflicts-action@v3
# Ignore any failures -- may occur (randomly?) for older, outdated PRs.
continue-on-error: true
# Add "merge conflict" label if a merge conflict is detected. Remove it when resolved.
# Note, the authentication token is created automatically
# See: https://docs.github.com/en/actions/configuring-and-managing-workflows/authenticating-with-the-github_token

View File

@@ -0,0 +1,46 @@
# This workflow will attempt to port a merged pull request to
# the branch specified in a "port to" label (if exists)
name: Port merged Pull Request
# Only run for merged PRs against the "main" or maintenance branches
# We allow this to run for `pull_request_target` so that github secrets are available
# (This is required when the PR comes from a forked repo)
on:
pull_request_target:
types: [ closed ]
branches:
- main
- 'dspace-**'
permissions:
contents: write # so action can add comments
pull-requests: write # so action can create pull requests
jobs:
port_pr:
runs-on: ubuntu-latest
# Don't run on closed *unmerged* pull requests
if: github.event.pull_request.merged
steps:
# Checkout code
- uses: actions/checkout@v3
# Port PR to other branch (ONLY if labeled with "port to")
# See https://github.com/korthout/backport-action
- name: Create backport pull requests
uses: korthout/backport-action@v1
with:
# Trigger based on a "port to [branch]" label on PR
# (This label must specify the branch name to port to)
label_pattern: '^port to ([^ ]+)$'
# Title to add to the (newly created) port PR
pull_title: '[Port ${target_branch}] ${pull_title}'
# Description to add to the (newly created) port PR
pull_description: 'Port of #${pull_number} by @${pull_author} to `${target_branch}`.'
# Copy all labels from original PR to (newly created) port PR
# NOTE: The labels matching 'label_pattern' are automatically excluded
copy_labels_pattern: '.*'
# Skip any merge commits in the ported PR. This means only non-merge commits are cherry-picked to the new PR
merge_commits: 'skip'
# Use a personal access token (PAT) to create PR as 'dspace-bot' user.
# A PAT is required in order for the new PR to trigger its own actions (for CI checks)
github_token: ${{ secrets.PR_PORT_TOKEN }}

View File

@@ -0,0 +1,24 @@
# This workflow runs whenever a new pull request is created
name: Pull Request opened
# Only run for newly opened PRs against the "main" or maintenance branches
# We allow this to run for `pull_request_target` so that github secrets are available
# (This is required to assign a PR back to the creator when the PR comes from a forked repo)
on:
pull_request_target:
types: [ opened ]
branches:
- main
- 'dspace-**'
permissions:
pull-requests: write
jobs:
automation:
runs-on: ubuntu-latest
steps:
# Assign the PR to whomever created it. This is useful for visualizing assignments on project boards
# See https://github.com/toshimaru/auto-author-assign
- name: Assign PR to creator
uses: toshimaru/auto-author-assign@v1.6.2

View File

@@ -157,8 +157,8 @@ DSPACE_UI_SSL => DSPACE_SSL
The same settings can also be overwritten by setting system environment variables instead, E.g.:
```bash
export DSPACE_HOST=api7.dspace.org
export DSPACE_UI_PORT=4200
export DSPACE_HOST=demo.dspace.org
export DSPACE_UI_PORT=4000
```
The priority works as follows: **environment variable** overrides **variable in `.env` file** overrides external config set by `DSPACE_APP_CONFIG_PATH` overrides **`config.(prod or dev).yml`**
@@ -288,7 +288,7 @@ E2E tests (aka integration tests) use [Cypress.io](https://www.cypress.io/). Con
The test files can be found in the `./cypress/integration/` folder.
Before you can run e2e tests, two things are REQUIRED:
1. You MUST be running the DSpace backend (i.e. REST API) locally. The e2e tests will *NOT* succeed if run against our demo REST API (https://api7.dspace.org/server/), as that server is uncontrolled and may have content added/removed at any time.
1. You MUST be running the DSpace backend (i.e. REST API) locally. The e2e tests will *NOT* succeed if run against our demo/sandbox REST API (https://demo.dspace.org/server/ or https://sandbox.dspace.org/server/), as those sites may have content added/removed at any time.
* After starting up your backend on localhost, make sure either your `config.prod.yml` or `config.dev.yml` has its `rest` settings defined to use that localhost backend.
* If you'd prefer, you may instead use environment variables as described at [Configuring](#configuring). For example:
```

View File

@@ -22,7 +22,7 @@ ui:
# 'synced' with the 'dspace.server.url' setting in your backend's local.cfg.
rest:
ssl: true
host: api7.dspace.org
host: demo.dspace.org
port: 443
# NOTE: Space is capitalized because 'namespace' is a reserved string in TypeScript
nameSpace: /server
@@ -208,6 +208,9 @@ languages:
- code: pt-BR
label: Português do Brasil
active: true
- code: sr-lat
label: Srpski (lat)
active: true
- code: fi
label: Suomi
active: true
@@ -232,6 +235,9 @@ languages:
- code: el
label: Ελληνικά
active: true
- code: sr-cyr
label: Српски
active: true
- code: uk
label: раї́нська
active: true
@@ -292,33 +298,33 @@ themes:
#
# # A theme with a handle property will match the community, collection or item with the given
# # handle, and all collections and/or items within it
# - name: 'custom',
# handle: '10673/1233'
# - name: custom
# handle: 10673/1233
#
# # A theme with a regex property will match the route using a regular expression. If it
# # matches the route for a community or collection it will also apply to all collections
# # and/or items within it
# - name: 'custom',
# regex: 'collections\/e8043bc2.*'
# - name: custom
# regex: collections\/e8043bc2.*
#
# # A theme with a uuid property will match the community, collection or item with the given
# # ID, and all collections and/or items within it
# - name: 'custom',
# uuid: '0958c910-2037-42a9-81c7-dca80e3892b4'
# - name: custom
# uuid: 0958c910-2037-42a9-81c7-dca80e3892b4
#
# # The extends property specifies an ancestor theme (by name). Whenever a themed component is not found
# # in the current theme, its ancestor theme(s) will be checked recursively before falling back to default.
# - name: 'custom-A',
# extends: 'custom-B',
# - name: custom-A
# extends: custom-B
# # Any of the matching properties above can be used
# handle: '10673/34'
# handle: 10673/34
#
# - name: 'custom-B',
# extends: 'custom',
# handle: '10673/12'
# - name: custom-B
# extends: custom
# handle: 10673/12
#
# # A theme with only a name will match every route
# name: 'custom'
# name: custom
#
# # This theme will use the default bootstrap styling for DSpace components
# - name: BASE_THEME_NAME
@@ -379,4 +385,4 @@ vocabularies:
# Default collection/community sorting order at Advanced search, Create/update community and collection when there are not a query.
comcolSelectionSort:
sortField: 'dc.title'
sortDirection: 'ASC'
sortDirection: 'ASC'

View File

@@ -1,5 +1,5 @@
rest:
ssl: true
host: api7.dspace.org
host: demo.dspace.org
port: 443
nameSpace: /server

View File

@@ -1,4 +1,3 @@
import { Options } from 'cypress-axe';
import { testA11y } from 'cypress/support/utils';
describe('Community List Page', () => {
@@ -13,13 +12,6 @@ describe('Community List Page', () => {
cy.get('[data-test="expand-button"]').click({ multiple: true });
// Analyze <ds-community-list-page> for accessibility issues
// Disable heading-order checks until it is fixed
testA11y('ds-community-list-page',
{
rules: {
'heading-order': { enabled: false }
}
} as Options
);
testA11y('ds-community-list-page');
});
});

View File

@@ -11,8 +11,7 @@ describe('Header', () => {
testA11y({
include: ['ds-header'],
exclude: [
['#search-navbar-container'], // search in navbar has duplicative ID. Will be fixed in #1174
['.dropdownLogin'] // "Log in" link has color contrast issues. Will be fixed in #1149
['#search-navbar-container'] // search in navbar has duplicative ID. Will be fixed in #1174
],
});
});

View File

@@ -1,4 +1,3 @@
import { Options } from 'cypress-axe';
import { TEST_ENTITY_PUBLICATION } from 'cypress/support/e2e';
import { testA11y } from 'cypress/support/utils';
@@ -19,13 +18,16 @@ describe('Item Page', () => {
cy.get('ds-item-page').should('be.visible');
// Analyze <ds-item-page> for accessibility issues
// Disable heading-order checks until it is fixed
testA11y('ds-item-page',
{
rules: {
'heading-order': { enabled: false }
}
} as Options
);
testA11y('ds-item-page');
});
it('should pass accessibility tests on full item page', () => {
cy.visit(ENTITYPAGE + '/full');
// <ds-full-item-page> tag must be loaded
cy.get('ds-full-item-page').should('be.visible');
// Analyze <ds-full-item-page> for accessibility issues
testA11y('ds-full-item-page');
});
});

View File

@@ -1,4 +1,5 @@
import { TEST_ADMIN_PASSWORD, TEST_ADMIN_USER, TEST_ENTITY_PUBLICATION } from 'cypress/support/e2e';
import { testA11y } from 'cypress/support/utils';
const page = {
openLoginMenu() {
@@ -123,4 +124,15 @@ describe('Login Modal', () => {
cy.location('pathname').should('eq', '/forgot');
cy.get('ds-forgot-email').should('exist');
});
it('should pass accessibility tests', () => {
cy.visit('/');
page.openLoginMenu();
cy.get('ds-log-in').should('exist');
// Analyze <ds-log-in> for accessibility issues
testA11y('ds-log-in');
});
});

View File

@@ -19,21 +19,7 @@ describe('My DSpace page', () => {
cy.get('.filter-toggle').click({ multiple: true });
// Analyze <ds-my-dspace-page> for accessibility issues
testA11y(
{
include: ['ds-my-dspace-page'],
exclude: [
['nouislider'] // Date filter slider is missing ARIA labels. Will be fixed by #1175
],
},
{
rules: {
// Search filters fail these two "moderate" impact rules
'heading-order': { enabled: false },
'landmark-unique': { enabled: false }
}
} as Options
);
testA11y('ds-my-dspace-page');
});
it('should have a working detailed view that passes accessibility tests', () => {

View File

@@ -1,8 +1,13 @@
import { testA11y } from 'cypress/support/utils';
describe('PageNotFound', () => {
it('should contain element ds-pagenotfound when navigating to page that doesnt exist', () => {
// request an invalid page (UUIDs at root path aren't valid)
cy.visit('/e9019a69-d4f1-4773-b6a3-bd362caa46f2', { failOnStatusCode: false });
cy.get('ds-pagenotfound').should('be.visible');
// Analyze <ds-pagenotfound> for accessibility issues
testA11y('ds-pagenotfound');
});
it('should not contain element ds-pagenotfound when navigating to existing page', () => {

View File

@@ -27,21 +27,7 @@ describe('Search Page', () => {
cy.get('[data-test="filter-toggle"]').click({ multiple: true });
// Analyze <ds-search-page> for accessibility issues
testA11y(
{
include: ['ds-search-page'],
exclude: [
['nouislider'] // Date filter slider is missing ARIA labels. Will be fixed by #1175
],
},
{
rules: {
// Search filters fail these two "moderate" impact rules
'heading-order': { enabled: false },
'landmark-unique': { enabled: false }
}
} as Options
);
testA11y('ds-search-page');
});
it('should have a working grid view that passes accessibility tests', () => {

View File

@@ -177,6 +177,8 @@ function generateViewEvent(uuid: string, dsoType: string): void {
[XSRF_REQUEST_HEADER] : csrfToken,
// use a known public IP address to avoid being seen as a "bot"
'X-Forwarded-For': '1.1.1.1',
// Use a user-agent of a Firefox browser on Windows. This again avoids being seen as a "bot"
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/119.0',
},
//form: true, // indicates the body should be form urlencoded
body: { targetId: uuid, targetType: dsoType },

View File

@@ -101,8 +101,8 @@ and the backend at http://localhost:8080/server/
## Run DSpace Angular dist build with DSpace Demo site backend
This allows you to run the Angular UI in *production* mode, pointing it at the demo backend
(https://api7.dspace.org/server/).
This allows you to run the Angular UI in *production* mode, pointing it at the demo or sandbox backend
(https://demo.dspace.org/server/ or https://sandbox.dspace.org/server/).
```
docker-compose -f docker/docker-compose-dist.yml pull

View File

@@ -24,7 +24,7 @@ services:
# This is because Server Side Rendering (SSR) currently requires a public URL,
# see this bug: https://github.com/DSpace/dspace-angular/issues/1485
DSPACE_REST_SSL: 'true'
DSPACE_REST_HOST: api7.dspace.org
DSPACE_REST_HOST: demo.dspace.org
DSPACE_REST_PORT: 443
DSPACE_REST_NAMESPACE: /server
image: dspace/dspace-angular:dspace-7_x-dist

View File

@@ -48,7 +48,7 @@ dspace-angular connects to your DSpace installation by using its REST endpoint.
```yaml
rest:
ssl: true
host: api7.dspace.org
host: demo.dspace.org
port: 443
nameSpace: /server
}
@@ -57,7 +57,7 @@ rest:
Alternately you can set the following environment variables. If any of these are set, it will override all configuration files:
```
DSPACE_REST_SSL=true
DSPACE_REST_HOST=api7.dspace.org
DSPACE_REST_HOST=demo.dspace.org
DSPACE_REST_PORT=443
DSPACE_REST_NAMESPACE=/server
```

View File

@@ -1,6 +1,6 @@
{
"name": "dspace-angular",
"version": "7.6.0",
"version": "7.6.1",
"scripts": {
"ng": "ng",
"config:watch": "nodemon",
@@ -15,14 +15,14 @@
"analyze": "webpack-bundle-analyzer dist/browser/stats.json",
"build": "ng build --configuration development",
"build:stats": "ng build --stats-json",
"build:prod": "yarn run build:ssr",
"build:prod": "cross-env NODE_ENV=production yarn run build:ssr",
"build:ssr": "ng build --configuration production && ng run dspace-angular:server:production",
"test": "ng test --source-map=true --watch=false --configuration test",
"test:watch": "nodemon --exec \"ng test --source-map=true --watch=true --configuration test\"",
"test:headless": "ng test --source-map=true --watch=false --configuration test --browsers=ChromeHeadless --code-coverage",
"lint": "ng lint",
"lint-fix": "ng lint --fix=true",
"e2e": "ng e2e",
"e2e": "cross-env NODE_ENV=production ng e2e",
"clean:dev:config": "rimraf src/assets/config.json",
"clean:coverage": "rimraf coverage",
"clean:dist": "rimraf dist",
@@ -82,7 +82,7 @@
"@types/grecaptcha": "^3.0.4",
"angular-idle-preload": "3.0.0",
"angulartics2": "^12.2.0",
"axios": "^0.27.2",
"axios": "^1.6.0",
"bootstrap": "^4.6.1",
"cerialize": "0.1.18",
"cli-progress": "^3.12.0",
@@ -99,6 +99,7 @@
"fast-json-patch": "^3.1.1",
"filesize": "^6.1.0",
"http-proxy-middleware": "^1.0.5",
"http-terminator": "^3.2.0",
"isbot": "^3.6.10",
"js-cookie": "2.2.1",
"js-yaml": "^4.1.0",
@@ -116,12 +117,12 @@
"morgan": "^1.10.0",
"ng-mocks": "^14.10.0",
"ng2-file-upload": "1.4.0",
"ng2-nouislider": "^1.8.3",
"ng2-nouislider": "^2.0.0",
"ngx-infinite-scroll": "^15.0.0",
"ngx-pagination": "6.0.3",
"ngx-sortablejs": "^11.1.0",
"ngx-ui-switch": "^14.0.3",
"nouislider": "^14.6.3",
"nouislider": "^15.7.1",
"pem": "1.14.7",
"prop-types": "^15.8.1",
"react-copy-to-clipboard": "^5.1.0",
@@ -159,11 +160,11 @@
"@types/sanitize-html": "^2.9.0",
"@typescript-eslint/eslint-plugin": "^5.59.1",
"@typescript-eslint/parser": "^5.59.1",
"axe-core": "^4.7.0",
"axe-core": "^4.7.2",
"compression-webpack-plugin": "^9.2.0",
"copy-webpack-plugin": "^6.4.1",
"cross-env": "^7.0.3",
"cypress": "12.10.0",
"cypress": "12.17.4",
"cypress-axe": "^1.4.0",
"deep-freeze": "0.0.1",
"eslint": "^8.39.0",

View File

@@ -32,6 +32,7 @@ import isbot from 'isbot';
import { createCertificate } from 'pem';
import { createServer } from 'https';
import { json } from 'body-parser';
import { createHttpTerminator } from 'http-terminator';
import { readFileSync } from 'fs';
import { join } from 'path';
@@ -320,22 +321,23 @@ function initCache() {
if (botCacheEnabled()) {
// Initialize a new "least-recently-used" item cache (where least recently used pages are removed first)
// See https://www.npmjs.com/package/lru-cache
// When enabled, each page defaults to expiring after 1 day
// When enabled, each page defaults to expiring after 1 day (defined in default-app-config.ts)
botCache = new LRU( {
max: environment.cache.serverSide.botCache.max,
ttl: environment.cache.serverSide.botCache.timeToLive || 24 * 60 * 60 * 1000, // 1 day
allowStale: environment.cache.serverSide.botCache.allowStale ?? true // if object is stale, return stale value before deleting
ttl: environment.cache.serverSide.botCache.timeToLive,
allowStale: environment.cache.serverSide.botCache.allowStale
});
}
if (anonymousCacheEnabled()) {
// NOTE: While caches may share SSR pages, this cache must be kept separately because the timeToLive
// may expire pages more frequently.
// When enabled, each page defaults to expiring after 10 seconds (to minimize anonymous users seeing out-of-date content)
// When enabled, each page defaults to expiring after 10 seconds (defined in default-app-config.ts)
// to minimize anonymous users seeing out-of-date content
anonymousCache = new LRU( {
max: environment.cache.serverSide.anonymousCache.max,
ttl: environment.cache.serverSide.anonymousCache.timeToLive || 10 * 1000, // 10 seconds
allowStale: environment.cache.serverSide.anonymousCache.allowStale ?? true // if object is stale, return stale value before deleting
ttl: environment.cache.serverSide.anonymousCache.timeToLive,
allowStale: environment.cache.serverSide.anonymousCache.allowStale
});
}
}
@@ -487,7 +489,7 @@ function saveToCache(req, page: any) {
*/
function hasNotSucceeded(statusCode) {
const rgx = new RegExp(/^20+/);
return !rgx.test(statusCode)
return !rgx.test(statusCode);
}
function retrieveHeaders(response) {
@@ -525,23 +527,46 @@ function serverStarted() {
* @param keys SSL credentials
*/
function createHttpsServer(keys) {
createServer({
const listener = createServer({
key: keys.serviceKey,
cert: keys.certificate
}, app).listen(environment.ui.port, environment.ui.host, () => {
serverStarted();
});
// Graceful shutdown when signalled
const terminator = createHttpTerminator({server: listener});
process.on('SIGINT', () => {
void (async ()=> {
console.debug('Closing HTTPS server on signal');
await terminator.terminate().catch(e => { console.error(e); });
console.debug('HTTPS server closed');
})();
});
}
/**
* Create an HTTP server with the configured port and host.
*/
function run() {
const port = environment.ui.port || 4000;
const host = environment.ui.host || '/';
// Start up the Node server
const server = app();
server.listen(port, host, () => {
const listener = server.listen(port, host, () => {
serverStarted();
});
// Graceful shutdown when signalled
const terminator = createHttpTerminator({server: listener});
process.on('SIGINT', () => {
void (async () => {
console.debug('Closing HTTP server on signal');
await terminator.terminate().catch(e => { console.error(e); });
console.debug('HTTP server closed.');return undefined;
})();
});
}
function start() {

View File

@@ -1,12 +1,22 @@
import { URLCombiner } from '../core/url-combiner/url-combiner';
import { getAccessControlModuleRoute } from '../app-routing-paths';
export const GROUP_EDIT_PATH = 'groups';
export const EPERSON_PATH = 'epeople';
export function getEPersonsRoute(): string {
return new URLCombiner(getAccessControlModuleRoute(), EPERSON_PATH).toString();
}
export function getEPersonEditRoute(id: string): string {
return new URLCombiner(getEPersonsRoute(), id, 'edit').toString();
}
export const GROUP_PATH = 'groups';
export function getGroupsRoute() {
return new URLCombiner(getAccessControlModuleRoute(), GROUP_EDIT_PATH).toString();
return new URLCombiner(getAccessControlModuleRoute(), GROUP_PATH).toString();
}
export function getGroupEditRoute(id: string) {
return new URLCombiner(getAccessControlModuleRoute(), GROUP_EDIT_PATH, id).toString();
return new URLCombiner(getGroupsRoute(), id, 'edit').toString();
}

View File

@@ -3,7 +3,7 @@ import { RouterModule } from '@angular/router';
import { EPeopleRegistryComponent } from './epeople-registry/epeople-registry.component';
import { GroupFormComponent } from './group-registry/group-form/group-form.component';
import { GroupsRegistryComponent } from './group-registry/groups-registry.component';
import { GROUP_EDIT_PATH } from './access-control-routing-paths';
import { EPERSON_PATH, GROUP_PATH } from './access-control-routing-paths';
import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver';
import { GroupPageGuard } from './group-registry/group-page.guard';
import {
@@ -13,12 +13,14 @@ import {
SiteAdministratorGuard
} from '../core/data/feature-authorization/feature-authorization-guard/site-administrator.guard';
import { BulkAccessComponent } from './bulk-access/bulk-access.component';
import { EPersonFormComponent } from './epeople-registry/eperson-form/eperson-form.component';
import { EPersonResolver } from './epeople-registry/eperson-resolver.service';
@NgModule({
imports: [
RouterModule.forChild([
{
path: 'epeople',
path: EPERSON_PATH,
component: EPeopleRegistryComponent,
resolve: {
breadcrumb: I18nBreadcrumbResolver
@@ -27,7 +29,26 @@ import { BulkAccessComponent } from './bulk-access/bulk-access.component';
canActivate: [SiteAdministratorGuard]
},
{
path: GROUP_EDIT_PATH,
path: `${EPERSON_PATH}/create`,
component: EPersonFormComponent,
resolve: {
breadcrumb: I18nBreadcrumbResolver,
},
data: { title: 'admin.access-control.epeople.add.title', breadcrumbKey: 'admin.access-control.epeople.add' },
canActivate: [SiteAdministratorGuard],
},
{
path: `${EPERSON_PATH}/:id/edit`,
component: EPersonFormComponent,
resolve: {
breadcrumb: I18nBreadcrumbResolver,
ePerson: EPersonResolver,
},
data: { title: 'admin.access-control.epeople.edit.title', breadcrumbKey: 'admin.access-control.epeople.edit' },
canActivate: [SiteAdministratorGuard],
},
{
path: GROUP_PATH,
component: GroupsRegistryComponent,
resolve: {
breadcrumb: I18nBreadcrumbResolver
@@ -36,7 +57,7 @@ import { BulkAccessComponent } from './bulk-access/bulk-access.component';
canActivate: [GroupAdministratorGuard]
},
{
path: `${GROUP_EDIT_PATH}/newGroup`,
path: `${GROUP_PATH}/create`,
component: GroupFormComponent,
resolve: {
breadcrumb: I18nBreadcrumbResolver
@@ -45,7 +66,7 @@ import { BulkAccessComponent } from './bulk-access/bulk-access.component';
canActivate: [GroupAdministratorGuard]
},
{
path: `${GROUP_EDIT_PATH}/:groupId`,
path: `${GROUP_PATH}/:groupId/edit`,
component: GroupFormComponent,
resolve: {
breadcrumb: I18nBreadcrumbResolver

View File

@@ -4,96 +4,91 @@
<div class="d-flex justify-content-between border-bottom mb-3">
<h2 id="header" class="pb-2">{{labelPrefix + 'head' | translate}}</h2>
<div *ngIf="!isEPersonFormShown">
<div>
<button class="mr-auto btn btn-success addEPerson-button"
(click)="isEPersonFormShown = true">
[routerLink]="'create'">
<i class="fas fa-plus"></i>
<span class="d-none d-sm-inline ml-1">{{labelPrefix + 'button.add' | translate}}</span>
</button>
</div>
</div>
<ds-eperson-form *ngIf="isEPersonFormShown" (submitForm)="reset()"
(cancelForm)="isEPersonFormShown = false"></ds-eperson-form>
<h3 id="search" class="border-bottom pb-2">{{labelPrefix + 'search.head' | translate}}
<div *ngIf="!isEPersonFormShown">
<h3 id="search" class="border-bottom pb-2">{{labelPrefix + 'search.head' | translate}}
</h3>
<form [formGroup]="searchForm" (ngSubmit)="search(searchForm.value)" class="d-flex justify-content-between">
<div>
<select name="scope" id="scope" formControlName="scope" class="form-control" aria-label="Search scope">
<option value="metadata">{{labelPrefix + 'search.scope.metadata' | translate}}</option>
<option value="email">{{labelPrefix + 'search.scope.email' | translate}}</option>
</select>
</div>
<div class="flex-grow-1 mr-3 ml-3">
<div class="form-group input-group">
<input type="text" name="query" id="query" formControlName="query"
class="form-control" [attr.aria-label]="labelPrefix + 'search.placeholder' | translate"
[placeholder]="(labelPrefix + 'search.placeholder' | translate)">
<span class="input-group-append">
</h3>
<form [formGroup]="searchForm" (ngSubmit)="search(searchForm.value)" class="d-flex justify-content-between">
<div>
<select name="scope" id="scope" formControlName="scope" class="form-control" aria-label="Search scope">
<option value="metadata">{{labelPrefix + 'search.scope.metadata' | translate}}</option>
<option value="email">{{labelPrefix + 'search.scope.email' | translate}}</option>
</select>
</div>
<div class="flex-grow-1 mr-3 ml-3">
<div class="form-group input-group">
<input type="text" name="query" id="query" formControlName="query"
class="form-control" [attr.aria-label]="labelPrefix + 'search.placeholder' | translate"
[placeholder]="(labelPrefix + 'search.placeholder' | translate)">
<span class="input-group-append">
<button type="submit" class="search-button btn btn-primary">
<i class="fas fa-search"></i> {{ labelPrefix + 'search.button' | translate }}
</button>
</span>
</div>
</div>
<div>
<button (click)="clearFormAndResetResult();"
class="search-button btn btn-secondary">{{labelPrefix + 'button.see-all' | translate}}</button>
</div>
</form>
<ds-themed-loading *ngIf="searching$ | async"></ds-themed-loading>
<ds-pagination
*ngIf="(pageInfoState$ | async)?.totalElements > 0 && !(searching$ | async)"
[paginationOptions]="config"
[pageInfoState]="pageInfoState$"
[collectionSize]="(pageInfoState$ | async)?.totalElements"
[hideGear]="true"
[hidePagerWhenSinglePage]="true">
<div class="table-responsive">
<table id="epeople" class="table table-striped table-hover table-bordered">
<thead>
<tr>
<th scope="col">{{labelPrefix + 'table.id' | translate}}</th>
<th scope="col">{{labelPrefix + 'table.name' | translate}}</th>
<th scope="col">{{labelPrefix + 'table.email' | translate}}</th>
<th>{{labelPrefix + 'table.edit' | translate}}</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let epersonDto of (ePeopleDto$ | async)?.page"
[ngClass]="{'table-primary' : isActive(epersonDto.eperson) | async}">
<td>{{epersonDto.eperson.id}}</td>
<td>{{ dsoNameService.getName(epersonDto.eperson) }}</td>
<td>{{epersonDto.eperson.email}}</td>
<td>
<div class="btn-group edit-field">
<button (click)="toggleEditEPerson(epersonDto.eperson)"
class="btn btn-outline-primary btn-sm access-control-editEPersonButton"
title="{{labelPrefix + 'table.edit.buttons.edit' | translate: { name: dsoNameService.getName(epersonDto.eperson) } }}">
<i class="fas fa-edit fa-fw"></i>
</button>
<button [disabled]="!epersonDto.ableToDelete" (click)="deleteEPerson(epersonDto.eperson)"
class="delete-button btn btn-outline-danger btn-sm access-control-deleteEPersonButton"
title="{{labelPrefix + 'table.edit.buttons.remove' | translate: { name: dsoNameService.getName(epersonDto.eperson) } }}">
<i class="fas fa-trash-alt fa-fw"></i>
</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</ds-pagination>
<div *ngIf="(pageInfoState$ | async)?.totalElements == 0" class="alert alert-info w-100 mb-2" role="alert">
{{labelPrefix + 'no-items' | translate}}
</div>
<div>
<button (click)="clearFormAndResetResult();"
class="search-button btn btn-secondary">{{labelPrefix + 'button.see-all' | translate}}</button>
</div>
</form>
<ds-themed-loading *ngIf="searching$ | async"></ds-themed-loading>
<ds-pagination
*ngIf="(pageInfoState$ | async)?.totalElements > 0 && !(searching$ | async)"
[paginationOptions]="config"
[pageInfoState]="pageInfoState$"
[collectionSize]="(pageInfoState$ | async)?.totalElements"
[hideGear]="true"
[hidePagerWhenSinglePage]="true">
<div class="table-responsive">
<table id="epeople" class="table table-striped table-hover table-bordered">
<thead>
<tr>
<th scope="col">{{labelPrefix + 'table.id' | translate}}</th>
<th scope="col">{{labelPrefix + 'table.name' | translate}}</th>
<th scope="col">{{labelPrefix + 'table.email' | translate}}</th>
<th>{{labelPrefix + 'table.edit' | translate}}</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let epersonDto of (ePeopleDto$ | async)?.page"
[ngClass]="{'table-primary' : isActive(epersonDto.eperson) | async}">
<td>{{epersonDto.eperson.id}}</td>
<td>{{ dsoNameService.getName(epersonDto.eperson) }}</td>
<td>{{epersonDto.eperson.email}}</td>
<td>
<div class="btn-group edit-field">
<button [routerLink]="getEditEPeoplePage(epersonDto.eperson.id)"
class="btn btn-outline-primary btn-sm access-control-editEPersonButton"
title="{{labelPrefix + 'table.edit.buttons.edit' | translate: { name: dsoNameService.getName(epersonDto.eperson) } }}">
<i class="fas fa-edit fa-fw"></i>
</button>
<button *ngIf="epersonDto.ableToDelete" (click)="deleteEPerson(epersonDto.eperson)"
class="delete-button btn btn-outline-danger btn-sm access-control-deleteEPersonButton"
title="{{labelPrefix + 'table.edit.buttons.remove' | translate: { name: dsoNameService.getName(epersonDto.eperson) } }}">
<i class="fas fa-trash-alt fa-fw"></i>
</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</ds-pagination>
<div *ngIf="(pageInfoState$ | async)?.totalElements == 0" class="alert alert-info w-100 mb-2" role="alert">
{{labelPrefix + 'no-items' | translate}}
</div>
</div>
</div>

View File

@@ -203,36 +203,6 @@ describe('EPeopleRegistryComponent', () => {
});
});
describe('toggleEditEPerson', () => {
describe('when you click on first edit eperson button', () => {
beforeEach(fakeAsync(() => {
const editButtons = fixture.debugElement.queryAll(By.css('.access-control-editEPersonButton'));
editButtons[0].triggerEventHandler('click', {
preventDefault: () => {/**/
}
});
tick();
fixture.detectChanges();
}));
it('editEPerson form is toggled', () => {
const ePeopleIds = fixture.debugElement.queryAll(By.css('#epeople tr td:first-child'));
ePersonDataServiceStub.getActiveEPerson().subscribe((activeEPerson: EPerson) => {
if (ePeopleIds[0] && activeEPerson === ePeopleIds[0].nativeElement.textContent) {
expect(component.isEPersonFormShown).toEqual(false);
} else {
expect(component.isEPersonFormShown).toEqual(true);
}
});
});
it('EPerson search section is hidden', () => {
expect(fixture.debugElement.query(By.css('#search'))).toBeNull();
});
});
});
describe('deleteEPerson', () => {
describe('when you click on first delete eperson button', () => {
let ePeopleIdsFoundBeforeDelete;

View File

@@ -22,6 +22,7 @@ import { PageInfo } from '../../core/shared/page-info.model';
import { NoContent } from '../../core/shared/NoContent.model';
import { PaginationService } from '../../core/pagination/pagination.service';
import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
import { getEPersonEditRoute, getEPersonsRoute } from '../access-control-routing-paths';
@Component({
selector: 'ds-epeople-registry',
@@ -64,11 +65,6 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy {
currentPage: 1
});
/**
* Whether or not to show the EPerson form
*/
isEPersonFormShown: boolean;
// The search form
searchForm;
@@ -114,17 +110,11 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy {
*/
initialisePage() {
this.searching$.next(true);
this.isEPersonFormShown = false;
this.search({scope: this.currentSearchScope, query: this.currentSearchQuery});
this.subs.push(this.epersonService.getActiveEPerson().subscribe((eperson: EPerson) => {
if (eperson != null && eperson.id) {
this.isEPersonFormShown = true;
}
}));
this.subs.push(this.ePeople$.pipe(
switchMap((epeople: PaginatedList<EPerson>) => {
if (epeople.pageInfo.totalElements > 0) {
return combineLatest([...epeople.page.map((eperson: EPerson) => {
return combineLatest(epeople.page.map((eperson: EPerson) => {
return this.authorizationService.isAuthorized(FeatureID.CanDelete, hasValue(eperson) ? eperson.self : undefined).pipe(
map((authorized) => {
const epersonDtoModel: EpersonDtoModel = new EpersonDtoModel();
@@ -133,7 +123,7 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy {
return epersonDtoModel;
})
);
})]).pipe(map((dtos: EpersonDtoModel[]) => {
})).pipe(map((dtos: EpersonDtoModel[]) => {
return buildPaginatedList(epeople.pageInfo, dtos);
}));
} else {
@@ -160,14 +150,14 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy {
const query: string = data.query;
const scope: string = data.scope;
if (query != null && this.currentSearchQuery !== query) {
this.router.navigate([this.epersonService.getEPeoplePageRouterLink()], {
void this.router.navigate([getEPersonsRoute()], {
queryParamsHandling: 'merge'
});
this.currentSearchQuery = query;
this.paginationService.resetPage(this.config.id);
}
if (scope != null && this.currentSearchScope !== scope) {
this.router.navigate([this.epersonService.getEPeoplePageRouterLink()], {
void this.router.navigate([getEPersonsRoute()], {
queryParamsHandling: 'merge'
});
this.currentSearchScope = scope;
@@ -205,23 +195,6 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy {
return this.epersonService.getActiveEPerson();
}
/**
* Start editing the selected EPerson
* @param ePerson
*/
toggleEditEPerson(ePerson: EPerson) {
this.getActiveEPerson().pipe(take(1)).subscribe((activeEPerson: EPerson) => {
if (ePerson === activeEPerson) {
this.epersonService.cancelEditEPerson();
this.isEPersonFormShown = false;
} else {
this.epersonService.editEPerson(ePerson);
this.isEPersonFormShown = true;
}
});
this.scrollToTop();
}
/**
* Deletes EPerson, show notification on success/failure & updates EPeople list
*/
@@ -242,7 +215,7 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy {
if (restResponse.hasSucceeded) {
this.notificationsService.success(this.translateService.get(this.labelPrefix + 'notification.deleted.success', {name: this.dsoNameService.getName(ePerson)}));
} else {
this.notificationsService.error('Error occured when trying to delete EPerson with id: ' + ePerson.id + ' with code: ' + restResponse.statusCode + ' and message: ' + restResponse.errorMessage);
this.notificationsService.error(this.translateService.get(this.labelPrefix + 'notification.deleted.success', { id: ePerson.id, statusCode: restResponse.statusCode, errorMessage: restResponse.errorMessage }));
}
});
}
@@ -264,16 +237,6 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy {
this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe());
}
scrollToTop() {
(function smoothscroll() {
const currentScroll = document.documentElement.scrollTop || document.body.scrollTop;
if (currentScroll > 0) {
window.requestAnimationFrame(smoothscroll);
window.scrollTo(0, currentScroll - (currentScroll / 8));
}
})();
}
/**
* Reset all input-fields to be empty and search all search
*/
@@ -284,20 +247,7 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy {
this.search({query: ''});
}
/**
* This method will set everything to stale, which will cause the lists on this page to update.
*/
reset(): void {
this.epersonService.getBrowseEndpoint().pipe(
take(1),
switchMap((href: string) => {
return this.requestService.setStaleByHrefSubstring(href).pipe(
take(1),
);
})
).subscribe(()=>{
this.epersonService.cancelEditEPerson();
this.isEPersonFormShown = false;
});
getEditEPeoplePage(id: string): string {
return getEPersonEditRoute(id);
}
}

View File

@@ -1,89 +1,97 @@
<div *ngIf="epersonService.getActiveEPerson() | async; then editheader; else createHeader"></div>
<div class="container">
<div class="group-form row">
<div class="col-12">
<ng-template #createHeader>
<h4>{{messagePrefix + '.create' | translate}}</h4>
</ng-template>
<div *ngIf="epersonService.getActiveEPerson() | async; then editHeader; else createHeader"></div>
<ng-template #editheader>
<h4>{{messagePrefix + '.edit' | translate}}</h4>
</ng-template>
<ng-template #createHeader>
<h2 class="border-bottom pb-2">{{messagePrefix + '.create' | translate}}</h2>
</ng-template>
<ds-form [formId]="formId"
[formModel]="formModel"
[formGroup]="formGroup"
[formLayout]="formLayout"
[displayCancel]="false"
[submitLabel]="submitLabel"
(submitForm)="onSubmit()">
<div before class="btn-group">
<button (click)="onCancel()"
class="btn btn-outline-secondary"><i class="fas fa-arrow-left"></i> {{messagePrefix + '.return' | translate}}</button>
</div>
<div *ngIf="displayResetPassword" between class="btn-group">
<button class="btn btn-primary" [disabled]="!(canReset$ | async)" (click)="resetPassword()">
<i class="fa fa-key"></i> {{'admin.access-control.epeople.actions.reset' | translate}}
</button>
</div>
<div between class="btn-group ml-1">
<button *ngIf="!isImpersonated" class="btn btn-primary" [ngClass]="{'d-none' : !(canImpersonate$ | async)}" (click)="impersonate()">
<i class="fa fa-user-secret"></i> {{'admin.access-control.epeople.actions.impersonate' | translate}}
</button>
<button *ngIf="isImpersonated" class="btn btn-primary" (click)="stopImpersonating()">
<i class="fa fa-user-secret"></i> {{'admin.access-control.epeople.actions.stop-impersonating' | translate}}
</button>
</div>
<button after class="btn btn-danger delete-button" [disabled]="!(canDelete$ | async)" (click)="delete()">
<i class="fas fa-trash"></i> {{'admin.access-control.epeople.actions.delete' | translate}}
</button>
</ds-form>
<ng-template #editHeader>
<h2 class="border-bottom pb-2">{{messagePrefix + '.edit' | translate}}</h2>
</ng-template>
<ds-themed-loading [showMessage]="false" *ngIf="!formGroup"></ds-themed-loading>
<ds-form [formId]="formId"
[formModel]="formModel"
[formGroup]="formGroup"
[formLayout]="formLayout"
[displayCancel]="false"
[submitLabel]="submitLabel"
(submitForm)="onSubmit()">
<div before class="btn-group">
<button (click)="onCancel()" type="button" class="btn btn-outline-secondary">
<i class="fas fa-arrow-left"></i> {{messagePrefix + '.return' | translate}}
</button>
</div>
<div *ngIf="displayResetPassword" between class="btn-group">
<button class="btn btn-primary" [disabled]="!(canReset$ | async)" type="button" (click)="resetPassword()">
<i class="fa fa-key"></i> {{'admin.access-control.epeople.actions.reset' | translate}}
</button>
</div>
<div *ngIf="canImpersonate$ | async" between class="btn-group">
<button *ngIf="!isImpersonated" class="btn btn-primary" type="button" (click)="impersonate()">
<i class="fa fa-user-secret"></i> {{'admin.access-control.epeople.actions.impersonate' | translate}}
</button>
<button *ngIf="isImpersonated" class="btn btn-primary" type="button" (click)="stopImpersonating()">
<i class="fa fa-user-secret"></i> {{'admin.access-control.epeople.actions.stop-impersonating' | translate}}
</button>
</div>
<button *ngIf="canDelete$ | async" after class="btn btn-danger delete-button" type="button" (click)="delete()">
<i class="fas fa-trash"></i> {{'admin.access-control.epeople.actions.delete' | translate}}
</button>
</ds-form>
<div *ngIf="epersonService.getActiveEPerson() | async">
<h5>{{messagePrefix + '.groupsEPersonIsMemberOf' | translate}}</h5>
<ds-themed-loading [showMessage]="false" *ngIf="!formGroup"></ds-themed-loading>
<ds-themed-loading [showMessage]="false" *ngIf="!(groups | async)"></ds-themed-loading>
<div *ngIf="epersonService.getActiveEPerson() | async">
<h5>{{messagePrefix + '.groupsEPersonIsMemberOf' | translate}}</h5>
<ds-pagination
*ngIf="(groups | async)?.payload?.totalElements > 0"
[paginationOptions]="config"
[pageInfoState]="(groups | async)?.payload"
[collectionSize]="(groups | async)?.payload?.totalElements"
[hideGear]="true"
[hidePagerWhenSinglePage]="true"
(pageChange)="onPageChange($event)">
<ds-themed-loading [showMessage]="false" *ngIf="!(groups | async)"></ds-themed-loading>
<div class="table-responsive">
<table id="groups" class="table table-striped table-hover table-bordered">
<thead>
<tr>
<th scope="col" class="align-middle">{{messagePrefix + '.table.id' | translate}}</th>
<th scope="col" class="align-middle">{{messagePrefix + '.table.name' | translate}}</th>
<th scope="col" class="align-middle">{{messagePrefix + '.table.collectionOrCommunity' | translate}}</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let group of (groups | async)?.payload?.page">
<td class="align-middle">{{group.id}}</td>
<td class="align-middle">
<a (click)="groupsDataService.startEditingNewGroup(group)"
[routerLink]="[groupsDataService.getGroupEditPageRouterLink(group)]">
{{ dsoNameService.getName(group) }}
</a>
</td>
<td class="align-middle">{{ dsoNameService.getName(undefined) }}</td>
</tr>
</tbody>
</table>
</div>
<ds-pagination
*ngIf="(groups | async)?.payload?.totalElements > 0"
[paginationOptions]="config"
[pageInfoState]="(groups | async)?.payload"
[collectionSize]="(groups | async)?.payload?.totalElements"
[hideGear]="true"
[hidePagerWhenSinglePage]="true"
(pageChange)="onPageChange($event)">
</ds-pagination>
<div class="table-responsive">
<table id="groups" class="table table-striped table-hover table-bordered">
<thead>
<tr>
<th scope="col" class="align-middle">{{messagePrefix + '.table.id' | translate}}</th>
<th scope="col" class="align-middle">{{messagePrefix + '.table.name' | translate}}</th>
<th scope="col" class="align-middle">{{messagePrefix + '.table.collectionOrCommunity' | translate}}</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let group of (groups | async)?.payload?.page">
<td class="align-middle">{{group.id}}</td>
<td class="align-middle">
<a (click)="groupsDataService.startEditingNewGroup(group)"
[routerLink]="[groupsDataService.getGroupEditPageRouterLink(group)]">
{{ dsoNameService.getName(group) }}
</a>
</td>
<td class="align-middle">{{ dsoNameService.getName(undefined) }}</td>
</tr>
</tbody>
</table>
</div>
<div *ngIf="(groups | async)?.payload?.totalElements == 0" class="alert alert-info w-100 mb-2" role="alert">
<div>{{messagePrefix + '.memberOfNoGroups' | translate}}</div>
<div>
<button [routerLink]="[groupsDataService.getGroupRegistryRouterLink()]"
class="btn btn-primary">{{messagePrefix + '.goToGroups' | translate}}</button>
</ds-pagination>
<div *ngIf="(groups | async)?.payload?.totalElements == 0" class="alert alert-info w-100 mb-2" role="alert">
<div>{{messagePrefix + '.memberOfNoGroups' | translate}}</div>
<div>
<button [routerLink]="[groupsDataService.getGroupRegistryRouterLink()]"
class="btn btn-primary">{{messagePrefix + '.goToGroups' | translate}}</button>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@@ -31,6 +31,10 @@ import { PaginationServiceStub } from '../../../shared/testing/pagination-servic
import { FindListOptions } from '../../../core/data/find-list-options.model';
import { ValidateEmailNotTaken } from './validators/email-taken.validator';
import { EpersonRegistrationService } from '../../../core/data/eperson-registration.service';
import { FollowLinkConfig } from '../../../shared/utils/follow-link-config.model';
import { ActivatedRoute, Router } from '@angular/router';
import { RouterStub } from '../../../shared/testing/router.stub';
import { ActivatedRouteStub } from '../../../shared/testing/active-router.stub';
describe('EPersonFormComponent', () => {
let component: EPersonFormComponent;
@@ -43,6 +47,8 @@ describe('EPersonFormComponent', () => {
let authorizationService: AuthorizationDataService;
let groupsDataService: GroupDataService;
let epersonRegistrationService: EpersonRegistrationService;
let route: ActivatedRouteStub;
let router: RouterStub;
let paginationService;
@@ -106,6 +112,9 @@ describe('EPersonFormComponent', () => {
},
getEPersonByEmail(email): Observable<RemoteData<EPerson>> {
return createSuccessfulRemoteDataObject$(null);
},
findById(_id: string, _useCachedVersionIfAvailable = true, _reRequestOnStale = true, ..._linksToFollow: FollowLinkConfig<EPerson>[]): Observable<RemoteData<EPerson>> {
return createSuccessfulRemoteDataObject$(null);
}
};
builderService = Object.assign(getMockFormBuilderService(),{
@@ -182,6 +191,8 @@ describe('EPersonFormComponent', () => {
});
paginationService = new PaginationServiceStub();
route = new ActivatedRouteStub();
router = new RouterStub();
TestBed.configureTestingModule({
imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule,
TranslateModule.forRoot({
@@ -202,6 +213,8 @@ describe('EPersonFormComponent', () => {
{ provide: PaginationService, useValue: paginationService },
{ provide: RequestService, useValue: jasmine.createSpyObj('requestService', ['removeByHrefSubstring'])},
{ provide: EpersonRegistrationService, useValue: epersonRegistrationService },
{ provide: ActivatedRoute, useValue: route },
{ provide: Router, useValue: router },
EPeopleRegistryComponent
],
schemas: [NO_ERRORS_SCHEMA]
@@ -263,24 +276,18 @@ describe('EPersonFormComponent', () => {
fixture.detectChanges();
});
describe('firstName, lastName and email should be required', () => {
it('form should be invalid because the firstName is required', waitForAsync(() => {
fixture.whenStable().then(() => {
expect(component.formGroup.controls.firstName.valid).toBeFalse();
expect(component.formGroup.controls.firstName.errors.required).toBeTrue();
});
}));
it('form should be invalid because the lastName is required', waitForAsync(() => {
fixture.whenStable().then(() => {
expect(component.formGroup.controls.lastName.valid).toBeFalse();
expect(component.formGroup.controls.lastName.errors.required).toBeTrue();
});
}));
it('form should be invalid because the email is required', waitForAsync(() => {
fixture.whenStable().then(() => {
expect(component.formGroup.controls.email.valid).toBeFalse();
expect(component.formGroup.controls.email.errors.required).toBeTrue();
});
}));
it('form should be invalid because the firstName is required', () => {
expect(component.formGroup.controls.firstName.valid).toBeFalse();
expect(component.formGroup.controls.firstName.errors.required).toBeTrue();
});
it('form should be invalid because the lastName is required', () => {
expect(component.formGroup.controls.lastName.valid).toBeFalse();
expect(component.formGroup.controls.lastName.errors.required).toBeTrue();
});
it('form should be invalid because the email is required', () => {
expect(component.formGroup.controls.email.valid).toBeFalse();
expect(component.formGroup.controls.email.errors.required).toBeTrue();
});
});
describe('after inserting information firstName,lastName and email not required', () => {
@@ -290,24 +297,18 @@ describe('EPersonFormComponent', () => {
component.formGroup.controls.email.setValue('test@test.com');
fixture.detectChanges();
});
it('firstName should be valid because the firstName is set', waitForAsync(() => {
fixture.whenStable().then(() => {
it('firstName should be valid because the firstName is set', () => {
expect(component.formGroup.controls.firstName.valid).toBeTrue();
expect(component.formGroup.controls.firstName.errors).toBeNull();
});
}));
it('lastName should be valid because the lastName is set', waitForAsync(() => {
fixture.whenStable().then(() => {
});
it('lastName should be valid because the lastName is set', () => {
expect(component.formGroup.controls.lastName.valid).toBeTrue();
expect(component.formGroup.controls.lastName.errors).toBeNull();
});
}));
it('email should be valid because the email is set', waitForAsync(() => {
fixture.whenStable().then(() => {
});
it('email should be valid because the email is set', () => {
expect(component.formGroup.controls.email.valid).toBeTrue();
expect(component.formGroup.controls.email.errors).toBeNull();
});
}));
});
});
@@ -316,12 +317,10 @@ describe('EPersonFormComponent', () => {
component.formGroup.controls.email.setValue('test@test');
fixture.detectChanges();
});
it('email should not be valid because the email pattern', waitForAsync(() => {
fixture.whenStable().then(() => {
it('email should not be valid because the email pattern', () => {
expect(component.formGroup.controls.email.valid).toBeFalse();
expect(component.formGroup.controls.email.errors.pattern).toBeTruthy();
});
}));
});
});
describe('after already utilized email', () => {
@@ -336,12 +335,10 @@ describe('EPersonFormComponent', () => {
fixture.detectChanges();
});
it('email should not be valid because email is already taken', waitForAsync(() => {
fixture.whenStable().then(() => {
it('email should not be valid because email is already taken', () => {
expect(component.formGroup.controls.email.valid).toBeFalse();
expect(component.formGroup.controls.email.errors.emailTaken).toBeTruthy();
});
}));
});
});
@@ -393,11 +390,9 @@ describe('EPersonFormComponent', () => {
fixture.detectChanges();
});
it('should emit a new eperson using the correct values', waitForAsync(() => {
fixture.whenStable().then(() => {
expect(component.submitForm.emit).toHaveBeenCalledWith(expected);
});
}));
it('should emit a new eperson using the correct values', () => {
expect(component.submitForm.emit).toHaveBeenCalledWith(expected);
});
});
describe('with an active eperson', () => {
@@ -428,11 +423,9 @@ describe('EPersonFormComponent', () => {
fixture.detectChanges();
});
it('should emit the existing eperson using the correct values', waitForAsync(() => {
fixture.whenStable().then(() => {
expect(component.submitForm.emit).toHaveBeenCalledWith(expectedWithId);
});
}));
it('should emit the existing eperson using the correct values', () => {
expect(component.submitForm.emit).toHaveBeenCalledWith(expectedWithId);
});
});
});
@@ -491,16 +484,16 @@ describe('EPersonFormComponent', () => {
});
it('the delete button should be active if the eperson can be deleted', () => {
it('the delete button should be visible if the ePerson can be deleted', () => {
const deleteButton = fixture.debugElement.query(By.css('.delete-button'));
expect(deleteButton.nativeElement.disabled).toBe(false);
expect(deleteButton).not.toBeNull();
});
it('the delete button should be disabled if the eperson cannot be deleted', () => {
it('the delete button should be hidden if the ePerson cannot be deleted', () => {
component.canDelete$ = observableOf(false);
fixture.detectChanges();
const deleteButton = fixture.debugElement.query(By.css('.delete-button'));
expect(deleteButton.nativeElement.disabled).toBe(true);
expect(deleteButton).toBeNull();
});
it('should call the epersonFormComponent delete when clicked on the button', () => {

View File

@@ -38,6 +38,8 @@ import { Registration } from '../../../core/shared/registration.model';
import { EpersonRegistrationService } from '../../../core/data/eperson-registration.service';
import { TYPE_REQUEST_FORGOT } from '../../../register-email-form/register-email-form.component';
import { DSONameService } from '../../../core/breadcrumbs/dso-name.service';
import { ActivatedRoute, Router } from '@angular/router';
import { getEPersonsRoute } from '../../access-control-routing-paths';
@Component({
selector: 'ds-eperson-form',
@@ -194,6 +196,8 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
public requestService: RequestService,
private epersonRegistrationService: EpersonRegistrationService,
public dsoNameService: DSONameService,
protected route: ActivatedRoute,
protected router: Router,
) {
this.subs.push(this.epersonService.getActiveEPerson().subscribe((eperson: EPerson) => {
this.epersonInitial = eperson;
@@ -213,7 +217,9 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
* This method will initialise the page
*/
initialisePage() {
this.subs.push(this.epersonService.findById(this.route.snapshot.params.id).subscribe((ePersonRD: RemoteData<EPerson>) => {
this.epersonService.editEPerson(ePersonRD.payload);
}));
observableCombineLatest([
this.translateService.get(`${this.messagePrefix}.firstName`),
this.translateService.get(`${this.messagePrefix}.lastName`),
@@ -339,6 +345,7 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
onCancel() {
this.epersonService.cancelEditEPerson();
this.cancelForm.emit();
void this.router.navigate([getEPersonsRoute()]);
}
/**
@@ -390,6 +397,8 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
if (rd.hasSucceeded) {
this.notificationsService.success(this.translateService.get(this.labelPrefix + 'notification.created.success', { name: this.dsoNameService.getName(ePersonToCreate) }));
this.submitForm.emit(ePersonToCreate);
this.epersonService.clearEPersonRequests();
void this.router.navigateByUrl(getEPersonsRoute());
} else {
this.notificationsService.error(this.translateService.get(this.labelPrefix + 'notification.created.failure', { name: this.dsoNameService.getName(ePersonToCreate) }));
this.cancelForm.emit();
@@ -429,6 +438,7 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
if (rd.hasSucceeded) {
this.notificationsService.success(this.translateService.get(this.labelPrefix + 'notification.edited.success', { name: this.dsoNameService.getName(editedEperson) }));
this.submitForm.emit(editedEperson);
void this.router.navigateByUrl(getEPersonsRoute());
} else {
this.notificationsService.error(this.translateService.get(this.labelPrefix + 'notification.edited.failure', { name: this.dsoNameService.getName(editedEperson) }));
this.cancelForm.emit();
@@ -495,6 +505,7 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
).subscribe(({ restResponse, eperson }: { restResponse: RemoteData<NoContent> | null, eperson: EPerson }) => {
if (restResponse?.hasSucceeded) {
this.notificationsService.success(this.translateService.get(this.labelPrefix + 'notification.deleted.success', { name: this.dsoNameService.getName(eperson) }));
void this.router.navigate([getEPersonsRoute()]);
} else {
this.notificationsService.error(`Error occurred when trying to delete EPerson with id: ${eperson?.id} with code: ${restResponse?.statusCode} and message: ${restResponse?.errorMessage}`);
}
@@ -541,16 +552,6 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
}
}
/**
* This method will ensure that the page gets reset and that the cache is cleared
*/
reset() {
this.epersonService.getActiveEPerson().pipe(take(1)).subscribe((eperson: EPerson) => {
this.requestService.removeByHrefSubstring(eperson.self);
});
this.initialisePage();
}
/**
* Checks for the given ePerson if there is already an ePerson in the system with that email
* and shows notification if this is the case

View File

@@ -0,0 +1,53 @@
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router';
import { Observable } from 'rxjs';
import { EPerson } from '../../core/eperson/models/eperson.model';
import { RemoteData } from '../../core/data/remote-data';
import { getFirstCompletedRemoteData } from '../../core/shared/operators';
import { ResolvedAction } from '../../core/resolving/resolver.actions';
import { EPersonDataService } from '../../core/eperson/eperson-data.service';
import { Store } from '@ngrx/store';
import { followLink, FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
export const EPERSON_EDIT_FOLLOW_LINKS: FollowLinkConfig<EPerson>[] = [
followLink('groups'),
];
/**
* This class represents a resolver that requests a specific {@link EPerson} before the route is activated
*/
@Injectable({
providedIn: 'root',
})
export class EPersonResolver implements Resolve<RemoteData<EPerson>> {
constructor(
protected ePersonService: EPersonDataService,
protected store: Store<any>,
) {
}
/**
* Method for resolving a {@link EPerson} based on the parameters in the current route
* @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot
* @param {RouterStateSnapshot} state The current RouterStateSnapshot
* @returns `Observable<<RemoteData<EPerson>>` Emits the found {@link EPerson} based on the parameters in the current
* route, or an error if something went wrong
*/
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<RemoteData<EPerson>> {
const ePersonRD$: Observable<RemoteData<EPerson>> = this.ePersonService.findById(route.params.id,
true,
false,
...EPERSON_EDIT_FOLLOW_LINKS,
).pipe(
getFirstCompletedRemoteData(),
);
ePersonRD$.subscribe((ePersonRD: RemoteData<EPerson>) => {
this.store.dispatch(new ResolvedAction(state.url, ePersonRD.payload));
});
return ePersonRD$;
}
}

View File

@@ -2,13 +2,13 @@
<div class="group-form row">
<div class="col-12">
<div *ngIf="groupDataService.getActiveGroup() | async; then editheader; else createHeader"></div>
<div *ngIf="groupDataService.getActiveGroup() | async; then editHeader; else createHeader"></div>
<ng-template #createHeader>
<h2 class="border-bottom pb-2">{{messagePrefix + '.head.create' | translate}}</h2>
</ng-template>
<ng-template #editheader>
<ng-template #editHeader>
<h2 class="border-bottom pb-2">
<span
*dsContextHelp="{
@@ -36,12 +36,12 @@
[displayCancel]="false"
(submitForm)="onSubmit()">
<div before class="btn-group">
<button (click)="onCancel()"
<button (click)="onCancel()" type="button"
class="btn btn-outline-secondary"><i class="fas fa-arrow-left"></i> {{messagePrefix + '.return' | translate}}</button>
</div>
<div after *ngIf="groupBeingEdited != null" class="btn-group">
<div after *ngIf="(canEdit$ | async) && !groupBeingEdited.permanent" class="btn-group">
<button class="btn btn-danger delete-button" [disabled]="!(canEdit$ | async) || groupBeingEdited.permanent"
(click)="delete()">
(click)="delete()" type="button">
<i class="fa fa-trash"></i> {{ messagePrefix + '.actions.delete' | translate}}
</button>
</div>

View File

@@ -10,7 +10,6 @@ import {
} from '@ng-dynamic-forms/core';
import { TranslateService } from '@ngx-translate/core';
import {
ObservedValueOf,
combineLatest as observableCombineLatest,
Observable,
of as observableOf,
@@ -37,7 +36,7 @@ import {
getFirstCompletedRemoteData,
getFirstSucceededRemoteDataPayload
} from '../../../core/shared/operators';
import { AlertType } from '../../../shared/alert/aletr-type';
import { AlertType } from '../../../shared/alert/alert-type';
import { ConfirmationModalComponent } from '../../../shared/confirmation-modal/confirmation-modal.component';
import { hasValue, isNotEmpty, hasValueOperator } from '../../../shared/empty.util';
import { FormBuilderService } from '../../../shared/form/builder/form-builder.service';
@@ -48,6 +47,7 @@ import { Operation } from 'fast-json-patch';
import { ValidateGroupExists } from './validators/group-exists.validator';
import { DSONameService } from '../../../core/breadcrumbs/dso-name.service';
import { environment } from '../../../../environments/environment';
import { getGroupEditRoute, getGroupsRoute } from '../../access-control-routing-paths';
@Component({
selector: 'ds-group-form',
@@ -165,19 +165,19 @@ export class GroupFormComponent implements OnInit, OnDestroy {
this.canEdit$ = this.groupDataService.getActiveGroup().pipe(
hasValueOperator(),
switchMap((group: Group) => {
return observableCombineLatest(
return observableCombineLatest([
this.authorizationService.isAuthorized(FeatureID.CanDelete, isNotEmpty(group) ? group.self : undefined),
this.hasLinkedDSO(group),
(isAuthorized: ObservedValueOf<Observable<boolean>>, hasLinkedDSO: ObservedValueOf<Observable<boolean>>) => {
return isAuthorized && !hasLinkedDSO;
});
})
]).pipe(
map(([isAuthorized, hasLinkedDSO]: [boolean, boolean]) => isAuthorized && !hasLinkedDSO),
);
}),
);
observableCombineLatest(
observableCombineLatest([
this.translateService.get(`${this.messagePrefix}.groupName`),
this.translateService.get(`${this.messagePrefix}.groupCommunity`),
this.translateService.get(`${this.messagePrefix}.groupDescription`)
).subscribe(([groupName, groupCommunity, groupDescription]) => {
]).subscribe(([groupName, groupCommunity, groupDescription]) => {
this.groupName = new DynamicInputModel({
id: 'groupName',
label: groupName,
@@ -215,12 +215,12 @@ export class GroupFormComponent implements OnInit, OnDestroy {
}
this.subs.push(
observableCombineLatest(
observableCombineLatest([
this.groupDataService.getActiveGroup(),
this.canEdit$,
this.groupDataService.getActiveGroup()
.pipe(filter((activeGroup) => hasValue(activeGroup)),switchMap((activeGroup) => this.getLinkedDSO(activeGroup).pipe(getFirstSucceededRemoteDataPayload())))
).subscribe(([activeGroup, canEdit, linkedObject]) => {
]).subscribe(([activeGroup, canEdit, linkedObject]) => {
if (activeGroup != null) {
@@ -230,12 +230,14 @@ export class GroupFormComponent implements OnInit, OnDestroy {
this.groupBeingEdited = activeGroup;
if (linkedObject?.name) {
this.formBuilderService.insertFormGroupControl(1, this.formGroup, this.formModel, this.groupCommunity);
this.formGroup.patchValue({
groupName: activeGroup.name,
groupCommunity: linkedObject?.name ?? '',
groupDescription: activeGroup.firstMetadataValue('dc.description'),
});
if (!this.formGroup.controls.groupCommunity) {
this.formBuilderService.insertFormGroupControl(1, this.formGroup, this.formModel, this.groupCommunity);
this.formGroup.patchValue({
groupName: activeGroup.name,
groupCommunity: linkedObject?.name ?? '',
groupDescription: activeGroup.firstMetadataValue('dc.description'),
});
}
} else {
this.formModel = [
this.groupName,
@@ -263,7 +265,7 @@ export class GroupFormComponent implements OnInit, OnDestroy {
onCancel() {
this.groupDataService.cancelEditGroup();
this.cancelForm.emit();
this.router.navigate([this.groupDataService.getGroupRegistryRouterLink()]);
void this.router.navigate([getGroupsRoute()]);
}
/**
@@ -310,7 +312,7 @@ export class GroupFormComponent implements OnInit, OnDestroy {
const groupSelfLink = rd.payload._links.self.href;
this.setActiveGroupWithLink(groupSelfLink);
this.groupDataService.clearGroupsRequests();
this.router.navigateByUrl(this.groupDataService.getGroupEditPageRouterLinkWithID(rd.payload.uuid));
void this.router.navigateByUrl(getGroupEditRoute(rd.payload.uuid));
}
} else {
this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.created.failure', { name: groupToCreate.name }));

View File

@@ -1,6 +1,60 @@
<ng-container>
<h3 class="border-bottom pb-2">{{messagePrefix + '.head' | translate}}</h3>
<h4>{{messagePrefix + '.headMembers' | translate}}</h4>
<ds-pagination *ngIf="(ePeopleMembersOfGroup | async)?.totalElements > 0"
[paginationOptions]="config"
[pageInfoState]="(ePeopleMembersOfGroup | async)"
[collectionSize]="(ePeopleMembersOfGroup | async)?.totalElements"
[hideGear]="true"
[hidePagerWhenSinglePage]="true">
<div class="table-responsive">
<table id="ePeopleMembersOfGroup" class="table table-striped table-hover table-bordered">
<thead>
<tr>
<th scope="col" class="align-middle">{{messagePrefix + '.table.id' | translate}}</th>
<th scope="col" class="align-middle">{{messagePrefix + '.table.name' | translate}}</th>
<th scope="col" class="align-middle">{{messagePrefix + '.table.identity' | translate}}</th>
<th class="align-middle">{{messagePrefix + '.table.edit' | translate}}</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let eperson of (ePeopleMembersOfGroup | async)?.page">
<td class="align-middle">{{eperson.id}}</td>
<td class="align-middle">
<a (click)="ePersonDataService.startEditingNewEPerson(eperson)"
[routerLink]="[ePersonDataService.getEPeoplePageRouterLink()]">
{{ dsoNameService.getName(eperson) }}
</a>
</td>
<td class="align-middle">
{{messagePrefix + '.table.email' | translate}}: {{ eperson.email ? eperson.email : '-' }}<br/>
{{messagePrefix + '.table.netid' | translate}}: {{ eperson.netid ? eperson.netid : '-' }}
</td>
<td class="align-middle">
<div class="btn-group edit-field">
<button (click)="deleteMemberFromGroup(eperson)"
[disabled]="actionConfig.remove.disabled"
[ngClass]="['btn btn-sm', actionConfig.remove.css]"
title="{{messagePrefix + '.table.edit.buttons.remove' | translate: { name: dsoNameService.getName(eperson) } }}">
<i [ngClass]="actionConfig.remove.icon"></i>
</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</ds-pagination>
<div *ngIf="(ePeopleMembersOfGroup | async) == undefined || (ePeopleMembersOfGroup | async)?.totalElements == 0" class="alert alert-info w-100 mb-2"
role="alert">
{{messagePrefix + '.no-members-yet' | translate}}
</div>
<h4 id="search" class="border-bottom pb-2">
<span
*dsContextHelp="{
@@ -15,14 +69,8 @@
</h4>
<form [formGroup]="searchForm" (ngSubmit)="search(searchForm.value)" class="d-flex justify-content-between">
<div>
<select name="scope" id="scope" formControlName="scope" class="form-control" aria-label="Search scope">
<option value="metadata">{{messagePrefix + '.search.scope.metadata' | translate}}</option>
<option value="email">{{messagePrefix + '.search.scope.email' | translate}}</option>
</select>
</div>
<div class="flex-grow-1 mr-3 ml-3">
<div class="form-group input-group">
<div class="flex-grow-1 mr-3">
<div class="form-group input-group mr-3">
<input type="text" name="query" id="query" formControlName="query"
class="form-control" aria-label="Search input">
<span class="input-group-append">
@@ -37,10 +85,10 @@
</div>
</form>
<ds-pagination *ngIf="(ePeopleSearchDtos | async)?.totalElements > 0"
<ds-pagination *ngIf="(ePeopleSearch | async)?.totalElements > 0"
[paginationOptions]="configSearch"
[pageInfoState]="(ePeopleSearchDtos | async)"
[collectionSize]="(ePeopleSearchDtos | async)?.totalElements"
[pageInfoState]="(ePeopleSearch | async)"
[collectionSize]="(ePeopleSearch | async)?.totalElements"
[hideGear]="true"
[hidePagerWhenSinglePage]="true">
@@ -55,33 +103,24 @@
</tr>
</thead>
<tbody>
<tr *ngFor="let ePerson of (ePeopleSearchDtos | async)?.page">
<td class="align-middle">{{ePerson.eperson.id}}</td>
<tr *ngFor="let eperson of (ePeopleSearch | async)?.page">
<td class="align-middle">{{eperson.id}}</td>
<td class="align-middle">
<a (click)="ePersonDataService.startEditingNewEPerson(ePerson.eperson)"
<a (click)="ePersonDataService.startEditingNewEPerson(eperson)"
[routerLink]="[ePersonDataService.getEPeoplePageRouterLink()]">
{{ dsoNameService.getName(ePerson.eperson) }}
{{ dsoNameService.getName(eperson) }}
</a>
</td>
<td class="align-middle">
{{messagePrefix + '.table.email' | translate}}: {{ ePerson.eperson.email ? ePerson.eperson.email : '-' }}<br/>
{{messagePrefix + '.table.netid' | translate}}: {{ ePerson.eperson.netid ? ePerson.eperson.netid : '-' }}
{{messagePrefix + '.table.email' | translate}}: {{ eperson.email ? eperson.email : '-' }}<br/>
{{messagePrefix + '.table.netid' | translate}}: {{ eperson.netid ? eperson.netid : '-' }}
</td>
<td class="align-middle">
<div class="btn-group edit-field">
<button *ngIf="ePerson.memberOfGroup"
(click)="deleteMemberFromGroup(ePerson)"
[disabled]="actionConfig.remove.disabled"
[ngClass]="['btn btn-sm', actionConfig.remove.css]"
title="{{messagePrefix + '.table.edit.buttons.remove' | translate: { name: dsoNameService.getName(ePerson.eperson) } }}">
<i [ngClass]="actionConfig.remove.icon"></i>
</button>
<button *ngIf="!ePerson.memberOfGroup"
(click)="addMemberToGroup(ePerson)"
<button (click)="addMemberToGroup(eperson)"
[disabled]="actionConfig.add.disabled"
[ngClass]="['btn btn-sm', actionConfig.add.css]"
title="{{messagePrefix + '.table.edit.buttons.add' | translate: { name: dsoNameService.getName(ePerson.eperson) } }}">
title="{{messagePrefix + '.table.edit.buttons.add' | translate: { name: dsoNameService.getName(eperson) } }}">
<i [ngClass]="actionConfig.add.icon"></i>
</button>
</div>
@@ -93,72 +132,10 @@
</ds-pagination>
<div *ngIf="(ePeopleSearchDtos | async)?.totalElements == 0 && searchDone"
<div *ngIf="(ePeopleSearch | async)?.totalElements == 0 && searchDone"
class="alert alert-info w-100 mb-2"
role="alert">
{{messagePrefix + '.no-items' | translate}}
</div>
<h4>{{messagePrefix + '.headMembers' | translate}}</h4>
<ds-pagination *ngIf="(ePeopleMembersOfGroupDtos | async)?.totalElements > 0"
[paginationOptions]="config"
[pageInfoState]="(ePeopleMembersOfGroupDtos | async)"
[collectionSize]="(ePeopleMembersOfGroupDtos | async)?.totalElements"
[hideGear]="true"
[hidePagerWhenSinglePage]="true">
<div class="table-responsive">
<table id="ePeopleMembersOfGroup" class="table table-striped table-hover table-bordered">
<thead>
<tr>
<th scope="col" class="align-middle">{{messagePrefix + '.table.id' | translate}}</th>
<th scope="col" class="align-middle">{{messagePrefix + '.table.name' | translate}}</th>
<th scope="col" class="align-middle">{{messagePrefix + '.table.identity' | translate}}</th>
<th class="align-middle">{{messagePrefix + '.table.edit' | translate}}</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let ePerson of (ePeopleMembersOfGroupDtos | async)?.page">
<td class="align-middle">{{ePerson.eperson.id}}</td>
<td class="align-middle">
<a (click)="ePersonDataService.startEditingNewEPerson(ePerson.eperson)"
[routerLink]="[ePersonDataService.getEPeoplePageRouterLink()]">
{{ dsoNameService.getName(ePerson.eperson) }}
</a>
</td>
<td class="align-middle">
{{messagePrefix + '.table.email' | translate}}: {{ ePerson.eperson.email ? ePerson.eperson.email : '-' }}<br/>
{{messagePrefix + '.table.netid' | translate}}: {{ ePerson.eperson.netid ? ePerson.eperson.netid : '-' }}
</td>
<td class="align-middle">
<div class="btn-group edit-field">
<button *ngIf="ePerson.memberOfGroup"
(click)="deleteMemberFromGroup(ePerson)"
[disabled]="actionConfig.remove.disabled"
[ngClass]="['btn btn-sm', actionConfig.remove.css]"
title="{{messagePrefix + '.table.edit.buttons.remove' | translate: { name: dsoNameService.getName(ePerson.eperson) } }}">
<i [ngClass]="actionConfig.remove.icon"></i>
</button>
<button *ngIf="!ePerson.memberOfGroup"
(click)="addMemberToGroup(ePerson)"
[disabled]="actionConfig.add.disabled"
[ngClass]="['btn btn-sm', actionConfig.add.css]"
title="{{messagePrefix + '.table.edit.buttons.add' | translate: { name: dsoNameService.getName(ePerson.eperson) } }}">
<i [ngClass]="actionConfig.add.icon"></i>
</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</ds-pagination>
<div *ngIf="(ePeopleMembersOfGroupDtos | async) == undefined || (ePeopleMembersOfGroupDtos | async)?.totalElements == 0" class="alert alert-info w-100 mb-2"
role="alert">
{{messagePrefix + '.no-members-yet' | translate}}
</div>
</ng-container>

View File

@@ -17,7 +17,7 @@ import { Group } from '../../../../core/eperson/models/group.model';
import { PageInfo } from '../../../../core/shared/page-info.model';
import { FormBuilderService } from '../../../../shared/form/builder/form-builder.service';
import { NotificationsService } from '../../../../shared/notifications/notifications.service';
import { GroupMock, GroupMock2 } from '../../../../shared/testing/group-mock';
import { GroupMock } from '../../../../shared/testing/group-mock';
import { MembersListComponent } from './members-list.component';
import { EPersonMock, EPersonMock2 } from '../../../../shared/testing/eperson.mock';
import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-data.utils';
@@ -39,28 +39,26 @@ describe('MembersListComponent', () => {
let ePersonDataServiceStub: any;
let groupsDataServiceStub: any;
let activeGroup;
let allEPersons: EPerson[];
let allGroups: Group[];
let epersonMembers: EPerson[];
let subgroupMembers: Group[];
let epersonNonMembers: EPerson[];
let paginationService;
beforeEach(waitForAsync(() => {
activeGroup = GroupMock;
epersonMembers = [EPersonMock2];
subgroupMembers = [GroupMock2];
allEPersons = [EPersonMock, EPersonMock2];
allGroups = [GroupMock, GroupMock2];
epersonNonMembers = [EPersonMock];
ePersonDataServiceStub = {
activeGroup: activeGroup,
epersonMembers: epersonMembers,
subgroupMembers: subgroupMembers,
epersonNonMembers: epersonNonMembers,
// This method is used to get all the current members
findListByHref(_href: string): Observable<RemoteData<PaginatedList<EPerson>>> {
return createSuccessfulRemoteDataObject$(buildPaginatedList<EPerson>(new PageInfo(), groupsDataServiceStub.getEPersonMembers()));
},
searchByScope(scope: string, query: string): Observable<RemoteData<PaginatedList<EPerson>>> {
// This method is used to search across *non-members*
searchNonMembers(query: string, group: string): Observable<RemoteData<PaginatedList<EPerson>>> {
if (query === '') {
return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), allEPersons));
return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), epersonNonMembers));
}
return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), []));
},
@@ -77,22 +75,22 @@ describe('MembersListComponent', () => {
groupsDataServiceStub = {
activeGroup: activeGroup,
epersonMembers: epersonMembers,
subgroupMembers: subgroupMembers,
allGroups: allGroups,
epersonNonMembers: epersonNonMembers,
getActiveGroup(): Observable<Group> {
return observableOf(activeGroup);
},
getEPersonMembers() {
return this.epersonMembers;
},
searchGroups(query: string): Observable<RemoteData<PaginatedList<Group>>> {
if (query === '') {
return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), this.allGroups));
}
return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), []));
},
addMemberToGroup(parentGroup, eperson: EPerson): Observable<RestResponse> {
this.epersonMembers = [...this.epersonMembers, eperson];
addMemberToGroup(parentGroup, epersonToAdd: EPerson): Observable<RestResponse> {
// Add eperson to list of members
this.epersonMembers = [...this.epersonMembers, epersonToAdd];
// Remove eperson from list of non-members
this.epersonNonMembers.forEach( (eperson: EPerson, index: number) => {
if (eperson.id === epersonToAdd.id) {
this.epersonNonMembers.splice(index, 1);
}
});
return observableOf(new RestResponse(true, 200, 'Success'));
},
clearGroupsRequests() {
@@ -105,14 +103,14 @@ describe('MembersListComponent', () => {
return '/access-control/groups/' + group.id;
},
deleteMemberFromGroup(parentGroup, epersonToDelete: EPerson): Observable<RestResponse> {
this.epersonMembers = this.epersonMembers.find((eperson: EPerson) => {
if (eperson.id !== epersonToDelete.id) {
return eperson;
// Remove eperson from list of members
this.epersonMembers.forEach( (eperson: EPerson, index: number) => {
if (eperson.id === epersonToDelete.id) {
this.epersonMembers.splice(index, 1);
}
});
if (this.epersonMembers === undefined) {
this.epersonMembers = [];
}
// Add eperson to list of non-members
this.epersonNonMembers = [...this.epersonNonMembers, epersonToDelete];
return observableOf(new RestResponse(true, 200, 'Success'));
}
};
@@ -160,13 +158,37 @@ describe('MembersListComponent', () => {
expect(comp).toBeDefined();
}));
it('should show list of eperson members of current active group', () => {
const epersonIdsFound = fixture.debugElement.queryAll(By.css('#ePeopleMembersOfGroup tr td:first-child'));
expect(epersonIdsFound.length).toEqual(1);
epersonMembers.map((eperson: EPerson) => {
expect(epersonIdsFound.find((foundEl) => {
return (foundEl.nativeElement.textContent.trim() === eperson.uuid);
})).toBeTruthy();
describe('current members list', () => {
it('should show list of eperson members of current active group', () => {
const epersonIdsFound = fixture.debugElement.queryAll(By.css('#ePeopleMembersOfGroup tr td:first-child'));
expect(epersonIdsFound.length).toEqual(1);
epersonMembers.map((eperson: EPerson) => {
expect(epersonIdsFound.find((foundEl) => {
return (foundEl.nativeElement.textContent.trim() === eperson.uuid);
})).toBeTruthy();
});
});
it('should show a delete button next to each member', () => {
const epersonsFound = fixture.debugElement.queryAll(By.css('#ePeopleMembersOfGroup tbody tr'));
epersonsFound.map((foundEPersonRowElement: DebugElement) => {
const addButton: DebugElement = foundEPersonRowElement.query(By.css('td:last-child .fa-plus'));
const deleteButton: DebugElement = foundEPersonRowElement.query(By.css('td:last-child .fa-trash-alt'));
expect(addButton).toBeNull();
expect(deleteButton).not.toBeNull();
});
});
describe('if first delete button is pressed', () => {
beforeEach(() => {
const deleteButton: DebugElement = fixture.debugElement.query(By.css('#ePeopleMembersOfGroup tbody .fa-trash-alt'));
deleteButton.nativeElement.click();
fixture.detectChanges();
});
it('then no ePerson remains as a member of the active group.', () => {
const epersonsFound = fixture.debugElement.queryAll(By.css('#ePeopleMembersOfGroup tbody tr'));
expect(epersonsFound.length).toEqual(0);
});
});
});
@@ -174,76 +196,40 @@ describe('MembersListComponent', () => {
describe('when searching without query', () => {
let epersonsFound: DebugElement[];
beforeEach(fakeAsync(() => {
spyOn(component, 'isMemberOfGroup').and.callFake((ePerson: EPerson) => {
return observableOf(activeGroup.epersons.includes(ePerson));
});
component.search({ scope: 'metadata', query: '' });
tick();
fixture.detectChanges();
epersonsFound = fixture.debugElement.queryAll(By.css('#epersonsSearch tbody tr'));
// Stop using the fake spy function (because otherwise the clicking on the buttons will not change anything
// because they don't change the value of activeGroup.epersons)
jasmine.getEnv().allowRespy(true);
spyOn(component, 'isMemberOfGroup').and.callThrough();
}));
it('should display all epersons', () => {
expect(epersonsFound.length).toEqual(2);
it('should display only non-members of the group', () => {
const epersonIdsFound = fixture.debugElement.queryAll(By.css('#epersonsSearch tbody tr td:first-child'));
expect(epersonIdsFound.length).toEqual(1);
epersonNonMembers.map((eperson: EPerson) => {
expect(epersonIdsFound.find((foundEl) => {
return (foundEl.nativeElement.textContent.trim() === eperson.uuid);
})).toBeTruthy();
});
});
describe('if eperson is already a eperson', () => {
it('should have delete button, else it should have add button', () => {
const memberIds: string[] = activeGroup.epersons.map((ePerson: EPerson) => ePerson.id);
epersonsFound.map((foundEPersonRowElement: DebugElement) => {
const epersonId: DebugElement = foundEPersonRowElement.query(By.css('td:first-child'));
const addButton: DebugElement = foundEPersonRowElement.query(By.css('td:last-child .fa-plus'));
const deleteButton: DebugElement = foundEPersonRowElement.query(By.css('td:last-child .fa-trash-alt'));
if (memberIds.includes(epersonId.nativeElement.textContent)) {
expect(addButton).toBeNull();
expect(deleteButton).not.toBeNull();
} else {
expect(deleteButton).toBeNull();
expect(addButton).not.toBeNull();
}
});
it('should display an add button next to non-members, not a delete button', () => {
epersonsFound.map((foundEPersonRowElement: DebugElement) => {
const addButton: DebugElement = foundEPersonRowElement.query(By.css('td:last-child .fa-plus'));
const deleteButton: DebugElement = foundEPersonRowElement.query(By.css('td:last-child .fa-trash-alt'));
expect(addButton).not.toBeNull();
expect(deleteButton).toBeNull();
});
});
describe('if first add button is pressed', () => {
beforeEach(fakeAsync(() => {
beforeEach(() => {
const addButton: DebugElement = fixture.debugElement.query(By.css('#epersonsSearch tbody .fa-plus'));
addButton.nativeElement.click();
tick();
fixture.detectChanges();
}));
it('then all the ePersons are member of the active group', () => {
epersonsFound = fixture.debugElement.queryAll(By.css('#epersonsSearch tbody tr'));
expect(epersonsFound.length).toEqual(2);
epersonsFound.map((foundEPersonRowElement: DebugElement) => {
const addButton: DebugElement = foundEPersonRowElement.query(By.css('td:last-child .fa-plus'));
const deleteButton: DebugElement = foundEPersonRowElement.query(By.css('td:last-child .fa-trash-alt'));
expect(addButton).toBeNull();
expect(deleteButton).not.toBeNull();
});
});
});
describe('if first delete button is pressed', () => {
beforeEach(fakeAsync(() => {
const deleteButton: DebugElement = fixture.debugElement.query(By.css('#epersonsSearch tbody .fa-trash-alt'));
deleteButton.nativeElement.click();
tick();
fixture.detectChanges();
}));
it('then no ePerson is member of the active group', () => {
it('then all (two) ePersons are member of the active group. No non-members left', () => {
epersonsFound = fixture.debugElement.queryAll(By.css('#epersonsSearch tbody tr'));
expect(epersonsFound.length).toEqual(2);
epersonsFound.map((foundEPersonRowElement: DebugElement) => {
const addButton: DebugElement = foundEPersonRowElement.query(By.css('td:last-child .fa-plus'));
const deleteButton: DebugElement = foundEPersonRowElement.query(By.css('td:last-child .fa-trash-alt'));
expect(deleteButton).toBeNull();
expect(addButton).not.toBeNull();
});
expect(epersonsFound.length).toEqual(0);
});
});
});

View File

@@ -4,28 +4,23 @@ import { Router } from '@angular/router';
import { TranslateService } from '@ngx-translate/core';
import {
Observable,
of as observableOf,
Subscription,
BehaviorSubject,
combineLatest as observableCombineLatest,
ObservedValueOf,
BehaviorSubject
} from 'rxjs';
import { defaultIfEmpty, map, mergeMap, switchMap, take } from 'rxjs/operators';
import { buildPaginatedList, PaginatedList } from '../../../../core/data/paginated-list.model';
import { map, switchMap, take } from 'rxjs/operators';
import { PaginatedList } from '../../../../core/data/paginated-list.model';
import { RemoteData } from '../../../../core/data/remote-data';
import { EPersonDataService } from '../../../../core/eperson/eperson-data.service';
import { GroupDataService } from '../../../../core/eperson/group-data.service';
import { EPerson } from '../../../../core/eperson/models/eperson.model';
import { Group } from '../../../../core/eperson/models/group.model';
import {
getFirstSucceededRemoteData,
getFirstCompletedRemoteData,
getAllCompletedRemoteData,
getRemoteDataPayload
} from '../../../../core/shared/operators';
import { NotificationsService } from '../../../../shared/notifications/notifications.service';
import { PaginationComponentOptions } from '../../../../shared/pagination/pagination-component-options.model';
import { EpersonDtoModel } from '../../../../core/eperson/models/eperson-dto.model';
import { PaginationService } from '../../../../core/pagination/pagination.service';
import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service';
@@ -34,8 +29,8 @@ import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service';
*/
enum SubKey {
ActiveGroup,
MembersDTO,
SearchResultsDTO,
Members,
SearchResults,
}
/**
@@ -96,11 +91,11 @@ export class MembersListComponent implements OnInit, OnDestroy {
/**
* EPeople being displayed in search result, initially all members, after search result of search
*/
ePeopleSearchDtos: BehaviorSubject<PaginatedList<EpersonDtoModel>> = new BehaviorSubject<PaginatedList<EpersonDtoModel>>(undefined);
ePeopleSearch: BehaviorSubject<PaginatedList<EPerson>> = new BehaviorSubject<PaginatedList<EPerson>>(undefined);
/**
* List of EPeople members of currently active group being edited
*/
ePeopleMembersOfGroupDtos: BehaviorSubject<PaginatedList<EpersonDtoModel>> = new BehaviorSubject<PaginatedList<EpersonDtoModel>>(undefined);
ePeopleMembersOfGroup: BehaviorSubject<PaginatedList<EPerson>> = new BehaviorSubject<PaginatedList<EPerson>>(undefined);
/**
* Pagination config used to display the list of EPeople that are result of EPeople search
@@ -129,7 +124,6 @@ export class MembersListComponent implements OnInit, OnDestroy {
// Current search in edit group - epeople search form
currentSearchQuery: string;
currentSearchScope: string;
// Whether or not user has done a EPeople search yet
searchDone: boolean;
@@ -148,18 +142,17 @@ export class MembersListComponent implements OnInit, OnDestroy {
public dsoNameService: DSONameService,
) {
this.currentSearchQuery = '';
this.currentSearchScope = 'metadata';
}
ngOnInit(): void {
this.searchForm = this.formBuilder.group(({
scope: 'metadata',
query: '',
}));
this.subs.set(SubKey.ActiveGroup, this.groupDataService.getActiveGroup().subscribe((activeGroup: Group) => {
if (activeGroup != null) {
this.groupBeingEdited = activeGroup;
this.retrieveMembers(this.config.currentPage);
this.search({query: ''});
}
}));
}
@@ -171,8 +164,8 @@ export class MembersListComponent implements OnInit, OnDestroy {
* @private
*/
retrieveMembers(page: number): void {
this.unsubFrom(SubKey.MembersDTO);
this.subs.set(SubKey.MembersDTO,
this.unsubFrom(SubKey.Members);
this.subs.set(SubKey.Members,
this.paginationService.getCurrentPagination(this.config.id, this.config).pipe(
switchMap((currentPagination) => {
return this.ePersonDataService.findListByHref(this.groupBeingEdited._links.epersons.href, {
@@ -189,49 +182,12 @@ export class MembersListComponent implements OnInit, OnDestroy {
return rd;
}
}),
switchMap((epersonListRD: RemoteData<PaginatedList<EPerson>>) => {
const dtos$ = observableCombineLatest([...epersonListRD.payload.page.map((member: EPerson) => {
const dto$: Observable<EpersonDtoModel> = observableCombineLatest(
this.isMemberOfGroup(member), (isMember: ObservedValueOf<Observable<boolean>>) => {
const epersonDtoModel: EpersonDtoModel = new EpersonDtoModel();
epersonDtoModel.eperson = member;
epersonDtoModel.memberOfGroup = isMember;
return epersonDtoModel;
});
return dto$;
})]);
return dtos$.pipe(defaultIfEmpty([]), map((dtos: EpersonDtoModel[]) => {
return buildPaginatedList(epersonListRD.payload.pageInfo, dtos);
}));
}))
.subscribe((paginatedListOfDTOs: PaginatedList<EpersonDtoModel>) => {
this.ePeopleMembersOfGroupDtos.next(paginatedListOfDTOs);
getRemoteDataPayload())
.subscribe((paginatedListOfEPersons: PaginatedList<EPerson>) => {
this.ePeopleMembersOfGroup.next(paginatedListOfEPersons);
}));
}
/**
* Whether the given ePerson is a member of the group currently being edited
* @param possibleMember EPerson that is a possible member (being tested) of the group currently being edited
*/
isMemberOfGroup(possibleMember: EPerson): Observable<boolean> {
return this.groupDataService.getActiveGroup().pipe(take(1),
mergeMap((group: Group) => {
if (group != null) {
return this.ePersonDataService.findListByHref(group._links.epersons.href, {
currentPage: 1,
elementsPerPage: 9999
})
.pipe(
getFirstSucceededRemoteData(),
getRemoteDataPayload(),
map((listEPeopleInGroup: PaginatedList<EPerson>) => listEPeopleInGroup.page.filter((ePersonInList: EPerson) => ePersonInList.id === possibleMember.id)),
map((epeople: EPerson[]) => epeople.length > 0));
} else {
return observableOf(false);
}
}));
}
/**
* Unsubscribe from a subscription if it's still subscribed, and remove it from the map of
* active subscriptions
@@ -248,14 +204,18 @@ export class MembersListComponent implements OnInit, OnDestroy {
/**
* Deletes a given EPerson from the members list of the group currently being edited
* @param ePerson EPerson we want to delete as member from group that is currently being edited
* @param eperson EPerson we want to delete as member from group that is currently being edited
*/
deleteMemberFromGroup(ePerson: EpersonDtoModel) {
ePerson.memberOfGroup = false;
deleteMemberFromGroup(eperson: EPerson) {
this.groupDataService.getActiveGroup().pipe(take(1)).subscribe((activeGroup: Group) => {
if (activeGroup != null) {
const response = this.groupDataService.deleteMemberFromGroup(activeGroup, ePerson.eperson);
this.showNotifications('deleteMember', response, this.dsoNameService.getName(ePerson.eperson), activeGroup);
const response = this.groupDataService.deleteMemberFromGroup(activeGroup, eperson);
this.showNotifications('deleteMember', response, this.dsoNameService.getName(eperson), activeGroup);
// Reload search results (if there is an active query).
// This will potentially add this deleted subgroup into the list of search results.
if (this.currentSearchQuery != null) {
this.search({query: this.currentSearchQuery});
}
} else {
this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.failure.noActiveGroup'));
}
@@ -264,14 +224,18 @@ export class MembersListComponent implements OnInit, OnDestroy {
/**
* Adds a given EPerson to the members list of the group currently being edited
* @param ePerson EPerson we want to add as member to group that is currently being edited
* @param eperson EPerson we want to add as member to group that is currently being edited
*/
addMemberToGroup(ePerson: EpersonDtoModel) {
ePerson.memberOfGroup = true;
addMemberToGroup(eperson: EPerson) {
this.groupDataService.getActiveGroup().pipe(take(1)).subscribe((activeGroup: Group) => {
if (activeGroup != null) {
const response = this.groupDataService.addMemberToGroup(activeGroup, ePerson.eperson);
this.showNotifications('addMember', response, this.dsoNameService.getName(ePerson.eperson), activeGroup);
const response = this.groupDataService.addMemberToGroup(activeGroup, eperson);
this.showNotifications('addMember', response, this.dsoNameService.getName(eperson), activeGroup);
// Reload search results (if there is an active query).
// This will potentially add this deleted subgroup into the list of search results.
if (this.currentSearchQuery != null) {
this.search({query: this.currentSearchQuery});
}
} else {
this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.failure.noActiveGroup'));
}
@@ -279,37 +243,25 @@ export class MembersListComponent implements OnInit, OnDestroy {
}
/**
* Search in the EPeople by name, email or metadata
* @param data Contains scope and query param
* Search all EPeople who are NOT a member of the current group by name, email or metadata
* @param data Contains query param
*/
search(data: any) {
this.unsubFrom(SubKey.SearchResultsDTO);
this.subs.set(SubKey.SearchResultsDTO,
this.unsubFrom(SubKey.SearchResults);
this.subs.set(SubKey.SearchResults,
this.paginationService.getCurrentPagination(this.configSearch.id, this.configSearch).pipe(
switchMap((paginationOptions) => {
const query: string = data.query;
const scope: string = data.scope;
if (query != null && this.currentSearchQuery !== query && this.groupBeingEdited) {
this.router.navigate([], {
queryParamsHandling: 'merge'
});
this.currentSearchQuery = query;
this.paginationService.resetPage(this.configSearch.id);
}
if (scope != null && this.currentSearchScope !== scope && this.groupBeingEdited) {
this.router.navigate([], {
queryParamsHandling: 'merge'
});
this.currentSearchScope = scope;
this.paginationService.resetPage(this.configSearch.id);
}
this.searchDone = true;
return this.ePersonDataService.searchByScope(this.currentSearchScope, this.currentSearchQuery, {
return this.ePersonDataService.searchNonMembers(this.currentSearchQuery, this.groupBeingEdited.id, {
currentPage: paginationOptions.currentPage,
elementsPerPage: paginationOptions.pageSize
});
}, false, true);
}),
getAllCompletedRemoteData(),
map((rd: RemoteData<any>) => {
@@ -319,23 +271,9 @@ export class MembersListComponent implements OnInit, OnDestroy {
return rd;
}
}),
switchMap((epersonListRD: RemoteData<PaginatedList<EPerson>>) => {
const dtos$ = observableCombineLatest([...epersonListRD.payload.page.map((member: EPerson) => {
const dto$: Observable<EpersonDtoModel> = observableCombineLatest(
this.isMemberOfGroup(member), (isMember: ObservedValueOf<Observable<boolean>>) => {
const epersonDtoModel: EpersonDtoModel = new EpersonDtoModel();
epersonDtoModel.eperson = member;
epersonDtoModel.memberOfGroup = isMember;
return epersonDtoModel;
});
return dto$;
})]);
return dtos$.pipe(defaultIfEmpty([]), map((dtos: EpersonDtoModel[]) => {
return buildPaginatedList(epersonListRD.payload.pageInfo, dtos);
}));
}))
.subscribe((paginatedListOfDTOs: PaginatedList<EpersonDtoModel>) => {
this.ePeopleSearchDtos.next(paginatedListOfDTOs);
getRemoteDataPayload())
.subscribe((paginatedListOfEPersons: PaginatedList<EPerson>) => {
this.ePeopleSearch.next(paginatedListOfEPersons);
}));
}

View File

@@ -1,6 +1,55 @@
<ng-container>
<h3 class="border-bottom pb-2">{{messagePrefix + '.head' | translate}}</h3>
<h4>{{messagePrefix + '.headSubgroups' | translate}}</h4>
<ds-pagination *ngIf="(subGroups$ | async)?.payload?.totalElements > 0"
[paginationOptions]="config"
[pageInfoState]="(subGroups$ | async)?.payload"
[collectionSize]="(subGroups$ | async)?.payload?.totalElements"
[hideGear]="true"
[hidePagerWhenSinglePage]="true">
<div class="table-responsive">
<table id="subgroupsOfGroup" class="table table-striped table-hover table-bordered">
<thead>
<tr>
<th scope="col" class="align-middle">{{messagePrefix + '.table.id' | translate}}</th>
<th scope="col" class="align-middle">{{messagePrefix + '.table.name' | translate}}</th>
<th scope="col" class="align-middle">{{messagePrefix + '.table.collectionOrCommunity' | translate}}</th>
<th>{{messagePrefix + '.table.edit' | translate}}</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let group of (subGroups$ | async)?.payload?.page">
<td class="align-middle">{{group.id}}</td>
<td class="align-middle">
<a (click)="groupDataService.startEditingNewGroup(group)"
[routerLink]="[groupDataService.getGroupEditPageRouterLink(group)]">
{{ dsoNameService.getName(group) }}
</a>
</td>
<td class="align-middle">{{ dsoNameService.getName((group.object | async)?.payload)}}</td>
<td class="align-middle">
<div class="btn-group edit-field">
<button (click)="deleteSubgroupFromGroup(group)"
class="btn btn-outline-danger btn-sm deleteButton"
title="{{messagePrefix + '.table.edit.buttons.remove' | translate: { name: dsoNameService.getName(group) } }}">
<i class="fas fa-trash-alt fa-fw"></i>
</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</ds-pagination>
<div *ngIf="(subGroups$ | async)?.payload?.totalElements == 0" class="alert alert-info w-100 mb-2"
role="alert">
{{messagePrefix + '.no-subgroups-yet' | translate}}
</div>
<h4 id="search" class="border-bottom pb-2">
<span *dsContextHelp="{
content: 'admin.access-control.groups.form.tooltip.editGroup.addSubgroups',
@@ -62,17 +111,7 @@
<td class="align-middle">{{ dsoNameService.getName((group.object | async)?.payload) }}</td>
<td class="align-middle">
<div class="btn-group edit-field">
<button *ngIf="(isSubgroupOfGroup(group) | async) && !(isActiveGroup(group) | async)"
(click)="deleteSubgroupFromGroup(group)"
class="btn btn-outline-danger btn-sm deleteButton"
title="{{messagePrefix + '.table.edit.buttons.remove' | translate: { name: dsoNameService.getName(group) } }}">
<i class="fas fa-trash-alt fa-fw"></i>
</button>
<span *ngIf="(isActiveGroup(group) | async)">{{ messagePrefix + '.table.edit.currentGroup' | translate }}</span>
<button *ngIf="!(isSubgroupOfGroup(group) | async) && !(isActiveGroup(group) | async)"
(click)="addSubgroupToGroup(group)"
<button (click)="addSubgroupToGroup(group)"
class="btn btn-outline-primary btn-sm addButton"
title="{{messagePrefix + '.table.edit.buttons.add' | translate: { name: dsoNameService.getName(group) } }}">
<i class="fas fa-plus fa-fw"></i>
@@ -90,53 +129,4 @@
{{messagePrefix + '.no-items' | translate}}
</div>
<h4>{{messagePrefix + '.headSubgroups' | translate}}</h4>
<ds-pagination *ngIf="(subGroups$ | async)?.payload?.totalElements > 0"
[paginationOptions]="config"
[pageInfoState]="(subGroups$ | async)?.payload"
[collectionSize]="(subGroups$ | async)?.payload?.totalElements"
[hideGear]="true"
[hidePagerWhenSinglePage]="true">
<div class="table-responsive">
<table id="subgroupsOfGroup" class="table table-striped table-hover table-bordered">
<thead>
<tr>
<th scope="col" class="align-middle">{{messagePrefix + '.table.id' | translate}}</th>
<th scope="col" class="align-middle">{{messagePrefix + '.table.name' | translate}}</th>
<th scope="col" class="align-middle">{{messagePrefix + '.table.collectionOrCommunity' | translate}}</th>
<th>{{messagePrefix + '.table.edit' | translate}}</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let group of (subGroups$ | async)?.payload?.page">
<td class="align-middle">{{group.id}}</td>
<td class="align-middle">
<a (click)="groupDataService.startEditingNewGroup(group)"
[routerLink]="[groupDataService.getGroupEditPageRouterLink(group)]">
{{ dsoNameService.getName(group) }}
</a>
</td>
<td class="align-middle">{{ dsoNameService.getName((group.object | async)?.payload)}}</td>
<td class="align-middle">
<div class="btn-group edit-field">
<button (click)="deleteSubgroupFromGroup(group)"
class="btn btn-outline-danger btn-sm deleteButton"
title="{{messagePrefix + '.table.edit.buttons.remove' | translate: { name: dsoNameService.getName(group) } }}">
<i class="fas fa-trash-alt fa-fw"></i>
</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</ds-pagination>
<div *ngIf="(subGroups$ | async)?.payload?.totalElements == 0" class="alert alert-info w-100 mb-2"
role="alert">
{{messagePrefix + '.no-subgroups-yet' | translate}}
</div>
</ng-container>

View File

@@ -1,12 +1,12 @@
import { CommonModule } from '@angular/common';
import { NO_ERRORS_SCHEMA, DebugElement } from '@angular/core';
import { ComponentFixture, fakeAsync, flush, inject, TestBed, tick, waitForAsync } from '@angular/core/testing';
import { ComponentFixture, fakeAsync, flush, inject, TestBed, waitForAsync } from '@angular/core/testing';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { BrowserModule, By } from '@angular/platform-browser';
import { Router } from '@angular/router';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { TranslateLoader, TranslateModule, TranslateService } from '@ngx-translate/core';
import { Observable, of as observableOf, BehaviorSubject } from 'rxjs';
import { Observable, of as observableOf } from 'rxjs';
import { RestResponse } from '../../../../core/cache/response.models';
import { buildPaginatedList, PaginatedList } from '../../../../core/data/paginated-list.model';
import { RemoteData } from '../../../../core/data/remote-data';
@@ -18,19 +18,18 @@ import { NotificationsService } from '../../../../shared/notifications/notificat
import { GroupMock, GroupMock2 } from '../../../../shared/testing/group-mock';
import { SubgroupsListComponent } from './subgroups-list.component';
import {
createSuccessfulRemoteDataObject$,
createSuccessfulRemoteDataObject
createSuccessfulRemoteDataObject$
} from '../../../../shared/remote-data.utils';
import { RouterMock } from '../../../../shared/mocks/router.mock';
import { getMockFormBuilderService } from '../../../../shared/mocks/form-builder-service.mock';
import { getMockTranslateService } from '../../../../shared/mocks/translate.service.mock';
import { TranslateLoaderMock } from '../../../../shared/testing/translate-loader.mock';
import { NotificationsServiceStub } from '../../../../shared/testing/notifications-service.stub';
import { map } from 'rxjs/operators';
import { PaginationService } from '../../../../core/pagination/pagination.service';
import { PaginationServiceStub } from '../../../../shared/testing/pagination-service.stub';
import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service';
import { DSONameServiceMock } from '../../../../shared/mocks/dso-name.service.mock';
import { EPersonMock2 } from 'src/app/shared/testing/eperson.mock';
describe('SubgroupsListComponent', () => {
let component: SubgroupsListComponent;
@@ -39,44 +38,70 @@ describe('SubgroupsListComponent', () => {
let builderService: FormBuilderService;
let ePersonDataServiceStub: any;
let groupsDataServiceStub: any;
let activeGroup;
let activeGroup: Group;
let subgroups: Group[];
let allGroups: Group[];
let groupNonMembers: Group[];
let routerStub;
let paginationService;
// Define a new mock activegroup for all tests below
let mockActiveGroup: Group = Object.assign(new Group(), {
handle: null,
subgroups: [GroupMock2],
epersons: [EPersonMock2],
selfRegistered: false,
permanent: false,
_links: {
self: {
href: 'https://rest.api/server/api/eperson/groups/activegroupid',
},
subgroups: { href: 'https://rest.api/server/api/eperson/groups/activegroupid/subgroups' },
object: { href: 'https://rest.api/server/api/eperson/groups/activegroupid/object' },
epersons: { href: 'https://rest.api/server/api/eperson/groups/activegroupid/epersons' }
},
_name: 'activegroupname',
id: 'activegroupid',
uuid: 'activegroupid',
type: 'group',
});
beforeEach(waitForAsync(() => {
activeGroup = GroupMock;
activeGroup = mockActiveGroup;
subgroups = [GroupMock2];
allGroups = [GroupMock, GroupMock2];
groupNonMembers = [GroupMock];
ePersonDataServiceStub = {};
groupsDataServiceStub = {
activeGroup: activeGroup,
subgroups$: new BehaviorSubject(subgroups),
subgroups: subgroups,
groupNonMembers: groupNonMembers,
getActiveGroup(): Observable<Group> {
return observableOf(this.activeGroup);
},
getSubgroups(): Group {
return this.activeGroup;
return this.subgroups;
},
// This method is used to get all the current subgroups
findListByHref(_href: string): Observable<RemoteData<PaginatedList<Group>>> {
return this.subgroups$.pipe(
map((currentGroups: Group[]) => {
return createSuccessfulRemoteDataObject(buildPaginatedList<Group>(new PageInfo(), currentGroups));
})
);
return createSuccessfulRemoteDataObject$(buildPaginatedList<Group>(new PageInfo(), groupsDataServiceStub.getSubgroups()));
},
getGroupEditPageRouterLink(group: Group): string {
return '/access-control/groups/' + group.id;
},
searchGroups(query: string): Observable<RemoteData<PaginatedList<Group>>> {
// This method is used to get all groups which are NOT currently a subgroup member
searchNonMemberGroups(query: string, group: string): Observable<RemoteData<PaginatedList<Group>>> {
if (query === '') {
return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), allGroups));
return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), groupNonMembers));
}
return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), []));
},
addSubGroupToGroup(parentGroup, subgroup: Group): Observable<RestResponse> {
this.subgroups$.next([...this.subgroups$.getValue(), subgroup]);
addSubGroupToGroup(parentGroup, subgroupToAdd: Group): Observable<RestResponse> {
// Add group to list of subgroups
this.subgroups = [...this.subgroups, subgroupToAdd];
// Remove group from list of non-members
this.groupNonMembers.forEach( (group: Group, index: number) => {
if (group.id === subgroupToAdd.id) {
this.groupNonMembers.splice(index, 1);
}
});
return observableOf(new RestResponse(true, 200, 'Success'));
},
clearGroupsRequests() {
@@ -85,12 +110,15 @@ describe('SubgroupsListComponent', () => {
clearGroupLinkRequests() {
// empty
},
deleteSubGroupFromGroup(parentGroup, subgroup: Group): Observable<RestResponse> {
this.subgroups$.next(this.subgroups$.getValue().filter((group: Group) => {
if (group.id !== subgroup.id) {
return group;
deleteSubGroupFromGroup(parentGroup, subgroupToDelete: Group): Observable<RestResponse> {
// Remove group from list of subgroups
this.subgroups.forEach( (group: Group, index: number) => {
if (group.id === subgroupToDelete.id) {
this.subgroups.splice(index, 1);
}
}));
});
// Add group to list of non-members
this.groupNonMembers = [...this.groupNonMembers, subgroupToDelete];
return observableOf(new RestResponse(true, 200, 'Success'));
}
};
@@ -99,7 +127,7 @@ describe('SubgroupsListComponent', () => {
translateService = getMockTranslateService();
paginationService = new PaginationServiceStub();
TestBed.configureTestingModule({
return TestBed.configureTestingModule({
imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule,
TranslateModule.forRoot({
loader: {
@@ -137,30 +165,38 @@ describe('SubgroupsListComponent', () => {
expect(comp).toBeDefined();
}));
it('should show list of subgroups of current active group', () => {
const groupIdsFound = fixture.debugElement.queryAll(By.css('#subgroupsOfGroup tr td:first-child'));
expect(groupIdsFound.length).toEqual(1);
activeGroup.subgroups.map((group: Group) => {
expect(groupIdsFound.find((foundEl) => {
return (foundEl.nativeElement.textContent.trim() === group.uuid);
})).toBeTruthy();
});
});
describe('if first group delete button is pressed', () => {
let groupsFound: DebugElement[];
beforeEach(fakeAsync(() => {
const addButton = fixture.debugElement.query(By.css('#subgroupsOfGroup tbody .deleteButton'));
addButton.triggerEventHandler('click', {
preventDefault: () => {/**/
}
describe('current subgroup list', () => {
it('should show list of subgroups of current active group', () => {
const groupIdsFound = fixture.debugElement.queryAll(By.css('#subgroupsOfGroup tr td:first-child'));
expect(groupIdsFound.length).toEqual(1);
subgroups.map((group: Group) => {
expect(groupIdsFound.find((foundEl) => {
return (foundEl.nativeElement.textContent.trim() === group.uuid);
})).toBeTruthy();
});
});
it('should show a delete button next to each subgroup', () => {
const subgroupsFound = fixture.debugElement.queryAll(By.css('#subgroupsOfGroup tbody tr'));
subgroupsFound.map((foundGroupRowElement: DebugElement) => {
const addButton: DebugElement = foundGroupRowElement.query(By.css('td:last-child .fa-plus'));
const deleteButton: DebugElement = foundGroupRowElement.query(By.css('td:last-child .fa-trash-alt'));
expect(addButton).toBeNull();
expect(deleteButton).not.toBeNull();
});
});
describe('if first group delete button is pressed', () => {
let groupsFound: DebugElement[];
beforeEach(() => {
const deleteButton = fixture.debugElement.query(By.css('#subgroupsOfGroup tbody .deleteButton'));
deleteButton.nativeElement.click();
fixture.detectChanges();
});
it('then no subgroup remains as a member of the active group', () => {
groupsFound = fixture.debugElement.queryAll(By.css('#subgroupsOfGroup tbody tr'));
expect(groupsFound.length).toEqual(0);
});
tick();
fixture.detectChanges();
}));
it('one less subgroup in list from 1 to 0 (of 2 total groups)', () => {
groupsFound = fixture.debugElement.queryAll(By.css('#subgroupsOfGroup tbody tr'));
expect(groupsFound.length).toEqual(0);
});
});
@@ -169,54 +205,38 @@ describe('SubgroupsListComponent', () => {
let groupsFound: DebugElement[];
beforeEach(fakeAsync(() => {
component.search({ query: '' });
fixture.detectChanges();
groupsFound = fixture.debugElement.queryAll(By.css('#groupsSearch tbody tr'));
}));
it('should display all groups', () => {
fixture.detectChanges();
groupsFound = fixture.debugElement.queryAll(By.css('#groupsSearch tbody tr'));
expect(groupsFound.length).toEqual(2);
groupsFound = fixture.debugElement.queryAll(By.css('#groupsSearch tbody tr'));
it('should display only non-member groups (i.e. groups that are not a subgroup)', () => {
const groupIdsFound: DebugElement[] = fixture.debugElement.queryAll(By.css('#groupsSearch tbody tr td:first-child'));
allGroups.map((group: Group) => {
expect(groupIdsFound.length).toEqual(1);
groupNonMembers.map((group: Group) => {
expect(groupIdsFound.find((foundEl: DebugElement) => {
return (foundEl.nativeElement.textContent.trim() === group.uuid);
})).toBeTruthy();
});
});
describe('if group is already a subgroup', () => {
it('should have delete button, else it should have add button', () => {
it('should display an add button next to non-member groups, not a delete button', () => {
groupsFound.map((foundGroupRowElement: DebugElement) => {
const addButton: DebugElement = foundGroupRowElement.query(By.css('td:last-child .fa-plus'));
const deleteButton: DebugElement = foundGroupRowElement.query(By.css('td:last-child .fa-trash-alt'));
expect(addButton).not.toBeNull();
expect(deleteButton).toBeNull();
});
});
describe('if first add button is pressed', () => {
beforeEach(() => {
const addButton: DebugElement = fixture.debugElement.query(By.css('#groupsSearch tbody .fa-plus'));
addButton.nativeElement.click();
fixture.detectChanges();
});
it('then all (two) Groups are subgroups of the active group. No non-members left', () => {
groupsFound = fixture.debugElement.queryAll(By.css('#groupsSearch tbody tr'));
const getSubgroups = groupsDataServiceStub.getSubgroups().subgroups;
if (getSubgroups !== undefined && getSubgroups.length > 0) {
groupsFound.map((foundGroupRowElement: DebugElement) => {
const groupId: DebugElement = foundGroupRowElement.query(By.css('td:first-child'));
const addButton: DebugElement = foundGroupRowElement.query(By.css('td:last-child .fa-plus'));
const deleteButton: DebugElement = foundGroupRowElement.query(By.css('td:last-child .fa-trash-alt'));
expect(addButton).toBeNull();
if (activeGroup.id === groupId.nativeElement.textContent) {
expect(deleteButton).toBeNull();
} else {
expect(deleteButton).not.toBeNull();
}
});
} else {
const subgroupIds: string[] = activeGroup.subgroups.map((group: Group) => group.id);
groupsFound.map((foundGroupRowElement: DebugElement) => {
const groupId: DebugElement = foundGroupRowElement.query(By.css('td:first-child'));
const addButton: DebugElement = foundGroupRowElement.query(By.css('td:last-child .fa-plus'));
const deleteButton: DebugElement = foundGroupRowElement.query(By.css('td:last-child .fa-trash-alt'));
if (subgroupIds.includes(groupId.nativeElement.textContent)) {
expect(addButton).toBeNull();
expect(deleteButton).not.toBeNull();
} else {
expect(deleteButton).toBeNull();
expect(addButton).not.toBeNull();
}
});
}
expect(groupsFound.length).toEqual(0);
});
});
});

View File

@@ -2,16 +2,15 @@ import { Component, Input, OnDestroy, OnInit } from '@angular/core';
import { UntypedFormBuilder } from '@angular/forms';
import { Router } from '@angular/router';
import { TranslateService } from '@ngx-translate/core';
import { BehaviorSubject, Observable, of as observableOf, Subscription } from 'rxjs';
import { map, mergeMap, switchMap, take } from 'rxjs/operators';
import { BehaviorSubject, Observable, Subscription } from 'rxjs';
import { map, switchMap, take } from 'rxjs/operators';
import { PaginatedList } from '../../../../core/data/paginated-list.model';
import { RemoteData } from '../../../../core/data/remote-data';
import { GroupDataService } from '../../../../core/eperson/group-data.service';
import { Group } from '../../../../core/eperson/models/group.model';
import {
getFirstCompletedRemoteData,
getFirstSucceededRemoteData,
getRemoteDataPayload
getAllCompletedRemoteData,
getFirstCompletedRemoteData
} from '../../../../core/shared/operators';
import { NotificationsService } from '../../../../shared/notifications/notifications.service';
import { PaginationComponentOptions } from '../../../../shared/pagination/pagination-component-options.model';
@@ -103,6 +102,7 @@ export class SubgroupsListComponent implements OnInit, OnDestroy {
if (activeGroup != null) {
this.groupBeingEdited = activeGroup;
this.retrieveSubGroups();
this.search({query: ''});
}
}));
}
@@ -131,47 +131,6 @@ export class SubgroupsListComponent implements OnInit, OnDestroy {
}));
}
/**
* Whether or not the given group is a subgroup of the group currently being edited
* @param possibleSubgroup Group that is a possible subgroup (being tested) of the group currently being edited
*/
isSubgroupOfGroup(possibleSubgroup: Group): Observable<boolean> {
return this.groupDataService.getActiveGroup().pipe(take(1),
mergeMap((activeGroup: Group) => {
if (activeGroup != null) {
if (activeGroup.uuid === possibleSubgroup.uuid) {
return observableOf(false);
} else {
return this.groupDataService.findListByHref(activeGroup._links.subgroups.href, {
currentPage: 1,
elementsPerPage: 9999
})
.pipe(
getFirstSucceededRemoteData(),
getRemoteDataPayload(),
map((listTotalGroups: PaginatedList<Group>) => listTotalGroups.page.filter((groupInList: Group) => groupInList.id === possibleSubgroup.id)),
map((groups: Group[]) => groups.length > 0));
}
} else {
return observableOf(false);
}
}));
}
/**
* Whether or not the given group is the current group being edited
* @param group Group that is possibly the current group being edited
*/
isActiveGroup(group: Group): Observable<boolean> {
return this.groupDataService.getActiveGroup().pipe(take(1),
mergeMap((activeGroup: Group) => {
if (activeGroup != null && activeGroup.uuid === group.uuid) {
return observableOf(true);
}
return observableOf(false);
}));
}
/**
* Deletes given subgroup from the group currently being edited
* @param subgroup Group we want to delete from the subgroups of the group currently being edited
@@ -181,6 +140,11 @@ export class SubgroupsListComponent implements OnInit, OnDestroy {
if (activeGroup != null) {
const response = this.groupDataService.deleteSubGroupFromGroup(activeGroup, subgroup);
this.showNotifications('deleteSubgroup', response, this.dsoNameService.getName(subgroup), activeGroup);
// Reload search results (if there is an active query).
// This will potentially add this deleted subgroup into the list of search results.
if (this.currentSearchQuery != null) {
this.search({query: this.currentSearchQuery});
}
} else {
this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.failure.noActiveGroup'));
}
@@ -197,6 +161,11 @@ export class SubgroupsListComponent implements OnInit, OnDestroy {
if (activeGroup.uuid !== subgroup.uuid) {
const response = this.groupDataService.addSubGroupToGroup(activeGroup, subgroup);
this.showNotifications('addSubgroup', response, this.dsoNameService.getName(subgroup), activeGroup);
// Reload search results (if there is an active query).
// This will potentially remove this added subgroup from search results.
if (this.currentSearchQuery != null) {
this.search({query: this.currentSearchQuery});
}
} else {
this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.failure.subgroupToAddIsActiveGroup'));
}
@@ -207,28 +176,38 @@ export class SubgroupsListComponent implements OnInit, OnDestroy {
}
/**
* Search in the groups (searches by group name and by uuid exact match)
* Search all non-member groups (searches by group name and by uuid exact match). Used to search for
* groups that could be added to current group as a subgroup.
* @param data Contains query param
*/
search(data: any) {
const query: string = data.query;
if (query != null && this.currentSearchQuery !== query) {
this.router.navigateByUrl(this.groupDataService.getGroupEditPageRouterLink(this.groupBeingEdited));
this.currentSearchQuery = query;
this.configSearch.currentPage = 1;
}
this.searchDone = true;
this.unsubFrom(SubKey.SearchResults);
this.subs.set(SubKey.SearchResults, this.paginationService.getCurrentPagination(this.configSearch.id, this.configSearch).pipe(
switchMap((config) => this.groupDataService.searchGroups(this.currentSearchQuery, {
currentPage: config.currentPage,
elementsPerPage: config.pageSize
}, true, true, followLink('object')
))
).subscribe((rd: RemoteData<PaginatedList<Group>>) => {
this.searchResults$.next(rd);
}));
this.subs.set(SubKey.SearchResults,
this.paginationService.getCurrentPagination(this.configSearch.id, this.configSearch).pipe(
switchMap((paginationOptions) => {
const query: string = data.query;
if (query != null && this.currentSearchQuery !== query && this.groupBeingEdited) {
this.currentSearchQuery = query;
this.paginationService.resetPage(this.configSearch.id);
}
this.searchDone = true;
return this.groupDataService.searchNonMemberGroups(this.currentSearchQuery, this.groupBeingEdited.id, {
currentPage: paginationOptions.currentPage,
elementsPerPage: paginationOptions.pageSize
}, false, true, followLink('object'));
}),
getAllCompletedRemoteData(),
map((rd: RemoteData<any>) => {
if (rd.hasFailed) {
this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.failure', { cause: rd.errorMessage }));
} else {
return rd;
}
}))
.subscribe((rd: RemoteData<PaginatedList<Group>>) => {
this.searchResults$.next(rd);
}));
}
/**

View File

@@ -5,7 +5,7 @@
<h2 id="header" class="pb-2">{{messagePrefix + 'head' | translate}}</h2>
<div>
<button class="mr-auto btn btn-success"
[routerLink]="['newGroup']">
[routerLink]="'create'">
<i class="fas fa-plus"></i>
<span class="d-none d-sm-inline ml-1">{{messagePrefix + 'button.add' | translate}}</span>
</button>

View File

@@ -216,18 +216,28 @@ export class GroupsRegistryComponent implements OnInit, OnDestroy {
/**
* Get the members (epersons embedded value of a group)
* NOTE: At this time we only grab the *first* member in order to receive the `totalElements` value
* needed for our HTML template.
* @param group
*/
getMembers(group: Group): Observable<RemoteData<PaginatedList<EPerson>>> {
return this.ePersonDataService.findListByHref(group._links.epersons.href).pipe(getFirstSucceededRemoteData());
return this.ePersonDataService.findListByHref(group._links.epersons.href, {
currentPage: 1,
elementsPerPage: 1,
}).pipe(getFirstSucceededRemoteData());
}
/**
* Get the subgroups (groups embedded value of a group)
* NOTE: At this time we only grab the *first* subgroup in order to receive the `totalElements` value
* needed for our HTML template.
* @param group
*/
getSubgroups(group: Group): Observable<RemoteData<PaginatedList<Group>>> {
return this.groupService.findListByHref(group._links.subgroups.href).pipe(getFirstSucceededRemoteData());
return this.groupService.findListByHref(group._links.subgroups.href, {
currentPage: 1,
elementsPerPage: 1,
}).pipe(getFirstSucceededRemoteData());
}
/**

View File

@@ -8,9 +8,9 @@ import {
import { UntypedFormGroup } from '@angular/forms';
import { RegistryService } from '../../../../core/registry/registry.service';
import { FormBuilderService } from '../../../../shared/form/builder/form-builder.service';
import { take } from 'rxjs/operators';
import { switchMap, take, tap } from 'rxjs/operators';
import { TranslateService } from '@ngx-translate/core';
import { combineLatest } from 'rxjs';
import { Observable, combineLatest } from 'rxjs';
import { MetadataSchema } from '../../../../core/metadata/metadata-schema.model';
@Component({
@@ -147,30 +147,48 @@ export class MetadataSchemaFormComponent implements OnInit, OnDestroy {
* Emit the updated/created schema using the EventEmitter submitForm
*/
onSubmit(): void {
this.registryService.clearMetadataSchemaRequests().subscribe();
this.registryService.getActiveMetadataSchema().pipe(take(1)).subscribe(
(schema: MetadataSchema) => {
const values = {
prefix: this.name.value,
namespace: this.namespace.value
};
if (schema == null) {
this.registryService.createOrUpdateMetadataSchema(Object.assign(new MetadataSchema(), values)).subscribe((newSchema) => {
this.submitForm.emit(newSchema);
this.registryService
.getActiveMetadataSchema()
.pipe(
take(1),
switchMap((schema: MetadataSchema) => {
const metadataValues = {
prefix: this.name.value,
namespace: this.namespace.value,
};
let createOrUpdate$: Observable<MetadataSchema>;
if (schema == null) {
createOrUpdate$ =
this.registryService.createOrUpdateMetadataSchema(
Object.assign(new MetadataSchema(), metadataValues)
);
} else {
const updatedSchema = Object.assign(
new MetadataSchema(),
schema,
{
namespace: metadataValues.namespace,
}
);
createOrUpdate$ =
this.registryService.createOrUpdateMetadataSchema(
updatedSchema
);
}
return createOrUpdate$;
}),
tap(() => {
this.registryService.clearMetadataSchemaRequests().subscribe();
})
)
.subscribe((updatedOrCreatedSchema: MetadataSchema) => {
this.submitForm.emit(updatedOrCreatedSchema);
this.clearFields();
this.registryService.cancelEditMetadataSchema();
});
} else {
this.registryService.createOrUpdateMetadataSchema(Object.assign(new MetadataSchema(), schema, {
id: schema.id,
prefix: schema.prefix,
namespace: values.namespace,
})).subscribe((updatedSchema: MetadataSchema) => {
this.submitForm.emit(updatedSchema);
});
}
this.clearFields();
this.registryService.cancelEditMetadataSchema();
}
);
}
/**

View File

@@ -3,7 +3,8 @@ import {
DynamicFormControlModel,
DynamicFormGroupModel,
DynamicFormLayout,
DynamicInputModel
DynamicInputModel,
DynamicTextAreaModel
} from '@ng-dynamic-forms/core';
import { UntypedFormGroup } from '@angular/forms';
import { RegistryService } from '../../../../core/registry/registry.service';
@@ -51,7 +52,7 @@ export class MetadataFieldFormComponent implements OnInit, OnDestroy {
/**
* A dynamic input model for the scopeNote field
*/
scopeNote: DynamicInputModel;
scopeNote: DynamicTextAreaModel;
/**
* A list of all dynamic input models
@@ -132,11 +133,12 @@ export class MetadataFieldFormComponent implements OnInit, OnDestroy {
maxLength: 'error.validation.metadata.qualifier.max-length',
},
});
this.scopeNote = new DynamicInputModel({
this.scopeNote = new DynamicTextAreaModel({
id: 'scopeNote',
label: scopenote,
name: 'scopeNote',
required: false,
rows: 5,
});
this.formModel = [
new DynamicFormGroupModel(

View File

@@ -41,7 +41,7 @@
</label>
</td>
<td class="selectable-row" (click)="editField(field)">{{field.id}}</td>
<td class="selectable-row" (click)="editField(field)">{{schema?.prefix}}.{{field.element}}<label *ngIf="field.qualifier" class="mb-0">.</label>{{field.qualifier}}</td>
<td class="selectable-row" (click)="editField(field)">{{schema?.prefix}}.{{field.element}}{{field.qualifier ? '.' + field.qualifier : ''}}</td>
<td class="selectable-row" (click)="editField(field)">{{field.scopeNote}}</td>
</tr>
</tbody>

View File

@@ -12,8 +12,7 @@ import { Router } from '@angular/router';
* Represents a non-expandable section in the admin sidebar
*/
@Component({
/* eslint-disable @angular-eslint/component-selector */
selector: 'li[ds-admin-sidebar-section]',
selector: 'ds-admin-sidebar-section',
templateUrl: './admin-sidebar-section.component.html',
styleUrls: ['./admin-sidebar-section.component.scss'],

View File

@@ -26,10 +26,10 @@
</div>
</li>
<ng-container *ngFor="let section of (sections | async)">
<li *ngFor="let section of (sections | async)">
<ng-container
*ngComponentOutlet="(sectionMap$ | async).get(section.id).component; injector: (sectionMap$ | async).get(section.id).injector;"></ng-container>
</ng-container>
</li>
</ul>
</div>
<div class="navbar-nav">

View File

@@ -15,8 +15,7 @@ import { Router } from '@angular/router';
* Represents a expandable section in the sidebar
*/
@Component({
/* eslint-disable @angular-eslint/component-selector */
selector: 'li[ds-expandable-admin-sidebar-section]',
selector: 'ds-expandable-admin-sidebar-section',
templateUrl: './expandable-admin-sidebar-section.component.html',
styleUrls: ['./expandable-admin-sidebar-section.component.scss'],
animations: [rotate, slide, bgColor]

View File

@@ -1,6 +1,6 @@
<ng-container *ngVar="(breadcrumbs$ | async) as breadcrumbs">
<nav *ngIf="(showBreadcrumbs$ | async)" aria-label="breadcrumb" class="nav-breadcrumb">
<ol class="container breadcrumb">
<ol class="container breadcrumb my-0">
<ng-container
*ngTemplateOutlet="breadcrumbs?.length > 0 ? breadcrumb : activeBreadcrumb; context: {text: 'home.breadcrumbs', url: '/'}"></ng-container>
<ng-container *ngFor="let bc of breadcrumbs; let last = last;">

View File

@@ -4,9 +4,8 @@
.breadcrumb {
border-radius: 0;
margin-top: calc(-1 * var(--ds-content-spacing));
padding-bottom: var(--ds-content-spacing / 3);
padding-top: var(--ds-content-spacing / 3);
padding-bottom: calc(var(--ds-content-spacing) / 2);
padding-top: calc(var(--ds-content-spacing) / 2);
background-color: var(--ds-breadcrumb-bg);
}

View File

@@ -89,11 +89,11 @@ export class BrowseByDatePageComponent extends BrowseByMetadataPageComponent {
const lastItemRD = this.browseService.getFirstItemFor(definition, scope, SortDirection.DESC);
this.subs.push(
observableCombineLatest([firstItemRD, lastItemRD]).subscribe(([firstItem, lastItem]) => {
let lowerLimit = this.getLimit(firstItem, metadataKeys, this.appConfig.browseBy.defaultLowerLimit);
let upperLimit = this.getLimit(lastItem, metadataKeys, new Date().getUTCFullYear());
const options = [];
const oneYearBreak = Math.floor((upperLimit - this.appConfig.browseBy.oneYearLimit) / 5) * 5;
const fiveYearBreak = Math.floor((upperLimit - this.appConfig.browseBy.fiveYearLimit) / 10) * 10;
let lowerLimit: number = this.getLimit(firstItem, metadataKeys, this.appConfig.browseBy.defaultLowerLimit);
let upperLimit: number = this.getLimit(lastItem, metadataKeys, new Date().getUTCFullYear());
const options: number[] = [];
const oneYearBreak: number = Math.floor((upperLimit - this.appConfig.browseBy.oneYearLimit) / 5) * 5;
const fiveYearBreak: number = Math.floor((upperLimit - this.appConfig.browseBy.fiveYearLimit) / 10) * 10;
if (lowerLimit <= fiveYearBreak) {
lowerLimit -= 10;
} else if (lowerLimit <= oneYearBreak) {
@@ -101,7 +101,7 @@ export class BrowseByDatePageComponent extends BrowseByMetadataPageComponent {
} else {
lowerLimit -= 1;
}
let i = upperLimit;
let i: number = upperLimit;
while (i > lowerLimit) {
options.push(i);
if (i <= fiveYearBreak) {

View File

@@ -32,7 +32,7 @@
<section class="comcol-page-browse-section">
<div class="browse-by-metadata w-100">
<ds-browse-by *ngIf="startsWithOptions" class="col-xs-12 w-100"
<ds-themed-browse-by *ngIf="startsWithOptions" class="col-xs-12 w-100"
title="{{'browse.title' | translate:
{
collection: dsoNameService.getName((parent$ | async)?.payload),
@@ -48,7 +48,7 @@
[startsWithOptions]="startsWithOptions"
(prev)="goPrev()"
(next)="goNext()">
</ds-browse-by>
</ds-themed-browse-by>
<ds-themed-loading *ngIf="!startsWithOptions" message="{{'loading.browse-by-page' | translate}}"></ds-themed-loading>
</div>
</section>

View File

@@ -161,7 +161,11 @@ export class BrowseByMetadataPageComponent implements OnInit, OnDestroy {
this.value = '';
}
if (typeof params.startsWith === 'string'){
if (params.startsWith === undefined || params.startsWith === '') {
this.startsWith = undefined;
}
if (typeof params.startsWith === 'string'){
this.startsWith = params.startsWith.trim();
}

View File

@@ -14,6 +14,7 @@ import { ThemedBrowseByTaxonomyPageComponent } from './browse-by-taxonomy-page/t
import { SharedBrowseByModule } from '../shared/browse-by/shared-browse-by.module';
import { DsoPageModule } from '../shared/dso-page/dso-page.module';
import { FormModule } from '../shared/form/form.module';
import { SharedModule } from '../shared/shared.module';
const ENTRY_COMPONENTS = [
// put only entry components that use custom decorator
@@ -35,6 +36,7 @@ const ENTRY_COMPONENTS = [
ComcolModule,
DsoPageModule,
FormModule,
SharedModule,
],
declarations: [
BrowseBySwitcherComponent,

View File

@@ -98,9 +98,8 @@ export class CollectionFormComponent extends ComColFormComponent<Collection> imp
// retrieve all entity types to populate the dropdowns selection
entities$.subscribe((entityTypes: ItemType[]) => {
entityTypes
.filter((type: ItemType) => type.label !== NONE_ENTITY_TYPE)
.forEach((type: ItemType, index: number) => {
entityTypes = entityTypes.filter((type: ItemType) => type.label !== NONE_ENTITY_TYPE);
entityTypes.forEach((type: ItemType, index: number) => {
this.entityTypeSelection.add({
disabled: false,
label: type.label,
@@ -112,7 +111,7 @@ export class CollectionFormComponent extends ComColFormComponent<Collection> imp
}
});
this.formModel = [...collectionFormModels, this.entityTypeSelection];
this.formModel = entityTypes.length === 0 ? collectionFormModels : [...collectionFormModels, this.entityTypeSelection];
super.ngOnInit();
this.chd.detectChanges();

View File

@@ -34,9 +34,6 @@
</ds-comcol-page-content>
</header>
<ds-dso-edit-menu></ds-dso-edit-menu>
<div class="pl-2 space-children-mr">
<ds-dso-page-subscription-button [dso]="collection"></ds-dso-page-subscription-button>
</div>
</div>
<section class="comcol-page-browse-section">
<!-- Browse-By Links -->

View File

@@ -8,7 +8,7 @@ import { ItemTemplateDataService } from '../../core/data/item-template-data.serv
import { getCollectionEditRoute } from '../collection-page-routing-paths';
import { Item } from '../../core/shared/item.model';
import { getFirstSucceededRemoteDataPayload } from '../../core/shared/operators';
import { AlertType } from '../../shared/alert/aletr-type';
import { AlertType } from '../../shared/alert/alert-type';
import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
@Component({

View File

@@ -1,4 +1,4 @@
<div class="container">
<h2>{{ 'communityList.title' | translate }}</h2>
<h1>{{ 'communityList.title' | translate }}</h1>
<ds-themed-community-list></ds-themed-community-list>
</div>

View File

@@ -24,8 +24,9 @@ import { FlatNode } from './flat-node.model';
import { ShowMoreFlatNode } from './show-more-flat-node.model';
import { FindListOptions } from '../core/data/find-list-options.model';
import { AppConfig, APP_CONFIG } from 'src/config/app-config.interface';
import { v4 as uuidv4 } from 'uuid';
// Helper method to combine an flatten an array of observables of flatNode arrays
// Helper method to combine and flatten an array of observables of flatNode arrays
export const combineAndFlatten = (obsList: Observable<FlatNode[]>[]): Observable<FlatNode[]> =>
observableCombineLatest([...obsList]).pipe(
map((matrix: any[][]) => [].concat(...matrix)),
@@ -186,7 +187,7 @@ export class CommunityListService {
return this.transformCommunity(community, level, parent, expandedNodes);
});
if (currentPage < listOfPaginatedCommunities.totalPages && currentPage === listOfPaginatedCommunities.currentPage) {
obsList = [...obsList, observableOf([showMoreFlatNode('community', level, parent)])];
obsList = [...obsList, observableOf([showMoreFlatNode(`community-${uuidv4()}`, level, parent)])];
}
return combineAndFlatten(obsList);
@@ -199,7 +200,7 @@ export class CommunityListService {
* Transforms a community in a list of FlatNodes containing firstly a flatnode of the community itself,
* followed by flatNodes of its possible subcommunities and collection
* It gets called recursively for each subcommunity to add its subcommunities and collections to the list
* Number of subcommunities and collections added, is dependant on the current page the parent is at for respectively subcommunities and collections.
* Number of subcommunities and collections added, is dependent on the current page the parent is at for respectively subcommunities and collections.
* @param community Community being transformed
* @param level Depth of the community in the list, subcommunities and collections go one level deeper
* @param parent Flatnode of the parent community
@@ -257,7 +258,7 @@ export class CommunityListService {
let nodes = rd.payload.page
.map((collection: Collection) => toFlatNode(collection, observableOf(false), level + 1, false, communityFlatNode));
if (currentCollectionPage < rd.payload.totalPages && currentCollectionPage === rd.payload.currentPage) {
nodes = [...nodes, showMoreFlatNode('collection', level + 1, communityFlatNode)];
nodes = [...nodes, showMoreFlatNode(`collection-${uuidv4()}`, level + 1, communityFlatNode)];
}
return nodes;
} else {
@@ -275,7 +276,7 @@ export class CommunityListService {
/**
* Checks if a community has subcommunities or collections by querying the respective services with a pageSize = 0
* Returns an observable that combines the result.payload.totalElements fo the observables that the
* Returns an observable that combines the result.payload.totalElements of the observables that the
* respective services return when queried
* @param community Community being checked whether it is expandable (if it has subcommunities or collections)
*/

View File

@@ -1,5 +1,5 @@
<ds-themed-loading *ngIf="(dataSource.loading$ | async) && !loadingNode" class="ds-themed-loading"></ds-themed-loading>
<cdk-tree [dataSource]="dataSource" [treeControl]="treeControl">
<cdk-tree [dataSource]="dataSource" [treeControl]="treeControl" [trackBy]="trackBy">
<!-- This is the tree node template for show more node -->
<cdk-tree-node *cdkTreeNodeDef="let node; when: isShowMore" cdkTreeNodePadding
class="example-tree-node show-more-node">
@@ -8,7 +8,7 @@
<span class="fa fa-chevron-right invisible" aria-hidden="true"></span>
</button>
<div class="align-middle pt-2">
<button *ngIf="node!==loadingNode" (click)="getNextPage(node)"
<button *ngIf="!(dataSource.loading$ | async)" (click)="getNextPage(node)"
class="btn btn-outline-primary btn-sm" role="button">
<i class="fas fa-angle-down"></i> {{ 'communityList.showMore' | translate }}
</button>
@@ -34,13 +34,13 @@
aria-hidden="true"></span>
</button>
<div class="d-flex flex-row">
<h5 class="align-middle pt-2">
<span class="align-middle pt-2 lead">
<a [routerLink]="node.route" class="lead">
{{ dsoNameService.getName(node.payload) }}
</a>
<span class="pr-2">&nbsp;</span>
<span *ngIf="node.payload.archivedItemsCount >= 0" class="badge badge-pill badge-secondary align-top archived-items-lead">{{node.payload.archivedItemsCount}}</span>
</h5>
</span>
</div>
</div>
<ds-truncatable [id]="node.id">

View File

@@ -17,6 +17,7 @@ import { By } from '@angular/platform-browser';
import { isEmpty, isNotEmpty } from '../../shared/empty.util';
import { FlatNode } from '../flat-node.model';
import { RouterLinkWithHref } from '@angular/router';
import { v4 as uuidv4 } from 'uuid';
describe('CommunityListComponent', () => {
let component: CommunityListComponent;
@@ -138,7 +139,7 @@ describe('CommunityListComponent', () => {
}
if (expandedNodes === null || isEmpty(expandedNodes)) {
if (showMoreTopComNode) {
return observableOf([...mockTopFlatnodesUnexpanded.slice(0, endPageIndex), showMoreFlatNode('community', 0, null)]);
return observableOf([...mockTopFlatnodesUnexpanded.slice(0, endPageIndex), showMoreFlatNode(`community-${uuidv4()}`, 0, null)]);
} else {
return observableOf(mockTopFlatnodesUnexpanded.slice(0, endPageIndex));
}
@@ -165,21 +166,21 @@ describe('CommunityListComponent', () => {
const endSubComIndex = this.pageSize * expandedParent.currentCommunityPage;
flatnodes = [...flatnodes, ...subComFlatnodes.slice(0, endSubComIndex)];
if (subComFlatnodes.length > endSubComIndex) {
flatnodes = [...flatnodes, showMoreFlatNode('community', topNode.level + 1, expandedParent)];
flatnodes = [...flatnodes, showMoreFlatNode(`community-${uuidv4()}`, topNode.level + 1, expandedParent)];
}
}
if (isNotEmpty(collFlatnodes)) {
const endColIndex = this.pageSize * expandedParent.currentCollectionPage;
flatnodes = [...flatnodes, ...collFlatnodes.slice(0, endColIndex)];
if (collFlatnodes.length > endColIndex) {
flatnodes = [...flatnodes, showMoreFlatNode('collection', topNode.level + 1, expandedParent)];
flatnodes = [...flatnodes, showMoreFlatNode(`collection-${uuidv4()}`, topNode.level + 1, expandedParent)];
}
}
}
}
});
if (showMoreTopComNode) {
flatnodes = [...flatnodes, showMoreFlatNode('community', 0, null)];
flatnodes = [...flatnodes, showMoreFlatNode(`community-${uuidv4()}`, 0, null)];
}
return observableOf(flatnodes);
}

View File

@@ -28,10 +28,9 @@ export class CommunityListComponent implements OnInit, OnDestroy {
treeControl = new FlatTreeControl<FlatNode>(
(node: FlatNode) => node.level, (node: FlatNode) => true
);
dataSource: CommunityListDatasource;
paginationConfig: FindListOptions;
trackBy = (index, node: FlatNode) => node.id;
constructor(
protected communityListService: CommunityListService,
@@ -58,24 +57,34 @@ export class CommunityListComponent implements OnInit, OnDestroy {
this.communityListService.saveCommunityListStateToStore(this.expandedNodes, this.loadingNode);
}
// whether or not this node has children (subcommunities or collections)
/**
* Whether this node has children (subcommunities or collections)
* @param _
* @param node
*/
hasChild(_: number, node: FlatNode) {
return node.isExpandable$;
}
// whether or not it is a show more node (contains no data, but is indication that there are more topcoms, subcoms or collections
/**
* Whether this is a show more node that contains no data, but indicates that there is
* one or more community or collection.
* @param _
* @param node
*/
isShowMore(_: number, node: FlatNode) {
return node.isShowMoreNode;
}
/**
* Toggles the expanded variable of a node, adds it to the expanded nodes list and reloads the tree so this node is expanded
* Toggles the expanded variable of a node, adds it to the expanded nodes list and reloads the tree
* so this node is expanded
* @param node Node we want to expand
*/
toggleExpanded(node: FlatNode) {
this.loadingNode = node;
if (node.isExpanded) {
this.expandedNodes = this.expandedNodes.filter((node2) => node2.name !== node.name);
this.expandedNodes = this.expandedNodes.filter((node2) => node2.id !== node.id);
node.isExpanded = false;
} else {
this.expandedNodes.push(node);
@@ -92,26 +101,28 @@ export class CommunityListComponent implements OnInit, OnDestroy {
/**
* Makes sure the next page of a node is added to the tree (top community, sub community of collection)
* > Finds its parent (if not top community) and increases its corresponding collection/subcommunity currentPage
* > Reloads tree with new page added to corresponding top community lis, sub community list or collection list
* @param node The show more node indicating whether it's an increase in top communities, sub communities or collections
* > Finds its parent (if not top community) and increases its corresponding collection/subcommunity
* currentPage
* > Reloads tree with new page added to corresponding top community lis, sub community list or
* collection list
* @param node The show more node indicating whether it's an increase in top communities, sub communities
* or collections
*/
getNextPage(node: FlatNode): void {
this.loadingNode = node;
if (node.parent != null) {
if (node.id === 'collection') {
if (node.id.startsWith('collection')) {
const parentNodeInExpandedNodes = this.expandedNodes.find((node2: FlatNode) => node.parent.id === node2.id);
parentNodeInExpandedNodes.currentCollectionPage++;
}
if (node.id === 'community') {
if (node.id.startsWith('community')) {
const parentNodeInExpandedNodes = this.expandedNodes.find((node2: FlatNode) => node.parent.id === node2.id);
parentNodeInExpandedNodes.currentCommunityPage++;
}
this.dataSource.loadCommunities(this.paginationConfig, this.expandedNodes);
} else {
this.paginationConfig.currentPage++;
this.dataSource.loadCommunities(this.paginationConfig, this.expandedNodes);
}
this.dataSource.loadCommunities(this.paginationConfig, this.expandedNodes);
}
}

View File

@@ -1,6 +1,6 @@
/**
* The show more links in the community tree are also represented by a flatNode so we know where in
* the tree it should be rendered an who its parent is (needed for the action resulting in clicking this link)
* the tree it should be rendered and who its parent is (needed for the action resulting in clicking this link)
*/
export class ShowMoreFlatNode {
}

View File

@@ -21,9 +21,6 @@
</ds-comcol-page-content>
</header>
<ds-dso-edit-menu></ds-dso-edit-menu>
<div class="pl-2 space-children-mr">
<ds-dso-page-subscription-button [dso]="communityPayload"></ds-dso-page-subscription-button>
</div>
</div>
<section class="comcol-page-browse-section">

View File

@@ -1,7 +1,7 @@
import { Component, Input, OnDestroy, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { BehaviorSubject, combineLatest as observableCombineLatest } from 'rxjs';
import { BehaviorSubject, combineLatest as observableCombineLatest, Subscription } from 'rxjs';
import { RemoteData } from '../../core/data/remote-data';
import { Collection } from '../../core/shared/collection.model';
@@ -50,6 +50,8 @@ export class CommunityPageSubCollectionListComponent implements OnInit, OnDestro
*/
subCollectionsRDObs: BehaviorSubject<RemoteData<PaginatedList<Collection>>> = new BehaviorSubject<RemoteData<PaginatedList<Collection>>>({} as any);
subscriptions: Subscription[] = [];
constructor(
protected cds: CollectionDataService,
protected paginationService: PaginationService,
@@ -77,7 +79,7 @@ export class CommunityPageSubCollectionListComponent implements OnInit, OnDestro
const pagination$ = this.paginationService.getCurrentPagination(this.config.id, this.config);
const sort$ = this.paginationService.getCurrentSort(this.config.id, this.sortConfig);
observableCombineLatest([pagination$, sort$]).pipe(
this.subscriptions.push(observableCombineLatest([pagination$, sort$]).pipe(
switchMap(([currentPagination, currentSort]) => {
return this.cds.findByParent(this.community.id, {
currentPage: currentPagination.currentPage,
@@ -87,11 +89,12 @@ export class CommunityPageSubCollectionListComponent implements OnInit, OnDestro
})
).subscribe((results) => {
this.subCollectionsRDObs.next(results);
});
}));
}
ngOnDestroy(): void {
this.paginationService.clearPagination(this.config.id);
this.paginationService.clearPagination(this.config?.id);
this.subscriptions.map((subscription: Subscription) => subscription.unsubscribe());
}
}

View File

@@ -1,7 +1,7 @@
import { Component, Input, OnDestroy, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { BehaviorSubject, combineLatest as observableCombineLatest } from 'rxjs';
import { BehaviorSubject, combineLatest as observableCombineLatest, Subscription } from 'rxjs';
import { RemoteData } from '../../core/data/remote-data';
import { Community } from '../../core/shared/community.model';
@@ -52,6 +52,8 @@ export class CommunityPageSubCommunityListComponent implements OnInit, OnDestroy
*/
subCommunitiesRDObs: BehaviorSubject<RemoteData<PaginatedList<Community>>> = new BehaviorSubject<RemoteData<PaginatedList<Community>>>({} as any);
subscriptions: Subscription[] = [];
constructor(
protected cds: CommunityDataService,
protected paginationService: PaginationService,
@@ -79,7 +81,7 @@ export class CommunityPageSubCommunityListComponent implements OnInit, OnDestroy
const pagination$ = this.paginationService.getCurrentPagination(this.config.id, this.config);
const sort$ = this.paginationService.getCurrentSort(this.config.id, this.sortConfig);
observableCombineLatest([pagination$, sort$]).pipe(
this.subscriptions.push(observableCombineLatest([pagination$, sort$]).pipe(
switchMap(([currentPagination, currentSort]) => {
return this.cds.findByParent(this.community.id, {
currentPage: currentPagination.currentPage,
@@ -89,11 +91,12 @@ export class CommunityPageSubCommunityListComponent implements OnInit, OnDestroy
})
).subscribe((results) => {
this.subCommunitiesRDObs.next(results);
});
}));
}
ngOnDestroy(): void {
this.paginationService.clearPagination(this.config.id);
this.paginationService.clearPagination(this.config?.id);
this.subscriptions.map((subscription: Subscription) => subscription.unsubscribe());
}
}

View File

@@ -152,12 +152,12 @@ export class AuthInterceptor implements HttpInterceptor {
let authMethodModel: AuthMethod;
if (splittedRealm.length === 1) {
authMethodModel = new AuthMethod(methodName);
authMethodModel = new AuthMethod(methodName, Number(j));
authMethodModels.push(authMethodModel);
} else if (splittedRealm.length > 1) {
let location = splittedRealm[1];
location = this.parseLocation(location);
authMethodModel = new AuthMethod(methodName, location);
authMethodModel = new AuthMethod(methodName, Number(j), location);
authMethodModels.push(authMethodModel);
}
}
@@ -165,7 +165,7 @@ export class AuthInterceptor implements HttpInterceptor {
// make sure the email + password login component gets rendered first
authMethodModels = this.sortAuthMethods(authMethodModels);
} else {
authMethodModels.push(new AuthMethod(AuthMethodType.Password));
authMethodModels.push(new AuthMethod(AuthMethodType.Password, 0));
}
return authMethodModels;

View File

@@ -598,9 +598,9 @@ describe('authReducer', () => {
authMethods: [],
idle: false
};
const authMethods = [
new AuthMethod(AuthMethodType.Password),
new AuthMethod(AuthMethodType.Shibboleth, 'location')
const authMethods: AuthMethod[] = [
new AuthMethod(AuthMethodType.Password, 0),
new AuthMethod(AuthMethodType.Shibboleth, 1, 'location'),
];
const action = new RetrieveAuthMethodsSuccessAction(authMethods);
const newState = authReducer(initialState, action);
@@ -632,7 +632,7 @@ describe('authReducer', () => {
loaded: false,
blocking: false,
loading: false,
authMethods: [new AuthMethod(AuthMethodType.Password)],
authMethods: [new AuthMethod(AuthMethodType.Password, 0)],
idle: false
};
expect(newState).toEqual(state);

View File

@@ -236,7 +236,7 @@ export function authReducer(state: any = initialState, action: AuthActions): Aut
return Object.assign({}, state, {
loading: false,
blocking: false,
authMethods: [new AuthMethod(AuthMethodType.Password)]
authMethods: [new AuthMethod(AuthMethodType.Password, 0)]
});
case AuthActionTypes.SET_REDIRECT_URL:

View File

@@ -2,11 +2,12 @@ import { AuthMethodType } from './auth.method-type';
export class AuthMethod {
authMethodType: AuthMethodType;
position: number;
location?: string;
// isStandalonePage? = true;
constructor(authMethodName: string, position: number, location?: string) {
this.position = position;
constructor(authMethodName: string, location?: string) {
switch (authMethodName) {
case 'ip': {
this.authMethodType = AuthMethodType.Ip;

View File

@@ -7,6 +7,7 @@ import { ServerSyncBufferEffects } from './cache/server-sync-buffer.effects';
import { ObjectUpdatesEffects } from './data/object-updates/object-updates.effects';
import { RouteEffects } from './services/route.effects';
import { RouterEffects } from './router/router.effects';
import { MenuEffects } from '../shared/menu/menu.effects';
export const coreEffects = [
RequestEffects,
@@ -18,4 +19,5 @@ export const coreEffects = [
ObjectUpdatesEffects,
RouteEffects,
RouterEffects,
MenuEffects,
];

View File

@@ -1,4 +1,3 @@
/* eslint-disable max-classes-per-file */
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';

View File

@@ -11,6 +11,8 @@ import { createSuccessfulRemoteDataObject } from '../../shared/remote-data.utils
import { Item } from '../shared/item.model';
import { EMBED_SEPARATOR } from './base/base-data.service';
import { HardRedirectService } from '../services/hard-redirect.service';
import { environment } from '../../../environments/environment.test';
import { AppConfig } from '../../../config/app-config.interface';
describe('DsoRedirectService', () => {
let scheduler: TestScheduler;
@@ -56,6 +58,7 @@ describe('DsoRedirectService', () => {
});
service = new DsoRedirectService(
environment as AppConfig,
requestService,
rdbService,
objectCache,
@@ -107,7 +110,7 @@ describe('DsoRedirectService', () => {
redir.subscribe();
scheduler.schedule(() => redir);
scheduler.flush();
expect(redirectService.redirect).toHaveBeenCalledWith('/items/' + remoteData.payload.uuid, 301);
expect(redirectService.redirect).toHaveBeenCalledWith(`${environment.ui.nameSpace}/items/${remoteData.payload.uuid}`, 301);
});
it('should navigate to entities route with the corresponding entity type', () => {
remoteData.payload.type = 'item';
@@ -124,7 +127,7 @@ describe('DsoRedirectService', () => {
redir.subscribe();
scheduler.schedule(() => redir);
scheduler.flush();
expect(redirectService.redirect).toHaveBeenCalledWith('/entities/publication/' + remoteData.payload.uuid, 301);
expect(redirectService.redirect).toHaveBeenCalledWith(`${environment.ui.nameSpace}/entities/publication/${remoteData.payload.uuid}`, 301);
});
it('should navigate to collections route', () => {
@@ -133,7 +136,7 @@ describe('DsoRedirectService', () => {
redir.subscribe();
scheduler.schedule(() => redir);
scheduler.flush();
expect(redirectService.redirect).toHaveBeenCalledWith('/collections/' + remoteData.payload.uuid, 301);
expect(redirectService.redirect).toHaveBeenCalledWith(`${environment.ui.nameSpace}/collections/${remoteData.payload.uuid}`, 301);
});
it('should navigate to communities route', () => {
@@ -142,7 +145,7 @@ describe('DsoRedirectService', () => {
redir.subscribe();
scheduler.schedule(() => redir);
scheduler.flush();
expect(redirectService.redirect).toHaveBeenCalledWith('/communities/' + remoteData.payload.uuid, 301);
expect(redirectService.redirect).toHaveBeenCalledWith(`${environment.ui.nameSpace}/communities/${remoteData.payload.uuid}`, 301);
});
});

View File

@@ -6,7 +6,7 @@
* http://www.dspace.org/license/
*/
/* eslint-disable max-classes-per-file */
import { Injectable } from '@angular/core';
import { Injectable, Inject } from '@angular/core';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
import { hasValue } from '../../shared/empty.util';
@@ -21,6 +21,7 @@ import { DSpaceObject } from '../shared/dspace-object.model';
import { IdentifiableDataService } from './base/identifiable-data.service';
import { getDSORoute } from '../../app-routing-paths';
import { HardRedirectService } from '../services/hard-redirect.service';
import { APP_CONFIG, AppConfig } from '../../../config/app-config.interface';
const ID_ENDPOINT = 'pid';
const UUID_ENDPOINT = 'dso';
@@ -70,6 +71,7 @@ export class DsoRedirectService {
private dataService: DsoByIdOrUUIDDataService;
constructor(
@Inject(APP_CONFIG) protected appConfig: AppConfig,
protected requestService: RequestService,
protected rdbService: RemoteDataBuildService,
protected objectCache: ObjectCacheService,
@@ -98,7 +100,7 @@ export class DsoRedirectService {
let newRoute = getDSORoute(dso);
if (hasValue(newRoute)) {
// Use a "301 Moved Permanently" redirect for SEO purposes
this.hardRedirectService.redirect(newRoute, 301);
this.hardRedirectService.redirect(this.appConfig.ui.nameSpace.replace(/\/$/, '') + newRoute, 301);
}
}
}

View File

@@ -74,7 +74,7 @@ export class AuthorizationDataService extends BaseDataService<Authorization> imp
return [];
}
}),
catchError(() => observableOf(false)),
catchError(() => observableOf([])),
oneAuthorizationMatchesFeature(featureId)
);
}

View File

@@ -68,13 +68,13 @@ export const oneAuthorizationMatchesFeature = (featureID: FeatureID) =>
source.pipe(
switchMap((authorizations: Authorization[]) => {
if (isNotEmpty(authorizations)) {
return observableCombineLatest(
return observableCombineLatest([
...authorizations
.filter((authorization: Authorization) => hasValue(authorization.feature))
.map((authorization: Authorization) => authorization.feature.pipe(
getFirstSucceededRemoteDataPayload()
))
);
]);
} else {
return observableOf([]);
}

View File

@@ -1,6 +1,6 @@
import { Store, StoreModule } from '@ngrx/store';
import { cold, getTestScheduler } from 'jasmine-marbles';
import { EMPTY, of as observableOf } from 'rxjs';
import { EMPTY, Observable, of as observableOf } from 'rxjs';
import { TestScheduler } from 'rxjs/testing';
import { getMockObjectCacheService } from '../../shared/mocks/object-cache.service.mock';
@@ -638,4 +638,87 @@ describe('RequestService', () => {
expect(done$).toBeObservable(cold('-----(t|)', { t: true }));
}));
});
describe('setStaleByHref', () => {
const uuid = 'c574a42c-4818-47ac-bbe1-6c3cd622c81f';
const href = 'https://rest.api/some/object';
const freshRE: any = {
request: { uuid, href },
state: RequestEntryState.Success
};
const staleRE: any = {
request: { uuid, href },
state: RequestEntryState.SuccessStale
};
it(`should call getByHref to retrieve the RequestEntry matching the href`, () => {
spyOn(service, 'getByHref').and.returnValue(observableOf(staleRE));
service.setStaleByHref(href);
expect(service.getByHref).toHaveBeenCalledWith(href);
});
it(`should dispatch a RequestStaleAction for the RequestEntry returned by getByHref`, (done: DoneFn) => {
spyOn(service, 'getByHref').and.returnValue(observableOf(staleRE));
spyOn(store, 'dispatch');
service.setStaleByHref(href).subscribe(() => {
const requestStaleAction = new RequestStaleAction(uuid);
requestStaleAction.lastUpdated = jasmine.any(Number) as any;
expect(store.dispatch).toHaveBeenCalledWith(requestStaleAction);
done();
});
});
it(`should emit true when the request in the store is stale`, () => {
spyOn(service, 'getByHref').and.returnValue(cold('a-b', {
a: freshRE,
b: staleRE
}));
const result$ = service.setStaleByHref(href);
expect(result$).toBeObservable(cold('--(c|)', { c: true }));
});
});
describe('setStaleByHrefSubstring', () => {
let dispatchSpy: jasmine.Spy;
let getByUUIDSpy: jasmine.Spy;
beforeEach(() => {
dispatchSpy = spyOn(store, 'dispatch');
getByUUIDSpy = spyOn(service, 'getByUUID').and.callThrough();
});
describe('with an empty/no matching requests in the state', () => {
it('should return true', () => {
const done$: Observable<boolean> = service.setStaleByHrefSubstring('https://rest.api/endpoint/selfLink');
expect(done$).toBeObservable(cold('(a|)', { a: true }));
});
});
describe('with a matching request in the state', () => {
beforeEach(() => {
const state = Object.assign({}, initialState, {
core: Object.assign({}, initialState.core, {
'index': {
'get-request/href-to-uuid': {
'https://rest.api/endpoint/selfLink': '5f2a0d2a-effa-4d54-bd54-5663b960f9eb'
}
}
})
});
mockStore.setState(state);
});
it('should return an Observable that emits true as soon as the request is stale', () => {
dispatchSpy.and.callFake(() => { /* empty */ }); // don't actually set as stale
getByUUIDSpy.and.returnValue(cold('a-b--c--d-', { // but fake the state in the cache
a: { state: RequestEntryState.ResponsePending },
b: { state: RequestEntryState.Success },
c: { state: RequestEntryState.SuccessStale },
d: { state: RequestEntryState.Error },
}));
const done$: Observable<boolean> = service.setStaleByHrefSubstring('https://rest.api/endpoint/selfLink');
expect(done$).toBeObservable(cold('-----(a|)', { a: true }));
});
});
});
});

View File

@@ -2,8 +2,8 @@ import { Injectable } from '@angular/core';
import { HttpHeaders } from '@angular/common/http';
import { createSelector, MemoizedSelector, select, Store } from '@ngrx/store';
import { Observable } from 'rxjs';
import { filter, map, take, tap } from 'rxjs/operators';
import { Observable, from as observableFrom } from 'rxjs';
import { filter, find, map, mergeMap, switchMap, take, tap, toArray } from 'rxjs/operators';
import cloneDeep from 'lodash/cloneDeep';
import { hasValue, isEmpty, isNotEmpty, hasNoValue } from '../../shared/empty.util';
import { ObjectCacheEntry } from '../cache/object-cache.reducer';
@@ -16,7 +16,7 @@ import {
RequestExecuteAction,
RequestStaleAction
} from './request.actions';
import { GetRequest} from './request.models';
import { GetRequest } from './request.models';
import { CommitSSBAction } from '../cache/server-sync-buffer.actions';
import { RestRequestMethod } from './rest-request-method';
import { coreSelector } from '../core.selectors';
@@ -300,22 +300,42 @@ export class RequestService {
* Set all requests that match (part of) the href to stale
*
* @param href A substring of the request(s) href
* @return Returns an observable emitting whether or not the cache is removed
* @return Returns an observable emitting when those requests are all stale
*/
setStaleByHrefSubstring(href: string): Observable<boolean> {
this.store.pipe(
const requestUUIDs$ = this.store.pipe(
select(uuidsFromHrefSubstringSelector(requestIndexSelector, href)),
take(1)
).subscribe((uuids: string[]) => {
);
requestUUIDs$.subscribe((uuids: string[]) => {
for (const uuid of uuids) {
this.store.dispatch(new RequestStaleAction(uuid));
}
});
this.requestsOnTheirWayToTheStore = this.requestsOnTheirWayToTheStore.filter((reqHref: string) => reqHref.indexOf(href) < 0);
return this.store.pipe(
select(uuidsFromHrefSubstringSelector(requestIndexSelector, href)),
map((uuids) => isEmpty(uuids))
// emit true after all requests are stale
return requestUUIDs$.pipe(
switchMap((uuids: string[]) => {
if (isEmpty(uuids)) {
// if there were no matching requests, emit true immediately
return [true];
} else {
// otherwise emit all request uuids in order
return observableFrom(uuids).pipe(
// retrieve the RequestEntry for each uuid
mergeMap((uuid: string) => this.getByUUID(uuid)),
// check whether it is undefined or stale
map((request: RequestEntry) => hasNoValue(request) || isStale(request.state)),
// if it is, complete
find((stale: boolean) => stale === true),
// after all observables above are completed, emit them as a single array
toArray(),
// when the array comes in, emit true
map(() => true)
);
}
})
);
}
@@ -331,7 +351,29 @@ export class RequestService {
map((request: RequestEntry) => isStale(request.state)),
filter((stale: boolean) => stale),
take(1),
);
);
}
/**
* Mark a request as stale
* @param href the href of the request
* @return an Observable that will emit true once the Request becomes stale
*/
setStaleByHref(href: string): Observable<boolean> {
const requestEntry$ = this.getByHref(href);
requestEntry$.pipe(
map((re: RequestEntry) => re.request.uuid),
take(1),
).subscribe((uuid: string) => {
this.store.dispatch(new RequestStaleAction(uuid));
});
return requestEntry$.pipe(
map((request: RequestEntry) => isStale(request.state)),
filter((stale: boolean) => stale),
take(1)
);
}
/**
@@ -344,10 +386,10 @@ export class RequestService {
// if it's not a GET request
if (request.method !== RestRequestMethod.GET) {
return true;
// if it is a GET request, check it isn't pending
// if it is a GET request, check it isn't pending
} else if (this.isPending(request)) {
return false;
// if it is pending, check if we're allowed to use a cached version
// if it is pending, check if we're allowed to use a cached version
} else if (!useCachedVersionIfAvailable) {
return true;
} else {

View File

@@ -1,16 +1,18 @@
import { RootDataService } from './root-data.service';
import { HALEndpointService } from '../shared/hal-endpoint.service';
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
import { Observable, of } from 'rxjs';
import {
createSuccessfulRemoteDataObject$,
createFailedRemoteDataObject$
} from '../../shared/remote-data.utils';
import { Observable } from 'rxjs';
import { RemoteData } from './remote-data';
import { Root } from './root.model';
import { RawRestResponse } from '../dspace-rest/raw-rest-response.model';
import { cold } from 'jasmine-marbles';
describe('RootDataService', () => {
let service: RootDataService;
let halService: HALEndpointService;
let restService;
let requestService;
let rootEndpoint;
let findByHrefSpy;
@@ -19,10 +21,10 @@ describe('RootDataService', () => {
halService = jasmine.createSpyObj('halService', {
getRootHref: rootEndpoint,
});
restService = jasmine.createSpyObj('halService', {
get: jasmine.createSpy('get'),
});
service = new RootDataService(null, null, null, halService, restService);
requestService = jasmine.createSpyObj('requestService', [
'setStaleByHref',
]);
service = new RootDataService(requestService, null, null, halService);
findByHrefSpy = spyOn(service as any, 'findByHref');
findByHrefSpy.and.returnValue(createSuccessfulRemoteDataObject$({}));
@@ -47,12 +49,8 @@ describe('RootDataService', () => {
let result$: Observable<boolean>;
it('should return observable of true when root endpoint is available', () => {
const mockResponse = {
statusCode: 200,
statusText: 'OK'
} as RawRestResponse;
spyOn(service, 'findRoot').and.returnValue(createSuccessfulRemoteDataObject$<Root>({} as any));
restService.get.and.returnValue(of(mockResponse));
result$ = service.checkServerAvailability();
expect(result$).toBeObservable(cold('(a|)', {
@@ -61,12 +59,8 @@ describe('RootDataService', () => {
});
it('should return observable of false when root endpoint is not available', () => {
const mockResponse = {
statusCode: 500,
statusText: 'Internal Server Error'
} as RawRestResponse;
spyOn(service, 'findRoot').and.returnValue(createFailedRemoteDataObject$<Root>('500'));
restService.get.and.returnValue(of(mockResponse));
result$ = service.checkServerAvailability();
expect(result$).toBeObservable(cold('(a|)', {
@@ -75,4 +69,12 @@ describe('RootDataService', () => {
});
});
describe(`invalidateRootCache`, () => {
it(`should set the cached root request to stale`, () => {
service.invalidateRootCache();
expect(halService.getRootHref).toHaveBeenCalled();
expect(requestService.setStaleByHref).toHaveBeenCalledWith(rootEndpoint);
});
});
});

View File

@@ -7,12 +7,11 @@ import { HALEndpointService } from '../shared/hal-endpoint.service';
import { Observable, of as observableOf } from 'rxjs';
import { RemoteData } from './remote-data';
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
import { DspaceRestService } from '../dspace-rest/dspace-rest.service';
import { RawRestResponse } from '../dspace-rest/raw-rest-response.model';
import { catchError, map } from 'rxjs/operators';
import { BaseDataService } from './base/base-data.service';
import { ObjectCacheService } from '../cache/object-cache.service';
import { dataService } from './base/data-service.decorator';
import { getFirstCompletedRemoteData } from '../shared/operators';
/**
* A service to retrieve the {@link Root} object from the REST API.
@@ -25,7 +24,6 @@ export class RootDataService extends BaseDataService<Root> {
protected rdbService: RemoteDataBuildService,
protected objectCache: ObjectCacheService,
protected halService: HALEndpointService,
protected restService: DspaceRestService,
) {
super('', requestService, rdbService, objectCache, halService, 6 * 60 * 60 * 1000);
}
@@ -34,12 +32,13 @@ export class RootDataService extends BaseDataService<Root> {
* Check if root endpoint is available
*/
checkServerAvailability(): Observable<boolean> {
return this.restService.get(this.halService.getRootHref()).pipe(
return this.findRoot().pipe(
catchError((err ) => {
console.error(err);
return observableOf(false);
}),
map((res: RawRestResponse) => res.statusCode === 200)
getFirstCompletedRemoteData(),
map((rootRd: RemoteData<Root>) => rootRd.statusCode === 200)
);
}
@@ -60,6 +59,6 @@ export class RootDataService extends BaseDataService<Root> {
* Set to sale the root endpoint cache hit
*/
invalidateRootCache() {
this.requestService.setStaleByHrefSubstring(this.halService.getRootHref());
this.requestService.setStaleByHref(this.halService.getRootHref());
}
}

View File

@@ -11,6 +11,7 @@ import {
EPeopleRegistryCancelEPersonAction,
EPeopleRegistryEditEPersonAction
} from '../../access-control/epeople-registry/epeople-registry.actions';
import { GroupMock } from '../../shared/testing/group-mock';
import { RequestParam } from '../cache/models/request-param.model';
import { ChangeAnalyzer } from '../data/change-analyzer';
import { PatchRequest, PostRequest } from '../data/request.models';
@@ -140,6 +141,30 @@ describe('EPersonDataService', () => {
});
});
describe('searchNonMembers', () => {
beforeEach(() => {
spyOn(service, 'searchBy');
});
it('search with empty query and a group ID', () => {
service.searchNonMembers('', GroupMock.id);
const options = Object.assign(new FindListOptions(), {
searchParams: [Object.assign(new RequestParam('query', '')),
Object.assign(new RequestParam('group', GroupMock.id))]
});
expect(service.searchBy).toHaveBeenCalledWith('isNotMemberOf', options, true, true);
});
it('search with query and a group ID', () => {
service.searchNonMembers('test', GroupMock.id);
const options = Object.assign(new FindListOptions(), {
searchParams: [Object.assign(new RequestParam('query', 'test')),
Object.assign(new RequestParam('group', GroupMock.id))]
});
expect(service.searchBy).toHaveBeenCalledWith('isNotMemberOf', options, true, true);
});
});
describe('updateEPerson', () => {
beforeEach(() => {
spyOn(service, 'findByHref').and.returnValue(createSuccessfulRemoteDataObject$(EPersonMock));

View File

@@ -34,6 +34,7 @@ import { PatchData, PatchDataImpl } from '../data/base/patch-data';
import { DeleteData, DeleteDataImpl } from '../data/base/delete-data';
import { RestRequestMethod } from '../data/rest-request-method';
import { dataService } from '../data/base/data-service.decorator';
import { getEPersonEditRoute, getEPersonsRoute } from '../../access-control/access-control-routing-paths';
const ePeopleRegistryStateSelector = (state: AppState) => state.epeopleRegistry;
const editEPersonSelector = createSelector(ePeopleRegistryStateSelector, (ePeopleRegistryState: EPeopleRegistryState) => ePeopleRegistryState.editEPerson);
@@ -176,6 +177,34 @@ export class EPersonDataService extends IdentifiableDataService<EPerson> impleme
return this.searchBy(searchMethod, findListOptions, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
}
/**
* Searches for all EPerons which are *not* a member of a given group, via a passed in query
* (searches all EPerson metadata and by exact UUID).
* Endpoint used: /eperson/epesons/search/isNotMemberOf?query=<:string>&group=<:uuid>
* @param query search query param
* @param group UUID of group to exclude results from. Members of this group will never be returned.
* @param options
* @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's
* no valid cached version. Defaults to true
* @param reRequestOnStale Whether or not the request should automatically be re-
* requested after the response becomes stale
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which
* {@link HALLink}s should be automatically resolved
*/
public searchNonMembers(query: string, group: string, options?: FindListOptions, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<EPerson>[]): Observable<RemoteData<PaginatedList<EPerson>>> {
const searchParams = [new RequestParam('query', query), new RequestParam('group', group)];
let findListOptions = new FindListOptions();
if (options) {
findListOptions = Object.assign(new FindListOptions(), options);
}
if (findListOptions.searchParams) {
findListOptions.searchParams = [...findListOptions.searchParams, ...searchParams];
} else {
findListOptions.searchParams = searchParams;
}
return this.searchBy('isNotMemberOf', findListOptions, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
}
/**
* Add a new patch to the object cache
* The patch is derived from the differences between the given object and its version in the object cache
@@ -281,15 +310,14 @@ export class EPersonDataService extends IdentifiableDataService<EPerson> impleme
this.editEPerson(ePerson);
}
});
return '/access-control/epeople';
return getEPersonEditRoute(ePerson.id);
}
/**
* Get EPeople admin page
* @param ePerson New EPerson to edit
*/
public getEPeoplePageRouterLink(): string {
return '/access-control/epeople';
return getEPersonsRoute();
}
/**

View File

@@ -43,11 +43,11 @@ describe('GroupDataService', () => {
let rdbService;
let objectCache;
function init() {
restEndpointURL = 'https://dspace.4science.it/dspace-spring-rest/api/eperson';
restEndpointURL = 'https://rest.api/server/api/eperson';
groupsEndpoint = `${restEndpointURL}/groups`;
groups = [GroupMock, GroupMock2];
groups$ = createSuccessfulRemoteDataObject$(createPaginatedList(groups));
rdbService = getMockRemoteDataBuildServiceHrefMap(undefined, { 'https://dspace.4science.it/dspace-spring-rest/api/eperson/groups': groups$ });
rdbService = getMockRemoteDataBuildServiceHrefMap(undefined, { 'https://rest.api/server/api/eperson/groups': groups$ });
halService = new HALEndpointServiceStub(restEndpointURL);
objectCache = getMockObjectCacheService();
TestBed.configureTestingModule({
@@ -111,6 +111,30 @@ describe('GroupDataService', () => {
});
});
describe('searchNonMemberGroups', () => {
beforeEach(() => {
spyOn(service, 'searchBy');
});
it('search with empty query and a group ID', () => {
service.searchNonMemberGroups('', GroupMock.id);
const options = Object.assign(new FindListOptions(), {
searchParams: [Object.assign(new RequestParam('query', '')),
Object.assign(new RequestParam('group', GroupMock.id))]
});
expect(service.searchBy).toHaveBeenCalledWith('isNotMemberOf', options, true, true);
});
it('search with query and a group ID', () => {
service.searchNonMemberGroups('test', GroupMock.id);
const options = Object.assign(new FindListOptions(), {
searchParams: [Object.assign(new RequestParam('query', 'test')),
Object.assign(new RequestParam('group', GroupMock.id))]
});
expect(service.searchBy).toHaveBeenCalledWith('isNotMemberOf', options, true, true);
});
});
describe('addSubGroupToGroup', () => {
beforeEach(() => {
objectCache.getByHref.and.returnValue(observableOf({

View File

@@ -3,7 +3,7 @@ import { Injectable } from '@angular/core';
import { createSelector, select, Store } from '@ngrx/store';
import { Observable, zip as observableZip } from 'rxjs';
import { filter, map, take } from 'rxjs/operators';
import { take } from 'rxjs/operators';
import {
GroupRegistryCancelGroupAction,
GroupRegistryEditGroupAction
@@ -40,6 +40,7 @@ import { DeleteData, DeleteDataImpl } from '../data/base/delete-data';
import { Operation } from 'fast-json-patch';
import { RestRequestMethod } from '../data/rest-request-method';
import { dataService } from '../data/base/data-service.decorator';
import { getGroupEditRoute } from '../../access-control/access-control-routing-paths';
const groupRegistryStateSelector = (state: AppState) => state.groupRegistry;
const editGroupSelector = createSelector(groupRegistryStateSelector, (groupRegistryState: GroupRegistryState) => groupRegistryState.editGroup);
@@ -104,23 +105,31 @@ export class GroupDataService extends IdentifiableDataService<Group> implements
}
/**
* Check if the current user is member of to the indicated group
*
* @param groupName
* the group name
* @return boolean
* true if user is member of the indicated group, false otherwise
* Searches for all groups which are *not* a member of a given group, via a passed in query
* (searches in group name and by exact UUID).
* Endpoint used: /eperson/groups/search/isNotMemberOf?query=<:string>&group=<:uuid>
* @param query search query param
* @param group UUID of group to exclude results from. Members of this group will never be returned.
* @param options
* @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's
* no valid cached version. Defaults to true
* @param reRequestOnStale Whether or not the request should automatically be re-
* requested after the response becomes stale
* @param linksToFollow List of {@link FollowLinkConfig} that indicate which
* {@link HALLink}s should be automatically resolved
*/
isMemberOf(groupName: string): Observable<boolean> {
const searchHref = 'isMemberOf';
const options = new FindListOptions();
options.searchParams = [new RequestParam('groupName', groupName)];
return this.searchBy(searchHref, options).pipe(
filter((groups: RemoteData<PaginatedList<Group>>) => !groups.isResponsePending),
take(1),
map((groups: RemoteData<PaginatedList<Group>>) => groups.payload.totalElements > 0)
);
public searchNonMemberGroups(query: string, group: string, options?: FindListOptions, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<Group>[]): Observable<RemoteData<PaginatedList<Group>>> {
const searchParams = [new RequestParam('query', query), new RequestParam('group', group)];
let findListOptions = new FindListOptions();
if (options) {
findListOptions = Object.assign(new FindListOptions(), options);
}
if (findListOptions.searchParams) {
findListOptions.searchParams = [...findListOptions.searchParams, ...searchParams];
} else {
findListOptions.searchParams = searchParams;
}
return this.searchBy('isNotMemberOf', findListOptions, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
}
/**
@@ -264,15 +273,15 @@ export class GroupDataService extends IdentifiableDataService<Group> implements
* @param group Group we want edit page for
*/
public getGroupEditPageRouterLink(group: Group): string {
return this.getGroupEditPageRouterLinkWithID(group.id);
return getGroupEditRoute(group.id);
}
/**
* Get Edit page of group
* @param groupID Group ID we want edit page for
*/
public getGroupEditPageRouterLinkWithID(groupId: string): string {
return '/access-control/groups/' + groupId;
public getGroupEditPageRouterLinkWithID(groupID: string): string {
return getGroupEditRoute(groupID);
}
/**

View File

@@ -13,9 +13,4 @@ export class EpersonDtoModel {
* Whether or not the linked EPerson is able to be deleted
*/
public ableToDelete: boolean;
/**
* Whether or not this EPerson is member of group on page it is being used on
*/
public memberOfGroup: boolean;
}

View File

@@ -1,68 +1,86 @@
import { ServerCheckGuard } from './server-check.guard';
import { Router } from '@angular/router';
import { Router, NavigationStart, UrlTree, NavigationEnd, RouterEvent } from '@angular/router';
import { of } from 'rxjs';
import { take } from 'rxjs/operators';
import { getPageInternalServerErrorRoute } from '../../app-routing-paths';
import { of, ReplaySubject } from 'rxjs';
import { RootDataService } from '../data/root-data.service';
import { TestScheduler } from 'rxjs/testing';
import SpyObj = jasmine.SpyObj;
describe('ServerCheckGuard', () => {
let guard: ServerCheckGuard;
let router: SpyObj<Router>;
let router: Router;
const eventSubject = new ReplaySubject<RouterEvent>(1);
let rootDataServiceStub: SpyObj<RootDataService>;
rootDataServiceStub = jasmine.createSpyObj('RootDataService', {
checkServerAvailability: jasmine.createSpy('checkServerAvailability'),
invalidateRootCache: jasmine.createSpy('invalidateRootCache')
});
router = jasmine.createSpyObj('Router', {
navigateByUrl: jasmine.createSpy('navigateByUrl')
});
let testScheduler: TestScheduler;
let redirectUrlTree: UrlTree;
beforeEach(() => {
testScheduler = new TestScheduler((actual, expected) => {
expect(actual).toEqual(expected);
});
rootDataServiceStub = jasmine.createSpyObj('RootDataService', {
checkServerAvailability: jasmine.createSpy('checkServerAvailability'),
invalidateRootCache: jasmine.createSpy('invalidateRootCache'),
findRoot: jasmine.createSpy('findRoot')
});
redirectUrlTree = new UrlTree();
router = {
events: eventSubject.asObservable(),
navigateByUrl: jasmine.createSpy('navigateByUrl'),
parseUrl: jasmine.createSpy('parseUrl').and.returnValue(redirectUrlTree)
} as any;
guard = new ServerCheckGuard(router, rootDataServiceStub);
});
afterEach(() => {
router.navigateByUrl.calls.reset();
rootDataServiceStub.invalidateRootCache.calls.reset();
});
it('should be created', () => {
expect(guard).toBeTruthy();
});
describe('when root endpoint has succeeded', () => {
describe('when root endpoint request has succeeded', () => {
beforeEach(() => {
rootDataServiceStub.checkServerAvailability.and.returnValue(of(true));
});
it('should not redirect to error page', () => {
guard.canActivateChild({} as any, {} as any).pipe(
take(1)
).subscribe((canActivate: boolean) => {
expect(canActivate).toEqual(true);
expect(rootDataServiceStub.invalidateRootCache).not.toHaveBeenCalled();
expect(router.navigateByUrl).not.toHaveBeenCalled();
it('should return true', () => {
testScheduler.run(({ expectObservable }) => {
const result$ = guard.canActivateChild({} as any, {} as any);
expectObservable(result$).toBe('(a|)', { a: true });
});
});
});
describe('when root endpoint has not succeeded', () => {
describe('when root endpoint request has not succeeded', () => {
beforeEach(() => {
rootDataServiceStub.checkServerAvailability.and.returnValue(of(false));
});
it('should redirect to error page', () => {
guard.canActivateChild({} as any, {} as any).pipe(
take(1)
).subscribe((canActivate: boolean) => {
expect(canActivate).toEqual(false);
expect(rootDataServiceStub.invalidateRootCache).toHaveBeenCalled();
expect(router.navigateByUrl).toHaveBeenCalledWith(getPageInternalServerErrorRoute());
it('should return a UrlTree with the route to the 500 error page', () => {
testScheduler.run(({ expectObservable }) => {
const result$ = guard.canActivateChild({} as any, {} as any);
expectObservable(result$).toBe('(b|)', { b: redirectUrlTree });
});
expect(router.parseUrl).toHaveBeenCalledWith('/500');
});
});
describe(`listenForRouteChanges`, () => {
it(`should retrieve the root endpoint, without using the cache, when the method is first called`, () => {
testScheduler.run(() => {
guard.listenForRouteChanges();
expect(rootDataServiceStub.findRoot).toHaveBeenCalledWith(false);
});
});
it(`should invalidate the root cache on every NavigationStart event`, () => {
testScheduler.run(() => {
guard.listenForRouteChanges();
eventSubject.next(new NavigationStart(1,''));
eventSubject.next(new NavigationEnd(1,'', ''));
eventSubject.next(new NavigationStart(2,''));
eventSubject.next(new NavigationEnd(2,'', ''));
eventSubject.next(new NavigationStart(3,''));
});
expect(rootDataServiceStub.invalidateRootCache).toHaveBeenCalledTimes(3);
});
});
});

View File

@@ -1,8 +1,15 @@
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, CanActivateChild, Router, RouterStateSnapshot } from '@angular/router';
import {
ActivatedRouteSnapshot,
CanActivateChild,
Router,
RouterStateSnapshot,
UrlTree,
NavigationStart
} from '@angular/router';
import { Observable } from 'rxjs';
import { take, tap } from 'rxjs/operators';
import { take, map, filter } from 'rxjs/operators';
import { RootDataService } from '../data/root-data.service';
import { getPageInternalServerErrorRoute } from '../../app-routing-paths';
@@ -23,17 +30,38 @@ export class ServerCheckGuard implements CanActivateChild {
*/
canActivateChild(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot): Observable<boolean> {
state: RouterStateSnapshot
): Observable<boolean | UrlTree> {
return this.rootDataService.checkServerAvailability().pipe(
take(1),
tap((isAvailable: boolean) => {
map((isAvailable: boolean) => {
if (!isAvailable) {
this.rootDataService.invalidateRootCache();
this.router.navigateByUrl(getPageInternalServerErrorRoute());
return this.router.parseUrl(getPageInternalServerErrorRoute());
} else {
return true;
}
})
);
}
/**
* Listen to all router events. Every time a new navigation starts, invalidate the cache
* for the root endpoint. That way we retrieve it once per routing operation to ensure the
* backend is not down. But if the guard is called multiple times during the same routing
* operation, the cached version is used.
*/
listenForRouteChanges(): void {
// we'll always be too late for the first NavigationStart event with the router subscribe below,
// so this statement is for the very first route operation. A `find` without using the cache,
// rather than an invalidateRootCache, because invalidating as the app is bootstrapping can
// break other features
this.rootDataService.findRoot(false);
this.router.events.pipe(
filter(event => event instanceof NavigationStart),
).subscribe(() => {
this.rootDataService.invalidateRootCache();
});
}
}

View File

@@ -38,8 +38,8 @@ export class BrowserHardRedirectService extends HardRedirectService {
/**
* Get the origin of the current URL
* i.e. <scheme> "://" <hostname> [ ":" <port> ]
* e.g. if the URL is https://demo7.dspace.org/search?query=test,
* the origin would be https://demo7.dspace.org
* e.g. if the URL is https://demo.dspace.org/search?query=test,
* the origin would be https://demo.dspace.org
*/
getCurrentOrigin(): string {
return this.location.origin;

View File

@@ -25,8 +25,8 @@ export abstract class HardRedirectService {
/**
* Get the origin of the current URL
* i.e. <scheme> "://" <hostname> [ ":" <port> ]
* e.g. if the URL is https://demo7.dspace.org/search?query=test,
* the origin would be https://demo7.dspace.org
* e.g. if the URL is https://demo.dspace.org/search?query=test,
* the origin would be https://demo.dspace.org
*/
abstract getCurrentOrigin(): string;
}

View File

@@ -69,8 +69,8 @@ export class ServerHardRedirectService extends HardRedirectService {
/**
* Get the origin of the current URL
* i.e. <scheme> "://" <hostname> [ ":" <port> ]
* e.g. if the URL is https://demo7.dspace.org/search?query=test,
* the origin would be https://demo7.dspace.org
* e.g. if the URL is https://demo.dspace.org/search?query=test,
* the origin would be https://demo.dspace.org
*/
getCurrentOrigin(): string {
return this.req.protocol + '://' + this.req.headers.host;

View File

@@ -29,6 +29,18 @@ export class WorkspaceitemSectionUploadFileObject {
value: string;
};
/**
* The file format information
*/
format: {
shortDescription: string,
description: string,
mimetype: string,
supportLevel: string,
internal: boolean,
type: string
};
/**
* The file url
*/

View File

@@ -1,4 +1,4 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { ComponentFixture, TestBed, waitForAsync, fakeAsync, flush } from '@angular/core/testing';
import { TranslateModule } from '@ngx-translate/core';
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { CurationFormComponent } from './curation-form.component';
@@ -16,6 +16,7 @@ import { ConfigurationDataService } from '../core/data/configuration-data.servic
import { ConfigurationProperty } from '../core/shared/configuration-property.model';
import { getProcessDetailRoute } from '../process-page/process-page-routing.paths';
import { HandleService } from '../shared/handle.service';
import { of as observableOf } from 'rxjs';
describe('CurationFormComponent', () => {
let comp: CurationFormComponent;
@@ -54,7 +55,7 @@ describe('CurationFormComponent', () => {
});
handleService = {
normalizeHandle: (a) => a
normalizeHandle: (a: string) => observableOf(a),
} as any;
notificationsService = new NotificationsServiceStub();
@@ -151,12 +152,13 @@ describe('CurationFormComponent', () => {
], []);
});
it(`should show an error notification and return when an invalid dsoHandle is provided`, () => {
it(`should show an error notification and return when an invalid dsoHandle is provided`, fakeAsync(() => {
comp.dsoHandle = 'test-handle';
spyOn(handleService, 'normalizeHandle').and.returnValue(null);
spyOn(handleService, 'normalizeHandle').and.returnValue(observableOf(null));
comp.submit();
flush();
expect(notificationsService.error).toHaveBeenCalled();
expect(scriptDataService.invoke).not.toHaveBeenCalled();
});
}));
});

View File

@@ -1,22 +1,22 @@
import { ChangeDetectorRef, Component, Input, OnInit } from '@angular/core';
import { ChangeDetectorRef, Component, Input, OnDestroy, OnInit } from '@angular/core';
import { ScriptDataService } from '../core/data/processes/script-data.service';
import { UntypedFormControl, UntypedFormGroup } from '@angular/forms';
import { getFirstCompletedRemoteData } from '../core/shared/operators';
import { find, map } from 'rxjs/operators';
import { getFirstCompletedRemoteData, getFirstSucceededRemoteDataPayload } from '../core/shared/operators';
import { map } from 'rxjs/operators';
import { NotificationsService } from '../shared/notifications/notifications.service';
import { TranslateService } from '@ngx-translate/core';
import { hasValue, isEmpty, isNotEmpty } from '../shared/empty.util';
import { RemoteData } from '../core/data/remote-data';
import { Router } from '@angular/router';
import { ProcessDataService } from '../core/data/processes/process-data.service';
import { Process } from '../process-page/processes/process.model';
import { ConfigurationDataService } from '../core/data/configuration-data.service';
import { ConfigurationProperty } from '../core/shared/configuration-property.model';
import { Observable } from 'rxjs';
import { Observable, Subscription } from 'rxjs';
import { getProcessDetailRoute } from '../process-page/process-page-routing.paths';
import { HandleService } from '../shared/handle.service';
export const CURATION_CFG = 'plugin.named.org.dspace.curate.CurationTask';
/**
* Component responsible for rendering the Curation Task form
*/
@@ -24,7 +24,7 @@ export const CURATION_CFG = 'plugin.named.org.dspace.curate.CurationTask';
selector: 'ds-curation-form',
templateUrl: './curation-form.component.html'
})
export class CurationFormComponent implements OnInit {
export class CurationFormComponent implements OnDestroy, OnInit {
config: Observable<RemoteData<ConfigurationProperty>>;
tasks: string[];
@@ -33,10 +33,11 @@ export class CurationFormComponent implements OnInit {
@Input()
dsoHandle: string;
subs: Subscription[] = [];
constructor(
private scriptDataService: ScriptDataService,
private configurationDataService: ConfigurationDataService,
private processDataService: ProcessDataService,
private notificationsService: NotificationsService,
private translateService: TranslateService,
private handleService: HandleService,
@@ -45,6 +46,10 @@ export class CurationFormComponent implements OnInit {
) {
}
ngOnDestroy(): void {
this.subs.forEach((sub: Subscription) => sub.unsubscribe());
}
ngOnInit(): void {
this.form = new UntypedFormGroup({
task: new UntypedFormControl(''),
@@ -52,16 +57,15 @@ export class CurationFormComponent implements OnInit {
});
this.config = this.configurationDataService.findByPropertyName(CURATION_CFG);
this.config.pipe(
find((rd: RemoteData<ConfigurationProperty>) => rd.hasSucceeded),
map((rd: RemoteData<ConfigurationProperty>) => rd.payload)
).subscribe((configProperties) => {
this.subs.push(this.config.pipe(
getFirstSucceededRemoteDataPayload(),
).subscribe((configProperties: ConfigurationProperty) => {
this.tasks = configProperties.values
.filter((value) => isNotEmpty(value) && value.includes('='))
.map((value) => value.split('=')[1].trim());
this.form.get('task').patchValue(this.tasks[0]);
this.cdr.detectChanges();
});
}));
}
/**
@@ -77,33 +81,41 @@ export class CurationFormComponent implements OnInit {
*/
submit() {
const taskName = this.form.get('task').value;
let handle;
let handle$: Observable<string | null>;
if (this.hasHandleValue()) {
handle = this.handleService.normalizeHandle(this.dsoHandle);
if (isEmpty(handle)) {
this.notificationsService.error(this.translateService.get('curation.form.submit.error.head'),
this.translateService.get('curation.form.submit.error.invalid-handle'));
return;
}
handle$ = this.handleService.normalizeHandle(this.dsoHandle).pipe(
map((handle: string | null) => {
if (isEmpty(handle)) {
this.notificationsService.error(this.translateService.get('curation.form.submit.error.head'),
this.translateService.get('curation.form.submit.error.invalid-handle'));
}
return handle;
}),
);
} else {
handle = this.handleService.normalizeHandle(this.form.get('handle').value);
if (isEmpty(handle)) {
handle = 'all';
}
handle$ = this.handleService.normalizeHandle(this.form.get('handle').value).pipe(
map((handle: string | null) => isEmpty(handle) ? 'all' : handle),
);
}
this.scriptDataService.invoke('curate', [
{ name: '-t', value: taskName },
{ name: '-i', value: handle },
], []).pipe(getFirstCompletedRemoteData()).subscribe((rd: RemoteData<Process>) => {
if (rd.hasSucceeded) {
this.notificationsService.success(this.translateService.get('curation.form.submit.success.head'),
this.translateService.get('curation.form.submit.success.content'));
this.router.navigateByUrl(getProcessDetailRoute(rd.payload.processId));
} else {
this.notificationsService.error(this.translateService.get('curation.form.submit.error.head'),
this.translateService.get('curation.form.submit.error.content'));
this.subs.push(handle$.subscribe((handle: string) => {
if (hasValue(handle)) {
this.subs.push(this.scriptDataService.invoke('curate', [
{ name: '-t', value: taskName },
{ name: '-i', value: handle },
], []).pipe(
getFirstCompletedRemoteData(),
).subscribe((rd: RemoteData<Process>) => {
if (rd.hasSucceeded) {
this.notificationsService.success(this.translateService.get('curation.form.submit.success.head'),
this.translateService.get('curation.form.submit.success.content'));
void this.router.navigateByUrl(getProcessDetailRoute(rd.payload.processId));
} else {
this.notificationsService.error(this.translateService.get('curation.form.submit.error.head'),
this.translateService.get('curation.form.submit.error.content'));
}
}));
}
});
}));
}
}

View File

@@ -1,5 +1,5 @@
import { Component, Inject, Injector, Input, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { AlertType } from '../../shared/alert/aletr-type';
import { AlertType } from '../../shared/alert/alert-type';
import { DSpaceObject } from '../../core/shared/dspace-object.model';
import { DsoEditMetadataForm } from './dso-edit-metadata-form';
import { map } from 'rxjs/operators';

View File

@@ -1,3 +1,3 @@
<ds-register-email-form
<ds-themed-register-email-form
[MESSAGE_PREFIX]="'forgot-email.form'" [typeRequest]="typeRequest">
</ds-register-email-form>
</ds-themed-register-email-form>

View File

@@ -1,4 +1,4 @@
<div [ngClass]="{'open': !(isNavBarCollapsed | async)}">
<div [ngClass]="{'open': !(isNavBarCollapsed | async)}" id="header-navbar-wrapper">
<ds-themed-header></ds-themed-header>
<ds-themed-navbar></ds-themed-navbar>
</div>

View File

@@ -1,4 +1,6 @@
:host {
position: relative;
z-index: var(--ds-nav-z-index);
div#header-navbar-wrapper {
border-bottom: 1px var(--ds-header-navbar-border-bottom-color) solid;
}
}

View File

@@ -1,6 +1,6 @@
import { Component, OnInit } from '@angular/core';
import { Component, OnInit, ElementRef } from '@angular/core';
import { ContextHelpService } from '../../shared/context-help.service';
import { Observable } from 'rxjs';
import { Observable, Subscription } from 'rxjs';
import { map } from 'rxjs/operators';
/**
@@ -15,12 +15,23 @@ import { map } from 'rxjs/operators';
export class ContextHelpToggleComponent implements OnInit {
buttonVisible$: Observable<boolean>;
subscriptions: Subscription[] = [];
constructor(
private contextHelpService: ContextHelpService,
) { }
protected elRef: ElementRef,
protected contextHelpService: ContextHelpService,
) {
}
ngOnInit(): void {
this.buttonVisible$ = this.contextHelpService.tooltipCount$().pipe(map(x => x > 0));
this.subscriptions.push(this.buttonVisible$.subscribe((showContextHelpToggle: boolean) => {
if (showContextHelpToggle) {
this.elRef.nativeElement.classList.remove('d-none');
} else {
this.elRef.nativeElement.classList.add('d-none');
}
}));
}
onClick() {

View File

@@ -7,12 +7,12 @@
<nav role="navigation" [attr.aria-label]="'nav.user.description' | translate" class="navbar navbar-light navbar-expand-md flex-shrink-0 px-0">
<ds-themed-search-navbar></ds-themed-search-navbar>
<ds-lang-switch></ds-lang-switch>
<ds-themed-lang-switch></ds-themed-lang-switch>
<ds-context-help-toggle></ds-context-help-toggle>
<ds-themed-auth-nav-menu></ds-themed-auth-nav-menu>
<ds-impersonate-navbar></ds-impersonate-navbar>
<div class="pl-2">
<button class="navbar-toggler" type="button" (click)="toggleNavbar()"
<div *ngIf="isXsOrSm$ | async" class="pl-2">
<button class="navbar-toggler px-0" type="button" (click)="toggleNavbar()"
aria-controls="collapsingNav"
aria-expanded="false" [attr.aria-label]="'nav.toggle' | translate">
<span class="navbar-toggler-icon fas fa-bars fa-fw" aria-hidden="true"></span>

View File

@@ -1,3 +1,7 @@
header {
background-color: var(--ds-header-bg);
}
.navbar-brand img {
max-height: var(--ds-header-logo-height);
max-width: 100%;
@@ -20,3 +24,8 @@
}
}
.navbar {
display: flex;
gap: calc(var(--bs-spacer) / 3);
align-items: center;
}

View File

@@ -10,6 +10,8 @@ import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { MenuService } from '../shared/menu/menu.service';
import { MenuServiceStub } from '../shared/testing/menu-service.stub';
import { HostWindowService } from '../shared/host-window.service';
import { HostWindowServiceStub } from '../shared/testing/host-window-service.stub';
let comp: HeaderComponent;
let fixture: ComponentFixture<HeaderComponent>;
@@ -26,6 +28,7 @@ describe('HeaderComponent', () => {
ReactiveFormsModule],
declarations: [HeaderComponent],
providers: [
{ provide: HostWindowService, useValue: new HostWindowServiceStub(0) },
{ provide: MenuService, useValue: menuService }
],
schemas: [NO_ERRORS_SCHEMA]
@@ -40,7 +43,7 @@ describe('HeaderComponent', () => {
fixture = TestBed.createComponent(HeaderComponent);
comp = fixture.componentInstance;
fixture.detectChanges();
});
describe('when the toggle button is clicked', () => {

Some files were not shown because too many files have changed in this diff Show More