Merge branch 'master' into resolvers-branch-angular6

Conflicts:
	package.json
	src/app/+search-page/search-filters/search-filters.component.ts
	src/app/core/auth/auth.effects.ts
	src/app/core/auth/auth.service.ts
	src/app/core/auth/server-auth.service.ts
	src/app/core/data/comcol-data.service.ts
	src/app/core/data/community-data.service.ts
	src/app/core/data/data.service.ts
	src/app/core/data/item-data.service.ts
	src/app/core/shared/dspace-object.model.ts
	src/app/header/header.component.spec.ts
	src/app/shared/auth-nav-menu/auth-nav-menu.component.ts
	src/app/shared/testing/auth-service-stub.ts
	yarn.lock
This commit is contained in:
lotte
2018-10-08 12:28:08 +02:00
78 changed files with 2011 additions and 265 deletions

View File

@@ -8,13 +8,13 @@ module.exports = {
nameSpace: '/' nameSpace: '/'
}, },
// The REST API server settings. // The REST API server settings.
rest: { rest: {
ssl: true, ssl: true,
host: 'dspace7.4science.it', host: 'dspace7.4science.it',
port: 443, port: 443,
// NOTE: Space is capitalized because 'namespace' is a reserved string in TypeScript // NOTE: Space is capitalized because 'namespace' is a reserved string in TypeScript
nameSpace: '/dspace-spring-rest/api' nameSpace: '/dspace-spring-rest/api'
}, },
// Caching settings // Caching settings
cache: { cache: {
// NOTE: how long should objects be cached for by default // NOTE: how long should objects be cached for by default

View File

@@ -89,11 +89,11 @@
"angular2-text-mask": "9.0.0", "angular2-text-mask": "9.0.0",
"angulartics2": "^6.2.0", "angulartics2": "^6.2.0",
"body-parser": "1.18.2", "body-parser": "1.18.2",
"bootstrap": "4.1.1", "bootstrap": "4.1.3",
"cerialize": "0.1.18", "cerialize": "0.1.18",
"compression": "1.7.1", "compression": "1.7.1",
"cookie-parser": "1.4.3", "cookie-parser": "1.4.3",
"core-js": "2.5.3", "core-js": "^2.5.7",
"express": "4.16.2", "express": "4.16.2",
"express-session": "1.15.6", "express-session": "1.15.6",
"font-awesome": "4.7.0", "font-awesome": "4.7.0",

277
resources/i18n/cs.json Normal file
View File

@@ -0,0 +1,277 @@
{
"footer": {
"copyright": "copyright © 2002-{{ year }}",
"link.dspace": "software DSpace",
"link.duraspace": "DuraSpace"
},
"collection": {
"page": {
"news": "Novinky",
"license": "Licence",
"browse": {
"recent": {
"head": "Poslední příspěvky"
}
}
}
},
"community": {
"page": {
"news": "Novinky",
"license": "Licence"
},
"sub-collection-list": {
"head": "Kolekce v této komunitě"
}
},
"item": {
"page": {
"author": "Autor",
"abstract": "Abstract",
"date": "Datum",
"uri": "URI",
"files": "Soubory",
"collections": "Kolekce",
"filesection": {
"download": "Stáhnout",
"name": "Název:",
"format": "Formát:",
"size": "Velikost:",
"description": "Popis:"
},
"link": {
"simple": "Minimální záznam",
"full": "Úplný záznam"
}
}
},
"nav": {
"home": "Domů",
"login": "Přihlásit se",
"logout": "Odhlásit se"
},
"pagination": {
"results-per-page": "Výsledků na stránku",
"sort-direction": "Seřazení",
"showing": {
"label": "Zobrazují se záznamy ",
"detail": "{{ range }} z {{ total }}"
}
},
"sorting": {
"score": {
"DESC": "Relevance"
},
"dc.title": {
"ASC": "Název vzestupně",
"DESC": "Název sestupně"
}
},
"title": "DSpace",
"404": {
"help": "Nepodařilo se najít stránku, kterou hledáte. Je možné, že stránka byla přesunuta nebo smazána. Pomocí tlačítka níže můžete přejít na domovskou stránku. ",
"page-not-found": "stránka nenalezena",
"link": {
"home-page": "Přejít na domovskou stránku"
}
},
"home": {
"title": "DSpace Angular :: Domů",
"description": "",
"top-level-communities": {
"head": "Komunity v DSpace",
"help": "Vybráním komunity můžete prohlížet její kolekce."
}
},
"search": {
"title": "DSpace Angular :: Hledat",
"description": "",
"form": {
"search": "Hledat",
"search_dspace": "Hledat v DSpace"
},
"results": {
"head": "Výsledky hledání",
"no-results": "Nebyli nalezeny žádné výsledky"
},
"sidebar": {
"close": "Zpět na výsledky",
"open": "Vyhledávací nástroje",
"results": "výsledky",
"filters": {
"title": "Filtry"
},
"settings": {
"title": "Nastavení",
"sort-by": "Řadit dle",
"rpp": "Výsledků na stránku"
}
},
"view-switch": {
"show-list": "Zobrazit seznam",
"show-grid": "Zobrazit mřížku"
},
"filters": {
"head": "Filtry",
"reset": "Obnovit filtry",
"applied": {
"f.author": "Autor",
"f.dateIssued.min": "Od data",
"f.dateIssued.max": "Do data",
"f.subject": "Předmět",
"f.has_content_in_original_bundle": "Má soubory"
},
"filter": {
"show-more": "Zobrazit více",
"show-less": "Sbalit",
"author": {
"placeholder": "Jméno autora",
"head": "Autor"
},
"scope": {
"placeholder": "Filtr rozsahu",
"head": "Rozsah"
},
"subject": {
"placeholder": "Předmět",
"head": "Předmět"
},
"dateIssued": {
"max": {
"placeholder": "Datum od"
},
"min": {
"placeholder": "Datum do"
},
"head": "Datum"
},
"has_content_in_original_bundle": {
"head": "Má soubory"
}
}
}
},
"browse": {
"title": "Prohlížíte {{ collection }} dle {{ field }} {{ value }}"
},
"admin": {
"registries": {
"metadata": {
"title": "DSpace Angular :: Registr metadat",
"head": "Registr metadat",
"description": "Registr metadat je seznam všech metadatových polí dostupných v repozitáři. Tyto pole mohou být rozdělena do více schémat. DSpace však vyžaduje použití schématu kvalifikový Dublin Core.",
"schemas": {
"table": {
"id": "ID",
"namespace": "Jmenný prostor",
"name": "Název"
},
"no-items": "Žádná schémata metadat."
}
},
"schema": {
"title": "DSpace Angular :: Registr schémat metadat",
"head": "Metadata Schema",
"description": "Toto je schéma metadat pro „{{namespace}}“.",
"fields": {
"head": "Pole schématu metadat",
"table": {
"field": "Pole",
"scopenote": "Poznámka o rozsahu"
},
"no-items": "Žádná metadatová pole."
}
},
"bitstream-formats": {
"title": "DSpace Angular :: Registr formátů souborů",
"head": "Registr formátů souborů",
"description": "Tento seznam formátů souborů poskytuje informace o známých formátech a o úrovni jejich podpory.",
"formats": {
"table": {
"name": "Název",
"mimetype": "Typ MIME",
"supportLevel": {
"head": "Úroveň podpory",
"0": "Neznámá",
"1": "Známá",
"2": "Podpora"
},
"internal": "interní"
},
"no-items": "Žádné formáty souborů."
}
}
}
},
"loading": {
"default": "Načítá se...",
"top-level-communities": "Načítají se komunity nejvyšší úrovně...",
"community": "Načítá se komunita...",
"collection": "Načítá se kolekce...",
"sub-collections": "Načítají se subkolekce...",
"recent-submissions": "Načítají se poslední příspěvky...",
"item": "Načítá se záznam...",
"objects": "Načítá se...",
"search-results": "Načítají se výsledky hledání...",
"browse-by": "Načítají se záznamy..."
},
"error": {
"default": "Chyba",
"top-level-communities": "Chyba během stahování komunit nejvyšší úrovně",
"community": "Chyba během stahování komunity",
"collection": "Chyba během stahování kolekce",
"sub-collections": "Chyba během stahování subkolekcí",
"recent-submissions": "Chyba během stahování posledních příspěvků",
"item": "Chyba během stahování záznamu",
"objects": "Chyba během stahování objektů",
"search-results": "Chyba během stahování výsledků hledání",
"browse-by": "Chyba během stahování záznamů",
"validation": {
"pattern": "Tento vstup je omezen dle vzoru: {{ pattern }}.",
"license": {
"notgranted": "Pro dokončení zaslání Musíte udělit licenci. Pokud v tuto chvíli tuto licenci nemůžete udělit, můžete svou práci uložit a později se k svému příspěveku vrátit nebo jej smazat."
}
}
},
"form": {
"submit": "Odeslat",
"cancel": "Zrušit",
"search": "Hledat",
"remove": "Smazat",
"first-name": "Křestní jméno",
"last-name": "Příjmení",
"loading": "Načítá se...",
"no-results": "Nebyli nalezeny žádné výsledky",
"no-value": "Nebyla zadána hodnota",
"group-collapse": "Sbalit",
"group-expand": "Rozbalit",
"group-collapse-help": "Kliknutím sem sbalíte",
"group-expand-help": "Kliknutím sem rozbalíte a přidáte další prvky"
},
"login": {
"title": "Přihlásit se",
"form": {
"header": "Prosím, přihlaste se do DSpace",
"email": "E-mailová adresa",
"forgot-password": "Zapomněli jste své heslo?",
"new-user": "Nový uživatel? Zaregistrujte se kliknutím sem.",
"password": "Heslo",
"submit": "Přihlásit se"
}
},
"logout": {
"title": "Odhlásit se",
"form": {
"header": "Odhlásit se z DSpace",
"submit": "Odhlásit se"
}
},
"auth": {
"messages": {
"expired": "Vaše relace vypršela. Prosím, znova se přihlaste."
},
"errors": {
"invalid-user": "Neplatná e-mailová adresa nebo heslo."
}
}
}

277
resources/i18n/de.json Normal file
View File

@@ -0,0 +1,277 @@
{
"footer": {
"copyright": "Copyright © 2002-{{ year }}",
"link.dspace": "DSpace Software",
"link.duraspace": "DuraSpace"
},
"collection": {
"page": {
"news": "Neuigkeiten",
"license": "Lizenz",
"browse": {
"recent": {
"head": "Aktuellste Veröffentlichungen"
}
}
}
},
"community": {
"page": {
"news": "Neuigkeiten",
"license": "Lizenz"
},
"sub-collection-list": {
"head": "Sammlungen in diesem Bereich"
}
},
"item": {
"page": {
"author": "Autor",
"abstract": "Kurzfassung",
"date": "Datum",
"uri": "URI",
"files": "Dateien",
"collections": "Sammlungen",
"filesection": {
"download": "Herunterladen",
"name": "Name:",
"format": "Format:",
"size": "Größe:",
"description": "Beschreibung:"
},
"link": {
"simple": "Kurzanzeige",
"full": "Vollanzeige"
}
}
},
"nav": {
"home": "Zur Startseite",
"login": "Anmelden",
"logout": "Abmelden"
},
"pagination": {
"results-per-page": "Ergebnisse pro Seite",
"sort-direction": "Sortiermöglichkeiten",
"showing": {
"label": "Anzeige der Treffer ",
"detail": "{{ range }} bis {{ total }}"
}
},
"sorting": {
"score": {
"DESC": "Relevanz"
},
"dc.title": {
"ASC": "Titel aufsteigend",
"DESC": "Titel absteigend"
}
},
"title": "DSpace",
"404": {
"help": "Die Seite, die Sie aufrufen wollten, konnte nicht gefunden werden. Sie könnte verschoben oder gelöscht worden sein. Mit dem Link unten kommen Sie zurück zur Startseite. ",
"page-not-found": "Seite nicht gefunden",
"link": {
"home-page": "Zurück zur Startseite"
}
},
"home": {
"title": "DSpace Angular :: Startseite",
"description": "",
"top-level-communities": {
"head": "Bereiche in DSpace",
"help": "Wählen Sie einen Bereich, um seine Sammlungen einzusehen."
}
},
"search": {
"title": "DSpace Angular :: Suche",
"description": "",
"form": {
"search": "Suche",
"search_dspace": "DSpace durchsuchen"
},
"results": {
"head": "Suchergebnisse",
"no-results": "Zu dieser Suche gibt es keine Treffer."
},
"sidebar": {
"close": "Zurück zu den Ergebnissen",
"open": "Suchwerkzeuge",
"results": "Ergebnisse",
"filters": {
"title": "Filter"
},
"settings": {
"title": "Einstellungen",
"sort-by": "Sortiere nach",
"rpp": "Treffer pro Seite"
}
},
"view-switch": {
"show-list": "Zeige als Liste",
"show-grid": "Zeige als Raster"
},
"filters": {
"head": "Filter",
"reset": "Filter zurücksetzen",
"applied": {
"f.author": "Autor",
"f.dateIssued.min": "Anfangsdatum",
"f.dateIssued.max": "Enddatum",
"f.subject": "Thema",
"f.has_content_in_original_bundle": "Besitzt Dateien"
},
"filter": {
"show-more": "Zeige mehr",
"show-less": "Zeige weniger",
"author": {
"placeholder": "Autor",
"head": "Autor"
},
"scope": {
"placeholder": "Bereichsfilter",
"head": "Bereich"
},
"subject": {
"placeholder": "Schlagwort",
"head": "Schlagwort"
},
"dateIssued": {
"max": {
"placeholder": "Frühestes Datum"
},
"min": {
"placeholder": "Ältestes Datum"
},
"head": "Datum"
},
"has_content_in_original_bundle": {
"head": "Besitzt Dateien"
}
}
}
},
"browse": {
"title": "Anzeige {{ collection }} nach {{ field }} {{ value }}"
},
"admin": {
"registries": {
"metadata": {
"title": "DSpace Angular :: Metadatenreferenzliste",
"head": "Metadatenreferenzliste",
"description": "Die Metadatenreferenzliste beinhaltet alle Metadatenfelder, die zur Verfügung stehen. Die Felder können in unterschiedlichen Schemata enthalten sein. Nichtsdestotrotz benötigt DSpace mindestens qualifiziertes Dublin Core.",
"schemas": {
"table": {
"id": "ID",
"namespace": "Namensraum",
"name": "Name"
},
"no-items": "Es gbit keine Metadatenschemata."
}
},
"schema": {
"title": "DSpace Angular :: Referenzliste der Metadatenschemata",
"head": "Metadatenschemata",
"description": "Dies ist das Metadatenschema für \"{{namespace}}\".",
"fields": {
"head": "Felder in diesem Schema",
"table": {
"field": "Feld",
"scopenote": "Gültigkeitsbereich"
},
"no-items": "Es gibt keine Felder in diesem Schema."
}
},
"bitstream-formats": {
"title": "DSpace Angular :: Referenzliste der Dateiformate",
"head": "Referenzliste der Dateiformate",
"description": "Diese Liste enhtält die in diesem Repositorium zulässigen Dateiformate und den jeweiligen Unterstützungsgrad.",
"formats": {
"table": {
"name": "Name",
"mimetype": "MIME Type",
"supportLevel": {
"head": "Unterstützungsgrad",
"0": "Unbekannt",
"1": "Bekannt",
"2": "Unterstützt"
},
"internal": "intern"
},
"no-items": "Es gibt keine Formate in dieser Referenzliste."
}
}
}
},
"loading": {
"default": "Am Laden ...",
"top-level-communities": "Die Hauptbereiche werden geladen ...",
"community": "Der Bereich wird geladen ...",
"collection": "Die Sammlung wird geladen ...",
"sub-collections": "Die untergeordneten Sammlungen werden geladen ...",
"recent-submissions": "Die aktuellsten Veröffentlichungen werden geladen ...",
"item": "Die Ressource wird geladen ...",
"objects": "Am Laden ...",
"search-results": "Die Suchergebnisse werden geladen ...",
"browse-by": "Die Ressourcen werden geladen ..."
},
"error": {
"default": "Fehler",
"top-level-communities": "Fehler beim Laden der Hauptbereiche.",
"community": "Fehler beim Laden des Bereiches.",
"collection": "Fehler beim Laden der Sammlung.",
"sub-collections": "Fehler beim Laden der untergeordneten Sammlungen.",
"recent-submissions": "Fehler beim Laden der aktuellsten Veröffentlichungen.",
"item": "Fehler beim Laden der Ressource.",
"objects": "Fehler beim Laden der Objekte.",
"search-results": "Fehler beim Laden der Suchergebnisse.",
"browse-by": "Fehler beim Laden der Ressourcen",
"validation": {
"pattern": "Die Eingabe kann nur folgendes Muster haben: {{ pattern }}.",
"license": {
"notgranted": "Sie müssen der Lizenz zustimmen, um die Ressource einzureichen. Wenn dies zur Zeit nicht geht, können Sie die Einreichung speichern und später wiederaufnehmen oder löschen."
}
}
},
"form": {
"submit": "Los",
"cancel": "Abbrechen",
"search": "Suchen",
"remove": "Löschen",
"first-name": "Vorname",
"last-name": "Nachname",
"loading": "Am Laden ...",
"no-results": "Keine Ergebnisse gefunden",
"no-value": "Kein Wert eingegeben",
"group-collapse": "Weniger",
"group-expand": "Mehr",
"group-collapse-help": "Hier klicken, um die Anzeige zu reduzieren",
"group-expand-help": "Hier klicken, um mehr Elemente anzuzeigen"
},
"login": {
"title": "Einloggen",
"form": {
"header": "Bitte Loggen Sie sich ein.",
"email": "E-Mail-Adresse",
"forgot-password": "Haben Sie Ihr Passwort vergessen?",
"new-user": "Sind Sie neu hier? Klicken Sie hier, um sich zu registrieren.",
"password": "Passwort",
"submit": "Einloggen"
}
},
"logout": {
"title": "Ausloggen",
"form": {
"header": "Ausloggen aus DSpace",
"submit": "Ausloggen"
}
},
"auth": {
"messages": {
"expired": "Ihre Sitzung ist abgelaufen, bitte melden Sie sich erneut an."
},
"errors": {
"invalid-user": "Ungültige E-Mail-Adresse oder Passwort."
}
}
}

View File

@@ -151,6 +151,9 @@
} }
} }
}, },
"browse": {
"title": "Browsing {{ collection }} by {{ field }} {{ value }}"
},
"admin": { "admin": {
"registries": { "registries": {
"metadata": { "metadata": {
@@ -202,18 +205,19 @@
}, },
"loading": { "loading": {
"default": "Loading...", "default": "Loading...",
"top-level-communities": "Loading top level communities...", "top-level-communities": "Loading top-level communities...",
"community": "Loading community...", "community": "Loading community...",
"collection": "Loading collection...", "collection": "Loading collection...",
"sub-collections": "Loading sub-collections...", "sub-collections": "Loading sub-collections...",
"recent-submissions": "Loading recent submissions...", "recent-submissions": "Loading recent submissions...",
"item": "Loading item...", "item": "Loading item...",
"objects": "Loading...", "objects": "Loading...",
"search-results": "Loading search results..." "search-results": "Loading search results...",
"browse-by": "Loading items..."
}, },
"error": { "error": {
"default": "Error", "default": "Error",
"top-level-communities": "Error fetching top level communities", "top-level-communities": "Error fetching top-level communities",
"community": "Error fetching community", "community": "Error fetching community",
"collection": "Error fetching collection", "collection": "Error fetching collection",
"sub-collections": "Error fetching sub-collections", "sub-collections": "Error fetching sub-collections",
@@ -221,6 +225,7 @@
"item": "Error fetching item", "item": "Error fetching item",
"objects": "Error fetching objects", "objects": "Error fetching objects",
"search-results": "Error fetching search results", "search-results": "Error fetching search results",
"browse-by": "Error fetching items",
"validation": { "validation": {
"pattern": "This input is restricted by the current pattern: {{ pattern }}.", "pattern": "This input is restricted by the current pattern: {{ pattern }}.",
"license": { "license": {
@@ -241,7 +246,7 @@
"group-collapse": "Collapse", "group-collapse": "Collapse",
"group-expand": "Expand", "group-expand": "Expand",
"group-collapse-help": "Click here to collapse", "group-collapse-help": "Click here to collapse",
"group-expand-help": "Click here to expand and add more element" "group-expand-help": "Click here to expand and add more elements"
}, },
"login": { "login": {
"title": "Login", "title": "Login",
@@ -266,7 +271,7 @@
"expired": "Your session has expired. Please log in again." "expired": "Your session has expired. Please log in again."
}, },
"errors": { "errors": {
"invalid-user": "Invalid email or password." "invalid-user": "Invalid email address or password."
} }
} }
} }

277
resources/i18n/nl.json Normal file
View File

@@ -0,0 +1,277 @@
{
"footer": {
"copyright": "copyright © 2002-{{ year }}",
"link.dspace": "DSpace software",
"link.duraspace": "DuraSpace"
},
"collection": {
"page": {
"news": "Nieuws",
"license": "Licentie",
"browse": {
"recent": {
"head": "Recent toegevoegd"
}
}
}
},
"community": {
"page": {
"news": "Nieuws",
"license": "Licentie"
},
"sub-collection-list": {
"head": "Collecties in deze Community"
}
},
"item": {
"page": {
"author": "Auteur",
"abstract": "Abstract",
"date": "Datum",
"uri": "URI",
"files": "Bestanden",
"collections": "Collecties",
"filesection": {
"download": "Download",
"name": "Naam:",
"format": "Formaat:",
"size": "Grootte:",
"description": "Beschrijving:"
},
"link": {
"simple": "Eenvoudige item weergave",
"full": "Volledige item weergave"
}
}
},
"nav": {
"home": "Home",
"login": "Log In",
"logout": "Log Uit"
},
"pagination": {
"results-per-page": "Resultaten per pagina",
"sort-direction": "Sorteer mogelijkheden",
"showing": {
"label": "Getoonde items ",
"detail": "{{ range }} tot {{ total }}"
}
},
"sorting": {
"score": {
"DESC": "Relevantie"
},
"dc.title": {
"ASC": "Oplopend op titel",
"DESC": "Aflopend op titel"
}
},
"title": "DSpace",
"404": {
"help": "De pagina die u zoekt kan niet gevonden worden. De pagina werd mogelijk verplaatst of verwijderd. U kan onderstaande knop gebruiken om terug naar de homepagina te gaan. ",
"page-not-found": "Pagina niet gevonden",
"link": {
"home-page": "Terug naar de homepagina"
}
},
"home": {
"title": "DSpace Angular :: Home",
"description": "",
"top-level-communities": {
"head": "Communities in DSpace",
"help": "Selecteer een community om diens collecties te verkennen."
}
},
"search": {
"title": "DSpace Angular :: Zoek",
"description": "",
"form": {
"search": "Zoek",
"search_dspace": "Zoek in DSpace"
},
"results": {
"head": "Zoekresultaten",
"no-results": "Er waren geen resultaten voor deze zoekopdracht"
},
"sidebar": {
"close": "Terug naar de resultaten",
"open": "Zoek Tools",
"results": "resultaten",
"filters": {
"title": "Filters"
},
"settings": {
"title": "Instellingen",
"sort-by": "Sorteer volgens",
"rpp": "Resultaten per pagina"
}
},
"view-switch": {
"show-list": "Toon als lijst",
"show-grid": "Toon in raster"
},
"filters": {
"head": "Filters",
"reset": "Filters verwijderen",
"applied": {
"f.author": "Auteur",
"f.dateIssued.min": "Start datum",
"f.dateIssued.max": "Eind datum",
"f.subject": "Sleutelwoord",
"f.has_content_in_original_bundle": "Heeft bestanden"
},
"filter": {
"show-more": "Toon meer",
"show-less": "Inklappen",
"author": {
"placeholder": "Auteursnaam",
"head": "Auteur"
},
"scope": {
"placeholder": "Bereik filter",
"head": "Bereik"
},
"subject": {
"placeholder": "Onderwerp",
"head": "Onderwerp"
},
"dateIssued": {
"max": {
"placeholder": "Vroegste Datum"
},
"min": {
"placeholder": "Laatste Datum"
},
"head": "Datum"
},
"has_content_in_original_bundle": {
"head": "Heeft bestanden"
}
}
}
},
"browse": {
"title": "Verken {{ collection }} volgens {{ field }} {{ value }}"
},
"admin": {
"registries": {
"metadata": {
"title": "DSpace Angular :: Metadata Register",
"head": "Metadata Register",
"description": "Het metadata register omvat de lijst van alle metadata velden die beschikbaar zijn in het systeem. Deze velden kunnen verspreid zijn over verschillende metadata schema's. Het qualified Dublin Core schema (dc) is een verplicht schema en kan niet worden verwijderd.",
"schemas": {
"table": {
"id": "ID",
"namespace": "Naamruimte",
"name": "Naam"
},
"no-items": "Er kunnen geen metadata schema's getoond worden."
}
},
"schema": {
"title": "DSpace Angular :: Metadata Schema Register",
"head": "Metadata Schema",
"description": "Dit is het metadata schema voor \"{{namespace}}\".",
"fields": {
"head": "Schema metadata velden",
"table": {
"field": "Veld",
"scopenote": "Opmerking over bereik"
},
"no-items": "Er kunnen geen metadata velden getoond worden."
}
},
"bitstream-formats": {
"title": "DSpace Angular :: Bitstream Formaat Register",
"head": "Bitstream Formaat Register",
"description": "Deze lijst van Bitstream formaten biedt informatie over de formaten die in deze repository zijn toegelaten en op welke manier ze ondersteund worden. De term Bitstream wordt in DSpace gebruikt om een bestand aan te duiden dat samen met metadata onderdeel uitmaakt van een item. De naam bitstream duidt op het feit dat het bestand achterliggend wordt opgeslaan zonder bestandsextensie.",
"formats": {
"table": {
"name": "Naam",
"mimetype": "MIME Type",
"supportLevel": {
"head": "Ondersteuning",
"0": "Onbekend",
"1": "Gekend",
"2": "Ondersteund"
},
"internal": "intern"
},
"no-items": "Er kunnen geen bitstream formaten getoond worden."
}
}
}
},
"loading": {
"default": "Laden...",
"top-level-communities": "Inladen van de Communities op het hoogste niveau...",
"community": "Community wordt ingeladen...",
"collection": "Collectie wordt ingeladen...",
"sub-collections": "De sub-collecties worden ingeladen...",
"recent-submissions": "Recent toegevoegde items worden ingeladen...",
"item": "Item wordt ingeladen...",
"objects": "Laden...",
"search-results": "Zoekresultaten worden ingeladen...",
"browse-by": "Items worden ingeladen..."
},
"error": {
"default": "Fout",
"top-level-communities": "Fout bij het inladen van communities op het hoogste niveau",
"community": "Fout bij het ophalen van een community",
"collection": "Fout bij het ophalen van een collectie",
"sub-collections": "Fout bij het ophalen van sub-collecties",
"recent-submissions": "Fout bij het ophalen van recent toegevoegde items",
"item": "Fout bij het ophalen van items",
"objects": "Fout bij het ophalen van objecten",
"search-results": "Fout bij het ophalen van zoekresultaten",
"browse-by": "Fout bij het ophalen van items",
"validation": {
"pattern": "Deze invoer is niet toegelaten volgens dit patroon: {{ pattern }}.",
"license": {
"notgranted": "U moet de invoerlicentie goedkeuren om de invoer af te werken. Indien u deze licentie momenteel niet kan of mag goedkeuren, kan u uw werk opslaan en de invoer later afwerken. U kan dit nieuwe item ook verwijderen indien u niet voldoet aan de vereisten van de invoer licentie."
}
}
},
"form": {
"submit": "Verstuur",
"cancel": "Annuleer",
"search": "Zoek",
"remove": "Verwijder",
"first-name": "Voornaam",
"last-name": "Achternaam",
"loading": "Inladen...",
"no-results": "Geen resultaten gevonden",
"no-value": "Geen waarde ingevoerd",
"group-collapse": "Inklappen",
"group-expand": "Uitklappen",
"group-collapse-help": "Klik hier op in te klappen",
"group-expand-help": "Klik hier om uit te klappen en om meer onderdelen toe te voegen"
},
"login": {
"title": "Aanmelden",
"form": {
"header": "Gelieve in te loggen in DSpace",
"email": "Email adres",
"forgot-password": "Bent u uw wachtwoord vergeten?",
"new-user": "Nieuwe gebruiker? Gelieve u hier te registreren",
"password": "Wachtwoord",
"submit": "Aanmelden"
}
},
"logout": {
"title": "Afmelden",
"form": {
"header": "Afmelden in DSpace",
"submit": "Afmelden"
}
},
"auth": {
"messages": {
"expired": "Uw sessie is vervallen. Gelieve opnieuw aan te melden."
},
"errors": {
"invalid-user": "Ongeldig email adres of wachtwoord."
}
}
}

View File

@@ -0,0 +1,11 @@
<div class="container">
<div class="browse-by-author w-100 row">
<ds-browse-by class="col-xs-12 w-100"
title="{{'browse.title' | translate:{collection: '', field: 'Author', value: (value)? '&quot;' + value + '&quot;': ''} }}"
[objects$]="(items$ !== undefined)? items$ : authors$"
[currentUrl]="currentUrl"
[paginationConfig]="paginationConfig"
[sortConfig]="sortConfig">
</ds-browse-by>
</div>
</div>

View File

@@ -0,0 +1,108 @@
import { Component, OnInit } from '@angular/core';
import { RemoteData } from '../../core/data/remote-data';
import { PaginatedList } from '../../core/data/paginated-list';
import { ItemDataService } from '../../core/data/item-data.service';
import { Observable } from 'rxjs/Observable';
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
import { Subscription } from 'rxjs/Subscription';
import { ActivatedRoute } from '@angular/router';
import { hasValue, isNotEmpty } from '../../shared/empty.util';
import { Metadatum } from '../../core/shared/metadatum.model';
import { BrowseService } from '../../core/browse/browse.service';
import { BrowseEntry } from '../../core/shared/browse-entry.model';
import { Item } from '../../core/shared/item.model';
@Component({
selector: 'ds-browse-by-author-page',
styleUrls: ['./browse-by-author-page.component.scss'],
templateUrl: './browse-by-author-page.component.html'
})
/**
* Component for browsing (items) by author (dc.contributor.author)
*/
export class BrowseByAuthorPageComponent implements OnInit {
authors$: Observable<RemoteData<PaginatedList<BrowseEntry>>>;
items$: Observable<RemoteData<PaginatedList<Item>>>;
paginationConfig: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), {
id: 'browse-by-author-pagination',
currentPage: 1,
pageSize: 20
});
sortConfig: SortOptions = new SortOptions('dc.contributor.author', SortDirection.ASC);
subs: Subscription[] = [];
currentUrl: string;
value = '';
public constructor(private itemDataService: ItemDataService, private route: ActivatedRoute, private browseService: BrowseService) {
}
ngOnInit(): void {
this.currentUrl = this.route.snapshot.pathFromRoot
.map((snapshot) => (snapshot.routeConfig) ? snapshot.routeConfig.path : '')
.join('/');
this.updatePage({
pagination: this.paginationConfig,
sort: this.sortConfig
});
this.subs.push(
Observable.combineLatest(
this.route.params,
this.route.queryParams,
(params, queryParams, ) => {
return Object.assign({}, params, queryParams);
})
.subscribe((params) => {
const page = +params.page || this.paginationConfig.currentPage;
const pageSize = +params.pageSize || this.paginationConfig.pageSize;
const sortDirection = params.sortDirection || this.sortConfig.direction;
const sortField = params.sortField || this.sortConfig.field;
this.value = +params.value || params.value || '';
const pagination = Object.assign({},
this.paginationConfig,
{ currentPage: page, pageSize: pageSize }
);
const sort = Object.assign({},
this.sortConfig,
{ direction: sortDirection, field: sortField }
);
const searchOptions = {
pagination: pagination,
sort: sort
};
if (isNotEmpty(this.value)) {
this.updatePageWithItems(searchOptions, this.value);
} else {
this.updatePage(searchOptions);
}
}));
}
/**
* Updates the current page with searchOptions
* @param searchOptions Options to narrow down your search:
* { pagination: PaginationComponentOptions,
* sort: SortOptions }
*/
updatePage(searchOptions) {
this.authors$ = this.browseService.getBrowseEntriesFor('author', searchOptions);
this.items$ = undefined;
}
/**
* Updates the current page with searchOptions and display items linked to author
* @param searchOptions Options to narrow down your search:
* { pagination: PaginationComponentOptions,
* sort: SortOptions }
* @param author The author's name for displaying items
*/
updatePageWithItems(searchOptions, author: string) {
this.items$ = this.browseService.getBrowseItemsFor('author', author, searchOptions);
}
ngOnDestroy(): void {
this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe());
}
}

View File

@@ -0,0 +1,11 @@
<div class="container">
<div class="browse-by-title w-100 row">
<ds-browse-by class="col-xs-12 w-100"
title="{{'browse.title' | translate:{collection: '', field: 'Title', value: ''} }}"
[objects$]="items$"
[currentUrl]="currentUrl"
[paginationConfig]="paginationConfig"
[sortConfig]="sortConfig">
</ds-browse-by>
</div>
</div>

View File

@@ -0,0 +1,92 @@
import { Component, OnInit } from '@angular/core';
import { RemoteData } from '../../core/data/remote-data';
import { DSpaceObject } from '../../core/shared/dspace-object.model';
import { PaginatedList } from '../../core/data/paginated-list';
import { ItemDataService } from '../../core/data/item-data.service';
import { Observable } from 'rxjs/Observable';
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
import { Item } from '../../core/shared/item.model';
import { Subscription } from 'rxjs/Subscription';
import { ActivatedRoute, PRIMARY_OUTLET, UrlSegmentGroup } from '@angular/router';
import { hasValue } from '../../shared/empty.util';
import { Collection } from '../../core/shared/collection.model';
@Component({
selector: 'ds-browse-by-title-page',
styleUrls: ['./browse-by-title-page.component.scss'],
templateUrl: './browse-by-title-page.component.html'
})
/**
* Component for browsing items by title (dc.title)
*/
export class BrowseByTitlePageComponent implements OnInit {
items$: Observable<RemoteData<PaginatedList<Item>>>;
paginationConfig: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), {
id: 'browse-by-title-pagination',
currentPage: 1,
pageSize: 20
});
sortConfig: SortOptions = new SortOptions('dc.title', SortDirection.ASC);
subs: Subscription[] = [];
currentUrl: string;
public constructor(private itemDataService: ItemDataService, private route: ActivatedRoute) {
}
ngOnInit(): void {
this.currentUrl = this.route.snapshot.pathFromRoot
.map((snapshot) => (snapshot.routeConfig) ? snapshot.routeConfig.path : '')
.join('/');
this.updatePage({
pagination: this.paginationConfig,
sort: this.sortConfig
});
this.subs.push(
Observable.combineLatest(
this.route.params,
this.route.queryParams,
(params, queryParams, ) => {
return Object.assign({}, params, queryParams);
})
.subscribe((params) => {
const page = +params.page || this.paginationConfig.currentPage;
const pageSize = +params.pageSize || this.paginationConfig.pageSize;
const sortDirection = params.sortDirection || this.sortConfig.direction;
const sortField = params.sortField || this.sortConfig.field;
const pagination = Object.assign({},
this.paginationConfig,
{ currentPage: page, pageSize: pageSize }
);
const sort = Object.assign({},
this.sortConfig,
{ direction: sortDirection, field: sortField }
);
this.updatePage({
pagination: pagination,
sort: sort
});
}));
}
/**
* Updates the current page with searchOptions
* @param searchOptions Options to narrow down your search:
* { pagination: PaginationComponentOptions,
* sort: SortOptions }
*/
updatePage(searchOptions) {
this.items$ = this.itemDataService.findAll({
currentPage: searchOptions.pagination.currentPage,
elementsPerPage: searchOptions.pagination.pageSize,
sort: searchOptions.sort
});
}
ngOnDestroy(): void {
this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe());
}
}

View File

@@ -0,0 +1,16 @@
import { RouterModule } from '@angular/router';
import { NgModule } from '@angular/core';
import { BrowseByTitlePageComponent } from './+browse-by-title-page/browse-by-title-page.component';
import { BrowseByAuthorPageComponent } from './+browse-by-author-page/browse-by-author-page.component';
@NgModule({
imports: [
RouterModule.forChild([
{ path: 'title', component: BrowseByTitlePageComponent },
{ path: 'author', component: BrowseByAuthorPageComponent }
])
]
})
export class BrowseByRoutingModule {
}

View File

@@ -0,0 +1,27 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { BrowseByTitlePageComponent } from './+browse-by-title-page/browse-by-title-page.component';
import { ItemDataService } from '../core/data/item-data.service';
import { SharedModule } from '../shared/shared.module';
import { BrowseByRoutingModule } from './browse-by-routing.module';
import { BrowseByAuthorPageComponent } from './+browse-by-author-page/browse-by-author-page.component';
import { BrowseService } from '../core/browse/browse.service';
@NgModule({
imports: [
BrowseByRoutingModule,
CommonModule,
SharedModule
],
declarations: [
BrowseByTitlePageComponent,
BrowseByAuthorPageComponent
],
providers: [
ItemDataService,
BrowseService
]
})
export class BrowseByModule {
}

View File

@@ -35,7 +35,7 @@
</ds-comcol-page-content> </ds-comcol-page-content>
</div> </div>
</div> </div>
<ds-error *ngIf="collectionRD?.hasFailed"0 message="{{'error.collection' | translate}}"></ds-error> <ds-error *ngIf="collectionRD?.hasFailed" message="{{'error.collection' | translate}}"></ds-error>
<ds-loading *ngIf="collectionRD?.isLoading" message="{{'loading.collection' | translate}}"></ds-loading> <ds-loading *ngIf="collectionRD?.isLoading" message="{{'loading.collection' | translate}}"></ds-loading>
<br> <br>
<ng-container *ngVar="(itemRD$ | async) as itemRD"> <ng-container *ngVar="(itemRD$ | async) as itemRD">

View File

@@ -6,13 +6,21 @@ import { CollectionDataService } from '../core/data/collection-data.service';
import { RemoteData } from '../core/data/remote-data'; import { RemoteData } from '../core/data/remote-data';
import { getSucceededRemoteData } from '../core/shared/operators'; import { getSucceededRemoteData } from '../core/shared/operators';
/**
* This class represents a resolver that requests a specific collection before the route is activated
*/
@Injectable() @Injectable()
export class CollectionPageResolver implements Resolve<RemoteData<Collection>> { export class CollectionPageResolver implements Resolve<RemoteData<Collection>> {
constructor(private collectionService: CollectionDataService) { constructor(private collectionService: CollectionDataService) {
} }
/**
* Method for resolving a collection based on the parameters in the current route
* @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot
* @param {RouterStateSnapshot} state The current RouterStateSnapshot
* @returns Observable<<RemoteData<Collection>> Emits the found collection based on the parameters in the current route
*/
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<RemoteData<Collection>> { resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<RemoteData<Collection>> {
return this.collectionService.findById(route.params.id).pipe( return this.collectionService.findById(route.params.id).pipe(
getSucceededRemoteData() getSucceededRemoteData()
); );

View File

@@ -6,13 +6,21 @@ import { getSucceededRemoteData } from '../core/shared/operators';
import { Community } from '../core/shared/community.model'; import { Community } from '../core/shared/community.model';
import { CommunityDataService } from '../core/data/community-data.service'; import { CommunityDataService } from '../core/data/community-data.service';
/**
* This class represents a resolver that requests a specific community before the route is activated
*/
@Injectable() @Injectable()
export class CommunityPageResolver implements Resolve<RemoteData<Community>> { export class CommunityPageResolver implements Resolve<RemoteData<Community>> {
constructor(private communityService: CommunityDataService) { constructor(private communityService: CommunityDataService) {
} }
/**
* Method for resolving a community based on the parameters in the current route
* @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot
* @param {RouterStateSnapshot} state The current RouterStateSnapshot
* @returns Observable<<RemoteData<Community>> Emits the found community based on the parameters in the current route
*/
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<RemoteData<Community>> { resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<RemoteData<Community>> {
return this.communityService.findById(route.params.id).pipe( return this.communityService.findById(route.params.id).pipe(
getSucceededRemoteData() getSucceededRemoteData()
); );

View File

@@ -6,11 +6,20 @@ import { getSucceededRemoteData } from '../core/shared/operators';
import { ItemDataService } from '../core/data/item-data.service'; import { ItemDataService } from '../core/data/item-data.service';
import { Item } from '../core/shared/item.model'; import { Item } from '../core/shared/item.model';
/**
* This class represents a resolver that requests a specific item before the route is activated
*/
@Injectable() @Injectable()
export class ItemPageResolver implements Resolve<RemoteData<Item>> { export class ItemPageResolver implements Resolve<RemoteData<Item>> {
constructor(private itemService: ItemDataService) { constructor(private itemService: ItemDataService) {
} }
/**
* Method for resolving an item based on the parameters in the current route
* @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot
* @param {RouterStateSnapshot} state The current RouterStateSnapshot
* @returns Observable<<RemoteData<Item>> Emits the found item based on the parameters in the current route
*/
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<RemoteData<Item>> { resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<RemoteData<Item>> {
return this.itemService.findById(route.params.id).pipe( return this.itemService.findById(route.params.id).pipe(
getSucceededRemoteData() getSucceededRemoteData()

View File

@@ -12,6 +12,7 @@ import { PageNotFoundComponent } from './pagenotfound/pagenotfound.component';
{ path: 'collections', loadChildren: './+collection-page/collection-page.module#CollectionPageModule' }, { path: 'collections', loadChildren: './+collection-page/collection-page.module#CollectionPageModule' },
{ path: 'items', loadChildren: './+item-page/item-page.module#ItemPageModule' }, { path: 'items', loadChildren: './+item-page/item-page.module#ItemPageModule' },
{ path: 'search', loadChildren: './+search-page/search-page.module#SearchPageModule' }, { path: 'search', loadChildren: './+search-page/search-page.module#SearchPageModule' },
{ path: 'browse', loadChildren: './+browse-by/browse-by.module#BrowseByModule' },
{ path: 'admin', loadChildren: './+admin/admin.module#AdminModule' }, { path: 'admin', loadChildren: './+admin/admin.module#AdminModule' },
{ path: 'login', loadChildren: './+login-page/login-page.module#LoginPageModule' }, { path: 'login', loadChildren: './+login-page/login-page.module#LoginPageModule' },
{ path: 'logout', loadChildren: './+logout-page/logout-page.module#LogoutPageModule' }, { path: 'logout', loadChildren: './+logout-page/logout-page.module#LogoutPageModule' },

View File

@@ -1,14 +1,15 @@
import { AuthType } from './auth-type'; import { AuthType } from './auth-type';
import { GenericConstructor } from '../shared/generic-constructor'; import { GenericConstructor } from '../shared/generic-constructor';
import { NormalizedAuthStatus } from './models/normalized-auth-status.model'; import { NormalizedAuthStatus } from './models/normalized-auth-status.model';
import { NormalizedDSpaceObject } from '../cache/models/normalized-dspace-object.model'; import { NormalizedEPerson } from '../eperson/models/normalized-eperson.model';
import { NormalizedEpersonModel } from '../eperson/models/NormalizedEperson.model'; import { NormalizedObject } from '../cache/models/normalized-object.model';
import { EPerson } from '../eperson/models/eperson.model';
export class AuthObjectFactory { export class AuthObjectFactory {
public static getConstructor(type): GenericConstructor<NormalizedDSpaceObject> { public static getConstructor(type): GenericConstructor<NormalizedObject> {
switch (type) { switch (type) {
case AuthType.Eperson: { case AuthType.EPerson: {
return NormalizedEpersonModel return NormalizedEPerson
} }
case AuthType.Status: { case AuthType.Status: {

View File

@@ -8,12 +8,13 @@ import { CoreState } from '../core.reducers';
import { AuthStatus } from './models/auth-status.model'; import { AuthStatus } from './models/auth-status.model';
import { AuthResponseParsingService } from './auth-response-parsing.service'; import { AuthResponseParsingService } from './auth-response-parsing.service';
import { AuthGetRequest, AuthPostRequest } from '../data/request.models'; import { AuthGetRequest, AuthPostRequest } from '../data/request.models';
import { getMockStore } from '../../shared/mocks/mock-store';
describe('ConfigResponseParsingService', () => { describe('AuthResponseParsingService', () => {
let service: AuthResponseParsingService; let service: AuthResponseParsingService;
const EnvConfig = {} as GlobalConfig; const EnvConfig = {cache: {msToLive: 1000}} as GlobalConfig;
const store = {} as Store<CoreState>; const store = getMockStore() as Store<CoreState>;
const objectCacheService = new ObjectCacheService(store); const objectCacheService = new ObjectCacheService(store);
beforeEach(() => { beforeEach(() => {
@@ -86,13 +87,19 @@ describe('ConfigResponseParsingService', () => {
type: 'eperson', type: 'eperson',
uuid: '4dc70ab5-cd73-492f-b007-3179d2d9296b', uuid: '4dc70ab5-cd73-492f-b007-3179d2d9296b',
_links: { _links: {
self: 'https://hasselt-dspace.dev01.4science.it/dspace-spring-rest/api/eperson/epersons/4dc70ab5-cd73-492f-b007-3179d2d9296b' self: {
href: 'https://hasselt-dspace.dev01.4science.it/dspace-spring-rest/api/eperson/epersons/4dc70ab5-cd73-492f-b007-3179d2d9296b'
}
} }
} }
}, },
_links: { _links: {
eperson: 'https://hasselt-dspace.dev01.4science.it/dspace-spring-rest/api/eperson/epersons/4dc70ab5-cd73-492f-b007-3179d2d9296b', eperson: {
self: 'https://hasselt-dspace.dev01.4science.it/dspace-spring-rest/api/authn/status' href: 'https://hasselt-dspace.dev01.4science.it/dspace-spring-rest/api/eperson/epersons/4dc70ab5-cd73-492f-b007-3179d2d9296b'
},
self: {
href: 'https://hasselt-dspace.dev01.4science.it/dspace-spring-rest/api/authn/status'
}
} }
}, },
statusCode: '200' statusCode: '200'

View File

@@ -12,22 +12,23 @@ import { ResponseParsingService } from '../data/parsing.service';
import { RestRequest } from '../data/request.models'; import { RestRequest } from '../data/request.models';
import { AuthType } from './auth-type'; import { AuthType } from './auth-type';
import { AuthStatus } from './models/auth-status.model'; import { AuthStatus } from './models/auth-status.model';
import { NormalizedAuthStatus } from './models/normalized-auth-status.model';
@Injectable() @Injectable()
export class AuthResponseParsingService extends BaseResponseParsingService implements ResponseParsingService { export class AuthResponseParsingService extends BaseResponseParsingService implements ResponseParsingService {
protected objectFactory = AuthObjectFactory; protected objectFactory = AuthObjectFactory;
protected toCache = false; protected toCache = true;
constructor(@Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig, constructor(@Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig,
protected objectCache: ObjectCacheService,) { protected objectCache: ObjectCacheService) {
super(); super();
} }
parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse { parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse {
if (isNotEmpty(data.payload) && isNotEmpty(data.payload._links) && (data.statusCode === '200' || data.statusCode === 'OK')) { if (isNotEmpty(data.payload) && isNotEmpty(data.payload._links) && (data.statusCode === '200' || data.statusCode === 'OK')) {
const response = this.process<AuthStatus, AuthType>(data.payload, request.href); const response = this.process<NormalizedAuthStatus, AuthType>(data.payload, request.href);
return new AuthStatusResponse(response[Object.keys(response)[0]][0], data.statusCode); return new AuthStatusResponse(response, data.statusCode);
} else { } else {
return new AuthStatusResponse(data.payload as AuthStatus, data.statusCode); return new AuthStatusResponse(data.payload as AuthStatus, data.statusCode);
} }

View File

@@ -1,4 +1,4 @@
export enum AuthType { export enum AuthType {
Eperson = 'eperson', EPerson = 'eperson',
Status = 'status' Status = 'status'
} }

View File

@@ -5,7 +5,7 @@ import { Action } from '@ngrx/store';
import { type } from '../../shared/ngrx/type'; import { type } from '../../shared/ngrx/type';
// import models // import models
import { Eperson } from '../eperson/models/eperson.model'; import { EPerson } from '../eperson/models/eperson.model';
import { AuthTokenInfo } from './models/auth-token-info.model'; import { AuthTokenInfo } from './models/auth-token-info.model';
export const AuthActionTypes = { export const AuthActionTypes = {
@@ -76,10 +76,10 @@ export class AuthenticatedSuccessAction implements Action {
payload: { payload: {
authenticated: boolean; authenticated: boolean;
authToken: AuthTokenInfo; authToken: AuthTokenInfo;
user: Eperson user: EPerson
}; };
constructor(authenticated: boolean, authToken: AuthTokenInfo, user: Eperson) { constructor(authenticated: boolean, authToken: AuthTokenInfo, user: EPerson) {
this.payload = { authenticated, authToken, user }; this.payload = { authenticated, authToken, user };
} }
} }
@@ -250,9 +250,9 @@ export class RefreshTokenErrorAction implements Action {
*/ */
export class RegistrationAction implements Action { export class RegistrationAction implements Action {
public type: string = AuthActionTypes.REGISTRATION; public type: string = AuthActionTypes.REGISTRATION;
payload: Eperson; payload: EPerson;
constructor(user: Eperson) { constructor(user: EPerson) {
this.payload = user; this.payload = user;
} }
} }
@@ -278,9 +278,9 @@ export class RegistrationErrorAction implements Action {
*/ */
export class RegistrationSuccessAction implements Action { export class RegistrationSuccessAction implements Action {
public type: string = AuthActionTypes.REGISTRATION_SUCCESS; public type: string = AuthActionTypes.REGISTRATION_SUCCESS;
payload: Eperson; payload: EPerson;
constructor(user: Eperson) { constructor(user: EPerson) {
this.payload = user; this.payload = user;
} }
} }

View File

@@ -25,12 +25,11 @@ import { AuthServiceStub } from '../../shared/testing/auth-service-stub';
import { AuthService } from './auth.service'; import { AuthService } from './auth.service';
import { TruncatablesState } from '../../shared/truncatable/truncatable.reducer'; import { TruncatablesState } from '../../shared/truncatable/truncatable.reducer';
import { EpersonMock } from '../../shared/testing/eperson-mock'; import { EPersonMock } from '../../shared/testing/eperson-mock';
describe('AuthEffects', () => { describe('AuthEffects', () => {
let authEffects: AuthEffects; let authEffects: AuthEffects;
let actions: Observable<any>; let actions: Observable<any>;
const authServiceStub = new AuthServiceStub(); const authServiceStub = new AuthServiceStub();
const store: Store<TruncatablesState> = jasmine.createSpyObj('store', { const store: Store<TruncatablesState> = jasmine.createSpyObj('store', {
/* tslint:disable:no-empty */ /* tslint:disable:no-empty */
@@ -105,7 +104,7 @@ describe('AuthEffects', () => {
it('should return a AUTHENTICATED_SUCCESS action in response to a AUTHENTICATED action', () => { it('should return a AUTHENTICATED_SUCCESS action in response to a AUTHENTICATED action', () => {
actions = hot('--a-', {a: {type: AuthActionTypes.AUTHENTICATED, payload: token}}); actions = hot('--a-', {a: {type: AuthActionTypes.AUTHENTICATED, payload: token}});
const expected = cold('--b-', {b: new AuthenticatedSuccessAction(true, token, EpersonMock)}); const expected = cold('--b-', {b: new AuthenticatedSuccessAction(true, token, EPersonMock)});
expect(authEffects.authenticated$).toBeObservable(expected); expect(authEffects.authenticated$).toBeObservable(expected);
}); });

View File

@@ -28,7 +28,7 @@ import {
RegistrationErrorAction, RegistrationErrorAction,
RegistrationSuccessAction RegistrationSuccessAction
} from './auth.actions'; } from './auth.actions';
import { Eperson } from '../eperson/models/eperson.model'; import { EPerson } from '../eperson/models/eperson.model';
import { AuthStatus } from './models/auth-status.model'; import { AuthStatus } from './models/auth-status.model';
import { AuthTokenInfo } from './models/auth-token-info.model'; import { AuthTokenInfo } from './models/auth-token-info.model';
import { AppState } from '../../app.reducer'; import { AppState } from '../../app.reducer';
@@ -66,7 +66,7 @@ export class AuthEffects {
ofType(AuthActionTypes.AUTHENTICATED), ofType(AuthActionTypes.AUTHENTICATED),
switchMap((action: AuthenticatedAction) => { switchMap((action: AuthenticatedAction) => {
return this.authService.authenticatedUser(action.payload).pipe( return this.authService.authenticatedUser(action.payload).pipe(
map((user: Eperson) => new AuthenticatedSuccessAction((user !== null), action.payload, user)), map((user: EPerson) => new AuthenticatedSuccessAction((user !== null), action.payload, user)),
catchError((error) => observableOf(new AuthenticatedErrorAction(error))),); catchError((error) => observableOf(new AuthenticatedErrorAction(error))),);
}) })
); );
@@ -94,7 +94,7 @@ export class AuthEffects {
debounceTime(500), // to remove when functionality is implemented debounceTime(500), // to remove when functionality is implemented
switchMap((action: RegistrationAction) => { switchMap((action: RegistrationAction) => {
return this.authService.create(action.payload).pipe( return this.authService.create(action.payload).pipe(
map((user: Eperson) => new RegistrationSuccessAction(user)), map((user: EPerson) => new RegistrationSuccessAction(user)),
catchError((error) => observableOf(new RegistrationErrorAction(error))) catchError((error) => observableOf(new RegistrationErrorAction(error)))
); );
}) })

View File

@@ -42,7 +42,7 @@ export class AuthInterceptor implements HttpInterceptor {
} }
private isSuccess(response: HttpResponseBase): boolean { private isSuccess(response: HttpResponseBase): boolean {
return response.status === 200; return (response.status === 200 || response.status === 204);
} }
private isAuthRequest(http: HttpRequest<any> | HttpResponseBase): boolean { private isAuthRequest(http: HttpRequest<any> | HttpResponseBase): boolean {

View File

@@ -21,7 +21,7 @@ import {
SetRedirectUrlAction SetRedirectUrlAction
} from './auth.actions'; } from './auth.actions';
import { AuthTokenInfo } from './models/auth-token-info.model'; import { AuthTokenInfo } from './models/auth-token-info.model';
import { EpersonMock } from '../../shared/testing/eperson-mock'; import { EPersonMock } from '../../shared/testing/eperson-mock';
describe('authReducer', () => { describe('authReducer', () => {
@@ -107,7 +107,7 @@ describe('authReducer', () => {
loading: true, loading: true,
info: undefined info: undefined
}; };
const action = new AuthenticatedSuccessAction(true, mockTokenInfo, EpersonMock); const action = new AuthenticatedSuccessAction(true, mockTokenInfo, EPersonMock);
const newState = authReducer(initialState, action); const newState = authReducer(initialState, action);
state = { state = {
authenticated: true, authenticated: true,
@@ -116,7 +116,7 @@ describe('authReducer', () => {
error: undefined, error: undefined,
loading: false, loading: false,
info: undefined, info: undefined,
user: EpersonMock user: EPersonMock
}; };
expect(newState).toEqual(state); expect(newState).toEqual(state);
}); });
@@ -182,7 +182,7 @@ describe('authReducer', () => {
error: undefined, error: undefined,
loading: false, loading: false,
info: undefined, info: undefined,
user: EpersonMock user: EPersonMock
}; };
const action = new LogOutAction(); const action = new LogOutAction();
@@ -199,7 +199,7 @@ describe('authReducer', () => {
error: undefined, error: undefined,
loading: false, loading: false,
info: undefined, info: undefined,
user: EpersonMock user: EPersonMock
}; };
const action = new LogOutSuccessAction(); const action = new LogOutSuccessAction();
@@ -225,7 +225,7 @@ describe('authReducer', () => {
error: undefined, error: undefined,
loading: false, loading: false,
info: undefined, info: undefined,
user: EpersonMock user: EPersonMock
}; };
const action = new LogOutErrorAction(mockError); const action = new LogOutErrorAction(mockError);
@@ -237,7 +237,7 @@ describe('authReducer', () => {
error: 'Test error message', error: 'Test error message',
loading: false, loading: false,
info: undefined, info: undefined,
user: EpersonMock user: EPersonMock
}; };
expect(newState).toEqual(state); expect(newState).toEqual(state);
}); });
@@ -250,7 +250,7 @@ describe('authReducer', () => {
error: undefined, error: undefined,
loading: false, loading: false,
info: undefined, info: undefined,
user: EpersonMock user: EPersonMock
}; };
const newTokenInfo = new AuthTokenInfo('Refreshed token'); const newTokenInfo = new AuthTokenInfo('Refreshed token');
const action = new RefreshTokenAction(newTokenInfo); const action = new RefreshTokenAction(newTokenInfo);
@@ -262,7 +262,7 @@ describe('authReducer', () => {
error: undefined, error: undefined,
loading: false, loading: false,
info: undefined, info: undefined,
user: EpersonMock, user: EPersonMock,
refreshing: true refreshing: true
}; };
expect(newState).toEqual(state); expect(newState).toEqual(state);
@@ -276,7 +276,7 @@ describe('authReducer', () => {
error: undefined, error: undefined,
loading: false, loading: false,
info: undefined, info: undefined,
user: EpersonMock, user: EPersonMock,
refreshing: true refreshing: true
}; };
const newTokenInfo = new AuthTokenInfo('Refreshed token'); const newTokenInfo = new AuthTokenInfo('Refreshed token');
@@ -289,7 +289,7 @@ describe('authReducer', () => {
error: undefined, error: undefined,
loading: false, loading: false,
info: undefined, info: undefined,
user: EpersonMock, user: EPersonMock,
refreshing: false refreshing: false
}; };
expect(newState).toEqual(state); expect(newState).toEqual(state);
@@ -303,7 +303,7 @@ describe('authReducer', () => {
error: undefined, error: undefined,
loading: false, loading: false,
info: undefined, info: undefined,
user: EpersonMock, user: EPersonMock,
refreshing: true refreshing: true
}; };
const action = new RefreshTokenErrorAction(); const action = new RefreshTokenErrorAction();
@@ -329,7 +329,7 @@ describe('authReducer', () => {
error: undefined, error: undefined,
loading: false, loading: false,
info: undefined, info: undefined,
user: EpersonMock user: EPersonMock
}; };
state = { state = {

View File

@@ -12,7 +12,7 @@ import {
SetRedirectUrlAction SetRedirectUrlAction
} from './auth.actions'; } from './auth.actions';
// import models // import models
import { Eperson } from '../eperson/models/eperson.model'; import { EPerson } from '../eperson/models/eperson.model';
import { AuthTokenInfo } from './models/auth-token-info.model'; import { AuthTokenInfo } from './models/auth-token-info.model';
/** /**
@@ -46,7 +46,7 @@ export interface AuthState {
refreshing?: boolean; refreshing?: boolean;
// the authenticated user // the authenticated user
user?: Eperson; user?: EPerson;
} }
/** /**

View File

@@ -17,10 +17,12 @@ import { AuthRequestServiceStub } from '../../shared/testing/auth-request-servic
import { AuthRequestService } from './auth-request.service'; import { AuthRequestService } from './auth-request.service';
import { AuthStatus } from './models/auth-status.model'; import { AuthStatus } from './models/auth-status.model';
import { AuthTokenInfo } from './models/auth-token-info.model'; import { AuthTokenInfo } from './models/auth-token-info.model';
import { Eperson } from '../eperson/models/eperson.model'; import { EPerson } from '../eperson/models/eperson.model';
import { EpersonMock } from '../../shared/testing/eperson-mock'; import { EPersonMock } from '../../shared/testing/eperson-mock';
import { AppState } from '../../app.reducer'; import { AppState } from '../../app.reducer';
import { ClientCookieService } from '../../shared/services/client-cookie.service'; import { ClientCookieService } from '../../shared/services/client-cookie.service';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
import { getMockRemoteDataBuildService } from '../../shared/mocks/mock-remote-data-build.service';
describe('AuthService test', () => { describe('AuthService test', () => {
@@ -41,9 +43,9 @@ describe('AuthService test', () => {
loaded: true, loaded: true,
loading: false, loading: false,
authToken: token, authToken: token,
user: EpersonMock user: EPersonMock
}; };
const rdbService = getMockRemoteDataBuildService();
describe('', () => { describe('', () => {
beforeEach(() => { beforeEach(() => {
@@ -60,6 +62,7 @@ describe('AuthService test', () => {
{provide: Router, useValue: routerStub}, {provide: Router, useValue: routerStub},
{provide: ActivatedRoute, useValue: routeStub}, {provide: ActivatedRoute, useValue: routeStub},
{provide: Store, useValue: mockStore}, {provide: Store, useValue: mockStore},
{provide: RemoteDataBuildService, useValue: rdbService},
CookieService, CookieService,
AuthService AuthService
], ],
@@ -78,7 +81,7 @@ describe('AuthService test', () => {
}); });
it('should return the authenticated user object when user token is valid', () => { it('should return the authenticated user object when user token is valid', () => {
authService.authenticatedUser(new AuthTokenInfo('test_token')).subscribe((user: Eperson) => { authService.authenticatedUser(new AuthTokenInfo('test_token')).subscribe((user: EPerson) => {
expect(user).toBeDefined(); expect(user).toBeDefined();
}); });
}); });
@@ -120,6 +123,7 @@ describe('AuthService test', () => {
{provide: AuthRequestService, useValue: authRequest}, {provide: AuthRequestService, useValue: authRequest},
{provide: REQUEST, useValue: {}}, {provide: REQUEST, useValue: {}},
{provide: Router, useValue: routerStub}, {provide: Router, useValue: routerStub},
{provide: RemoteDataBuildService, useValue: rdbService},
CookieService CookieService
] ]
}).compileComponents(); }).compileComponents();
@@ -131,7 +135,7 @@ describe('AuthService test', () => {
(state as any).core = Object.create({}); (state as any).core = Object.create({});
(state as any).core.auth = authenticatedState; (state as any).core.auth = authenticatedState;
}); });
authService = new AuthService({}, window, authReqService, router, cookieService, store); authService = new AuthService({}, window, authReqService, router, cookieService, store, rdbService);
})); }));
it('should return true when user is logged in', () => { it('should return true when user is logged in', () => {
@@ -183,14 +187,14 @@ describe('AuthService test', () => {
loaded: true, loaded: true,
loading: false, loading: false,
authToken: expiredToken, authToken: expiredToken,
user: EpersonMock user: EPersonMock
}; };
store store
.subscribe((state) => { .subscribe((state) => {
(state as any).core = Object.create({}); (state as any).core = Object.create({});
(state as any).core.auth = authenticatedState; (state as any).core.auth = authenticatedState;
}); });
authService = new AuthService({}, window, authReqService, router, cookieService, store); authService = new AuthService({}, window, authReqService, router, cookieService, store, rdbService);
storage = (authService as any).storage; storage = (authService as any).storage;
spyOn(storage, 'get'); spyOn(storage, 'get');
spyOn(storage, 'remove'); spyOn(storage, 'remove');

View File

@@ -16,8 +16,10 @@ import { REQUEST } from '@nguniversal/express-engine/tokens';
import { RouterReducerState } from '@ngrx/router-store'; import { RouterReducerState } from '@ngrx/router-store';
import { select, Store } from '@ngrx/store'; import { select, Store } from '@ngrx/store';
import { CookieAttributes } from 'js-cookie'; import { CookieAttributes } from 'js-cookie';
import { Observable } from 'rxjs/Observable';
import { map, switchMap, withLatestFrom } from 'rxjs/operators';
import { Eperson } from '../eperson/models/eperson.model'; import { EPerson } from '../eperson/models/eperson.model';
import { AuthRequestService } from './auth-request.service'; import { AuthRequestService } from './auth-request.service';
import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service'; import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service';
@@ -35,6 +37,8 @@ import { AppState, routerStateSelector } from '../../app.reducer';
import { ResetAuthenticationMessagesAction, SetRedirectUrlAction } from './auth.actions'; import { ResetAuthenticationMessagesAction, SetRedirectUrlAction } from './auth.actions';
import { NativeWindowRef, NativeWindowService } from '../../shared/services/window.service'; import { NativeWindowRef, NativeWindowService } from '../../shared/services/window.service';
import { Base64EncodeUrl } from '../../shared/utils/encode-decode.util'; import { Base64EncodeUrl } from '../../shared/utils/encode-decode.util';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
import { NormalizedEPerson } from '../eperson/models/normalized-eperson.model';
export const LOGIN_ROUTE = '/login'; export const LOGIN_ROUTE = '/login';
export const LOGOUT_ROUTE = '/logout'; export const LOGOUT_ROUTE = '/logout';
@@ -58,7 +62,9 @@ export class AuthService {
protected authRequestService: AuthRequestService, protected authRequestService: AuthRequestService,
protected router: Router, protected router: Router,
protected storage: CookieService, protected storage: CookieService,
protected store: Store<AppState>) { protected store: Store<AppState>,
protected rdbService: RemoteDataBuildService
) {
this.store.pipe( this.store.pipe(
select(isAuthenticated), select(isAuthenticated),
startWith(false) startWith(false)
@@ -132,7 +138,7 @@ export class AuthService {
* Returns the authenticated user * Returns the authenticated user
* @returns {User} * @returns {User}
*/ */
public authenticatedUser(token: AuthTokenInfo): Observable<Eperson> { public authenticatedUser(token: AuthTokenInfo): Observable<EPerson> {
// Determine if the user has an existing auth session on the server // Determine if the user has an existing auth session on the server
const options: HttpOptions = Object.create({}); const options: HttpOptions = Object.create({});
let headers = new HttpHeaders(); let headers = new HttpHeaders();
@@ -140,13 +146,18 @@ export class AuthService {
headers = headers.append('Authorization', `Bearer ${token.accessToken}`); headers = headers.append('Authorization', `Bearer ${token.accessToken}`);
options.headers = headers; options.headers = headers;
return this.authRequestService.getRequest('status', options).pipe( return this.authRequestService.getRequest('status', options).pipe(
map((status: AuthStatus) => { switchMap((status: AuthStatus) => {
if (status.authenticated) { if (status.authenticated) {
return status.eperson[0]; // TODO this should be cleaned up, AuthStatus could be parsed by the RemoteDataService as a whole...
// Review when https://jira.duraspace.org/browse/DS-4006 is fixed
// See https://github.com/DSpace/dspace-angular/issues/292
const person$ = this.rdbService.buildSingle<NormalizedEPerson, EPerson>(status.eperson.toString());
return person$.pipe(map((eperson) => eperson.payload));
} else { } else {
throw(new Error('Not authenticated')); throw(new Error('Not authenticated'));
} }
})); }))
} }
/** /**
@@ -206,7 +217,7 @@ export class AuthService {
* Create a new user * Create a new user
* @returns {User} * @returns {User}
*/ */
public create(user: Eperson): Observable<Eperson> { public create(user: EPerson): Observable<EPerson> {
// Normally you would do an HTTP request to POST the user // Normally you would do an HTTP request to POST the user
// details and then return the new user object // details and then return the new user object
// but, let's just return the new user for this example. // but, let's just return the new user for this example.
@@ -357,8 +368,12 @@ export class AuthService {
this.router.navigated = false; this.router.navigated = false;
const url = decodeURIComponent(redirectUrl); const url = decodeURIComponent(redirectUrl);
this.router.navigateByUrl(url); this.router.navigateByUrl(url);
/* TODO Reenable hard redirect when REST API can handle x-forwarded-for, see https://github.com/DSpace/DSpace/pull/2207 */
// this._window.nativeWindow.location.href = url;
} else { } else {
this.router.navigate(['/']); this.router.navigate(['/']);
/* TODO Reenable hard redirect when REST API can handle x-forwarded-for, see https://github.com/DSpace/DSpace/pull/2207 */
// this._window.nativeWindow.location.href = '/';
} }
}) })

View File

@@ -1,7 +1,8 @@
import { AuthError } from './auth-error.model'; import { AuthError } from './auth-error.model';
import { AuthTokenInfo } from './auth-token-info.model'; import { AuthTokenInfo } from './auth-token-info.model';
import { DSpaceObject } from '../../shared/dspace-object.model'; import { EPerson } from '../../eperson/models/eperson.model';
import { Eperson } from '../../eperson/models/eperson.model'; import { RemoteData } from '../../data/remote-data';
import { Observable } from 'rxjs/Observable';
export class AuthStatus { export class AuthStatus {
@@ -13,7 +14,7 @@ export class AuthStatus {
error?: AuthError; error?: AuthError;
eperson: Eperson[]; eperson: Observable<RemoteData<EPerson>>;
token?: AuthTokenInfo; token?: AuthTokenInfo;

View File

@@ -1,12 +1,18 @@
import { AuthStatus } from './auth-status.model'; import { AuthStatus } from './auth-status.model';
import { autoserialize, autoserializeAs, inheritSerialization } from 'cerialize'; import { autoserialize, autoserializeAs, inheritSerialization } from 'cerialize';
import { mapsTo } from '../../cache/builders/build-decorators'; import { mapsTo, relationship } from '../../cache/builders/build-decorators';
import { NormalizedDSpaceObject } from '../../cache/models/normalized-dspace-object.model'; import { ResourceType } from '../../shared/resource-type';
import { Eperson } from '../../eperson/models/eperson.model'; import { NormalizedObject } from '../../cache/models/normalized-object.model';
import { IDToUUIDSerializer } from '../../cache/id-to-uuid-serializer';
@mapsTo(AuthStatus) @mapsTo(AuthStatus)
@inheritSerialization(NormalizedDSpaceObject) @inheritSerialization(NormalizedObject)
export class NormalizedAuthStatus extends NormalizedDSpaceObject { export class NormalizedAuthStatus extends NormalizedObject {
@autoserialize
id: string;
@autoserializeAs(new IDToUUIDSerializer('auth-status'), 'id')
uuid: string;
/** /**
* True if REST API is up and running, should never return false * True if REST API is up and running, should never return false
@@ -20,7 +26,7 @@ export class NormalizedAuthStatus extends NormalizedDSpaceObject {
@autoserialize @autoserialize
authenticated: boolean; authenticated: boolean;
@autoserializeAs(Eperson) @relationship(ResourceType.EPerson, false)
eperson: Eperson[]; @autoserialize
eperson: string;
} }

View File

@@ -1,5 +1,4 @@
import { first, map, switchMap } from 'rxjs/operators';
import {first, map} from 'rxjs/operators';
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
@@ -10,7 +9,8 @@ import { isNotEmpty } from '../../shared/empty.util';
import { AuthService } from './auth.service'; import { AuthService } from './auth.service';
import { AuthTokenInfo } from './models/auth-token-info.model'; import { AuthTokenInfo } from './models/auth-token-info.model';
import { CheckAuthenticationTokenAction } from './auth.actions'; import { CheckAuthenticationTokenAction } from './auth.actions';
import { Eperson } from '../eperson/models/eperson.model'; import { EPerson } from '../eperson/models/eperson.model';
import { NormalizedEPerson } from '../eperson/models/normalized-eperson.model';
/** /**
* The auth service. * The auth service.
@@ -22,7 +22,7 @@ export class ServerAuthService extends AuthService {
* Returns the authenticated user * Returns the authenticated user
* @returns {User} * @returns {User}
*/ */
public authenticatedUser(token: AuthTokenInfo): Observable<Eperson> { public authenticatedUser(token: AuthTokenInfo): Observable<EPerson> {
// Determine if the user has an existing auth session on the server // Determine if the user has an existing auth session on the server
const options: HttpOptions = Object.create({}); const options: HttpOptions = Object.create({});
let headers = new HttpHeaders(); let headers = new HttpHeaders();
@@ -35,13 +35,18 @@ export class ServerAuthService extends AuthService {
options.headers = headers; options.headers = headers;
return this.authRequestService.getRequest('status', options).pipe( return this.authRequestService.getRequest('status', options).pipe(
map((status: AuthStatus) => { switchMap((status: AuthStatus) => {
if (status.authenticated) { if (status.authenticated) {
return status.eperson[0];
// TODO this should be cleaned up, AuthStatus could be parsed by the RemoteDataService as a whole...
const person$ = this.rdbService.buildSingle<NormalizedEPerson, EPerson>(status.eperson.toString());
// person$.subscribe(() => console.log('test'));
return person$.pipe(map((eperson) => eperson.payload));
} else { } else {
throw(new Error('Not authenticated')); throw(new Error('Not authenticated'));
} }
})); }))
} }
/** /**

View File

@@ -6,7 +6,7 @@ import { getMockResponseCacheService } from '../../shared/mocks/mock-response-ca
import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service-stub'; import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service-stub';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
import { ResponseCacheService } from '../cache/response-cache.service'; import { ResponseCacheService } from '../cache/response-cache.service';
import { BrowseEndpointRequest, BrowseEntriesRequest } from '../data/request.models'; import { BrowseEndpointRequest, BrowseEntriesRequest, BrowseItemsRequest } from '../data/request.models';
import { RequestService } from '../data/request.service'; import { RequestService } from '../data/request.service';
import { BrowseDefinition } from '../shared/browse-definition.model'; import { BrowseDefinition } from '../shared/browse-definition.model';
import { BrowseService } from './browse.service'; import { BrowseService } from './browse.service';
@@ -143,7 +143,9 @@ describe('BrowseService', () => {
}); });
describe('getBrowseEntriesFor', () => { describe('getBrowseEntriesFor and getBrowseItemsFor', () => {
const mockAuthorName = 'Donald Smith';
beforeEach(() => { beforeEach(() => {
responseCache = initMockResponseCacheService(true); responseCache = initMockResponseCacheService(true);
requestService = getMockRequestService(); requestService = getMockRequestService();
@@ -156,7 +158,7 @@ describe('BrowseService', () => {
spyOn(rdbService, 'toRemoteDataObservable').and.callThrough(); spyOn(rdbService, 'toRemoteDataObservable').and.callThrough();
}); });
describe('when called with a valid browse definition id', () => { describe('when getBrowseEntriesFor is called with a valid browse definition id', () => {
it('should configure a new BrowseEntriesRequest', () => { it('should configure a new BrowseEntriesRequest', () => {
const expected = new BrowseEntriesRequest(requestService.generateRequestId(), browseDefinitions[1]._links.entries); const expected = new BrowseEntriesRequest(requestService.generateRequestId(), browseDefinitions[1]._links.entries);
@@ -175,7 +177,26 @@ describe('BrowseService', () => {
}); });
describe('when called with an invalid browse definition id', () => { describe('when getBrowseItemsFor is called with a valid browse definition id', () => {
it('should configure a new BrowseItemsRequest', () => {
const expected = new BrowseItemsRequest(requestService.generateRequestId(), browseDefinitions[1]._links.items + '?filterValue=' + mockAuthorName);
scheduler.schedule(() => service.getBrowseItemsFor(browseDefinitions[1].id, mockAuthorName).subscribe());
scheduler.flush();
expect(requestService.configure).toHaveBeenCalledWith(expected);
});
it('should call RemoteDataBuildService to create the RemoteData Observable', () => {
service.getBrowseItemsFor(browseDefinitions[1].id, mockAuthorName);
expect(rdbService.toRemoteDataObservable).toHaveBeenCalled();
});
});
describe('when getBrowseEntriesFor is called with an invalid browse definition id', () => {
it('should throw an Error', () => { it('should throw an Error', () => {
const definitionID = 'invalidID'; const definitionID = 'invalidID';
@@ -184,6 +205,16 @@ describe('BrowseService', () => {
expect(service.getBrowseEntriesFor(definitionID)).toBeObservable(expected); expect(service.getBrowseEntriesFor(definitionID)).toBeObservable(expected);
}); });
}); });
describe('when getBrowseItemsFor is called with an invalid browse definition id', () => {
it('should throw an Error', () => {
const definitionID = 'invalidID';
const expected = cold('--#-', undefined, new Error(`No metadata browse definition could be found for id '${definitionID}'`))
expect(service.getBrowseItemsFor(definitionID, mockAuthorName)).toBeObservable(expected);
});
});
}); });
describe('getBrowseURLFor', () => { describe('getBrowseURLFor', () => {

View File

@@ -16,19 +16,27 @@ import { ResponseCacheEntry } from '../cache/response-cache.reducer';
import { ResponseCacheService } from '../cache/response-cache.service'; import { ResponseCacheService } from '../cache/response-cache.service';
import { PaginatedList } from '../data/paginated-list'; import { PaginatedList } from '../data/paginated-list';
import { RemoteData } from '../data/remote-data'; import { RemoteData } from '../data/remote-data';
import { BrowseEndpointRequest, BrowseEntriesRequest, RestRequest } from '../data/request.models'; import {
BrowseEndpointRequest,
BrowseEntriesRequest,
BrowseItemsRequest,
GetRequest,
RestRequest
} from '../data/request.models';
import { RequestService } from '../data/request.service'; import { RequestService } from '../data/request.service';
import { BrowseDefinition } from '../shared/browse-definition.model'; import { BrowseDefinition } from '../shared/browse-definition.model';
import { BrowseEntry } from '../shared/browse-entry.model'; import { BrowseEntry } from '../shared/browse-entry.model';
import { HALEndpointService } from '../shared/hal-endpoint.service'; import { HALEndpointService } from '../shared/hal-endpoint.service';
import { import {
configureRequest, configureRequest,
filterSuccessfulResponses, filterSuccessfulResponses, getBrowseDefinitionLinks,
getRemoteDataPayload, getRemoteDataPayload,
getRequestFromSelflink, getRequestFromSelflink,
getResponseFromSelflink getResponseFromSelflink
} from '../shared/operators'; } from '../shared/operators';
import { URLCombiner } from '../url-combiner/url-combiner'; import { URLCombiner } from '../url-combiner/url-combiner';
import { Item } from '../shared/item.model';
import { DSpaceObject } from '../shared/dspace-object.model';
@Injectable() @Injectable()
export class BrowseService { export class BrowseService {
@@ -71,6 +79,8 @@ export class BrowseService {
map((entry: ResponseCacheEntry) => entry.response), map((entry: ResponseCacheEntry) => entry.response),
map((response: GenericSuccessResponse<BrowseDefinition[]>) => response.payload), map((response: GenericSuccessResponse<BrowseDefinition[]>) => response.payload),
ensureArrayHasValue(), ensureArrayHasValue(),
map((definitions: BrowseDefinition[]) => definitions
.map((definition: BrowseDefinition) => Object.assign(new BrowseDefinition(), definition))),
distinctUntilChanged() distinctUntilChanged()
); );
@@ -82,17 +92,7 @@ export class BrowseService {
sort?: SortOptions; sort?: SortOptions;
} = {}): Observable<RemoteData<PaginatedList<BrowseEntry>>> { } = {}): Observable<RemoteData<PaginatedList<BrowseEntry>>> {
const request$ = this.getBrowseDefinitions().pipe( const request$ = this.getBrowseDefinitions().pipe(
getRemoteDataPayload(), getBrowseDefinitionLinks(definitionID),
map((browseDefinitions: BrowseDefinition[]) => browseDefinitions
.find((def: BrowseDefinition) => def.id === definitionID && def.metadataBrowse === true)
),
map((def: BrowseDefinition) => {
if (isNotEmpty(def)) {
return def._links;
} else {
throw new Error(`No metadata browse definition could be found for id '${definitionID}'`);
}
}),
hasValueOperator(), hasValueOperator(),
map((_links: any) => _links.entries), map((_links: any) => _links.entries),
hasValueOperator(), hasValueOperator(),
@@ -124,6 +124,66 @@ export class BrowseService {
filterSuccessfulResponses(), filterSuccessfulResponses(),
map((entry: ResponseCacheEntry) => entry.response), map((entry: ResponseCacheEntry) => entry.response),
map((response: GenericSuccessResponse<BrowseEntry[]>) => new PaginatedList(response.pageInfo, response.payload)), map((response: GenericSuccessResponse<BrowseEntry[]>) => new PaginatedList(response.pageInfo, response.payload)),
map((list: PaginatedList<BrowseEntry>) => Object.assign(list, {
page: list.page ? list.page.map((entry: BrowseEntry) => Object.assign(new BrowseEntry(), entry)) : list.page
})),
distinctUntilChanged()
);
return this.rdb.toRemoteDataObservable(requestEntry$, responseCache$, payload$);
}
/**
* Get all items linked to a certain metadata value
* @param {string} definitionID definition ID to define the metadata-field (e.g. author)
* @param {string} filterValue metadata value to filter by (e.g. author's name)
* @param options Options to narrow down your search:
* { pagination: PaginationComponentOptions,
* sort: SortOptions }
* @returns {Observable<RemoteData<PaginatedList<Item>>>}
*/
getBrowseItemsFor(definitionID: string, filterValue: string, options: {
pagination?: PaginationComponentOptions;
sort?: SortOptions;
} = {}): Observable<RemoteData<PaginatedList<Item>>> {
const request$ = this.getBrowseDefinitions().pipe(
getBrowseDefinitionLinks(definitionID),
hasValueOperator(),
map((_links: any) => _links.items),
hasValueOperator(),
map((href: string) => {
const args = [];
if (isNotEmpty(options.sort)) {
args.push(`sort=${options.sort.field},${options.sort.direction}`);
}
if (isNotEmpty(options.pagination)) {
args.push(`page=${options.pagination.currentPage - 1}`);
args.push(`size=${options.pagination.pageSize}`);
}
if (isNotEmpty(filterValue)) {
args.push(`filterValue=${filterValue}`);
}
if (isNotEmpty(args)) {
href = new URLCombiner(href, `?${args.join('&')}`).toString();
}
return href;
}),
map((endpointURL: string) => new BrowseItemsRequest(this.requestService.generateRequestId(), endpointURL)),
configureRequest(this.requestService)
);
const href$ = request$.pipe(map((request: RestRequest) => request.href));
const requestEntry$ = href$.pipe(getRequestFromSelflink(this.requestService));
const responseCache$ = href$.pipe(getResponseFromSelflink(this.responseCache));
const payload$ = responseCache$.pipe(
filterSuccessfulResponses(),
map((entry: ResponseCacheEntry) => entry.response),
map((response: GenericSuccessResponse<Item[]>) => new PaginatedList(response.pageInfo, response.payload)),
map((list: PaginatedList<Item>) => Object.assign(list, {
page: list.page ? list.page.map((item: DSpaceObject) => Object.assign(new Item(), item)) : list.page
})),
distinctUntilChanged() distinctUntilChanged()
); );

View File

@@ -8,6 +8,8 @@ import { ResourceType } from '../../shared/resource-type';
import { NormalizedObject } from './normalized-object.model'; import { NormalizedObject } from './normalized-object.model';
import { NormalizedBitstreamFormat } from './normalized-bitstream-format.model'; import { NormalizedBitstreamFormat } from './normalized-bitstream-format.model';
import { NormalizedResourcePolicy } from './normalized-resource-policy.model'; import { NormalizedResourcePolicy } from './normalized-resource-policy.model';
import { NormalizedEPerson } from '../../eperson/models/normalized-eperson.model';
import { NormalizedGroup } from '../../eperson/models/normalized-group.model';
export class NormalizedObjectFactory { export class NormalizedObjectFactory {
public static getConstructor(type: ResourceType): GenericConstructor<NormalizedObject> { public static getConstructor(type: ResourceType): GenericConstructor<NormalizedObject> {
@@ -33,6 +35,12 @@ export class NormalizedObjectFactory {
case ResourceType.ResourcePolicy: { case ResourceType.ResourcePolicy: {
return NormalizedResourcePolicy return NormalizedResourcePolicy
} }
case ResourceType.EPerson: {
return NormalizedEPerson
}
case ResourceType.Group: {
return NormalizedGroup
}
default: { default: {
return undefined; return undefined;
} }

View File

@@ -62,6 +62,7 @@ import { RegistryMetadatafieldsResponseParsingService } from './data/registry-me
import { RegistryBitstreamformatsResponseParsingService } from './data/registry-bitstreamformats-response-parsing.service'; import { RegistryBitstreamformatsResponseParsingService } from './data/registry-bitstreamformats-response-parsing.service';
import { NotificationsService } from '../shared/notifications/notifications.service'; import { NotificationsService } from '../shared/notifications/notifications.service';
import { UploaderService } from '../shared/uploader/uploader.service'; import { UploaderService } from '../shared/uploader/uploader.service';
import { BrowseItemsResponseParsingService } from './data/browse-items-response-parsing-service';
import { DSpaceObjectDataService } from './data/dspace-object-data.service'; import { DSpaceObjectDataService } from './data/dspace-object-data.service';
const IMPORTS = [ const IMPORTS = [
@@ -115,6 +116,7 @@ const PROVIDERS = [
ServerResponseService, ServerResponseService,
BrowseResponseParsingService, BrowseResponseParsingService,
BrowseEntriesResponseParsingService, BrowseEntriesResponseParsingService,
BrowseItemsResponseParsingService,
BrowseService, BrowseService,
ConfigResponseParsingService, ConfigResponseParsingService,
RouteService, RouteService,

View File

@@ -7,6 +7,8 @@ import { GlobalConfig } from '../../../config/global-config.interface';
import { GenericConstructor } from '../shared/generic-constructor'; import { GenericConstructor } from '../shared/generic-constructor';
import { PaginatedList } from './paginated-list'; import { PaginatedList } from './paginated-list';
import { NormalizedObject } from '../cache/models/normalized-object.model'; import { NormalizedObject } from '../cache/models/normalized-object.model';
import { ResourceType } from '../shared/resource-type';
import { RESTURLCombiner } from '../url-combiner/rest-url-combiner';
function isObjectLevel(halObj: any) { function isObjectLevel(halObj: any) {
return isNotEmpty(halObj._links) && hasValue(halObj._links.self); return isNotEmpty(halObj._links) && hasValue(halObj._links.self);
@@ -34,6 +36,7 @@ export abstract class BaseResponseParsingService {
} else if (Array.isArray(data)) { } else if (Array.isArray(data)) {
return this.processArray(data, requestHref); return this.processArray(data, requestHref);
} else if (isObjectLevel(data)) { } else if (isObjectLevel(data)) {
data = this.fixBadEPersonRestResponse(data);
const object = this.deserialize(data); const object = this.deserialize(data);
if (isNotEmpty(data._embedded)) { if (isNotEmpty(data._embedded)) {
Object Object
@@ -53,6 +56,7 @@ export abstract class BaseResponseParsingService {
} }
}); });
} }
this.cache(object, requestHref); this.cache(object, requestHref);
return object; return object;
} }
@@ -145,4 +149,23 @@ export abstract class BaseResponseParsingService {
} }
return obj[keys[0]]; return obj[keys[0]];
} }
// TODO Remove when https://jira.duraspace.org/browse/DS-4006 is fixed
// See https://github.com/DSpace/dspace-angular/issues/292
private fixBadEPersonRestResponse(obj: any): any {
if (obj.type === ResourceType.EPerson) {
const groups = obj.groups;
const normGroups = [];
if (isNotEmpty(groups)) {
groups.forEach((group) => {
const parts = ['eperson', 'groups', group.uuid];
const href = new RESTURLCombiner(this.EnvConfig, ...parts).toString();
normGroups.push(href);
}
)
}
return Object.assign({}, obj, { groups: normGroups });
}
return obj;
}
} }

View File

@@ -0,0 +1,168 @@
import { getMockObjectCacheService } from '../../shared/mocks/mock-object-cache.service';
import { ErrorResponse, GenericSuccessResponse } from '../cache/response-cache.models';
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
import { BrowseEntriesResponseParsingService } from './browse-entries-response-parsing.service';
import { BrowseEntriesRequest, BrowseItemsRequest } from './request.models';
import { BrowseItemsResponseParsingService } from './browse-items-response-parsing-service';
describe('BrowseItemsResponseParsingService', () => {
let service: BrowseItemsResponseParsingService;
beforeEach(() => {
service = new BrowseItemsResponseParsingService(undefined, getMockObjectCacheService());
});
describe('parse', () => {
const request = new BrowseItemsRequest('client/f5b4ccb8-fbb0-4548-b558-f234d9fdfad6', 'https://rest.api/discover/browses/author/items');
const validResponse = {
payload: {
_embedded: {
items: [
{
id: 'd7b6bc6f-ff6c-444a-a0d3-0cd9b68043e7',
uuid: 'd7b6bc6f-ff6c-444a-a0d3-0cd9b68043e7',
name: 'Development of Local Supply Chain : A Critical Link for Concentrated Solar Power in India',
handle: '10986/17472',
metadata: [
{
key: 'dc.creator',
value: 'World Bank',
language: null
}
],
inArchive: true,
discoverable: true,
withdrawn: false,
lastModified: '2018-05-25T09:32:58.005+0000',
type: 'item',
_links: {
bitstreams: {
href: 'https://dspace7-internal.atmire.com/rest/api/core/items/d7b6bc6f-ff6c-444a-a0d3-0cd9b68043e7/bitstreams'
},
owningCollection: {
href: 'https://dspace7-internal.atmire.com/rest/api/core/items/d7b6bc6f-ff6c-444a-a0d3-0cd9b68043e7/owningCollection'
},
templateItemOf: {
href: 'https://dspace7-internal.atmire.com/rest/api/core/items/d7b6bc6f-ff6c-444a-a0d3-0cd9b68043e7/templateItemOf'
},
self: {
href: 'https://dspace7-internal.atmire.com/rest/api/core/items/d7b6bc6f-ff6c-444a-a0d3-0cd9b68043e7'
}
}
},
{
id: '27c6f976-257c-4ad0-a0ef-c5e34ffe4d5b',
uuid: '27c6f976-257c-4ad0-a0ef-c5e34ffe4d5b',
name: 'Development of Local Supply Chain : The Missing Link for Concentrated Solar Power Projects in India',
handle: '10986/17475',
metadata: [
{
key: 'dc.creator',
value: 'World Bank',
language: null
}
],
inArchive: true,
discoverable: true,
withdrawn: false,
lastModified: '2018-05-25T09:33:42.526+0000',
type: 'item',
_links: {
bitstreams: {
href: 'https://dspace7-internal.atmire.com/rest/api/core/items/27c6f976-257c-4ad0-a0ef-c5e34ffe4d5b/bitstreams'
},
owningCollection: {
href: 'https://dspace7-internal.atmire.com/rest/api/core/items/27c6f976-257c-4ad0-a0ef-c5e34ffe4d5b/owningCollection'
},
templateItemOf: {
href: 'https://dspace7-internal.atmire.com/rest/api/core/items/27c6f976-257c-4ad0-a0ef-c5e34ffe4d5b/templateItemOf'
},
self: {
href: 'https://dspace7-internal.atmire.com/rest/api/core/items/27c6f976-257c-4ad0-a0ef-c5e34ffe4d5b'
}
}
}
]
},
_links: {
first: {
href: 'https://dspace7-internal.atmire.com/rest/api/discover/browses/author/items?page=0&size=2'
},
self: {
href: 'https://dspace7-internal.atmire.com/rest/api/discover/browses/author/items'
},
next: {
href: 'https://dspace7-internal.atmire.com/rest/api/discover/browses/author/items?page=1&size=2'
},
last: {
href: 'https://dspace7-internal.atmire.com/rest/api/discover/browses/author/items?page=7&size=2'
}
},
page: {
size: 2,
totalElements: 16,
totalPages: 8,
number: 0
}
},
statusCode: '200'
} as DSpaceRESTV2Response;
const invalidResponseNotAList = {
payload: {
id: 'd7b6bc6f-ff6c-444a-a0d3-0cd9b68043e7',
uuid: 'd7b6bc6f-ff6c-444a-a0d3-0cd9b68043e7',
name: 'Development of Local Supply Chain : A Critical Link for Concentrated Solar Power in India',
handle: '10986/17472',
metadata: [
{
key: 'dc.creator',
value: 'World Bank',
language: null
}
],
inArchive: true,
discoverable: true,
withdrawn: false,
lastModified: '2018-05-25T09:32:58.005+0000',
type: 'item',
_links: {
bitstreams: {
href: 'https://dspace7-internal.atmire.com/rest/api/core/items/d7b6bc6f-ff6c-444a-a0d3-0cd9b68043e7/bitstreams'
},
owningCollection: {
href: 'https://dspace7-internal.atmire.com/rest/api/core/items/d7b6bc6f-ff6c-444a-a0d3-0cd9b68043e7/owningCollection'
},
templateItemOf: {
href: 'https://dspace7-internal.atmire.com/rest/api/core/items/d7b6bc6f-ff6c-444a-a0d3-0cd9b68043e7/templateItemOf'
},
self: {
href: 'https://dspace7-internal.atmire.com/rest/api/core/items/d7b6bc6f-ff6c-444a-a0d3-0cd9b68043e7'
}
}
},
statusCode: '200'
} as DSpaceRESTV2Response;
const invalidResponseStatusCode = {
payload: {}, statusCode: '500'
} as DSpaceRESTV2Response;
it('should return a GenericSuccessResponse if data contains a valid browse items response', () => {
const response = service.parse(request, validResponse);
expect(response.constructor).toBe(GenericSuccessResponse);
});
it('should return an ErrorResponse if data contains an invalid browse entries response', () => {
const response = service.parse(request, invalidResponseNotAList);
expect(response.constructor).toBe(ErrorResponse);
});
it('should return an ErrorResponse if data contains a statuscode other than 200', () => {
const response = service.parse(request, invalidResponseStatusCode);
expect(response.constructor).toBe(ErrorResponse);
});
});
});

View File

@@ -0,0 +1,58 @@
import { Inject, Injectable } from '@angular/core';
import { GLOBAL_CONFIG } from '../../../config';
import { GlobalConfig } from '../../../config/global-config.interface';
import { isNotEmpty } from '../../shared/empty.util';
import { ObjectCacheService } from '../cache/object-cache.service';
import {
ErrorResponse,
GenericSuccessResponse,
RestResponse
} from '../cache/response-cache.models';
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer';
import { BaseResponseParsingService } from './base-response-parsing.service';
import { ResponseParsingService } from './parsing.service';
import { RestRequest } from './request.models';
import { Item } from '../shared/item.model';
import { DSpaceObject } from '../shared/dspace-object.model';
/**
* A ResponseParsingService used to parse DSpaceRESTV2Response coming from the REST API to Browse Items (DSpaceObject[])
*/
@Injectable()
export class BrowseItemsResponseParsingService extends BaseResponseParsingService implements ResponseParsingService {
protected objectFactory = {
getConstructor: () => DSpaceObject
};
protected toCache = false;
constructor(
@Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig,
protected objectCache: ObjectCacheService,
) { super();
}
/**
* Parses data from the browse endpoint to a list of DSpaceObjects
* @param {RestRequest} request
* @param {DSpaceRESTV2Response} data
* @returns {RestResponse}
*/
parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse {
if (isNotEmpty(data.payload) && isNotEmpty(data.payload._embedded)
&& Array.isArray(data.payload._embedded[Object.keys(data.payload._embedded)[0]])) {
const serializer = new DSpaceRESTv2Serializer(DSpaceObject);
const items = serializer.deserializeArray(data.payload._embedded[Object.keys(data.payload._embedded)[0]]);
return new GenericSuccessResponse(items, data.statusCode, this.processPageInfo(data.payload));
} else {
return new ErrorResponse(
Object.assign(
new Error('Unexpected response from browse endpoint'),
{ statusText: data.statusCode }
)
);
}
}
}

View File

@@ -9,7 +9,7 @@ import { ResponseCacheService } from '../cache/response-cache.service';
import { CoreState } from '../core.reducers'; import { CoreState } from '../core.reducers';
import { ComColDataService } from './comcol-data.service'; import { ComColDataService } from './comcol-data.service';
import { CommunityDataService } from './community-data.service'; import { CommunityDataService } from './community-data.service';
import { FindByIDRequest } from './request.models'; import { FindAllOptions, FindByIDRequest } from './request.models';
import { RequestService } from './request.service'; import { RequestService } from './request.service';
import { NormalizedObject } from '../cache/models/normalized-object.model'; import { NormalizedObject } from '../cache/models/normalized-object.model';
import { HALEndpointService } from '../shared/hal-endpoint.service'; import { HALEndpointService } from '../shared/hal-endpoint.service';
@@ -52,6 +52,10 @@ describe('ComColDataService', () => {
const EnvConfig = {} as GlobalConfig; const EnvConfig = {} as GlobalConfig;
const scopeID = 'd9d30c0c-69b7-4369-8397-ca67c888974d'; const scopeID = 'd9d30c0c-69b7-4369-8397-ca67c888974d';
const options = Object.assign(new FindAllOptions(), {
scopeID: scopeID
});
const communitiesEndpoint = 'https://rest.api/core/communities'; const communitiesEndpoint = 'https://rest.api/core/communities';
const communityEndpoint = `${communitiesEndpoint}/${scopeID}`; const communityEndpoint = `${communitiesEndpoint}/${scopeID}`;
const scopedEndpoint = `${communityEndpoint}/${LINK_NAME}`; const scopedEndpoint = `${communityEndpoint}/${LINK_NAME}`;
@@ -98,7 +102,7 @@ describe('ComColDataService', () => {
); );
} }
describe('getScopedEndpoint', () => { describe('getBrowseEndpoint', () => {
beforeEach(() => { beforeEach(() => {
scheduler = getTestScheduler(); scheduler = getTestScheduler();
}); });
@@ -112,7 +116,7 @@ describe('ComColDataService', () => {
const expected = new FindByIDRequest(requestService.generateRequestId(), communityEndpoint, scopeID); const expected = new FindByIDRequest(requestService.generateRequestId(), communityEndpoint, scopeID);
scheduler.schedule(() => service.getScopedEndpoint(scopeID).subscribe()); scheduler.schedule(() => service.getBrowseEndpoint(options).subscribe());
scheduler.flush(); scheduler.flush();
expect(requestService.configure).toHaveBeenCalledWith(expected); expect(requestService.configure).toHaveBeenCalledWith(expected);
@@ -128,13 +132,13 @@ describe('ComColDataService', () => {
}); });
it('should fetch the scope Community from the cache', () => { it('should fetch the scope Community from the cache', () => {
scheduler.schedule(() => service.getScopedEndpoint(scopeID).subscribe()); scheduler.schedule(() => service.getBrowseEndpoint(options).subscribe());
scheduler.flush(); scheduler.flush();
expect(objectCache.getByUUID).toHaveBeenCalledWith(scopeID); expect(objectCache.getByUUID).toHaveBeenCalledWith(scopeID);
}); });
it('should return the endpoint to fetch resources within the given scope', () => { it('should return the endpoint to fetch resources within the given scope', () => {
const result = service.getScopedEndpoint(scopeID); const result = service.getBrowseEndpoint(options);
const expected = cold('--e-', { e: scopedEndpoint }); const expected = cold('--e-', { e: scopedEndpoint });
expect(result).toBeObservable(expected); expect(result).toBeObservable(expected);
@@ -151,7 +155,7 @@ describe('ComColDataService', () => {
}); });
it('should throw an error', () => { it('should throw an error', () => {
const result = service.getScopedEndpoint(scopeID); const result = service.getBrowseEndpoint(options);
const expected = cold('--#-', undefined, new Error(`The Community with scope ${scopeID} couldn't be retrieved`)); const expected = cold('--#-', undefined, new Error(`The Community with scope ${scopeID} couldn't be retrieved`));
expect(result).toBeObservable(expected); expect(result).toBeObservable(expected);

View File

@@ -7,7 +7,7 @@ import { ResponseCacheEntry } from '../cache/response-cache.reducer';
import { CommunityDataService } from './community-data.service'; import { CommunityDataService } from './community-data.service';
import { DataService } from './data.service'; import { DataService } from './data.service';
import { FindByIDRequest } from './request.models'; import { FindAllOptions, FindByIDRequest } from './request.models';
import { NormalizedObject } from '../cache/models/normalized-object.model'; import { NormalizedObject } from '../cache/models/normalized-object.model';
import { HALEndpointService } from '../shared/hal-endpoint.service'; import { HALEndpointService } from '../shared/hal-endpoint.service';
import { DSOSuccessResponse } from '../cache/response-cache.models'; import { DSOSuccessResponse } from '../cache/response-cache.models';
@@ -27,17 +27,18 @@ export abstract class ComColDataService<TNormalized extends NormalizedObject, TD
* @return { Observable<string> } * @return { Observable<string> }
* an Observable<string> containing the scoped URL * an Observable<string> containing the scoped URL
*/ */
public getScopedEndpoint(scopeID: string): Observable<string> { public getBrowseEndpoint(options: FindAllOptions = {}): Observable<string> {
if (isEmpty(scopeID)) { if (isEmpty(options.scopeID)) {
return this.halService.getEndpoint(this.linkPath); return this.halService.getEndpoint(this.linkPath);
} else { } else {
const scopeCommunityHrefObs = this.cds.getEndpoint().pipe( const scopeCommunityHrefObs = this.cds.getEndpoint()
mergeMap((endpoint: string) => this.cds.getFindByIDHref(endpoint, scopeID)), .flatMap((endpoint: string) => this.cds.getFindByIDHref(endpoint, options.scopeID))
first((href: string) => isNotEmpty(href)), .filter((href: string) => isNotEmpty(href))
tap((href: string) => { .take(1)
const request = new FindByIDRequest(this.requestService.generateRequestId(), href, scopeID); .do((href: string) => {
const request = new FindByIDRequest(this.requestService.generateRequestId(), href, options.scopeID);
this.requestService.configure(request); this.requestService.configure(request);
}),); });
// return scopeCommunityHrefObs.pipe( // return scopeCommunityHrefObs.pipe(
// mergeMap((href: string) => this.responseCache.get(href)), // mergeMap((href: string) => this.responseCache.get(href)),
@@ -61,16 +62,15 @@ export abstract class ComColDataService<TNormalized extends NormalizedObject, TD
map((entry: ResponseCacheEntry) => entry.response)); map((entry: ResponseCacheEntry) => entry.response));
const errorResponses = responses.pipe( const errorResponses = responses.pipe(
filter((response) => !response.isSuccessful), filter((response) => !response.isSuccessful),
mergeMap(() => observableThrowError(new Error(`The Community with scope ${scopeID} couldn't be retrieved`))) mergeMap(() => observableThrowError(new Error(`The Community with scope ${options.scopeID} couldn't be retrieved`)))
); );
const successResponses = responses.pipe( const successResponses = responses.pipe(
filter((response) => response.isSuccessful), filter((response) => response.isSuccessful),
mergeMap(() => this.objectCache.getByUUID(scopeID)), mergeMap(() => this.objectCache.getByUUID(options.scopeID)),
map((nc: NormalizedCommunity) => nc._links[this.linkPath]), map((nc: NormalizedCommunity) => nc._links[this.linkPath]),
filter((href) => isNotEmpty(href)) filter((href) => isNotEmpty(href))
); );
return observableMerge(errorResponses, successResponses).pipe(distinctUntilChanged()); return observableMerge(errorResponses, successResponses).pipe(distinctUntilChanged());
} }
} }

View File

@@ -10,7 +10,7 @@ import { Observable } from 'rxjs';
import { FindAllOptions } from './request.models'; import { FindAllOptions } from './request.models';
import { SortOptions, SortDirection } from '../cache/models/sort-options.model'; import { SortOptions, SortDirection } from '../cache/models/sort-options.model';
const LINK_NAME = 'test' const endpoint = 'https://rest.api/core';
// tslint:disable:max-classes-per-file // tslint:disable:max-classes-per-file
class NormalizedTestObject extends NormalizedObject { class NormalizedTestObject extends NormalizedObject {
@@ -28,10 +28,9 @@ class TestService extends DataService<NormalizedTestObject, any> {
super(); super();
} }
public getScopedEndpoint(scope: string): Observable<string> { public getBrowseEndpoint(options: FindAllOptions): Observable<string> {
throw new Error('getScopedEndpoint is abstract in DataService'); return Observable.of(endpoint);
} }
} }
describe('DataService', () => { describe('DataService', () => {
@@ -42,7 +41,6 @@ describe('DataService', () => {
const halService = {} as HALEndpointService; const halService = {} as HALEndpointService;
const rdbService = {} as RemoteDataBuildService; const rdbService = {} as RemoteDataBuildService;
const store = {} as Store<CoreState>; const store = {} as Store<CoreState>;
const endpoint = 'https://rest.api/core';
function initTestService(): TestService { function initTestService(): TestService {
return new TestService( return new TestService(
@@ -50,7 +48,7 @@ describe('DataService', () => {
requestService, requestService,
rdbService, rdbService,
store, store,
LINK_NAME, endpoint,
halService halService
); );
} }
@@ -62,25 +60,17 @@ describe('DataService', () => {
it('should return an observable with the endpoint', () => { it('should return an observable with the endpoint', () => {
options = {}; options = {};
(service as any).getFindAllHref(endpoint).subscribe((value) => { (service as any).getFindAllHref(options).subscribe((value) => {
expect(value).toBe(endpoint); expect(value).toBe(endpoint);
} }
); );
}); });
// getScopedEndpoint is not implemented in abstract DataService
it('should throw error if scopeID provided in options', () => {
options = { scopeID: 'somevalue' };
expect(() => { (service as any).getFindAllHref(endpoint, options) })
.toThrowError('getScopedEndpoint is abstract in DataService');
});
it('should include page in href if currentPage provided in options', () => { it('should include page in href if currentPage provided in options', () => {
options = { currentPage: 2 }; options = { currentPage: 2 };
const expected = `${endpoint}?page=${options.currentPage - 1}`; const expected = `${endpoint}?page=${options.currentPage - 1}`;
(service as any).getFindAllHref(endpoint, options).subscribe((value) => { (service as any).getFindAllHref(options).subscribe((value) => {
expect(value).toBe(expected); expect(value).toBe(expected);
}); });
}); });
@@ -89,7 +79,7 @@ describe('DataService', () => {
options = { elementsPerPage: 5 }; options = { elementsPerPage: 5 };
const expected = `${endpoint}?size=${options.elementsPerPage}`; const expected = `${endpoint}?size=${options.elementsPerPage}`;
(service as any).getFindAllHref(endpoint, options).subscribe((value) => { (service as any).getFindAllHref(options).subscribe((value) => {
expect(value).toBe(expected); expect(value).toBe(expected);
}); });
}); });
@@ -99,7 +89,7 @@ describe('DataService', () => {
options = { sort: sortOptions}; options = { sort: sortOptions};
const expected = `${endpoint}?sort=${sortOptions.field},${sortOptions.direction}`; const expected = `${endpoint}?sort=${sortOptions.field},${sortOptions.direction}`;
(service as any).getFindAllHref(endpoint, options).subscribe((value) => { (service as any).getFindAllHref(options).subscribe((value) => {
expect(value).toBe(expected); expect(value).toBe(expected);
}); });
}); });
@@ -108,7 +98,7 @@ describe('DataService', () => {
options = { startsWith: 'ab' }; options = { startsWith: 'ab' };
const expected = `${endpoint}?startsWith=${options.startsWith}`; const expected = `${endpoint}?startsWith=${options.startsWith}`;
(service as any).getFindAllHref(endpoint, options).subscribe((value) => { (service as any).getFindAllHref(options).subscribe((value) => {
expect(value).toBe(expected); expect(value).toBe(expected);
}); });
}); });
@@ -124,7 +114,7 @@ describe('DataService', () => {
const expected = `${endpoint}?page=${options.currentPage - 1}&size=${options.elementsPerPage}` + const expected = `${endpoint}?page=${options.currentPage - 1}&size=${options.elementsPerPage}` +
`&sort=${sortOptions.field},${sortOptions.direction}&startsWith=${options.startsWith}`; `&sort=${sortOptions.field},${sortOptions.direction}&startsWith=${options.startsWith}`;
(service as any).getFindAllHref(endpoint, options).subscribe((value) => { (service as any).getFindAllHref(options).subscribe((value) => {
expect(value).toBe(expected); expect(value).toBe(expected);
}); });
}) })

View File

@@ -1,7 +1,5 @@
import { filter, take, first } from 'rxjs/operators';
import {of as observableOf, Observable } from 'rxjs'; import {of as observableOf, Observable } from 'rxjs';
import {mergeMap, first, take, distinctUntilChanged, map, filter} from 'rxjs/operators';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { hasValue, isNotEmpty } from '../../shared/empty.util'; import { hasValue, isNotEmpty } from '../../shared/empty.util';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
@@ -14,6 +12,8 @@ import { RemoteData } from './remote-data';
import { FindAllOptions, FindAllRequest, FindByIDRequest, GetRequest } from './request.models'; import { FindAllOptions, FindAllRequest, FindByIDRequest, GetRequest } from './request.models';
import { RequestService } from './request.service'; import { RequestService } from './request.service';
import { NormalizedObject } from '../cache/models/normalized-object.model'; import { NormalizedObject } from '../cache/models/normalized-object.model';
import { promise } from 'selenium-webdriver';
import map = promise.map;
export abstract class DataService<TNormalized extends NormalizedObject, TDomain> { export abstract class DataService<TNormalized extends NormalizedObject, TDomain> {
protected abstract responseCache: ResponseCacheService; protected abstract responseCache: ResponseCacheService;
@@ -23,17 +23,13 @@ export abstract class DataService<TNormalized extends NormalizedObject, TDomain>
protected abstract linkPath: string; protected abstract linkPath: string;
protected abstract halService: HALEndpointService; protected abstract halService: HALEndpointService;
public abstract getScopedEndpoint(scope: string): Observable<string> public abstract getBrowseEndpoint(options: FindAllOptions): Observable<string>
protected getFindAllHref(endpoint, options: FindAllOptions = {}): Observable<string> { protected getFindAllHref(options: FindAllOptions = {}): Observable<string> {
let result: Observable<string>; let result: Observable<string>;
const args = []; const args = [];
if (hasValue(options.scopeID)) { result = this.getBrowseEndpoint(options).distinctUntilChanged();
result = this.getScopedEndpoint(options.scopeID).pipe(distinctUntilChanged());
} else {
result = observableOf(endpoint);
}
if (hasValue(options.currentPage) && typeof options.currentPage === 'number') { if (hasValue(options.currentPage) && typeof options.currentPage === 'number') {
/* TODO: this is a temporary fix for the pagination start index (0 or 1) discrepancy between the rest and the frontend respectively */ /* TODO: this is a temporary fix for the pagination start index (0 or 1) discrepancy between the rest and the frontend respectively */
@@ -60,12 +56,11 @@ export abstract class DataService<TNormalized extends NormalizedObject, TDomain>
} }
findAll(options: FindAllOptions = {}): Observable<RemoteData<PaginatedList<TDomain>>> { findAll(options: FindAllOptions = {}): Observable<RemoteData<PaginatedList<TDomain>>> {
const hrefObs = this.halService.getEndpoint(this.linkPath).pipe(filter((href: string) => isNotEmpty(href)), const hrefObs = this.getFindAllHref(options);
mergeMap((endpoint: string) => this.getFindAllHref(endpoint, options)),);
hrefObs.pipe( hrefObs.pipe(
filter((href: string) => hasValue(href)), filter((href: string) => hasValue(href)),
take(1),) take(1))
.subscribe((href: string) => { .subscribe((href: string) => {
const request = new FindAllRequest(this.requestService.generateRequestId(), href, options); const request = new FindAllRequest(this.requestService.generateRequestId(), href, options);
this.requestService.configure(request); this.requestService.configure(request);

View File

@@ -10,6 +10,7 @@ import { HALEndpointService } from '../shared/hal-endpoint.service';
import { DataService } from './data.service'; import { DataService } from './data.service';
import { RemoteData } from './remote-data'; import { RemoteData } from './remote-data';
import { RequestService } from './request.service'; import { RequestService } from './request.service';
import { FindAllOptions } from './request.models';
/* tslint:disable:max-classes-per-file */ /* tslint:disable:max-classes-per-file */
class DataServiceImpl extends DataService<NormalizedDSpaceObject, DSpaceObject> { class DataServiceImpl extends DataService<NormalizedDSpaceObject, DSpaceObject> {
@@ -24,8 +25,8 @@ class DataServiceImpl extends DataService<NormalizedDSpaceObject, DSpaceObject>
super(); super();
} }
getScopedEndpoint(scope: string): Observable<string> { getBrowseEndpoint(options: FindAllOptions): Observable<string> {
return undefined; return this.halService.getEndpoint(this.linkPath);
} }
getFindByIDHref(endpoint, resourceID): string { getFindByIDHref(endpoint, resourceID): string {

View File

@@ -8,6 +8,7 @@ import { CoreState } from '../core.reducers';
import { ItemDataService } from './item-data.service'; import { ItemDataService } from './item-data.service';
import { RequestService } from './request.service'; import { RequestService } from './request.service';
import { HALEndpointService } from '../shared/hal-endpoint.service'; import { HALEndpointService } from '../shared/hal-endpoint.service';
import { FindAllOptions } from './request.models';
describe('ItemDataService', () => { describe('ItemDataService', () => {
let scheduler: TestScheduler; let scheduler: TestScheduler;
@@ -20,6 +21,14 @@ describe('ItemDataService', () => {
const halEndpointService = {} as HALEndpointService; const halEndpointService = {} as HALEndpointService;
const scopeID = '4af28e99-6a9c-4036-a199-e1b587046d39'; const scopeID = '4af28e99-6a9c-4036-a199-e1b587046d39';
const options = Object.assign(new FindAllOptions(), {
scopeID: scopeID,
sort: {
field: '',
direction: undefined
}
});
const browsesEndpoint = 'https://rest.api/discover/browses'; const browsesEndpoint = 'https://rest.api/discover/browses';
const itemBrowseEndpoint = `${browsesEndpoint}/author/items`; const itemBrowseEndpoint = `${browsesEndpoint}/author/items`;
const scopedEndpoint = `${itemBrowseEndpoint}?scope=${scopeID}`; const scopedEndpoint = `${itemBrowseEndpoint}?scope=${scopeID}`;
@@ -46,16 +55,16 @@ describe('ItemDataService', () => {
); );
} }
describe('getScopedEndpoint', () => { describe('getBrowseEndpoint', () => {
beforeEach(() => { beforeEach(() => {
scheduler = getTestScheduler(); scheduler = getTestScheduler();
}); });
it('should return the endpoint to fetch Items within the given scope', () => { it('should return the endpoint to fetch Items within the given scope and starting with the given string', () => {
bs = initMockBrowseService(true); bs = initMockBrowseService(true);
service = initTestService(); service = initTestService();
const result = service.getScopedEndpoint(scopeID); const result = service.getBrowseEndpoint(options);
const expected = cold('--b-', { b: scopedEndpoint }); const expected = cold('--b-', { b: scopedEndpoint });
expect(result).toBeObservable(expected); expect(result).toBeObservable(expected);
@@ -67,7 +76,7 @@ describe('ItemDataService', () => {
service = initTestService(); service = initTestService();
}); });
it('should throw an error', () => { it('should throw an error', () => {
const result = service.getScopedEndpoint(scopeID); const result = service.getBrowseEndpoint(options);
const expected = cold('--#-', undefined, browseError); const expected = cold('--#-', undefined, browseError);
expect(result).toBeObservable(expected); expect(result).toBeObservable(expected);

View File

@@ -1,11 +1,7 @@
import { Injectable } from '@angular/core';
import {distinctUntilChanged, map, filter} from 'rxjs/operators';
import { Inject, Injectable } from '@angular/core';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { GLOBAL_CONFIG, GlobalConfig } from '../../../config'; import { isNotEmpty } from '../../shared/empty.util';
import { isEmpty, isNotEmpty } from '../../shared/empty.util';
import { BrowseService } from '../browse/browse.service'; import { BrowseService } from '../browse/browse.service';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
import { NormalizedItem } from '../cache/models/normalized-item.model'; import { NormalizedItem } from '../cache/models/normalized-item.model';
@@ -17,6 +13,7 @@ import { URLCombiner } from '../url-combiner/url-combiner';
import { DataService } from './data.service'; import { DataService } from './data.service';
import { RequestService } from './request.service'; import { RequestService } from './request.service';
import { HALEndpointService } from '../shared/hal-endpoint.service'; import { HALEndpointService } from '../shared/hal-endpoint.service';
import { FindAllOptions } from './request.models';
@Injectable() @Injectable()
export class ItemDataService extends DataService<NormalizedItem, Item> { export class ItemDataService extends DataService<NormalizedItem, Item> {
@@ -32,15 +29,21 @@ export class ItemDataService extends DataService<NormalizedItem, Item> {
super(); super();
} }
public getScopedEndpoint(scopeID: string): Observable<string> { /**
if (isEmpty(scopeID)) { * Get the endpoint for browsing items
return this.halService.getEndpoint(this.linkPath); * (When options.sort.field is empty, the default field to browse by will be 'dc.date.issued')
} else { * @param {FindAllOptions} options
return this.bs.getBrowseURLFor('dc.date.issued', this.linkPath).pipe( * @returns {Observable<string>}
filter((href: string) => isNotEmpty(href)), */
map((href: string) => new URLCombiner(href, `?scope=${scopeID}`).toString()), public getBrowseEndpoint(options: FindAllOptions = {}): Observable<string> {
distinctUntilChanged(),); let field = 'dc.date.issued';
if (options.sort && options.sort.field) {
field = options.sort.field;
} }
return this.bs.getBrowseURLFor(field, this.linkPath)
.filter((href: string) => isNotEmpty(href))
.map((href: string) => new URLCombiner(href, `?scope=${options.scopeID}`).toString())
.distinctUntilChanged();
} }
} }

View File

@@ -12,6 +12,7 @@ import { AuthResponseParsingService } from '../auth/auth-response-parsing.servic
import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service'; import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service';
import { HttpHeaders } from '@angular/common/http'; import { HttpHeaders } from '@angular/common/http';
import { IntegrationResponseParsingService } from '../integration/integration-response-parsing.service'; import { IntegrationResponseParsingService } from '../integration/integration-response-parsing.service';
import { BrowseItemsResponseParsingService } from './browse-items-response-parsing-service';
/* tslint:disable:max-classes-per-file */ /* tslint:disable:max-classes-per-file */
@@ -184,6 +185,12 @@ export class BrowseEntriesRequest extends GetRequest {
} }
} }
export class BrowseItemsRequest extends GetRequest {
getResponseParser(): GenericConstructor<ResponseParsingService> {
return BrowseItemsResponseParsingService;
}
}
export class ConfigRequest extends GetRequest { export class ConfigRequest extends GetRequest {
constructor(uuid: string, href: string) { constructor(uuid: string, href: string) {
super(uuid, href); super(uuid, href);

View File

@@ -1,7 +1,7 @@
import { DSpaceObject } from '../../shared/dspace-object.model'; import { DSpaceObject } from '../../shared/dspace-object.model';
import { Group } from './group.model'; import { Group } from './group.model';
export class Eperson extends DSpaceObject { export class EPerson extends DSpaceObject {
public handle: string; public handle: string;

View File

@@ -2,13 +2,13 @@ import { autoserialize, inheritSerialization } from 'cerialize';
import { CacheableObject } from '../../cache/object-cache.reducer'; import { CacheableObject } from '../../cache/object-cache.reducer';
import { ListableObject } from '../../../shared/object-collection/shared/listable-object.model'; import { ListableObject } from '../../../shared/object-collection/shared/listable-object.model';
import { NormalizedDSpaceObject } from '../../cache/models/normalized-dspace-object.model'; import { NormalizedDSpaceObject } from '../../cache/models/normalized-dspace-object.model';
import { Eperson } from './eperson.model'; import { EPerson } from './eperson.model';
import { mapsTo, relationship } from '../../cache/builders/build-decorators'; import { mapsTo, relationship } from '../../cache/builders/build-decorators';
import { ResourceType } from '../../shared/resource-type'; import { ResourceType } from '../../shared/resource-type';
@mapsTo(Eperson) @mapsTo(EPerson)
@inheritSerialization(NormalizedDSpaceObject) @inheritSerialization(NormalizedDSpaceObject)
export class NormalizedEpersonModel extends NormalizedDSpaceObject implements CacheableObject, ListableObject { export class NormalizedEPerson extends NormalizedDSpaceObject implements CacheableObject, ListableObject {
@autoserialize @autoserialize
public handle: string; public handle: string;

View File

@@ -2,13 +2,12 @@ import { autoserialize, inheritSerialization } from 'cerialize';
import { CacheableObject } from '../../cache/object-cache.reducer'; import { CacheableObject } from '../../cache/object-cache.reducer';
import { ListableObject } from '../../../shared/object-collection/shared/listable-object.model'; import { ListableObject } from '../../../shared/object-collection/shared/listable-object.model';
import { NormalizedDSpaceObject } from '../../cache/models/normalized-dspace-object.model'; import { NormalizedDSpaceObject } from '../../cache/models/normalized-dspace-object.model';
import { Eperson } from './eperson.model';
import { mapsTo } from '../../cache/builders/build-decorators'; import { mapsTo } from '../../cache/builders/build-decorators';
import { Group } from './group.model'; import { Group } from './group.model';
@mapsTo(Group) @mapsTo(Group)
@inheritSerialization(NormalizedDSpaceObject) @inheritSerialization(NormalizedDSpaceObject)
export class NormalizedGroupModel extends NormalizedDSpaceObject implements CacheableObject, ListableObject { export class NormalizedGroup extends NormalizedDSpaceObject implements CacheableObject, ListableObject {
@autoserialize @autoserialize
public handle: string; public handle: string;

View File

@@ -1,6 +1,7 @@
import { autoserialize, autoserializeAs } from 'cerialize'; import { autoserialize, autoserializeAs } from 'cerialize';
import { ListableObject } from '../../shared/object-collection/shared/listable-object.model';
export class BrowseEntry { export class BrowseEntry implements ListableObject {
@autoserialize @autoserialize
type: string; type: string;

View File

@@ -5,6 +5,7 @@ import { RemoteData } from '../data/remote-data';
import { ResourceType } from './resource-type'; import { ResourceType } from './resource-type';
import { ListableObject } from '../../shared/object-collection/shared/listable-object.model'; import { ListableObject } from '../../shared/object-collection/shared/listable-object.model';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { autoserialize } from 'cerialize';
/** /**
* An abstract model class for a DSpaceObject. * An abstract model class for a DSpaceObject.
@@ -16,11 +17,13 @@ export class DSpaceObject implements CacheableObject, ListableObject {
/** /**
* The human-readable identifier of this DSpaceObject * The human-readable identifier of this DSpaceObject
*/ */
@autoserialize
id: string; id: string;
/** /**
* The universally unique identifier of this DSpaceObject * The universally unique identifier of this DSpaceObject
*/ */
@autoserialize
uuid: string; uuid: string;
/** /**
@@ -31,11 +34,13 @@ export class DSpaceObject implements CacheableObject, ListableObject {
/** /**
* The name for this DSpaceObject * The name for this DSpaceObject
*/ */
@autoserialize
name: string; name: string;
/** /**
* An array containing all metadata of this DSpaceObject * An array containing all metadata of this DSpaceObject
*/ */
@autoserialize
metadata: Metadatum[]; metadata: Metadatum[];
/** /**

View File

@@ -1,6 +1,6 @@
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { filter, first, flatMap, map, tap } from 'rxjs/operators'; import { filter, first, flatMap, map, tap } from 'rxjs/operators';
import { hasValueOperator } from '../../shared/empty.util'; import { hasValueOperator, isNotEmpty } from '../../shared/empty.util';
import { DSOSuccessResponse } from '../cache/response-cache.models'; import { DSOSuccessResponse } from '../cache/response-cache.models';
import { ResponseCacheEntry } from '../cache/response-cache.reducer'; import { ResponseCacheEntry } from '../cache/response-cache.reducer';
import { ResponseCacheService } from '../cache/response-cache.service'; import { ResponseCacheService } from '../cache/response-cache.service';
@@ -8,6 +8,7 @@ import { RemoteData } from '../data/remote-data';
import { RestRequest } from '../data/request.models'; import { RestRequest } from '../data/request.models';
import { RequestEntry } from '../data/request.reducer'; import { RequestEntry } from '../data/request.reducer';
import { RequestService } from '../data/request.service'; import { RequestService } from '../data/request.service';
import { BrowseDefinition } from './browse-definition.model';
import { DSpaceObject } from './dspace-object.model'; import { DSpaceObject } from './dspace-object.model';
import { PaginatedList } from '../data/paginated-list'; import { PaginatedList } from '../data/paginated-list';
import { SearchResult } from '../../+search-page/search-result.model'; import { SearchResult } from '../../+search-page/search-result.model';
@@ -62,3 +63,24 @@ export const toDSpaceObjectListRD = () =>
return Object.assign(rd, {payload: payload}); return Object.assign(rd, {payload: payload});
}) })
); );
/**
* Get the browse links from a definition by ID given an array of all definitions
* @param {string} definitionID
* @returns {(source: Observable<RemoteData<BrowseDefinition[]>>) => Observable<any>}
*/
export const getBrowseDefinitionLinks = (definitionID: string) =>
(source: Observable<RemoteData<BrowseDefinition[]>>): Observable<any> =>
source.pipe(
getRemoteDataPayload(),
map((browseDefinitions: BrowseDefinition[]) => browseDefinitions
.find((def: BrowseDefinition) => def.id === definitionID && def.metadataBrowse === true)
),
map((def: BrowseDefinition) => {
if (isNotEmpty(def)) {
return def._links;
} else {
throw new Error(`No metadata browse definition could be found for id '${definitionID}'`);
}
})
);

View File

@@ -6,7 +6,7 @@ export enum ResourceType {
Item = 'item', Item = 'item',
Collection = 'collection', Collection = 'collection',
Community = 'community', Community = 'community',
Eperson = 'eperson', EPerson = 'eperson',
Group = 'group', Group = 'group',
ResourcePolicy = 'resourcePolicy' ResourcePolicy = 'resourcePolicy'
} }

View File

@@ -20,6 +20,8 @@ import { RouterStub } from '../shared/testing/router-stub';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import * as ngrx from '@ngrx/store'; import * as ngrx from '@ngrx/store';
import { NO_ERRORS_SCHEMA } from '@angular/core';
let comp: HeaderComponent; let comp: HeaderComponent;
let fixture: ComponentFixture<HeaderComponent>; let fixture: ComponentFixture<HeaderComponent>;
let store: Store<HeaderState>; let store: Store<HeaderState>;
@@ -35,11 +37,12 @@ describe('HeaderComponent', () => {
NgbCollapseModule.forRoot(), NgbCollapseModule.forRoot(),
NoopAnimationsModule, NoopAnimationsModule,
ReactiveFormsModule], ReactiveFormsModule],
declarations: [HeaderComponent, AuthNavMenuComponent, LoadingComponent, LogInComponent, LogOutComponent], declarations: [HeaderComponent],
providers: [ providers: [
{ provide: HostWindowService, useValue: new HostWindowServiceStub(800) }, { provide: HostWindowService, useValue: new HostWindowServiceStub(800) },
{ provide: Router, useClass: RouterStub }, { provide: Router, useClass: RouterStub },
] ],
schemas: [NO_ERRORS_SCHEMA]
}) })
.compileComponents(); // compile template and css .compileComponents(); // compile template and css
})); }));

View File

@@ -5,7 +5,7 @@ import { By } from '@angular/platform-browser';
import { Store, StoreModule } from '@ngrx/store'; import { Store, StoreModule } from '@ngrx/store';
import { authReducer, AuthState } from '../../core/auth/auth.reducer'; import { authReducer, AuthState } from '../../core/auth/auth.reducer';
import { EpersonMock } from '../testing/eperson-mock'; import { EPersonMock } from '../testing/eperson-mock';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
import { AppState } from '../../app.reducer'; import { AppState } from '../../app.reducer';
import { AuthNavMenuComponent } from './auth-nav-menu.component'; import { AuthNavMenuComponent } from './auth-nav-menu.component';
@@ -13,6 +13,7 @@ import { HostWindowServiceStub } from '../testing/host-window-service-stub';
import { HostWindowService } from '../host-window.service'; import { HostWindowService } from '../host-window.service';
import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { AuthTokenInfo } from '../../core/auth/models/auth-token-info.model'; import { AuthTokenInfo } from '../../core/auth/models/auth-token-info.model';
import { AuthService } from '../../core/auth/auth.service';
describe('AuthNavMenuComponent', () => { describe('AuthNavMenuComponent', () => {
@@ -31,7 +32,7 @@ describe('AuthNavMenuComponent', () => {
loaded: true, loaded: true,
loading: false, loading: false,
authToken: new AuthTokenInfo('test_token'), authToken: new AuthTokenInfo('test_token'),
user: EpersonMock user: EPersonMock
}; };
let routerState = { let routerState = {
url: '/home' url: '/home'
@@ -53,6 +54,7 @@ describe('AuthNavMenuComponent', () => {
], ],
providers: [ providers: [
{provide: HostWindowService, useValue: window}, {provide: HostWindowService, useValue: window},
{provide: AuthService, useValue: {setRedirectUrl: () => { /*empty*/ }}}
], ],
schemas: [ schemas: [
CUSTOM_ELEMENTS_SCHEMA CUSTOM_ELEMENTS_SCHEMA
@@ -222,6 +224,7 @@ describe('AuthNavMenuComponent', () => {
], ],
providers: [ providers: [
{provide: HostWindowService, useValue: window}, {provide: HostWindowService, useValue: window},
{provide: AuthService, useValue: {setRedirectUrl: () => { /*empty*/ }}}
], ],
schemas: [ schemas: [
CUSTOM_ELEMENTS_SCHEMA CUSTOM_ELEMENTS_SCHEMA

View File

@@ -14,8 +14,9 @@ import {
isAuthenticated, isAuthenticated,
isAuthenticationLoading isAuthenticationLoading
} from '../../core/auth/selectors'; } from '../../core/auth/selectors';
import { Eperson } from '../../core/eperson/models/eperson.model'; import { EPerson } from '../../core/eperson/models/eperson.model';
import { LOGIN_ROUTE, LOGOUT_ROUTE } from '../../core/auth/auth.service'; import { AuthService, LOGIN_ROUTE, LOGOUT_ROUTE } from '../../core/auth/auth.service';
import { Subscription } from 'rxjs/Subscription';
@Component({ @Component({
selector: 'ds-auth-nav-menu', selector: 'ds-auth-nav-menu',
@@ -40,10 +41,14 @@ export class AuthNavMenuComponent implements OnInit {
public showAuth = observableOf(false); public showAuth = observableOf(false);
public user: Observable<Eperson>; public user: Observable<EPerson>;
public sub: Subscription;
constructor(private store: Store<AppState>, constructor(private store: Store<AppState>,
private windowService: HostWindowService) { private windowService: HostWindowService,
private authService: AuthService
) {
this.isXsOrSm$ = this.windowService.isXsOrSm(); this.isXsOrSm$ = this.windowService.isXsOrSm();
} }
@@ -56,12 +61,15 @@ export class AuthNavMenuComponent implements OnInit {
this.user = this.store.pipe(select(getAuthenticatedUser)); this.user = this.store.pipe(select(getAuthenticatedUser));
this.showAuth = this.store.pipe( this.showAuth = this.store.select(routerStateSelector)
select(routerStateSelector), .filter((router: RouterReducerState) => isNotUndefined(router) && isNotUndefined(router.state))
filter((router: RouterReducerState) => isNotUndefined(router) && isNotUndefined(router.state)), .map((router: RouterReducerState) => {
map((router: RouterReducerState) => { const url = router.state.url;
return !router.state.url.startsWith(LOGIN_ROUTE) && !router.state.url.startsWith(LOGOUT_ROUTE); const show = !router.state.url.startsWith(LOGIN_ROUTE) && !router.state.url.startsWith(LOGOUT_ROUTE);
}) if (show) {
); this.authService.setRedirectUrl(url);
}
return show;
});
} }
} }

View File

@@ -0,0 +1,12 @@
<ng-container *ngVar="(objects$ | async) as objects">
<h2 class="w-100">{{title}}</h2>
<div *ngIf="objects?.hasSucceeded && !objects?.isLoading && objects?.payload?.page.length > 0" @fadeIn>
<ds-viewable-collection
[config]="paginationConfig"
[sortConfig]="sortConfig"
[objects]="objects">
</ds-viewable-collection>
</div>
<ds-loading *ngIf="!objects || objects?.payload?.page.length <= 0" message="{{'loading.browse-by' | translate}}"></ds-loading>
<ds-error *ngIf="objects?.hasFailed" message="{{'error.browse-by' | translate}}"></ds-error>
</ng-container>

View File

@@ -0,0 +1,44 @@
import { BrowseByComponent } from './browse-by.component';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { TranslateModule } from '@ngx-translate/core';
import { By } from '@angular/platform-browser';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { SharedModule } from '../shared.module';
describe('BrowseByComponent', () => {
let comp: BrowseByComponent;
let fixture: ComponentFixture<BrowseByComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [TranslateModule.forRoot(), SharedModule],
declarations: [],
schemas: [NO_ERRORS_SCHEMA]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(BrowseByComponent);
comp = fixture.componentInstance;
});
it('should display a loading message when objects is empty',() => {
(comp as any).objects = undefined;
fixture.detectChanges();
expect(fixture.debugElement.query(By.css('ds-loading'))).toBeDefined();
});
it('should display results when objects is not empty', () => {
(comp as any).objects = Observable.of({
payload: {
page: {
length: 1
}
}
});
fixture.detectChanges();
expect(fixture.debugElement.query(By.css('ds-viewable-collection'))).toBeDefined();
});
});

View File

@@ -0,0 +1,30 @@
import { Component, Input } from '@angular/core';
import { RemoteData } from '../../core/data/remote-data';
import { PaginatedList } from '../../core/data/paginated-list';
import { PaginationComponentOptions } from '../pagination/pagination-component-options.model';
import { SortOptions } from '../../core/cache/models/sort-options.model';
import { fadeIn, fadeInOut } from '../animations/fade';
import { Observable } from 'rxjs/Observable';
import { Item } from '../../core/shared/item.model';
import { ListableObject } from '../object-collection/shared/listable-object.model';
@Component({
selector: 'ds-browse-by',
styleUrls: ['./browse-by.component.scss'],
templateUrl: './browse-by.component.html',
animations: [
fadeIn,
fadeInOut
]
})
/**
* Component to display a browse-by page for any ListableObject
*/
export class BrowseByComponent {
@Input() title: string;
@Input() objects$: Observable<RemoteData<PaginatedList<ListableObject>>>;
@Input() paginationConfig: PaginationComponentOptions;
@Input() sortConfig: SortOptions;
@Input() currentUrl: string;
query: string;
}

View File

@@ -7,8 +7,8 @@ import { Store, StoreModule } from '@ngrx/store';
import { LogInComponent } from './log-in.component'; import { LogInComponent } from './log-in.component';
import { authReducer } from '../../core/auth/auth.reducer'; import { authReducer } from '../../core/auth/auth.reducer';
import { EpersonMock } from '../testing/eperson-mock'; import { EPersonMock } from '../testing/eperson-mock';
import { Eperson } from '../../core/eperson/models/eperson.model'; import { EPerson } from '../../core/eperson/models/eperson.model';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
import { AuthService } from '../../core/auth/auth.service'; import { AuthService } from '../../core/auth/auth.service';
import { AuthServiceStub } from '../testing/auth-service-stub'; import { AuthServiceStub } from '../testing/auth-service-stub';
@@ -19,7 +19,7 @@ describe('LogInComponent', () => {
let component: LogInComponent; let component: LogInComponent;
let fixture: ComponentFixture<LogInComponent>; let fixture: ComponentFixture<LogInComponent>;
let page: Page; let page: Page;
let user: Eperson; let user: EPerson;
const authState = { const authState = {
authenticated: false, authenticated: false,
@@ -28,7 +28,7 @@ describe('LogInComponent', () => {
}; };
beforeEach(() => { beforeEach(() => {
user = EpersonMock; user = EPersonMock;
}); });
beforeEach(async(() => { beforeEach(async(() => {

View File

@@ -5,8 +5,8 @@ import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { Store, StoreModule } from '@ngrx/store'; import { Store, StoreModule } from '@ngrx/store';
import { authReducer } from '../../core/auth/auth.reducer'; import { authReducer } from '../../core/auth/auth.reducer';
import { EpersonMock } from '../testing/eperson-mock'; import { EPersonMock } from '../testing/eperson-mock';
import { Eperson } from '../../core/eperson/models/eperson.model'; import { EPerson } from '../../core/eperson/models/eperson.model';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { AppState } from '../../app.reducer'; import { AppState } from '../../app.reducer';
@@ -18,7 +18,7 @@ describe('LogOutComponent', () => {
let component: LogOutComponent; let component: LogOutComponent;
let fixture: ComponentFixture<LogOutComponent>; let fixture: ComponentFixture<LogOutComponent>;
let page: Page; let page: Page;
let user: Eperson; let user: EPerson;
const authState = { const authState = {
authenticated: false, authenticated: false,
@@ -28,7 +28,7 @@ describe('LogOutComponent', () => {
const routerStub = new RouterStub(); const routerStub = new RouterStub();
beforeEach(() => { beforeEach(() => {
user = EpersonMock; user = EPersonMock;
}); });
beforeEach(async(() => { beforeEach(async(() => {

View File

@@ -5,6 +5,7 @@ import { ResponseCacheEntry } from '../../core/cache/response-cache.reducer';
import { RemoteData } from '../../core/data/remote-data'; import { RemoteData } from '../../core/data/remote-data';
import { RequestEntry } from '../../core/data/request.reducer'; import { RequestEntry } from '../../core/data/request.reducer';
import { hasValue } from '../empty.util'; import { hasValue } from '../empty.util';
import { NormalizedObject } from '../../core/cache/models/normalized-object.model';
export function getMockRemoteDataBuildService(toRemoteDataObservable$?: Observable<RemoteData<any>>): RemoteDataBuildService { export function getMockRemoteDataBuildService(toRemoteDataObservable$?: Observable<RemoteData<any>>): RemoteDataBuildService {
return { return {
@@ -17,7 +18,8 @@ export function getMockRemoteDataBuildService(toRemoteDataObservable$?: Observab
payload payload
} as RemoteData<any>))) } as RemoteData<any>)))
} }
} },
buildSingle: (href$: string | Observable<string>) => Observable.of(new RemoteData(false, false, true, undefined, {}))
} as RemoteDataBuildService; } as RemoteDataBuildService;
} }

View File

@@ -8,7 +8,7 @@
<ds-object-grid [config]="config" <ds-object-grid [config]="config"
[sortConfig]="sortConfig" [sortConfig]="sortConfig"
[objects]="objects" [objects]="objects"
[hideGear]="true" [hideGear]="hideGear"
*ngIf="getViewMode()===viewModeEnum.Grid"> *ngIf="getViewMode()===viewModeEnum.Grid">
</ds-object-grid> </ds-object-grid>

View File

@@ -1,22 +1,28 @@
<div class="card"> <ds-truncatable [id]="object.id">
<div class="card">
<a [routerLink]="['/items/', object.id]" class="card-img-top">
<ds-grid-thumbnail [thumbnail]="object.getThumbnail()">
</ds-grid-thumbnail>
</a>
<div class="card-body">
<h4 class="card-title">{{object.findMetadata('dc.title')}}</h4>
<a [routerLink]="['/items/', object.id]" class="card-img-top"> <ds-truncatable-part [id]="object.id" [minLines]="2">
<ds-grid-thumbnail [thumbnail]="object.getThumbnail()"> <p *ngIf="object.filterMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']).length > 0" class="item-authors card-text text-muted">
</ds-grid-thumbnail> <span *ngFor="let authorMd of object.filterMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']); let last=last;">{{authorMd.value}}
</a> <span *ngIf="!last">; </span>
<div class="card-body"> </span>
<h4 class="card-title">{{object.findMetadata('dc.title')}}</h4> <span *ngIf="hasValue(object.findMetadata('dc.date.issued'))" class="item-date">{{object.findMetadata("dc.date.issued")}}</span>
<p *ngIf="object.filterMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']).length > 0" class="item-authors card-text text-muted"> </p>
<span *ngFor="let authorMd of object.filterMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']); let last=last;">{{authorMd.value}} </ds-truncatable-part>
<span *ngIf="!last">; </span>
</span>
<span *ngIf="hasValue(object.findMetadata('dc.date.issued'))" class="item-date">{{object.findMetadata("dc.date.issued")}}</span>
</p>
<p *ngIf="object.findMetadata('dc.description.abstract')" class="item-abstract card-text">{{object.findMetadata("dc.description.abstract") | dsTruncate:[200] }}</p> <ds-truncatable-part [id]="object.id" [minLines]="5">
<p *ngIf="object.findMetadata('dc.description.abstract')" class="item-abstract card-text">{{object.findMetadata("dc.description.abstract") }}</p>
</ds-truncatable-part>
<div class="text-center"> <div class="text-center pt-2">
<a [routerLink]="['/items/', object.id]" class="lead btn btn-primary viewButton">View</a> <a [routerLink]="['/items/', object.id]" class="lead btn btn-primary viewButton">View</a>
</div>
</div> </div>
</div> </div>
</div> </ds-truncatable>

View File

@@ -0,0 +1,7 @@
<div class="d-flex flex-row">
<a [routerLink]="" [queryParams]="{value: object.value}" class="lead">
{{object.value}}
</a>
<span class="pr-2">&nbsp;</span>
<span class="badge badge-pill badge-secondary align-self-center">{{object.count}}</span>
</div>

View File

@@ -0,0 +1 @@
@import '../../../../styles/variables';

View File

@@ -0,0 +1,47 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
import { By } from '@angular/platform-browser';
import { TruncatePipe } from '../../utils/truncate.pipe';
import { Metadatum } from '../../../core/shared/metadatum.model';
import { BrowseEntryListElementComponent } from './browse-entry-list-element.component';
import { BrowseEntry } from '../../../core/shared/browse-entry.model';
let browseEntryListElementComponent: BrowseEntryListElementComponent;
let fixture: ComponentFixture<BrowseEntryListElementComponent>;
const mockValue: BrowseEntry = Object.assign(new BrowseEntry(), {
type: 'browseEntry',
value: 'De Langhe Kristof'
});
describe('MetadataListElementComponent', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ BrowseEntryListElementComponent , TruncatePipe],
providers: [
{ provide: 'objectElementProvider', useValue: {mockValue}}
],
schemas: [ NO_ERRORS_SCHEMA ]
}).overrideComponent(BrowseEntryListElementComponent, {
set: { changeDetection: ChangeDetectionStrategy.Default }
}).compileComponents();
}));
beforeEach(async(() => {
fixture = TestBed.createComponent(BrowseEntryListElementComponent);
browseEntryListElementComponent = fixture.componentInstance;
}));
describe('When the metadatum is loaded', () => {
beforeEach(() => {
browseEntryListElementComponent.object = mockValue;
fixture.detectChanges();
});
it('should show the value as a link', () => {
const browseEntryLink = fixture.debugElement.query(By.css('a.lead'));
expect(browseEntryLink.nativeElement.textContent.trim()).toBe(mockValue.value);
});
});
});

View File

@@ -0,0 +1,18 @@
import { Component, Input, Inject } from '@angular/core';
import { AbstractListableElementComponent } from '../../object-collection/shared/object-collection-element/abstract-listable-element.component';
import { renderElementsFor } from '../../object-collection/shared/dso-element-decorator';
import { BrowseEntry } from '../../../core/shared/browse-entry.model';
import { ViewMode } from '../../../core/shared/view-mode.model';
@Component({
selector: 'ds-browse-entry-list-element',
styleUrls: ['./browse-entry-list-element.component.scss'],
templateUrl: './browse-entry-list-element.component.html'
})
/**
* This component is automatically used to create a list view for BrowseEntry objects when used in ObjectCollectionComponent
*/
@renderElementsFor(BrowseEntry, ViewMode.List)
export class BrowseEntryListElementComponent extends AbstractListableElementComponent<BrowseEntry> {}

View File

@@ -1,18 +1,24 @@
<a [routerLink]="['/items/' + object.id]" class="lead"> <ds-truncatable [id]="object.id">
{{object.findMetadata("dc.title")}} <a [routerLink]="['/items/' + object.id]" class="lead">
</a> {{object.findMetadata("dc.title")}}
<div> </a>
<span class="text-muted"> <div>
<span *ngIf="object.filterMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']).length > 0" <ds-truncatable-part [id]="object.id" [minLines]="1">
class="item-list-authors"> <span class="text-muted">
<span *ngFor="let authorMd of object.filterMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']); let last=last;">{{authorMd.value}} <span *ngIf="object.filterMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']).length > 0"
<span *ngIf="!last">; </span> class="item-list-authors">
<span *ngFor="let authorMd of object.filterMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']); let last=last;">{{authorMd.value}}
<span *ngIf="!last">; </span>
</span>
</span> </span>
</span> (<span *ngIf="hasValue(object.findMetadata('dc.publisher'))" class="item-list-publisher">{{object.findMetadata("dc.publisher")}}, </span><span
(<span *ngIf="hasValue(object.findMetadata('dc.publisher'))" class="item-list-publisher">{{object.findMetadata("dc.publisher")}}, </span><span *ngIf="hasValue(object.findMetadata('dc.date.issued'))" class="item-list-date">{{object.findMetadata("dc.date.issued")}}</span>)
*ngIf="hasValue(object.findMetadata('dc.date.issued'))" class="item-list-date">{{object.findMetadata("dc.date.issued")}}</span>) </span>
</span> </ds-truncatable-part>
<div *ngIf="object.findMetadata('dc.description.abstract')" class="item-list-abstract"> <ds-truncatable-part [id]="object.id" [minLines]="3">
{{object.findMetadata("dc.description.abstract") | dsTruncate:[200] }} <div *ngIf="object.findMetadata('dc.description.abstract')" class="item-list-abstract">
</div> {{object.findMetadata("dc.description.abstract")}}
</div> </div>
</ds-truncatable-part>
</div>
</ds-truncatable>

View File

@@ -76,6 +76,9 @@ import { NumberPickerComponent } from './number-picker/number-picker.component';
import { DsDatePickerComponent } from './form/builder/ds-dynamic-form-ui/models/date-picker/date-picker.component'; import { DsDatePickerComponent } from './form/builder/ds-dynamic-form-ui/models/date-picker/date-picker.component';
import { DsDynamicLookupComponent } from './form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component'; import { DsDynamicLookupComponent } from './form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component';
import { MockAdminGuard } from './mocks/mock-admin-guard.service'; import { MockAdminGuard } from './mocks/mock-admin-guard.service';
import { BrowseByModule } from '../+browse-by/browse-by.module';
import { BrowseByComponent } from './browse-by/browse-by.component';
import { BrowseEntryListElementComponent } from './object-list/browse-entry-list-element/browse-entry-list-element.component';
import { DebounceDirective } from './utils/debounce.directive'; import { DebounceDirective } from './utils/debounce.directive';
import { ClickOutsideDirective } from './utils/click-outside.directive'; import { ClickOutsideDirective } from './utils/click-outside.directive';
import { EmphasizePipe } from './utils/emphasize.pipe'; import { EmphasizePipe } from './utils/emphasize.pipe';
@@ -155,6 +158,7 @@ const COMPONENTS = [
ViewModeSwitchComponent, ViewModeSwitchComponent,
TruncatableComponent, TruncatableComponent,
TruncatablePartComponent, TruncatablePartComponent,
BrowseByComponent,
InputSuggestionsComponent InputSuggestionsComponent
]; ];
@@ -168,6 +172,7 @@ const ENTRY_COMPONENTS = [
CollectionGridElementComponent, CollectionGridElementComponent,
CommunityGridElementComponent, CommunityGridElementComponent,
SearchResultGridElementComponent, SearchResultGridElementComponent,
BrowseEntryListElementComponent
]; ];
const PROVIDERS = [ const PROVIDERS = [

View File

@@ -2,12 +2,13 @@ import {of as observableOf, Observable } from 'rxjs';
import { HttpOptions } from '../../core/dspace-rest-v2/dspace-rest-v2.service'; import { HttpOptions } from '../../core/dspace-rest-v2/dspace-rest-v2.service';
import { AuthStatus } from '../../core/auth/models/auth-status.model'; import { AuthStatus } from '../../core/auth/models/auth-status.model';
import { AuthTokenInfo } from '../../core/auth/models/auth-token-info.model'; import { AuthTokenInfo } from '../../core/auth/models/auth-token-info.model';
import { Eperson } from '../../core/eperson/models/eperson.model'; import { EPerson } from '../../core/eperson/models/eperson.model';
import { isNotEmpty } from '../empty.util'; import { isNotEmpty } from '../empty.util';
import { EpersonMock } from './eperson-mock'; import { EPersonMock } from './eperson-mock';
import { RemoteData } from '../../core/data/remote-data';
export class AuthRequestServiceStub { export class AuthRequestServiceStub {
protected mockUser: Eperson = EpersonMock; protected mockUser: EPerson = EPersonMock;
protected mockTokenInfo = new AuthTokenInfo('test_token'); protected mockTokenInfo = new AuthTokenInfo('test_token');
public postToEndpoint(method: string, body: any, options?: HttpOptions): Observable<any> { public postToEndpoint(method: string, body: any, options?: HttpOptions): Observable<any> {
@@ -26,7 +27,7 @@ export class AuthRequestServiceStub {
if (this.validateToken(token)) { if (this.validateToken(token)) {
authStatusStub.authenticated = true; authStatusStub.authenticated = true;
authStatusStub.token = this.mockTokenInfo; authStatusStub.token = this.mockTokenInfo;
authStatusStub.eperson = [this.mockUser]; authStatusStub.eperson = Observable.of(new RemoteData<EPerson>(false, false, true, undefined, this.mockUser));
} else { } else {
authStatusStub.authenticated = false; authStatusStub.authenticated = false;
} }
@@ -45,7 +46,7 @@ export class AuthRequestServiceStub {
if (this.validateToken(token)) { if (this.validateToken(token)) {
authStatusStub.authenticated = true; authStatusStub.authenticated = true;
authStatusStub.token = this.mockTokenInfo; authStatusStub.token = this.mockTokenInfo;
authStatusStub.eperson = [this.mockUser]; authStatusStub.eperson = Observable.of(new RemoteData<EPerson>(false, false, true, undefined, this.mockUser));
} else { } else {
authStatusStub.authenticated = false; authStatusStub.authenticated = false;
} }

View File

@@ -2,8 +2,9 @@
import {of as observableOf, Observable } from 'rxjs'; import {of as observableOf, Observable } from 'rxjs';
import { AuthStatus } from '../../core/auth/models/auth-status.model'; import { AuthStatus } from '../../core/auth/models/auth-status.model';
import { AuthTokenInfo } from '../../core/auth/models/auth-token-info.model'; import { AuthTokenInfo } from '../../core/auth/models/auth-token-info.model';
import { EpersonMock } from './eperson-mock'; import { EPersonMock } from './eperson-mock';
import { Eperson } from '../../core/eperson/models/eperson.model'; import { EPerson } from '../../core/eperson/models/eperson.model';
import { RemoteData } from '../../core/data/remote-data';
export class AuthServiceStub { export class AuthServiceStub {
@@ -20,7 +21,7 @@ export class AuthServiceStub {
authStatus.okay = true; authStatus.okay = true;
authStatus.authenticated = true; authStatus.authenticated = true;
authStatus.token = this.token; authStatus.token = this.token;
authStatus.eperson = [EpersonMock]; authStatus.eperson = Observable.of(new RemoteData<EPerson>(false, false, true, undefined, EPersonMock));
return observableOf(authStatus); return observableOf(authStatus);
} else { } else {
console.log('error'); console.log('error');
@@ -28,9 +29,9 @@ export class AuthServiceStub {
} }
} }
public authenticatedUser(token: AuthTokenInfo): Observable<Eperson> { public authenticatedUser(token: AuthTokenInfo): Observable<EPerson> {
if (token.accessToken === 'token_test') { if (token.accessToken === 'token_test') {
return observableOf(EpersonMock); return Observable.of(EPersonMock);
} else { } else {
throw(new Error('Message Error test')); throw(new Error('Message Error test'));
} }

View File

@@ -1,6 +1,6 @@
import { Eperson } from '../../core/eperson/models/eperson.model'; import { EPerson } from '../../core/eperson/models/eperson.model';
export const EpersonMock: Eperson = Object.assign(new Eperson(),{ export const EPersonMock: EPerson = Object.assign(new EPerson(),{
handle: null, handle: null,
groups: [], groups: [],
netid: 'test@test.com', netid: 'test@test.com',

View File

@@ -1216,9 +1216,9 @@ boom@5.x.x:
dependencies: dependencies:
hoek "4.x.x" hoek "4.x.x"
bootstrap@4.1.1: bootstrap@4.1.3:
version "4.1.1" version "4.1.3"
resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-4.1.1.tgz#3aec85000fa619085da8d2e4983dfd67cf2114cb" resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-4.1.3.tgz#0eb371af2c8448e8c210411d0cb824a6409a12be"
boxen@^1.2.1: boxen@^1.2.1:
version "1.3.0" version "1.3.0"
@@ -1965,14 +1965,14 @@ copy-webpack-plugin@^4.4.1:
p-limit "^1.0.0" p-limit "^1.0.0"
serialize-javascript "^1.4.0" serialize-javascript "^1.4.0"
core-js@2.5.3:
version "2.5.3"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.3.tgz#8acc38345824f16d8365b7c9b4259168e8ed603e"
core-js@^2.2.0, core-js@^2.4.0: core-js@^2.2.0, core-js@^2.4.0:
version "2.5.7" version "2.5.7"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.7.tgz#f972608ff0cead68b841a16a932d0b183791814e" resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.7.tgz#f972608ff0cead68b841a16a932d0b183791814e"
core-js@^2.5.7:
version "2.5.7"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.7.tgz#f972608ff0cead68b841a16a932d0b183791814e"
core-js@~2.3.0: core-js@~2.3.0:
version "2.3.0" version "2.3.0"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.3.0.tgz#fab83fbb0b2d8dc85fa636c4b9d34c75420c6d65" resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.3.0.tgz#fab83fbb0b2d8dc85fa636c4b9d34c75420c6d65"