From 06d2ac74be4b2530fc640a46451ac85452e7c5bc Mon Sep 17 00:00:00 2001 From: Art Lowel Date: Mon, 28 May 2018 17:22:17 +0200 Subject: [PATCH 001/604] added custom item page fields for Projects, OrgUnits and People --- resources/i18n/en.json | 32 ++++++++++++++ resources/images/orgunit-placeholder.jpg | Bin 0 -> 71990 bytes resources/images/person-placeholder.png | Bin 0 -> 141375 bytes resources/images/project-placeholder.png | Bin 0 -> 320715 bytes src/app/+item-page/item-page.module.ts | 24 +++++++++-- .../item-page-abstract-field.component.ts | 6 +-- .../item-page-author-field.component.ts | 6 +-- .../date/item-page-date-field.component.ts | 6 +-- .../generic-item-page-field.component.ts | 20 +++++++++ ...nt.html => item-page-field.component.html} | 2 +- ...ponent.ts => item-page-field.component.ts} | 4 +- .../title/item-page-title-field.component.ts | 4 +- .../uri/item-page-uri-field.component.html | 2 +- .../uri/item-page-uri-field.component.ts | 4 +- .../simple/item-page.component.html | 22 +--------- .../+item-page/simple/item-page.component.ts | 3 ++ .../item/item-page-fields.component.html | 21 +++++++++ .../item/item-page-fields.component.scss | 1 + .../item/item-page-fields.component.ts | 23 ++++++++++ .../orgunit-page-fields.component.html | 32 ++++++++++++++ .../orgunit-page-fields.component.scss | 1 + .../orgunit/orgunit-page-fields.component.ts | 18 ++++++++ .../person/person-page-fields.component.html | 40 ++++++++++++++++++ .../person/person-page-fields.component.scss | 1 + .../person/person-page-fields.component.ts | 18 ++++++++ .../project-page-fields.component.html | 32 ++++++++++++++ .../project-page-fields.component.scss | 1 + .../project/project-page-fields.component.ts | 18 ++++++++ .../relationship-type-switcher.component.html | 1 + .../relationship-type-switcher.component.scss | 1 + .../relationship-type-switcher.component.ts | 36 ++++++++++++++++ src/app/+search-page/search-options.model.ts | 12 ++---- .../search-results.component.ts | 5 ++- .../search-service/search.service.spec.ts | 24 +++++------ .../search-service/search.service.ts | 8 ++-- .../search-settings.component.ts | 5 ++- .../entities/relationship-type-decorator.ts | 22 ++++++++++ .../object-collection.component.spec.ts | 6 +-- .../object-collection.component.ts | 8 ++-- .../shared/dso-element-decorator.spec.ts | 6 +-- .../shared/dso-element-decorator.ts | 6 +-- .../collection-grid-element.component.ts | 4 +- .../community-grid-element.component.ts | 4 +- .../item-grid-element.component.ts | 4 +- ...on-search-result-grid-element.component.ts | 4 +- ...ty-search-result-grid-element.component.ts | 4 +- ...em-search-result-grid-element.component.ts | 4 +- .../wrapper-grid-element.component.ts | 4 +- .../collection-list-element.component.ts | 4 +- .../community-list-element.component.ts | 4 +- .../item-list-element.component.ts | 4 +- ...on-search-result-list-element.component.ts | 4 +- ...ty-search-result-list-element.component.ts | 4 +- ...em-search-result-list-element.component.ts | 4 +- .../wrapper-list-element.component.ts | 4 +- .../testing/hal-endpoint-service-stub.ts | 2 +- src/app/shared/testing/search-service-stub.ts | 14 +++--- .../view-mode-switch.component.spec.ts | 10 ++--- .../view-mode-switch.component.ts | 10 ++--- src/app/shared/view-mode.ts | 11 +++++ src/app/thumbnail/thumbnail.component.html | 2 +- src/app/thumbnail/thumbnail.component.scss | 3 ++ src/app/thumbnail/thumbnail.component.spec.ts | 2 +- src/app/thumbnail/thumbnail.component.ts | 4 +- 64 files changed, 463 insertions(+), 132 deletions(-) create mode 100644 resources/images/orgunit-placeholder.jpg create mode 100644 resources/images/person-placeholder.png create mode 100644 resources/images/project-placeholder.png create mode 100644 src/app/+item-page/simple/field-components/specific-field/generic/generic-item-page-field.component.ts rename src/app/+item-page/simple/field-components/specific-field/{item-page-specific-field.component.html => item-page-field.component.html} (77%) rename src/app/+item-page/simple/field-components/specific-field/{item-page-specific-field.component.ts => item-page-field.component.ts} (86%) create mode 100644 src/app/+item-page/simple/relationship-types/item/item-page-fields.component.html create mode 100644 src/app/+item-page/simple/relationship-types/item/item-page-fields.component.scss create mode 100644 src/app/+item-page/simple/relationship-types/item/item-page-fields.component.ts create mode 100644 src/app/+item-page/simple/relationship-types/orgunit/orgunit-page-fields.component.html create mode 100644 src/app/+item-page/simple/relationship-types/orgunit/orgunit-page-fields.component.scss create mode 100644 src/app/+item-page/simple/relationship-types/orgunit/orgunit-page-fields.component.ts create mode 100644 src/app/+item-page/simple/relationship-types/person/person-page-fields.component.html create mode 100644 src/app/+item-page/simple/relationship-types/person/person-page-fields.component.scss create mode 100644 src/app/+item-page/simple/relationship-types/person/person-page-fields.component.ts create mode 100644 src/app/+item-page/simple/relationship-types/project/project-page-fields.component.html create mode 100644 src/app/+item-page/simple/relationship-types/project/project-page-fields.component.scss create mode 100644 src/app/+item-page/simple/relationship-types/project/project-page-fields.component.ts create mode 100644 src/app/+item-page/simple/relationship-types/switcher/relationship-type-switcher.component.html create mode 100644 src/app/+item-page/simple/relationship-types/switcher/relationship-type-switcher.component.scss create mode 100644 src/app/+item-page/simple/relationship-types/switcher/relationship-type-switcher.component.ts create mode 100644 src/app/shared/entities/relationship-type-decorator.ts create mode 100644 src/app/shared/view-mode.ts diff --git a/resources/i18n/en.json b/resources/i18n/en.json index 53ae9015f6..06671e0809 100644 --- a/resources/i18n/en.json +++ b/resources/i18n/en.json @@ -45,6 +45,38 @@ } } }, + "person": { + "page": { + "jobtitle": "Job Title", + "lastname": "Last Name", + "firstname": "First Name", + "email": "Email Address", + "orcid": "ORCID", + "birthdate": "Birth Date", + "staffid": "Staff ID", + "link": { + "full": "Show all metadata" + } + } + }, + "project": { + "page": { + "status": "Status", + "id": "ID", + "expectedcompletion": "Expected Completion", + "description": "Description", + "keywords": "Keywords" + } + }, + "orgunit": { + "page": { + "dateestablished": "Date established", + "city": "City", + "country": "Country", + "id": "ID", + "description": "Description" + } + }, "nav": { "home": "Home" }, diff --git a/resources/images/orgunit-placeholder.jpg b/resources/images/orgunit-placeholder.jpg new file mode 100644 index 0000000000000000000000000000000000000000..11564bc6357bf93aa88ab107d923c742051f42f6 GIT binary patch literal 71990 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 literal 0 HcmV?d00001 diff --git a/resources/images/person-placeholder.png b/resources/images/person-placeholder.png new file mode 100644 index 0000000000000000000000000000000000000000..eb6232e00f835b8efb3e72b65279e8580a513d51 GIT binary patch literal 141375 zcmbrkbx@p7w>3I5xLa^{x4~V5ySqzpcL^Tc-4i4b+-;EH4uPNv?!gAPKrZ>c=bZOF z-*@Z&bEj&mdb)deKfC*>z1LcMV$@V*(U6Id0RRA+yqvTK005eO{azu0UVGZmK5GI1 z1S)n?Qfl&2QWR?LE;e?K)&PJWQjNYJzqaY!#zDT&mdTaygwno1&Ve79X2*U`En_T= zycW1TS@KipzLq2dZ4P#sygEY$Jk4%fUDWxVU(wz3xliH2$yJ9#UH7zYP4|qbZ4F8Y zAcqJCC$tfSNr)@!(aM2$bA5?N+XG8L0RtZb-4PJ z@@M@k9wxyt24EYp>ltM6OikGX3rJiMi^4+y^uUY{;p|u(`eIwC5YZrBV3Q)$S%9oC z_@9H#W3)+iZE*mItAONDEhbh1fQ=>tzA_Xe$~zmzj66U~{M{RpR$PmqKbTDE7krWh zEh#DAJcZF4;x~FvVE>?KRtCR!(y09U{({|fQ_k0Nes(z<(SGh%(w`YRFcsOb!wqIQ zt3ZU!5o%pNhqLw8H(pPrsC%RIqHdVM6HLBH$)w<&!en1XzUZUJl9vWLj?56^Z5N=H zCUGZfJfzoH2W;ddV*N@pP2;6eiu+xy}5n39gUJr3t(HG+lQ9urF26Jw=( zY7sh5(Tz}64q&!n$}eU~l4mS?@4YNb$mY!~)jVCM!gWCXxJe()`d${~~7L7k;Pw=_0pJ zEbo1>ClSkG6~hE1Zj)f|r53TErJ=KbsG359LT81D97|sAp-CdY4FknqcwVrA-~54X z55Lf6?iE=h$yIq8*sQY_;y|u>j#Z$D*L#!wSQ4HVR?Xu&%r7!Z?mptSa~}#)A@#S2 z;0o!n6F(|C{H8zhXYW>(<|#$HOGy9sf1ubyDJ`-5%p! znON`3qe3LMAj~64-wio_I}duTm>D){nluU;dJGOvSeI*Yg?4)Hwt>2~H7*+9FhI~4 zA*61)DuAbd?I|gq2pH1J1c)F0Gx}*uRLvAN>t^e{l?K#rcOJMI*7RPObt_AR zJ_JsbFi@Nz8iw#NjcFFPvkx;H=&%Ve3dLWBEg=I@Q65Xzlb*olP#}=NzTHHu0B9HE z<-_6kY0ttshYD?q9>OH`X}0sOhuoRLPlmpQm54@!+Y-OW#FEBNk=4R-k|yVra>TM7 zM7ekymcl?owvu=v>7E?8i|9^LDK$;Klx#REQcCU@29YdJW*^0PBQ?fA&lI8XQK5p@ zK4P`F_E3hKU@eTUgzym93|k{TQ>;5{co=kzB^cWyYs(qJY5p^tpghFRk}ex&KLO61 zn+11bIJQBdnyd@`;E|FyR@oATACw3~AFCO1Jdb@4bv5GjwQT!#D{<4_P1lvQ{X@HM zJK;}NVL0~atdY6_!!41EulLe{bUg&7;l1&_Xhj8l_X42MKILS(cVBJi<1l5GWtL?b z=(#Zske(wr`&m-tcB#j4ZlckpnK%=(<%N}mr5q{IF*h)K)3W8LXtYxW#`500RiMXB zQ%T8{hbcvxqk>@$rx_50M~DJM(L@KNHfU^f=i^f&K!@w=dZQr$U?_?`~%i=)e!2Mu#TzlLr%^{t5Cv&@XJ8^S* zucuFB@Ht#IdL)`Y*_K^ZWCk!JTnDJ5bfIwJb%8mjUMCOy6lfE;73deJ`%rkbbH8_A zaF6qlbh)u#gtz%-78i;?iq}rTK^2WWjwFcUj8lz$f|rkrjn{}7izUeNosFAylGTxc zgwBZJCyOv`KjkNOY$gqcIjT#Qbj9y-+>*`G%~GB+N@|lDEG8%IdP&RML`OW%4s%P~ z)qZT#47IFeG$st|bfVOhROa*z#@o$<(KO+utya;ylw&GmevEdE7?mO#x@B}_eq}YK zX=SR4Vrd^o2}T`LD$@zmX-DlRq9!sYZpS{l6S-5mm$`ReRzS)FD*`(L{}}u>C}>f4 zU|vFJPR5OYgZ)N@8KSYP)2ywe3;A-dE-yJ|$f#PWdGxt4%00rp0mpa_#rkUq70EZjdcew$#^DOe@!~6DXtoxL2iJ z&Qm>Ad0Nq}`c#W~4{&+c`R9+AqBHgMK(*qyC^5Vf=j$+OwH%t<%;UB)=IJH7l^ZH;r<=y=wS zRUg05qFG>Xar$td7*Fal$1-Q{13p9Dc%U#A5D|O+!Iosa#1#Ra0}p!}o`@EV<;zcy zte13srb5ZW&0T2Hbvobb7HfiO>~c@{i1o<4eK%ez?4!eMe|}u@33Ly^(sv()4#CPQfbj@b=P>;oe6&o`cxa$ zOZVC4!Rq_iH-m%KM8TTQPR+2-?y4o9cT`miE(GR%7Ox#GyCt75;LoD+JvNPgclaCx z&0rYUcg%XLKUqA}jwxDa39#9kO4QOA=X4Ocdo_QnYWS0T)6#m}xx~Jq261bYudckN zTblZ5GrS+apOLwnxw4qOcx3psZFrTpO~mQWk^IE<*I|c~$I}_sehDpu(uxLTg@$>Oh9s(-7vj}&PukEZo^o9CK z_F2kVD(OQ|lq@-daA6?!#qO0{|LFF7v+cd@%8Y&yelKRA`h&);^+3h`;goK9U8iXm zBv^1?>^4}~JMc((*l_&EHd0DVr~JZLIpbAjj}P&Bw;%acu$$l&%#RO^NnmpJAn@<+ zB0B}2E^h6NP?hwVGp{~Enk3QDOeRS4MLxyA+kdC z#{BpYN{QT2)op7`^Y508^dB_Q!KRiuHkxJ_yA|6cJDEH3+fG}ySWwJryho-+6K<)K zQ2xA#rrqGmC$PAVGMn7X>?&KNA1$FZeFpc!ynPb4nlf%p!s?2k?qaur>bLpDJDz0+ z-}S>Bq%p1e$&KH|Z%g)290q0j7yH>JMVGcxcPu}jv;C~KcSb);A$t-z?mi3XUFF!} z>UDGDf?AheJ}}-B!|@|Hqa&l1N3}=PV&I_3_RCTmP$j)RjYW@f`}s*3CxLb-IF>lp zMWsTSWjQzZP0C5jlzo>;tj?Ex^aAdZ0^TFCE)dtY_xUu-Pt5`^Hx~zQpXu42tS5T( zz|m)SS6&yk_pN5FP+871Ox>~70X*5?iazNX=}Y8JmF@2xq4#oo?-sOf+K5hQbZd+X zE*OjEV{DOUOe)S@?gI-?!&cvBDK=zbm{fN8cV8^F+#c8PIddELtjroMX)G87S5>ff z1qH}1J7o#w7C;J6d}PmWyw^H5)`N~EG*zWsS@rO$o8 z?^?Z6pSPcHA}11kd~fmO={M}Qe|m6g>f7`4A@_;ZK2s1McIn%F(vOK)X6C9Z_QZUt z0S;|PY#*{B87R%Kls5#Wgk?F4$r+5t*#&yP^)*+Rmh%M4+x0qH{3;;&fm!#ah+w2j zBo|D191*s~#{J6sYMQk4=;o;SjOse0)WEN+SIBFzY2*C3oC`Ph>%ZKH1acP)mYT^A zAtrZDA`+_NrbB(x9Q_PS_xbWgM+hFNk;!FWGICn-5_+A|*L0q=J{Tu<6@Uwxj2eya zI;^{FeQErnd^`N}oH?MJ8;9MDZXPM;0`_^PS$|GLCPJ+xeuRlgrb~@|oHq~JTB80% zrC8!#GH?0LFz{Ve164zuThjInofX}q6OW>XO6D?Ma{+rKbA}g!`H0c#ae?vCfe)@V z&DnakUH1t0gf}}+fbDclkk4%|Ka|twb~A9@@ntD2ryt|({17=tkl3WZ^k?ob)#Z?b zL%pHz4SXY}I--@o>Ush`b(9}j>7IVjwD`02( z!uLd*cNAL2HqdNxdz7!rGOI!`8hPFo5Nx_lyB-;D*wu_{i#E*1CfJX?wJ)-?rr}|Hm_M)5R zGrQBFy_>l%Zz=A;KkSZ>4o0V@81}KYB7=SWvtH2<5)N!%@H@qNZ>WZx;yZ(EbACjZ zgk?fI?8vxI(r%$)hW)3^d2aUm>DoAt=qEhW)Oyjaw-&29A8Yv10@FIuWLpc?dzgo~ zwCTK@WdjVpPH%e78@m%ix^lo0`)wkY`WUyoR_7BR&onnwe~P(p8ady7g$l<7-u@PX zBStPqNsEh*O~9}wr6V0%WE|Rt-w(_*~p6GQ@FWR~D-LLo5 zUfq4@%nGmy;JYUM_5MQ5%k@u5(g0ghT|%@7tAFiv>Vx*S@3hq%!V|j(ipurFAMJpW zUL3?+z?(kd`5-aj&w$Y`BZB z9Is+PX&%paVA79y8HH<{tC$eAPjD2wKXH^QibE{^?d?DylouRDRY71M4__Q$mdO|L z(l^K{#6nm#6y-+qQ_zT}7Woc#Cb2D)P+tg0Nq{|v=p-%tE!Q{sKAT1+mcrPQ0_@B2 z9sJv@*;06#f5vJ?OK~{mdGz#+{J6=utc~tIUG~v#189-8ddJbkvHN8IgioMZpw_i^ z$ML{@pJ*rXfD-!VO5uX{a^*_-(iW)#F&;?=@d0T91YQ0?m>`)?Swr=cSe4|vcV4Kx1bys^EZnVs9{tQ>7i6NKYrM~ z`SGan$p^60jH&lve4tHSi1 zRH(#A%>U)SOj$Rlc7e~yF~tR^!zs9}G4Chyycl0LAInwgpOtS}A51VHHw6aweJ@9o z_)uj(729BI$|u>K?ajG_y)Q+a%m!6Y2#p3FW(Xj0# z6Nr4DBJ`*w>Z#O_ssMHBBuojbpSZ6PR}+K#VL6E%g%(2+1{89**bsb(lQwUlKcIY2 zMOc`Awtf*R=5KMnVfT?%(o7WH*0a_OX4y8}A3S;ncgH-DZ2Gw`Dc*i?kI#wmW4?lo z!&k(p5^!M76E#HXLJGkDfHLBdo%6CezL&YvGt(5Am3N0+gtKAzLIaEyZz=w`*{)Vt z--6gQ>hUek0%0*CmRJ$00Lg+ddZ`J@4eRM>BpMess_sgwC%*5x~W$6HFqFux6@rHRtbd5~LhvvEtF1 z`oZ-&j)XckscuVdioVo=yusNNip%o8D&OgKQsqkD%~6D-O9@aTJ5VNNE9E#RdyTd+ zB&N4b9^%W?A3 z!(rh(b-&AkO$pC1T=`F%8@zQL$HnJX*^?5|wR~-3nV*ogE)mvlGH1lghaS5ucFBZ` zfH%dsdU%tVja>1Z5T-dcqCumwwXop3jb_8bG?wqXC%b# zu{BFOx0lYWm=I8>xOUi;v;JvCa6xBr%N1eCZ^z^$=;7?5uq3DuCLaTVY!BT~%!E+j zYI5sgcjErr-qO*|a^5wPr~8u7&nMD(a(19*ZKzDG&WIMEwc5JI7*EP3hlwMKqm3)ds?KD}!pkzwnt2%v_J0v>3af6f$^SI--Z7b| zTDKpDl6c^9CoUtSP6L2>1=O21)Yq>z($}vI8yn^V7*Gd47fI=s|54StO-=0FB`f<@ z_eb;vgW7a_ikn3i1pojR*lFo`>M1J;TDmx~nOnJ7ShM*$xxQ8&003cM!PlmfwWm3S zual#*hoG+r)ju-?U)z7X*{LZ0nd0dnLZzpyMj_?mZcV|%#?8h-C5lWzK_TpJWh1B| zE%RUIufIg7-g$bu3bM2N`1r8-aIv|#+p=>C2neuqfZ4%d*4G)V9)8ZA=Dw`X9@PI@ z%f0F^sm$Ur}y=^M3IHr|L5pMkz*3E>Hz?8fV{MXmM`$6`>2U_#BFa3WH0ft zKAZ+nyUCP-O>2Mj^2cd%*m1J+v*BXn3wVCsKwkb0W9l+;3vdUEekZH&49o4R2dXLKgV&{M)DL!DMYB1b-)?I1}@~h7O~3ZPjB$ zDw27ZxIH8*)^1tciZE}KbSz(Y9HZn-Q1k_w;~$&I-fLT%XipD$@4Fh$qXz;pjEQi+w==q`WF(j{) z>)^qzD+nQSldyb51$7VaoYGE5GKhVU;}ujSH~M(u|BV03?1d35wQD^o=(TVfKU}R~XE} zK(&U)vnVBfVPa}Lg`)U`w~r1Z7)Uvhrl&F0rQr1IK0Ds-vs;+ zwB;>DGDJC~6JZ0Tn8V0q3US767Vk3^=^Y6|}iOVO&{B z1aq61F)K!nSfN`-l>xceCQ)1}4?V!`E7S`>h|7zHe$so)u)xnIo>}MXE zxZ1f)uih9)hUshhAdwIB298%xPRgNT^yLw@#NV+dUHEfg-Di&26lFo@%&vU>#oVp0LO3rFQ`Xf$KEC%l~6Pw z(4|x79LVBvlfiP*0vfXWK43S9A8Pn-Hgqr?sM|71eII1D)`KYnCdoEPM8S0vSdkS2 z%@Ah66z+4ExiQ&FeD?}&#o^|A&;Ymk47=dW)$^1 znTwq7+~5tUPyc}?+t{TsT%;IR=6>$l8y3Gb{o@-({AFelMi<=^YeLJ?J;HjRt1gls z_HO>iz2QF>?_bu!5$>G4pJft1p1mUxay{v5fEhvmuZHbe$XW`Eskgq#7QZv!^v|zS z!T)QV-Y3N6@_yWX;Nszc*=tS&FaKh0oS()=rXjQq%t6WI1A{Z3c1v-Xc{N!}n=J5^ zi_G$x#usEY8P=C32BL|F9X|i2bn;{{{)2nGqw5Ui%63H3LDC9uAl{tYAVgtHL?CoG$0CMXcMIfB09iv4;_F34u+ zQ`s?U6+@#nD|B3&VA!Jty1Kv9tzYHa7%*=my_qg)Ce|zS1}0qNhl{VBLBwk_SM@9$ zd?``2AS@*oC2oFG}YPIbnOsu_(EQW0aBo$p1gNWw%WO|)Aj4?%{yL(E5WKrM8s*B z!x*P`PNFww5WRFS@l|hlp`u+3m39`OhqpLuK$VG=U?f{%EQN|EiqLwNx;E}CN4N`H z1*g$BEtpT{ro>U^kIUint<9%8umjmh8Iv5nY^6g4A=?4ncobxyUq+kA^G06;kgyEV z)va}3oR`ni9%h$KSyShYSgSUWs0z&WygoyhbyWTkbet(+L^oa(CX%`lH$)R7Zd@U~!k zKlRb@E1iS@vYjpAsnNP4PV-EELUWDjCG)u$TV`a-p50IEJxRyaTs2wE)<|yPu}@~Z zvAuldHzy!N46fcV<<%B?R>njzG?s6&$8G;dd-~DL?wiV<4?D5J7g8#Oq`FUdoCiy*Mf`%j>MP z1SspevUPA_<^b!97HGTsc2ax?1K53O$Sw%r8d*3#a_ibi+O$p9PS(@3gi)m9o5-6K zl4pIkdM1k^0o011Zll(tsM%QC$XNIS)+Z~hAufLLOW`qnZNj`y5Y#XV^m$q1i;`N& z>NvpWm52;rbj^8U0=(`bfi!9t+7Yi|f(syBEP|Uq~!N5COUyL6< z3BwII0T{qbs~DJYVWl0F^byKos50);ki#nRnbnYN-;i0Jq}#wL(lRDWTHZXp7C zsz^AoVRJGY+$i9kn*djp(aFO64jbEj75zdlbIZhB4nGC6{XH7L_ssE!M0fA3 zgeysqWCDFe0!A?j#zl~X>KLT$W+O*Zcva1i0Xw*1V?@~~*)s*wQwcqah`#1!GmH8_GLxT8~U0 zRrT9Lz{&H#TKDpbfloTs%wVBqVK@ z68s#XYs+$sRF(^piD@v{$(V1oU95~U$56&d$x8}3-EDhHZ>gN2muo_GghxV-BS`-v zDbMG0(2d2Ou}q(?8BJETN=iq*Wb1dTTB?GffaiYZkCE@_70c1_^#<&FS~T1#nOqti zF1H!a_585DyDn9$RkB*ir!}_Jr(R0H+y6vr!o%aepx21o4VO+z_brrm zJXg6sin%5>!6;*@$2{GPQpu$eN~Y^0VofMG=^ZFfd$OiZCIrnt4+(;j_v+sZypKf=WI)?F> zI4ca{TazSZ9IKMKuD*rt6+DL&3c|7w7Qp~c<9;6yVYsiPee-lMkWzxS(RnG&?-K!S za+OA!h*mJJLIk+k!;hMrE*z=*LodKP|uJS-&_?Fx?b1V+9i?`kjdXc<43u~Me$ zxKq>NZJtat7A$Rc3TuMPLJ11*8x7!i>(iB2atq@6+u!Aup*s1(>bU6G(?XrhJC`l0 zYqBXCTlB)m>LVN&S?gSA!9aLmW}LlA+K`TyB=WK!(58C*6S7{eOjHE8qUBcvZ(GVS zVoO4u>=yGnY-=qyOC_;Jys~PvDdZAgX1v_=inxiUSz~O{`1+KTnH|)$X&)9@87PmJ ztYJ}56^wh|h@>Z#LivurH%qD3y;k5@hg&M8av}S&2hA8izmtH^pP0s4k)h-*?=U^< zvOx>8ivTsA+LE1BIvFIL&SP9#bBxLv6rSg#9^+VL6u)OI0;zU8ZX|Bncc?I$>SAj@ znh5AbyN8?Sq0DOC_f_obI8IqoFp=j~W%e~$S?8JtarBeNaqaDwhExeaN=s8d zo=iiAAx}aCqj+h7U$_y~l8=LLEI1{}IAf~YJi9DEbFumODZ!zP_=~AVLq?Wr*vN*4 zU6(sV?0;RW!;jEC+{ABf0Y*Q5Qs4h~j{PUu_DBoWKctRl9d;%uiu^Alc2|T<;>4YU z%icZ_F&WZ-V@PP(kI=I$Ex`?44L%u&7_kVpYU`Y#T*x3IwKyRx6uvL^hBmw&f$m2c zjWormoD}@d=|{z*(2;#K+J@oz)X6MAhBda36BRA|1O&3DmIfM}T|O~55-TYD?{5Xs z;>O&=;V|_Y@nVWe;N>l@SrEvjEV}?A$-e`~5h-pde7io05Cw)A>c^SjcQ`_$a|l#a zUvcMLpSfKAY=?IMA(*+xG=B?M_`6slz7N135ivIrp$tIkiwD)%D|t3j0Xko&W-IqDS#NH;8-CO(yl)jHf^j%oBw%u(1Y*0HNeZME>|(*1 zJp@0;!xYcNw{-4~hVjFEdWS`21~H|7q{xGs2;@>JA8LSPG?mQa(v_-i@bDD6bUbUafF9Jmd)e9Z;B%Lk8!=XM`HmK=2xy-76?z^9EXlbWn^k za3Kl=lUPvST%#C<0+Q$dQjGP!58w|0GTSM%uLQ%=+NQe*mpct(>|}Ljaw&P~B<;Xh zlM3M5g%4Y(A#)OpbM2Ud4Ge+B2+Tc+FuTq@SsP2L+3Ug1sczu}E9&2*rR$}}>b0!E*K1_S-^&pH}dj!W;QkYzwN1IG6o zaFRMaA<@MsY$pxTCo=%SH^8h_$bmy%7pC6>v z7XjE0W5G##NG`$70d^B&w`lo?{ zIEDATv_vpM2mvTa$yZ07z?JsTk?Adh?z zQu`5_Ry36dop>@a9F!phsPbAT*;t4fs9vGR7ru%`#mo!MheEMVz{KQQ-+(fL0WoTU$8{6=z_MC^B}y-?!T@*h*qY0#g%&8HW_e zfQ1slR)#>97w1mql?j$shG<0bs1ZSZ-w<2r&BjUW1Bn3M09F_(de*v}=&@_{P1Mp4 zpWk?-Vt~qt>vhmC{F6AndO?hH*ig5RG^QFIiW{y=f>583l75_`vdmg#d3dCL?Te;3}w{-*DM<9g~(aG128-n%lphx z0nz$YflqAwFh^Tbe*c_LF&xyYk3gBEkgORD9rNre^%_d*T!u>q?uSv7Oj7LLyG18_ zpJ#W31Bt5~zhVzR81iwFGr}0z%%3+3Nf|je!8)@_qBKnoVqB(DCQYh^KFTqG%~KOh zUj@R3C*|3uw+gTTY+ihejWIsoW)1*Tn#MRH(Y~97^Xs>XQ3_dGBg|K(h0$ zL@O!3rAOZ0#O>c#3b_ANugQxc&>7&p8MpE4+aOl&0n~sLZ-^jX-d1tUaP-Y9 z#1~JjkzJeouG8_$@lH48^w*;}fgIR;7Pc5%BwxHonAFr4MU@R8Y@&ef!pyzRkd>xk zh1pz1OEL)Tkw0OLpD?4^cW*=)J{d*jhtwitXNljifemL+Pl1X$lGLFmaI9aWlHf3tJhu;mbwpWj% z?*vVkrt5){GV*7FTbyPa<&;r9q{8%fzu<7PiCfS!=o*l|bn>zdn$MP=)Yj+C*N)~E zz8HJfF`vp$UlH@vq6WB0Nus__QMHiUW5U}X*MUj4iFg#m%xNAC4Rh~$_j zI+2{waB@*-r}yonbM#C~9~WiLt9~2~5<@E81A5DGzBsa40j@)wkF8X^t)s9&CICD{ zYh4$sWkE|0e^f$1*`;*@7e)|X@f$>tsSPR&l(O)|LJmWwDg(ZZoW{}ic$ITS$_2r; zm|c%n2w^9MjTnog3>UKmV=Y6 zj_LVQN72~EXZy>yV!TG9$_&9)U4u{YF8gJk@q)w>I0tv5nVIgtJ9sl09V@@1*7T|5 zai^3=ASCZ(Pk}Nmo~8%2dsQl}v5?xYzL@KvaRuAu=sgZE-LWdw)ln>_l_S1lslQ8+aV*n0;>9OU-jpW>8XhxtVH2M-Ka? zr-H23Y!yAPfs_uJycmwvRh|fC48el;6lWSQSaQ|WDXg%tSAaorZfs$e3HM~{ZZ&2N zy%YGL1Ee81>*;S$Cm;G>392BJ>dC&@QM88(5Wmfm3KwAt!pwXKX$Wy zrugwIgqD=bP0f!j9Qras7rj$A{E&8w@8U-gYhm(*-j$MHEH&)=?Q7TUesSNgLNi8t z?w2tg;m5;3TsoW(A-a|zQLwTNl^>m6l=gO~#W^o6(_@F;@6J+W2h<4RM>IotVev=5 z2C&^**cdORPoc>^87LC!^qN`rVf1{HJ07SryQeK|FRLo^&RixkiLQ^wi$hWNYRllx zk`u}^8AB-vEGPJpniY3b6Bs&Oo56BySL@|B%$iHEW@@426X@u2j*t3pPHk zRA0e&pmUfiEF}y?xTHE=#Eu0!=N~ev)mu4 z+?ZUgN#xhU&05Xs(y>g}q(PYvoCw3!oE^BXV8JV5a`fju^wOl%` z1^-)lftNsUX>dI+Aoc%6N+b0jRTt}C()8a_ONr9`KO(&Ny}ntC>1meMiOjxWqPpW|4XTo!(iuEmy77|{4vS7< z)kfMTm&ow3JQQ{`8!nXoJ#6~sOnGyvRfcibk|*3j>fhrBJn6sU4q>twS;$@3>%%9h zQ%rzx6g;fyth^^q#!(eDm=PuYwla429^CvC7SGK`ft#xX6w`n%fVGZ|4NVln=2X1F z@Kff9ugL#ZJzyH_L^$S8hvegB|MU2mGAAs$0k4p^6@Tt_B-fwy#T-lr4E{H+`%7y-<;xb7p{nE5rJJLr!wZuQ!4u+LTO|91 zz=x2EPlSxhPdLz7yvo3T0UhH|{y#*nIztMwZW(W!>I^V$NTDv&_JR{70rvT1P|Ji- zG=ZTKv&w1A0a!SoVz|FzlKv|PUYE-Q_(_2tUm@Khwjq)p14x#9BCA^fH{x>%; z^X?u)zYWnL*ixMTfn}7e5v5UTWL9@w3NdcTvW`#5wC#iMGp5~@Pxgq)Jh-nLs%76n ztqY##=-%Kz0aP*qLpLH?EnZJBTWXn$wH(5?UWe1YgqVa4aj!)(RuK2;Jb%Q<4fQ-+MJ3ThJ^*S&;-Z#>2cLF^D+V$H1;?m_q+sPY}_wqwz zF-?U)2_>Y>kGMGhBh?qOkP%XY=SgGN!&BPwLY6~DL}!V^QBnLkM}@^<`p}E5#dk~h zcyKsh2~vLOH8kBq|KAB%W;Cj`~1pE9$)7!T=l6 zbScl#wQhwFAkG{QEjv0zWRqi*V$@)(*Y(#}uU(!156ew(nZazm&QC*SzMRK8u3D1e+MRSuI|0Q zRkAv$Q!3#^*@#`(r+cU$OG(_2SJC(?DA($R1g{S3g+-{ug9z!}VBeD)q3R&44SxJe z3ZbiSitk^*4PRU8rF*u*f*6ng1cU-Qy*Tqe)PW}!;JPzPVnXRn`+OW;_Wdk~_NZ`@ z8(VlY>l2-A^aiHqk|s1IgCjwgbMz%Swmi#r$?qa9wnF*x04On5b{;Y_H`S zPC1hBiA0?m{|16!SC?t1h?_(a(7vl!=|Z_`pq7|oq{lHmJ>P=PV+8&$ykdGuTAwAJ zY1lc%Op>*p!DV6~V7th`3)O+ALmgY|i)eHxatIq5_qYkb4lI9-JO4+DNv&>lxXZ2G zG;+740K=GxRVn(1j!<7j-jQH@iY)A-1n=fi zW)(Tl#?)o3OzM6(j<{r&=I25x3@*WmjjaPzS=L%pjgDo_;@1X-1RouYMmu%;p*HlC zyX<#SQ~B)3mbMJ|Jb)@blxCXyQ{=b?lK`a(f7UqzxM+Dhyy6K{EA4s?C*2=!v+htq z?|{LCJwrVbf7M*xvf7%`C2Kak+Nu=Fk1XZ>aSdc~#lI5tWGmLn-xRvWXQb5UcM~Y@ zOi<8QJU@zyBfs-QYr=i&VwKmt0gwk8eey;c6j$2BBGcW0w?A5G3A!oZK$*ow{ z+niDNz(j}(uz=aCL7&T+Y$SLRo1p##F0=UBOu@W)^flO?jXLwlDs_DDADF8ejNaD47u3~-Of25dU&`N#Fvkv#JVQ5p4I#LC2- zNR^5_-KOG<0V5VhwRH^9m&>EtEAG7U*xcV?!`&SA<4=_jY$*)_b3zHa%%~PE zI@w0;?|xVmH1d`&Vd8kAbNBEO{d#ajjmQ5CmDCD7fA@7l(Ap&@5;gSWb=dxDgyy~T z(8QvEgn2R&cUfh1OT%|;KHUaUk;U~KxiO-wsNw4MFx`Y>wm6JXy0w91y&)|)GyLR` z4q`hdXcW3|F**}dwqQS-y#SMo@5*~CnHHV2#XFPYsKuWaLISMzLD5KC{^pH(nyV}| z5b2JNvP#$@>Nv5bGszJBaH4e9*~1$o$mwm{sOn^Um!w5^a1Wpk)@R_zKRLsR1m95H z$NUG0X-#`MtM#33-jyRju4b+%LDo`yCd6wF;Pry!R&rO5NYnl^AwbbDivAM#MO&+Z7#~a?)k;kONEgP~ zQ;;|o7{UhPLkpK{qKh2PLK0V-PFCy3rug+*=d934igwIm$D z@f;zU2t9dHN{m3T!eDdfPV&P?{DF8I?cE3--%uZG5gGACC4voPq;$|ryaTKfOAw~` z;>-TUY3t-!2GiIe2nTkXRukRss}R!`r?+f<2+I|cM_s2x5vxRTj9@^6Rdh|u)u91h zX$!w&#MT%EBR$AKKsFwGbPS9QD+6*@YWO68b_h=v>K@2g7H<@t;e8*6i!&{%{73;Ui|Vh(0MIhbyQ1rY{CkCs zz)bcSbn}BIn0#T1&%b@Lxk@?L9q@FyH-6bz?V-!F5U6cN2?(UTlTdByZGen)kQ}2YZmh3| zM6^SzhSGqSd=ss15H<)RdSjuT8Z``qat$gB1MW+=`8^T zk={Y1D1wFFr6%+)9YXItbOBkj@)zE4_Os$y??!l2_$!asX@ap*4`f1htx8B4wJNjl z2wJQb@Kk868YwABt%U8#-qGX9M|}pdP0)wISs zngu9*e59^n$Wph~L#faCPI|uQhV{)*yult6+42r2TKZ)je(o^a&>0c3nHldTm5!{C z@}w@QSKHDie!vHTdS*$2UMRcGS2C^5db^TqD4mI$^pjoJ3D-jKTF+>^Bs6Te=?F+q z;jfbLlWrLE*;#{3hJFWV+8kSH>~pOC>udwcfe4^cKb+pa^>3PpB`7M7B982=V#3?T zc)`Z9do9nt0bVN3LMY_=G;F8@N~^~jHc-~IDitc3qzERH?UY(5P5aD1I45`;-!Xb_ zuT{7|eF8!hPaa8Gm5S(~>=mUKzJg@)Wp3*>b>2~={- z>I!=C5qhehb<7-1qLMS5%}3qWwt9lMd%w}}j*moHpB>(vsDl2^l8aSJ_NZ}8Pv%H!$^vj8da(`gqu+I8^ z=iqC+Tk-Ky@l)%Xyvre97!jXk5?S!yijY)^X4Fu+hidZD|K1;pH>XkSb&t1dJt%wN zrYvc-b9E|Q|38KO!0=NivmGk+V8l_oh1Yg=$?S%OZczE|WTnEV-QS_|-Xu+DMb#0r zU(63@dN;R{0|L)yzZgEUI4v)c!~H@nns_)dAV}ct{5?V4agotM9cHs^)OY`YeBQn! z1@Z97Yk@>*RArI`1ulQQ_r@1n?k^JW2ZYRiA!Moezdyn&SJvh3}U4`Y&th;By!ByvT&3dbDA|&Cz3UP-oSx~$(Qs+~ZRh3RnmISXe|M*K2qbRFMT{d~|lBa)oa(C<#vD5^~ zck*QJe+B{B>*K7~s9dbO)G!3*Yh1sZPYkVV)hsSExmDXX9`Rn# z^Sna((YPx#Zz85oE?WyMpL~81^Zr=L`m#6nkntcLacHUFP?nR5*G!2efzaq<;|6o zZtcg>=82g_D^W;P6F%}EGaZ3tC$|rCDZejiZc5;# zY=i)ofv4Pjk{~ipow`aw!Lsaj>)W#C5)&hLK>PxK5r&Dw39C0=drZN)wr@@Erm_7f zUa(pL#uIhOZv2orMuxN}VPn7KCJ41T84iQrMbDkVR zfZQlMs^n&iMhxsxJ|rgP0amG>>=Ejhp6^zSfzf@@4XN4pL^fX0%x54$mQ)rKJG|Ai zBV$x?S`}X0&j0sa6vmAR&}?s#d=4YGJ>`LfOuL0TH`ZK_R-VnzYy^uwuWB3LSAAzA z&6zsEY;m@BYu1k$W}M(|ViFG#Mtq1Mx>wI%>@ofOBFG%ZMb|q0`mB7a9R`V;|4YY- z<$R@|BaVh|^=pZe1=Y9w--qK4r&$Zl=xLvoc39C48hSN16|T73GrG50+Y_9h;!b9lP0&`C^h)tOj#ZqxG~o^F5;zX7nkWiizxy)wrMG z+`I+b(iz58gE{!RYe~I+ZH9dK1$U>vmizG?_q4nlzv;f$kTdMqRCT=O4opx~M6G>G zzqxz^699ailT8Mpj5o`-qGCI3!6gT9=~+kDFjrd>G zrCj3P-7RI#E9BLG0i`PJLEZ^7CVqC;*eBcLE^7&6nJtmit#RT6ZTw+I|fz zcbKrtCZ{laTnBRi=N0XpKMc?P!0R-6fV~1fvbH%ku$(dL7awCI0dN!nmGV~hX7(De zlp#fdg|jF1v9Ag)!y_A${mBfHYRHKcaXkzS&jW|9Kaqaqzbq^cxTA;+rg4t>bySS~ zZhw6jM>Pb!RN;YYr8I$o7K3)tJ)?RwyI<>h0~No(k%S-lW*{uaYM~jWzv8VI3+&IM z!jg1eRW|gErkDr$>gr_EmiZ4N^_L0qN*rHdA{Dy?@q$u1eV=C?Ff&=3cb99&SZiiD zc87S$vauLYzH1k5&MwHmhcL>&HqHkZmg3d^Jiu@pI!W}(E6bdy+Ea3t!j%lT^u*e$ zZ(&L6PwCMdAMiV>R-Pk0et*r8i3UemTNRG^TTL4@1~izC0JF<}vDy>Iv%~>K|HE%H zc4^3VCh+gT?MMFgN5lQ87ImKMsD9HLi%OYgaKI403!=ZRC`0_y+0e2;e(>R0MW>#k z|K(Zta%Xnn#llc-c(#Ji{_h3_zsvpRS4D#&V~%6xmZF8DO!uewlQOO6lU9n|BEsl$ zDE-_)B5!Dx+qTzh?ppb_C6D|$@Kp^V;jeBSZe{XOplZx9=U%=m{Ie(Tx$5kCYpDS1 z3Q@F>@a@}rMJC1RH^x(=e)Jj@3&sv|v+T?iyQLQd#yKwOqI6@}%8b?OiU()S_-`-_ zn%_V#MDKiT=@6sX*D5ux)1xGWG(_Dk=6z;7IkDS9Dl~j(%ons(5Fq>WdXxP#hd|a_ z*>Vn_)mSNKZG;McLLEM3CY2~t++d`kG;dQ$JQsn`J@Mf;zFzJMHTw3jL|H2_LM5Ap z5brEPQR1N{v8?`A7HGWT#bBPaWjH5?hVfg*h(3*2{g2=HAk66yPYhOBt3-@-@BPD9 z=Av`G@^|x?!U+vnmuUzceiqP~RJ8pbq=1L(2tW8R!SMA<)L@&l?GQrv+VicQ!>LI{ z(QCPZyQLEXNdDHf=^}WhoD3Mq37QkG0SB)BnXUJ>sPi~l>t}MA6A^#9Fhhe}jaQG& zlK0u)NJP^pVOJK_xdJZ^I%$+@aT`NCrik@AH~U^e$I19Y$Ad^RbUvjuKH)2A5NbB( zR1_xPkoAxG$r1kx=_AX=|AW|v0bSy)K(&jA%&@OE+-vi#m5cV61a`Qon9@*1*^Q#>X;1vbmjGeZ)h0;k zug)v^uS59%md=}QEPCM{u{EHCzH|%ZYqfCyjUFzVu<+lEp4e-`PR*u*o8FwV?|d<9 z%(=)mct)1QQqgLGJ6lN%=L$O8+*ACGL(k9G)YJOQerrM=gk<*H%P30)fB&wHT94V# z(XzM2r!D=@uC|)cBF_KA-LXoB{phFq>D9W0EtM+`)chL6)nT{lFWHk(ghcHeYIS6| z%mg0wr-J=Y^5ndo&F9_UWCe2ega_VOTaM+tMAdNN)@rg5{hh7qddRF$LxVrCqb>&O zwZB)C003dmm}9P(u^JM^h|J-EnwEn*#Kt5>Sp8Qz|Bcjvi9i{F&Yf!a$DQk}|_5exf|tqPTC0 zuSuIp0Rios3zVRAm^%Ng_-qP15i#c@K5k)CmT>+0@k1hu8=uE^K3

C-s>Px#k}Y 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≷T1&aud6Y+*F5d zRuxF^%iyM@qF@en-Cn4f+;}1}FlWr`wg1%Yb@FW9q0Vx$GEhpq26dr2*sP}lAKt61 z#F~PGcR+mayx~&T3e(2?!)OVVCc%(_Ih_X=w+D-X!_!koU~H}KJm&h>cq7etJAt@7 zNxi!^YWhSq7n*A1wwm3y`a|Coe4?$9tt>J0$edWK{QUjGQO-b7wua7&6B%8)UO22! zsb_t>FY76GK$6yDx$40zwo)y-#_58=M7_Cc6iv@ZQ1y(B>7rN$8ar~WBeJ~56?RgI z^1I{2F}tG#>aQf2Uu!8nc_S7q0na{mQc=BjOwXr@zP<53@9azBq7?mPLFxJ3g5rEK zAfyo);UDPHh7i9SGr9)sib;4^O3@YGcftqm{%2Qr*Wo2i=gaw&ZVPwds4$>w524UD zT&l^bZ{=uu@H@4FB=ww`aD^t|%FaYxsB++G4=?$&^_r465M}dQ@V_g24u7`1AqGrh z2iIfu?r^?+#p=t!Xm<&t=YBUQ(aqN(KvbjWb>YT*PUlNz38ARDa&Hq4P!mjz&BI=( zYx^zjLE=j3Kp(x_GQV#Ln_o#m%myvUqsbHn(j;tCTL@p*Qw3jEcFewD;lhOQFaLC^ zvTw>6o~=ZNd}Y~@l|$!vcSd(#19AL2HBdk7+~Y`gVHkM<&@otc0OtB`ZQt*%xS`&; zPVkIV3TL<84sXX?+sn84JX;>K@m3u;j<5cC9OJv)#8d1*G#@XSZ>%Kh$Ebf(&(?pd z^_*p2b*G%tGnjE=!|3#mc_9)C(BbtwLNJ}+qT zOgF+IrUMG*srH6Gx0U{$ru(!SehVko!lg;ZnAP>smT)+w^C$XtDvOJXg#OLsQ@MpO{okUT|T2tx@-d=6hua6|cv=A)D?N z@bc9lS6}gwQysjB8Uof`8?K1wbD#cw;<)Hg6$kX?p6z2<0kKvNp;s62^8o<2wmBnm zn?qHD&LrSs8PEayL!bW#vbB#OMO`Zg8VL{#olz&$o3Y4N{a0hYA{nTGd+vs+35GA2uQdT-w!3NDg zX?=sSkwCf3yY7h7GfMHE8mWd?5a#fkLTD>>SEAvAZ3Ak6(JaUQ@mNh`lEHU-=kaJ3 znuLNqU4Fa&EJikQS&I9)2n_0b1xiNFs{iWnS73OnoEgggM%*=`^s03wwf82k*u z(uYqt$NP%trcdII3JBhH{JIdFtDg(7P)YBAU{S{Hy9vo&0(_toI>5_&d?N;t7ZBZ} zXYuRO|E@xyUqR=0ys?Skc(gEzyx@VprGk24jyYe80bFH7V#*BmW(i&)81j`%Vabd) zQp&#mogSU)PWJ$5G0TUbt=PZ(1I3^MT?4l7Y#f$)uf``R){X+xD#a&*anp#(G$eEo zUP26$WO{`)nb^|g8>&cvFYm?0-lLbKSWx$5PG7ikqFmKug=3Lx`8`8(A7+ckow=C| z$o*=5a7iV>dhc$)c}rBgr0PXPuMSk3I58f_!`TGB>D;5HD%1(tt?eE*Tk5CO`m`Y& zbxM+GAKr1HV@VkHqNvOJIkkRGqNZqM+fhmnqacH5Lc|{EfT4EL`xQxtJtK#tBnKXO zGdsW3^y`sT(zD&&mJSIIzH*92;l5giu3Rp?AVC?&SuM5sezKz4TusmcN(Tl)_zDhq zB50(60hFp5a{4}F(`$$LTK_iA4`b;+HKzpZkW>&+}lv| z0=x%mjPIPkf3+LVb+Syij$`NrE;yq1_-5MhDv(K{%P_{{@C-t5P1*|t2@6%$zhka} zHVAV2g&}%YW*TMn>(-?22Hi56;`JLo>|Qt2XreN&bGn{&9Ds(sf|>u7{dw8x{6N7A z&}gv!IFDb({EN=x#~?RQ5r~$FV*dA=r$zo4FP&j9Dc^Z|S(58*$|zGi)M74+zIv zQ{8mIyLR~TF8ELG#mXF{c>?;Mo|ASldLKIjH>G-f z{CYcRNw;zWrzI2YW3n229B2NKcv-tAOrBw6328p?9<|HaJPHen*=eZ{Is==5)dRgn zBcMaLOcv>*$vbxE|8HvWjb~ zlTfg>)l^T|?u~i5=z|h&eUZtvr&*lx^u0c>)VvFE1W1D{Rn<}@Z}lC%jP2ZbQ;*~n z>bE1^pjC~&W5GRTdM9MFS=DVFo2=~OrIjc4SzbKPbxjl`Zs>o+-I7*1))tVL zrKT?(2)NFRu_XyMpIpebB**{QD$<>XNc^hW#g`A`3sDu+yK!4)nt@u8P#WSUF zwaVcmvyRm&)+~R{^CXFiD-2KRnz*kIQxk$}tTamtoo-4~>WBM`0;ac#jfEe6bdIW* zSnB^My1|P&|0{O0oKtoo%PX+FAiPsRhwa6Wz#rnUW_m#evI?7z@#gUrdn!7w8ybe1 za@19_e|qwt)4oj+x54Rwj#v}K_nN~vzWs$nvQIoVIVZ5ZCwKtzOnvjiWlNLd{dlMX zwxpWA%^7S0z;!w)Zm95z$a50^z~t>FR~L${&kM5xuA(zy4Sd*tan?U8^K{&Kc1O9ss3qu8WTQs5Xs)tL{OVGBk@ohlgdi*Ei#Ld z%o*{wE)#uzTTM)R4^ae1g;#Y$mI6r=2Ybz;)oReCh=2@NIcU*V_u=S4T(C?84o-?4 zHkjSonueyYXe$VXJ;1ndUL7q|xXbiL*Tz7Zrf+(}`)ZA&;w;j_h)Q)4x7M z%pnu}@LZG{%AL6uLoeJ0z(73=&gg*MXXW|o_bT11_Yu;7dOyO40LwOXsBu=fog`Ru z_U3TfaD>0f;3t_C`6%bXqL_%Shhw(8{n&8NPT8=x=8;k3iTQRRTM*{rd~bY7wiEwU z-J)7(Lk?X9zd~N}h>`P#W~sbtl+?6ZmtdlQS!G=vs&$alp#4$OM^v4QRkKH(YC`lj zeKOT5=KT-zs~*kDC4&L3`H30A<_}~_^A|D&1CS{=NCN)tIQmJFKu}K@Z(|#ch5VTX z14ot!EqqpYi_`&i+%^gUAIoAU+b+Wgj4YjxKVkI;yKAXIQ;FFd($wM~m79D!pL(=! zVlAsv3)&eo{tcR`o zd!XNaaLT?7OKoDWk+Cu$pF(LVAN%^~SK=JO5%lsdIJwCW?IP=k&Yw9cd%$kfm(t}1 zVIq2+VW#l9V#l-&rg#P*ssMw2h0_32hRTcLbcp3y0{fE~6sYK1+e$A+!}-vK*B2L$ zD2h1^x>mbUZO$?hY<``Tuj5WOC=c%?M>6RLuI_ai@Xqz=)?%By;fQ3xa*)jFjuuv{ zrP>@f`!I<=Sd<|M!s)A-6gp#MYmj=p!ceSg>oghkK3aM<@ua}gJfA)NNVn5#XZc|v z4r{>E-U-#PQ!90w;mV4ZH!YS(w2wC6os&*kFQF?5J4wbXP;!f+qZ7$RKiVR&4MV!E z;nHMYlt}}!zQf9CYZt1fKtSXJ5X@%a9xJcY)YGFdG`xwjFCYiJYIgSbzBNUvcl(#L z4cpf4N*sH13W${qd#PGb?qel4hv< zMUNPjz`hI-xaH*qeUQPO8M<&rf{X{*Weo#KHZ-4zg=8|}Rj)8neIcCGN(fM6DOqV$ z0>vyQ$}bWXUL#Df{^9iGL$Cjc(*5Jp)2ZAM)Z?yNN>i}aO$T@|T;7NEAyzi0Ii3_( zoF-Mmsb`kbeADcaLP{MJ5+lSY-mRUjtGf$}Ksn~l#*6_u1a!-XYx6_m2=5d_0rxf^ zE}kghKoOV?sLATB)_6AYnf4{lQ-uyTdK25%#e9s1~KEk~!i%l&&477Nj6mbugv0h>Rb!=|^ zdfG#<3S6f_ATq*Z_)o{C1g0N0L)lR!{mc_bF@MKJ#IvvK_IsHk{^-k%%`$+k7*zZQ zNnv7V2@ua9K|ni;$nU`YBc3(fi)YD0@@pbRVI+|e&gKMt%d%4{!8I?>eflmGzNOi3 zCYUTD1sP!`Y6<>0LvI`>3PWmP9f>U`L9cM9d)*RkN7m~V(APqPwUa|y&BVBgV1!^B zrsSK&oZrt+CMVDas4c+WVZkVM(x9*=bW3u_IG2BdJe{K`EfD{z)-$ABo=Bx{_l6A6cSsl`#`H80ufC@H-)3)3 zye@7oh&Je76$B1^f_X;9LiT-JRQbf?^8XLEwz%N!7F9$}g^$VfOYLIL4)6(S$ca?Z zyJp-8&7q>+H?Mn^3w#mbi8c>5aauGfcw%^z#Ed!~!&{p=8M?Z{_`^wC`x`qwbwthEFjnOb%3w%%o7(Y!n?W&42X zT>1*|x~Kt>Voa@k6f3UxUYFCrj`(JD=pq0x$d~tK&OMcG&G{`9@T7k|J!zktcq=e> zC#iUA(BBF8p4yk}UzRosTME?RqB0Lm72L1+N|-U(zs8ZSRcl-uE+voBc)lai!6Q-> zq=rV#O&*EH?-0~r&sO^+9>9X}*#S2zENhS5&~e3Skn0PUG#69(9w^B)W=cy|dNe7nAR_V{c4wO8YtBn7z&+(j5R zCH^~OQiv0i`Forb-&@thEM{+s45#6TqP(O?y=EGl2HezE9Y&)RxXNY^l%x$rj38{S z?GAOgUaLTuO6kB5k-=zN9~-&1y3u$e+%fm`%C&jkQ{dP+pw3xY{FpO&dmK`LuKQd5 z_NTL2RBixWYZ>|;qPP|#ov_Ao`DL?5a}d{bwFc*7=g?@1$r+@^JR`s06}1MX-R~}r zzZ3;v(XYQf8L&|{gFWjY+|SNoCK05>{T$qgsKw_Tz0nieeM<3|XBFm<%{{PhOr&8OA%K&~x5tDcX@} zXzoMc{-`dGm+gi(Ap=^IM@wTQpN^4GZ6rVMsR<#KxrrS9#{Dcr-kSis&vR`su3M~? z3{LuA*l2=Nl-BgGDU$>CUxb+rheP}BHY?!pC;VTUYeNpNDee!av%+oZo%yGjc!J`7 z17JMC6fy-8VM;G1u)R=Jnr8}%{7zr!_&pgtA^of41=276U*_w%nJ7Qwzj_nqG|+=y z;Zi)%yaHD9vK9J(2#69exBUFyu%eX39;CCMr8!FCy_4cRM-rPK0(Ou-qxdA65>S6} zoW;rtfBScILj~zfn5KQk3KBGvR$NWez>KrmbfsckwrX>7|8C8uQYC%Oep@~0_hY~x zi6AX_Vx(7PB*d(zTFZojjgKeaj8mjV?+@F`Z@^%}pbD0ATl`0XnB&qqHVMZh zOi<{5Ghxt3NtZEU_RY|8mBMCau773UcHB+h#xEKpN>{=_jyHtbOq#)Lmy6`P+&6E< zzLwPjg=F&@ZL>Zt2cc8LSYHgEvML7fAo6$CFB3H#!x9%F9YN1JTH~l0Mo{@Hrkl!x(t4{XITF%gt~bI|W&dJn;IdXJ5EiB#``1AL7Ey)rnLQ`tRgm~)`xG;B@>;`m zp=c?5yj|^7Vk=$*3*_M!qi{f%tph=gc{|egmH%31?ok|0CyLv*1nnKk>+7i`qptl9 zP8C6n-vTsRD$)Q)h#hhi^(Zk}<41tC-2X7jBiU-ewC|+qdf4@)a(~s<83kwCx5xg4 z#At<|ZP#eEInNnY=t`M8`?7aXBUy%&f2uxB`U=9CZeX)MltpM72HCtD}?md+Y6oaexaFC zU|$+3rs5ha`4xPLVnHqtBFa&fo5$=;y&@007R^Ap=da`5;O-WaJ>X>E!iZR3G#afY zYjo+lpELf(?XVZ`tY16&?lhJg9)h>3QV=;2v zSbW}CER_`wTD*vQ2)f)WT`i|fx0l4+7$I=}^QjGo^7W1RU1P&j^xV2BcW^uIFnbB3 zE}wnXxz>vMM;5^LYXrJ?9XzZN2#G6Tn}6zm^#Fcl*v_**8E{pK3@41bI?xduKR|NN zxNb6OSnfBnyMibR$%v0qoG*uaw6#`DonSSEcs0R2DTKLak^I|Q=UR{icYx8f3pe?1 z`uiyZ@BNga6@9Xz1}&`v3dvUbTOQ{i!bXvk3qTnmw}%Xi`)Br>vMy2pzN6cOmTT` z5d#(lGF(WP9w#o*QI{PnElBa9G|rQ8w@3$PV+8}jk86_8C_T6P{8ssfBwBaZ>=V(O z!QTC)LS0l^io0B8t|m0Yyt?e1{LcsOO|j#nhvFg(1?)!MtUmdD8RofLaQv$-9=raM z)4|XHElRI(qw~bt`;J*`haQ97TyYR8Xg97?PfDU(0X+ylYuVaO&?UHb``DT1Yij<_ zOt9xkPS8Rmk3dvyRtc;FbC2)=#%z%J6ye#Zr9Ll(*3PO4rJ@ulD7sV*bV?-aLw_bB5!$CLY(<^DV@$QN zp0h`g)3RnjxMil0Si$XD-2Gewc;}n=0b|_wgRPPo_0L?={9r|PBM`r8-6OaUW>y>2S` z?xOe3QIONM@u~V!cw=k<>bq@SRNb&1L0NJ4?oMri7he_GcB6A%^7RacPt*zWVw471 zn~Zse`+gwGV8@Emn^jr1xTJ*N;ZNHhMqw!vxQ}-aRMU{ru2b}-Cu{H`Qw~-qul~>A zZ{%-=jOpTR{;CX=o-4YMip9O-1!cTBM1v&rR=FD9~ZILiQJ&N5th{NKO;X3Xq1Y zBWbxUe_0DnNhJ)d>;91Z7;!gU*0$R6c$|OQb|Wjc@rznssU@da3;Qgw^sSTISTkZ8 zEiu#97#qA8I0Z?3{lm&PW=^h{ecZW*_pQy$U(b}IuK-WJh?Pp9vCsgyF2NL&;0o#2 z_(jZ^G7@@V1D9xnQJ*{W6JA{uVWCm@C*NM%ib6?tB|M=w>`fMD{PV)iewfU(<_<;R zxVP$iI*Um0_*GJyCUEKfd*fIv-=72`vZ3jFM#QT-C7HP;rK@)5njZ@BbaJuJf+nG~ z^kho6zBht7jIL8lZ%R@EIXv#vhPfj1C))x3QR2s&oeQWwr-coV;I~O(R9<25Yg6<+ zKXf2b;F7UF5{{GJupsDqJnf33yppA)^mxu4u<*s7(vv10{mf*D^$p~U;`uKy9{mnx zptvkwS^C0rd)0=qV~f6ezg=Iv!|T|P!^wap+ztVa#Ffa6~ZaOL{N?w@TEWGW0MGBq-#6E<~X+P|~K814ApS3*{1`Qy`vv-K{sl0tdxX&FR ziPQoMRt&sN?syD+6i7H zZ5eW$N3<*>lI#KsWucT3E(8VrHwsHV_$o+jojh2lF?k&S=ZOmvCGX!fueA~f>=8WR ze-XVLdDU@weV{kJ&f}E+)#mx_CGthXeBX$m->VHnKi#O8jT84Vu%Y%mhXyhbE(I^(4pNeT(WLTdJ=H@Y7GhSagdmS;73D^yqi7f zZyj^wbdg$UoOc)+KnxYZZ)wGtXqwUw_vY%)t(tNNdMS_;4<3_%(ulbMAt7!SO!)3o zNWDk*892S5n6z{qg&0pBGh?*{aF{`rutm&42ZnDCy7iqY0Hox1_CJvlLdK??tn~iq zz6dtklqWNnNyJ z3lLT~4#k{( zu5keqxul&RyACs4(k_nd5$22uMq_M=QSvE~S7Pl6K|khrSKC#_9+ru6Kgo(33e8UF zjTwqFH~f==$COL$KqCEGz#|zT^0&Mjp=YUECANUzCLC9db;&PZof2-aY|KW)@!mnp zwy2-CJ0PKu5*@E1j)5en;YS2*E?Wszs-JI+)z7tnZjPu_QsK96o<^brnFY(NdQ=F_ zI~ICVb)HerCp_BsZ8~`!?xp#pAg1ZYV%nthQ49-*+b1?t~{iGB4 zw+2V5Ue3K>%ykE7wwfXe=8~MbU!6}_stH~6a!=J(XXnP}{w8X#dwRAZ9Q-Y#NIu%t zy$-oU0^)x@9kL-Kv^H0^f1$g)hv&~O-2ZvoTX82U$}hk4+f&M~>M!8EVvD#3b@PW+ z1|o;kCE?|z{a?O{<`T;npiiDjlxIumO9M@iTw)QSfcQ7SJ{8uAkFJ)fK2(JW^tkEN~o!b(!VDjiVVDb z5@GzLx7Sox;asqyh0k23U0Q3Q?vA<{QrFC_DNEg#590w$X?0#f4*8hVf}m^cLw1Dv zV<;!-4|)eO8t>oqA+_G0>E$Z;V@)5lhEpS9H)4GbXdhtndyR|3u9TN5Z_&3*%jd7c zOHOwxYXJm7xg6^rL7@FNTE!;MyvPZm-fRbhar|G)8_IW2nL9&K>Z=u= zsT6v3@wNT~IJ9xga7RU+LPp&~L?CC7^ZE*yMY2Dc;r$ z-T?z|w6G_g!QmmM^g%9WlJiqa*184QB5q;&fvsaC6pj|ne6ekLtP*J2H7`kLel9S zNgJ_#O6D>^UFms&et0TNU@5dxLzu0yF$@Q*ejotA6Qnu$gjZGDPbu!`e*%Fht^Gr03JZ#l|#g7siY$xZoWY<$-h!wTlvJaMA)~k zVJ65`Yi{h>CuLHR^Nf9P73(Zq+6{$ML z%FQp$552)gJwUqfymLIUtIxeXYfqwKTkCqc%~4TAZG8G zIPX&A?GwMwY7Msuao+%o2_O`CZb@DR!a?bwT<+2r{^LUDcI_n-&;~jqh%M}mrcb0R zgtbc1ziUnWVE_1&{wvDwS(z*=*^fL`WKUPRX zkL`&X!oAt2RA&RdM|y)8n>I8kXc{#3Kn>HZ-*XeVYG}xu@EjW==KVaUvEm9zP>@-u z(F#$V@Z0GZy-Mq$Dt)`%DA_3Ws0^naHRrTDE9&o9>4>`wA`}fK&hD4$xmD(QN~Zg& zFl(JtBKmTkE1@O4i{Lq!0jn8vQJ*@eYU|B{{+p=>-lUlC#lR|kTvUoKUpfSPP8Gto z3tjaI`0{#)vv73Xl#0WuE9{*T+kVxdCJG^pF1R>U36HV*Awdf-=ZDt>xZ&f@9xqRUb-{R- z@3mBg%`|fZeG3~OLkZsxOIk*5A#M4>jh+=*BfpDU6fwucWg#oew_xm^jz6i7J{}Sa zCW+SMbS>LT>UCY_m~`=9Vl+-Up$c*gWm+JrFD9=y3!F%#ht4u4l#(q)J+B3C9aKbo z;GM+LE&8AI3&3vl_9PmJjQW0k6aqZGOiQ&7_3y21(PEL=bVQ4dMj59JSxPv*W;619 z&}9=V#^ap{I-%({SY+(Nm1qlQ@5@VcF??D0a;xU^fP}OnL&cN~HgU{Y73JToUzBHd zG$!dPXnUAx%>Y66ESwTH6H|C5>aVE>!KJ@{=SlciJaD)fXJ>0}oJWb{M zzHh5QJ1fMKZF`X$Gw>W$rKkJ<;Ym#9W2>~s*^OI1Hi9avR(bAdvq@TFhPKrdQ)i)Q zo9TbUo$Op_g@Hr#ua*Dvy$?28>H|w(i@(%Vi+_}9&WB~cz`*Khrsbx^8||O>!{go} z?+NsWu;N}F4(4G`A`;P#?4NakWnPS+S_rlq!O@7I*6C`&R*@K;Xrp+~3C;7ML-il&MB(ZZiF30WcMd~9QQw!(rK&@OwZCjqMGbHsjuYtP?1jb z%Y@FFHKt{C#0%9_ihS?rz$htx8!0RgU>DCtpy3^lo=+Lu{D}DZ>OJ1vqlD&cX~2b6 z6K%>ZBB72Ep=ZQJdiSv3X75Hn@P(}xi&>V0;PUg_Wv{T{2b)CU*VhlPpE6k&S5M-! z`P?j-eqXpGLfatC-r4bbELee$_5+hP}&;oGPY1F)${k`@#A`x`ql|!Yr2yYNRJe?*ykKjsM zbB$xmnE)q1I$lJOy_5WNK$U#N;UnTiG3iJa`iO8<{brsuId-<3&T&O2xu+r&)jcK+ zbq+lGg9{cLFooAaO3E2mlmrgf+{+JfE?pXdM#<>YTD{n9>SOQqRHywlh_!@YAMTZD4Dx!7 zC3OUR6(vw;vA6PmX4bOAe7C=Awl=qs&yp>Vc;_k=O# zz0aUlO5lKq^oU%m5laFDV$bFGqh!t2!z0{7lTw8#Q;7C+RxMA}Xbm)u{#dXR&%@Ut zw|?@3sM0ka>#GibWq7$2Nosm$XdX0%iW5?fQWaJ4V#5Kh@0T_ECKGfx30brw!Do;k zjOehPFkQA|9!et3V@v1=VK)h|oYM2lhiZTi-XEda+TCNXIF^DCo7>?0cJAp2(agG2 zeUf8l@?F2@jSmUqL(NSW-Mke{z~lkdNXt@Px16|=D6+SuJYor&O~Qps==@x>gr7|p z2f0+(zv=h^e>9q<8b$l(=@i??s@MUF>V#9H0@+#lE8A^GqwFuHw3nJ$su|t&ijP>Y zNuQln?)p%g)D%3;})Fd+QIpr0a&K@li z*p)iE_#ZMT;l{9q$fAgDSNS>svVV6rHlpj7(eAO_&(H~O{d~3)!sW%vODCm|Ck!{o zR*m{b&|hdp4LD-y8}#63Jx+LCI7a+;=;1{f=T#Z!OA^Gt!^kugDp*nxuqz5M7PO(0 zUU9%g3^$aFn^;kTUK|F<*j-8FDoYr;;ZSKaWjO1r~*; z>|}Y|Pn1Zc*3b#2Y|Y+1qF)RW%#f z9{LC(=4OYDJ3|Lr`UWblC@|0&YJoJs z?~G()X5~q~iWbDX7K}knaviki6riz2bayH>aplJIZD^#B2vrjc(f~Nw>a3{0wskm% zVle5PyvFvfB0C>i5o4VvD>Q38eZn(}b+=bEWuX+lR!PsZjy7nm)tKw4RK9$ zDW4L~h2DF@`2*W<_Cp!5q3f4I^tdTSX)?<2sLqF-h>Os$Mx8^!+;sV~gr!uaE*vmcW-rG4rek<>1_g(21T=E@v%X_Y(K!I zkaa?gYC};wK{*MVyjFC!G!$&W^D>GsHTfuQE_gaPGib#=ib8Q01X14GsT6|2? zdw^k?rft8>;yF&H?95AGQX;8Lsv-LQgsxOt$^Nk+zAR47+gN<(si)cZPBV#083 z?=6NY7JiFTu2rnTgw18zzB5XdB4~fpE~?8it}r2xAmUu|=cLLF=3xFPEM=Fg4YSB5 z++us7>HI=!2Wc69lFT$;N@2VMGPhxvKKtbt*|!lsZTh3lVVz%uhHAX%vJan-o7zen zvKek9CpnDjpZ3%m(w8QZoKZn&6GNN-#JI_Hpzy(uC|o6fmr#g?=*meaGzMkEKU+|!1Tl+lIxnS6l#!ijS}8M= zR2IVqI6GZ8e+h=!iEikLYn0hH%C5&26M`%7p>0B6(Y~I7X~Jwe@eOAffN#stLp9^S zr#@yF9*$-4YDOp(;jj~ySdFR|DXlR_icln^sV;YGSO$~KpoYm>*^9pWoFr&4vaUt5 znadKm2A*fujy|_yb7TGxIEh68U^XQ35WEd5@9UDgmXA4P<>GENZME$Fh@rTHRz-VP z6A-8>P3Ji8C;4G$t6ru7G>!>vlLyGHg-_DsU_O|Om^n$~bK_y^ou*<;Ud8Wx1ybOH zd~u~1Syb2%7D`EmU#r}cnsjT2pq0Y8Vit{AK|LZMbmnLl1|&CHs7A_` zezMt4pgR2z(6K+MQQE-O3g$m{oVh%LLBxq3XKD`!I9};yX`fl$yf_&7;10D0wIHESrp0Si zajHyzG&eVR)I5-?f4lSZrIQsm_|TVcLWOoiQbvz0Xn;M^D!R@qM0n3)-M-*Up+|;W zxz2}Ec*Zmp#=uDd{X^m{DW9Um)t$grANp#C&8@;R6=4vCFUCs9Y@!=&f(y+VfY(3V z1>DZJc)lCW{+8W`e*;eTe=;t9jOEUpMt z|HibnvAsv{p1=V4G_1M`112x+^LulpAeET0&G+I|Dmx$b^%wh$uPDdFvwV7aqJK&E zj=KlwEE#YPc6m4^5pkIem7wwQq4M;6 z+VF)pQ$k;=yrI=o`wKAB0LR&B<464*F;koO`SHw<{*f~YkSYBe>)Q{hi)U%xE0I*= zpH#wGj$u5$Wm>TtaCKa)AGglZ`bFSwt$HGs?kKr92NKa%PtXQWzScnzH`tDMICCqyuhX#8`^=6Yz49l?WDF4w@Xq)G2yY z2TuL$iHw_z=U*3#7%XyZ08k9j+gh!N^%O_nJNy_;RU$?8(R1;0|ky@qERs8_CVUh4S`1=kLxn=AkXG1P1FN;qy~`*JVY1;d4bX5$DB1>P)Lv!tybU;_<1pXO17XG zUg`PQ{F)hINtF%x=?m1QPa_YTz2*jBEQC1u9c+GJxb=DPe>ho7C1&dA9mU}ln{?CN zL0`r~ECLSbg5<_KOh18@47o@vZ;J+dG`g_}FYc*}@?o8lDCEzDj!s@nxVrvbrdROM z>4yOmUdd~BC%5qgi?u2JKz8b4Nrf|SFJV8HIB{@*jZxNpa^; zp!qxTUk2c!s%xA+(!tO0*`Vazz~4{*P^}cH&@}?Ui^)1c$>r0&MmW-rIN^&M)%$?v zck4x7pJsIQ@T9lu)n{aMHT}O|Xg59kH5;V9fPQ}!v2p#gspI^6FNb#-@26Ls2B%cc zTGFP6+OXya`CBjcM9ZL<3YvT9#& zir$Q$v<|4c4_Za%)IwSIpI!dat(E=p6}29JoTt2%TYH!tw@BCUGrsw?=PT9jLE|Z4 z)4DJK7*B78`&ASG|D~8ZI4=heYaV1y6a+yw@S`&49@zx+#mJdniT#^J>?ta>hT>&#%|jJGw#sXkxU*S`0w2*uoDk z2Ie)kJ%QXhEv*5%WK2V1+tp9Q*^5a$Na-GX{@|iPk6iUj4YEs6;bl{qiSUt1jN9!}t?!Q^)p`I9sT(LSX{V#l7Th2+_quajAoP?%wUqK61 z4Isj5t*TKB^4peP#Ve&qp$mjViDP5*znb#G!t>O->vhzw$xt-47gG1QYz6J~%-TDG zwXLq%q*Z5YTHRB( zlzgdWVezPbf2=NI>1Z4NH^TkgKU1}1(3llinn9UwvC=dJLOGazpxb9r5}hUJWkK#I zUZ%l?YIXKx!YBVWMZ-O~^<5e2!s)PYUH+T|H1LQbB}t9g6CJNcE%Ku2f4+^wt#KNS6C{#$g3hKF>8=uEu#A7*`P` z6kN4czyN%pw&=CU@_J34giq8tRWY?QzycD=`Z(#6U?qjEqX*^kdY!~9vn=^3a6Gb$ zEjd#mNBs6L=k{R7Fytm37NPIdsDTtTKhlM+in_%_V{#%GKQyI(LGxh@Z_@K~(%A&5 za2V#8zQhyUQ4|`Ztlg%+wzwYnb#!dpE2VHk1U3{%$mZ{D)qd2(_1-lz`pLrav{otf z@owu{ojb|Uk1H=6g;#5^cFVF#wN4Igd@K1}3*<(g;O=OJC0|=jG-h^DG>cJIiLiAF zbr8)7$)Gwf{YvMZ!ilZ(!{ekrv}+PGhHl5gEvW{drVAEeM2X9o=caUJd%8)2TX4Yf z&gP@K>y_iABP86(phE9tpl;i|DHD&p#hU3Qq*$UK?<^cew=14uPK?yx(Avy>+DFT2qGe9#?8rKM&Fp>V|QO+9~C3 z4PIaW?CtehxOV*h)SVZG=UxqX+Q8JCioM>3R^!tYG(g<;RLtGl(%Y~n%bTZveR(Y3 zecLY_ZwOs>f-$z3rOr&wiV*e1cJ2;jwB|=aiP)nD&<&|A9f})gB zrYxyEF+Nju8eWQR$L$v6PrEJuX(XNW7gIwbS?-prrrNtVOJWK1V`kEeu{Y<3`uX4{ zmZH)AsR94CB+26+7CJ^iu^za^H#eO`ASWopk)gu8|8r#*lf_BMC&eEDX-^PdVxO|R zrCFQpFF>rUrcv0fmifq-5)r3Yf!bU;yl0Aej^*E#Gh}&xc9>-4OiCOziqB|~SQT~B ztR_uJN_`qPG(^kZHlR(y53_JK5QQI*2ByyXZj*;g6QLx~9Tg1+n>3_`bMH_*v2;$3 zw9h>G^$uUqM1^kjZN8FVP^`9zDhzSrVP*WKON=u)=E^yZ<`hrQug(VdN&_S;6%U9& zq1>jfF2CL;JA&Xeg-epBykWwHx{SbNCdG)V-&HXIlTWw3=U4PIZIgD@UNVb1+ikK| zXuTQa{a#(Ir#CaSAGBjD&HubjMs<}Gc#HV{QmwPTq7d^;a{$`;ya+?5mZLe`cOWn| z-g!U=a()gXm&%&pVy(kuQ2|tMG*XDJ85llBryon@Mac05pcgfw)Lyi&31YwVU;rZB z3Vb#-K3rdn*|4+L5zg%E>x$ZRCBcWpDVdciGg1s*tgb#c%JQVhK{0c7?@wH~b-A|D zN&ud#Y6s2}d0YJ8*zB&*!W4jz5FSwH@yQ=z@f)q|JWiOv*)?-=o3JkgprmPk)s2-RsPhC@+OujD9ndT!bwl9wvC6Xq@np=o>fbm&dyDxo~CG z+&nFM_)cUv1rc7_*P<9oj|Xdh@(|Z>D|=o)KvxiCpj?wQO4~R*z=!!L%vMg`4%xP@Y1( z(y9=W8I8_wabRcUYjB7vEjk#z+=@@s2m6e!HbMU~)AZTKW3Z8e4q*i9o1k=vC-+j~axn$1oo`k2VE(HQfsN@Aw5B68 z98$_ekS7)W=L2NL`;zMeWUjLsvi~h>Gb^?nDjNdWdDl1i610VPfn1Ze``9n%gilnH zLtuidIG41p0@CCypFQNF#8qDn?TOCm)FyY8(3U4Dxi1Mwp(tB9w`ne68OnPb)8|2O zv22xxBnz^P^NaG(PoCMS@EwqI#)z*OmRFqr#R)d-6qzmd>rW`NC%4A3Dj3YRZ_h(h zW#l#^K4c^=?op`rA4DKh9hhh-HSucU8`joBj&9UX^8_uN(h&JvEaQ6-9{b3NaHVTB z@7hT$Y>R4_?iy3&Olw3#c$XbOKmb#B+KXEg8OR-0AssEwSz@ulsrv3Q0Z|F&Xhe*{ zR2Q)x$uUw_8E>;72@x0t#VNH^cPF|^Lg!;zNh%B9-ce!q7BA3Mu)>p1!89Fm0Xz5B z);$M^X7jz`3PGu7P#2JL7+NSQo(4I=q%GH@jO}3>Gx{Ff@Hv?_I$IFMzK5<)Z9JZR_eqLPuuswW!U4CbYF#57N=lBo4lamakUyN(XeFxv z2?uFy0?(RavHg%_n3erSG2mqCJP^|ioQ8}=N&^5ZCP4@R!k|oo?g{GPWQ;DjEHL3S zEB%gV@2i^D%v(x9BZsuTkDdzN9J&)h5~3*~O13Damj0zg3GRO!654~E_{H2ETJCIn zR}yuF+htP-Z*N1XNi+Pts0Q9%Yi!87o{H}S8Xf;%4#}P2hqezRBWGi#`#Hucb09=~ z*_PMl#n+?LVX^Ss?T^iZt~KMO?F7{P562OI_%9NQSll2_UMPm0<<1dvPYM+fX(E-1 zskmR6_iu|hQO|Nm)mJjCjGx)sF(>|vT-nA|r1;`egwf4iLiR>y7tOd(0UdWt*p6mI z=i1PuGpOPjj6BXFnIH5H#TdamXws#@Ywww+6kn?!yVN;xh}MqThq0Fu0~9mRrsC~g zKTVeKR2(>0UK2RJvn7z|eN?82_Eo59$ZmH>^v4H=xR{JRI=XH!qWM$pM~sq>81wTP z+Bg;Q&0LBB9~nNcQl)Lp7rlNOy3;3o1u%9$)pn4eulW7xpqF>w;QOyWXJqk{0wm7| z2B3*)!WJhEdD{lL&)9raO#inp?axO`hH z{&n?Wt*K*Q)qNOsF5YfP=fr03`RjE0kLTc_-S?umbwFbbcV`Q75D9;XII4kGIl%lh8^-+60Z$;fT^0EJn@*Duuiv4?No&31oy%OIM? zusnozS=T)wLEjz2*SA(R?ND6@Dj%U^LXiZl5iWFQVhSb?DqGm4 zXTv5*L0~An+B6tsP7ZIi!qgQEbN)m_<-APXl`tNTI$~`Ytpb8^&Xv_8X3xq~Cx>Zq zM%D=&dM%$)+=jgW4*|4z2var|EvpDGOmcT82`oUJ5=>)*Ob`a@EGk>PKo1=%O|zEW z5kh`GhDY1Fa?Gb%Pb0x5i-y%n+Q3mJv5A*~CBD#hE1O9}P@&znY-757oe?J+w23fn zj7XCRs~D<-1yaTyB7=`wU+4ZMnsHtm;zJ)0Ikrt(Dd{M4OVPES9)U}{vIi!9?U zKq@~J<~8YGHXff2x|lx0-=R(-H4CQ65p1Iv(#T8F+}6fGJo+GN^1*a@ZdSG`^;Y3dE|jln6iTzJ%z+Ru@kF9QFyK@Rnj zRQm*PGh8wR-Ot2(vFK3>?7`tDZiT%&`+fcKpwFMrT6{8aA|gI};8p7>_Yj0?AUr2) zT^TUZBiai^`DZG@#!*;>m}^#1S#l}FL08q{5Cn-vRiHc?8zomz3cyx zD@{1UZ2$F}Ww5rFRX7FULJ>3Wb2?a$aX`|q!eVxgLA8Ij&FdH+lRZOe&?4^3gG;l9 zzIo6MO;8>=U+#L61#W8uJ@;H!Y$qfxQCbES@dVD40vx6ppnO1o=#bnpS>;ax!!sEzG% z*1Z!19_|=4so{^Iv8M!g4h7(kccp3!zF&IMUkrT?x&0B*O-D7W{u?Q&-4->HgsVxqT{^)#E|u zo6iLHg)jn~3_+l@ zc&HwLor@tiT53XMT?M<$|&d#R=K@X#cQGAbpCz4jP1Rld#U;`#@!l<~x2Kj%+4SFG6 zau9$S7eT|97FVymnqSf}ecVOYmZZqFyJX1{eF(ohD8lrGfvj{;Ui?Dv8wXC)s2a*L zGC+~4C4dOAnyacPTxh4%Nx}Q7vh;>81W64SVpDP3A;$&ML-jt08|tw^aIWBj&(O2U z`q9%TfH(RMm5Lg^JjIvQQqp z)|54^<=EHOQJBt>X3aX-ZMRtPRcI1EIxcEJUBoJ$u+B#^*^r8D=L`P}#)6>*99r4| zfQ=YOH|GD6GM9&wO_@Q^tGV|z&6%3xVF0eM4QysqhwrsjL`^C|w#D|;F%KfS{ja5Y zl2EDSptP)l-qC@K8&i~##>}j4d)@z`kgOzp6=CpX zMLK)c@-&+3o7`up(nLZa*rdv6RE1#3EOtI+eHzmQT{E%v@Ocii&X*$emx}SLD4$(bDnD-oi9nG8sL4fR#M?4mSV4I zRVCRQ&@qp_JVk5Te~S;xDmE_hmY$cZMISFhASM#xt&dKJDkK6es>fG8+Xo-7PCC|S zq=U)Vs^$9r)gIKjZ7!M?GjA+xr-+dmO8ingZsT0U(yxE?2T8yBaEOCaiFm&@2!Y<8 z9o6@EPMJrOxp_2t7<%L%Nq5A1-FJd=WU$xN4^{1kA-~w+A)tCG*%Z_&I%k zL~1^Hl+GD3t*Q4;-nA6df@YrvJc?Ls}=*N8DA|*GbHV*V%OWX+?q%+{mG&6$fLJjM}G&zDIzqi$|U$+*?}>&ETA0i8=6f>>Q-WkDi$Y4qxbsqFWv4Vg z(YhK&rK%@t{ZExq<_ANZMTz}D6c0y69g$>s5Z>?qn-!|Aa_azrE(u_Wa6%1k_u)1|G zhC~}nQ1uKfiIHZ258!(M%i@^HN9ARTRTj4tb49WpDm&jfcuIqMe>S34wl6DQ;d-ET z?YzIO>30xGp`kXwkGTXv{R!p@%Pf6LIZW7cBoVvXny1>D>C9X~fj1HvaH;3e8@=f1JO#-91zf!_16ne9Et+atlvaV2)=$8Eh^+R~;5 zG65HZPxsYkub*$usT$#^wPM@~ouSM`U-gwg`0eDVdFL><`$E2+u+1E~8V8?8YWBSv z9aC#*!+%)McQQHlviC{j1_a5`FWet5ZNK(dPX6La<7UT3E;`F4Kx12bNep!9s6IGw zmoAbr!f6i!9^b8;E5E`JB-v zBi=+5n$f@2>-GGoRRGJG!xRcbgfes*V*=kTv^KrU)V&IQk>Rn| zsqB&K9STu2LJQSQ{2TD#$;^;kgceTzhuaObB*F?7JhtEzkc)KK?i8pK{(r|(PT+47 zz%RT7lf)9(0lv~$TiQ>b%~qSuN7X)cV)#=o(MdJ@k*)=Hzox5E$-RcPNE!=eSvMQK%eVHg0w7&#!SoBvqI@Vpx3NXQn|lqi+iWA=XIw&0_Z>?ZkQ`Tv;ICkc|u z4%_4;WzU0DQlC1RphjXst}CJbUNhMbcPg9&%{s34M&#j-4Z;%22^s^fj?^p83wu~m zp7fn>%feDkodfz zOYf~-So6L@$~rHTG?(jwQ`A@l2binh$mp~JuU!Sw{hDWJ4xE>#8>QE>(4V*eY;C|p z6?=p0Kfe=_N@jm}@3ecTtG}zLgzKCpTtGkaAB{{B(dZ)HOdh1ynk0_)R8(uDlCX)3trHw(A=3aCH7%=hVsA1XXOk^< z4(U=#!D1lKbenJrD&fm-cz0BME9s}>jP8OUtdsoQ{=_~VBPFUIX%e=e!4{YyTFlOG z!O3(2U?fux1U-c9o>5vqw#8vYc!3jQ1EDKgeBn@{c#Ky1+J&zqt<+;&7jo=D{~_V> zAt*Z+B~r(7)P3k+UbCior4us92pG(zD9`Fx{i}2ggwn%5o&I*`dA)SQ{PcPEXfc!E zv&3QR#(2x=A_6PqtN@;{r;Q=+$oBY?v*xKU8SjtyDYyt*nyjU5lq+d|%Z6uS$FRQ< zq8HwT_~BnTu9Kq`GxJDr7fxgT^%s0%NFg|Bf-Yi9ivbwq0F z!)SK6(!ukcuc^~C3S&_Pm8uv=&O`Lb@Fkwr4hnCA{^Pb}jsTUL|mq z)hYV#qQZk{uPgB}_>~p&y~WV4WP9ue)-}A(xTYlbZVS{sc`cq<@r-@+c?P#lu;(Q4 zyQ!u3wiF%{#7Fi^Ci9N<_dA>51AZ0#i0Ai0NMLtlWqd}HfekNYQVrqjxiA-(Mmvx%6)|f&5?EHnFTRcE?8bn&k%tnHI#qIS@ z%%uX)gnBnO`&iP^nIAwZ)ix2wq}oZ(YCWYS@0?#*Pv@FHr#<}q6{|r3-$X^L>(|x9 zIGl>9a-n?&SIH-7lOy$GbDxTogsfiMbLR*IE`20RK_i*qSH&WWkH}_Yji`)JPGM^R zt}`8C=TF&84~TBL=rM`AVQ*=-cX-lV8nex8#XGp?IVHVvy1-FSN%cknAMd|-aqk}& z*XI>V!`emV_;tRqfgUA%4^m`MfoSFNhpMs^4-;GaU@jL?=_Pj7>!4<)#DM=IMx}6R z^hBt8;0Mv{TN&SgjfpjVw2d#)9m40XHV&7r1AM%pqVy&=C)*qQ$Pi&x#J1NZlW9^L zVeIA<_yxw(^Xcl|hED+4$ADBWv!!F{=k|A zs6rl1jr{C3d@$CU;4{#76Il3?qn+&c@!I?)j%bAI(dd%ruUAd3Vt&TP0q3VeW2Fd3 zhCjjLosh;J zwf&k#1R+2S?=76dpm6488zjH$mLxPMM6t{992KOUZlRXI|8p{XlnC@S?f( z(;H~KR5i*_R~v~jO&FGQF7g(v^k{h0raPEQR_0ZRr$i{rBJV5h{(5>9-dKn`bT7Z67%G};{^hVUB319_Wo>Rx;$KLL(LTs2w}k~hgeQfADTqJo;{ zle7RA@O19CByxLiV3KZs~gFPT7->sy_T_5aiqt2Q%uGu7igd5qBykOf^+S z?kw-^=nhrpdw)Q-*v+qpzK%AZOLiJPxL+2#|I8LUGm`|1&d#ZC~bdAsPAp@boNV9WUw_Q_USN{Alsq5()Fl# z_SCLmHs~EH6>EPY5{~K|b=ZA(GZt`j_v38qUDxXEYyS$_=X{;W`^saj5niEcYhp2l zrCWa7;O`vqB#bi`mb0q7WKw>n$^rzsD48%%_}fn3DOd|%wdH-{zjP?Fu)%DbChv;B@*tVU(KtMf3vXYK_Z8C;!2IKr+Zea}zl z%yOiCQC<7Q=H^CMpq#BDB@aIfJeI5XWan4@?0G*B>HZ$cT$>L5RB>`L96{X3em=Z( zxaoPx`>VOhXGQ*UIP_;j$uEtupXFbG^ko<@hLz6V17n{Cr$f}dveTRnDZk@oO6h^% zAqnSltdzUNE7qol!r7qL?#ou5qN_dw0_>Y*fAf71Pi9^$lt{dc%H92MRAOto`kIQM z53EnazT=xVxsv91e(SrPJq5i>`qD=9IX`GN$XdJ;BM3HwZqnBLAz;j`6*ZBlq~lk> z$?pT4{g~xb;GgOD5?@tB2!~dM0K{Tyh&}}iYD{aL9|RDg;Ejr!%t}mQB17H3<{_MjtHDcZNptLM_ zUD$3T@fP3vWy2jF1ff*FUG=^aZmy@rW@!7(#B=eykWo7p|PZ^H&XIn0r z?*FWb@gBkMPZ@akJ2x1PyNq9qyyV+5;&=IbPI&9(g>eV%xH6e=R|JI4kS1*2s1*3> z;l~EO#2tc{lkR=-8`^~w)DpT1|7!OLK@|SJtH1D`Grjmr-h= z&7c$QYIQq0L@X{2a=e0A(tQD8?&`JKFE4Lc!la^N>?z-by+{O?b87bSd*jSrHmAThKK41b0hIuZ2W*E;-sj=DmqI&Rlal1mze0Mf&*jK*@JyK{`6MO_yig&2RZANi(j{8eDWf2W#dOM0Ku(&b!S zoTaJ0ZNRA34OmIlwsD6FeVekCPPdt!Bj1iX-1>`ER0b17sxw`*SK$i5{LhHmyut4c zqf}52UjT<1pjR5-xUF|g^1WR@9m$Z`yVGxd z@AsyK_5g``6Q%Eoc0}1|K=;c%;Ou%dhuG});3%F+#_NOn*eV-A@zbRPgB8DxYSWL_ zyKWdg2=Pt5ER>;cXfs-3I`v*u?XLP*#VE+#hh^WoH6S4J?IUSeaCK6LzhB9XYs6~* zaH{ax(^OO`s>N7rak^i9w*TIw<4cI6#QptH0f+jpV)eRAnT&foJJi0A?{?k-6))fH zJ6`!`{@$cN|B59eg*9f=aG^S&6rKmymc$fdDI$>*}NJw0{>!d^|Q_+*JD}b5Y4n+lkgy(-K;pAZ&F}?RQPg~l=I|EU>-^83DqJq1s0Dp# zSn+#uz0Y~Lls=>kk5L}WQSnsdc`uP-QgUrmsGc1! z4+@ga`FWT5&w~x87h;%t-gxWFveL}Rsu5S`agCiJej&@S@?2q0WIkY~G?~C;s`3n`>Q|g1u z9Sz-^@K|1$!Kh zobgf1BU1{!y|u;b=y2(yam)1|Wz#-)?YAb)a)dMwjFaE!V<(ZC;XR%^Kc3ft`B_s| zba*RXm*I)>4lH8??EFpfU0cx$p>CM%&2-A3h(IZPas@eTNce&r09{Pt*v=jR!b-9i z6+*#c&a9JNZVN=OQ&9;$z3ig}ltnHRsXs>n=9$KyQgnq`1AD9{`+N-Q6r=G#%Kk#Cp!#ZvFlZ|gtyp}sw^rHv?+5s zxd}vc3f{j*)3d%*R3_b|3=z^*tPlayqgR#@)s8F1Rvok2@8Z(05=J~~X@Ke36v=_J zb~V(r+;O=o&+!?(?yL~Fj;!Iz)B5!J74R5C>IfX$U~#)SRIqMakIBJWPd#fW?XSKM z`<^W2jV<*R1-=r-{(>(!2-+oIT)7?l+E5H^f~BO{$CjXuGucZzc65Hg5wSJ8w#H$M zIZr@;f_c&}hTbayCOEd#G;BUtd`l!V&+Eue=c8C0RIT)?M5BT046ZsAE>u z-Rw+tf4t0k_$w^AQM53~QNG%=OC%2l{H*x5f;Y?r#Zd9x?sY8mfQkYZ0#K8Q)pMl! zvL?pN8!+giZzFgbI4mm2!hh-Y{ru^T^Ai~1xu(6`vHtD-r~Cq*LxK#oEmwJ)k>bgq2?=>nyZFp|QN5!d4m2G1g2u(SNiX)sIR5#O`BdJ8JNwJggCS#M zDuc9Y*KCVdHe4^K#8Fgv#vRT0k<=uEr;}N`-_2=qU$y_;!9WhNX}ezoFv|7jU6lq_={v_gv+9FYJoLfw8>F(35 z96o;(rKz`lcT$^rapCTTQ?ylc0}|xaeBr81+Yc-Epwj@Ac#3wfh^0$(6^}Q>P`5tDmTQs~7Xp z!31i@#>F)b0^ji-dyF@>Vq_%#kM6!YD$1{IcPIgc0YL<55G4g9Bqdd(Q$kt^=^=-1 zK|n&JQzV84LAqNSX^@zqd+2634=TU+{bHRz&Uel~hi5HVYZmjwzW2WO9oKa|QsMe< zD&`S>8Z=;VUr}cGPkLw-26cU>9c8e6soFH0+u|XqdWuLcnaB~PIQ#!X+#8Ae zJdMakZD}Mb$v-uwIOb$amJv5-=H%xT{Et-i4kGy8gflp~bIhN5E=BifYh$`DL_4y= zLL%@k?=Wy@SEQ*Bzv+;D{O^2DsZZVf_Sn46PtG^BlS~E1ygruKE3a9&4BOHmJ=yVk zaSRa39cecMfqvIVqyMhg)rpAkED@q)ET`eo3kOQLFzCR}&^t@+)0)(@LBn_QFSiIH z{G%jDf?gdwZYWx>brXzvk%=o_+rpL28^x2Et4G|Y1ClY&S#1ANH1>8d#{cC8aQIM03uq14?Y;x+|;n#KMUUtJ0sAm z^$h(Ly(WB%VWfpbmeit|qaI@#9H?*N{!xHHN+HZpB})Ow@%uO9VU`2wN`QfT{I38% z%(79w@8z_B7gGMrZBW9H;@$O-KxS|_)Y&<*da$)or~B;SUq$J^<8&t|$K0%_vPdU#ZC&q`d;l;1#MYBR7&=BWuX{t%GVJ^=m6V&qn)zW&+gdN!1%h;4O=5HN)yx zR)g@8uSnL*iyZ=3g*5E4zQCSP%Rir<&UbpSmrgJ$vu4! z=4@?&J(cOyFo!Sc1|AodQMp6Ulk2oAj@YdWRa;|Sh5VkFYf)T1mwP+-_z)YW25}|5 z)T?zr!xrhcyKKuFL((-;$$qxnx-Rxwv{a;;|3PM-w)u_h`K zY+xS%%}CztP4=odnJ3V&m0m4s5cSyI??p5rCQnaD`b*gwZq$=F*4Fw($%$qKP1TzW z0M7Pe6D(wrXOa>jB2w_p%|~E!IT4CD`@A(ZSqHU2u8mktA&~woPZEoAF^$L8@ zD$vu89~wooh862#0PdZN53}0d6xsQC>{z(lFOXxl_RKpuY!Y9%Ifih;C*H=tOQPJ| z8e#$_UHZbb3H)&^+yQsA-W#PJ2&F2Shzicfit9|{Xp+SM%^Fju&|`nH1IACG^X@JYCdezbp$qq= zJ{Qib&YU|zvb1?JA4tvN#)n%MbbWDf3H%LnII5{yDZS>QnlV6nzB>5dQ^9c!Eiu0- zw^QG+f$wabtc#+YQOka@A?$1;?L%Wp84^>I_Bd?ynAY@i@vv?N{&!?{yqIx6qv#ab zb^t~*8Zqcz{1 z)omVYHT-LA{3@4G9ryV;y4e$7H~r}RY!!BaO)wZbm@4dIf4nolk1xDbQAh2lWAzym zo&ElQyKY>@fw(+;>9cduAp1<$UYz_@e3qKTU4ZL4j3-AHApQ7iCo&FFDfa&Pp8O|0}NxLM<-0jXJFRMejr?gmoHdbb*{t@DY=_nF*} z8e=F4>h;;Ca%E}ZeE{KQeHkU_m$EM1=R` zuZdcF)1$lW!{Zwq!^d&;Z#Eg6h`R;&vi{7SkacgHt-<%18tjPK{-W9G%e^+5qtv}b zL^TGrJ>NFm^(1D(*0JCLA>>}C`48g_Gi0w zLa`kBGpAf*X}!6BMvP+vG}E!KDddG(>eW4W+nK`;b2{Hy4$&y87K)iWb=`2vxO_n{ zi?c`d^Ww$e6aE~q8kE18V=2QtnLYKrye5;TlZWFWCLiuoVd2u6QrqQ2c9Esvr$@$* z5(x z#N^$tiepdZ1brw}k{Q3A*zf*Of1_gip_CC+IgntgSg2d)A6^9{mv!|s7kVWlOpg~Q zlj*5St8Z1RraHZ2lVwJi8&;y-raUdlWym1}sqkZicJ_aAg7E8ff|&Dy^Nb+gH$I-u zS2CE|k=RLo0*Tz~7zS$gi}(Lk^WFoR$BQA;yJorqcIJ<|Zd5P6}Q@ zwc6_Y=ZJhW=NOiN>~+~*ym>kU{a&T7g7}%(AX`X!3gOOm!w34Q&ocX(k4{>J@DwqQK>Zf^!+Icd18`} zL3cFbb0VC{&6)V*YuI8_R-ED7g_SO(^;(;Ubq=2gSYs>1?1*?4izZ+r5FmH@ePm*| z{0hRVXgkr}+lcOt-9|2_ikzRo1q!#F4>zcsI3SIZ+OPLkcU^BtS_im&rBJz{A{O`Y znA3sA5Iu81hP5fJHG=t>x3P1Nrg%=aH!POno;n(PH&%bHcfy_A8M9&2fo*9g+RgB{ z8n;RPso|A3!cbPgeE~g!m5t8fqJ0L&pS4%<8~|6U1H(TaTxCBEmz4TH0aa`fg%Y^_ zDKQ7tBj#C)hPoRbxx9Uk`j*mFSvuJHj;Vij;b&Q5gws)T_a}Y{fDK8N6RaG`7;%4{VvpoOGBLmg6Rgi+IXsgEe^Bv06jEmy%JvLL(?)RCD zTt`FZDo>k#yEca&?^=%A$wbS;r_HL)U~yT+>@FFfZo-2r*CrFskMS`OMl35y8}D2d z3L-Gub|U9Dhq@r=l7Ah~eBSKcNv?Sz0Q7$U9NSug#pO<@!$oQ z=WmU_qeJMrd$chuSWv{0qh7K#2S3QkKSK>g{?C+7>c69QlFxN2%^b)<;_nDPG1FG_t z6SkR`CwrB3_|{UlXVL5X^xPB506vq{25Zrc=UhP1RTn>rePVi|*3kl6!%V08`#m`i zQ9=K|^X{s^xB}uEp4uFr(!4~~T2ycEi79C5l>FGV+;p;8dIUX&6x?*P5R)~Hu) zuekWdFkW2P>O=x4n5wjwpwy3V&4ISl?rjb@ok8zC5c!Lsdva=;*lH-DuvJcS5zIYJxE zD_!Ms5Xm4(!$Nwf{}9~lUzg392MOqL3@j-m!tHX{p8Bzll*Id1*5KqwajDnf;zhHp zwbkH!dX7JX1rfsdQT!?J27#|k$QcHxlnyFI9cPN00ohpw`u!R6NUV~gQNer7Ow zl3baug$ZV6zL81p4}puhZPCMENYIU~;1E?t{y3lh%DJ;o9C{n|T-0l*qoD_H#z*pE zqCJ46lA4DxHSAevUZAkvxVkDfF0*Xt8GBjjxYOJ(I^Fmnz#P!lro%XpZ3!T8Rtf>yq+qCm> zoP?YgYD95tb2YYz73zhWKh3JoyKOSEoxOieK=o2;ZC9MZv)a+>f)dzVP7fTkJvU6Z ztKLU9_SwwPnYmHuuh@K-&->$QF@+kfP=|*PQ685UBOT!Y?P3f$OO+(#6@xA8th9g` z;>oKHAL&Y5I-ve(odTQt+s0d_Xh z;INGIe8I4SRJys^%lg#zm?E_{AYE4CW>oso%ZX5NfzWy#yC0KAs>yLe^0@rOObDVv z#P+@oT6*GiJR$1MK#4<8dwh5-yR|#?Xb@;o#U545icf$KzRO|=IlJGYvRHRs6KXv2 zR&5}+!U9Bp?6HpnDT2Y;UVR|1SiRPUR4>%8Tu@OIhvW3^j{!jPVM zD)wFez_--S>(WSheogC5d&_+dY(QdT1LOv?$FA>srQ%CD@(xQ(dy`?w%z-i0m2z5! z`XS*2hX}q$bMvtW2GZ;JgSMZwRpD%X*=Nz|>fV}n3v5TLGEmsgDJz}elTCV0Ic({T zCP`BU5|S@Ul4#syO;#a!y%KJH)%4N%9`-w) z3hxmf+-6LjBBkhOch1$YCB9QuSD8HEdr`-3@k#uSOp$`uZqEbmMYhoH58v2eh{c%r zAR$O!pYuU0oYJm4?IAN?kzSgrqN`YD;PK6}@;oSPwji=@nloI;-`@x_)MUFcbg0CD3dqCkCk9 zBK#gz+N`2bA~P0{S(VD2-k0T|i!-&;vb9g04h$l_k43i?0@LbdrGKGifO>JF#O(3W zPVFF#qCLbm9k;p;w9+diT$kb@TnFK8S?F3_MsElgJeUEEpCBI?^%j8#r<(e!EGDs+ zt%Tv7xR3^}S+f26V94gC)l7X^2tj4o*4M{ptUV~JKb7UH7^=7u!<}zVhhVK2&Q*lWYiO|R*San5mRSJn*@^E)8y01C1Y^)<v}1oyWr!s+>c1kJL`4QIvEg}ogd^ev`Y}V8-fe97n!(E*%BreneC2 zjyiudQ!%>ZGP$=0dwsY38w#Z<%NlLedTY3Z)L!y*1*c=K6)X5V=^ie%YWb&S6sE{b z@GS{jjG}Hg{)}&dc|1?fB*{;yIP95w88ttdO#F@H2Lw5F`Z}QxW~%oCX^NhD?4E{; zri!|6?*ed2cU2aJvTct7(b{kB>vL!G4kkGa_w@`W-?DT4F2EMw=hpC89Dk#Yl5wWk zy+yIyP*dZvd^i)$8tV{!!z;&1^zGtIh5z*vDUPDmv+hvcgs#JLV)#TGI^JJ()57|P zvSiUms};-gOMZPj_9)edsYt)56-)g#*-tmEn3KUu4gWk_OT19S-(PQZ$zZ^f8aQVi zQcI?5LN5s@lUkO#$n=+zWuawFDZ|xln^{wzc2;0t9NR)o=Yi5yVucfjl@)~g!oTzp zNArrhp@Dad04`r9UB8`9R;f*nxMpwPgb$CnSiO$=MZET8%#wiw-MyPpTDz$%w^iI< z0(=cx7Rj9ybhZ>R$*!yz?m1)1FFvJLRe=^tVGTe=C>y{#5(8Wm>q4!ZR6Ec%lN=fP zJ-mB%*tnQ#!#=>S&BPob*AgG^Zzi_-Q9~a$&%yCPGRUr(9Y`U3x|Tkz*d>H;5IV~ePt zEC3A&Pp?xrA`glrfD~iEUKgti&v>z@7u5vZr)mG`?}%x_zZUs+!2{P-hOL6g`6iU$ zJ6jvo=P>m}zn3&3a6jOUph%rsdu;uoiusDouXnco_W7>7F=SE*Zb}W2rZ3uVq#6Pm zEvTnnJ79go8C}f;g%f`aYD6!2?>3)4UJi6HKC-A>wZwclB;}qiPZ!k#YKd)b>5`nt z`Ro_0phe2L1c8-VOQ$2UM_XCH%<_vYY=TVVAS2SKewH(7uDM8`TD*8E zY#5LAOJ*`qO24X_SOV=z*Bwy_`Z+*{G#AK^(juhpy_u8vt(t-jFm!2iCFY)DX$t!U zM9DugZS4k9Bh!NpTj>A+=R%2BM9M&J^blZPOzV`Yh%SQQ%5j{STx47eU?QCCY>e=j zKe-Vg6*S~Mc(16QNPTr}Es$u@UzhM}TmYH8*}e9*M(zhI>aJZ)Xhuz;%9%CZM!f!P z8g|*zEbo`n{9m4g_(=XpCJ)9I6O;{!N)u!L94983$Kwkiq3#Zk~Qx3lR%dOoODGslTNBWws&fe4D1|KO;iVDPzA!36Xja0Av*_y@UpDH}BndWA@v`e^*0{W-s38Y{ z!Q?@c*HIb}rzHYa&Ay!rNCWC{zsWBaDN0#b0GkFoeDz8skpHP&0KyE*VZ5(LTlq+U za!^W&Kc*)o#?ebn{+7fuS09B&U3|T4TQiFb7)>GkZQ*eSSBv0XqK$vDYTyDCt74w4 z!fu;uPf&HvJBQ(-DHn$w+*7PnP_8yK0-m(-%>nHKF%^ra2`cYUfEyj>7s6Cxy(cziSs)qq%a z*ffZ?#J12?*Qbb-Yi&5WFR8od-ly!)3jLnvQ-6iSn$WE(Ihd}iOXPDpc&zcdLN|2& zd*$MvRwB@JGX_@UbNAr6<sgJz-isPue(3>Hq)AY#81 z!H1u>>4XOp7LmukaS1TrQe>bi82qzI7{n3e6_`&qkmG3{U(pt#NFF8?uJ+cD+sq*K zHMMK(EtUh3MRT4D);^O=N+Qlp#rh;VTt9YMJ%eOla1p#>{i>6b!}W4`>JG`5(g1Yh zwl7hQs+6(l&%_ma85Yc$IC*_aB3R?ui9x++Vzn@SnnC8o*L@!(({eF*;3Gf~aqqu4 z>D$gY%U0b*w6#^U{`6B#i8l>WXT3!%kA`n8aQ zmmA2ePP*yu$*2MJ6A&#J0O4RZ2`t1&%0~tno8i_ndHi78Xh@>+#|^B`B{2d*Qv8QW z49he`nE+&m%+afhLH$%hNj{$Lb|g$uECZvKYF;wA!UnN)yXKg)o8!`XcV&n5+_0fO zr9SCF!x(h;yuG(#Vrx2S`CYLbN7z36ByitsU{X@+ApF}vpwL~fUYOcokpR*h{yOy5 zx_wU zi$5bAP&t7s#oF0qQ{NAv!ryp+_*_VHNWjOQba~QYF*{vDDdOon@f!;Bx1hV;Xe-#r zr%L^;(iNzMY~P#I_jEajBf9-sB$Hg4eto0aT(Q_G(6L!T_i!&u{f$z^6XIHQ4@f6R z5&&)C%k7Kk8t73u$3dz?uN3dYu<c{seID5$+%A2$wtGxOtXucaCP5k9N{!i zU9oP)Mt#jsHbTs_H+zH`FTb8fMAU#bYs)3j(~JajeL_@c=7^a{M^OjMd9xnSS2g5O z<>};U##ZD1L}lAKLXFd54pH!p_^SePGQ>;7`3fuOdcJV5uPXz4$XiB0dvVqKd0By% zO$lzQzv1)=qW0DUKlXi=ZYTiD=yv9n|F>BH$o7h%(nV8^1VR-1fu=~PcGMh0_a3Q@ zZBcRA-ZFTou(`Mfa?VWU75J7Xu{O_k>EaB-9sRhb#!d7b;s}L#j2|2~WG!8nfPqbO zmuf&d#@>0X9*X5geR+PcGkdwS-; zu1J@cTIj}tlE!z*eye{4QWK!DE04l@Lv#%55SZ|47LS#xl1>;(x%X9CCH`% zTvjC@e2`Wjw#F``zBqwVTrPW^?JUz2QBu3RA6e6QAhd4vb4}G%%pWOxz#Rf5@@9^2 zjsA=VxCox@2t>Sv1_$bVsBC@ANt4XD+Wz@S5YH8^^!k8@Y&4cG+n^3B0)g<_i_fdcLv+${3 z@QfJX7xmrP?R{PD4!!CH*#Pw}-!EMsGQ>_nT@E+9*f{BB2}>Ao7J1l!f-bu%VYI=G z9!2S(T~Zu6um>P1z;55eG~N1Japl?;qxlRt#1KF&n@E|@HlM`~XO2bRk?@KR`wIU- zRzm=~p2m23qlg2|4yM}8ZhSGBOzdk0@&F9*#f%=fz&l9h>9jr#M>NKEC7=_sPV>_L zj+YBr1)|baS~1aFPKIP!;@Jd*ARAR-X}BdhX{NZbt2muLTz_>8qfa8egT#d;fp?3D z_`n;}i~!WC@$>q#w?V(WE|)L&r@c=4p*y{sHnUsvSyaN&h}9gc6Xj`BG0a+ty)nqP zl5~~S=M9&Six)M1y}busM;v2F4=7gb2a_WQwRE8$k^cV{SOTj=MsT3P>n#BWsqIvC zRp2}hfNcLc>*?a_cWa36rgp8!OQJ<9ocUyXLyPyTNP{;j!v*4(37en zp7s-gPuU{T*VKflfnozal~bF`v!l!IOLx=Cnj6hhuGPujxWSJWnPK=tx;7OQ208ca4F>hec2_4P6PtRBI~7bKu62wf-l;ob$_HLcgd5f?02Jcw%vY z4=4G@;iM554?hv%DWfhqKI}g5^?n`|fn!N8Mla)QrL?tiuP><}ceSUT+ZlSORqz^S zo$Qu(Hqkb$u5rZl_2!xDVZTN%8M_X@e!s8#_6&t%VCE92m+I~;U+0{R!`S}K4xiQw zH>Of4J^h)dXQPQ4HaZO%nPC_Y$qBtdn6w}?97`&n1nA1c_958pO8h6MYy2f zO|(u_b?kxcO%OnulYS69ri@EKdomgtroIY@XHOlr6b^lvRoX) zvv0Ek8hqDh7K4uy4p^oG(_*P-wPMD-Oz+NNrymE6=ZO`sVroj=rhR8L+guB7hfn_N z_UvV)6k!x%ZRULqRKL#MtfHEC#Gu^P`wl*i7zmxvyHTBZ;PS|V$aZH=7CGB+Ffm*2 zy1goD&b?Y?e`%ld*3`T??R{uF!7R9?xn2EI!q)cBE%_{Bnp8+1 z+SpFHzeRSP%oz3 zyVtS1_XQickE=oR*?b7srYwu!=WD2UIanK`5p`Bel&;APYa4!3->eMPPty5lUcPhO zX=ymGfvk7hThe>v*n>>79hM1I6ip_F(8PZX0Fn+WAJ<~f@~0sjS7%wj|0S8+F~75A zvAc%GbH8nD_I&-kiu)iul*S;w2r|4^$Xj>3wKD`sS_`#)2uN59W+OsJOi*=9L84d-=Pd^(6d z(yQ@Um(dM>;!PQQYhIWRR~jVnc$-K zCMyq0W?M`uAA>KTzBUu_X}Hg>_}SWOnw#l7)Z-Q`Lsc9aUCge%W+}*y$m$>u#@%Bf z>3XZ3&Z~1Hk(kU0-!EFz76v1>17Ui(4n#c~ad;~Yic1&}powzf8S zDcr@cmfP=X?sz*}An>lR$i-gz#OIPTeId_~mFj>r@Nd5Xn2<54af5E$1G5ySX5QRn zIq!cd0Xg~mU3tRoA=Xmdz!a5txEQ0@>? zV68p3h%1?@Us3)#B#U=IZ+N-Cp)6dwAs6mI!)!mhc=K+zu$IzNF#q^1H*$mzIpSuR zDTAX;wjRYpe!!3%MMe>;I54*og|JiP{vtwjJP6xe2p9zD}vQ-0%c+ zp#OZc7~QAs5lMl~@lJFXmB<_yp6i!YyIUg>R>&0(5iZr5>16 zy9VCIcKc~G5%D&23;&+=BLGR7OA8O zm-D`EF)kMHF&yL63?hNLnupKLvZ1)3}SSfFY-`_N(24cVSl1+d=!0u z=PjJ!uidm28x?bk+rJ%%TCfxT8UAf|oMDBq6MsJsqyA;a#_YdqiCE102$JMY!xj72 zp8V*TORSVrN&@Jjw-nI+nl2ghu42x%(uNbtzl!DWE^(W-*jeT|5?u%#*2;Xn?26xG zf-T7l%mPiR@4EbK`GP-GSAHN7q`>ibI6jWL?d)ZnZbx-EUX2=exJ-}30r)=n@M~uY zU5Csx1y|&x7C7eR)29wd%kx4TM3e)|N+Hpxv23z@$AhVTP07cGdYKX~mKNUwx()2n zTM7+}iCDMxhpF4@LS4vc)FDcMIjQe{*E*Q~{=>GQw8G&@Y1llG-RqjyxI)P@&j#I@ zl3RFr6HQWHn7Httx&3r|NI$Wta9~fD!bs}_{3`AP%AC&()_diH9M?{sVJr3Ff*s3kRqn2@i<0b^4A^0K3TpA<@j#g z?^X(=$JyCF&>{TYL?NL7%U#F5kNm{vnI*-ThC`KTa)>x)~^>TM<=ue6SX>QaRmx=3;dH6Z0OBIqWTaC3G&>E_e+J7F6vgE5`WsT zW5$|}`jK856T~I-^eH$?yva~wF-ujR)FJqOsJQKz1~VUHiH^YYwH|rhl=kxcc&$+n zSH~(}OKTaX=N&vXV{ICDWGs7##qkT?riiJE%l`_fySE{w_KZXaR$=^Uup6&XiCQG3 z-sik%RZFZho?n*0w1qZD)XF}QWOC4zoz$ir^Ur=1%ku7Jx5{ZCIp+*G64)s0@`vl_ z?&l;g8UzgwNgnmJBTw%tNW*-$SPXT`hbJ6#n&rcJ?+`D&Vu|V~e_>IH_k@vigZu{T zw>bsHP35Uyae+IASlAENvXO@3iLYkmLny}t-DFL<*ZJA$U}bsH#Y3y|484Ja0y?Ut zW=m#9F@dEGq;;nBk-F@eUlNI297eg6O<=H&CLSjKjoc}R+*Mx%+lCCZLX$iqy%dzR1C8Q8c(ujB1o9E^nd0e+ z^3%U{ZLBBH8!OZ*b4r+7hu{B+7m^_8Wub3H5C$GVY{BwRY|(o=+lsRG*uvg92k+Xw zCV4Yb=G$%yp+Igy-_+dW z*X{1%+u_wLFFu&S$mr_b)!Wa$rH$p!sO9L|;^*C<#>}~)jn&=LxS@%XzR2Usq2kD@ z=xe$!l2^4jkMI*mBGr2xW=lKfRDGs>)^=h z=G5c4hvVha1hPA{sFh0-O z&win?f3U!KpRwM&(bL51`It zpp1Or(7J$cX1dndjd^Xy)z0hQ!l=p3=**zF$HhN0KdH^qMc zzL~z#&w#VUc&WVCtB&dE+1am_!_UThq_nTX!f%(Vah$Q*vyT4&0&0?{!LOH&yvcK@ zz1q2(<>AoZvVpb1y?(5{j=aXO(A3tZirdApd9A?A)yd1sxR!!+Y@xZvzo~(#x9Zrs zlYDT-#j&)Zir~1La-+Jf(%8GFmEYXUhON42jG?8_*raw}ro+eD)WVp=&DqSjzRAC5 zO-t3Me_by@&z*hPyrcWhnj0NL{~&dwR3KJI!L~n zh0(;Wu!L!sab8F~L{VmXcA>Ogb%%AEt(3~ux_xzPmak!!u&Cq6PB1{zvYoAFXU4Rm zuDrJV3J$uEgIR8WcverGYgf{^rn`uH)31lnrk${&@Sg5qEN>yi! zvb@B*p1Z%ai$PG;;@roVb%v?5yRf3?*v4Ots>_6QqQ1W5<*`m(aAjIsO>Kw9nvcAY zag4>%uA!P+hojezd{1|hMp0v_hIxHqT5*)5#j1W)fSW^1TzYXxe!jYSyew?SzP*+KIvf(&AT8qIiX|v32aP79X@oQ#+ z%&&T~l=%+sF8~DLaY;l$RCwBqz0FIU>DD!V5F{U+aNA%c-0;v{T@y>{7b-+OBZ-Ow z=~B)lghJ#@k|6N|fD_UE^!=Y{+wMY{Q??jZr); zduIQ`)55pC@R@sf9r}#s&-m!LJ{#j3W8;>0 zdiW?jOY*G9)9G}26BG4%y(nt!X0y30&slhm^*Q9b^TXrA>{(siTwPt2_u->M`B*;K z+{7C^@7}#zTwL7QIX&Ip-hP%o+t0S~@Ll+fot>R`JMZ`$9UaNL@I&wUN&Z}3hF@@c zx+wn?`PZ7BK08|u|IY1pyH+b|MN!o43BMb8!p}6rXDsXQYw$S>zgPTrzxsqv;pc@f z(Tq=9ANio$#lJ!JL-w6^E&Oy>e!7Owso7JO@|`Z8@F`_mzeWw8RO)9n%Azj6FZ|BJ z|4tMC`Kc#Q_+>>SJViZx3$KOzDTRM@Dc|Hx*Pp5Inmo%>Q%!l_oT@%udBrc;m9G*0 z@Pr?f*B39| zym+y;7GC-M;~)Q6T9d!5;jgUx&6U5x_rDEK`1^bv&&_+oo~!yQFX6p>oIigf#9fIH zcP+j~nA?z-2yvr8xI1_u$j!>j-f(Q&lh-}`eE6*Jy0^#oRd@>k7vA8@wl*B?#({Hy zxDCa&yW_V6-4x-z|Ka;@0CCrEeslfyXCU0C`=1_uGK71y^oR)8nF%laYY{M^O(d8W zfD?#2Bf_2IC7{~5=4057}{;8qia1I7WU1kj%0JwA4v+u05fw`dS| z^ltMgyb*+ZyR-d1JODR*3on1Q~o*fqWIQyEW`V*Lt0k>55T1urwB;+BJwI8*6@iD4{Q9z z266IRD&=z%_QfN_0oDZG7N!>h+D$J!2FT4# z<7dLp&*8%bfw+Ki&lTbX*$x800pk7)uSW@-Jr_I^)O(&n+|lzV2MYn><{tkMAg(JI zC-__bPxv?N*Xx8Tc>$VrPJm$qIJ^j|t)HxS^aldGtqaDPB1Q@+($*XkKyG7G42fz?u8>9o~1`?kDfjP!a2wVg!>j4_ucjHZr;0j zZ&qJg;Z5tFyj&GvyQ<&h&Fs~ytpSLuusDEQWN{nUh;Rvu8{N1A3I}|OgZu(3BMEM)O1$aAlh@#7y`rr55jZdJZ=Eu1mXO37>^skH-9HQt_dzzfjEOTL#gnObBL37f@MUrnthyL z9DYPT5$_f8gwK^-er7l7xC^x|3$m8((qQc?=$brHs z$o*q2aJazYp318O+-Rzi#aQ$OGQeDnHe9aDwFB`gjY*O>_zhyPIo;V>#Sm(Rlg+eqRZae>BV*oKeaZwtl+2zxK1YM^mDuigRQ5*8;kZu^-sIC-~yDTC^w z4XS6#;sPWT7qttZK`)TFos*I^E%&NlV_OOnd9kPeJBcyPYC`S@E zDxk*cRM_0un36a`+_;UYT*L{9lQ|VEPDq>qoKC4+#F51nfH(jE{?5zT##P+8PO1iM zFAt5p39w;e1=g)%NR11465=ZgKE=9Rl~Y$W<87RxanbwIDlaI*0f!m0bA1{&Sp}9 zxDxb@uVin8xD?zB-pK9BfQ#gBZTX)75J~hCoGUUGSD)y`5w(-Ot4^%S`bcT}Iws?I*^hyRb|7bEbhPKgi2z!XIeuGQ)#cYc1JN)1ndYO9-^QB`pO;E}`4>zw*-@!gK;4coci0O0x{2=|_X!)d@c z@VM~20>bS?7N`3-S;cLiF3!&b;ueg=1pv7aFaW+4LgVV`oNA-$vJp7JxHCmJU&jTw z#P1u6$muw&%HS?Ej=xU6P8&?lXjhG+sr;i#d@G&ADUGc8C)6@3r$5P`@pYUa4nC!8 z2v;-!bA{rZ{3mG`k?R@4aUJK&xw`))g#KVO4u3$UjK$!QyY04cIF|m)9)xSFvz`MQ8OC5hO60pgmvh?}C=0EuCFI+<1Z%!%N@Q8xU4PzKcqaZiNB z1qyfYr{bMp+MlVm@anlioM7DBw>v?Dc*03_PQ-@)*-a^2-w2#++)fPORutVfb2^ROpS=^HB;+{V8 zRUAp&ccwN7$c=!-1&|YDYmKxxsK%{a>t4EuGl;w5@OCB6sX4&mT>^2f@Hz^7yW@(g z=?&Q~ZjbB^Yq;5#E#zkX*EpjajW7Dx7H)&H>X>T{cf%U)maXEjj0^G0kE5b0BP!K~V;@m<6Su01I3Sw}4Tn0WE(*R;ZaCc`z->Pp zWN~=j6VHO)uyaI3^=JpnI3aQPrI!kF*v0)@S=`xKqBh8w8nCUPj0hT+tm4{{!Bqu^ zWs4ig=rmCIjoY@Ezc3*Y*586F0wfLfXld}KU4Yi zPt_XU+5?$YcX0vY{@4{3*9X1{#07xD5K8D;k3;H8{LTtq)?I)wr9qZ={)g{!K{SCi&ZHqBRJ}Eegi%ES^G9HQAuT<%n|cw+-WOaW9d~)Drjb>{xTrP+EanO5&0w9Mx2lRQFU?g*k5aM@P8Xsx8iR)eX@aM$Ip>VQMOOac&vWF*Rrn z*M-BuG9rIO4|2mum{VmD7l?r(90pbXV)M$)T$ri4Wu6$K~l`yBOCnSidrZp@?Q=E>f{O~NYxaT&gx^V;~?s;Yqc@IK#NT|E@B*79DZU;*`S8W@@TfPr&2uZ~zuH z)o_jArdq1Jjz?D7081KIPQvDo*887(xkg>JpPx+*xKBv2EPyp?gJy$6*~u zQ{17=s6aT?8Ul$62=^5l_aV-zaZ<%5?&#f)Vw|zKOCxa!#DT@xoQiSI#)h5^s>b4) zIg4v9r;2J$=9W{uTgIrm3?dipB28BpfLSGXXJxJ@IM#`+m&^hNQwkSA4z`hCjDz#nNE{iQsHq*U*b;Hf5M2MTnjK3-g z^}yniZCtMhfSZ|#2Dt>~R9p4;QBozy{o|>yI5&+bi@Qx0CnPQms$mlsgoeQ4Mtt!m zc&4kkd-sTM`7-WrH?9o4RBdsoX@m%OM-)|=BL(9;+|bGgRAX{K&hlreFwE+k1mX6G za&Z2()Hkv*+8PfFKj7Oakxssi-4%rU!SvMcZ{0FMHCRS&>zw)tB<|6t9N|Q3kVpOp zx?m8XsTenX%w?STegm=@#I5Sf=AmO-fHmYC?!tu2? zoWyl16i(eEeRlzy^cVI~1SN@1)KpWIS+%=61(1{9Vt}@`j;UQR*Dl9Yp=50!T-S^z z261>R>H~jNzOW#i{6=)OH|4vyIY}IV$t@)K2L3j;obKWvH~1#*0Br8Sc5z2Zy5W^( z9Z2Fn*rfX8$;$;HaepAs5CE>;CxxR+|g z)$=HcvA9%i=*`HpwKlV+zLBj8#;Hq=@rD=aE>1?(r_@y4GxBZ5;%?r%KC6PN&^3WJ zDh&)ZsPi{n!{Nif4dB}qnntb-FgdU|^~G(Blg71@2v=npw^wCyt$Y{PN_KH~)H9+1 z2l<-g*`TTbM-mqR?p9pHsn|eEoN5htdGzo>7TyZ~OBuw~5N!y5mzQl^GY%JKR56ao zsEWK6R&a;ZP6>BFwnNOQs{(FMJiY~tJC#9IM1xCH4W8w)12xt698?$IDZ&xrFsky` z3Wo!W3*2zk2Gt6SbC9b@s@X1X&PZHSxS5$kmLqfXk<}oOGHi-(&34pB+ZlhukChxt zxusL_!11kJHID?euzei8!0m4QbYpRaW;axG$h{-{rd+Nb`9Q@(JG z^JJ<1JWV;YW#^^@PB>lo{er(W>G~_3#|cithvE=dQq73*t)O98Fl;V@oFZI0ry9;J zb4;c6*C7sE?m0|yPt+D?a>LtXANN)v4j>2Dh(KJ>OO(WUWGi34?MEWFvM^;U$u>7ERLh94y%a;*g_5Vvi9p6pHmg&p3)c>c5#ULft9B?($OS)a^x?Q&-bDwF!i~kY!v9H(W%B%QaO>qstap zP|ry7EOp2&C+HS_&H*k-H8^Q26|#!j0MNnVMmh%7l~y-J#C9^$cH2q3%R^lHXz;4e z5EnqJ=c+>_aGh$%!LmglbNyr?XLF_L4eip@fn5VK{Gm<33(ViQsSRD`FOJ8`TL^;Jb8h){XBaLH5 zH9iV)H2$9MFu{;);$9CX^3DDG&Md?4PRZhIi6@h)u{a81?oG|5ed<`Say`=P@SMSwx z8V7*G8v!|D+}04)AsXY}q%1CM;{uHnwc#g#90yg6sM7f>G;TyPs?2K5M)D>KYXH79 zyY*Kr?%Iw2z~V+JH?&COgvV79Tzl$@!zOM|GpcMFnKe1pR8=Beiy3^(sz`2OPTlaX zkumX$1jk5#Hyczrr+yziai67PuM5iS@;;QK99Z0L49f}dcVFgYV}wu)P2 z$;c-CB8R$xTlMJHJ2n5p_#4<5wrzus5mQxxai>P(4CIiC*cKKizg9+6n^R90-zkfm zwmFqOBND$){;MhoSL&FmJdWPo<>idb`999I2LCbki?|rec~|Yec^WIBZXj#)?tkRF9Ew3~QYDT@r*E%gN<*iU?c?e(Ar9|IWN{6J${4iL-Vv07 z>|zRqz#tI@=&Ja4Qo{)Re&ld9nNr0lV!>3yI=l!s3>X)F;qKH_W*Gs-NitP2PTtKi zn>v@UIL&Wm5O<(VPD@3OvR)Bi#C`ZcQB`yJ0pb3zIW;gikht|sWl$gki-U@a4@_{d zz{B>g&;7vdOD^Wjni3m%|r(e<$KA9ykC1!RaC>E`qX!E_v0?^J0hIK z8%Aas+8RL;^&a0`y=N>A@Fu2_)?c$Y0Nh`(xI4Ow8y_`^Kobi?Se!2CbWq*%0o9Fh zvnj@@EpEI8M_hPDH$0ho7mg8Sad8*-PoZ(*iTi}?mhiX);R3|%OFUIDF8npIO`QC{ zc{!nRXXj@SQ(aBPq`G>(%CJ2#Zj(JqKBGRVKpZeG02S=Jm{Pxx!@bU74$rA0+@_jF zPNOf5W2%S^iDx8W9IKQ#rz*NlxY_`4Xmivu8a9 zc?x@5q{!7SeeR6Yr^K}~Z9<@|taFaodx?2=)cQ3Mc&h<^iL$^u-$V`779 zDyf<+PCuSIo2H#)8A+})R~xDJ9r?#Rwuozhb{e;+1;Dwc6Kzq>giAysg=S#yeHE4YCnRes>>ySPf#NQK3L$f5BF z7{5MOt2%HTAEKWpKwy>v{)hog5)Ymg7j_iI`z232h0$l_A9;j^%~+n+6+sw^(N zK6>=Pw1$p4M$BE&b=$9M4QGsQNr6(RiW77Tiz*rfHU?SL zf~H@pRPIAH;jpOScq+g0J?B)e;}F7Ehzo;iC=6&WE5g+z+R!WLy_`r{9Iz#^#pSxH zBb?2qXED;{?h;Ei=$x9?A{n+7={~Nka5ROuS~}ml@fSf+Ybo(Se-~zwQnT^_PN+GE zToBOeV&dyVs`HCHtU8f6&kVz1&Xr=O zhShg4k#ETc`8^4~?RH~XoR=x7tU8_WxalnPU@Q&@cO=~HNEdRRQT5`ax9OM~p7-+d z_B}C`$fbR7e-Ub{as&EuCDQ#nJWKVijh9H?gLSk;4VM z$k^R6+r%XSd0WmlF;ri~QsBGYdBujRV4g z%qhfS8OIs*&^R0)RSh!Dz)9R?T*t`}`oWiR!9^#(a8st#cl?#7@1LpQ0PC-?xKnM5 zQxYc}skoq=3Qa51RNHAvRb>XBQxm8&7N?V{W>|l3{=V$U*hZyE#V`U?Gn;Wj<_g2N z8gg8vsH|meAfp<$aq6?v)I@20B>=cm6=RVu%}GBj!_^sL|4nn0{RRNe!V<9*1nMf} zfl?4|*XosMVVPn>6#iRLB8N+ali#C_x@;ODac&sMg-`ZHoCLwq2? zhin7HhS?D%aQ7Hxz_$^dQLm1SIKce{i;HvWjhw|9!~uTTTd4>)LW{2osyeb7w=*Z) ztO}}wm6)T@RPSidhy+weRZ9iH3Ao*rx3_#w{r-oziBk@DTRd^$<$+?{WHgLuPaIiX zJ0)@O{X$PYJCB3vp)feUD8Q|Hagu4NvG8~C-Dc))Qx5mq-&X9uh;X49^W7o|pqPf= zdo?h&aogS&2V2~UVGn7eTF{ z@Sz`+^plj4{ISi!_I%Sa6HKdrK6!GK1i0R2W9k>7a@n9NEUw-^ftZ>g+&YnM zpL~r&s;%JmR}x1sdVf#W;f%AYINb;sMRt74SzLV9XdPKg9l(xq)~57Ai!pN+Cjj@; z!?=t47Ay`;BZ_eJ!rfyP<~;>Dz6OvRY5hk<^~SX;71PKdizAiOVmB+rYz=h%8o)(i z)%n|xc~_jXIA9&eQ-tsDVoJr!U6UJbMX4d|;><9DDV5G&A#e{Am*N>&Hd`E;;$VtH-B2>B>f~2JxUB13Mb$)B)#>yMMrcPxLCV_i)xwZM zyDK@X7DZwoDV3ku2Y@)$Rb^oxJC)j58E{QW6VmZ0Rr1b}dL!w_oEToLbYc^feXCm& zFcM6n%=YVPdR}*Og^!r>Pu$grg%z*c$0e*Unru+k7T0m`+tE*4BI32n^w*SYsseG` zy3H|!KbKqMjK&e+vRxcLJdnb;KXr5+kQ+XHcm{+6#Jv@W3$4F4sG?kC;w0u$1>jap zL?xx;j|s4m#A%D%iPemB@C6{KoDe%&2}RyEs7HJ%k(X5#z-5%cvIhRG&;o61Aaq^)Fc5Fh$kMM#eTqS&1n; z?hXNNWH6;VtL-9a7-3IbR`xqvIobeG^$u*m8@h_a4vzT7QY|oe(f(f9A_kO=8Z#mOI;(Yhsic>)u~)+8d$!ic@WJREfrk!w zLRq!cngN*5>PhajlE?21ORCmA%oTd$`nrwl+mxzpP0785E*nH-S~W7+RAR}F;I>*u z8j9(u|yxdh?>ZkrX!ftu=( zyGHcoEjip5v;GFth}+^gr$!pLuOq73;nsiVj|5zkk955#Q|#g_qiP8*=`@!?HRuc@qBV^8 z#Rg?@OseV%Zsck!Q65@DS=}Y~wBg?$+JA6dP^<;<$;E7A4e~b4q2|Z=i4wL~Yn7#zAaAId1JjZ#FQvrGqLL zaRPJ6s0zu!0B# zM%~u(Oos61Mc;UNe5QjcXTuA|H|xIqm13oA88?;2R0(qFlsfJ8F2^w&%?3Q_&wU%V~6<9c{*4Q~xGmLA*g3O8fK*k{lxq5cc zA?uBkYc+N2cvCuunUf2&vbGBaM32|(Y7d@3R@(;bnx?dHLZ=Z8aq_32b1r-6M67TQ zC$p+F!lkonMQd1I_JHbwLflbqj0@!>fyGIA(x2I=O5g974+e2NrZxl?$0qZWbQK5h zn@Xrg;}rAOlY)_0X})f)<|ik5@xSPh}?vcImwWX<4mW(in-}>M9;;vn>5Lb$D9>X8gc!PF=(Cslh zq}beU(#$4C-+|j(dvO^jBQFUxK^ zUtT*fPn>q7K}_YSN<$ppsA=S|BC4(iypeBUap84--e?@E;?(*9!2wG+dHv$PkuR3u zQc3mH6xAJzZ&6Z3#|VwTaQ<#;2!A<3Tp`LykAI_kiEYGEs_n|0>U|?#>#Li%rtIN< zr#ntROZ_5kwSPppp-6{Pe9WdvDwM)^x|s>CUB(GE+rrf}@l{I-;~0oQ<%k}uA?eaM zJpoR5LnpV6M8Pi&Ip9qriBmR!bS%gumn)+Ycvt?BZLtE&yebeU;AZ6~ybMNTTrKLV zJif)Ba%r|Haj<4Q-N5S&F^z9`9Om*V6=DMvRmu&6Brg289dCFGfU{NH7t**yP@Ve& zEY5K0MDq;J;WDtpgu1VyYNul%F3zcfauX`7o{&QG|8s~5>6ltq7Do=3NUET4w2rJL zO~61o4dMR^PWOg`>KkWqkFq)STNYvl(+J>gWQ2mMT1M{CGXhw%@Rrn%#D(1dn#EnK zHjN;NZxJqKah22-uuYFQc>JCmF1-Gj(YR5oQfkSNy1^+mlqiYDfDsiePUX~FI;949 zBa6HK>9bC$9&6y3>LhNW2clE+!&}iY;(`N-oEm_I$BFSbK{(-YM7V%%;d_KPaJfaj zzQszUXWKfa3dX&DP08VP-Z%0-z!zy8_0+f8rlc$m9L{ZV+{L*oj%tH7f7_Ws&v2ze zLlYs@W@e4^*|e#V1|e`(poADcnP9h_Z{mt5H7KC6qe$h3c3I|`2OggBuV6o&_~I1f z%C_2FYW7*U#+9?U!GuF6^^L^h-~5t8(gIxYJeNfiY6fxbUEyg4ab0fbgw>Vy8>!Vw zE!HHX^Xr1O!Ww_uUEX>?`fH8=C(Xahif=qrGyT{&oc_p}t>Rp6$T99fG43ciP!l2U ziw>$mQQbL8JR{EHPEL4jX5G+j-_cFk+)5f-U9kv0Ue=Ar`O;JW2k6x~uyRy&4%bO} z+!noY&g8g|L&uT;+pmDRUo%lP*y1F>^_1y`pX>tHw_0mXL6z$`YlyRXbcAict*ck_ zbVKVeSlp0i^Nnk^i6g?b;EXdGHydwoWo?*yRy3lOzsW0~A06Z3$p($xtLazkFmFLm zm1M)Me}10?8@^Gs0gp(jpD?CQD#EQB#A!TLvRkS&Sc%e^+J28o;`G}gRfcol!_8Ay z6@Uw6>!2ENN~%TzRGpsgSgU0iP{Z>j(74~8r8G|6zw)U&zKTn#l(xa)gvI?^ zNZfq%j4b;q?!q*!iHW+0xU4Y_V`kS;PLAhkwjq@q@b-864v_7PD?25E+7g%gJqk*l zig1O}Ege{GAZ=&;N`MDS3{)H_NEpb~^zdrQ+p6+iMMBQPb-$^~zfXTv8&|Jqt&J1* zu8%L$P?Xb|wWh{hLpeg6jGHjA2$S2@>#O0VLCv9ov9#d^JIQoGtY82?oV*FbY|{?2 z&Uv@_oQ6_0&ZX>ac{*whE;opF8jFAbv;z)$NA)P;o3G-07Y9LANSvq*0HqUaF^3<{ zK#eG8qwD_qevWZTgzH4@aZGXXnyQlNiCzS_4iOGwL#G$#RE0S=kA$}K2y#47v!?D} z7gWLHDtA=BQx->v8yT@+!@Vdoq(-=O5%-rYj>eGzMRhDX<2Kab(Mr2UD!w>paSCaB z{JhwQnWY=(Bi_)nTRNWx9!Jx!d{e_nWO34H9vECu8+1;E=Qp0++7AG?!cAPyF1R$8 z_C*!ET2Hg2dc&Lg3^U@3g_jM)*sB;Xj-RRfciHMMX^G=nuG!SKrr-T4 zP0h|~)$$Wr{AU%$xCZm91UA}8tm2%ZuEam0wvmzzC2gxMDa&|&C+WtlXREkQKCM=w zT#?H4V^Wp!s+^IQ4&X-NX$NzUAnKrtH-R{P{ML1I#RZ6KX`y+-;__!Cg}I?Dj@Xv9#*JO$sc6660EF9UjgF=)ZgwEu zfZV-JsCfA?_l_hPRXf{Y1#xII7cbo1D5q-0$Su2``WgAu&(7i=KKKN?$b&%Q_ER|( z$p);Iy(SJMPOn4*yS zc?(2t@zmthw<?nt_O(?K=X{^kG&76%gdt3ysb(VKxv+w57g zVO1yAyd%>i`yD$qs313SLV)X0P0d}u!rlOJ9b3q4#X)tAGwK#CaO#B9PR!K(OBM%X z9Ou+;5#R#CY0t=rusB8>;x=w5&KbxJWpP&qa`-0_*5IL^E;_yy>Lmjn-<1333l0}8guu{@fyuCu5G0? zaS4MflG`zbTRLF+H%gcJgTc&@udjIvItNjpU+N$!73T2ojy_F;6!VP~)&e&$qjm;M zfMfaZM6!nKH~i=g|Mcuwpg`6xd&zf|lht-1Ky>Z90%ySQ!sbH@uL@ZIQ*HT1g}M_~3;h94v7% zs=gVp{+cn4f~x8bH*HQGQH?>fsjZQ(0GtBfAdB)ORkboy{mp5Qs;+)BKzQK z8?i5L)D~+4JtHlB=i9ja^49E+aTjM@aZ2LudNOqkIiBwmV^VO2VRpS?F<8~`rh z+;6&*%gznzpsF-ZjlUk^+O`lr!VP*R+E;OPf?G^3=`t>shC7X$(gauK=Ah*qxP&`f zj@KHR&4HRY!kEeo@VhqrV`FyJCg3)2Nvjb!X$v-$vYmIG*VICSQMRU+qZ=Gl3)?(a z!$b_5eDE-w74mkZi6LgOz3 z_*#D{GIWle)zYkIbZ zv3sWPg~{QAH|qQqj8hW#ld-tl(ua9d&eMqVmk>A78lhKYIEC{$HNeT=2GrCma%=F~ zwZU7e8`rMI1Q#qF)En*$PO5tr!Z)+X-mux!RR{C^-r*)rucl%+-5Ap?oEBlKWyC5* zV2b;cOsNkavFmqIhSYsKo;uMp64&(m(7uN|KRY)$Ref=X`YLY@GhZArZa#Sphn#u0 z!O{&rs0y;-5s(AM`K%f+5D2#;ByJHPx2=7_rxsEL&kG2razm;%XnH2Df02v6(6*5w zH)>*QT(g;p4!Vm=c6L@V(zHvdu}8h8ybUD)rPcEmZdZv_NVt$dJ+*7*;j*j^RH8ep zvs6C%=KP97*-(fp>QP>)ChIC(2Gs^Hw5XH-*4Cz%Ib#S!6-a`W$VMLuGj zT@?%sBVXd63W!tBi0|US!EFCFxnF%OwyM541-X?xt_t;mEKW9ZrlsblUlO>^fcrP@ z<7N!zh;c-@H;Q$Mpz0W>#~Wb${SFybOsNWS^uOJ^`W3z5Ke4!qE$+qui@T$8Lx8xe zcaqx;1KT*?#3_*j()~C%s1oMxSh8W12#4U--7#y6gJFa$?gs#zgi|Hi;Fb~5Qz11x zU`yNzisJCUOWjM9R8GVxgm_v|{UXPraG@#g z_vLty#`M%A+aN8R$u7Z2is^TcmDjgo|3MUT$!#)10lcD6mCapH0;B zh(jDxJNfPCG{)8E$r_?d3K7KqmJ>-Bx`2Ndjb;SYVs?NBC#T|LDK@cwS#Z_5c zU0IymqA{UiGQ0?bTf#fr$!RyHoT$-*T##8;*0a0=sX_-gY>93Yy@8^UR8Wlphlq1) z^p8YTV?gzVJAX}Yh`u;vRKK-KOt|8(i@OI7cQ4v~iDxbUuw~#k$l}tvxQg-j+D5jE z3w=tXt?IGp+0+@g=Ry3$Hqy$oTM3cVDOKb5V*t3jh;YI88=kuYaLjJ~Q;Xt|-TL&I zEbh?*o@}7h5KvC$RK zLc{KsRn?VTsZpt#SDg3M3DWzkPNjN`IdDkrPtY+EfK4FIlB%o$F02UHftd`A!>R2S z!RZRqiR1E)N4Ie8riM1C(4~n{wdvQUMO2ln>hu)V21mHZImStPE2JBqsPk9K;bOGm zKwWY`xFc0l*`uTg_l#9aPqO?L(_3085;kwyGS1F6{JidKIOTF0bjU|mWpOg566AWh zqB>)56x({SG7iY5-Z&dr!%Kz60quxzCO5=%!)=ut?vck)ZjePBFGCxR8%aR!FIgPk zT)Uz?t`fw*rjjamab}7et+Kd1{pJe85RD;weuVLtrV*UnvcQ(!+`0?fFZ;}IaZdfG z`hIVJHcwngZmB2k!P0|$4>mABTraR3QVti(MV!T*YsHA3rtxsYp)ol(|LTN_WGcjl zcZ=@~;ucSLcz-MCrXn&Z$O+1QQF~lU;~3)NtSVOE?bGTm?o_LkSQp1^gPw@))oo6t z=#UuW2Fga7Nx;F5(JX7ruhF~gW`m#4N&>0_AZPB8LL*M7#x)yom*^I|d|kFEO3%6a zuAXOWSWHs0CBH8GTtPN(Z^X8fYRZ8Ou5USqc#^B2JuZ+q-l8VFu?kpDKqHyeu;4;c zK#U32@@gr}X^RrON@^%l3PWNW;I`XL%0{AdBo3$x$-(GA;$Gr7O|XoZ-VnQ#L~U>y z_bOaV_3+j+!#KT86A;eB4fTl$Pp2BfEv2Rr$2lKV**#+SxMGpq3a%0=k?WY{cOolN z+8P#9HTE_GkkiwHYl?DlS`}>L+c#@Ir~dTtr=RZsoh%ON200b2g6cg>GJFNod=n>W z&=AA_-+4xy#clW{)p1U$nf14FN;Mu9bT8a&&f@kG9@nz7g5vsx!Z3ysO!$6<#*M@F zD_Vor{aOOwmT@Acir63*8(@jU4M_eUy})*;sTUijC4wJy)Z=4a!Epg6Z&y(^BCo67 za83!8D5oID>=xg?BN-DGcd3f%7v06#J9Wnq!{GL1HzsZq@&cD{fQ)=dZK6a5|Hb=X{2u@CF=g| zOC-O6s?CDkZ`4aUZ0lirrnPE%M}o3~VjJHzYKdV^;$LFtL*?%-eAlr|DPmlBf}?}2 zA{7?5z$?)&57ZZjtX3*FJWpuctDS_!efW?iRP{EEKpYY7#37CZZb_hS(ohaa_Va$y zb-u6Bd_T|yE~nCJhhwUsTc?6>c1G0zu4mDPtqkXk#3_&C2JVeYt6*^)RGq~U;iC0- zLiY#u=jyAM7sLoa+Td6(n4%cvD zO1*;Rm-shbA zbswssKX2Pz6=7iv;?ujP)r# zZ`rCVOQgPI{`QG^q@ZxRLyP`5NBJPNl+Ayc4E-`gz)3?(u|HR$g|KvHi=Y2dC{S?D zzBo5-M36_iTWY)drOxCI$$^!$l3gXGlWy2mB%KUT4yb1atE(EA{OnfW%pGMDX0%sv z>B8KEb2mt#iZ$zB%7v2hOl&BR0KY**a!Y+9?XtQ34ubX^u+^!Xz}x4afD-qbZA#zL zU+dlqhq%~xJpAc&ij5~ zZP$r22RK*s$_C||!lX2XBX{{o)nr-6fg5W2NRS8BUs}balE1>QYfjEY>zpBOuZofN zamCgHNtGV#{Q6NvA|h>i0&!{YyZ(j#dLFP)RwyBLE68?&bLaa>TwLnqDBRIR1Uh!NmU@} zy_e8Gj~#4TA9gb?KyR&$YF%1s);)y~`QOV+eIAU4D(Qnaa+K_k%gekO9B_Oh>eG<% zz|Sw&QzWw?(AudJ%cemUz{P)=xxq1?vN?+Y00~%-)}408m_m4Stna2n!&o&x*;BJ& z6@70NSy;YMoE8aK2YXSp@`Z`ymxl|8jnvHr2SM4qRs*Mj2nTPM`DWd1)H4`;-8XkS zaW0CIy#seujAU!CBgU$&*BCM?3p+ydO1aerpm4d3A3)oLEsYPmi60bw22%RB*(e^5 zk1LPL=u#g9nhks`!W>bNb8BKVaZBTSjiY)nAF#ub)0NI=!!?R&0A?I5DE_hnS&S53fz>*L%I*X|{u8YVTcuxx#7N`UGn;315h z{Fm&RWszCHriD^%{B;R_^oe5W`?qxbFG6gEC2I5srYDhU_G!3!krj_Pz$YG%zOG6< z)o2nZYW66{!q=#3417^M+BE$?N;47S~Nf-(N>(xXrvLj7c^@$$R|dvCoKg zXht^)2~!`dNwSuZ1ZBkV?KUU)jXN*r&1UQlEvFb16)~WrORyC5S=$)%gL>gDGP9Mg zeYdW_{8uhV3Ntb689p||(@g)(EY*L85EpGv{57s&>Lm-P_D5w07InDD#vR4!p)V=I zqU8f2$H9u?izU4=smpXBC&5QhyImkRthFR}5BrYd1?}L~$K1;8FN?WNyJppMRa(e~ z9DcC^{m%V)iVB(y$LMw~1m}CK+wC(Bj3_=zyxhWpj&BC!hrN++OaX!o1HX<#JhY}J zOA}L+I|jecamvCb03lvcY%W0U)64jE7cXJFNrgr1%085V#1gX$MA$3niuT}dt{8d% z(U0Ev%0>Na$w0f-0OHL4=B6}y6%Sq4#DLr?zz0y?d6dZJnl%{AIsUHLf9cdLBQ|AG z<#lsg{;-hER`1EB-3Q)xvsJRw4nJe37H1FBJ8tp9&D%lt<;2Y;H#!|0mdu8`8ZgMy zN58^v_1(nzese~da*xO#afGv*>6pMd^Thse+-W|qbLOkNJy0DYosAT1LKjjPKAkCP z2ihL>r(d6x?qe-fXh`A%PgtaI)L0sG>OV6ISbMhN^S88W$`Ef09pF+3_k!}jjQZ2W zCDnio{U(NEofRGu=l8~TUsE~Ydz-|cWt-y+>G@~3UVt?v+*ep6q6wq^IoI_DLGZ*% zm?P_e^UQbH|Hdk!kwDXecw`XcVg+X9{U9!K1^EydCC*Pln^6AEPH7yWfWd2zCSrEe z*S-CII?>|3>+gv3Q25p1N8%DvwFf+IvA@qzQF+9lo-6P*TkD!DZf0w~Y+#1`=96RD zDed(a#`|mux~PRuO2%tVk#9o(8vGpWWQY&c(mzi(bjF|yr$3lMJu|YH`&J)>qaK}Y z%}Z0{H=yEj=eGyL|9*AmF;(LVG_)u$Kul!gL6!ms6@PvaReRa2s_m1(HsXu7JLkum zJET9lGrTAO$7UQ*xD9pjzJi#{aV71J*8Qw~g9D}t4EGH{9o5b)EZ6Ut1P8c`T7G@D zaN&*3lzw5YTqpLEv?tZG8J_n|PnbM($s7aE>^C=$M}(r$r{wsXxo-gsj_`WaP*NwW zCub&_vq*^QV#i;yqK}>*=-|0iNxRndLERW$#+;7!P8IYH_Rt7*J8|i0mVCJ}H-A39 z6VMn>wIa{dVNKXV#HRMa(=n|)iqc;Z5Qvhd1ABf5g3VKre){-LXf_QoHKPPO#85f}bVCBMJ6KI)u3y`X~i0m_%q8$p5QL zt@Z;Qe}4ASgZmsF6hT!dyA_NAwgAp8_LV$A-G@7df9ObUQzOQB>o$v^BYNc*po-4-`X~2mbmB>EP|z8UITbl36bXl4uvsR_{Y+7r##R zN6Ng8aBU=5ata&Rd^~!j_j(d!;QAGBI=}na9y!_alieXIQ_!ifm4M}v`BA!F z;ea`YvZMVxd=&9koGTDjGJd1a0n-w;XPm_Aav`4%cniH=m^O>L`-32MTcsf=KivFx zH28Jn!)G}XZ!{v{6r{bN>zm~CJ<6UI)TCGc%rYTep8U=OpyIV0|T8QFoLA(gAG?&-Mg_-dl0rRD! zep!cWv72*VDZb7D`~hRTe69aZgm}Il?#R_#AE{to(}MVEG{9e~S4lKBx?4Lc;La}S z$mKcBS6AD~roxJk3uPZpQbC%}+s}x&VnH_pR_^dR89aB1E@Zw(>irI|Cnj@o z+s<=`Xb^;tVoRU>++wv8|BbRW?nyecOnFVpz$agk_>1Y^CFNw-wsJu#LL{AJVM}-0>K&f}Q zntzf+IVXj$)Qz~Zw5hI06hBN~!j%kPSa*l6J^1wAqRrLT>?5qzZWHu8SoYk5&==-< z91j^VBmTO~*Sc!OQvx) zJkcsvw-*zvf2H#Ycn|KStC}Pzp~OQ7yrnUbvM659yBj!)3y(QBdYJKG3lP*28TlOU zl2Hl~?M!l}5o+6Xwt!kpbAsof!1{;e*-hwP@i2|1Z9yDkytk2AEw9T(kwo+-8aAZC zT}GszW`_y2f&l5OMG_Uc5A6&-nuVf_*!tb;GzCv@<+IFkzx#To$q%WL^-+2`o#FH7 z@`8E5l`bgJsn%+?p(zWWMx_jp3QBr;U<~+igB4tb#nnPD1N4{ci+?MOQS!I~EnM5{ z#g8F9d}Ln(+{4x|4ukW?q8d;x|5Z#OA0@YIM&yl=%w{?mI!OOY2IrKotR83j+`7ec zx6tv-wsVywI_0%;vGC8jr{i9D2mb|N*%r5F)bQ6bZxp<0-9Dr2V#$5Hc7cmn4lyed zl4mUOT!4*w`ch9Kr7M#$*~>yDHaEP!d}o{=g#ZFFw2b?F9rvL`)v6bz^#kh49QSPF z&~B>4c6=3YRI&o;WLxx`0U>|KV6MYWRQ0z;_HiW34WCh!acm_ps>QGFzvH+~^wwwh zVLXbenkwR!n_Ood_h+;|gO%!X<~h#PrFNu%rbvG9dDq9~(Wg;bmFFT2&g8#8nm3ua zL)gbO-3aLWpQ_E8PO59bpBc;~&Ny$vT=)Ai``8&}Mt$F3(h9B?>$!2inIT?Q-Trte zB0rTga>>h-f%S(8D#w&}GmKqj%zE?}7I+7)^5tEM1aTYN#C51d7;0qZ@;Smiv*UxU zfn^q7!+~cLfs3E8E`kZ?&*67(mRgzUKN01EU#~gdf}=NH)ks#z#~1>l%rS4iN{mmD zFTVeehSIIcqupH>$1S3w<3@q(pi^cDvxRIb+dAOl5u{kO6lM}8{8`WYhx%t%bv|$} z5c6KIyVioimf${$7W@C~uJ3Z<6sCnwyWtjFzxFNu=5VsXO4-OrAA;5?#|PJ;PJzgJ zE8KKrwV!(Va%B(pma#=+{;+8AMPE*iLvtLrKVcLu{`J^%J#p=q%tvkk@Z!q22Amkg zuzyEiQz5gYVS?%5)5SfNRYOLdFf%71J#8$@Jg&8jPFt0zzH$&8J<>pqlM&p$)JGoi zJgRy(|%rb%@O7y9o#kIAg+WKtHG|Jw3A$+jI8kPforcCf&PJ!(B0L=qb? zTcqcx9=ZVDCr>C_kGJ|{H8?lK>A>qy%Mty89Xcs}WN}ZzF$Bi&ml&AeKacgEr?M=a zggrS|I2fcE^`c3_}3iA;~Cu8Um~Lc*dw&L(5W_EkB_+ zTZ)F>36_l_aC3qfUt;c1!w=IyXP2j)8_5;q%=qhrbnEG8CmqP;3gyNFpn1QwcBlVo zTQch9dNeeN?OG$Zl($>>Pj4h&;(nn;IIE^c(iCUn!&g zX*FEHF16(7XO!2~qA|u@bE8jf9Xt=q>e~>B?bhi(+V&a5Hs|ATi!{~mVw^Qb#wKuY z+=7iju^F;Dgl*wjS(~8$r;_Uy>!~A6={~EO#}^fn>r2yG%tHn&VSWAmxu3yH?n~)M ztc9${SQdZV*8S=b-nZ} zil2&krIbgkXQ}s8eZy84On)Jx0))F!MJs2~EMoDXFYY@E)DbV36L}$S9v)Ucr4A8U zWTiVD4T;QeJPey`Xx+HPumn*{Z4(mSNBZ5hZ5tnS^G&p1Vui4W&^H-_Xgf$Ol$#r1 zQ2l_#l+K1lNP>hIEPZFu>SGgv7W@ul7&7rz_=z5~{|Kv=ihk2-U3=a{qDPjzBwoE< zlNFym6Y^(HR-xVdR-OSSQ=%IGZ|FrPy#HJT4q%L@^?$NfCRmYM0kAMBjj*6Tje)z! zoMT>HMllV$4z8fCIDp?z<9DzK3#+;%Tu2WXXQ*>7S%=bk3Vd-ccWwe1ENw>-A^vmr ziCT93!`pAX?4FUkNTm`b7}flT{di;I-L zdjBdst97!Vs$>;B*^qJL?p53(MUp=+ryEt<>5rTl;BYt={pCjfd~deyUW8WRxg)pm zowB~8kaS{YkbB(22eI)>9BvYKa{^qar_6}hQ#I#^jxp}wB<8sM%x^VKR+-VzCJ5fZP zyyNh0rXIPJ@N57f#8Gg5lsqfey&Fb-@o0r=li8mLmdD7(c!r}Cucq_(G`pX4u5H5@r6-%m6f#>IbUWH*D4^q^&i)4>|~f^Tx__M z9xf@Y=mlU3k4~SR5ivCFohhq+78$VX6+V0m=-5h*MII-H+je31|8&v9FCLBl;?7O_ zi)w3A)wG?AHI*zpK!zAB`~*KsZPfC|&MVATUl zW9m{jTR?_7^*F#ltoc&%s6!mUj~C?)NoSVqCw%~}X4de5i6Meqwj=W{?A2aw-U+p+ z2u#V*p|}1*MhGCYhDheN=t|5Qjz4+(KFIsaBIaSRun{xy1F)RDBH+2|L88kfDGEc` z9Q$vD0k(?aiD?fmc&Dt1efwa;-X0|Zm7d)RAOB9nup+`T6iEK=d&vGgJ_RE~$){7E zY3M;aYSs^}B!E$PxM8bD#Emmk@V-1?C_4&<(O-=2?tE$67ZI~WyE=Ff>dN}P_U_B$ ze4g2F)LA$q{2i;J^Mqpdx%5~6l2(?D3;FUXiBxNhicf3d(SoBjMMHq7wZ|vXBN2Z~ znzP62i(N>Yzi}`2sV{O@;7OsikrrwAq@3CXGQ+&*-28;^!g10FcR2B1*vrLcXgV{8 z46W*_k6cwfURq>pvWDwn8!gxPmN^;Hr$5K>OwfDDB3dIpZ)$`5FU4&){_hYdqaj0f z4<#dS9*T8uDY3|DGX%`-7C#LQe?;N#Z*v5`ocTlwdkLhs<_il?sKQ_416!HrYDbVX z#O{?#+%USWeOZig-cfmqU7~^q;=|1he}?9vn($YLnUgBIzVZYe;Y>_4#Wm1jCR{O1-{p!#DAIO-4_8oegx`p;*{LXYe*yVzx+|EBDvNzE zNMhC)&ciHIAsL^7_D2s_qvF$aAg^$YotNmjKJZz4GY^lOy&bV)iRpPYLOTV}v;J;O zy-IpMV#mLzuwHqDZN`R=M>mG$EOHpD>)ZGfW6bhr>1ns^>%=r6ix9i1h^wIIW&;S! z5rqnO)Fabq#JD99>N6MQ$J3s;<6d`!%xQqL?pZWHaRBKlLr)MnuFSaW|Wm>Ie+JykKDrRj~)Q^vo|pY+ibh9l$}MD zf__EzOL;;7y7`GlMG|FQfpnR$+AnMZ1Dy&`9k2%EHL69v{KTujSBVG<(if+>IPKb{ zNP8fQ$9jpGDp{>Hi4o1(pCC5t0W*~sX9`|D*w zDykC|^AGmdMM{?f6W?O|*F^;e8Ai-OM>mP{3^cF@kKzLT3TO}3i_5KG_c+pNI)^ic zu+N)&z$xeIC(KkLjCTI(;K2{0hBUJoJv9HTH!=I`B1PX6?i7q*<#E9Kc#ZRu9B!5? zW~&2P4vc&4(@tfPtU@sz@pt5BNPU}dOUc;hFLN*eIoBdhz()@#hR@u(#PuyGKW#Q z`M&<`&RZ(f&G%ttV<=dAKLEuUmizFbkir@XAG>y0J8GoR1gz9@Yx_jjhL*zKa_2Dv z9!iu5l_VG1P-zw-^T^%q$280O=D(dg@A+ZhmdWx6mq|ESt!66=aAM9f2Oasc6S=Bw z*h!NGYpvKJan^erW)AnsTdjO>EKwYW-iNVXY)n}7vp&~Qj&E=8?OOe6rY*bqv)Vly z^X|@KYbI($NLL+`nu0o7Wop$Rg6ZGAvU#m&(`=n<(EkcEupR$!j1#8?OTy&~N&l8X zD8^G%D#-I5deocQY}dMwck@b(7a_B1pPuh2HqLN{BRMebD%H|2Jn5s9rJ5tAi;uip z?gI8}Bck8yx>9c6CHUV|lr`r$ZM%!A0mIB7=-IUX6AC|t<~55Io{?W9EJc@JEDf7) zvakJAIqgXwtOGt(=8&IP$$dYMGfv5oXiW=H{ng<+KruV>6Y(HK_z$1kz|hQ!<2`RU zVbOl3YiG;i9Lso_H#dfLioi+b3JepcSh&_?C@g;ev2ERJaTr5aW@o!Oza-1~J8XphEb0EG98YhtKsNHyPPcp9B_5S|jRP`J37!q=b0raCrG}|EmNx76J9CzMRbS*FszjTNDvPIn$}C)i1j>u0 zs}v`pjjER!_{aQpSH`p#i#)1SkAPf*`ZA{dy}FYrnflfDeh@v=PhO zSvHc#yY2csawdj9Fx|I~X}j}uSipItJ>Nr>)~Oqq@qk8SVc{;yI1y30U&=yPLd{pT zzSy1(^7VZZVx|in&+^d4{<%7*`j7L#pV45|Lk}F$;73S6QJ2usq%VDb*M$`m>-jCl z;YJX}oVuo1u79w>viX;ve3{Rt&sy+ru8o0tjI$6(p*HG~A<_y%l(OGvf>9%EVJMSZ z;B#~I5k>lREDN`&HOvTo z<6;_K45t;m4bEsc(?IycO$&z;1K+R^&yv3=5E5}y;A@HK|Hz$)nm8B6OOiMjRL7Y5 znVXuKnmh^F#ak0I?wHIs{X070p#VM}J9KCDu$mP*eY^oN0zMdaWnb+Uy8 zO>-PbjaA&0MWQxwuZcw;SQzZkS6jEVBmgNg?mSl7RqqljZxK*aQ3p(LXliiQUOu7jcII#mDEVxypYOcy zBS<|w5E`^6e;8c-U(G?Ekh)o)@2-5Ej1+yOe$!@_5ujBoeHzmB8Ivkbn4~=Vum;50 z6gN%o3cHO}5;GgnxmDUdOy2^rpTcqKLTF|rF4nMWwAaBC_x#nv`x;g&5`2u*JeYY_pT3EjyRdoC-LU%g?diao@Wh;PBM-2~*IIy~KGe^%m*hV&pWh&U`sY>&>4uSu?lY3z?03`t?sExgff)kauJ|Yn zcRr$m4$}q{>F6&&9J*9)L@-I)`vpBJOX0yxXpBw; zJfVwrX)doP4U(_$U}9+T+~2tcJ-csC^HUaWTNM*hsA2Pm148L?iy(SVx3gLo(n%kF zI_32_7!u;dDi2QDN}0U+MM2i)I|PZl&36-{;pcu|%h-~Df8dJ_K6JU^lt@!31q}%$ z{N~Y&&rfR#d6F!Mfc_#nbO6uYTmV^wc-ZAFETT)L3wo=Fw)?d;xK06ao6tC8r3F8d z6<^U~0?C!}aX5tUu=JA)NdFL%(NiM&7$}aQ{DCaz z?H5fo(Nx5l`b>dPO)p1j@19$@0S9kpFOve4sp<~deBDr^OaYq?CH7ooeGnL#b;BQA(A zXuJA*vO?uzjM!fHR1wen8aZ4mejCadXISR-b8<0>;I9SwKJ;Xm%ydgQdaCl?zQMQ67cWO3~Y64Ena zsr|S_bo`^szm!TSvQ%J>VJLJOStj53lkZ^f=EBe6DBG7=FJapSrSp;hsM+7~iF58| zy$M(jv%PIs@hWyQ5?FdSss_$~%e!{8VvPCA`?KIXGPL)WIf|M7Nwf>@O9Nw=XCg)9 zjx{g8r+l*}+xes+$1C11*#0>mD%E7x;8LYY!p&Hl4z0!Ao3~#Fe_9M$BUyDS+icgoLgE^59>_6Xa9NE^QiQ5AaM-@%U7Vq(Vj( z!0>yf#mSNd2WZ64OqMowe|~VtorM$peGzf!IF*K(nvD35}b;E!M-HCMLj|yui?IzgDum?Y zmrws5_5)ZB=(yd@dc6J;=do>1PcxSw)*)V}Ug9I)J6K75QiAr4@4?hQ{_^sZ^ zYqv%bsgu%mVjC+JMGa@2$1_6bX$HKWXt|8EAnic_pb@XZt)!P=BNE9XUGC9-lHsw1H`j5ChCC!rFhj7I5^GC*q}XdKq38wy3I#}_ zZ1xvHwDBA9*VJ`Tsc;cW|VX(XI{GbIeHKhKBmsqRdjK2{wsjZ%M zZKr!PUurh|gaLuFk7QYP#3)wtTZqix$De|6@&OHdkQz=>6@ePPh*>t0I5;~w13f|V z$%de*9lU_MuQ%L5pberLAA2I0r}{Iu^;|Kz1-6=>ZbK#+MBAAL!)4Rff;H;BXI72p zycu?4$TqQ@k%ojn)u*-nRRTjQaCwzY;bY&KjWrW}(3`F)g}$52V%A5Q*O}&Y^yO-; z0NQ|a@0LRJ7JRysQ7PtW1g?Ki)M-ZtIJl2vRu z6RN|>9kN@4x#nBvEKD?B1%5(&{)vUqTxcMs$K$)|YIJ4oDA%2%wQ zQgVVdw%>gau9jI{NOtpVY)|Nu_E6)Wr7nh@Y^mwE@8^iRRU8`CC5KmOlt&kck?okb;?c#^rH9?r_Pnrvye<|Sw?M<6QvK|ZLE{~{17^S}LW`y6H^4K5 z4(uB z><7Iu$;=S#{*B<946Tw*OYpyY#Iq=kc{pqX6Js)?$eeS$X ziRt8{D=p`9nQKh1JcgyyFZDJ-sw`!xcv)XjFUmx^OC`YgKqpYD1|1JYi_`Fwcs1M4 zLPyI%R=@dZe^p71bGn*os9r`hDs5A8ti*qV?;@kOb**RqR%E&9Gu$9UP`Vz-3sWz1 z``AIn#h;z20mbmz zy;VEP9pZx5tn88;7?*7r&Ml?_oCG*CyglE2OTF5k4}O_QR+nIenRJ$M7w=b5B86?~`xachtsT{jsplHNi7#xKjOGvC zj7RO)G@5PwX6 z@kH#e?}a?GIW8SSo)rrX;5RhNy9s=pX3D(hjpnKO)9?@l4Ivzyee_~;B09*HxMi_j zzC3nqqLf|i#_CD$%*zehDW6bS@TCUW1rWm+73LXv#2_G>Jka@sK8oWmWj_AKf6b12 zPCE4`_6*cmwMWn9Vvbue{C%h$NPD#38*~(Z^e?ns(Kxqi#{S@+-E795RD!GkuFHpW zv5#zsmxQ&-H69m>8pyfcj)jxYwo$XiW_l^dL*?RZ)X(h=T<_*_nr83;*5eJvO3=R4 z@xgDWK^f@RATO3o>KJ~3!35jw$j&nl z6W6d^CD1hZeoYXJSv6hza&18Q_?y^)I>{vK1JvjCtBW2Q$W%JQi1@`Oehu#M4E5U7 zuGHLBrDj;oazwYH$5(3&{c)NTVD~Mud@=ia#29|Vq+nOad4sR-oSn0N|3F-T zG+Zf7?wiWEsp70rb~;g}dqL)_cb^IZH;U((8KY(@0s+gdyN50n4LoKHue zdT)iDDVT=InmhUXG&0J_DDExdseEM=)##0QVv%v=-qnWnP{(%WtGtU|URb29ZWFtc ztZ->JS(Nbds>FvUM!xQlFfV!)*Oqf3WSG6#ILlGj+q{9QOxF#-Hj63n6ISC@*~6JP zedE-^r{~3C=V`sx(lUu>5}yNnS>Dl*CsawMSn?+b&k$GnviSV8Vh9qZHSncdPqt6~ z?^Wy5==*xmx@c}lYaC&e;chLJ9HdM`J~(NO$=}Xga}#C-ds}{nTkhTbA~-R_$Le}9 zM2TX4z}4A@AIZSNydz{Iz!|@I^K%@TCy^=9&?g$b(Xohb;h2BfMT4E8mETvK9ZyHuCagOrp!R{3h7`jUj~D>1i)>OT^=M1axKfbeSvu?-MNVrMxS&kBeDmYbGi$H0RCaGQ2K2ihMYr*sMYK56EQ5 z{-{gi@qO~Rr9KEP^X_M4^4ObdSS`)2dnB~1g4hysAF=ALR^6?Evfjr3xkS?=_6SJKlA=Q|T=RRupIhEI<>}$h83x&t zjAzK|%CGYuq=b zETvhOK>%3-*et2U5-S2qRJTAs2lw|ldd}G3Ydqn&|334SWvoAf5IYQAVU=(DOyX2%bVfH~b(Cf!ZFmvUcGx}M4hWScOk-XKwf~wDM z6M>Uog>V^^*Y9yE-ARj#A0KaKyIOuR%lRhS{xD$sHN_1H?a3CUsKn7d9ZNUD%Fd4l z$eXDTWtm@C?<=M5t& z$?4-8gWZGoX^d>$g*1(GQpF}%uq;br&cSwo$-r5FEuNjrzhh_g^Ue`zJQ$*8=OKU8 zEU_wDY#YLD-)cK9;>9f6NU{(?YB_I0`a(u%(!t1BiEo4@aD3F*(V`eX(L?#tCUqfG zyR>gB?Bj#qM81ZAmb{EXd@S3GzdNf$ytqpl6fD@!Oa1`k;DR28@*hu7!QF#cg=KLf z`gtRjbBD$|B=b>56$meOf0{Gbf^ziP(+Z?0(W-H7ytdv=s-4`I`-eJBk~Ztm}62R*)G$N z?d;JJTI_I@!7Z)A{X=$`;0J3r6GVxdmKguKZ&(AFRI;35~~Ow_Y7kzElczlc;d}D2yCa$m*_vDgwa`H4k&o6 z8#SCB^XYUx*)_3ZuF8>O?=It13NjFLYh>>J=JR{gwsqEjSe)cy#Pbv#vvV|{3Ko{} zrMcjgd(*Ep`z@XCBZ#OghShCk3*0QA!`ITr^Y_a(i zuL*ZuFg7XpT9PdMz zpDhL~*{-&wEB^?~)kS}iH-p{XRroU7n>8puPsj5BEY)vJ)W`PwJJJd!l$(ZI=n{1y z;NOh0caY|n2Fcd3o z(x>zVx7f6!wBA&-O&vXj;;uXJlxymoGllBA!W!@Hx_u9BGRjgA>1dN5(FH+Cf;TnR z7|R#RA>-}o&bphD)9NObAJ09MJ9G^fysM|Gpf}Fu`(T${wUv9b%!GmSJUDdDvE%ho z`-(JI0*~~wB*&lr>jbZ+*e<|PEOBa8@->D3fKtNiyR%^1iYuBrjLLHevucXitf4=_ zIQ~JHNd)f4%OqP~DD-4Fk{cSvxZ8$K?`SyzJ+qEo~xk?v*^h0vCvmIX1i10<>I z&Nh@oGjnbLT^*r}RBmbUMAG42;H#Sx-S0+HcLw^3>JkSWIMMgNlIzkbsiNv`-1&v( zfIurxWE{m;enVN$|B$KZYArpwc9eJL(}iuYUZN?X4vcywXx9B^e8^UB86aTDh!KPvH#og`vrMhB3Nx@8eE$gFrM(_+3rgp|0R}|i4GsNzu)qZXh*>V6NYmi@ZwUEiItZlo|Woh z(+B$C#rr9tH_CP_M%Fn8_WA5YdzULcrm|NMzpLyP1)8ksKR|*uUVdZ2u`p_Yp{H(v z^B@4lM})12ARg#qJc;WLe?fu2R1#m7_G*(0NnPSm3>E^|#@l=d7vZgN;D}%@U~k|# zKsv>i4ed%WVBzMN0+G04a~36x=##RUAv|Ag%WHLD5jA(JViBEm6}|=ZG+ni-rEqLh zKryFo6zCzX4g4^MFP}81gZGW75hjV?n!)eF?=!ce`Mrr|UBj#<-G&Px)E0L+6Em$! z;#{QIW>HGg9{SS$Frj#t*;u8FyEbBy7By);rnLJ4TSAwn7SJo;xinQDD~uPnuKO)D z4tbUynXr|`AvT%^oSr>`f<0B(JdDCmWStmjX?$JBApk#vUsJ@e<%Vkf;ODYpt=*5I zirO366YBXV7h7*gVJ}81cAkR;pXoM33%1Ab)iJz3x<8RajF)?ilMN~VyEFFM5?h+t z(?;I9IfXwj#C2-Pwy$*`d@^30OM^!_QY*r@1ngKw-=6iJ3Q4ma{U-)^k@c)JSe-28 z$xf$|;_=|76`X>P&`;3I$hCpW(Vqp6o83mAy&SKcLQtqBv658c7$LvqAok7a9fX~do(`)o%@2YD!N$&F%03cqFDn9*+zTKq_Co!p9d zqo$v)4&MLEbmEI*QI5serGs}_5{gA)+%^Es}f3pDZok)^qNxM=wd%a|H z*a3I80dh<|bpyTfz>`dXmUM^ojX)%$o19tSl!gX$zbgB*G+V2N;?d0l_I(*UCU*cjd(yJT4qBUsoCU{q5$K*`M1 z1&_Kg-59%*4;6py1}+rIHJul+F#D_x6bP*FcUch0nAJYyF=sv5Vc|`PU&;m!brR8zZ=q8R3*GcO zM??~P$BkJ&OC$vjwgHK8rb$Ky*_v5$FMT3U!V@9B_a16!eXu_+s-?XwR@un+LJ7w=gb~F|2}fLJ9J)2v5Gc51tRc>icy9Uqof0^U`X##YSn`;4w|~k zS-C7z5Cz9vlGzce<0|>c5Bih(ohiJ-fE!|qS=IzAMXGJuuKh)gWEMH7r_$srAi#zAe56=8x^Oh(zK^ShGNb#WI(B>= zOl^V@95#?gDk7tmqJ)E7f>Pz$N|) zmG&L`7XAK3xeo}xRW7NySCV4VEmz6y{kSQ3C;|3}x^_qzWpV9q62}~19wn55$|LaT zri*woQM>@89UM_Eh21>lyV3HkqkNUoP5BAEel2J;uQQ!_`U&>)j|!Muys3dRz`z?* zwY9LOmOvH61RcH3fyvExIg8QGjjbulKl)pP6f>^*P#5s71~}$Vl{Kchg8BqZA=)j; z&`hVcX*;V)*EsD@<)O~IY<65WvbAGGp$l7+o}2H2QD)aBel zIm{>n@Md>$Ir@|PL+363xA{{j;J+{Fo?`%sJk$%5Xy)mINSWAybS1iTE(e^{?Ttse z{e!jZh4KGUbQXS1y=@prkx+pjA-T~dEj>_58fh3ICEamIkq+sQk{Z&n0YgAwgwhQY zfq@`78YGpFeE0qd=kqz|ex7q(-}|~S-0|#3w%vPleW7`26xB)WWU(7&`W#O8Gc>2w z##GQQD&{nmo}}bjX^r@c2J!uYrW(E;D^gk4W~WpLd%%*&*tRsx<7k1@Pn~_T6T?Ox zI|-@vaLS|*^LsIZO-an`Ye1#8W__-QnwY<;HtlPQ3)mx8R|)R}biwhV^$ZM;LFfW$ zd-8*+$BsuZ7ZY#_+)GQK=G=^C@G%O?N8c*p{+DeQ_A62qO9;K;liLg4O+(o~O6a`+ zIx3_U*d=DnYv2z&$JT1_E(#4nst>n1GHq#8OVTW#EVkYXJO;?(&3uIrQos|ZJpC|^ zotEYG+;$G+@;vC#O!Rj;sCTfLX|}aYTB3{QZruhjfIeh{y@2FaEy;A}hav6^V58}O zJ~eKAsg(7cM2pTd3s6;_eBtylu}Dx!$@%_MKOYVZ#qWsMZVF(%Di~s`i*vgGDy@ia zB>uTqdCJfSmp`G_1l$%s$eFnLwNE?Cl5u}gAC*&u6Wd3Kpudg!HOGF;mev7pl&5i{ zPB7Vr0VPe0omb&cw4JLnoE)kygYAi9Qff(n#{6MO_luKC^l?s6T5;PK)%ZSs zC7ZA-c0;77Hf1cliPyYwKa!pg%xWH`@GpMlLq2u3`npWqdb-L@vAZ2 z)GvX~fJ5JpP-ySw@5OsmiXXkX<|XH)wx2V4AH+9}6RUXd24yAGeHp&)pR65D60Pz+ zmRV#xa^GG|I9{5<6r16PRbQ9(l1rU&UYc6tQHbjk3v2`lPB)5o)eNQbg|Oxn%l=Szx@9wELD1`MN7XMa@DEnydN zT+kG{KzSS{LnaA?I4V7h9G@tmQ=qI=-xMpmpK4Z~ARt?Sd`D||-nWAN^%wRIJn#>R zV0iNw7=@Hf!V=JgPm;~Q1@ENSU4M0El3VPcHyu_;VdQ2bWGvR!yE?vPQT`+zzL+A(^ zY>shLV5AkyVvpmS_%tq8V^bWdw($h}7KUk-Gwp>3KxcpxI3SA7hRMcq?t4Eo-A<>6 zNsg?FWvDg}sNAv#zvg@%dhaWVoljA8n)a};kl%D_HFU!}*y}SK$h-~8BO~+Gqo%AJ zBz!2*LafT~;ikcg*1Nq`@69w3)8~vc*r}tLgxOzxjZUU}{7tF`{nKvDwj?p10+(p9 ziS90bwK@Ip9_k_Ja8zOQWajg6<)o0mHo*y()9gm#ny%0!w-4e$GE6$5gu+=5qb}aW z*ylXV(uh*jO7OZ>z&s86ua&@@A%0FnJj$-j7=QNR@#+;96wdS)hA?j{Pe&O^Nq%`2 zb`@7l#>CvS7rP}lZPQJpJuvkCG1%dN&^o~~x-?XWEp|;DjJMDWg*$w{Z#7ukw3|{b zn7H(}$^WlTPV(gcVyt>X;-H@HIW?}9vuYp2Y=iPc44{><<< z2#aOoK<{~M|Nfm6ovdc1*vZXx+^`-!O26+puskr_pOJQ;t}Vs^{x7}h3bN1n#IGQ$ zH*kYI4$j%$_{RYKXCMM$bj{CQ;7HmR+u&5HN2^c3KZwUSy%gNU+~zk(WPiHa)6o6`^y@b~cdNB*1%|s2Io25X&-4 zPEa5gia+(5~nr^&8P*=32lIxxWkqF`pQdcbInx~%JZ}b z*3Cn(1nyXxBH$V?_<1TvO8T?<`g@ineE@s1%2Sz$4lx^mfU;kvOaRvGsu#8*?h*NI z(_@9uet*-uRXBSn`FpOpW+D(Xq>wF22il(ZqMyWm=v!=6N+IxAJ|Rc1fNWuO;40T6 zdV#fd#HFT>N?DkzHUjjhLZxZ^5=2u#11&IYBuMTPV#pp~qnfrTo>lnv#$II5AS z*!hUSU%N)Zb();nP3t3D$&Gt_{RKqFa#UPA_PzC0G&vkB?+NuDe)0#Tkq-6de#zg- zUM;-N@Hu&T3VS!Y<2}@5$~)))C0g|!V-RM}5C@!d6JAZ{Cp;Z=#Jp7hPF7xJQ`T-t zQTb+eA19ab>P7U{e1&_5^u7o>8g#4q_xtG-3O;L)PIrDKji23%)lB6LGSGW6%PpMa zc(w2fQDDrPV%I$Z5k>0rR%`&oUSfzyQ|}(dR_h+rm4xKO3}rF1TQ#7p5qT=XD4SX8RHDo=iF$C->FO+kF0Aekiam(PrwQUXY9 zwWV>n(!&p7l^^Tev)Eu-Xd|Ou8MP7VEO%^N%@9nfiZ|$~)Et`T^{1mM+=pvBd`w+=xp~O_qx9R3^;=nPFml{*p=(&dcQ6TXeqL6>RrkgO%z5Me(wy4^ zt8uE(`7JOJ59z{Ez__JuGG zRLFT}+-|=f#FBrbx~ z!)EHQ_uc)}GO&3~Yww->5^JYh{(&N`|6bzr$j(Ot1=-Iu&HGkyYq?Jf8LGVZ?whu> z5t6UYL*6|2W)t31@LTTRQE~R;K?1Sq4{eCYC|9oK?2ammXwa8mkQd@=gGGbbi=uhH z1Pa4jk-k+NJY2=_w%wXV`y=pr^(aY)tZzfM-A)1s$n4i@RL){WkBT!VAXL1K#3;uZ zc0Tt<5_Dz`jEfy|M0jvbYL%+9bKH)@AqVOOp;{{f4GDa6W(a+VJmfQy7FIFSLD1Sk z1K;!}t6h67lzEeDV}M{D%T|(V=(D`$%u43RjKj~EcSQubX9%^Vwme`kuH>Ob8YnQf z+7Jnt2*#la*#O>DpO0P`kGh-+gla6AjC9VMv8fy>?>w+MWmR7k?+Q2#RWIBPQE8p+ z!fsX~+Yl7oKVgfcly0&jxc#E9=OJ+bt?t=qDMAs8eiZ9-<9Gn$^7j zSQ`22O`=YDUa0d&-`+CndPwD6iFd2o+XVunTgxtmQ{$S1UMP6MV7v(7gP@I9S413o zl3#rs#L5%Uc2ucoT?jP8VuB2h7X#Z+-x)J{kDWgM6Y2|J-X02-OJ|;5=u+hUJBeE- z5Bs}Xs>**EvLb|Cq4M4&`3_gy{uW!;&L6@^zcwR2x{6x+#Da#=k;H?NG!g!Gj4-C? zRzH{yS*SCPe3-imIuOHM=3$c54b~4

qVQCeOc)*Fr$i}e!=@13K+DKV!1+%R&rw>3mbH$ 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
B5TbI1V|a54m}UTX${_A) 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 KMNUMnLSTXs5Sa)7 literal 0 HcmV?d00001 diff --git a/src/app/+item-page/item-page.module.ts b/src/app/+item-page/item-page.module.ts index bd801923e3..d903baf8f7 100644 --- a/src/app/+item-page/item-page.module.ts +++ b/src/app/+item-page/item-page.module.ts @@ -2,6 +2,7 @@ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { SharedModule } from './../shared/shared.module'; +import { GenericItemPageFieldComponent } from './simple/field-components/specific-field/generic/generic-item-page-field.component'; import { ItemPageComponent } from './simple/item-page.component'; import { ItemPageRoutingModule } from './item-page-routing.module'; @@ -13,11 +14,16 @@ import { ItemPageDateFieldComponent } from './simple/field-components/specific-f import { ItemPageAbstractFieldComponent } from './simple/field-components/specific-field/abstract/item-page-abstract-field.component'; import { ItemPageUriFieldComponent } from './simple/field-components/specific-field/uri/item-page-uri-field.component'; import { ItemPageTitleFieldComponent } from './simple/field-components/specific-field/title/item-page-title-field.component'; -import { ItemPageSpecificFieldComponent } from './simple/field-components/specific-field/item-page-specific-field.component'; +import { ItemPageFieldComponent } from './simple/field-components/specific-field/item-page-field.component'; import { FileSectionComponent } from './simple/field-components/file-section/file-section.component'; import { CollectionsComponent } from './field-components/collections/collections.component'; import { FullItemPageComponent } from './full/full-item-page.component'; import { FullFileSectionComponent } from './full/field-components/file-section/full-file-section.component'; +import { ItemPageFieldsComponent } from './simple/relationship-types/item/item-page-fields.component'; +import { OrgUnitPageFieldsComponent } from './simple/relationship-types/orgunit/orgunit-page-fields.component'; +import { PersonPageFieldsComponent } from './simple/relationship-types/person/person-page-fields.component'; +import { ProjectPageFieldsComponent } from './simple/relationship-types/project/project-page-fields.component'; +import { RelationshipTypeSwitcherComponent } from './simple/relationship-types/switcher/relationship-type-switcher.component'; @NgModule({ imports: [ @@ -36,10 +42,22 @@ import { FullFileSectionComponent } from './full/field-components/file-section/f ItemPageAbstractFieldComponent, ItemPageUriFieldComponent, ItemPageTitleFieldComponent, - ItemPageSpecificFieldComponent, + ItemPageFieldComponent, FileSectionComponent, CollectionsComponent, - FullFileSectionComponent + FullFileSectionComponent, + RelationshipTypeSwitcherComponent, + ItemPageFieldsComponent, + ProjectPageFieldsComponent, + OrgUnitPageFieldsComponent, + PersonPageFieldsComponent, + GenericItemPageFieldComponent + ], + entryComponents: [ + ItemPageFieldsComponent, + ProjectPageFieldsComponent, + OrgUnitPageFieldsComponent, + PersonPageFieldsComponent ] }) export class ItemPageModule { diff --git a/src/app/+item-page/simple/field-components/specific-field/abstract/item-page-abstract-field.component.ts b/src/app/+item-page/simple/field-components/specific-field/abstract/item-page-abstract-field.component.ts index a8cc309ab6..d7d4c0af0e 100644 --- a/src/app/+item-page/simple/field-components/specific-field/abstract/item-page-abstract-field.component.ts +++ b/src/app/+item-page/simple/field-components/specific-field/abstract/item-page-abstract-field.component.ts @@ -1,13 +1,13 @@ import { Component, Input } from '@angular/core'; import { Item } from '../../../../../core/shared/item.model'; -import { ItemPageSpecificFieldComponent } from '../item-page-specific-field.component'; +import { ItemPageFieldComponent } from '../item-page-field.component'; @Component({ selector: 'ds-item-page-abstract-field', - templateUrl: './../item-page-specific-field.component.html' + templateUrl: '../item-page-field.component.html' }) -export class ItemPageAbstractFieldComponent extends ItemPageSpecificFieldComponent { +export class ItemPageAbstractFieldComponent extends ItemPageFieldComponent { @Input() item: Item; diff --git a/src/app/+item-page/simple/field-components/specific-field/author/item-page-author-field.component.ts b/src/app/+item-page/simple/field-components/specific-field/author/item-page-author-field.component.ts index e84a52d1b9..ac566c5e53 100644 --- a/src/app/+item-page/simple/field-components/specific-field/author/item-page-author-field.component.ts +++ b/src/app/+item-page/simple/field-components/specific-field/author/item-page-author-field.component.ts @@ -1,13 +1,13 @@ import { Component, Input } from '@angular/core'; import { Item } from '../../../../../core/shared/item.model'; -import { ItemPageSpecificFieldComponent } from '../item-page-specific-field.component'; +import { ItemPageFieldComponent } from '../item-page-field.component'; @Component({ selector: 'ds-item-page-author-field', - templateUrl: './../item-page-specific-field.component.html' + templateUrl: '../item-page-field.component.html' }) -export class ItemPageAuthorFieldComponent extends ItemPageSpecificFieldComponent { +export class ItemPageAuthorFieldComponent extends ItemPageFieldComponent { @Input() item: Item; diff --git a/src/app/+item-page/simple/field-components/specific-field/date/item-page-date-field.component.ts b/src/app/+item-page/simple/field-components/specific-field/date/item-page-date-field.component.ts index 6950944f87..b0f02bb718 100644 --- a/src/app/+item-page/simple/field-components/specific-field/date/item-page-date-field.component.ts +++ b/src/app/+item-page/simple/field-components/specific-field/date/item-page-date-field.component.ts @@ -1,13 +1,13 @@ import { Component, Input } from '@angular/core'; import { Item } from '../../../../../core/shared/item.model'; -import { ItemPageSpecificFieldComponent } from '../item-page-specific-field.component'; +import { ItemPageFieldComponent } from '../item-page-field.component'; @Component({ selector: 'ds-item-page-date-field', - templateUrl: './../item-page-specific-field.component.html' + templateUrl: '../item-page-field.component.html' }) -export class ItemPageDateFieldComponent extends ItemPageSpecificFieldComponent { +export class ItemPageDateFieldComponent extends ItemPageFieldComponent { @Input() item: Item; diff --git a/src/app/+item-page/simple/field-components/specific-field/generic/generic-item-page-field.component.ts b/src/app/+item-page/simple/field-components/specific-field/generic/generic-item-page-field.component.ts new file mode 100644 index 0000000000..fe1ecec57e --- /dev/null +++ b/src/app/+item-page/simple/field-components/specific-field/generic/generic-item-page-field.component.ts @@ -0,0 +1,20 @@ +import { Component, Input } from '@angular/core'; + +import { Item } from '../../../../../core/shared/item.model'; +import { ItemPageFieldComponent } from '../item-page-field.component'; + +@Component({ + selector: 'ds-generic-item-page-field', + templateUrl: '../item-page-field.component.html' +}) +export class GenericItemPageFieldComponent extends ItemPageFieldComponent { + + @Input() item: Item; + + @Input() separator: string; + + @Input() fields: string[]; + + @Input() label: string; + +} diff --git a/src/app/+item-page/simple/field-components/specific-field/item-page-specific-field.component.html b/src/app/+item-page/simple/field-components/specific-field/item-page-field.component.html similarity index 77% rename from src/app/+item-page/simple/field-components/specific-field/item-page-specific-field.component.html rename to src/app/+item-page/simple/field-components/specific-field/item-page-field.component.html index 4a27848ec6..bf74aca347 100644 --- a/src/app/+item-page/simple/field-components/specific-field/item-page-specific-field.component.html +++ b/src/app/+item-page/simple/field-components/specific-field/item-page-field.component.html @@ -1,3 +1,3 @@ -
+
diff --git a/src/app/+item-page/simple/field-components/specific-field/item-page-specific-field.component.ts b/src/app/+item-page/simple/field-components/specific-field/item-page-field.component.ts similarity index 86% rename from src/app/+item-page/simple/field-components/specific-field/item-page-specific-field.component.ts rename to src/app/+item-page/simple/field-components/specific-field/item-page-field.component.ts index f69671a5b5..525c898c0e 100644 --- a/src/app/+item-page/simple/field-components/specific-field/item-page-specific-field.component.ts +++ b/src/app/+item-page/simple/field-components/specific-field/item-page-field.component.ts @@ -9,9 +9,9 @@ import { Item } from '../../../../core/shared/item.model'; */ @Component({ - templateUrl: './item-page-specific-field.component.html' + templateUrl: './item-page-field.component.html' }) -export class ItemPageSpecificFieldComponent { +export class ItemPageFieldComponent { @Input() item: Item; diff --git a/src/app/+item-page/simple/field-components/specific-field/title/item-page-title-field.component.ts b/src/app/+item-page/simple/field-components/specific-field/title/item-page-title-field.component.ts index be8102359a..3ec11c8e3d 100644 --- a/src/app/+item-page/simple/field-components/specific-field/title/item-page-title-field.component.ts +++ b/src/app/+item-page/simple/field-components/specific-field/title/item-page-title-field.component.ts @@ -1,13 +1,13 @@ import { Component, Input } from '@angular/core'; import { Item } from '../../../../../core/shared/item.model'; -import { ItemPageSpecificFieldComponent } from '../item-page-specific-field.component'; +import { ItemPageFieldComponent } from '../item-page-field.component'; @Component({ selector: 'ds-item-page-title-field', templateUrl: './item-page-title-field.component.html' }) -export class ItemPageTitleFieldComponent extends ItemPageSpecificFieldComponent { +export class ItemPageTitleFieldComponent extends ItemPageFieldComponent { @Input() item: Item; diff --git a/src/app/+item-page/simple/field-components/specific-field/uri/item-page-uri-field.component.html b/src/app/+item-page/simple/field-components/specific-field/uri/item-page-uri-field.component.html index fde79d6a04..fdd60d5f68 100644 --- a/src/app/+item-page/simple/field-components/specific-field/uri/item-page-uri-field.component.html +++ b/src/app/+item-page/simple/field-components/specific-field/uri/item-page-uri-field.component.html @@ -1,3 +1,3 @@ -
+
diff --git a/src/app/+item-page/simple/field-components/specific-field/uri/item-page-uri-field.component.ts b/src/app/+item-page/simple/field-components/specific-field/uri/item-page-uri-field.component.ts index 4f06337032..805fdc160f 100644 --- a/src/app/+item-page/simple/field-components/specific-field/uri/item-page-uri-field.component.ts +++ b/src/app/+item-page/simple/field-components/specific-field/uri/item-page-uri-field.component.ts @@ -1,13 +1,13 @@ import { Component, Input } from '@angular/core'; import { Item } from '../../../../../core/shared/item.model'; -import { ItemPageSpecificFieldComponent } from '../item-page-specific-field.component'; +import { ItemPageFieldComponent } from '../item-page-field.component'; @Component({ selector: 'ds-item-page-uri-field', templateUrl: './item-page-uri-field.component.html' }) -export class ItemPageUriFieldComponent extends ItemPageSpecificFieldComponent { +export class ItemPageUriFieldComponent extends ItemPageFieldComponent { @Input() item: Item; diff --git a/src/app/+item-page/simple/item-page.component.html b/src/app/+item-page/simple/item-page.component.html index 5f938364e2..a6dd4a47a1 100644 --- a/src/app/+item-page/simple/item-page.component.html +++ b/src/app/+item-page/simple/item-page.component.html @@ -1,27 +1,7 @@
- -
-
- - - - - - -
- -
+
diff --git a/src/app/+item-page/simple/item-page.component.ts b/src/app/+item-page/simple/item-page.component.ts index 58a056a5dd..fee7753c82 100644 --- a/src/app/+item-page/simple/item-page.component.ts +++ b/src/app/+item-page/simple/item-page.component.ts @@ -12,6 +12,7 @@ import { MetadataService } from '../../core/metadata/metadata.service'; import { fadeInOut } from '../../shared/animations/fade'; import { hasValue } from '../../shared/empty.util'; +import * as viewMode from '../../shared/view-mode'; /** * This component renders a simple item page. @@ -35,6 +36,8 @@ export class ItemPageComponent implements OnInit { thumbnailObs: Observable; + ElementViewMode = viewMode.ElementViewMode; + constructor( private route: ActivatedRoute, private items: ItemDataService, diff --git a/src/app/+item-page/simple/relationship-types/item/item-page-fields.component.html b/src/app/+item-page/simple/relationship-types/item/item-page-fields.component.html new file mode 100644 index 0000000000..6301bc1bc9 --- /dev/null +++ b/src/app/+item-page/simple/relationship-types/item/item-page-fields.component.html @@ -0,0 +1,21 @@ + +
+
+ + + + + + +
+ +
diff --git a/src/app/+item-page/simple/relationship-types/item/item-page-fields.component.scss b/src/app/+item-page/simple/relationship-types/item/item-page-fields.component.scss new file mode 100644 index 0000000000..3575cae797 --- /dev/null +++ b/src/app/+item-page/simple/relationship-types/item/item-page-fields.component.scss @@ -0,0 +1 @@ +@import '../../../../../styles/variables.scss'; diff --git a/src/app/+item-page/simple/relationship-types/item/item-page-fields.component.ts b/src/app/+item-page/simple/relationship-types/item/item-page-fields.component.ts new file mode 100644 index 0000000000..39333503cb --- /dev/null +++ b/src/app/+item-page/simple/relationship-types/item/item-page-fields.component.ts @@ -0,0 +1,23 @@ +import { ChangeDetectionStrategy, Component, Inject } from '@angular/core'; +import { Item } from '../../../../core/shared/item.model'; +import { + DEFAULT_RELATIONSHIP_TYPE, + rendersRelationshipType +} from '../../../../shared/entities/relationship-type-decorator'; +import { ElementViewMode } from '../../../../shared/view-mode'; +import { ITEM } from '../switcher/relationship-type-switcher.component'; + +@rendersRelationshipType('Item', ElementViewMode.Full) +@rendersRelationshipType(DEFAULT_RELATIONSHIP_TYPE, ElementViewMode.Full) +@Component({ + selector: 'ds-item-page-fields', + styleUrls: ['./item-page-fields.component.scss'], + templateUrl: './item-page-fields.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ItemPageFieldsComponent { + + constructor(@Inject(ITEM) public item: Item) { + } + +} diff --git a/src/app/+item-page/simple/relationship-types/orgunit/orgunit-page-fields.component.html b/src/app/+item-page/simple/relationship-types/orgunit/orgunit-page-fields.component.html new file mode 100644 index 0000000000..6307bcc25f --- /dev/null +++ b/src/app/+item-page/simple/relationship-types/orgunit/orgunit-page-fields.component.html @@ -0,0 +1,32 @@ +

+ +

+
+
+ + + + + + + + + + + +
+
+ + +
+
diff --git a/src/app/+item-page/simple/relationship-types/orgunit/orgunit-page-fields.component.scss b/src/app/+item-page/simple/relationship-types/orgunit/orgunit-page-fields.component.scss new file mode 100644 index 0000000000..3575cae797 --- /dev/null +++ b/src/app/+item-page/simple/relationship-types/orgunit/orgunit-page-fields.component.scss @@ -0,0 +1 @@ +@import '../../../../../styles/variables.scss'; diff --git a/src/app/+item-page/simple/relationship-types/orgunit/orgunit-page-fields.component.ts b/src/app/+item-page/simple/relationship-types/orgunit/orgunit-page-fields.component.ts new file mode 100644 index 0000000000..d65b51ad7e --- /dev/null +++ b/src/app/+item-page/simple/relationship-types/orgunit/orgunit-page-fields.component.ts @@ -0,0 +1,18 @@ +import { Component, Inject } from '@angular/core'; +import { Item } from '../../../../core/shared/item.model'; +import { rendersRelationshipType } from '../../../../shared/entities/relationship-type-decorator'; +import { ElementViewMode } from '../../../../shared/view-mode'; +import { ITEM } from '../switcher/relationship-type-switcher.component'; + +@rendersRelationshipType('OrgUnit', ElementViewMode.Full) +@Component({ + selector: 'ds-orgunit-page-fields', + styleUrls: ['./orgunit-page-fields.component.scss'], + templateUrl: './orgunit-page-fields.component.html' +}) +export class OrgUnitPageFieldsComponent { + + constructor(@Inject(ITEM) public item: Item) { + } + +} diff --git a/src/app/+item-page/simple/relationship-types/person/person-page-fields.component.html b/src/app/+item-page/simple/relationship-types/person/person-page-fields.component.html new file mode 100644 index 0000000000..4763670b94 --- /dev/null +++ b/src/app/+item-page/simple/relationship-types/person/person-page-fields.component.html @@ -0,0 +1,40 @@ +

+ +

+
+
+ + + + + + + + + + + +
+
+ + + + + + +
+
diff --git a/src/app/+item-page/simple/relationship-types/person/person-page-fields.component.scss b/src/app/+item-page/simple/relationship-types/person/person-page-fields.component.scss new file mode 100644 index 0000000000..3575cae797 --- /dev/null +++ b/src/app/+item-page/simple/relationship-types/person/person-page-fields.component.scss @@ -0,0 +1 @@ +@import '../../../../../styles/variables.scss'; diff --git a/src/app/+item-page/simple/relationship-types/person/person-page-fields.component.ts b/src/app/+item-page/simple/relationship-types/person/person-page-fields.component.ts new file mode 100644 index 0000000000..fc89cd8644 --- /dev/null +++ b/src/app/+item-page/simple/relationship-types/person/person-page-fields.component.ts @@ -0,0 +1,18 @@ +import { Component, Inject } from '@angular/core'; +import { Item } from '../../../../core/shared/item.model'; +import { rendersRelationshipType } from '../../../../shared/entities/relationship-type-decorator'; +import { ElementViewMode } from '../../../../shared/view-mode'; +import { ITEM } from '../switcher/relationship-type-switcher.component'; + +@rendersRelationshipType('Person', ElementViewMode.Full) +@Component({ + selector: 'ds-person-page-fields', + styleUrls: ['./person-page-fields.component.scss'], + templateUrl: './person-page-fields.component.html' +}) +export class PersonPageFieldsComponent { + + constructor(@Inject(ITEM) public item: Item) { + } + +} diff --git a/src/app/+item-page/simple/relationship-types/project/project-page-fields.component.html b/src/app/+item-page/simple/relationship-types/project/project-page-fields.component.html new file mode 100644 index 0000000000..ccf342c867 --- /dev/null +++ b/src/app/+item-page/simple/relationship-types/project/project-page-fields.component.html @@ -0,0 +1,32 @@ +

+ +

+
+
+ + + + + + + + + +
+
+ + + + +
+
diff --git a/src/app/+item-page/simple/relationship-types/project/project-page-fields.component.scss b/src/app/+item-page/simple/relationship-types/project/project-page-fields.component.scss new file mode 100644 index 0000000000..3575cae797 --- /dev/null +++ b/src/app/+item-page/simple/relationship-types/project/project-page-fields.component.scss @@ -0,0 +1 @@ +@import '../../../../../styles/variables.scss'; diff --git a/src/app/+item-page/simple/relationship-types/project/project-page-fields.component.ts b/src/app/+item-page/simple/relationship-types/project/project-page-fields.component.ts new file mode 100644 index 0000000000..da9a4416b7 --- /dev/null +++ b/src/app/+item-page/simple/relationship-types/project/project-page-fields.component.ts @@ -0,0 +1,18 @@ +import { Component, Inject } from '@angular/core'; +import { Item } from '../../../../core/shared/item.model'; +import { rendersRelationshipType } from '../../../../shared/entities/relationship-type-decorator'; +import { ElementViewMode } from '../../../../shared/view-mode'; +import { ITEM } from '../switcher/relationship-type-switcher.component'; + +@rendersRelationshipType('Project', ElementViewMode.Full) +@Component({ + selector: 'ds-project-page-fields', + styleUrls: ['./project-page-fields.component.scss'], + templateUrl: './project-page-fields.component.html' +}) +export class ProjectPageFieldsComponent { + + constructor(@Inject(ITEM) public item: Item) { + } + +} diff --git a/src/app/+item-page/simple/relationship-types/switcher/relationship-type-switcher.component.html b/src/app/+item-page/simple/relationship-types/switcher/relationship-type-switcher.component.html new file mode 100644 index 0000000000..4965359495 --- /dev/null +++ b/src/app/+item-page/simple/relationship-types/switcher/relationship-type-switcher.component.html @@ -0,0 +1 @@ + diff --git a/src/app/+item-page/simple/relationship-types/switcher/relationship-type-switcher.component.scss b/src/app/+item-page/simple/relationship-types/switcher/relationship-type-switcher.component.scss new file mode 100644 index 0000000000..3575cae797 --- /dev/null +++ b/src/app/+item-page/simple/relationship-types/switcher/relationship-type-switcher.component.scss @@ -0,0 +1 @@ +@import '../../../../../styles/variables.scss'; diff --git a/src/app/+item-page/simple/relationship-types/switcher/relationship-type-switcher.component.ts b/src/app/+item-page/simple/relationship-types/switcher/relationship-type-switcher.component.ts new file mode 100644 index 0000000000..cb7293f514 --- /dev/null +++ b/src/app/+item-page/simple/relationship-types/switcher/relationship-type-switcher.component.ts @@ -0,0 +1,36 @@ +import { Component, InjectionToken, Injector, Input, OnInit } from '@angular/core'; +import { GenericConstructor } from '../../../../core/shared/generic-constructor'; +import { Item } from '../../../../core/shared/item.model'; +import { getComponentByRelationshipType } from '../../../../shared/entities/relationship-type-decorator'; +import { rendersDSOType } from '../../../../shared/object-collection/shared/dso-element-decorator'; +import { ListableObject } from '../../../../shared/object-collection/shared/listable-object.model'; +import { ElementViewMode, SetViewMode } from '../../../../shared/view-mode'; + +export const ITEM: InjectionToken = new InjectionToken('item'); + +@Component({ + selector: 'ds-relationship-type-switcher', + styleUrls: ['./relationship-type-switcher.component.scss'], + templateUrl: './relationship-type-switcher.component.html' +}) +export class RelationshipTypeSwitcherComponent implements OnInit { + @Input() item: Item; + @Input() viewMode: ElementViewMode; + objectInjector: Injector; + + constructor(private injector: Injector) { + } + + ngOnInit(): void { + this.objectInjector = Injector.create({ + providers: [{ provide: ITEM, useFactory: () => this.item, deps:[] }], + parent: this.injector + }); + + } + + getComponent(): string { + const type = this.item.findMetadata('relationship.type'); + return getComponentByRelationshipType(type, this.viewMode); + } +} diff --git a/src/app/+search-page/search-options.model.ts b/src/app/+search-page/search-options.model.ts index df8d8e713a..770d123f92 100644 --- a/src/app/+search-page/search-options.model.ts +++ b/src/app/+search-page/search-options.model.ts @@ -1,14 +1,10 @@ -import { isNotEmpty } from '../shared/empty.util'; -import { URLCombiner } from '../core/url-combiner/url-combiner'; import 'core-js/fn/object/entries'; - -export enum ViewMode { - List = 'list', - Grid = 'grid' -} +import { URLCombiner } from '../core/url-combiner/url-combiner'; +import { isNotEmpty } from '../shared/empty.util'; +import { SetViewMode } from '../shared/view-mode'; export class SearchOptions { - view?: ViewMode = ViewMode.List; + view?: SetViewMode = SetViewMode.List; scope?: string; query?: string; filters?: any; diff --git a/src/app/+search-page/search-results/search-results.component.ts b/src/app/+search-page/search-results/search-results.component.ts index 14ccb5d541..976f422a38 100644 --- a/src/app/+search-page/search-results/search-results.component.ts +++ b/src/app/+search-page/search-results/search-results.component.ts @@ -2,7 +2,8 @@ import { Component, Input } from '@angular/core'; import { RemoteData } from '../../core/data/remote-data'; import { DSpaceObject } from '../../core/shared/dspace-object.model'; import { fadeIn, fadeInOut } from '../../shared/animations/fade'; -import { SearchOptions, ViewMode } from '../search-options.model'; +import { SetViewMode } from '../../shared/view-mode'; +import { SearchOptions} from '../search-options.model'; import { SortOptions } from '../../core/cache/models/sort-options.model'; import { SearchResult } from '../search-result.model'; import { PaginatedList } from '../../core/data/paginated-list'; @@ -24,5 +25,5 @@ export class SearchResultsComponent { @Input() searchResults: RemoteData>>; @Input() searchConfig: SearchOptions; @Input() sortConfig: SortOptions; - @Input() viewMode: ViewMode; + @Input() viewMode: SetViewMode; } diff --git a/src/app/+search-page/search-service/search.service.spec.ts b/src/app/+search-page/search-service/search.service.spec.ts index 4b558f8726..e8eddc6cdb 100644 --- a/src/app/+search-page/search-service/search.service.spec.ts +++ b/src/app/+search-page/search-service/search.service.spec.ts @@ -6,7 +6,7 @@ import { Component } from '@angular/core'; import { SearchService } from './search.service'; import { ItemDataService } from './../../core/data/item-data.service'; -import { ViewMode } from '../../+search-page/search-options.model'; +import { SetViewMode } from '../../shared/view-mode'; import { RouteService } from '../../shared/route.service'; import { GLOBAL_CONFIG } from '../../../config'; import { RemoteDataBuildService } from '../../core/cache/builders/remote-data-build.service'; @@ -68,7 +68,7 @@ describe('SearchService', () => { it('should return list view mode', () => { searchService.getViewMode().subscribe((viewMode) => { - expect(viewMode).toBe(ViewMode.List); + expect(viewMode).toBe(SetViewMode.List); }); }); }); @@ -124,33 +124,33 @@ describe('SearchService', () => { }); it('should call the navigate method on the Router with view mode list parameter as a parameter when setViewMode is called', () => { - searchService.setViewMode(ViewMode.List); + searchService.setViewMode(SetViewMode.List); expect(router.navigate).toHaveBeenCalledWith(['/search'], { - queryParams: { view: ViewMode.List }, + queryParams: { view: SetViewMode.List }, queryParamsHandling: 'merge' }); }); it('should call the navigate method on the Router with view mode grid parameter as a parameter when setViewMode is called', () => { - searchService.setViewMode(ViewMode.Grid); + searchService.setViewMode(SetViewMode.Grid); expect(router.navigate).toHaveBeenCalledWith(['/search'], { - queryParams: { view: ViewMode.Grid }, + queryParams: { view: SetViewMode.Grid }, queryParamsHandling: 'merge' }); }); it('should return ViewMode.List when the viewMode is set to ViewMode.List in the ActivatedRoute', () => { - let viewMode = ViewMode.Grid; - route.testParams = { view: ViewMode.List }; + let viewMode = SetViewMode.Grid; + route.testParams = { view: SetViewMode.List }; searchService.getViewMode().subscribe((mode) => viewMode = mode); - expect(viewMode).toEqual(ViewMode.List); + expect(viewMode).toEqual(SetViewMode.List); }); it('should return ViewMode.Grid when the viewMode is set to ViewMode.Grid in the ActivatedRoute', () => { - let viewMode = ViewMode.List; - route.testParams = { view: ViewMode.Grid }; + let viewMode = SetViewMode.List; + route.testParams = { view: SetViewMode.Grid }; searchService.getViewMode().subscribe((mode) => viewMode = mode); - expect(viewMode).toEqual(ViewMode.Grid); + expect(viewMode).toEqual(SetViewMode.Grid); }); describe('when search is called', () => { diff --git a/src/app/+search-page/search-service/search.service.ts b/src/app/+search-page/search-service/search.service.ts index 0351a9a54c..f936c4b4ba 100644 --- a/src/app/+search-page/search-service/search.service.ts +++ b/src/app/+search-page/search-service/search.service.ts @@ -5,7 +5,7 @@ import { } from '@angular/router'; import { Observable } from 'rxjs/Observable'; import { flatMap, map, tap } from 'rxjs/operators'; -import { ViewMode } from '../../+search-page/search-options.model'; +import { SetViewMode } from '../../shared/view-mode'; import { RemoteDataBuildService } from '../../core/cache/builders/remote-data-build.service'; import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model'; import { @@ -217,17 +217,17 @@ export class SearchService implements OnDestroy { return this.rdb.toRemoteDataObservable(requestEntryObs, responseCacheObs, payloadObs); } - getViewMode(): Observable { + getViewMode(): Observable { return this.route.queryParams.map((params) => { if (isNotEmpty(params.view) && hasValue(params.view)) { return params.view; } else { - return ViewMode.List; + return SetViewMode.List; } }); } - setViewMode(viewMode: ViewMode) { + setViewMode(viewMode: SetViewMode) { const navigationExtras: NavigationExtras = { queryParams: { view: viewMode }, queryParamsHandling: 'merge' diff --git a/src/app/+search-page/search-settings/search-settings.component.ts b/src/app/+search-page/search-settings/search-settings.component.ts index 145b58e27b..bd26663583 100644 --- a/src/app/+search-page/search-settings/search-settings.component.ts +++ b/src/app/+search-page/search-settings/search-settings.component.ts @@ -1,6 +1,7 @@ import { Component, Input, OnInit, ChangeDetectionStrategy } from '@angular/core'; +import { SetViewMode } from '../../shared/view-mode'; import { SearchService } from '../search-service/search.service'; -import { SearchOptions, ViewMode } from '../search-options.model'; +import { SearchOptions} from '../search-options.model'; import { SortDirection } from '../../core/cache/models/sort-options.model'; import { ActivatedRoute, NavigationExtras, Router } from '@angular/router'; import { PaginatedSearchOptions } from '../paginated-search-options.model'; @@ -48,7 +49,7 @@ export class SearchSettingsComponent implements OnInit { this.page = +params.page || this.searchOptions.pagination.currentPage; this.pageSize = +params.pageSize || this.searchOptions.pagination.pageSize; this.direction = params.sortDirection || this.searchOptions.sort.direction; - if (params.view === ViewMode.Grid) { + if (params.view === SetViewMode.Grid) { this.pageSizeOptions = this.pageSizeOptions; } else { this.pageSizeOptions = this.pageSizeOptions; diff --git a/src/app/shared/entities/relationship-type-decorator.ts b/src/app/shared/entities/relationship-type-decorator.ts new file mode 100644 index 0000000000..8d510e53b9 --- /dev/null +++ b/src/app/shared/entities/relationship-type-decorator.ts @@ -0,0 +1,22 @@ +import { hasNoValue } from '../empty.util'; +import { ElementViewMode } from '../view-mode'; + +export const DEFAULT_RELATIONSHIP_TYPE = 'Default'; + +const map = new Map(); +export function rendersRelationshipType(type: string, viewMode: ElementViewMode) { + return function decorator(component: any) { + if (hasNoValue(map.get(viewMode))) { + map.set(viewMode, new Map()); + } + map.get(viewMode).set(type, component); + }; +} + +export function getComponentByRelationshipType(type: string, viewMode: ElementViewMode) { + let component = map.get(viewMode).get(type); + if (hasNoValue(component)) { + component = map.get(viewMode).get(DEFAULT_RELATIONSHIP_TYPE); + } + return component; +} diff --git a/src/app/shared/object-collection/object-collection.component.spec.ts b/src/app/shared/object-collection/object-collection.component.spec.ts index b27a342eac..b033d1fe97 100644 --- a/src/app/shared/object-collection/object-collection.component.spec.ts +++ b/src/app/shared/object-collection/object-collection.component.spec.ts @@ -1,5 +1,5 @@ import { ObjectCollectionComponent } from './object-collection.component'; -import { ViewMode } from '../../+search-page/search-options.model'; +import { SetViewMode } from '../view-mode'; import { element } from 'protractor'; import { By } from '@angular/platform-browser'; import { async, ComponentFixture, TestBed } from '@angular/core/testing'; @@ -38,14 +38,14 @@ describe('ObjectCollectionComponent', () => { })); it('should only show the grid component when the viewmode is set to grid', () => { - objectCollectionComponent.currentMode = ViewMode.Grid; + objectCollectionComponent.currentMode = SetViewMode.Grid; expect(fixture.debugElement.query(By.css('ds-object-grid'))).toBeDefined(); expect(fixture.debugElement.query(By.css('ds-object-list'))).toBeNull(); }); it('should only show the list component when the viewmode is set to list', () => { - objectCollectionComponent.currentMode = ViewMode.List; + objectCollectionComponent.currentMode = SetViewMode.List; expect(fixture.debugElement.query(By.css('ds-object-list'))).toBeDefined(); expect(fixture.debugElement.query(By.css('ds-object-grid'))).toBeNull(); diff --git a/src/app/shared/object-collection/object-collection.component.ts b/src/app/shared/object-collection/object-collection.component.ts index 3af92443c1..05e22514b7 100644 --- a/src/app/shared/object-collection/object-collection.component.ts +++ b/src/app/shared/object-collection/object-collection.component.ts @@ -14,7 +14,7 @@ import { PaginationComponentOptions } from '../pagination/pagination-component-o import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model'; import { ListableObject } from './shared/listable-object.model'; -import { ViewMode } from '../../+search-page/search-options.model'; +import { SetViewMode } from '../view-mode'; import { hasValue, isNotEmpty } from '../empty.util'; @Component({ @@ -56,8 +56,8 @@ export class ObjectCollectionComponent implements OnChanges, OnInit { */ @Output() sortFieldChange: EventEmitter = new EventEmitter(); data: any = {}; - currentMode: ViewMode = ViewMode.List; - viewModeEnum = ViewMode; + currentMode: SetViewMode = SetViewMode.List; + viewModeEnum = SetViewMode; ngOnChanges(changes: SimpleChanges) { if (changes.objects && !changes.objects.isFirstChange()) { @@ -87,7 +87,7 @@ export class ObjectCollectionComponent implements OnChanges, OnInit { private router: Router) { } - getViewMode(): ViewMode { + getViewMode(): SetViewMode { this.route.queryParams.map((params) => { if (isNotEmpty(params.view) && hasValue(params.view)) { this.currentMode = params.view; diff --git a/src/app/shared/object-collection/shared/dso-element-decorator.spec.ts b/src/app/shared/object-collection/shared/dso-element-decorator.spec.ts index 9f54bebed9..12da0f5b36 100644 --- a/src/app/shared/object-collection/shared/dso-element-decorator.spec.ts +++ b/src/app/shared/object-collection/shared/dso-element-decorator.spec.ts @@ -1,10 +1,10 @@ -import { ViewMode } from '../../../+search-page/search-options.model'; +import { SetViewMode } from '../../view-mode'; import { renderElementsFor } from './dso-element-decorator'; import { Item } from '../../../core/shared/item.model'; describe('ElementDecorator', () => { - const gridDecorator = renderElementsFor(Item, ViewMode.Grid); - const listDecorator = renderElementsFor(Item, ViewMode.List); + const gridDecorator = renderElementsFor(Item, SetViewMode.Grid); + const listDecorator = renderElementsFor(Item, SetViewMode.List); it('should have a decorator for both list and grid', () => { expect(listDecorator.length).not.toBeNull(); expect(gridDecorator.length).not.toBeNull(); diff --git a/src/app/shared/object-collection/shared/dso-element-decorator.ts b/src/app/shared/object-collection/shared/dso-element-decorator.ts index 51aa57bc50..4e4d37579a 100644 --- a/src/app/shared/object-collection/shared/dso-element-decorator.ts +++ b/src/app/shared/object-collection/shared/dso-element-decorator.ts @@ -1,9 +1,9 @@ import { GenericConstructor } from '../../../core/shared/generic-constructor'; import { ListableObject } from './listable-object.model'; -import { ViewMode } from '../../../+search-page/search-options.model'; +import { SetViewMode } from '../../view-mode'; const dsoElementMap = new Map(); -export function renderElementsFor(listable: GenericConstructor, viewMode: ViewMode) { +export function renderElementsFor(listable: GenericConstructor, viewMode: SetViewMode) { return function decorator(objectElement: any) { if (!objectElement) { return; @@ -15,6 +15,6 @@ export function renderElementsFor(listable: GenericConstructor, }; } -export function rendersDSOType(listable: GenericConstructor, viewMode: ViewMode) { +export function rendersDSOType(listable: GenericConstructor, viewMode: SetViewMode) { return dsoElementMap.get(viewMode).get(listable); } diff --git a/src/app/shared/object-grid/collection-grid-element/collection-grid-element.component.ts b/src/app/shared/object-grid/collection-grid-element/collection-grid-element.component.ts index f383367e3f..af6027aa1a 100644 --- a/src/app/shared/object-grid/collection-grid-element/collection-grid-element.component.ts +++ b/src/app/shared/object-grid/collection-grid-element/collection-grid-element.component.ts @@ -2,7 +2,7 @@ import { Component, Inject } from '@angular/core'; import { Collection } from '../../../core/shared/collection.model'; import { renderElementsFor} from '../../object-collection/shared/dso-element-decorator'; -import { ViewMode } from '../../../+search-page/search-options.model'; +import { SetViewMode } from '../../view-mode'; import { AbstractListableElementComponent } from '../../object-collection/shared/object-collection-element/abstract-listable-element.component'; @Component({ @@ -11,5 +11,5 @@ import { AbstractListableElementComponent } from '../../object-collection/shared templateUrl: './collection-grid-element.component.html' }) -@renderElementsFor(Collection, ViewMode.Grid) +@renderElementsFor(Collection, SetViewMode.Grid) export class CollectionGridElementComponent extends AbstractListableElementComponent {} diff --git a/src/app/shared/object-grid/community-grid-element/community-grid-element.component.ts b/src/app/shared/object-grid/community-grid-element/community-grid-element.component.ts index 21d73ff0fa..4f69e3e2c7 100644 --- a/src/app/shared/object-grid/community-grid-element/community-grid-element.component.ts +++ b/src/app/shared/object-grid/community-grid-element/community-grid-element.component.ts @@ -3,7 +3,7 @@ import { Component, Input, Inject } from '@angular/core'; import { Community } from '../../../core/shared/community.model'; import { AbstractListableElementComponent } from '../../object-collection/shared/object-collection-element/abstract-listable-element.component'; import { renderElementsFor} from '../../object-collection/shared/dso-element-decorator'; -import { ViewMode } from '../../../+search-page/search-options.model'; +import { SetViewMode } from '../../view-mode'; @Component({ selector: 'ds-community-grid-element', @@ -11,5 +11,5 @@ import { ViewMode } from '../../../+search-page/search-options.model'; templateUrl: './community-grid-element.component.html' }) -@renderElementsFor(Community, ViewMode.Grid) +@renderElementsFor(Community, SetViewMode.Grid) export class CommunityGridElementComponent extends AbstractListableElementComponent {} diff --git a/src/app/shared/object-grid/item-grid-element/item-grid-element.component.ts b/src/app/shared/object-grid/item-grid-element/item-grid-element.component.ts index 5a8d46f1f2..a4137b3c26 100644 --- a/src/app/shared/object-grid/item-grid-element/item-grid-element.component.ts +++ b/src/app/shared/object-grid/item-grid-element/item-grid-element.component.ts @@ -3,7 +3,7 @@ import { Component, Input, Inject } from '@angular/core'; import { Item } from '../../../core/shared/item.model'; import { renderElementsFor} from '../../object-collection/shared/dso-element-decorator'; import { AbstractListableElementComponent } from '../../object-collection/shared/object-collection-element/abstract-listable-element.component'; -import { ViewMode } from '../../../+search-page/search-options.model'; +import { SetViewMode } from '../../view-mode'; @Component({ selector: 'ds-item-grid-element', @@ -11,5 +11,5 @@ import { ViewMode } from '../../../+search-page/search-options.model'; templateUrl: './item-grid-element.component.html' }) -@renderElementsFor(Item, ViewMode.Grid) +@renderElementsFor(Item, SetViewMode.Grid) export class ItemGridElementComponent extends AbstractListableElementComponent {} diff --git a/src/app/shared/object-grid/search-result-grid-element/collection-search-result/collection-search-result-grid-element.component.ts b/src/app/shared/object-grid/search-result-grid-element/collection-search-result/collection-search-result-grid-element.component.ts index 7f8a3bb9fd..61cc5dca1c 100644 --- a/src/app/shared/object-grid/search-result-grid-element/collection-search-result/collection-search-result-grid-element.component.ts +++ b/src/app/shared/object-grid/search-result-grid-element/collection-search-result/collection-search-result-grid-element.component.ts @@ -3,7 +3,7 @@ import { Component } from '@angular/core'; import { renderElementsFor} from '../../../object-collection/shared/dso-element-decorator'; import { SearchResultGridElementComponent } from '../search-result-grid-element.component'; import { Collection } from '../../../../core/shared/collection.model'; -import { ViewMode } from '../../../../+search-page/search-options.model'; +import { SetViewMode } from '../../../view-mode'; import { CollectionSearchResult } from '../../../object-collection/shared/collection-search-result.model'; @Component({ @@ -12,5 +12,5 @@ import { CollectionSearchResult } from '../../../object-collection/shared/collec templateUrl: 'collection-search-result-grid-element.component.html' }) -@renderElementsFor(CollectionSearchResult, ViewMode.Grid) +@renderElementsFor(CollectionSearchResult, SetViewMode.Grid) export class CollectionSearchResultGridElementComponent extends SearchResultGridElementComponent {} diff --git a/src/app/shared/object-grid/search-result-grid-element/community-search-result/community-search-result-grid-element.component.ts b/src/app/shared/object-grid/search-result-grid-element/community-search-result/community-search-result-grid-element.component.ts index 7c34207f5e..d445e27ed6 100644 --- a/src/app/shared/object-grid/search-result-grid-element/community-search-result/community-search-result-grid-element.component.ts +++ b/src/app/shared/object-grid/search-result-grid-element/community-search-result/community-search-result-grid-element.component.ts @@ -2,7 +2,7 @@ import { Component } from '@angular/core'; import { Community } from '../../../../core/shared/community.model'; import { renderElementsFor } from '../../../object-collection/shared/dso-element-decorator'; import { SearchResultGridElementComponent } from '../search-result-grid-element.component'; -import { ViewMode } from '../../../../+search-page/search-options.model'; +import { SetViewMode } from '../../../view-mode'; import { CommunitySearchResult } from '../../../object-collection/shared/community-search-result.model'; @Component({ @@ -11,7 +11,7 @@ import { CommunitySearchResult } from '../../../object-collection/shared/communi templateUrl: 'community-search-result-grid-element.component.html' }) -@renderElementsFor(CommunitySearchResult, ViewMode.Grid) +@renderElementsFor(CommunitySearchResult, SetViewMode.Grid) export class CommunitySearchResultGridElementComponent extends SearchResultGridElementComponent { } diff --git a/src/app/shared/object-grid/search-result-grid-element/item-search-result/item-search-result-grid-element.component.ts b/src/app/shared/object-grid/search-result-grid-element/item-search-result/item-search-result-grid-element.component.ts index 518fc23a44..30c36b3af9 100644 --- a/src/app/shared/object-grid/search-result-grid-element/item-search-result/item-search-result-grid-element.component.ts +++ b/src/app/shared/object-grid/search-result-grid-element/item-search-result/item-search-result-grid-element.component.ts @@ -4,7 +4,7 @@ import { renderElementsFor } from '../../../object-collection/shared/dso-element import { SearchResultGridElementComponent } from '../search-result-grid-element.component'; import { Item } from '../../../../core/shared/item.model'; import { ItemSearchResult } from '../../../object-collection/shared/item-search-result.model'; -import { ViewMode } from '../../../../+search-page/search-options.model'; +import { SetViewMode } from '../../../view-mode'; import { focusShadow } from '../../../../shared/animations/focus'; @Component({ @@ -14,5 +14,5 @@ import { focusShadow } from '../../../../shared/animations/focus'; animations: [focusShadow], }) -@renderElementsFor(ItemSearchResult, ViewMode.Grid) +@renderElementsFor(ItemSearchResult, SetViewMode.Grid) export class ItemSearchResultGridElementComponent extends SearchResultGridElementComponent {} diff --git a/src/app/shared/object-grid/wrapper-grid-element/wrapper-grid-element.component.ts b/src/app/shared/object-grid/wrapper-grid-element/wrapper-grid-element.component.ts index f807542e76..84f9357b2d 100644 --- a/src/app/shared/object-grid/wrapper-grid-element/wrapper-grid-element.component.ts +++ b/src/app/shared/object-grid/wrapper-grid-element/wrapper-grid-element.component.ts @@ -1,5 +1,5 @@ import { Component, Injector, Input, OnInit } from '@angular/core'; -import { ViewMode } from '../../../+search-page/search-options.model'; +import { SetViewMode } from '../../view-mode'; import { GenericConstructor } from '../../../core/shared/generic-constructor'; import { rendersDSOType } from '../../object-collection/shared/dso-element-decorator'; import { ListableObject } from '../../object-collection/shared/listable-object.model'; @@ -26,6 +26,6 @@ export class WrapperGridElementComponent implements OnInit { getGridElement(): string { const f: GenericConstructor = this.object.constructor as GenericConstructor; - return rendersDSOType(f, ViewMode.Grid); + return rendersDSOType(f, SetViewMode.Grid); } } diff --git a/src/app/shared/object-list/collection-list-element/collection-list-element.component.ts b/src/app/shared/object-list/collection-list-element/collection-list-element.component.ts index f95c087d5c..3fe64f5bb9 100644 --- a/src/app/shared/object-list/collection-list-element/collection-list-element.component.ts +++ b/src/app/shared/object-list/collection-list-element/collection-list-element.component.ts @@ -2,7 +2,7 @@ import { Component, Inject } from '@angular/core'; import { Collection } from '../../../core/shared/collection.model'; import { renderElementsFor } from '../../object-collection/shared/dso-element-decorator'; -import { ViewMode } from '../../../+search-page/search-options.model'; +import { SetViewMode } from '../../view-mode'; import { AbstractListableElementComponent } from '../../object-collection/shared/object-collection-element/abstract-listable-element.component'; @Component({ @@ -11,5 +11,5 @@ import { AbstractListableElementComponent } from '../../object-collection/shared templateUrl: './collection-list-element.component.html' }) -@renderElementsFor(Collection, ViewMode.List) +@renderElementsFor(Collection, SetViewMode.List) export class CollectionListElementComponent extends AbstractListableElementComponent {} diff --git a/src/app/shared/object-list/community-list-element/community-list-element.component.ts b/src/app/shared/object-list/community-list-element/community-list-element.component.ts index e92c080a31..5e254cb1ac 100644 --- a/src/app/shared/object-list/community-list-element/community-list-element.component.ts +++ b/src/app/shared/object-list/community-list-element/community-list-element.component.ts @@ -3,7 +3,7 @@ import { Component, Input, Inject } from '@angular/core'; import { Community } from '../../../core/shared/community.model'; import { AbstractListableElementComponent } from '../../object-collection/shared/object-collection-element/abstract-listable-element.component'; import { renderElementsFor } from '../../object-collection/shared/dso-element-decorator'; -import { ViewMode } from '../../../+search-page/search-options.model'; +import { SetViewMode } from '../../view-mode'; @Component({ selector: 'ds-community-list-element', @@ -11,5 +11,5 @@ import { ViewMode } from '../../../+search-page/search-options.model'; templateUrl: './community-list-element.component.html' }) -@renderElementsFor(Community, ViewMode.List) +@renderElementsFor(Community, SetViewMode.List) export class CommunityListElementComponent extends AbstractListableElementComponent {} diff --git a/src/app/shared/object-list/item-list-element/item-list-element.component.ts b/src/app/shared/object-list/item-list-element/item-list-element.component.ts index e4efbd6b08..8dd09f71b9 100644 --- a/src/app/shared/object-list/item-list-element/item-list-element.component.ts +++ b/src/app/shared/object-list/item-list-element/item-list-element.component.ts @@ -3,7 +3,7 @@ import { Component, Input, Inject } from '@angular/core'; import { Item } from '../../../core/shared/item.model'; import { AbstractListableElementComponent } from '../../object-collection/shared/object-collection-element/abstract-listable-element.component'; import { renderElementsFor } from '../../object-collection/shared/dso-element-decorator'; -import { ViewMode } from '../../../+search-page/search-options.model'; +import { SetViewMode } from '../../view-mode'; @Component({ selector: 'ds-item-list-element', @@ -11,5 +11,5 @@ import { ViewMode } from '../../../+search-page/search-options.model'; templateUrl: './item-list-element.component.html' }) -@renderElementsFor(Item, ViewMode.List) +@renderElementsFor(Item, SetViewMode.List) export class ItemListElementComponent extends AbstractListableElementComponent {} diff --git a/src/app/shared/object-list/search-result-list-element/collection-search-result/collection-search-result-list-element.component.ts b/src/app/shared/object-list/search-result-list-element/collection-search-result/collection-search-result-list-element.component.ts index 746e700f05..2205155bbd 100644 --- a/src/app/shared/object-list/search-result-list-element/collection-search-result/collection-search-result-list-element.component.ts +++ b/src/app/shared/object-list/search-result-list-element/collection-search-result/collection-search-result-list-element.component.ts @@ -4,7 +4,7 @@ import { renderElementsFor } from '../../../object-collection/shared/dso-element import { SearchResultListElementComponent } from '../search-result-list-element.component'; import { Collection } from '../../../../core/shared/collection.model'; -import { ViewMode } from '../../../../+search-page/search-options.model'; +import { SetViewMode } from '../../../view-mode'; import { CollectionSearchResult } from '../../../object-collection/shared/collection-search-result.model'; @Component({ @@ -13,5 +13,5 @@ import { CollectionSearchResult } from '../../../object-collection/shared/collec templateUrl: 'collection-search-result-list-element.component.html' }) -@renderElementsFor(CollectionSearchResult, ViewMode.List) +@renderElementsFor(CollectionSearchResult, SetViewMode.List) export class CollectionSearchResultListElementComponent extends SearchResultListElementComponent {} diff --git a/src/app/shared/object-list/search-result-list-element/community-search-result/community-search-result-list-element.component.ts b/src/app/shared/object-list/search-result-list-element/community-search-result/community-search-result-list-element.component.ts index 2ca89fc9c5..3f7e08d11c 100644 --- a/src/app/shared/object-list/search-result-list-element/community-search-result/community-search-result-list-element.component.ts +++ b/src/app/shared/object-list/search-result-list-element/community-search-result/community-search-result-list-element.component.ts @@ -4,7 +4,7 @@ import { renderElementsFor } from '../../../object-collection/shared/dso-element import { SearchResultListElementComponent } from '../search-result-list-element.component'; import { Community } from '../../../../core/shared/community.model'; -import { ViewMode } from '../../../../+search-page/search-options.model'; +import { SetViewMode } from '../../../view-mode'; import { CommunitySearchResult } from '../../../object-collection/shared/community-search-result.model'; @Component({ @@ -13,7 +13,7 @@ import { CommunitySearchResult } from '../../../object-collection/shared/communi templateUrl: 'community-search-result-list-element.component.html' }) -@renderElementsFor(CommunitySearchResult, ViewMode.List) +@renderElementsFor(CommunitySearchResult, SetViewMode.List) export class CommunitySearchResultListElementComponent extends SearchResultListElementComponent { } diff --git a/src/app/shared/object-list/search-result-list-element/item-search-result/item-search-result-list-element.component.ts b/src/app/shared/object-list/search-result-list-element/item-search-result/item-search-result-list-element.component.ts index b776abc214..37dcb907ef 100644 --- a/src/app/shared/object-list/search-result-list-element/item-search-result/item-search-result-list-element.component.ts +++ b/src/app/shared/object-list/search-result-list-element/item-search-result/item-search-result-list-element.component.ts @@ -4,7 +4,7 @@ import { renderElementsFor } from '../../../object-collection/shared/dso-element import { SearchResultListElementComponent } from '../search-result-list-element.component'; import { Item } from '../../../../core/shared/item.model'; import { ItemSearchResult } from '../../../object-collection/shared/item-search-result.model'; -import { ViewMode } from '../../../../+search-page/search-options.model'; +import { SetViewMode } from '../../../view-mode'; import { ListableObject } from '../../../object-collection/shared/listable-object.model'; import { focusBackground } from '../../../animations/focus'; @@ -16,6 +16,6 @@ import { focusBackground } from '../../../animations/focus'; }) -@renderElementsFor(ItemSearchResult, ViewMode.List) +@renderElementsFor(ItemSearchResult, SetViewMode.List) export class ItemSearchResultListElementComponent extends SearchResultListElementComponent { } diff --git a/src/app/shared/object-list/wrapper-list-element/wrapper-list-element.component.ts b/src/app/shared/object-list/wrapper-list-element/wrapper-list-element.component.ts index 217c1776c5..c8d3316983 100644 --- a/src/app/shared/object-list/wrapper-list-element/wrapper-list-element.component.ts +++ b/src/app/shared/object-list/wrapper-list-element/wrapper-list-element.component.ts @@ -1,5 +1,5 @@ import { Component, Injector, Input, OnInit } from '@angular/core'; -import { ViewMode } from '../../../+search-page/search-options.model'; +import { SetViewMode } from '../../view-mode'; import { GenericConstructor } from '../../../core/shared/generic-constructor'; import { rendersDSOType } from '../../object-collection/shared/dso-element-decorator' import { ListableObject } from '../../object-collection/shared/listable-object.model'; @@ -24,6 +24,6 @@ export class WrapperListElementComponent implements OnInit { getListElement(): string { const f: GenericConstructor = this.object.constructor as GenericConstructor; - return rendersDSOType(f, ViewMode.List); + return rendersDSOType(f, SetViewMode.List); } } diff --git a/src/app/shared/testing/hal-endpoint-service-stub.ts b/src/app/shared/testing/hal-endpoint-service-stub.ts index e7dbe8bea1..3ebe2c1d25 100644 --- a/src/app/shared/testing/hal-endpoint-service-stub.ts +++ b/src/app/shared/testing/hal-endpoint-service-stub.ts @@ -1,5 +1,5 @@ import { Observable } from 'rxjs/Observable'; -import { ViewMode } from '../../+search-page/search-options.model'; +import { SetViewMode } from '../view-mode'; import { BehaviorSubject } from 'rxjs/BehaviorSubject'; export class HALEndpointServiceStub { diff --git a/src/app/shared/testing/search-service-stub.ts b/src/app/shared/testing/search-service-stub.ts index 7ad0d871ce..999ab3794b 100644 --- a/src/app/shared/testing/search-service-stub.ts +++ b/src/app/shared/testing/search-service-stub.ts @@ -1,23 +1,23 @@ import { Observable } from 'rxjs/Observable'; -import { ViewMode } from '../../+search-page/search-options.model'; +import { SetViewMode } from '../view-mode'; import { BehaviorSubject } from 'rxjs/BehaviorSubject'; export class SearchServiceStub { - private _viewMode: ViewMode; + private _viewMode: SetViewMode; private subject?: BehaviorSubject = new BehaviorSubject(this.testViewMode); viewMode = this.subject.asObservable(); constructor(private searchLink: string = '/search') { - this.setViewMode(ViewMode.List); + this.setViewMode(SetViewMode.List); } - getViewMode(): Observable { + getViewMode(): Observable { return this.viewMode; } - setViewMode(viewMode: ViewMode) { + setViewMode(viewMode: SetViewMode) { this.testViewMode = viewMode; } @@ -25,11 +25,11 @@ export class SearchServiceStub { return null; } - get testViewMode(): ViewMode { + get testViewMode(): SetViewMode { return this._viewMode; } - set testViewMode(viewMode: ViewMode) { + set testViewMode(viewMode: SetViewMode) { this._viewMode = viewMode; this.subject.next(viewMode); } diff --git a/src/app/shared/view-mode-switch/view-mode-switch.component.spec.ts b/src/app/shared/view-mode-switch/view-mode-switch.component.spec.ts index 1b4bf6da46..2fe405de3f 100644 --- a/src/app/shared/view-mode-switch/view-mode-switch.component.spec.ts +++ b/src/app/shared/view-mode-switch/view-mode-switch.component.spec.ts @@ -8,7 +8,7 @@ import { ChangeDetectionStrategy, Component } from '@angular/core'; import { SearchService } from '../../+search-page/search-service/search.service'; import { ViewModeSwitchComponent } from './view-mode-switch.component'; -import { ViewMode } from '../../+search-page/search-options.model'; +import { SetViewMode } from '../view-mode'; import { SearchServiceStub } from '../testing/search-service-stub'; @Component({ template: '' }) @@ -55,19 +55,19 @@ describe('ViewModeSwitchComponent', () => { }); it('should set list button as active when on list mode', fakeAsync(() => { - searchService.setViewMode(ViewMode.List); + searchService.setViewMode(SetViewMode.List); tick(); fixture.detectChanges(); - expect(comp.currentMode).toBe(ViewMode.List); + expect(comp.currentMode).toBe(SetViewMode.List); expect(listButton.classList).toContain('active'); expect(gridButton.classList).not.toContain('active'); })); it('should set grid button as active when on grid mode', fakeAsync(() => { - searchService.setViewMode(ViewMode.Grid); + searchService.setViewMode(SetViewMode.Grid); tick(); fixture.detectChanges(); - expect(comp.currentMode).toBe(ViewMode.Grid); + expect(comp.currentMode).toBe(SetViewMode.Grid); expect(listButton.classList).not.toContain('active'); expect(gridButton.classList).toContain('active'); })); diff --git a/src/app/shared/view-mode-switch/view-mode-switch.component.ts b/src/app/shared/view-mode-switch/view-mode-switch.component.ts index f6e04816fb..11db82b3e9 100644 --- a/src/app/shared/view-mode-switch/view-mode-switch.component.ts +++ b/src/app/shared/view-mode-switch/view-mode-switch.component.ts @@ -1,6 +1,6 @@ import { Subscription } from 'rxjs/Subscription'; import { Component, OnInit, OnDestroy } from '@angular/core'; -import { ViewMode } from '../../+search-page/search-options.model'; +import { SetViewMode } from '../view-mode'; import { SearchService } from './../../+search-page/search-service/search.service'; /** @@ -12,20 +12,20 @@ import { SearchService } from './../../+search-page/search-service/search.servic templateUrl: './view-mode-switch.component.html' }) export class ViewModeSwitchComponent implements OnInit, OnDestroy { - currentMode: ViewMode = ViewMode.List; - viewModeEnum = ViewMode; + currentMode: SetViewMode = SetViewMode.List; + viewModeEnum = SetViewMode; private sub: Subscription; constructor(private searchService: SearchService) { } ngOnInit(): void { - this.sub = this.searchService.getViewMode().subscribe((viewMode: ViewMode) => { + this.sub = this.searchService.getViewMode().subscribe((viewMode: SetViewMode) => { this.currentMode = viewMode; }); } - switchViewTo(viewMode: ViewMode) { + switchViewTo(viewMode: SetViewMode) { this.searchService.setViewMode(viewMode); } diff --git a/src/app/shared/view-mode.ts b/src/app/shared/view-mode.ts new file mode 100644 index 0000000000..c12d051943 --- /dev/null +++ b/src/app/shared/view-mode.ts @@ -0,0 +1,11 @@ +export enum SetViewMode { + List, + Grid +} + +export enum ElementViewMode { + Full, + setElement +} + +export type ViewMode = SetViewMode | ElementViewMode; diff --git a/src/app/thumbnail/thumbnail.component.html b/src/app/thumbnail/thumbnail.component.html index 6221cbaba1..dd0d79d094 100644 --- a/src/app/thumbnail/thumbnail.component.html +++ b/src/app/thumbnail/thumbnail.component.html @@ -1,4 +1,4 @@
- +
diff --git a/src/app/thumbnail/thumbnail.component.scss b/src/app/thumbnail/thumbnail.component.scss index da97dd7a62..9388327700 100644 --- a/src/app/thumbnail/thumbnail.component.scss +++ b/src/app/thumbnail/thumbnail.component.scss @@ -1 +1,4 @@ @import '../../styles/variables.scss'; +img { + max-width: 100%; +} diff --git a/src/app/thumbnail/thumbnail.component.spec.ts b/src/app/thumbnail/thumbnail.component.spec.ts index fc145d2397..f2be55d52c 100644 --- a/src/app/thumbnail/thumbnail.component.spec.ts +++ b/src/app/thumbnail/thumbnail.component.spec.ts @@ -36,7 +36,7 @@ describe('ThumbnailComponent', () => { it('should display placeholder', () => { fixture.detectChanges(); const image: HTMLElement = de.query(By.css('img')).nativeElement; - expect(image.getAttribute('src')).toBe(comp.holderSource); + expect(image.getAttribute('src')).toBe(comp.defaultImage); }); }); diff --git a/src/app/thumbnail/thumbnail.component.ts b/src/app/thumbnail/thumbnail.component.ts index cac1909b2b..5e2b713b31 100644 --- a/src/app/thumbnail/thumbnail.component.ts +++ b/src/app/thumbnail/thumbnail.component.ts @@ -21,10 +21,10 @@ export class ThumbnailComponent { /** * The default 'holder.js' image */ - holderSource = 'data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2293%22%20height%3D%22120%22%20viewBox%3D%220%200%2093%20120%22%20preserveAspectRatio%3D%22none%22%3E%3C!--%0ASource%20URL%3A%20holder.js%2F93x120%3Ftext%3DNo%20Thumbnail%0ACreated%20with%20Holder.js%202.8.2.%0ALearn%20more%20at%20http%3A%2F%2Fholderjs.com%0A(c)%202012-2015%20Ivan%20Malopinsky%20-%20http%3A%2F%2Fimsky.co%0A--%3E%3Cdefs%3E%3Cstyle%20type%3D%22text%2Fcss%22%3E%3C!%5BCDATA%5B%23holder_1543e460b05%20text%20%7B%20fill%3A%23AAAAAA%3Bfont-weight%3Abold%3Bfont-family%3AArial%2C%20Helvetica%2C%20Open%20Sans%2C%20sans-serif%2C%20monospace%3Bfont-size%3A10pt%20%7D%20%5D%5D%3E%3C%2Fstyle%3E%3C%2Fdefs%3E%3Cg%20id%3D%22holder_1543e460b05%22%3E%3Crect%20width%3D%2293%22%20height%3D%22120%22%20fill%3D%22%23EEEEEE%22%2F%3E%3Cg%3E%3Ctext%20x%3D%2235.6171875%22%20y%3D%2257%22%3ENo%3C%2Ftext%3E%3Ctext%20x%3D%2210.8125%22%20y%3D%2272%22%3EThumbnail%3C%2Ftext%3E%3C%2Fg%3E%3C%2Fg%3E%3C%2Fsvg%3E'; + @Input() defaultImage? = 'data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2293%22%20height%3D%22120%22%20viewBox%3D%220%200%2093%20120%22%20preserveAspectRatio%3D%22none%22%3E%3C!--%0ASource%20URL%3A%20holder.js%2F93x120%3Ftext%3DNo%20Thumbnail%0ACreated%20with%20Holder.js%202.8.2.%0ALearn%20more%20at%20http%3A%2F%2Fholderjs.com%0A(c)%202012-2015%20Ivan%20Malopinsky%20-%20http%3A%2F%2Fimsky.co%0A--%3E%3Cdefs%3E%3Cstyle%20type%3D%22text%2Fcss%22%3E%3C!%5BCDATA%5B%23holder_1543e460b05%20text%20%7B%20fill%3A%23AAAAAA%3Bfont-weight%3Abold%3Bfont-family%3AArial%2C%20Helvetica%2C%20Open%20Sans%2C%20sans-serif%2C%20monospace%3Bfont-size%3A10pt%20%7D%20%5D%5D%3E%3C%2Fstyle%3E%3C%2Fdefs%3E%3Cg%20id%3D%22holder_1543e460b05%22%3E%3Crect%20width%3D%2293%22%20height%3D%22120%22%20fill%3D%22%23EEEEEE%22%2F%3E%3Cg%3E%3Ctext%20x%3D%2235.6171875%22%20y%3D%2257%22%3ENo%3C%2Ftext%3E%3Ctext%20x%3D%2210.8125%22%20y%3D%2272%22%3EThumbnail%3C%2Ftext%3E%3C%2Fg%3E%3C%2Fg%3E%3C%2Fsvg%3E'; errorHandler(event) { - event.currentTarget.src = this.holderSource; + event.currentTarget.src = this.defaultImage; } } From 26098c6bd07bf816b5f6ad74809c269ea5afd003 Mon Sep 17 00:00:00 2001 From: Art Lowel Date: Tue, 29 May 2018 10:45:30 +0200 Subject: [PATCH 002/604] Throw an error when there are multiple components to render the same type/viewmode combination --- src/app/shared/entities/relationship-type-decorator.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/app/shared/entities/relationship-type-decorator.ts b/src/app/shared/entities/relationship-type-decorator.ts index 8d510e53b9..ee91ad4970 100644 --- a/src/app/shared/entities/relationship-type-decorator.ts +++ b/src/app/shared/entities/relationship-type-decorator.ts @@ -1,4 +1,4 @@ -import { hasNoValue } from '../empty.util'; +import { hasNoValue, hasValue } from '../empty.util'; import { ElementViewMode } from '../view-mode'; export const DEFAULT_RELATIONSHIP_TYPE = 'Default'; @@ -9,6 +9,9 @@ export function rendersRelationshipType(type: string, viewMode: ElementViewMode) if (hasNoValue(map.get(viewMode))) { map.set(viewMode, new Map()); } + if (hasValue(map.get(viewMode).get(type))) { + throw new Error(`There can't be more than one component to render Items of type "${type}" in view mode "${viewMode}"`); + } map.get(viewMode).set(type, component); }; } From 7db32b2e243c64bc1fb28dd6458eb743bfbbd098 Mon Sep 17 00:00:00 2001 From: Art Lowel Date: Tue, 29 May 2018 11:55:56 +0200 Subject: [PATCH 003/604] refactoring --- resources/i18n/en.json | 2 +- src/app/+item-page/item-page.module.ts | 12 ++++++------ .../item/item-page-fields.component.html | 0 .../item/item-page-fields.component.scss | 0 .../item/item-page-fields.component.ts | 12 ++++++------ .../orgunit/orgunit-page-fields.component.html | 0 .../orgunit/orgunit-page-fields.component.scss | 0 .../orgunit/orgunit-page-fields.component.ts | 6 +++--- .../person/person-page-fields.component.html | 0 .../person/person-page-fields.component.scss | 0 .../person/person-page-fields.component.ts | 6 +++--- .../project/project-page-fields.component.html | 4 ++-- .../project/project-page-fields.component.scss | 0 .../project/project-page-fields.component.ts | 6 +++--- .../switcher/entity-type-switcher.component.html} | 0 .../switcher/entity-type-switcher.component.scss} | 0 .../switcher/entity-type-switcher.component.ts} | 12 ++++++------ src/app/+item-page/simple/item-page.component.html | 2 +- ...ip-type-decorator.ts => entity-type-decorator.ts} | 8 ++++---- 19 files changed, 35 insertions(+), 35 deletions(-) rename src/app/+item-page/simple/{relationship-types => entity-types}/item/item-page-fields.component.html (100%) rename src/app/+item-page/simple/{relationship-types => entity-types}/item/item-page-fields.component.scss (100%) rename src/app/+item-page/simple/{relationship-types => entity-types}/item/item-page-fields.component.ts (61%) rename src/app/+item-page/simple/{relationship-types => entity-types}/orgunit/orgunit-page-fields.component.html (100%) rename src/app/+item-page/simple/{relationship-types => entity-types}/orgunit/orgunit-page-fields.component.scss (100%) rename src/app/+item-page/simple/{relationship-types => entity-types}/orgunit/orgunit-page-fields.component.ts (65%) rename src/app/+item-page/simple/{relationship-types => entity-types}/person/person-page-fields.component.html (100%) rename src/app/+item-page/simple/{relationship-types => entity-types}/person/person-page-fields.component.scss (100%) rename src/app/+item-page/simple/{relationship-types => entity-types}/person/person-page-fields.component.ts (65%) rename src/app/+item-page/simple/{relationship-types => entity-types}/project/project-page-fields.component.html (93%) rename src/app/+item-page/simple/{relationship-types => entity-types}/project/project-page-fields.component.scss (100%) rename src/app/+item-page/simple/{relationship-types => entity-types}/project/project-page-fields.component.ts (65%) rename src/app/+item-page/simple/{relationship-types/switcher/relationship-type-switcher.component.html => entity-types/switcher/entity-type-switcher.component.html} (100%) rename src/app/+item-page/simple/{relationship-types/switcher/relationship-type-switcher.component.scss => entity-types/switcher/entity-type-switcher.component.scss} (100%) rename src/app/+item-page/simple/{relationship-types/switcher/relationship-type-switcher.component.ts => entity-types/switcher/entity-type-switcher.component.ts} (71%) rename src/app/shared/entities/{relationship-type-decorator.ts => entity-type-decorator.ts} (67%) diff --git a/resources/i18n/en.json b/resources/i18n/en.json index 06671e0809..78a2b1a408 100644 --- a/resources/i18n/en.json +++ b/resources/i18n/en.json @@ -65,7 +65,7 @@ "id": "ID", "expectedcompletion": "Expected Completion", "description": "Description", - "keywords": "Keywords" + "keyword": "Keywords" } }, "orgunit": { diff --git a/src/app/+item-page/item-page.module.ts b/src/app/+item-page/item-page.module.ts index d903baf8f7..fdf9d19674 100644 --- a/src/app/+item-page/item-page.module.ts +++ b/src/app/+item-page/item-page.module.ts @@ -19,11 +19,11 @@ import { FileSectionComponent } from './simple/field-components/file-section/fil import { CollectionsComponent } from './field-components/collections/collections.component'; import { FullItemPageComponent } from './full/full-item-page.component'; import { FullFileSectionComponent } from './full/field-components/file-section/full-file-section.component'; -import { ItemPageFieldsComponent } from './simple/relationship-types/item/item-page-fields.component'; -import { OrgUnitPageFieldsComponent } from './simple/relationship-types/orgunit/orgunit-page-fields.component'; -import { PersonPageFieldsComponent } from './simple/relationship-types/person/person-page-fields.component'; -import { ProjectPageFieldsComponent } from './simple/relationship-types/project/project-page-fields.component'; -import { RelationshipTypeSwitcherComponent } from './simple/relationship-types/switcher/relationship-type-switcher.component'; +import { ItemPageFieldsComponent } from './simple/entity-types/item/item-page-fields.component'; +import { OrgUnitPageFieldsComponent } from './simple/entity-types/orgunit/orgunit-page-fields.component'; +import { PersonPageFieldsComponent } from './simple/entity-types/person/person-page-fields.component'; +import { ProjectPageFieldsComponent } from './simple/entity-types/project/project-page-fields.component'; +import { EntityTypeSwitcherComponent } from './simple/entity-types/switcher/entity-type-switcher.component'; @NgModule({ imports: [ @@ -46,7 +46,7 @@ import { RelationshipTypeSwitcherComponent } from './simple/relationship-types/s FileSectionComponent, CollectionsComponent, FullFileSectionComponent, - RelationshipTypeSwitcherComponent, + EntityTypeSwitcherComponent, ItemPageFieldsComponent, ProjectPageFieldsComponent, OrgUnitPageFieldsComponent, diff --git a/src/app/+item-page/simple/relationship-types/item/item-page-fields.component.html b/src/app/+item-page/simple/entity-types/item/item-page-fields.component.html similarity index 100% rename from src/app/+item-page/simple/relationship-types/item/item-page-fields.component.html rename to src/app/+item-page/simple/entity-types/item/item-page-fields.component.html diff --git a/src/app/+item-page/simple/relationship-types/item/item-page-fields.component.scss b/src/app/+item-page/simple/entity-types/item/item-page-fields.component.scss similarity index 100% rename from src/app/+item-page/simple/relationship-types/item/item-page-fields.component.scss rename to src/app/+item-page/simple/entity-types/item/item-page-fields.component.scss diff --git a/src/app/+item-page/simple/relationship-types/item/item-page-fields.component.ts b/src/app/+item-page/simple/entity-types/item/item-page-fields.component.ts similarity index 61% rename from src/app/+item-page/simple/relationship-types/item/item-page-fields.component.ts rename to src/app/+item-page/simple/entity-types/item/item-page-fields.component.ts index 39333503cb..90504812d7 100644 --- a/src/app/+item-page/simple/relationship-types/item/item-page-fields.component.ts +++ b/src/app/+item-page/simple/entity-types/item/item-page-fields.component.ts @@ -1,14 +1,14 @@ import { ChangeDetectionStrategy, Component, Inject } from '@angular/core'; import { Item } from '../../../../core/shared/item.model'; import { - DEFAULT_RELATIONSHIP_TYPE, - rendersRelationshipType -} from '../../../../shared/entities/relationship-type-decorator'; + DEFAULT_ENTITY_TYPE, + rendersEntityType +} from '../../../../shared/entities/entity-type-decorator'; import { ElementViewMode } from '../../../../shared/view-mode'; -import { ITEM } from '../switcher/relationship-type-switcher.component'; +import { ITEM } from '../switcher/entity-type-switcher.component'; -@rendersRelationshipType('Item', ElementViewMode.Full) -@rendersRelationshipType(DEFAULT_RELATIONSHIP_TYPE, ElementViewMode.Full) +@rendersEntityType('Item', ElementViewMode.Full) +@rendersEntityType(DEFAULT_ENTITY_TYPE, ElementViewMode.Full) @Component({ selector: 'ds-item-page-fields', styleUrls: ['./item-page-fields.component.scss'], diff --git a/src/app/+item-page/simple/relationship-types/orgunit/orgunit-page-fields.component.html b/src/app/+item-page/simple/entity-types/orgunit/orgunit-page-fields.component.html similarity index 100% rename from src/app/+item-page/simple/relationship-types/orgunit/orgunit-page-fields.component.html rename to src/app/+item-page/simple/entity-types/orgunit/orgunit-page-fields.component.html diff --git a/src/app/+item-page/simple/relationship-types/orgunit/orgunit-page-fields.component.scss b/src/app/+item-page/simple/entity-types/orgunit/orgunit-page-fields.component.scss similarity index 100% rename from src/app/+item-page/simple/relationship-types/orgunit/orgunit-page-fields.component.scss rename to src/app/+item-page/simple/entity-types/orgunit/orgunit-page-fields.component.scss diff --git a/src/app/+item-page/simple/relationship-types/orgunit/orgunit-page-fields.component.ts b/src/app/+item-page/simple/entity-types/orgunit/orgunit-page-fields.component.ts similarity index 65% rename from src/app/+item-page/simple/relationship-types/orgunit/orgunit-page-fields.component.ts rename to src/app/+item-page/simple/entity-types/orgunit/orgunit-page-fields.component.ts index d65b51ad7e..97a0d4f477 100644 --- a/src/app/+item-page/simple/relationship-types/orgunit/orgunit-page-fields.component.ts +++ b/src/app/+item-page/simple/entity-types/orgunit/orgunit-page-fields.component.ts @@ -1,10 +1,10 @@ import { Component, Inject } from '@angular/core'; import { Item } from '../../../../core/shared/item.model'; -import { rendersRelationshipType } from '../../../../shared/entities/relationship-type-decorator'; +import { rendersEntityType } from '../../../../shared/entities/entity-type-decorator'; import { ElementViewMode } from '../../../../shared/view-mode'; -import { ITEM } from '../switcher/relationship-type-switcher.component'; +import { ITEM } from '../switcher/entity-type-switcher.component'; -@rendersRelationshipType('OrgUnit', ElementViewMode.Full) +@rendersEntityType('OrgUnit', ElementViewMode.Full) @Component({ selector: 'ds-orgunit-page-fields', styleUrls: ['./orgunit-page-fields.component.scss'], diff --git a/src/app/+item-page/simple/relationship-types/person/person-page-fields.component.html b/src/app/+item-page/simple/entity-types/person/person-page-fields.component.html similarity index 100% rename from src/app/+item-page/simple/relationship-types/person/person-page-fields.component.html rename to src/app/+item-page/simple/entity-types/person/person-page-fields.component.html diff --git a/src/app/+item-page/simple/relationship-types/person/person-page-fields.component.scss b/src/app/+item-page/simple/entity-types/person/person-page-fields.component.scss similarity index 100% rename from src/app/+item-page/simple/relationship-types/person/person-page-fields.component.scss rename to src/app/+item-page/simple/entity-types/person/person-page-fields.component.scss diff --git a/src/app/+item-page/simple/relationship-types/person/person-page-fields.component.ts b/src/app/+item-page/simple/entity-types/person/person-page-fields.component.ts similarity index 65% rename from src/app/+item-page/simple/relationship-types/person/person-page-fields.component.ts rename to src/app/+item-page/simple/entity-types/person/person-page-fields.component.ts index fc89cd8644..ec0577426c 100644 --- a/src/app/+item-page/simple/relationship-types/person/person-page-fields.component.ts +++ b/src/app/+item-page/simple/entity-types/person/person-page-fields.component.ts @@ -1,10 +1,10 @@ import { Component, Inject } from '@angular/core'; import { Item } from '../../../../core/shared/item.model'; -import { rendersRelationshipType } from '../../../../shared/entities/relationship-type-decorator'; +import { rendersEntityType } from '../../../../shared/entities/entity-type-decorator'; import { ElementViewMode } from '../../../../shared/view-mode'; -import { ITEM } from '../switcher/relationship-type-switcher.component'; +import { ITEM } from '../switcher/entity-type-switcher.component'; -@rendersRelationshipType('Person', ElementViewMode.Full) +@rendersEntityType('Person', ElementViewMode.Full) @Component({ selector: 'ds-person-page-fields', styleUrls: ['./person-page-fields.component.scss'], diff --git a/src/app/+item-page/simple/relationship-types/project/project-page-fields.component.html b/src/app/+item-page/simple/entity-types/project/project-page-fields.component.html similarity index 93% rename from src/app/+item-page/simple/relationship-types/project/project-page-fields.component.html rename to src/app/+item-page/simple/entity-types/project/project-page-fields.component.html index ccf342c867..65a7c7f270 100644 --- a/src/app/+item-page/simple/relationship-types/project/project-page-fields.component.html +++ b/src/app/+item-page/simple/entity-types/project/project-page-fields.component.html @@ -25,8 +25,8 @@ [label]="'project.page.description'"> + [fields]="['project.identifier.keyword']" + [label]="'project.page.keyword'">
diff --git a/src/app/+item-page/simple/relationship-types/project/project-page-fields.component.scss b/src/app/+item-page/simple/entity-types/project/project-page-fields.component.scss similarity index 100% rename from src/app/+item-page/simple/relationship-types/project/project-page-fields.component.scss rename to src/app/+item-page/simple/entity-types/project/project-page-fields.component.scss diff --git a/src/app/+item-page/simple/relationship-types/project/project-page-fields.component.ts b/src/app/+item-page/simple/entity-types/project/project-page-fields.component.ts similarity index 65% rename from src/app/+item-page/simple/relationship-types/project/project-page-fields.component.ts rename to src/app/+item-page/simple/entity-types/project/project-page-fields.component.ts index da9a4416b7..f6e93dcfa6 100644 --- a/src/app/+item-page/simple/relationship-types/project/project-page-fields.component.ts +++ b/src/app/+item-page/simple/entity-types/project/project-page-fields.component.ts @@ -1,10 +1,10 @@ import { Component, Inject } from '@angular/core'; import { Item } from '../../../../core/shared/item.model'; -import { rendersRelationshipType } from '../../../../shared/entities/relationship-type-decorator'; +import { rendersEntityType } from '../../../../shared/entities/entity-type-decorator'; import { ElementViewMode } from '../../../../shared/view-mode'; -import { ITEM } from '../switcher/relationship-type-switcher.component'; +import { ITEM } from '../switcher/entity-type-switcher.component'; -@rendersRelationshipType('Project', ElementViewMode.Full) +@rendersEntityType('Project', ElementViewMode.Full) @Component({ selector: 'ds-project-page-fields', styleUrls: ['./project-page-fields.component.scss'], diff --git a/src/app/+item-page/simple/relationship-types/switcher/relationship-type-switcher.component.html b/src/app/+item-page/simple/entity-types/switcher/entity-type-switcher.component.html similarity index 100% rename from src/app/+item-page/simple/relationship-types/switcher/relationship-type-switcher.component.html rename to src/app/+item-page/simple/entity-types/switcher/entity-type-switcher.component.html diff --git a/src/app/+item-page/simple/relationship-types/switcher/relationship-type-switcher.component.scss b/src/app/+item-page/simple/entity-types/switcher/entity-type-switcher.component.scss similarity index 100% rename from src/app/+item-page/simple/relationship-types/switcher/relationship-type-switcher.component.scss rename to src/app/+item-page/simple/entity-types/switcher/entity-type-switcher.component.scss diff --git a/src/app/+item-page/simple/relationship-types/switcher/relationship-type-switcher.component.ts b/src/app/+item-page/simple/entity-types/switcher/entity-type-switcher.component.ts similarity index 71% rename from src/app/+item-page/simple/relationship-types/switcher/relationship-type-switcher.component.ts rename to src/app/+item-page/simple/entity-types/switcher/entity-type-switcher.component.ts index cb7293f514..d2e2374e8e 100644 --- a/src/app/+item-page/simple/relationship-types/switcher/relationship-type-switcher.component.ts +++ b/src/app/+item-page/simple/entity-types/switcher/entity-type-switcher.component.ts @@ -1,7 +1,7 @@ import { Component, InjectionToken, Injector, Input, OnInit } from '@angular/core'; import { GenericConstructor } from '../../../../core/shared/generic-constructor'; import { Item } from '../../../../core/shared/item.model'; -import { getComponentByRelationshipType } from '../../../../shared/entities/relationship-type-decorator'; +import { getComponentByEntityType } from '../../../../shared/entities/entity-type-decorator'; import { rendersDSOType } from '../../../../shared/object-collection/shared/dso-element-decorator'; import { ListableObject } from '../../../../shared/object-collection/shared/listable-object.model'; import { ElementViewMode, SetViewMode } from '../../../../shared/view-mode'; @@ -9,11 +9,11 @@ import { ElementViewMode, SetViewMode } from '../../../../shared/view-mode'; export const ITEM: InjectionToken = new InjectionToken('item'); @Component({ - selector: 'ds-relationship-type-switcher', - styleUrls: ['./relationship-type-switcher.component.scss'], - templateUrl: './relationship-type-switcher.component.html' + selector: 'ds-entity-type-switcher', + styleUrls: ['./entity-type-switcher.component.scss'], + templateUrl: './entity-type-switcher.component.html' }) -export class RelationshipTypeSwitcherComponent implements OnInit { +export class EntityTypeSwitcherComponent implements OnInit { @Input() item: Item; @Input() viewMode: ElementViewMode; objectInjector: Injector; @@ -31,6 +31,6 @@ export class RelationshipTypeSwitcherComponent implements OnInit { getComponent(): string { const type = this.item.findMetadata('relationship.type'); - return getComponentByRelationshipType(type, this.viewMode); + return getComponentByEntityType(type, this.viewMode); } } diff --git a/src/app/+item-page/simple/item-page.component.html b/src/app/+item-page/simple/item-page.component.html index a6dd4a47a1..7c75887f38 100644 --- a/src/app/+item-page/simple/item-page.component.html +++ b/src/app/+item-page/simple/item-page.component.html @@ -1,7 +1,7 @@
- +
diff --git a/src/app/shared/entities/relationship-type-decorator.ts b/src/app/shared/entities/entity-type-decorator.ts similarity index 67% rename from src/app/shared/entities/relationship-type-decorator.ts rename to src/app/shared/entities/entity-type-decorator.ts index ee91ad4970..60bdab7412 100644 --- a/src/app/shared/entities/relationship-type-decorator.ts +++ b/src/app/shared/entities/entity-type-decorator.ts @@ -1,10 +1,10 @@ import { hasNoValue, hasValue } from '../empty.util'; import { ElementViewMode } from '../view-mode'; -export const DEFAULT_RELATIONSHIP_TYPE = 'Default'; +export const DEFAULT_ENTITY_TYPE = 'Default'; const map = new Map(); -export function rendersRelationshipType(type: string, viewMode: ElementViewMode) { +export function rendersEntityType(type: string, viewMode: ElementViewMode) { return function decorator(component: any) { if (hasNoValue(map.get(viewMode))) { map.set(viewMode, new Map()); @@ -16,10 +16,10 @@ export function rendersRelationshipType(type: string, viewMode: ElementViewMode) }; } -export function getComponentByRelationshipType(type: string, viewMode: ElementViewMode) { +export function getComponentByEntityType(type: string, viewMode: ElementViewMode) { let component = map.get(viewMode).get(type); if (hasNoValue(component)) { - component = map.get(viewMode).get(DEFAULT_RELATIONSHIP_TYPE); + component = map.get(viewMode).get(DEFAULT_ENTITY_TYPE); } return component; } From 5149c48eddae065978d8ff391dfb65e0e59137e3 Mon Sep 17 00:00:00 2001 From: Art Lowel Date: Tue, 29 May 2018 19:36:32 +0200 Subject: [PATCH 004/604] first version of relationships - too resource intensive --- resources/i18n/en.json | 1 + .../person/person-page-fields.component.html | 7 ++ .../person/person-page-fields.component.ts | 86 ++++++++++++++++++- .../entities/normalized-entity-type.model.ts | 20 +++++ .../normalized-relationship-type.model.ts | 43 ++++++++++ .../entities/normalized-relationship.model.ts | 30 +++++++ .../cache/models/normalized-item.model.ts | 4 + .../cache/models/normalized-object-factory.ts | 12 +++ .../core/shared/entities/entity-type.model.ts | 9 ++ .../entities/relationship-type.model.ts | 33 +++++++ .../shared/entities/relationship.model.ts | 23 +++++ src/app/core/shared/item.model.ts | 3 + src/app/core/shared/operators.ts | 2 +- src/app/core/shared/resource-type.ts | 5 +- 14 files changed, 275 insertions(+), 3 deletions(-) create mode 100644 src/app/core/cache/models/entities/normalized-entity-type.model.ts create mode 100644 src/app/core/cache/models/entities/normalized-relationship-type.model.ts create mode 100644 src/app/core/cache/models/entities/normalized-relationship.model.ts create mode 100644 src/app/core/shared/entities/entity-type.model.ts create mode 100644 src/app/core/shared/entities/relationship-type.model.ts create mode 100644 src/app/core/shared/entities/relationship.model.ts diff --git a/resources/i18n/en.json b/resources/i18n/en.json index 78a2b1a408..a06f77463f 100644 --- a/resources/i18n/en.json +++ b/resources/i18n/en.json @@ -54,6 +54,7 @@ "orcid": "ORCID", "birthdate": "Birth Date", "staffid": "Staff ID", + "publications": "Publications", "link": { "full": "Show all metadata" } diff --git a/src/app/+item-page/simple/entity-types/person/person-page-fields.component.html b/src/app/+item-page/simple/entity-types/person/person-page-fields.component.html index 4763670b94..a19170b77d 100644 --- a/src/app/+item-page/simple/entity-types/person/person-page-fields.component.html +++ b/src/app/+item-page/simple/entity-types/person/person-page-fields.component.html @@ -24,6 +24,13 @@
+ + + + {{publ.name}} + + + diff --git a/src/app/+item-page/simple/entity-types/person/person-page-fields.component.ts b/src/app/+item-page/simple/entity-types/person/person-page-fields.component.ts index ec0577426c..6599ad580e 100644 --- a/src/app/+item-page/simple/entity-types/person/person-page-fields.component.ts +++ b/src/app/+item-page/simple/entity-types/person/person-page-fields.component.ts @@ -1,5 +1,17 @@ import { Component, Inject } from '@angular/core'; +import ensureArray from 'rollup/dist/typings/utils/ensureArray'; +import { Observable } from 'rxjs/Observable'; +import { filter, flatMap, map } from 'rxjs/operators'; +import { relationship } from '../../../../core/cache/builders/build-decorators'; +import { RemoteDataBuildService } from '../../../../core/cache/builders/remote-data-build.service'; +import { ItemDataService } from '../../../../core/data/item-data.service'; +import { PaginatedList } from '../../../../core/data/paginated-list'; +import { RemoteData } from '../../../../core/data/remote-data'; +import { RelationshipType } from '../../../../core/shared/entities/relationship-type.model'; +import { Relationship } from '../../../../core/shared/entities/relationship.model'; import { Item } from '../../../../core/shared/item.model'; +import { getRemoteDataPayload } from '../../../../core/shared/operators'; +import { ensureArrayHasValue, hasValue } from '../../../../shared/empty.util'; import { rendersEntityType } from '../../../../shared/entities/entity-type-decorator'; import { ElementViewMode } from '../../../../shared/view-mode'; import { ITEM } from '../switcher/entity-type-switcher.component'; @@ -11,8 +23,80 @@ import { ITEM } from '../switcher/entity-type-switcher.component'; templateUrl: './person-page-fields.component.html' }) export class PersonPageFieldsComponent { + publications$: Observable; + isProjectOfPersonRels$: Observable; + isOrgUnitOfPersonRels$: Observable; + + constructor( + @Inject(ITEM) public item: Item, + private ids: ItemDataService + ) { + const relsCurrentPage$ = item.relationships.pipe( + filter((rd: RemoteData>) => rd.hasSucceeded), + getRemoteDataPayload(), + map((pl: PaginatedList) => pl.page), + ensureArrayHasValue(), + ); + + const relTypesCurrentPage$ = relsCurrentPage$.pipe( + flatMap((rels: Relationship[]) => + Observable.combineLatest( + ...rels.map((rel: Relationship) => rel.relationshipType), + (...arr: Array>) => + arr.map((d: RemoteData) => d.payload) + ) + ) + ); + + const resolvedRelsAndTypes$ = Observable.combineLatest( + relsCurrentPage$, + relTypesCurrentPage$ + ); + + this.publications$ = resolvedRelsAndTypes$.pipe( + map(([relsCurrentPage, relTypesCurrentPage]) => + relsCurrentPage.filter((rel: Relationship, idx: number) => + hasValue(relTypesCurrentPage[idx]) && (relTypesCurrentPage[idx].leftLabel === 'isPublicationOfAuthor' || + relTypesCurrentPage[idx].rightLabel === 'isPublicationOfAuthor') + ) + ), + flatMap((rels: Relationship[]) => + Observable.combineLatest( + ...rels.map((rel: Relationship) => { + let queryId = rel.leftId; + if (rel.leftId === this.item.id) { + queryId = rel.rightId; + } + return this.ids.findById(queryId); + }), + (...arr: Array>) => + arr + .filter((d: RemoteData) => d.hasSucceeded) + .map((d: RemoteData) => d.payload) + ) + ) + ); + + //TODO status het lijkt te werken maar duurt minuten om te laden: too much recursion? + + this.isProjectOfPersonRels$ = resolvedRelsAndTypes$.pipe( + map(([relsCurrentPage, relTypesCurrentPage]) => + relsCurrentPage.filter((rel: Relationship, idx: number) => + hasValue(relTypesCurrentPage[idx]) && (relTypesCurrentPage[idx].leftLabel === 'isProjectOfPerson' || + relTypesCurrentPage[idx].rightLabel === 'isProjectOfPerson') + ) + ) + ); + + this.isOrgUnitOfPersonRels$ = resolvedRelsAndTypes$.pipe( + map(([relsCurrentPage, relTypesCurrentPage]) => + relsCurrentPage.filter((rel: Relationship, idx: number) => + hasValue(relTypesCurrentPage[idx]) && (relTypesCurrentPage[idx].leftLabel === 'isOrgUnitOfPerson' || + relTypesCurrentPage[idx].rightLabel === 'isOrgUnitOfPerson') + ) + ) + ); - constructor(@Inject(ITEM) public item: Item) { } } diff --git a/src/app/core/cache/models/entities/normalized-entity-type.model.ts b/src/app/core/cache/models/entities/normalized-entity-type.model.ts new file mode 100644 index 0000000000..d9939cef71 --- /dev/null +++ b/src/app/core/cache/models/entities/normalized-entity-type.model.ts @@ -0,0 +1,20 @@ +import { autoserialize, autoserializeAs, inheritSerialization } from 'cerialize'; +import { EntityType } from '../../../shared/entities/entity-type.model'; +import { ResourceType } from '../../../shared/resource-type'; +import { mapsTo } from '../../builders/build-decorators'; +import { IDToUUIDSerializer } from '../../it-to-uuid-serializer'; +import { NormalizedObject } from '../normalized-object.model'; + +@mapsTo(EntityType) +@inheritSerialization(NormalizedObject) +export class NormalizedEntityType extends NormalizedObject { + + @autoserialize + label: string; + + @autoserialize + id: string; + + @autoserializeAs(new IDToUUIDSerializer(ResourceType.EntityType), 'id') + uuid: string; +} diff --git a/src/app/core/cache/models/entities/normalized-relationship-type.model.ts b/src/app/core/cache/models/entities/normalized-relationship-type.model.ts new file mode 100644 index 0000000000..262359cab9 --- /dev/null +++ b/src/app/core/cache/models/entities/normalized-relationship-type.model.ts @@ -0,0 +1,43 @@ +import { autoserialize, autoserializeAs, inheritSerialization } from 'cerialize'; +import { RelationshipType } from '../../../shared/entities/relationship-type.model'; +import { ResourceType } from '../../../shared/resource-type'; +import { mapsTo, relationship } from '../../builders/build-decorators'; +import { IDToUUIDSerializer } from '../../it-to-uuid-serializer'; +import { NormalizedDSpaceObject } from '../normalized-dspace-object.model'; +import { NormalizedObject } from '../normalized-object.model'; + +@mapsTo(RelationshipType) +@inheritSerialization(NormalizedDSpaceObject) +export class NormalizedRelationshipType extends NormalizedObject { + @autoserialize + id: string; + + @autoserialize + leftLabel: string; + + @autoserialize + leftMaxCardinality: number; + + @autoserialize + leftMinCardinality: number; + + @autoserialize + rightLabel: string; + + @autoserialize + rightMaxCardinality: number; + + @autoserialize + rightMinCardinality: number; + + @autoserialize + @relationship(ResourceType.EntityType, false) + leftType: string; + + @autoserialize + @relationship(ResourceType.EntityType, false) + rightType: string; + + @autoserializeAs(new IDToUUIDSerializer(ResourceType.RelationshipType), 'id') + uuid: string; +} diff --git a/src/app/core/cache/models/entities/normalized-relationship.model.ts b/src/app/core/cache/models/entities/normalized-relationship.model.ts new file mode 100644 index 0000000000..48d33e5f01 --- /dev/null +++ b/src/app/core/cache/models/entities/normalized-relationship.model.ts @@ -0,0 +1,30 @@ +import { autoserialize, autoserializeAs, inheritSerialization } from 'cerialize'; +import { Relationship } from '../../../shared/entities/relationship.model'; +import { ResourceType } from '../../../shared/resource-type'; +import { mapsTo, relationship } from '../../builders/build-decorators'; +import { IDToUUIDSerializer } from '../../it-to-uuid-serializer'; +import { NormalizedObject } from '../normalized-object.model'; + +@mapsTo(Relationship) +@inheritSerialization(NormalizedObject) +export class NormalizedRelationship extends NormalizedObject { + + @autoserialize + id: string; + + @autoserialize + leftId: string; + + @autoserialize + place: number; + + @autoserialize + rightId: string; + + @autoserialize + @relationship(ResourceType.RelationshipType, false) + relationshipType: string; + + @autoserializeAs(new IDToUUIDSerializer(ResourceType.Relationship), 'id') + uuid: string; +} diff --git a/src/app/core/cache/models/normalized-item.model.ts b/src/app/core/cache/models/normalized-item.model.ts index d6e9165471..252e866be9 100644 --- a/src/app/core/cache/models/normalized-item.model.ts +++ b/src/app/core/cache/models/normalized-item.model.ts @@ -57,4 +57,8 @@ export class NormalizedItem extends NormalizedDSpaceObject { @relationship(ResourceType.Bitstream, true) bitstreams: string[]; + @autoserialize + @relationship(ResourceType.Relationship, true) + relationships: string[]; + } diff --git a/src/app/core/cache/models/normalized-object-factory.ts b/src/app/core/cache/models/normalized-object-factory.ts index df67a1f2ce..ae07e6c0dc 100644 --- a/src/app/core/cache/models/normalized-object-factory.ts +++ b/src/app/core/cache/models/normalized-object-factory.ts @@ -1,3 +1,6 @@ +import { NormalizedEntityType } from './entities/normalized-entity-type.model'; +import { NormalizedRelationshipType } from './entities/normalized-relationship-type.model'; +import { NormalizedRelationship } from './entities/normalized-relationship.model'; import { NormalizedBitstream } from './normalized-bitstream.model'; import { NormalizedBundle } from './normalized-bundle.model'; import { NormalizedItem } from './normalized-item.model'; @@ -33,6 +36,15 @@ export class NormalizedObjectFactory { case ResourceType.ResourcePolicy: { return NormalizedResourcePolicy } + case ResourceType.Relationship: { + return NormalizedRelationship + } + case ResourceType.RelationshipType: { + return NormalizedRelationshipType + } + case ResourceType.EntityType: { + return NormalizedEntityType + } default: { return undefined; } diff --git a/src/app/core/shared/entities/entity-type.model.ts b/src/app/core/shared/entities/entity-type.model.ts new file mode 100644 index 0000000000..93e60f3efc --- /dev/null +++ b/src/app/core/shared/entities/entity-type.model.ts @@ -0,0 +1,9 @@ +import { CacheableObject } from '../../cache/object-cache.reducer'; +import { ResourceType } from '../resource-type'; + +export class EntityType implements CacheableObject { + id: string; + self: string; + type: ResourceType; + uuid: string; +} diff --git a/src/app/core/shared/entities/relationship-type.model.ts b/src/app/core/shared/entities/relationship-type.model.ts new file mode 100644 index 0000000000..ff3abd2d1b --- /dev/null +++ b/src/app/core/shared/entities/relationship-type.model.ts @@ -0,0 +1,33 @@ +import { Observable } from 'rxjs/Observable'; +import { CacheableObject } from '../../cache/object-cache.reducer'; +import { RemoteData } from '../../data/remote-data'; +import { ResourceType } from '../resource-type'; +import { EntityType } from './entity-type.model'; + +export class RelationshipType implements CacheableObject { + self: string; + + type: ResourceType; + + label: string; + + id: string; + + uuid: string; + + leftLabel: string; + + leftMaxCardinality: number; + + leftMinCardinality: number; + + rightLabel: string; + + rightMaxCardinality: number; + + rightMinCardinality: number; + + leftType: Observable>; + + rightType: Observable>; +} diff --git a/src/app/core/shared/entities/relationship.model.ts b/src/app/core/shared/entities/relationship.model.ts new file mode 100644 index 0000000000..a26d6f5179 --- /dev/null +++ b/src/app/core/shared/entities/relationship.model.ts @@ -0,0 +1,23 @@ +import { Observable } from 'rxjs/Observable'; +import { CacheableObject } from '../../cache/object-cache.reducer'; +import { RemoteData } from '../../data/remote-data'; +import { ResourceType } from '../resource-type'; +import { RelationshipType } from './relationship-type.model'; + +export class Relationship implements CacheableObject { + self: string; + + type: ResourceType; + + uuid: string; + + id: string; + + leftId: string; + + place: string; + + rightId: string; + + relationshipType: Observable>; +} diff --git a/src/app/core/shared/item.model.ts b/src/app/core/shared/item.model.ts index cc84694e84..e8695c9e09 100644 --- a/src/app/core/shared/item.model.ts +++ b/src/app/core/shared/item.model.ts @@ -6,6 +6,7 @@ import { RemoteData } from '../data/remote-data'; import { Bitstream } from './bitstream.model'; import { hasValue, isNotEmpty } from '../../shared/empty.util'; import { PaginatedList } from '../data/paginated-list'; +import { Relationship } from './entities/relationship.model'; export class Item extends DSpaceObject { @@ -50,6 +51,8 @@ export class Item extends DSpaceObject { bitstreams: Observable>>; + relationships: Observable>>; + /** * Retrieves the thumbnail of this item * @returns {Observable} the primaryBitstream of the 'THUMBNAIL' bundle diff --git a/src/app/core/shared/operators.ts b/src/app/core/shared/operators.ts index c0b9be3fbf..17d5887443 100644 --- a/src/app/core/shared/operators.ts +++ b/src/app/core/shared/operators.ts @@ -1,6 +1,6 @@ import { Observable } from 'rxjs/Observable'; import { filter, flatMap, map, tap } from 'rxjs/operators'; -import { hasValueOperator } from '../../shared/empty.util'; +import { hasValue, hasValueOperator } from '../../shared/empty.util'; import { DSOSuccessResponse } from '../cache/response-cache.models'; import { ResponseCacheEntry } from '../cache/response-cache.reducer'; import { ResponseCacheService } from '../cache/response-cache.service'; diff --git a/src/app/core/shared/resource-type.ts b/src/app/core/shared/resource-type.ts index c7e41c5744..242e277187 100644 --- a/src/app/core/shared/resource-type.ts +++ b/src/app/core/shared/resource-type.ts @@ -6,5 +6,8 @@ export enum ResourceType { Item = 'item', Collection = 'collection', Community = 'community', - ResourcePolicy = 'resourcePolicy' + ResourcePolicy = 'resourcePolicy', + Relationship = 'relationship', + RelationshipType = 'relationshiptype', + EntityType = 'entitytype', } From 82977ce5cf9d71695309c45775c83057d287c5ef Mon Sep 17 00:00:00 2001 From: Art Lowel Date: Wed, 30 May 2018 10:27:59 +0200 Subject: [PATCH 005/604] optimized and refactored --- .../person/person-page-fields.component.ts | 122 ++++++++++-------- 1 file changed, 70 insertions(+), 52 deletions(-) diff --git a/src/app/+item-page/simple/entity-types/person/person-page-fields.component.ts b/src/app/+item-page/simple/entity-types/person/person-page-fields.component.ts index 6599ad580e..7e93f7a007 100644 --- a/src/app/+item-page/simple/entity-types/person/person-page-fields.component.ts +++ b/src/app/+item-page/simple/entity-types/person/person-page-fields.component.ts @@ -1,9 +1,6 @@ -import { Component, Inject } from '@angular/core'; -import ensureArray from 'rollup/dist/typings/utils/ensureArray'; +import { Component, Inject, OnInit } from '@angular/core'; import { Observable } from 'rxjs/Observable'; -import { filter, flatMap, map } from 'rxjs/operators'; -import { relationship } from '../../../../core/cache/builders/build-decorators'; -import { RemoteDataBuildService } from '../../../../core/cache/builders/remote-data-build.service'; +import { distinctUntilChanged, filter, flatMap, map } from 'rxjs/operators'; import { ItemDataService } from '../../../../core/data/item-data.service'; import { PaginatedList } from '../../../../core/data/paginated-list'; import { RemoteData } from '../../../../core/data/remote-data'; @@ -11,41 +8,92 @@ import { RelationshipType } from '../../../../core/shared/entities/relationship- import { Relationship } from '../../../../core/shared/entities/relationship.model'; import { Item } from '../../../../core/shared/item.model'; import { getRemoteDataPayload } from '../../../../core/shared/operators'; -import { ensureArrayHasValue, hasValue } from '../../../../shared/empty.util'; +import { hasValue } from '../../../../shared/empty.util'; import { rendersEntityType } from '../../../../shared/entities/entity-type-decorator'; import { ElementViewMode } from '../../../../shared/view-mode'; import { ITEM } from '../switcher/entity-type-switcher.component'; +const compareArraysUsing = (mapFn: (t: T) => any) => + (a: T[], b: T[]): boolean => { + if (!Array.isArray(a) || ! Array.isArray(b)) { + return false + } + + const aIds = a.map(mapFn); + const bIds = b.map(mapFn); + + return aIds.length === bIds.length && + aIds.every((e) => bIds.includes(e)) && + bIds.every((e) => aIds.includes(e)); + }; + +const filterRelationsByTypeLabel = (label: string) => + (source: Observable<[Relationship[], RelationshipType[]]>): Observable => + source.pipe( + map(([relsCurrentPage, relTypesCurrentPage]) => + relsCurrentPage.filter((rel: Relationship, idx: number) => + hasValue(relTypesCurrentPage[idx]) && (relTypesCurrentPage[idx].leftLabel === label || + relTypesCurrentPage[idx].rightLabel === label) + ) + ), + distinctUntilChanged(compareArraysUsing((e: Relationship) => hasValue(e) ? e.id : undefined)) + ); + +const relationsToItems = (thisId: string, ids: ItemDataService) => + (source: Observable): Observable => + source.pipe( + flatMap((rels: Relationship[]) => + Observable.zip( + ...rels.map((rel: Relationship) => { + let queryId = rel.leftId; + if (rel.leftId === thisId) { + queryId = rel.rightId; + } + return ids.findById(queryId); + }) + ) + ), + map((arr: Array>) => + arr + .filter((d: RemoteData) => d.hasSucceeded) + .map((d: RemoteData) => d.payload)), + distinctUntilChanged(compareArraysUsing((rdi: Item) => hasValue(rdi) ? rdi.uuid : undefined)), + ); + + @rendersEntityType('Person', ElementViewMode.Full) @Component({ selector: 'ds-person-page-fields', styleUrls: ['./person-page-fields.component.scss'], templateUrl: './person-page-fields.component.html' }) -export class PersonPageFieldsComponent { +export class PersonPageFieldsComponent implements OnInit { publications$: Observable; - isProjectOfPersonRels$: Observable; - isOrgUnitOfPersonRels$: Observable; + projects$: Observable; + orgUnits$: Observable; constructor( @Inject(ITEM) public item: Item, private ids: ItemDataService - ) { - const relsCurrentPage$ = item.relationships.pipe( + ) {} + + ngOnInit(): void { + const relsCurrentPage$ = this.item.relationships.pipe( filter((rd: RemoteData>) => rd.hasSucceeded), getRemoteDataPayload(), map((pl: PaginatedList) => pl.page), - ensureArrayHasValue(), + distinctUntilChanged(compareArraysUsing((e: Relationship) => hasValue(e) ? e.id : undefined)) ); const relTypesCurrentPage$ = relsCurrentPage$.pipe( flatMap((rels: Relationship[]) => - Observable.combineLatest( + Observable.zip( ...rels.map((rel: Relationship) => rel.relationshipType), (...arr: Array>) => arr.map((d: RemoteData) => d.payload) ) - ) + ), + distinctUntilChanged(compareArraysUsing((e: RelationshipType) => hasValue(e) ? e.id : undefined)) ); const resolvedRelsAndTypes$ = Observable.combineLatest( @@ -54,49 +102,19 @@ export class PersonPageFieldsComponent { ); this.publications$ = resolvedRelsAndTypes$.pipe( - map(([relsCurrentPage, relTypesCurrentPage]) => - relsCurrentPage.filter((rel: Relationship, idx: number) => - hasValue(relTypesCurrentPage[idx]) && (relTypesCurrentPage[idx].leftLabel === 'isPublicationOfAuthor' || - relTypesCurrentPage[idx].rightLabel === 'isPublicationOfAuthor') - ) - ), - flatMap((rels: Relationship[]) => - Observable.combineLatest( - ...rels.map((rel: Relationship) => { - let queryId = rel.leftId; - if (rel.leftId === this.item.id) { - queryId = rel.rightId; - } - return this.ids.findById(queryId); - }), - (...arr: Array>) => - arr - .filter((d: RemoteData) => d.hasSucceeded) - .map((d: RemoteData) => d.payload) - ) - ) + filterRelationsByTypeLabel('isPublicationOfAuthor'), + relationsToItems(this.item.id, this.ids) ); - //TODO status het lijkt te werken maar duurt minuten om te laden: too much recursion? - - this.isProjectOfPersonRels$ = resolvedRelsAndTypes$.pipe( - map(([relsCurrentPage, relTypesCurrentPage]) => - relsCurrentPage.filter((rel: Relationship, idx: number) => - hasValue(relTypesCurrentPage[idx]) && (relTypesCurrentPage[idx].leftLabel === 'isProjectOfPerson' || - relTypesCurrentPage[idx].rightLabel === 'isProjectOfPerson') - ) - ) + this.projects$ = resolvedRelsAndTypes$.pipe( + filterRelationsByTypeLabel('isProjectOfPerson'), + relationsToItems(this.item.id, this.ids) ); - this.isOrgUnitOfPersonRels$ = resolvedRelsAndTypes$.pipe( - map(([relsCurrentPage, relTypesCurrentPage]) => - relsCurrentPage.filter((rel: Relationship, idx: number) => - hasValue(relTypesCurrentPage[idx]) && (relTypesCurrentPage[idx].leftLabel === 'isOrgUnitOfPerson' || - relTypesCurrentPage[idx].rightLabel === 'isOrgUnitOfPerson') - ) - ) + this.orgUnits$ = resolvedRelsAndTypes$.pipe( + filterRelationsByTypeLabel('isOrgUnitOfPerson'), + relationsToItems(this.item.id, this.ids) ); - } } From f1b35577b2da9deeaa885d9536a5dac01f34f5f8 Mon Sep 17 00:00:00 2001 From: Art Lowel Date: Wed, 30 May 2018 10:57:33 +0200 Subject: [PATCH 006/604] refactor --- .../person/person-page-fields.component.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/app/+item-page/simple/entity-types/person/person-page-fields.component.ts b/src/app/+item-page/simple/entity-types/person/person-page-fields.component.ts index 7e93f7a007..058ca8a7cc 100644 --- a/src/app/+item-page/simple/entity-types/person/person-page-fields.component.ts +++ b/src/app/+item-page/simple/entity-types/person/person-page-fields.component.ts @@ -27,6 +27,9 @@ const compareArraysUsing = (mapFn: (t: T) => any) => bIds.every((e) => aIds.includes(e)); }; +const compareArraysUsingIds = () => + compareArraysUsing((t: T) => hasValue(t) ? t.id : undefined); + const filterRelationsByTypeLabel = (label: string) => (source: Observable<[Relationship[], RelationshipType[]]>): Observable => source.pipe( @@ -36,7 +39,7 @@ const filterRelationsByTypeLabel = (label: string) => relTypesCurrentPage[idx].rightLabel === label) ) ), - distinctUntilChanged(compareArraysUsing((e: Relationship) => hasValue(e) ? e.id : undefined)) + distinctUntilChanged(compareArraysUsingIds()) ); const relationsToItems = (thisId: string, ids: ItemDataService) => @@ -57,7 +60,7 @@ const relationsToItems = (thisId: string, ids: ItemDataService) => arr .filter((d: RemoteData) => d.hasSucceeded) .map((d: RemoteData) => d.payload)), - distinctUntilChanged(compareArraysUsing((rdi: Item) => hasValue(rdi) ? rdi.uuid : undefined)), + distinctUntilChanged(compareArraysUsingIds()), ); @@ -82,7 +85,7 @@ export class PersonPageFieldsComponent implements OnInit { filter((rd: RemoteData>) => rd.hasSucceeded), getRemoteDataPayload(), map((pl: PaginatedList) => pl.page), - distinctUntilChanged(compareArraysUsing((e: Relationship) => hasValue(e) ? e.id : undefined)) + distinctUntilChanged(compareArraysUsingIds()) ); const relTypesCurrentPage$ = relsCurrentPage$.pipe( @@ -93,7 +96,7 @@ export class PersonPageFieldsComponent implements OnInit { arr.map((d: RemoteData) => d.payload) ) ), - distinctUntilChanged(compareArraysUsing((e: RelationshipType) => hasValue(e) ? e.id : undefined)) + distinctUntilChanged(compareArraysUsingIds()) ); const resolvedRelsAndTypes$ = Observable.combineLatest( From e5fc17da76689d2690493dfd6baab6e98ba19c91 Mon Sep 17 00:00:00 2001 From: Art Lowel Date: Wed, 30 May 2018 15:22:43 +0200 Subject: [PATCH 007/604] w2p 51849 - display entities as list results --- src/app/+item-page/item-page.module.ts | 2 - .../item/item-page-fields.component.ts | 2 +- .../orgunit/orgunit-page-fields.component.ts | 2 +- .../person/person-page-fields.component.ts | 3 +- .../project/project-page-fields.component.ts | 2 +- .../entity-type-switcher.component.scss | 1 - .../entity-type-switcher.component.ts | 36 --------- .../simple/item-page.component.html | 2 +- src/app/+search-page/search-result.model.ts | 1 + .../entity-type-switcher.component.html | 0 .../entity-type-switcher.component.scss} | 0 .../entity-type-switcher.component.ts | 44 +++++++++++ .../entity-list-element.component.html | 1 + .../entity-list-element.component.scss | 1 + ... => entity-list-element.component.spec.ts} | 14 ++-- ...nt.ts => entity-list-element.component.ts} | 15 ++-- .../entity-search-result-component.ts | 74 +++++++++++++++++++ .../item/item-list-element.component.html | 24 ++++++ .../item/item-list-element.component.scss | 1 + .../item/item-list-element.component.ts | 15 ++++ .../orgunit-list-element.component.html | 16 ++++ .../orgunit-list-element.component.scss | 1 + .../orgunit/orgunit-list-element.component.ts | 14 ++++ .../person/person-list-element.component.html | 16 ++++ .../person/person-list-element.component.scss | 1 + .../person/person-list-element.component.ts | 14 ++++ .../project-list-element.component.html | 16 ++++ .../project-list-element.component.scss | 1 + .../project/project-list-element.component.ts | 14 ++++ .../item-list-element.component.html | 18 ----- ...-search-result-list-element.component.html | 25 +------ ...em-search-result-list-element.component.ts | 11 +-- src/app/shared/shared.module.ts | 16 +++- src/app/shared/view-mode.ts | 2 +- 34 files changed, 297 insertions(+), 108 deletions(-) delete mode 100644 src/app/+item-page/simple/entity-types/switcher/entity-type-switcher.component.scss delete mode 100644 src/app/+item-page/simple/entity-types/switcher/entity-type-switcher.component.ts rename src/app/{+item-page/simple/entity-types => shared/entities}/switcher/entity-type-switcher.component.html (100%) rename src/app/shared/{object-list/item-list-element/item-list-element.component.scss => entities/switcher/entity-type-switcher.component.scss} (100%) create mode 100644 src/app/shared/entities/switcher/entity-type-switcher.component.ts create mode 100644 src/app/shared/object-list/item-list-element/entity-list-element.component.html create mode 100644 src/app/shared/object-list/item-list-element/entity-list-element.component.scss rename src/app/shared/object-list/item-list-element/{item-list-element.component.spec.ts => entity-list-element.component.spec.ts} (87%) rename src/app/shared/object-list/item-list-element/{item-list-element.component.ts => entity-list-element.component.ts} (51%) create mode 100644 src/app/shared/object-list/item-list-element/entity-types/entity-search-result-component.ts create mode 100644 src/app/shared/object-list/item-list-element/entity-types/item/item-list-element.component.html create mode 100644 src/app/shared/object-list/item-list-element/entity-types/item/item-list-element.component.scss create mode 100644 src/app/shared/object-list/item-list-element/entity-types/item/item-list-element.component.ts create mode 100644 src/app/shared/object-list/item-list-element/entity-types/orgunit/orgunit-list-element.component.html create mode 100644 src/app/shared/object-list/item-list-element/entity-types/orgunit/orgunit-list-element.component.scss create mode 100644 src/app/shared/object-list/item-list-element/entity-types/orgunit/orgunit-list-element.component.ts create mode 100644 src/app/shared/object-list/item-list-element/entity-types/person/person-list-element.component.html create mode 100644 src/app/shared/object-list/item-list-element/entity-types/person/person-list-element.component.scss create mode 100644 src/app/shared/object-list/item-list-element/entity-types/person/person-list-element.component.ts create mode 100644 src/app/shared/object-list/item-list-element/entity-types/project/project-list-element.component.html create mode 100644 src/app/shared/object-list/item-list-element/entity-types/project/project-list-element.component.scss create mode 100644 src/app/shared/object-list/item-list-element/entity-types/project/project-list-element.component.ts delete mode 100644 src/app/shared/object-list/item-list-element/item-list-element.component.html diff --git a/src/app/+item-page/item-page.module.ts b/src/app/+item-page/item-page.module.ts index fdf9d19674..62253a3183 100644 --- a/src/app/+item-page/item-page.module.ts +++ b/src/app/+item-page/item-page.module.ts @@ -23,7 +23,6 @@ import { ItemPageFieldsComponent } from './simple/entity-types/item/item-page-fi import { OrgUnitPageFieldsComponent } from './simple/entity-types/orgunit/orgunit-page-fields.component'; import { PersonPageFieldsComponent } from './simple/entity-types/person/person-page-fields.component'; import { ProjectPageFieldsComponent } from './simple/entity-types/project/project-page-fields.component'; -import { EntityTypeSwitcherComponent } from './simple/entity-types/switcher/entity-type-switcher.component'; @NgModule({ imports: [ @@ -46,7 +45,6 @@ import { EntityTypeSwitcherComponent } from './simple/entity-types/switcher/enti FileSectionComponent, CollectionsComponent, FullFileSectionComponent, - EntityTypeSwitcherComponent, ItemPageFieldsComponent, ProjectPageFieldsComponent, OrgUnitPageFieldsComponent, diff --git a/src/app/+item-page/simple/entity-types/item/item-page-fields.component.ts b/src/app/+item-page/simple/entity-types/item/item-page-fields.component.ts index 90504812d7..4a5469c3c2 100644 --- a/src/app/+item-page/simple/entity-types/item/item-page-fields.component.ts +++ b/src/app/+item-page/simple/entity-types/item/item-page-fields.component.ts @@ -5,7 +5,7 @@ import { rendersEntityType } from '../../../../shared/entities/entity-type-decorator'; import { ElementViewMode } from '../../../../shared/view-mode'; -import { ITEM } from '../switcher/entity-type-switcher.component'; +import { ITEM } from '../../../../shared/entities/switcher/entity-type-switcher.component'; @rendersEntityType('Item', ElementViewMode.Full) @rendersEntityType(DEFAULT_ENTITY_TYPE, ElementViewMode.Full) diff --git a/src/app/+item-page/simple/entity-types/orgunit/orgunit-page-fields.component.ts b/src/app/+item-page/simple/entity-types/orgunit/orgunit-page-fields.component.ts index 97a0d4f477..74e75e1dc9 100644 --- a/src/app/+item-page/simple/entity-types/orgunit/orgunit-page-fields.component.ts +++ b/src/app/+item-page/simple/entity-types/orgunit/orgunit-page-fields.component.ts @@ -2,7 +2,7 @@ import { Component, Inject } from '@angular/core'; import { Item } from '../../../../core/shared/item.model'; import { rendersEntityType } from '../../../../shared/entities/entity-type-decorator'; import { ElementViewMode } from '../../../../shared/view-mode'; -import { ITEM } from '../switcher/entity-type-switcher.component'; +import { ITEM } from '../../../../shared/entities/switcher/entity-type-switcher.component'; @rendersEntityType('OrgUnit', ElementViewMode.Full) @Component({ diff --git a/src/app/+item-page/simple/entity-types/person/person-page-fields.component.ts b/src/app/+item-page/simple/entity-types/person/person-page-fields.component.ts index 058ca8a7cc..dce8fe5dbe 100644 --- a/src/app/+item-page/simple/entity-types/person/person-page-fields.component.ts +++ b/src/app/+item-page/simple/entity-types/person/person-page-fields.component.ts @@ -11,7 +11,7 @@ import { getRemoteDataPayload } from '../../../../core/shared/operators'; import { hasValue } from '../../../../shared/empty.util'; import { rendersEntityType } from '../../../../shared/entities/entity-type-decorator'; import { ElementViewMode } from '../../../../shared/view-mode'; -import { ITEM } from '../switcher/entity-type-switcher.component'; +import { ITEM } from '../../../../shared/entities/switcher/entity-type-switcher.component'; const compareArraysUsing = (mapFn: (t: T) => any) => (a: T[], b: T[]): boolean => { @@ -63,7 +63,6 @@ const relationsToItems = (thisId: string, ids: ItemDataService) => distinctUntilChanged(compareArraysUsingIds()), ); - @rendersEntityType('Person', ElementViewMode.Full) @Component({ selector: 'ds-person-page-fields', diff --git a/src/app/+item-page/simple/entity-types/project/project-page-fields.component.ts b/src/app/+item-page/simple/entity-types/project/project-page-fields.component.ts index f6e93dcfa6..d648247a58 100644 --- a/src/app/+item-page/simple/entity-types/project/project-page-fields.component.ts +++ b/src/app/+item-page/simple/entity-types/project/project-page-fields.component.ts @@ -2,7 +2,7 @@ import { Component, Inject } from '@angular/core'; import { Item } from '../../../../core/shared/item.model'; import { rendersEntityType } from '../../../../shared/entities/entity-type-decorator'; import { ElementViewMode } from '../../../../shared/view-mode'; -import { ITEM } from '../switcher/entity-type-switcher.component'; +import { ITEM } from '../../../../shared/entities/switcher/entity-type-switcher.component'; @rendersEntityType('Project', ElementViewMode.Full) @Component({ diff --git a/src/app/+item-page/simple/entity-types/switcher/entity-type-switcher.component.scss b/src/app/+item-page/simple/entity-types/switcher/entity-type-switcher.component.scss deleted file mode 100644 index 3575cae797..0000000000 --- a/src/app/+item-page/simple/entity-types/switcher/entity-type-switcher.component.scss +++ /dev/null @@ -1 +0,0 @@ -@import '../../../../../styles/variables.scss'; diff --git a/src/app/+item-page/simple/entity-types/switcher/entity-type-switcher.component.ts b/src/app/+item-page/simple/entity-types/switcher/entity-type-switcher.component.ts deleted file mode 100644 index d2e2374e8e..0000000000 --- a/src/app/+item-page/simple/entity-types/switcher/entity-type-switcher.component.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { Component, InjectionToken, Injector, Input, OnInit } from '@angular/core'; -import { GenericConstructor } from '../../../../core/shared/generic-constructor'; -import { Item } from '../../../../core/shared/item.model'; -import { getComponentByEntityType } from '../../../../shared/entities/entity-type-decorator'; -import { rendersDSOType } from '../../../../shared/object-collection/shared/dso-element-decorator'; -import { ListableObject } from '../../../../shared/object-collection/shared/listable-object.model'; -import { ElementViewMode, SetViewMode } from '../../../../shared/view-mode'; - -export const ITEM: InjectionToken = new InjectionToken('item'); - -@Component({ - selector: 'ds-entity-type-switcher', - styleUrls: ['./entity-type-switcher.component.scss'], - templateUrl: './entity-type-switcher.component.html' -}) -export class EntityTypeSwitcherComponent implements OnInit { - @Input() item: Item; - @Input() viewMode: ElementViewMode; - objectInjector: Injector; - - constructor(private injector: Injector) { - } - - ngOnInit(): void { - this.objectInjector = Injector.create({ - providers: [{ provide: ITEM, useFactory: () => this.item, deps:[] }], - parent: this.injector - }); - - } - - getComponent(): string { - const type = this.item.findMetadata('relationship.type'); - return getComponentByEntityType(type, this.viewMode); - } -} diff --git a/src/app/+item-page/simple/item-page.component.html b/src/app/+item-page/simple/item-page.component.html index 7c75887f38..17d271f368 100644 --- a/src/app/+item-page/simple/item-page.component.html +++ b/src/app/+item-page/simple/item-page.component.html @@ -1,7 +1,7 @@
- +
diff --git a/src/app/+search-page/search-result.model.ts b/src/app/+search-page/search-result.model.ts index cc2bd8cd58..2298f453e1 100644 --- a/src/app/+search-page/search-result.model.ts +++ b/src/app/+search-page/search-result.model.ts @@ -1,5 +1,6 @@ import { DSpaceObject } from '../core/shared/dspace-object.model'; import { Metadatum } from '../core/shared/metadatum.model'; +import { hasNoValue, isEmpty } from '../shared/empty.util'; import { ListableObject } from '../shared/object-collection/shared/listable-object.model'; export class SearchResult implements ListableObject { diff --git a/src/app/+item-page/simple/entity-types/switcher/entity-type-switcher.component.html b/src/app/shared/entities/switcher/entity-type-switcher.component.html similarity index 100% rename from src/app/+item-page/simple/entity-types/switcher/entity-type-switcher.component.html rename to src/app/shared/entities/switcher/entity-type-switcher.component.html diff --git a/src/app/shared/object-list/item-list-element/item-list-element.component.scss b/src/app/shared/entities/switcher/entity-type-switcher.component.scss similarity index 100% rename from src/app/shared/object-list/item-list-element/item-list-element.component.scss rename to src/app/shared/entities/switcher/entity-type-switcher.component.scss diff --git a/src/app/shared/entities/switcher/entity-type-switcher.component.ts b/src/app/shared/entities/switcher/entity-type-switcher.component.ts new file mode 100644 index 0000000000..a981f2665c --- /dev/null +++ b/src/app/shared/entities/switcher/entity-type-switcher.component.ts @@ -0,0 +1,44 @@ +import { Component, InjectionToken, Injector, Input, OnInit } from '@angular/core'; +import { SearchResult } from '../../../+search-page/search-result.model'; +import { Item } from '../../../core/shared/item.model'; +import { hasValue } from '../../empty.util'; +import { ItemSearchResult } from '../../object-collection/shared/item-search-result.model'; +import { getComponentByEntityType } from '../entity-type-decorator'; +import { ElementViewMode } from '../../view-mode'; + +export const ITEM: InjectionToken = new InjectionToken('item'); + +@Component({ + selector: 'ds-entity-type-switcher', + styleUrls: ['./entity-type-switcher.component.scss'], + templateUrl: './entity-type-switcher.component.html' +}) +export class EntityTypeSwitcherComponent implements OnInit { + @Input() object: Item | SearchResult; + @Input() viewMode: ElementViewMode; + objectInjector: Injector; + + constructor(private injector: Injector) { + } + + ngOnInit(): void { + this.objectInjector = Injector.create({ + providers: [{ provide: ITEM, useFactory: () => this.object, deps:[] }], + parent: this.injector + }); + + } + + getComponent(): string { + let item: Item; + if (hasValue((this.object as any).dspaceObject)) { + const searchResult = this.object as ItemSearchResult; + item = searchResult.dspaceObject; + } else { + item = this.object as Item; + } + + const type = item.findMetadata('relationship.type'); + return getComponentByEntityType(type, this.viewMode); + } +} diff --git a/src/app/shared/object-list/item-list-element/entity-list-element.component.html b/src/app/shared/object-list/item-list-element/entity-list-element.component.html new file mode 100644 index 0000000000..777f131a23 --- /dev/null +++ b/src/app/shared/object-list/item-list-element/entity-list-element.component.html @@ -0,0 +1 @@ + diff --git a/src/app/shared/object-list/item-list-element/entity-list-element.component.scss b/src/app/shared/object-list/item-list-element/entity-list-element.component.scss new file mode 100644 index 0000000000..45a533cd01 --- /dev/null +++ b/src/app/shared/object-list/item-list-element/entity-list-element.component.scss @@ -0,0 +1 @@ +@import '../../../../styles/variables'; diff --git a/src/app/shared/object-list/item-list-element/item-list-element.component.spec.ts b/src/app/shared/object-list/item-list-element/entity-list-element.component.spec.ts similarity index 87% rename from src/app/shared/object-list/item-list-element/item-list-element.component.spec.ts rename to src/app/shared/object-list/item-list-element/entity-list-element.component.spec.ts index fc40527693..8ff375c1ea 100644 --- a/src/app/shared/object-list/item-list-element/item-list-element.component.spec.ts +++ b/src/app/shared/object-list/item-list-element/entity-list-element.component.spec.ts @@ -1,4 +1,4 @@ -import { ItemListElementComponent } from './item-list-element.component'; +import { EntityListElementComponent } from './entity-list-element.component'; import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core'; import { By } from '@angular/platform-browser'; @@ -6,8 +6,8 @@ import { TruncatePipe } from '../../utils/truncate.pipe'; import { Item } from '../../../core/shared/item.model'; import { Observable } from 'rxjs/Observable'; -let itemListElementComponent: ItemListElementComponent; -let fixture: ComponentFixture; +let itemListElementComponent: EntityListElementComponent; +let fixture: ComponentFixture; const mockItemWithAuthorAndDate: Item = Object.assign(new Item(), { bitstreams: Observable.of({}), @@ -38,22 +38,22 @@ const mockItemWithoutAuthorAndDate: Item = Object.assign(new Item(), { }] }); -describe('ItemListElementComponent', () => { +describe('EntityListElementComponent', () => { beforeEach(async(() => { TestBed.configureTestingModule({ - declarations: [ ItemListElementComponent , TruncatePipe], + declarations: [ EntityListElementComponent , TruncatePipe], providers: [ { provide: 'objectElementProvider', useValue: {mockItemWithAuthorAndDate}} ], schemas: [ NO_ERRORS_SCHEMA ] - }).overrideComponent(ItemListElementComponent, { + }).overrideComponent(EntityListElementComponent, { set: { changeDetection: ChangeDetectionStrategy.Default } }).compileComponents(); })); beforeEach(async(() => { - fixture = TestBed.createComponent(ItemListElementComponent); + fixture = TestBed.createComponent(EntityListElementComponent); itemListElementComponent = fixture.componentInstance; })); diff --git a/src/app/shared/object-list/item-list-element/item-list-element.component.ts b/src/app/shared/object-list/item-list-element/entity-list-element.component.ts similarity index 51% rename from src/app/shared/object-list/item-list-element/item-list-element.component.ts rename to src/app/shared/object-list/item-list-element/entity-list-element.component.ts index 8dd09f71b9..0a284708a9 100644 --- a/src/app/shared/object-list/item-list-element/item-list-element.component.ts +++ b/src/app/shared/object-list/item-list-element/entity-list-element.component.ts @@ -1,15 +1,18 @@ -import { Component, Input, Inject } from '@angular/core'; +import { Component } from '@angular/core'; import { Item } from '../../../core/shared/item.model'; -import { AbstractListableElementComponent } from '../../object-collection/shared/object-collection-element/abstract-listable-element.component'; +import * as viewMode from '../../../shared/view-mode'; import { renderElementsFor } from '../../object-collection/shared/dso-element-decorator'; +import { AbstractListableElementComponent } from '../../object-collection/shared/object-collection-element/abstract-listable-element.component'; import { SetViewMode } from '../../view-mode'; @Component({ - selector: 'ds-item-list-element', - styleUrls: ['./item-list-element.component.scss'], - templateUrl: './item-list-element.component.html' + selector: 'ds-entity-list-element', + styleUrls: ['./entity-list-element.component.scss'], + templateUrl: './entity-list-element.component.html' }) @renderElementsFor(Item, SetViewMode.List) -export class ItemListElementComponent extends AbstractListableElementComponent {} +export class EntityListElementComponent extends AbstractListableElementComponent { + ElementViewMode = viewMode.ElementViewMode; +} diff --git a/src/app/shared/object-list/item-list-element/entity-types/entity-search-result-component.ts b/src/app/shared/object-list/item-list-element/entity-types/entity-search-result-component.ts new file mode 100644 index 0000000000..bcf2e45a8c --- /dev/null +++ b/src/app/shared/object-list/item-list-element/entity-types/entity-search-result-component.ts @@ -0,0 +1,74 @@ +import { Component, Inject } from '@angular/core'; +import { Observable } from 'rxjs/Observable'; +import { Item } from '../../../../core/shared/item.model'; +import { Metadatum } from '../../../../core/shared/metadatum.model'; +import { hasNoValue, hasValue, isEmpty } from '../../../empty.util'; +import { ITEM } from '../../../entities/switcher/entity-type-switcher.component'; +import { ItemSearchResult } from '../../../object-collection/shared/item-search-result.model'; +import { TruncatableService } from '../../../truncatable/truncatable.service'; + +// TODO lot of overlap with SearchResultListElementComponent => refactor! +@Component({ + selector: 'ds-entity-search-result', + template: '' +}) +export class EntitySearchResultComponent { + item: Item; + searchResult: ItemSearchResult; + + constructor( + private truncatableService: TruncatableService, + @Inject(ITEM) public object: Item | ItemSearchResult, + ) { + + if (hasValue((this.object as any).dspaceObject)) { + this.searchResult = this.object as ItemSearchResult; + this.item = this.searchResult.dspaceObject; + } else { + this.searchResult = { + dspaceObject: this.object as Item, + hitHighlights: [] + }; + this.item = this.object as Item; + } + } + + getValues(keys: string[]): string[] { + const results: string[] = new Array(); + this.searchResult.hitHighlights.forEach( + (md: Metadatum) => { + if (keys.indexOf(md.key) > -1) { + results.push(md.value); + } + } + ); + if (isEmpty(results)) { + this.item.filterMetadata(keys).forEach( + (md: Metadatum) => { + results.push(md.value); + } + ); + } + return results; + } + + getFirstValue(key: string): string { + let result: string; + this.searchResult.hitHighlights.some( + (md: Metadatum) => { + if (key === md.key) { + result = md.value; + return true; + } + } + ); + if (hasNoValue(result)) { + result = this.item.findMetadata(key); + } + return result; + } + + isCollapsed(): Observable { + return this.truncatableService.isCollapsed(this.item.id); + } +} diff --git a/src/app/shared/object-list/item-list-element/entity-types/item/item-list-element.component.html b/src/app/shared/object-list/item-list-element/entity-types/item/item-list-element.component.html new file mode 100644 index 0000000000..b5c125afaf --- /dev/null +++ b/src/app/shared/object-list/item-list-element/entity-types/item/item-list-element.component.html @@ -0,0 +1,24 @@ + + + + + (, ) + + + + + + + +
+ + +
+
diff --git a/src/app/shared/object-list/item-list-element/entity-types/item/item-list-element.component.scss b/src/app/shared/object-list/item-list-element/entity-types/item/item-list-element.component.scss new file mode 100644 index 0000000000..5ab410dcb0 --- /dev/null +++ b/src/app/shared/object-list/item-list-element/entity-types/item/item-list-element.component.scss @@ -0,0 +1 @@ +@import '../../../../../../styles/variables'; diff --git a/src/app/shared/object-list/item-list-element/entity-types/item/item-list-element.component.ts b/src/app/shared/object-list/item-list-element/entity-types/item/item-list-element.component.ts new file mode 100644 index 0000000000..db68ecec7b --- /dev/null +++ b/src/app/shared/object-list/item-list-element/entity-types/item/item-list-element.component.ts @@ -0,0 +1,15 @@ +import { Component } from '@angular/core'; +import { DEFAULT_ENTITY_TYPE, rendersEntityType } from '../../../../entities/entity-type-decorator'; +import { ElementViewMode } from '../../../../view-mode'; +import { EntitySearchResultComponent } from '../entity-search-result-component'; + +@rendersEntityType('Item', ElementViewMode.SetElement) +@rendersEntityType(DEFAULT_ENTITY_TYPE, ElementViewMode.SetElement) +@Component({ + selector: 'ds-item-list-element', + styleUrls: ['./item-list-element.component.scss'], + templateUrl: './item-list-element.component.html' +}) + +export class ItemListElementComponent extends EntitySearchResultComponent { +} diff --git a/src/app/shared/object-list/item-list-element/entity-types/orgunit/orgunit-list-element.component.html b/src/app/shared/object-list/item-list-element/entity-types/orgunit/orgunit-list-element.component.html new file mode 100644 index 0000000000..901d521033 --- /dev/null +++ b/src/app/shared/object-list/item-list-element/entity-types/orgunit/orgunit-list-element.component.html @@ -0,0 +1,16 @@ + + + + + + + + + + + + diff --git a/src/app/shared/object-list/item-list-element/entity-types/orgunit/orgunit-list-element.component.scss b/src/app/shared/object-list/item-list-element/entity-types/orgunit/orgunit-list-element.component.scss new file mode 100644 index 0000000000..5ab410dcb0 --- /dev/null +++ b/src/app/shared/object-list/item-list-element/entity-types/orgunit/orgunit-list-element.component.scss @@ -0,0 +1 @@ +@import '../../../../../../styles/variables'; diff --git a/src/app/shared/object-list/item-list-element/entity-types/orgunit/orgunit-list-element.component.ts b/src/app/shared/object-list/item-list-element/entity-types/orgunit/orgunit-list-element.component.ts new file mode 100644 index 0000000000..9ae7489e97 --- /dev/null +++ b/src/app/shared/object-list/item-list-element/entity-types/orgunit/orgunit-list-element.component.ts @@ -0,0 +1,14 @@ +import { Component } from '@angular/core'; +import { rendersEntityType } from '../../../../entities/entity-type-decorator'; +import { ElementViewMode } from '../../../../view-mode'; +import { EntitySearchResultComponent } from '../entity-search-result-component'; + +@rendersEntityType('OrgUnit', ElementViewMode.SetElement) +@Component({ + selector: 'ds-orgunit-list-element', + styleUrls: ['./orgunit-list-element.component.scss'], + templateUrl: './orgunit-list-element.component.html' +}) + +export class OrgUnitListElementComponent extends EntitySearchResultComponent { +} diff --git a/src/app/shared/object-list/item-list-element/entity-types/person/person-list-element.component.html b/src/app/shared/object-list/item-list-element/entity-types/person/person-list-element.component.html new file mode 100644 index 0000000000..81e9300bb3 --- /dev/null +++ b/src/app/shared/object-list/item-list-element/entity-types/person/person-list-element.component.html @@ -0,0 +1,16 @@ + + + + + + + + + + + + + diff --git a/src/app/shared/object-list/item-list-element/entity-types/person/person-list-element.component.scss b/src/app/shared/object-list/item-list-element/entity-types/person/person-list-element.component.scss new file mode 100644 index 0000000000..5ab410dcb0 --- /dev/null +++ b/src/app/shared/object-list/item-list-element/entity-types/person/person-list-element.component.scss @@ -0,0 +1 @@ +@import '../../../../../../styles/variables'; diff --git a/src/app/shared/object-list/item-list-element/entity-types/person/person-list-element.component.ts b/src/app/shared/object-list/item-list-element/entity-types/person/person-list-element.component.ts new file mode 100644 index 0000000000..b1156b1372 --- /dev/null +++ b/src/app/shared/object-list/item-list-element/entity-types/person/person-list-element.component.ts @@ -0,0 +1,14 @@ +import { Component } from '@angular/core'; +import { rendersEntityType } from '../../../../entities/entity-type-decorator'; +import { ElementViewMode } from '../../../../view-mode'; +import { EntitySearchResultComponent } from '../entity-search-result-component'; + +@rendersEntityType('Person', ElementViewMode.SetElement) +@Component({ + selector: 'ds-person-list-element', + styleUrls: ['./person-list-element.component.scss'], + templateUrl: './person-list-element.component.html' +}) + +export class PersonListElementComponent extends EntitySearchResultComponent { +} diff --git a/src/app/shared/object-list/item-list-element/entity-types/project/project-list-element.component.html b/src/app/shared/object-list/item-list-element/entity-types/project/project-list-element.component.html new file mode 100644 index 0000000000..7eafd91317 --- /dev/null +++ b/src/app/shared/object-list/item-list-element/entity-types/project/project-list-element.component.html @@ -0,0 +1,16 @@ + + + + + + + + + + + + + diff --git a/src/app/shared/object-list/item-list-element/entity-types/project/project-list-element.component.scss b/src/app/shared/object-list/item-list-element/entity-types/project/project-list-element.component.scss new file mode 100644 index 0000000000..5ab410dcb0 --- /dev/null +++ b/src/app/shared/object-list/item-list-element/entity-types/project/project-list-element.component.scss @@ -0,0 +1 @@ +@import '../../../../../../styles/variables'; diff --git a/src/app/shared/object-list/item-list-element/entity-types/project/project-list-element.component.ts b/src/app/shared/object-list/item-list-element/entity-types/project/project-list-element.component.ts new file mode 100644 index 0000000000..f5275c47e8 --- /dev/null +++ b/src/app/shared/object-list/item-list-element/entity-types/project/project-list-element.component.ts @@ -0,0 +1,14 @@ +import { Component } from '@angular/core'; +import { rendersEntityType } from '../../../../entities/entity-type-decorator'; +import { ElementViewMode } from '../../../../view-mode'; +import { EntitySearchResultComponent } from '../entity-search-result-component'; + +@rendersEntityType('Project', ElementViewMode.SetElement) +@Component({ + selector: 'ds-project-list-element', + styleUrls: ['./project-list-element.component.scss'], + templateUrl: './project-list-element.component.html' +}) + +export class ProjectListElementComponent extends EntitySearchResultComponent { +} diff --git a/src/app/shared/object-list/item-list-element/item-list-element.component.html b/src/app/shared/object-list/item-list-element/item-list-element.component.html deleted file mode 100644 index b4259c25c2..0000000000 --- a/src/app/shared/object-list/item-list-element/item-list-element.component.html +++ /dev/null @@ -1,18 +0,0 @@ - - {{object.findMetadata("dc.title")}} - -
- - - {{authorMd.value}} - ; - - - ({{object.findMetadata("dc.publisher")}}, {{object.findMetadata("dc.date.issued")}}) - -
- {{object.findMetadata("dc.description.abstract") | dsTruncate:[200] }} -
-
diff --git a/src/app/shared/object-list/search-result-list-element/item-search-result/item-search-result-list-element.component.html b/src/app/shared/object-list/search-result-list-element/item-search-result/item-search-result-list-element.component.html index b8f3197a7c..777f131a23 100644 --- a/src/app/shared/object-list/search-result-list-element/item-search-result/item-search-result-list-element.component.html +++ b/src/app/shared/object-list/search-result-list-element/item-search-result/item-search-result-list-element.component.html @@ -1,24 +1 @@ - - - - - (, ) - - - - - - - -
- - -
-
\ No newline at end of file + diff --git a/src/app/shared/object-list/search-result-list-element/item-search-result/item-search-result-list-element.component.ts b/src/app/shared/object-list/search-result-list-element/item-search-result/item-search-result-list-element.component.ts index 37dcb907ef..b9ac52ca40 100644 --- a/src/app/shared/object-list/search-result-list-element/item-search-result/item-search-result-list-element.component.ts +++ b/src/app/shared/object-list/search-result-list-element/item-search-result/item-search-result-list-element.component.ts @@ -1,12 +1,12 @@ -import { ChangeDetectorRef, Component, Inject, OnInit } from '@angular/core'; +import { Component } from '@angular/core'; +import { Item } from '../../../../core/shared/item.model'; +import { focusBackground } from '../../../animations/focus'; import { renderElementsFor } from '../../../object-collection/shared/dso-element-decorator'; -import { SearchResultListElementComponent } from '../search-result-list-element.component'; -import { Item } from '../../../../core/shared/item.model'; import { ItemSearchResult } from '../../../object-collection/shared/item-search-result.model'; +import * as viewMode from '../../../../shared/view-mode'; import { SetViewMode } from '../../../view-mode'; -import { ListableObject } from '../../../object-collection/shared/listable-object.model'; -import { focusBackground } from '../../../animations/focus'; +import { SearchResultListElementComponent } from '../search-result-list-element.component'; @Component({ selector: 'ds-item-search-result-list-element', @@ -18,4 +18,5 @@ import { focusBackground } from '../../../animations/focus'; @renderElementsFor(ItemSearchResult, SetViewMode.List) export class ItemSearchResultListElementComponent extends SearchResultListElementComponent { + ElementViewMode = viewMode.ElementViewMode; } diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index 79ddd8680f..90d498eedd 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -8,6 +8,12 @@ import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; import { TranslateModule } from '@ngx-translate/core'; import { NgxPaginationModule } from 'ngx-pagination'; +import { EntityTypeSwitcherComponent } from './entities/switcher/entity-type-switcher.component'; +import { EntitySearchResultComponent } from './object-list/item-list-element/entity-types/entity-search-result-component'; +import { ItemListElementComponent } from './object-list/item-list-element/entity-types/item/item-list-element.component'; +import { OrgUnitListElementComponent } from './object-list/item-list-element/entity-types/orgunit/orgunit-list-element.component'; +import { PersonListElementComponent } from './object-list/item-list-element/entity-types/person/person-list-element.component'; +import { ProjectListElementComponent } from './object-list/item-list-element/entity-types/project/project-list-element.component'; import { EnumKeysPipe } from './utils/enum-keys-pipe'; import { FileSizePipe } from './utils/file-size-pipe'; @@ -15,7 +21,7 @@ import { SafeUrlPipe } from './utils/safe-url-pipe'; import { CollectionListElementComponent } from './object-list/collection-list-element/collection-list-element.component'; import { CommunityListElementComponent } from './object-list/community-list-element/community-list-element.component'; -import { ItemListElementComponent } from './object-list/item-list-element/item-list-element.component'; +import { EntityListElementComponent } from './object-list/item-list-element/entity-list-element.component'; import { SearchResultListElementComponent } from './object-list/search-result-list-element/search-result-list-element.component'; import { WrapperListElementComponent } from './object-list/wrapper-list-element/wrapper-list-element.component'; import { ObjectListComponent } from './object-list/object-list.component'; @@ -89,11 +95,13 @@ const COMPONENTS = [ ViewModeSwitchComponent, TruncatableComponent, TruncatablePartComponent, + EntitySearchResultComponent, + EntityTypeSwitcherComponent ]; const ENTRY_COMPONENTS = [ // put shared entry components (components that are created dynamically) here - ItemListElementComponent, + EntityListElementComponent, CollectionListElementComponent, CommunityListElementComponent, SearchResultListElementComponent, @@ -101,6 +109,10 @@ const ENTRY_COMPONENTS = [ CollectionGridElementComponent, CommunityGridElementComponent, SearchResultGridElementComponent, + ItemListElementComponent, + PersonListElementComponent, + OrgUnitListElementComponent, + ProjectListElementComponent ]; const PROVIDERS = [ diff --git a/src/app/shared/view-mode.ts b/src/app/shared/view-mode.ts index c12d051943..e78da4fa83 100644 --- a/src/app/shared/view-mode.ts +++ b/src/app/shared/view-mode.ts @@ -5,7 +5,7 @@ export enum SetViewMode { export enum ElementViewMode { Full, - setElement + SetElement } export type ViewMode = SetViewMode | ElementViewMode; From 8423261cc7a98a4fdb2004d29263d4ca4f045b88 Mon Sep 17 00:00:00 2001 From: Art Lowel Date: Wed, 30 May 2018 17:43:24 +0200 Subject: [PATCH 008/604] w2p 51851 - finished relation display on entity pages --- resources/i18n/en.json | 10 +- .../full/full-item-page.component.html | 2 +- src/app/+item-page/item-page.module.ts | 10 +- .../item/item-page-fields.component.ts | 23 ---- .../orgunit-page-fields.component.html | 12 +++ .../orgunit/orgunit-page-fields.component.ts | 39 ++++++- .../person/person-page-fields.component.html | 19 ++-- .../person/person-page-fields.component.ts | 102 +++--------------- .../project-page-fields.component.html | 12 +++ .../project/project-page-fields.component.ts | 37 ++++++- .../publication-page-fields.component.html} | 14 ++- .../publication-page-fields.component.scss} | 0 .../publication-page-fields.component.ts | 54 ++++++++++ .../shared/entity-page-fields.component.ts | 100 +++++++++++++++++ .../related-entities-component.ts | 14 +++ .../related-entities.component.html | 5 + .../related-entities.component.scss | 0 .../orgunit-list-element.component.html | 2 +- .../publication-list-element.component.html} | 4 +- .../publication-list-element.component.scss} | 0 .../publication-list-element.component.ts} | 10 +- src/app/shared/shared.module.ts | 4 +- 22 files changed, 331 insertions(+), 142 deletions(-) delete mode 100644 src/app/+item-page/simple/entity-types/item/item-page-fields.component.ts rename src/app/+item-page/simple/entity-types/{item/item-page-fields.component.html => publication/publication-page-fields.component.html} (61%) rename src/app/+item-page/simple/entity-types/{item/item-page-fields.component.scss => publication/publication-page-fields.component.scss} (100%) create mode 100644 src/app/+item-page/simple/entity-types/publication/publication-page-fields.component.ts create mode 100644 src/app/+item-page/simple/entity-types/shared/entity-page-fields.component.ts create mode 100644 src/app/+item-page/simple/related-entities/related-entities-component.ts create mode 100644 src/app/+item-page/simple/related-entities/related-entities.component.html create mode 100644 src/app/+item-page/simple/related-entities/related-entities.component.scss rename src/app/shared/object-list/item-list-element/entity-types/{item/item-list-element.component.html => publication/publication-list-element.component.html} (84%) rename src/app/shared/object-list/item-list-element/entity-types/{item/item-list-element.component.scss => publication/publication-list-element.component.scss} (100%) rename src/app/shared/object-list/item-list-element/entity-types/{item/item-list-element.component.ts => publication/publication-list-element.component.ts} (54%) diff --git a/resources/i18n/en.json b/resources/i18n/en.json index a06f77463f..5057410ff8 100644 --- a/resources/i18n/en.json +++ b/resources/i18n/en.json @@ -26,7 +26,7 @@ }, "item": { "page": { - "author": "Author", + "author": "Authors", "abstract": "Abstract", "date": "Date", "uri": "URI", @@ -45,6 +45,13 @@ } } }, + "relationships": { + "isPublicationOf": "Publications", + "isProjectOf": "Projects", + "isOrgUnitOf": "Org Units", + "isAuthorOf": "Authors", + "isPersonOf": "Authors" + }, "person": { "page": { "jobtitle": "Job Title", @@ -54,7 +61,6 @@ "orcid": "ORCID", "birthdate": "Birth Date", "staffid": "Staff ID", - "publications": "Publications", "link": { "full": "Show all metadata" } diff --git a/src/app/+item-page/full/full-item-page.component.html b/src/app/+item-page/full/full-item-page.component.html index 4c44b72602..d79c4cdc34 100644 --- a/src/app/+item-page/full/full-item-page.component.html +++ b/src/app/+item-page/full/full-item-page.component.html @@ -1,6 +1,6 @@
-
+
+ + + + + + diff --git a/src/app/+item-page/simple/entity-types/orgunit/orgunit-page-fields.component.ts b/src/app/+item-page/simple/entity-types/orgunit/orgunit-page-fields.component.ts index 74e75e1dc9..9f3437c227 100644 --- a/src/app/+item-page/simple/entity-types/orgunit/orgunit-page-fields.component.ts +++ b/src/app/+item-page/simple/entity-types/orgunit/orgunit-page-fields.component.ts @@ -1,8 +1,14 @@ -import { Component, Inject } from '@angular/core'; +import { Component, Inject, OnInit } from '@angular/core'; +import { Observable } from 'rxjs/Observable'; +import { ItemDataService } from '../../../../core/data/item-data.service'; import { Item } from '../../../../core/shared/item.model'; import { rendersEntityType } from '../../../../shared/entities/entity-type-decorator'; import { ElementViewMode } from '../../../../shared/view-mode'; import { ITEM } from '../../../../shared/entities/switcher/entity-type-switcher.component'; +import { + EntityPageFieldsComponent, filterRelationsByTypeLabel, + relationsToItems +} from '../shared/entity-page-fields.component'; @rendersEntityType('OrgUnit', ElementViewMode.Full) @Component({ @@ -10,9 +16,34 @@ import { ITEM } from '../../../../shared/entities/switcher/entity-type-switcher. styleUrls: ['./orgunit-page-fields.component.scss'], templateUrl: './orgunit-page-fields.component.html' }) -export class OrgUnitPageFieldsComponent { +export class OrgUnitPageFieldsComponent extends EntityPageFieldsComponent implements OnInit { - constructor(@Inject(ITEM) public item: Item) { + people$: Observable; + projects$: Observable; + publications$: Observable; + + constructor( + @Inject(ITEM) public item: Item, + private ids: ItemDataService + ) { + super(item); } -} + ngOnInit(): void { + super.ngOnInit(); + + this.people$ = this.resolvedRelsAndTypes$.pipe( + filterRelationsByTypeLabel('isPersonOfOrgUnit'), + relationsToItems(this.item.id, this.ids) + ); + + this.projects$ = this.resolvedRelsAndTypes$.pipe( + filterRelationsByTypeLabel('isProjectOfOrgUnit'), + relationsToItems(this.item.id, this.ids) + ); + + this.publications$ = this.resolvedRelsAndTypes$.pipe( + filterRelationsByTypeLabel('isPublicationOfOrgUnit'), + relationsToItems(this.item.id, this.ids) + ); + }} diff --git a/src/app/+item-page/simple/entity-types/person/person-page-fields.component.html b/src/app/+item-page/simple/entity-types/person/person-page-fields.component.html index a19170b77d..16a33a65de 100644 --- a/src/app/+item-page/simple/entity-types/person/person-page-fields.component.html +++ b/src/app/+item-page/simple/entity-types/person/person-page-fields.component.html @@ -24,13 +24,18 @@
- - - - {{publ.name}} - - - + + + + + + diff --git a/src/app/+item-page/simple/entity-types/person/person-page-fields.component.ts b/src/app/+item-page/simple/entity-types/person/person-page-fields.component.ts index dce8fe5dbe..61b2d87389 100644 --- a/src/app/+item-page/simple/entity-types/person/person-page-fields.component.ts +++ b/src/app/+item-page/simple/entity-types/person/person-page-fields.component.ts @@ -1,67 +1,14 @@ -import { Component, Inject, OnInit } from '@angular/core'; +import { Component, Inject } from '@angular/core'; import { Observable } from 'rxjs/Observable'; -import { distinctUntilChanged, filter, flatMap, map } from 'rxjs/operators'; import { ItemDataService } from '../../../../core/data/item-data.service'; -import { PaginatedList } from '../../../../core/data/paginated-list'; -import { RemoteData } from '../../../../core/data/remote-data'; -import { RelationshipType } from '../../../../core/shared/entities/relationship-type.model'; -import { Relationship } from '../../../../core/shared/entities/relationship.model'; import { Item } from '../../../../core/shared/item.model'; -import { getRemoteDataPayload } from '../../../../core/shared/operators'; -import { hasValue } from '../../../../shared/empty.util'; import { rendersEntityType } from '../../../../shared/entities/entity-type-decorator'; -import { ElementViewMode } from '../../../../shared/view-mode'; import { ITEM } from '../../../../shared/entities/switcher/entity-type-switcher.component'; - -const compareArraysUsing = (mapFn: (t: T) => any) => - (a: T[], b: T[]): boolean => { - if (!Array.isArray(a) || ! Array.isArray(b)) { - return false - } - - const aIds = a.map(mapFn); - const bIds = b.map(mapFn); - - return aIds.length === bIds.length && - aIds.every((e) => bIds.includes(e)) && - bIds.every((e) => aIds.includes(e)); - }; - -const compareArraysUsingIds = () => - compareArraysUsing((t: T) => hasValue(t) ? t.id : undefined); - -const filterRelationsByTypeLabel = (label: string) => - (source: Observable<[Relationship[], RelationshipType[]]>): Observable => - source.pipe( - map(([relsCurrentPage, relTypesCurrentPage]) => - relsCurrentPage.filter((rel: Relationship, idx: number) => - hasValue(relTypesCurrentPage[idx]) && (relTypesCurrentPage[idx].leftLabel === label || - relTypesCurrentPage[idx].rightLabel === label) - ) - ), - distinctUntilChanged(compareArraysUsingIds()) - ); - -const relationsToItems = (thisId: string, ids: ItemDataService) => - (source: Observable): Observable => - source.pipe( - flatMap((rels: Relationship[]) => - Observable.zip( - ...rels.map((rel: Relationship) => { - let queryId = rel.leftId; - if (rel.leftId === thisId) { - queryId = rel.rightId; - } - return ids.findById(queryId); - }) - ) - ), - map((arr: Array>) => - arr - .filter((d: RemoteData) => d.hasSucceeded) - .map((d: RemoteData) => d.payload)), - distinctUntilChanged(compareArraysUsingIds()), - ); +import { ElementViewMode } from '../../../../shared/view-mode'; +import { + EntityPageFieldsComponent, filterRelationsByTypeLabel, + relationsToItems +} from '../shared/entity-page-fields.component'; @rendersEntityType('Person', ElementViewMode.Full) @Component({ @@ -69,7 +16,7 @@ const relationsToItems = (thisId: string, ids: ItemDataService) => styleUrls: ['./person-page-fields.component.scss'], templateUrl: './person-page-fields.component.html' }) -export class PersonPageFieldsComponent implements OnInit { +export class PersonPageFieldsComponent extends EntityPageFieldsComponent { publications$: Observable; projects$: Observable; orgUnits$: Observable; @@ -77,46 +24,25 @@ export class PersonPageFieldsComponent implements OnInit { constructor( @Inject(ITEM) public item: Item, private ids: ItemDataService - ) {} - + ) { + super(item); + } ngOnInit(): void { - const relsCurrentPage$ = this.item.relationships.pipe( - filter((rd: RemoteData>) => rd.hasSucceeded), - getRemoteDataPayload(), - map((pl: PaginatedList) => pl.page), - distinctUntilChanged(compareArraysUsingIds()) - ); + super.ngOnInit(); - const relTypesCurrentPage$ = relsCurrentPage$.pipe( - flatMap((rels: Relationship[]) => - Observable.zip( - ...rels.map((rel: Relationship) => rel.relationshipType), - (...arr: Array>) => - arr.map((d: RemoteData) => d.payload) - ) - ), - distinctUntilChanged(compareArraysUsingIds()) - ); - - const resolvedRelsAndTypes$ = Observable.combineLatest( - relsCurrentPage$, - relTypesCurrentPage$ - ); - - this.publications$ = resolvedRelsAndTypes$.pipe( + this.publications$ = this.resolvedRelsAndTypes$.pipe( filterRelationsByTypeLabel('isPublicationOfAuthor'), relationsToItems(this.item.id, this.ids) ); - this.projects$ = resolvedRelsAndTypes$.pipe( + this.projects$ = this.resolvedRelsAndTypes$.pipe( filterRelationsByTypeLabel('isProjectOfPerson'), relationsToItems(this.item.id, this.ids) ); - this.orgUnits$ = resolvedRelsAndTypes$.pipe( + this.orgUnits$ = this.resolvedRelsAndTypes$.pipe( filterRelationsByTypeLabel('isOrgUnitOfPerson'), relationsToItems(this.item.id, this.ids) ); } - } diff --git a/src/app/+item-page/simple/entity-types/project/project-page-fields.component.html b/src/app/+item-page/simple/entity-types/project/project-page-fields.component.html index 65a7c7f270..51168011f1 100644 --- a/src/app/+item-page/simple/entity-types/project/project-page-fields.component.html +++ b/src/app/+item-page/simple/entity-types/project/project-page-fields.component.html @@ -20,6 +20,18 @@
+ + + + + + diff --git a/src/app/+item-page/simple/entity-types/project/project-page-fields.component.ts b/src/app/+item-page/simple/entity-types/project/project-page-fields.component.ts index d648247a58..844d0f3808 100644 --- a/src/app/+item-page/simple/entity-types/project/project-page-fields.component.ts +++ b/src/app/+item-page/simple/entity-types/project/project-page-fields.component.ts @@ -1,8 +1,14 @@ -import { Component, Inject } from '@angular/core'; +import { Component, Inject, OnInit } from '@angular/core'; +import { Observable } from 'rxjs/Observable'; +import { ItemDataService } from '../../../../core/data/item-data.service'; import { Item } from '../../../../core/shared/item.model'; import { rendersEntityType } from '../../../../shared/entities/entity-type-decorator'; import { ElementViewMode } from '../../../../shared/view-mode'; import { ITEM } from '../../../../shared/entities/switcher/entity-type-switcher.component'; +import { + EntityPageFieldsComponent, filterRelationsByTypeLabel, + relationsToItems +} from '../shared/entity-page-fields.component'; @rendersEntityType('Project', ElementViewMode.Full) @Component({ @@ -10,9 +16,34 @@ import { ITEM } from '../../../../shared/entities/switcher/entity-type-switcher. styleUrls: ['./project-page-fields.component.scss'], templateUrl: './project-page-fields.component.html' }) -export class ProjectPageFieldsComponent { +export class ProjectPageFieldsComponent extends EntityPageFieldsComponent implements OnInit { + people$: Observable; + publications$: Observable; + orgUnits$: Observable; - constructor(@Inject(ITEM) public item: Item) { + constructor( + @Inject(ITEM) public item: Item, + private ids: ItemDataService + ) { + super(item); } + ngOnInit(): void { + super.ngOnInit(); + + this.people$ = this.resolvedRelsAndTypes$.pipe( + filterRelationsByTypeLabel('isPersonOfProject'), + relationsToItems(this.item.id, this.ids) + ); + + this.publications$ = this.resolvedRelsAndTypes$.pipe( + filterRelationsByTypeLabel('isPublicationOfProject'), + relationsToItems(this.item.id, this.ids) + ); + + this.orgUnits$ = this.resolvedRelsAndTypes$.pipe( + filterRelationsByTypeLabel('isOrgUnitOfProject'), + relationsToItems(this.item.id, this.ids) + ); + } } diff --git a/src/app/+item-page/simple/entity-types/item/item-page-fields.component.html b/src/app/+item-page/simple/entity-types/publication/publication-page-fields.component.html similarity index 61% rename from src/app/+item-page/simple/entity-types/item/item-page-fields.component.html rename to src/app/+item-page/simple/entity-types/publication/publication-page-fields.component.html index 6301bc1bc9..f83338095a 100644 --- a/src/app/+item-page/simple/entity-types/item/item-page-fields.component.html +++ b/src/app/+item-page/simple/entity-types/publication/publication-page-fields.component.html @@ -6,9 +6,21 @@ - +
+ + + + + + diff --git a/src/app/+item-page/simple/entity-types/item/item-page-fields.component.scss b/src/app/+item-page/simple/entity-types/publication/publication-page-fields.component.scss similarity index 100% rename from src/app/+item-page/simple/entity-types/item/item-page-fields.component.scss rename to src/app/+item-page/simple/entity-types/publication/publication-page-fields.component.scss diff --git a/src/app/+item-page/simple/entity-types/publication/publication-page-fields.component.ts b/src/app/+item-page/simple/entity-types/publication/publication-page-fields.component.ts new file mode 100644 index 0000000000..e65bba2945 --- /dev/null +++ b/src/app/+item-page/simple/entity-types/publication/publication-page-fields.component.ts @@ -0,0 +1,54 @@ +import { ChangeDetectionStrategy, Component, Inject, OnInit } from '@angular/core'; +import { Observable } from 'rxjs/Observable'; +import { ItemDataService } from '../../../../core/data/item-data.service'; +import { Item } from '../../../../core/shared/item.model'; +import { + DEFAULT_ENTITY_TYPE, + rendersEntityType +} from '../../../../shared/entities/entity-type-decorator'; +import { ITEM } from '../../../../shared/entities/switcher/entity-type-switcher.component'; +import { ElementViewMode } from '../../../../shared/view-mode'; +import { + EntityPageFieldsComponent, + filterRelationsByTypeLabel, relationsToItems +} from '../shared/entity-page-fields.component'; + +@rendersEntityType('Publication', ElementViewMode.Full) +@rendersEntityType(DEFAULT_ENTITY_TYPE, ElementViewMode.Full) +@Component({ + selector: 'ds-publication-page-fields', + styleUrls: ['./publication-page-fields.component.scss'], + templateUrl: './publication-page-fields.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class PublicationPageFieldsComponent extends EntityPageFieldsComponent implements OnInit { + authors$: Observable; + projects$: Observable; + orgUnits$: Observable; + + constructor( + @Inject(ITEM) public item: Item, + private ids: ItemDataService + ) { + super(item); + } + + ngOnInit(): void { + super.ngOnInit(); + + this.authors$ = this.resolvedRelsAndTypes$.pipe( + filterRelationsByTypeLabel('isAuthorOfPublication'), + relationsToItems(this.item.id, this.ids) + ); + + this.projects$ = this.resolvedRelsAndTypes$.pipe( + filterRelationsByTypeLabel('isProjectOfPublication'), + relationsToItems(this.item.id, this.ids) + ); + + this.orgUnits$ = this.resolvedRelsAndTypes$.pipe( + filterRelationsByTypeLabel('isOrgUnitOfPublication'), + relationsToItems(this.item.id, this.ids) + ); + } +} diff --git a/src/app/+item-page/simple/entity-types/shared/entity-page-fields.component.ts b/src/app/+item-page/simple/entity-types/shared/entity-page-fields.component.ts new file mode 100644 index 0000000000..712b84a37b --- /dev/null +++ b/src/app/+item-page/simple/entity-types/shared/entity-page-fields.component.ts @@ -0,0 +1,100 @@ +import { Component, Inject, OnInit } from '@angular/core'; +import { Observable } from 'rxjs/Observable'; +import { distinctUntilChanged, filter, flatMap, map } from 'rxjs/operators'; +import { ItemDataService } from '../../../../core/data/item-data.service'; +import { PaginatedList } from '../../../../core/data/paginated-list'; +import { RemoteData } from '../../../../core/data/remote-data'; +import { RelationshipType } from '../../../../core/shared/entities/relationship-type.model'; +import { Relationship } from '../../../../core/shared/entities/relationship.model'; +import { Item } from '../../../../core/shared/item.model'; +import { getRemoteDataPayload } from '../../../../core/shared/operators'; +import { hasValue } from '../../../../shared/empty.util'; +import { ITEM } from '../../../../shared/entities/switcher/entity-type-switcher.component'; + +const compareArraysUsing = (mapFn: (t: T) => any) => + (a: T[], b: T[]): boolean => { + if (!Array.isArray(a) || ! Array.isArray(b)) { + return false + } + + const aIds = a.map(mapFn); + const bIds = b.map(mapFn); + + return aIds.length === bIds.length && + aIds.every((e) => bIds.includes(e)) && + bIds.every((e) => aIds.includes(e)); + }; + +const compareArraysUsingIds = () => + compareArraysUsing((t: T) => hasValue(t) ? t.id : undefined); + +export const filterRelationsByTypeLabel = (label: string) => + (source: Observable<[Relationship[], RelationshipType[]]>): Observable => + source.pipe( + map(([relsCurrentPage, relTypesCurrentPage]) => + relsCurrentPage.filter((rel: Relationship, idx: number) => + hasValue(relTypesCurrentPage[idx]) && (relTypesCurrentPage[idx].leftLabel === label || + relTypesCurrentPage[idx].rightLabel === label) + ) + ), + distinctUntilChanged(compareArraysUsingIds()) + ); + +export const relationsToItems = (thisId: string, ids: ItemDataService) => + (source: Observable): Observable => + source.pipe( + flatMap((rels: Relationship[]) => + Observable.zip( + ...rels.map((rel: Relationship) => { + let queryId = rel.leftId; + if (rel.leftId === thisId) { + queryId = rel.rightId; + } + return ids.findById(queryId); + }) + ) + ), + map((arr: Array>) => + arr + .filter((d: RemoteData) => d.hasSucceeded) + .map((d: RemoteData) => d.payload)), + distinctUntilChanged(compareArraysUsingIds()), + ); + +@Component({ + selector: 'ds-entity-page-fields', + template: '' +}) +export class EntityPageFieldsComponent implements OnInit { + resolvedRelsAndTypes$: Observable<[Relationship[], RelationshipType[]]> + + constructor( + @Inject(ITEM) public item: Item + ) {} + + ngOnInit(): void { + const relsCurrentPage$ = this.item.relationships.pipe( + filter((rd: RemoteData>) => rd.hasSucceeded), + getRemoteDataPayload(), + map((pl: PaginatedList) => pl.page), + distinctUntilChanged(compareArraysUsingIds()) + ); + + const relTypesCurrentPage$ = relsCurrentPage$.pipe( + flatMap((rels: Relationship[]) => + Observable.zip( + ...rels.map((rel: Relationship) => rel.relationshipType), + (...arr: Array>) => + arr.map((d: RemoteData) => d.payload) + ) + ), + distinctUntilChanged(compareArraysUsingIds()) + ); + + this.resolvedRelsAndTypes$ = Observable.combineLatest( + relsCurrentPage$, + relTypesCurrentPage$ + ); + } + +} diff --git a/src/app/+item-page/simple/related-entities/related-entities-component.ts b/src/app/+item-page/simple/related-entities/related-entities-component.ts new file mode 100644 index 0000000000..b49e89c7aa --- /dev/null +++ b/src/app/+item-page/simple/related-entities/related-entities-component.ts @@ -0,0 +1,14 @@ +import { Component, Input } from '@angular/core'; +import { Item } from '../../../core/shared/item.model'; +import * as viewMode from '../../../shared/view-mode'; + +@Component({ + selector: 'ds-related-entities', + styleUrls: ['./related-entities.component.scss'], + templateUrl: './related-entities.component.html' +}) +export class RelatedEntitiesComponent { + @Input() entities: Item[]; + @Input() label: string; + ElementViewMode = viewMode.ElementViewMode +} diff --git a/src/app/+item-page/simple/related-entities/related-entities.component.html b/src/app/+item-page/simple/related-entities/related-entities.component.html new file mode 100644 index 0000000000..f09f0ccdef --- /dev/null +++ b/src/app/+item-page/simple/related-entities/related-entities.component.html @@ -0,0 +1,5 @@ + + + + diff --git a/src/app/+item-page/simple/related-entities/related-entities.component.scss b/src/app/+item-page/simple/related-entities/related-entities.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/shared/object-list/item-list-element/entity-types/orgunit/orgunit-list-element.component.html b/src/app/shared/object-list/item-list-element/entity-types/orgunit/orgunit-list-element.component.html index 901d521033..824a90a3de 100644 --- a/src/app/shared/object-list/item-list-element/entity-types/orgunit/orgunit-list-element.component.html +++ b/src/app/shared/object-list/item-list-element/entity-types/orgunit/orgunit-list-element.component.html @@ -3,7 +3,7 @@ [routerLink]="['/items/' + item.id]" class="lead" [innerHTML]="getFirstValue('orgunit.identifier.name')"> - + - ((, ) + [innerHTML]="getFirstValue('dc.date.issued')">) diff --git a/src/app/shared/object-list/item-list-element/entity-types/item/item-list-element.component.scss b/src/app/shared/object-list/item-list-element/entity-types/publication/publication-list-element.component.scss similarity index 100% rename from src/app/shared/object-list/item-list-element/entity-types/item/item-list-element.component.scss rename to src/app/shared/object-list/item-list-element/entity-types/publication/publication-list-element.component.scss diff --git a/src/app/shared/object-list/item-list-element/entity-types/item/item-list-element.component.ts b/src/app/shared/object-list/item-list-element/entity-types/publication/publication-list-element.component.ts similarity index 54% rename from src/app/shared/object-list/item-list-element/entity-types/item/item-list-element.component.ts rename to src/app/shared/object-list/item-list-element/entity-types/publication/publication-list-element.component.ts index db68ecec7b..3f8b7a59f3 100644 --- a/src/app/shared/object-list/item-list-element/entity-types/item/item-list-element.component.ts +++ b/src/app/shared/object-list/item-list-element/entity-types/publication/publication-list-element.component.ts @@ -3,13 +3,13 @@ import { DEFAULT_ENTITY_TYPE, rendersEntityType } from '../../../../entities/ent import { ElementViewMode } from '../../../../view-mode'; import { EntitySearchResultComponent } from '../entity-search-result-component'; -@rendersEntityType('Item', ElementViewMode.SetElement) +@rendersEntityType('Publication', ElementViewMode.SetElement) @rendersEntityType(DEFAULT_ENTITY_TYPE, ElementViewMode.SetElement) @Component({ - selector: 'ds-item-list-element', - styleUrls: ['./item-list-element.component.scss'], - templateUrl: './item-list-element.component.html' + selector: 'ds-publication-list-element', + styleUrls: ['./publication-list-element.component.scss'], + templateUrl: './publication-list-element.component.html' }) -export class ItemListElementComponent extends EntitySearchResultComponent { +export class PublicationListElementComponent extends EntitySearchResultComponent { } diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index 90d498eedd..0da82f5e2f 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -10,7 +10,7 @@ import { TranslateModule } from '@ngx-translate/core'; import { NgxPaginationModule } from 'ngx-pagination'; import { EntityTypeSwitcherComponent } from './entities/switcher/entity-type-switcher.component'; import { EntitySearchResultComponent } from './object-list/item-list-element/entity-types/entity-search-result-component'; -import { ItemListElementComponent } from './object-list/item-list-element/entity-types/item/item-list-element.component'; +import { PublicationListElementComponent } from './object-list/item-list-element/entity-types/publication/publication-list-element.component'; import { OrgUnitListElementComponent } from './object-list/item-list-element/entity-types/orgunit/orgunit-list-element.component'; import { PersonListElementComponent } from './object-list/item-list-element/entity-types/person/person-list-element.component'; import { ProjectListElementComponent } from './object-list/item-list-element/entity-types/project/project-list-element.component'; @@ -109,7 +109,7 @@ const ENTRY_COMPONENTS = [ CollectionGridElementComponent, CommunityGridElementComponent, SearchResultGridElementComponent, - ItemListElementComponent, + PublicationListElementComponent, PersonListElementComponent, OrgUnitListElementComponent, ProjectListElementComponent From f50e0edbc07fdce4c02db47ca9966cb960e82178 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Thu, 31 May 2018 12:40:05 +0200 Subject: [PATCH 009/604] 52213: Item lists: displayed metadata journal-types --- .../journal-issue-list-element.component.html | 20 +++++++++++++++++++ .../journal-issue-list-element.component.scss | 0 .../journal-issue-list-element.component.ts | 14 +++++++++++++ ...journal-volume-list-element.component.html | 20 +++++++++++++++++++ ...journal-volume-list-element.component.scss | 0 .../journal-volume-list-element.component.ts | 14 +++++++++++++ .../journal-list-element.component.html | 15 ++++++++++++++ .../journal-list-element.component.scss | 0 .../journal/journal-list-element.component.ts | 14 +++++++++++++ src/app/shared/shared.module.ts | 8 +++++++- 10 files changed, 104 insertions(+), 1 deletion(-) create mode 100644 src/app/shared/object-list/item-list-element/entity-types/journal-issue/journal-issue-list-element.component.html create mode 100644 src/app/shared/object-list/item-list-element/entity-types/journal-issue/journal-issue-list-element.component.scss create mode 100644 src/app/shared/object-list/item-list-element/entity-types/journal-issue/journal-issue-list-element.component.ts create mode 100644 src/app/shared/object-list/item-list-element/entity-types/journal-volume/journal-volume-list-element.component.html create mode 100644 src/app/shared/object-list/item-list-element/entity-types/journal-volume/journal-volume-list-element.component.scss create mode 100644 src/app/shared/object-list/item-list-element/entity-types/journal-volume/journal-volume-list-element.component.ts create mode 100644 src/app/shared/object-list/item-list-element/entity-types/journal/journal-list-element.component.html create mode 100644 src/app/shared/object-list/item-list-element/entity-types/journal/journal-list-element.component.scss create mode 100644 src/app/shared/object-list/item-list-element/entity-types/journal/journal-list-element.component.ts diff --git a/src/app/shared/object-list/item-list-element/entity-types/journal-issue/journal-issue-list-element.component.html b/src/app/shared/object-list/item-list-element/entity-types/journal-issue/journal-issue-list-element.component.html new file mode 100644 index 0000000000..08011cf04e --- /dev/null +++ b/src/app/shared/object-list/item-list-element/entity-types/journal-issue/journal-issue-list-element.component.html @@ -0,0 +1,20 @@ + + + + + + + + + + + - + + + + + + diff --git a/src/app/shared/object-list/item-list-element/entity-types/journal-issue/journal-issue-list-element.component.scss b/src/app/shared/object-list/item-list-element/entity-types/journal-issue/journal-issue-list-element.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/shared/object-list/item-list-element/entity-types/journal-issue/journal-issue-list-element.component.ts b/src/app/shared/object-list/item-list-element/entity-types/journal-issue/journal-issue-list-element.component.ts new file mode 100644 index 0000000000..8580757997 --- /dev/null +++ b/src/app/shared/object-list/item-list-element/entity-types/journal-issue/journal-issue-list-element.component.ts @@ -0,0 +1,14 @@ +import { Component } from '@angular/core'; +import { rendersEntityType } from '../../../../entities/entity-type-decorator'; +import { ElementViewMode } from '../../../../view-mode'; +import { EntitySearchResultComponent } from '../entity-search-result-component'; + +@rendersEntityType('JournalIssue', ElementViewMode.SetElement) +@Component({ + selector: 'ds-journal-issue-list-element', + styleUrls: ['./journal-issue-list-element.component.scss'], + templateUrl: './journal-issue-list-element.component.html' +}) + +export class JournalIssueListElementComponent extends EntitySearchResultComponent { +} diff --git a/src/app/shared/object-list/item-list-element/entity-types/journal-volume/journal-volume-list-element.component.html b/src/app/shared/object-list/item-list-element/entity-types/journal-volume/journal-volume-list-element.component.html new file mode 100644 index 0000000000..b071179950 --- /dev/null +++ b/src/app/shared/object-list/item-list-element/entity-types/journal-volume/journal-volume-list-element.component.html @@ -0,0 +1,20 @@ + + + + + + + + + + + + () + + + + + diff --git a/src/app/shared/object-list/item-list-element/entity-types/journal-volume/journal-volume-list-element.component.scss b/src/app/shared/object-list/item-list-element/entity-types/journal-volume/journal-volume-list-element.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/shared/object-list/item-list-element/entity-types/journal-volume/journal-volume-list-element.component.ts b/src/app/shared/object-list/item-list-element/entity-types/journal-volume/journal-volume-list-element.component.ts new file mode 100644 index 0000000000..956afe99be --- /dev/null +++ b/src/app/shared/object-list/item-list-element/entity-types/journal-volume/journal-volume-list-element.component.ts @@ -0,0 +1,14 @@ +import { Component } from '@angular/core'; +import { rendersEntityType } from '../../../../entities/entity-type-decorator'; +import { ElementViewMode } from '../../../../view-mode'; +import { EntitySearchResultComponent } from '../entity-search-result-component'; + +@rendersEntityType('JournalVolume', ElementViewMode.SetElement) +@Component({ + selector: 'ds-journal-volume-list-element', + styleUrls: ['./journal-volume-list-element.component.scss'], + templateUrl: './journal-volume-list-element.component.html' +}) + +export class JournalVolumeListElementComponent extends EntitySearchResultComponent { +} diff --git a/src/app/shared/object-list/item-list-element/entity-types/journal/journal-list-element.component.html b/src/app/shared/object-list/item-list-element/entity-types/journal/journal-list-element.component.html new file mode 100644 index 0000000000..fb5284d398 --- /dev/null +++ b/src/app/shared/object-list/item-list-element/entity-types/journal/journal-list-element.component.html @@ -0,0 +1,15 @@ + + + + + + + + + + + + diff --git a/src/app/shared/object-list/item-list-element/entity-types/journal/journal-list-element.component.scss b/src/app/shared/object-list/item-list-element/entity-types/journal/journal-list-element.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/shared/object-list/item-list-element/entity-types/journal/journal-list-element.component.ts b/src/app/shared/object-list/item-list-element/entity-types/journal/journal-list-element.component.ts new file mode 100644 index 0000000000..d5d2cee1cc --- /dev/null +++ b/src/app/shared/object-list/item-list-element/entity-types/journal/journal-list-element.component.ts @@ -0,0 +1,14 @@ +import { Component } from '@angular/core'; +import { rendersEntityType } from '../../../../entities/entity-type-decorator'; +import { ElementViewMode } from '../../../../view-mode'; +import { EntitySearchResultComponent } from '../entity-search-result-component'; + +@rendersEntityType('Journal', ElementViewMode.SetElement) +@Component({ + selector: 'ds-journal-list-element', + styleUrls: ['./journal-list-element.component.scss'], + templateUrl: './journal-list-element.component.html' +}) + +export class JournalListElementComponent extends EntitySearchResultComponent { +} diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index 0da82f5e2f..2dd35cb6f8 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -14,6 +14,9 @@ import { PublicationListElementComponent } from './object-list/item-list-element import { OrgUnitListElementComponent } from './object-list/item-list-element/entity-types/orgunit/orgunit-list-element.component'; import { PersonListElementComponent } from './object-list/item-list-element/entity-types/person/person-list-element.component'; import { ProjectListElementComponent } from './object-list/item-list-element/entity-types/project/project-list-element.component'; +import { JournalListElementComponent } from './object-list/item-list-element/entity-types/journal/journal-list-element.component'; +import { JournalVolumeListElementComponent } from './object-list/item-list-element/entity-types/journal-volume/journal-volume-list-element.component'; +import { JournalIssueListElementComponent } from './object-list/item-list-element/entity-types/journal-issue/journal-issue-list-element.component'; import { EnumKeysPipe } from './utils/enum-keys-pipe'; import { FileSizePipe } from './utils/file-size-pipe'; @@ -112,7 +115,10 @@ const ENTRY_COMPONENTS = [ PublicationListElementComponent, PersonListElementComponent, OrgUnitListElementComponent, - ProjectListElementComponent + ProjectListElementComponent, + JournalListElementComponent, + JournalVolumeListElementComponent, + JournalIssueListElementComponent ]; const PROVIDERS = [ From c99d05c5f98d3d8b6b17459fb31e98f2a71201a3 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Thu, 31 May 2018 14:02:04 +0200 Subject: [PATCH 010/604] 52211: Item display regular metadata journal-types --- resources/i18n/en.json | 22 ++++++++++++++ src/app/+item-page/item-page.module.ts | 13 ++++++-- .../journal-issue-page-fields.component.html | 28 +++++++++++++++++ .../journal-issue-page-fields.component.scss | 1 + .../journal-issue-page-fields.component.ts | 30 +++++++++++++++++++ .../journal-volume-page-fields.component.html | 24 +++++++++++++++ .../journal-volume-page-fields.component.scss | 1 + .../journal-volume-page-fields.component.ts | 30 +++++++++++++++++++ .../journal-page-fields.component.html | 24 +++++++++++++++ .../journal-page-fields.component.scss | 1 + .../journal/journal-page-fields.component.ts | 30 +++++++++++++++++++ 11 files changed, 202 insertions(+), 2 deletions(-) create mode 100644 src/app/+item-page/simple/entity-types/journal-issue/journal-issue-page-fields.component.html create mode 100644 src/app/+item-page/simple/entity-types/journal-issue/journal-issue-page-fields.component.scss create mode 100644 src/app/+item-page/simple/entity-types/journal-issue/journal-issue-page-fields.component.ts create mode 100644 src/app/+item-page/simple/entity-types/journal-volume/journal-volume-page-fields.component.html create mode 100644 src/app/+item-page/simple/entity-types/journal-volume/journal-volume-page-fields.component.scss create mode 100644 src/app/+item-page/simple/entity-types/journal-volume/journal-volume-page-fields.component.ts create mode 100644 src/app/+item-page/simple/entity-types/journal/journal-page-fields.component.html create mode 100644 src/app/+item-page/simple/entity-types/journal/journal-page-fields.component.scss create mode 100644 src/app/+item-page/simple/entity-types/journal/journal-page-fields.component.ts diff --git a/resources/i18n/en.json b/resources/i18n/en.json index 5057410ff8..ab53d312e5 100644 --- a/resources/i18n/en.json +++ b/resources/i18n/en.json @@ -84,6 +84,28 @@ "description": "Description" } }, + "journal": { + "page": { + "issn": "ISSN", + "publisher": "Publisher", + "description": "Description" + } + }, + "journalvolume": { + "page": { + "volume": "Volume", + "issuedate": "Issue Date", + "description": "Description" + } + }, + "journalissue": { + "page": { + "number": "Number", + "issuedate": "Issue Date", + "description": "Description", + "keyword": "Keywords" + } + }, "nav": { "home": "Home" }, diff --git a/src/app/+item-page/item-page.module.ts b/src/app/+item-page/item-page.module.ts index 763f549b26..ce859e0168 100644 --- a/src/app/+item-page/item-page.module.ts +++ b/src/app/+item-page/item-page.module.ts @@ -25,6 +25,9 @@ import { OrgUnitPageFieldsComponent } from './simple/entity-types/orgunit/orguni import { PersonPageFieldsComponent } from './simple/entity-types/person/person-page-fields.component'; import { ProjectPageFieldsComponent } from './simple/entity-types/project/project-page-fields.component'; import { RelatedEntitiesComponent } from './simple/related-entities/related-entities-component'; +import { JournalPageFieldsComponent } from './simple/entity-types/journal/journal-page-fields.component'; +import { JournalIssuePageFieldsComponent } from './simple/entity-types/journal-issue/journal-issue-page-fields.component'; +import { JournalVolumePageFieldsComponent } from './simple/entity-types/journal-volume/journal-volume-page-fields.component'; @NgModule({ imports: [ @@ -53,13 +56,19 @@ import { RelatedEntitiesComponent } from './simple/related-entities/related-enti PersonPageFieldsComponent, RelatedEntitiesComponent, EntityPageFieldsComponent, - GenericItemPageFieldComponent + GenericItemPageFieldComponent, + JournalPageFieldsComponent, + JournalIssuePageFieldsComponent, + JournalVolumePageFieldsComponent ], entryComponents: [ PublicationPageFieldsComponent, ProjectPageFieldsComponent, OrgUnitPageFieldsComponent, - PersonPageFieldsComponent + PersonPageFieldsComponent, + JournalPageFieldsComponent, + JournalIssuePageFieldsComponent, + JournalVolumePageFieldsComponent ] }) export class ItemPageModule { diff --git a/src/app/+item-page/simple/entity-types/journal-issue/journal-issue-page-fields.component.html b/src/app/+item-page/simple/entity-types/journal-issue/journal-issue-page-fields.component.html new file mode 100644 index 0000000000..ae1e6f9376 --- /dev/null +++ b/src/app/+item-page/simple/entity-types/journal-issue/journal-issue-page-fields.component.html @@ -0,0 +1,28 @@ +

+ +

+
+
+ + + + + + + +
+
+ + + + +
+
diff --git a/src/app/+item-page/simple/entity-types/journal-issue/journal-issue-page-fields.component.scss b/src/app/+item-page/simple/entity-types/journal-issue/journal-issue-page-fields.component.scss new file mode 100644 index 0000000000..3575cae797 --- /dev/null +++ b/src/app/+item-page/simple/entity-types/journal-issue/journal-issue-page-fields.component.scss @@ -0,0 +1 @@ +@import '../../../../../styles/variables.scss'; diff --git a/src/app/+item-page/simple/entity-types/journal-issue/journal-issue-page-fields.component.ts b/src/app/+item-page/simple/entity-types/journal-issue/journal-issue-page-fields.component.ts new file mode 100644 index 0000000000..bbf2515945 --- /dev/null +++ b/src/app/+item-page/simple/entity-types/journal-issue/journal-issue-page-fields.component.ts @@ -0,0 +1,30 @@ +import { Component, Inject } from '@angular/core'; +import { Observable } from 'rxjs/Observable'; +import { ItemDataService } from '../../../../core/data/item-data.service'; +import { Item } from '../../../../core/shared/item.model'; +import { rendersEntityType } from '../../../../shared/entities/entity-type-decorator'; +import { ITEM } from '../../../../shared/entities/switcher/entity-type-switcher.component'; +import { ElementViewMode } from '../../../../shared/view-mode'; +import { + EntityPageFieldsComponent, filterRelationsByTypeLabel, + relationsToItems +} from '../shared/entity-page-fields.component'; + +@rendersEntityType('JournalIssue', ElementViewMode.Full) +@Component({ + selector: 'ds-journal-issue-page-fields', + styleUrls: ['./journal-issue-page-fields.component.scss'], + templateUrl: './journal-issue-page-fields.component.html' +}) +export class JournalIssuePageFieldsComponent extends EntityPageFieldsComponent { + + constructor( + @Inject(ITEM) public item: Item, + private ids: ItemDataService + ) { + super(item); + } + ngOnInit(): void { + super.ngOnInit(); + } +} diff --git a/src/app/+item-page/simple/entity-types/journal-volume/journal-volume-page-fields.component.html b/src/app/+item-page/simple/entity-types/journal-volume/journal-volume-page-fields.component.html new file mode 100644 index 0000000000..eabf0ede98 --- /dev/null +++ b/src/app/+item-page/simple/entity-types/journal-volume/journal-volume-page-fields.component.html @@ -0,0 +1,24 @@ +

+ +

+
+
+ + + + + + + +
+
+ + +
+
diff --git a/src/app/+item-page/simple/entity-types/journal-volume/journal-volume-page-fields.component.scss b/src/app/+item-page/simple/entity-types/journal-volume/journal-volume-page-fields.component.scss new file mode 100644 index 0000000000..3575cae797 --- /dev/null +++ b/src/app/+item-page/simple/entity-types/journal-volume/journal-volume-page-fields.component.scss @@ -0,0 +1 @@ +@import '../../../../../styles/variables.scss'; diff --git a/src/app/+item-page/simple/entity-types/journal-volume/journal-volume-page-fields.component.ts b/src/app/+item-page/simple/entity-types/journal-volume/journal-volume-page-fields.component.ts new file mode 100644 index 0000000000..1f0a046ce6 --- /dev/null +++ b/src/app/+item-page/simple/entity-types/journal-volume/journal-volume-page-fields.component.ts @@ -0,0 +1,30 @@ +import { Component, Inject } from '@angular/core'; +import { Observable } from 'rxjs/Observable'; +import { ItemDataService } from '../../../../core/data/item-data.service'; +import { Item } from '../../../../core/shared/item.model'; +import { rendersEntityType } from '../../../../shared/entities/entity-type-decorator'; +import { ITEM } from '../../../../shared/entities/switcher/entity-type-switcher.component'; +import { ElementViewMode } from '../../../../shared/view-mode'; +import { + EntityPageFieldsComponent, filterRelationsByTypeLabel, + relationsToItems +} from '../shared/entity-page-fields.component'; + +@rendersEntityType('JournalVolume', ElementViewMode.Full) +@Component({ + selector: 'ds-journal-volume-page-fields', + styleUrls: ['./journal-volume-page-fields.component.scss'], + templateUrl: './journal-volume-page-fields.component.html' +}) +export class JournalVolumePageFieldsComponent extends EntityPageFieldsComponent { + + constructor( + @Inject(ITEM) public item: Item, + private ids: ItemDataService + ) { + super(item); + } + ngOnInit(): void { + super.ngOnInit(); + } +} diff --git a/src/app/+item-page/simple/entity-types/journal/journal-page-fields.component.html b/src/app/+item-page/simple/entity-types/journal/journal-page-fields.component.html new file mode 100644 index 0000000000..eaa72b8e95 --- /dev/null +++ b/src/app/+item-page/simple/entity-types/journal/journal-page-fields.component.html @@ -0,0 +1,24 @@ +

+ +

+
+
+ + + + + + + +
+
+ + +
+
diff --git a/src/app/+item-page/simple/entity-types/journal/journal-page-fields.component.scss b/src/app/+item-page/simple/entity-types/journal/journal-page-fields.component.scss new file mode 100644 index 0000000000..3575cae797 --- /dev/null +++ b/src/app/+item-page/simple/entity-types/journal/journal-page-fields.component.scss @@ -0,0 +1 @@ +@import '../../../../../styles/variables.scss'; diff --git a/src/app/+item-page/simple/entity-types/journal/journal-page-fields.component.ts b/src/app/+item-page/simple/entity-types/journal/journal-page-fields.component.ts new file mode 100644 index 0000000000..27b4710886 --- /dev/null +++ b/src/app/+item-page/simple/entity-types/journal/journal-page-fields.component.ts @@ -0,0 +1,30 @@ +import { Component, Inject } from '@angular/core'; +import { Observable } from 'rxjs/Observable'; +import { ItemDataService } from '../../../../core/data/item-data.service'; +import { Item } from '../../../../core/shared/item.model'; +import { rendersEntityType } from '../../../../shared/entities/entity-type-decorator'; +import { ITEM } from '../../../../shared/entities/switcher/entity-type-switcher.component'; +import { ElementViewMode } from '../../../../shared/view-mode'; +import { + EntityPageFieldsComponent, filterRelationsByTypeLabel, + relationsToItems +} from '../shared/entity-page-fields.component'; + +@rendersEntityType('Journal', ElementViewMode.Full) +@Component({ + selector: 'ds-journal-page-fields', + styleUrls: ['./journal-page-fields.component.scss'], + templateUrl: './journal-page-fields.component.html' +}) +export class JournalPageFieldsComponent extends EntityPageFieldsComponent { + + constructor( + @Inject(ITEM) public item: Item, + private ids: ItemDataService + ) { + super(item); + } + ngOnInit(): void { + super.ngOnInit(); + } +} From 34dbc24ba3677a4e156359aa763afd8f5b52ff64 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Thu, 31 May 2018 14:38:12 +0200 Subject: [PATCH 011/604] 52212: Item display relations journal-types --- resources/i18n/en.json | 7 ++++++- .../journal-issue-page-fields.component.html | 4 ++++ .../journal-issue-page-fields.component.ts | 6 ++++++ .../journal-volume-page-fields.component.html | 8 ++++++++ .../journal-volume-page-fields.component.ts | 11 +++++++++++ .../journal/journal-page-fields.component.html | 4 ++++ .../journal/journal-page-fields.component.ts | 6 ++++++ 7 files changed, 45 insertions(+), 1 deletion(-) diff --git a/resources/i18n/en.json b/resources/i18n/en.json index ab53d312e5..ab0d82aabe 100644 --- a/resources/i18n/en.json +++ b/resources/i18n/en.json @@ -50,7 +50,12 @@ "isProjectOf": "Projects", "isOrgUnitOf": "Org Units", "isAuthorOf": "Authors", - "isPersonOf": "Authors" + "isPersonOf": "Authors", + "isJournalOf": "Journals", + "isSingleJournalOf": "Journal", + "isVolumeOf": "Volumes", + "isSingleVolumeOf": "Volume", + "isIssueOf": "Issues" }, "person": { "page": { diff --git a/src/app/+item-page/simple/entity-types/journal-issue/journal-issue-page-fields.component.html b/src/app/+item-page/simple/entity-types/journal-issue/journal-issue-page-fields.component.html index ae1e6f9376..3f86ab912b 100644 --- a/src/app/+item-page/simple/entity-types/journal-issue/journal-issue-page-fields.component.html +++ b/src/app/+item-page/simple/entity-types/journal-issue/journal-issue-page-fields.component.html @@ -16,6 +16,10 @@
+ + diff --git a/src/app/+item-page/simple/entity-types/journal-issue/journal-issue-page-fields.component.ts b/src/app/+item-page/simple/entity-types/journal-issue/journal-issue-page-fields.component.ts index bbf2515945..2f98c187f2 100644 --- a/src/app/+item-page/simple/entity-types/journal-issue/journal-issue-page-fields.component.ts +++ b/src/app/+item-page/simple/entity-types/journal-issue/journal-issue-page-fields.component.ts @@ -17,6 +17,7 @@ import { templateUrl: './journal-issue-page-fields.component.html' }) export class JournalIssuePageFieldsComponent extends EntityPageFieldsComponent { + volumes$: Observable; constructor( @Inject(ITEM) public item: Item, @@ -26,5 +27,10 @@ export class JournalIssuePageFieldsComponent extends EntityPageFieldsComponent { } ngOnInit(): void { super.ngOnInit(); + + this.volumes$ = this.resolvedRelsAndTypes$.pipe( + filterRelationsByTypeLabel('isJournalVolumeOfIssue'), + relationsToItems(this.item.id, this.ids) + ); } } diff --git a/src/app/+item-page/simple/entity-types/journal-volume/journal-volume-page-fields.component.html b/src/app/+item-page/simple/entity-types/journal-volume/journal-volume-page-fields.component.html index eabf0ede98..b1dbc5429f 100644 --- a/src/app/+item-page/simple/entity-types/journal-volume/journal-volume-page-fields.component.html +++ b/src/app/+item-page/simple/entity-types/journal-volume/journal-volume-page-fields.component.html @@ -16,6 +16,14 @@
+ + + + diff --git a/src/app/+item-page/simple/entity-types/journal-volume/journal-volume-page-fields.component.ts b/src/app/+item-page/simple/entity-types/journal-volume/journal-volume-page-fields.component.ts index 1f0a046ce6..cd3725018d 100644 --- a/src/app/+item-page/simple/entity-types/journal-volume/journal-volume-page-fields.component.ts +++ b/src/app/+item-page/simple/entity-types/journal-volume/journal-volume-page-fields.component.ts @@ -17,6 +17,8 @@ import { templateUrl: './journal-volume-page-fields.component.html' }) export class JournalVolumePageFieldsComponent extends EntityPageFieldsComponent { + journals$: Observable; + issues$: Observable; constructor( @Inject(ITEM) public item: Item, @@ -26,5 +28,14 @@ export class JournalVolumePageFieldsComponent extends EntityPageFieldsComponent } ngOnInit(): void { super.ngOnInit(); + + this.journals$ = this.resolvedRelsAndTypes$.pipe( + filterRelationsByTypeLabel('isJournalOfVolume'), + relationsToItems(this.item.id, this.ids) + ); + this.issues$ = this.resolvedRelsAndTypes$.pipe( + filterRelationsByTypeLabel('isIssueOfJournalVolume'), + relationsToItems(this.item.id, this.ids) + ); } } diff --git a/src/app/+item-page/simple/entity-types/journal/journal-page-fields.component.html b/src/app/+item-page/simple/entity-types/journal/journal-page-fields.component.html index eaa72b8e95..452e1d61ea 100644 --- a/src/app/+item-page/simple/entity-types/journal/journal-page-fields.component.html +++ b/src/app/+item-page/simple/entity-types/journal/journal-page-fields.component.html @@ -16,6 +16,10 @@
+ + diff --git a/src/app/+item-page/simple/entity-types/journal/journal-page-fields.component.ts b/src/app/+item-page/simple/entity-types/journal/journal-page-fields.component.ts index 27b4710886..b43805ec52 100644 --- a/src/app/+item-page/simple/entity-types/journal/journal-page-fields.component.ts +++ b/src/app/+item-page/simple/entity-types/journal/journal-page-fields.component.ts @@ -17,6 +17,7 @@ import { templateUrl: './journal-page-fields.component.html' }) export class JournalPageFieldsComponent extends EntityPageFieldsComponent { + volumes$: Observable; constructor( @Inject(ITEM) public item: Item, @@ -26,5 +27,10 @@ export class JournalPageFieldsComponent extends EntityPageFieldsComponent { } ngOnInit(): void { super.ngOnInit(); + + this.volumes$ = this.resolvedRelsAndTypes$.pipe( + filterRelationsByTypeLabel('isVolumeOfJournal'), + relationsToItems(this.item.id, this.ids) + ); } } From 733ca643cc9f8e01d72bfe615d2ffcf797d22673 Mon Sep 17 00:00:00 2001 From: Lotte Hofstede Date: Thu, 31 May 2018 16:48:27 +0200 Subject: [PATCH 012/604] fix for entity page changes when going back/forth using browser --- src/app/+item-page/full/full-item-page.component.ts | 7 ++++--- src/app/+item-page/simple/item-page.component.ts | 8 +++++--- src/app/shared/view-mode.ts | 4 ++-- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/app/+item-page/full/full-item-page.component.ts b/src/app/+item-page/full/full-item-page.component.ts index aa1fc4cc78..97e08099c1 100644 --- a/src/app/+item-page/full/full-item-page.component.ts +++ b/src/app/+item-page/full/full-item-page.component.ts @@ -1,4 +1,4 @@ -import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnInit } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { Observable } from 'rxjs/Observable'; @@ -34,8 +34,9 @@ export class FullItemPageComponent extends ItemPageComponent implements OnInit { metadataObs: Observable; - constructor(route: ActivatedRoute, items: ItemDataService, metadataService: MetadataService) { - super(route, items, metadataService); + constructor(route: ActivatedRoute, items: ItemDataService, metadataService: MetadataService, + ref: ChangeDetectorRef) { + super(route, items, metadataService, ref); } /*** AoT inheritance fix, will hopefully be resolved in the near future **/ diff --git a/src/app/+item-page/simple/item-page.component.ts b/src/app/+item-page/simple/item-page.component.ts index fee7753c82..cbcad3aa99 100644 --- a/src/app/+item-page/simple/item-page.component.ts +++ b/src/app/+item-page/simple/item-page.component.ts @@ -1,4 +1,4 @@ -import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnInit } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { Observable } from 'rxjs/Observable'; @@ -41,7 +41,8 @@ export class ItemPageComponent implements OnInit { constructor( private route: ActivatedRoute, private items: ItemDataService, - private metadataService: MetadataService + private metadataService: MetadataService, + private ref: ChangeDetectorRef ) { } @@ -50,16 +51,17 @@ export class ItemPageComponent implements OnInit { this.sub = this.route.params.subscribe((params) => { this.initialize(params); }); + } initialize(params) { this.id = +params.id; this.itemRDObs = this.items.findById(params.id); + this.ref.detectChanges(); this.metadataService.processRemoteData(this.itemRDObs); this.thumbnailObs = this.itemRDObs .map((rd: RemoteData) => rd.payload) .filter((item: Item) => hasValue(item)) .flatMap((item: Item) => item.getThumbnail()); } - } diff --git a/src/app/shared/view-mode.ts b/src/app/shared/view-mode.ts index e78da4fa83..b0e180c290 100644 --- a/src/app/shared/view-mode.ts +++ b/src/app/shared/view-mode.ts @@ -1,6 +1,6 @@ export enum SetViewMode { - List, - Grid + List = 'list', + Grid = 'grid' } export enum ElementViewMode { From 691b466d9679cbb36aa21305244d51375ba47921 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Fri, 1 Jun 2018 09:48:31 +0200 Subject: [PATCH 013/604] 52425: Publication - JournalIssue relation --- resources/i18n/en.json | 4 +++- .../journal-issue/journal-issue-page-fields.component.html | 4 ++++ .../journal-issue/journal-issue-page-fields.component.ts | 5 +++++ .../publication/publication-page-fields.component.html | 6 +++++- .../publication/publication-page-fields.component.ts | 6 ++++++ 5 files changed, 23 insertions(+), 2 deletions(-) diff --git a/resources/i18n/en.json b/resources/i18n/en.json index ab0d82aabe..0de22af7e5 100644 --- a/resources/i18n/en.json +++ b/resources/i18n/en.json @@ -55,7 +55,9 @@ "isSingleJournalOf": "Journal", "isVolumeOf": "Volumes", "isSingleVolumeOf": "Volume", - "isIssueOf": "Issues" + "isIssueOf": "Issues", + "isJournalIssueOf": "Journal Issue", + "isPublicationOfJournalIssue": "Articles" }, "person": { "page": { diff --git a/src/app/+item-page/simple/entity-types/journal-issue/journal-issue-page-fields.component.html b/src/app/+item-page/simple/entity-types/journal-issue/journal-issue-page-fields.component.html index 3f86ab912b..f22e98795d 100644 --- a/src/app/+item-page/simple/entity-types/journal-issue/journal-issue-page-fields.component.html +++ b/src/app/+item-page/simple/entity-types/journal-issue/journal-issue-page-fields.component.html @@ -20,6 +20,10 @@ [entities]="volumes$ | async" [label]="'relationships.isSingleVolumeOf' | translate"> + + diff --git a/src/app/+item-page/simple/entity-types/journal-issue/journal-issue-page-fields.component.ts b/src/app/+item-page/simple/entity-types/journal-issue/journal-issue-page-fields.component.ts index 2f98c187f2..eab4f14fa3 100644 --- a/src/app/+item-page/simple/entity-types/journal-issue/journal-issue-page-fields.component.ts +++ b/src/app/+item-page/simple/entity-types/journal-issue/journal-issue-page-fields.component.ts @@ -18,6 +18,7 @@ import { }) export class JournalIssuePageFieldsComponent extends EntityPageFieldsComponent { volumes$: Observable; + publications$: Observable; constructor( @Inject(ITEM) public item: Item, @@ -32,5 +33,9 @@ export class JournalIssuePageFieldsComponent extends EntityPageFieldsComponent { filterRelationsByTypeLabel('isJournalVolumeOfIssue'), relationsToItems(this.item.id, this.ids) ); + this.publications$ = this.resolvedRelsAndTypes$.pipe( + filterRelationsByTypeLabel('isPublicationOfJournalIssue'), + relationsToItems(this.item.id, this.ids) + ); } } diff --git a/src/app/+item-page/simple/entity-types/publication/publication-page-fields.component.html b/src/app/+item-page/simple/entity-types/publication/publication-page-fields.component.html index f83338095a..84cd5a151b 100644 --- a/src/app/+item-page/simple/entity-types/publication/publication-page-fields.component.html +++ b/src/app/+item-page/simple/entity-types/publication/publication-page-fields.component.html @@ -18,9 +18,13 @@ [label]="'relationships.isProjectOf' | translate"> + + diff --git a/src/app/+item-page/simple/entity-types/publication/publication-page-fields.component.ts b/src/app/+item-page/simple/entity-types/publication/publication-page-fields.component.ts index e65bba2945..419d415122 100644 --- a/src/app/+item-page/simple/entity-types/publication/publication-page-fields.component.ts +++ b/src/app/+item-page/simple/entity-types/publication/publication-page-fields.component.ts @@ -25,6 +25,7 @@ export class PublicationPageFieldsComponent extends EntityPageFieldsComponent im authors$: Observable; projects$: Observable; orgUnits$: Observable; + journalIssues$: Observable; constructor( @Inject(ITEM) public item: Item, @@ -50,5 +51,10 @@ export class PublicationPageFieldsComponent extends EntityPageFieldsComponent im filterRelationsByTypeLabel('isOrgUnitOfPublication'), relationsToItems(this.item.id, this.ids) ); + + this.journalIssues$ = this.resolvedRelsAndTypes$.pipe( + filterRelationsByTypeLabel('isJournalIssueOfPublication'), + relationsToItems(this.item.id, this.ids) + ); } } From 90126888bbede4e3f0e59146e52855bcd16f2645 Mon Sep 17 00:00:00 2001 From: Lotte Hofstede Date: Fri, 1 Jun 2018 10:06:50 +0200 Subject: [PATCH 014/604] fixed with subject instead of changedetection hack --- src/app/+item-page/full/full-item-page.component.ts | 8 ++++---- src/app/+item-page/simple/item-page.component.ts | 9 ++++----- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/src/app/+item-page/full/full-item-page.component.ts b/src/app/+item-page/full/full-item-page.component.ts index 97e08099c1..778478a6b6 100644 --- a/src/app/+item-page/full/full-item-page.component.ts +++ b/src/app/+item-page/full/full-item-page.component.ts @@ -14,6 +14,7 @@ import { MetadataService } from '../../core/metadata/metadata.service'; import { fadeInOut } from '../../shared/animations/fade'; import { hasValue } from '../../shared/empty.util'; +import { BehaviorSubject } from 'rxjs/BehaviorSubject'; /** * This component renders a simple item page. @@ -30,13 +31,12 @@ import { hasValue } from '../../shared/empty.util'; }) export class FullItemPageComponent extends ItemPageComponent implements OnInit { - itemRDObs: Observable>; + itemRDObs: BehaviorSubject>; metadataObs: Observable; - constructor(route: ActivatedRoute, items: ItemDataService, metadataService: MetadataService, - ref: ChangeDetectorRef) { - super(route, items, metadataService, ref); + constructor(route: ActivatedRoute, items: ItemDataService, metadataService: MetadataService) { + super(route, items, metadataService); } /*** AoT inheritance fix, will hopefully be resolved in the near future **/ diff --git a/src/app/+item-page/simple/item-page.component.ts b/src/app/+item-page/simple/item-page.component.ts index cbcad3aa99..55dfe8a7b0 100644 --- a/src/app/+item-page/simple/item-page.component.ts +++ b/src/app/+item-page/simple/item-page.component.ts @@ -13,6 +13,7 @@ import { MetadataService } from '../../core/metadata/metadata.service'; import { fadeInOut } from '../../shared/animations/fade'; import { hasValue } from '../../shared/empty.util'; import * as viewMode from '../../shared/view-mode'; +import { BehaviorSubject } from 'rxjs/BehaviorSubject'; /** * This component renders a simple item page. @@ -32,17 +33,16 @@ export class ItemPageComponent implements OnInit { private sub: any; - itemRDObs: Observable>; - thumbnailObs: Observable; + itemRDObs?: BehaviorSubject> = new BehaviorSubject(new RemoteData(true, true, false, null, null)); + ElementViewMode = viewMode.ElementViewMode; constructor( private route: ActivatedRoute, private items: ItemDataService, private metadataService: MetadataService, - private ref: ChangeDetectorRef ) { } @@ -56,8 +56,7 @@ export class ItemPageComponent implements OnInit { initialize(params) { this.id = +params.id; - this.itemRDObs = this.items.findById(params.id); - this.ref.detectChanges(); + this.items.findById(params.id).filter((rd) => hasValue(rd.payload)).first().subscribe((item) => this.itemRDObs.next(item)); this.metadataService.processRemoteData(this.itemRDObs); this.thumbnailObs = this.itemRDObs .map((rd: RemoteData) => rd.payload) From 8ab6eb6ec882d19fcb53d4ba5fc7945fbcd9ab44 Mon Sep 17 00:00:00 2001 From: Lotte Hofstede Date: Fri, 1 Jun 2018 12:06:21 +0200 Subject: [PATCH 015/604] item page reload fix --- .../+item-page/simple/item-page.component.html | 8 ++++---- src/app/+item-page/simple/item-page.component.ts | 16 +++++++++------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/src/app/+item-page/simple/item-page.component.html b/src/app/+item-page/simple/item-page.component.html index 17d271f368..58dbbe3aa1 100644 --- a/src/app/+item-page/simple/item-page.component.html +++ b/src/app/+item-page/simple/item-page.component.html @@ -1,9 +1,9 @@
-
+
- +
- - + +
diff --git a/src/app/+item-page/simple/item-page.component.ts b/src/app/+item-page/simple/item-page.component.ts index 55dfe8a7b0..1dfa93f52d 100644 --- a/src/app/+item-page/simple/item-page.component.ts +++ b/src/app/+item-page/simple/item-page.component.ts @@ -14,6 +14,7 @@ import { fadeInOut } from '../../shared/animations/fade'; import { hasValue } from '../../shared/empty.util'; import * as viewMode from '../../shared/view-mode'; import { BehaviorSubject } from 'rxjs/BehaviorSubject'; +import { Subscription } from 'rxjs/Subscription'; /** * This component renders a simple item page. @@ -31,11 +32,11 @@ export class ItemPageComponent implements OnInit { id: number; - private sub: any; - + private sub: Subscription; + private itemSub: Subscription; thumbnailObs: Observable; - itemRDObs?: BehaviorSubject> = new BehaviorSubject(new RemoteData(true, true, false, null, null)); + itemRDObs: BehaviorSubject> = new BehaviorSubject(new RemoteData(true, true, undefined, null, null)); ElementViewMode = viewMode.ElementViewMode; @@ -43,9 +44,7 @@ export class ItemPageComponent implements OnInit { private route: ActivatedRoute, private items: ItemDataService, private metadataService: MetadataService, - ) { - - } + ) { } ngOnInit(): void { this.sub = this.route.params.subscribe((params) => { @@ -56,7 +55,10 @@ export class ItemPageComponent implements OnInit { initialize(params) { this.id = +params.id; - this.items.findById(params.id).filter((rd) => hasValue(rd.payload)).first().subscribe((item) => this.itemRDObs.next(item)); + if (hasValue(this.itemSub)) { + this.itemSub.unsubscribe(); + } + this.itemSub = this.items.findById(params.id).subscribe((item) => this.itemRDObs.next(item)); this.metadataService.processRemoteData(this.itemRDObs); this.thumbnailObs = this.itemRDObs .map((rd: RemoteData) => rd.payload) From f1a8517b5f36ba449da8222e31db2379f3fbb760 Mon Sep 17 00:00:00 2001 From: Lotte Hofstede Date: Fri, 1 Jun 2018 12:09:22 +0200 Subject: [PATCH 016/604] clean up after fix --- src/app/+item-page/simple/item-page.component.html | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/app/+item-page/simple/item-page.component.html b/src/app/+item-page/simple/item-page.component.html index 58dbbe3aa1..17d271f368 100644 --- a/src/app/+item-page/simple/item-page.component.html +++ b/src/app/+item-page/simple/item-page.component.html @@ -1,9 +1,9 @@
-
+
- +
- - + +
From 8ea8f1194468276103df4eea6fcafd78f24a67fe Mon Sep 17 00:00:00 2001 From: lotte Date: Mon, 18 Jun 2018 11:27:48 +0200 Subject: [PATCH 017/604] 52935: entity filter messages --- resources/i18n/en.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/resources/i18n/en.json b/resources/i18n/en.json index 0de22af7e5..5e30d08ed0 100644 --- a/resources/i18n/en.json +++ b/resources/i18n/en.json @@ -197,6 +197,10 @@ "has_content_in_original_bundle": { "placeholder": "Has files", "head": "Has files" + }, + "entityType": { + "placeholder": "Entity Type", + "head": "Entity Type" } } } From d4c00dbaa07e94f892ec3a86657e0a4aeebd41f7 Mon Sep 17 00:00:00 2001 From: lotte Date: Mon, 18 Jun 2018 12:07:00 +0200 Subject: [PATCH 018/604] Fixed typo that broke AoT build --- .../entity-types/project/project-page-fields.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/+item-page/simple/entity-types/project/project-page-fields.component.html b/src/app/+item-page/simple/entity-types/project/project-page-fields.component.html index 51168011f1..a1c5502307 100644 --- a/src/app/+item-page/simple/entity-types/project/project-page-fields.component.html +++ b/src/app/+item-page/simple/entity-types/project/project-page-fields.component.html @@ -29,7 +29,7 @@ [label]="'relationships.isPublicationOf' | translate"> Date: Mon, 18 Jun 2018 12:11:25 +0200 Subject: [PATCH 019/604] fixed another typo to fix AoT build.. --- .../entity-types/person/person-page-fields.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/+item-page/simple/entity-types/person/person-page-fields.component.html b/src/app/+item-page/simple/entity-types/person/person-page-fields.component.html index 16a33a65de..d7cc3f1158 100644 --- a/src/app/+item-page/simple/entity-types/person/person-page-fields.component.html +++ b/src/app/+item-page/simple/entity-types/person/person-page-fields.component.html @@ -33,7 +33,7 @@ [label]="'relationships.isProjectOf' | translate"> Date: Fri, 29 Jun 2018 15:21:47 +0200 Subject: [PATCH 020/604] 53029: start of filtered discovery pages --- .../search-fixed-filter.service.ts | 78 +++++++++++++++++++ .../data/base-response-parsing.service.ts | 2 +- .../data/search-response-parsing.service.ts | 2 +- src/app/shared/route.service.ts | 26 +++---- 4 files changed, 93 insertions(+), 15 deletions(-) create mode 100644 src/app/+search-page/search-filters/search-filter/search-fixed-filter.service.ts diff --git a/src/app/+search-page/search-filters/search-filter/search-fixed-filter.service.ts b/src/app/+search-page/search-filters/search-filter/search-fixed-filter.service.ts new file mode 100644 index 0000000000..ed5d2c5496 --- /dev/null +++ b/src/app/+search-page/search-filters/search-filter/search-fixed-filter.service.ts @@ -0,0 +1,78 @@ +import { Injectable } from '@angular/core'; +import { debounceTime, distinctUntilChanged, flatMap, map } from 'rxjs/operators'; +import { SearchFiltersState, SearchFilterState } from './search-filter.reducer'; +import { createSelector, MemoizedSelector, Store } from '@ngrx/store'; +import { Observable } from 'rxjs/Observable'; +import { + SearchFilterCollapseAction, + SearchFilterDecrementPageAction, SearchFilterExpandAction, + SearchFilterIncrementPageAction, + SearchFilterInitialCollapseAction, + SearchFilterInitialExpandAction, SearchFilterResetPageAction, + SearchFilterToggleAction +} from './search-filter.actions'; +import { hasValue, isEmpty, isNotEmpty, } from '../../../shared/empty.util'; +import { SearchFilterConfig } from '../../search-service/search-filter-config.model'; +import { SearchService } from '../../search-service/search.service'; +import { RouteService } from '../../../shared/route.service'; +import ObjectExpression from 'rollup/dist/typings/ast/nodes/ObjectExpression'; +import { SortDirection, SortOptions } from '../../../core/cache/models/sort-options.model'; +import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model'; +import { SearchOptions } from '../../search-options.model'; +import { PaginatedSearchOptions } from '../../paginated-search-options.model'; +import { RemoteData } from '../../../core/data/remote-data'; +import { configureRequest } from '../../../core/shared/operators'; +import { HALEndpointService } from '../../../core/shared/hal-endpoint.service'; +import { GetRequest, RestRequest } from '../../../core/data/request.models'; +import { FacetConfigSuccessResponse } from '../../../core/cache/response-cache.models'; +import { ResponseCacheEntry } from '../../../core/cache/response-cache.reducer'; +import { RequestService } from '../../../core/data/request.service'; +import { ResponseCacheService } from '../../../core/cache/response-cache.service'; +import { SearchResponseParsingService } from '../../../core/data/search-response-parsing.service'; +import { ResponseParsingService } from '../../../core/data/parsing.service'; +import { GenericConstructor } from '../../../core/shared/generic-constructor'; + + +@Injectable() +export class SearchFixedFilterService { + private queryByFilterPath = 'config/filtered-discovery-pages'; + + constructor(private routeService: RouteService, + protected requestService: RequestService, + protected responseCache: ResponseCacheService, + private halService: HALEndpointService) { + + } + + getQueryByFilterName(filterName?: string): Observable> { + const requestObs = this.halService.getEndpoint(this.queryByFilterPath).pipe( + map((url: string) => { + url += ('/' + filterName); + const request = new GetRequest(this.requestService.generateRequestId(), url); + return Object.assign(request, { + getResponseParser(): GenericConstructor { + return FilterDiscoveryPageResponseParsingService; + } + }); + }), + ); + + const requestEntryObs = requestObs.pipe( + flatMap((request: RestRequest) => this.requestService.getByHref(request.href)) + ); + + const responseCacheObs = requestObs.pipe( + flatMap((request: RestRequest) => this.responseCache.get(request.href)) + ); + + // get search results from response cache + const fixedFilterConfigObs: Observable = responseCacheObs.pipe( + map((entry: ResponseCacheEntry) => entry.response), + map((response: FacetConfigSuccessResponse) => + response.results.map((result: any) => Object.assign(new SearchFilterConfig(), result))) + ); + + return this.rdb.toRemoteDataObservable(requestEntryObs, responseCacheObs, facetConfigObs); + } + +} diff --git a/src/app/core/data/base-response-parsing.service.ts b/src/app/core/data/base-response-parsing.service.ts index 050b3c2da5..3f64be7515 100644 --- a/src/app/core/data/base-response-parsing.service.ts +++ b/src/app/core/data/base-response-parsing.service.ts @@ -13,7 +13,7 @@ function isObjectLevel(halObj: any) { } function isPaginatedResponse(halObj: any) { - return isNotEmpty(halObj.page) && hasValue(halObj._embedded); + return isNotEmpty(halObj.page) /** && hasValue(halObj._embedded)**/; } /* tslint:disable:max-classes-per-file */ diff --git a/src/app/core/data/search-response-parsing.service.ts b/src/app/core/data/search-response-parsing.service.ts index c7456aa2f9..ebefbe4ba4 100644 --- a/src/app/core/data/search-response-parsing.service.ts +++ b/src/app/core/data/search-response-parsing.service.ts @@ -16,7 +16,7 @@ export class SearchResponseParsingService implements ResponseParsingService { } parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse { - const payload = data.payload; + const payload = data.payload._embedded.searchResult; const hitHighlights = payload._embedded.objects .map((object) => object.hitHighlights) .map((hhObject) => { diff --git a/src/app/shared/route.service.ts b/src/app/shared/route.service.ts index 9c2b64ede1..716a77213b 100644 --- a/src/app/shared/route.service.ts +++ b/src/app/shared/route.service.ts @@ -1,10 +1,6 @@ import { Injectable } from '@angular/core'; import { Observable } from 'rxjs/Observable'; -import { - ActivatedRoute, convertToParamMap, NavigationExtras, Params, - Router, -} from '@angular/router'; -import { isNotEmpty } from './empty.util'; +import { ActivatedRoute, Params, } from '@angular/router'; @Injectable() export class RouteService { @@ -28,16 +24,20 @@ export class RouteService { return this.route.queryParamMap.map((map) => map.getAll(paramName).indexOf(paramValue) > -1).distinctUntilChanged(); } + getRouteParameterValue(paramName: string): Observable { + return this.route.params.map((params) => params[paramName]).distinctUntilChanged(); + } + getQueryParamsWithPrefix(prefix: string): Observable { return this.route.queryParamMap .map((map) => { - const params = {}; - map.keys - .filter((key) => key.startsWith(prefix)) - .forEach((key) => { - params[key] = [...map.getAll(key)]; - }); - return params; - }).distinctUntilChanged(); + const params = {}; + map.keys + .filter((key) => key.startsWith(prefix)) + .forEach((key) => { + params[key] = [...map.getAll(key)]; + }); + return params; + }).distinctUntilChanged(); } } From 1231d3ebadc644407d668add4a60a63eb353088a Mon Sep 17 00:00:00 2001 From: lotte Date: Tue, 3 Jul 2018 15:24:31 +0200 Subject: [PATCH 021/604] 53029: almost finished filtered discovery pages --- .../filtered-search-page.component.ts | 34 ++++++++++++++ .../search-filter/search-filter.service.ts | 23 ++++++--- .../search-fixed-filter.service.ts | 47 +++++-------------- src/app/+search-page/search-options.model.ts | 5 +- .../search-page-routing.module.ts | 4 +- src/app/+search-page/search-page.component.ts | 13 +++-- src/app/+search-page/search-page.module.ts | 6 ++- src/app/core/cache/response-cache.models.ts | 10 ++++ ...discovery-page-response-parsing.service.ts | 24 ++++++++++ src/app/shared/route.service.ts | 4 ++ 10 files changed, 120 insertions(+), 50 deletions(-) create mode 100644 src/app/+search-page/filtered-search-page.component.ts create mode 100644 src/app/core/data/filtered-discovery-page-response-parsing.service.ts diff --git a/src/app/+search-page/filtered-search-page.component.ts b/src/app/+search-page/filtered-search-page.component.ts new file mode 100644 index 0000000000..5e68556e59 --- /dev/null +++ b/src/app/+search-page/filtered-search-page.component.ts @@ -0,0 +1,34 @@ +import { CommunityDataService } from '../core/data/community-data.service'; +import { HostWindowService } from '../shared/host-window.service'; +import { SearchFilterService } from './search-filters/search-filter/search-filter.service'; +import { SearchService } from './search-service/search.service'; +import { SearchSidebarService } from './search-sidebar/search-sidebar.service'; +import { SearchPageComponent } from './search-page.component'; +import { RouteService } from '../shared/route.service'; +import { ChangeDetectionStrategy, Component, Injectable } from '@angular/core'; +import { pushInOut } from '../shared/animations/push'; + +/** + * This component renders a simple item page. + * The route parameter 'id' is used to request the item it represents. + * All fields of the item that should be displayed, are defined in its template. + */ +@Component({selector: 'ds-filtered-search-page', + styleUrls: ['./search-page.component.scss'], + templateUrl: './search-page.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + animations: [pushInOut] +}) + +export class FilteredSearchPageComponent extends SearchPageComponent { + + constructor(protected service: SearchService, + protected communityService: CommunityDataService, + protected sidebarService: SearchSidebarService, + protected windowService: HostWindowService, + protected filterService: SearchFilterService, + protected routeService: RouteService) { + super(service, communityService, sidebarService, windowService, filterService, routeService); + } + +} diff --git a/src/app/+search-page/search-filters/search-filter/search-filter.service.ts b/src/app/+search-page/search-filters/search-filter/search-filter.service.ts index 44d9c7e709..f6227645db 100644 --- a/src/app/+search-page/search-filters/search-filter/search-filter.service.ts +++ b/src/app/+search-page/search-filters/search-filter/search-filter.service.ts @@ -20,6 +20,7 @@ import { SortDirection, SortOptions } from '../../../core/cache/models/sort-opti import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model'; import { SearchOptions } from '../../search-options.model'; import { PaginatedSearchOptions } from '../../paginated-search-options.model'; +import { SearchFixedFilterService } from './search-fixed-filter.service'; const filterStateSelector = (state: SearchFiltersState) => state.searchFilter; @@ -27,7 +28,8 @@ const filterStateSelector = (state: SearchFiltersState) => state.searchFilter; export class SearchFilterService { constructor(private store: Store, - private routeService: RouteService) { + private routeService: RouteService, + private fixedFilterService: SearchFixedFilterService) { } isFilterActiveWithValue(paramName: string, filterValue: string): Observable { @@ -72,6 +74,11 @@ export class SearchFilterService { return this.routeService.getQueryParamsWithPrefix('f.'); } + getCurrentFixedFilter(): Observable { + const filter: Observable = this.routeService.getRouteParameterValue('filter'); + return filter.flatMap((f) => this.fixedFilterService.getQueryByFilterName(f)); + } + getCurrentView() { return this.routeService.getQueryParameterValue('view'); } @@ -83,9 +90,10 @@ export class SearchFilterService { this.getCurrentView(), this.getCurrentScope(), this.getCurrentQuery(), - this.getCurrentFilters()).pipe( + this.getCurrentFilters(), + this.getCurrentFixedFilter()).pipe( distinctUntilChanged((a, b) => JSON.stringify(a) === JSON.stringify(b)), - map(([pagination, sort, view, scope, query, filters]) => { + map(([pagination, sort, view, scope, query, filters, fixedFilter]) => { return Object.assign(new PaginatedSearchOptions(), defaults, { @@ -94,7 +102,8 @@ export class SearchFilterService { view: view, scope: scope || defaults.scope, query: query, - filters: filters + filters: filters, + fixedFilter: fixedFilter }) }) ) @@ -106,14 +115,16 @@ export class SearchFilterService { this.getCurrentScope(), this.getCurrentQuery(), this.getCurrentFilters(), - (view, scope, query, filters) => { + this.getCurrentFixedFilter(), + (view, scope, query, filters, fixedFilter) => { return Object.assign(new SearchOptions(), defaults, { view: view, scope: scope || defaults.scope, query: query, - filters: filters + filters: filters, + fixedFilter: fixedFilter }) } ) diff --git a/src/app/+search-page/search-filters/search-filter/search-fixed-filter.service.ts b/src/app/+search-page/search-filters/search-filter/search-fixed-filter.service.ts index ed5d2c5496..f562f4c680 100644 --- a/src/app/+search-page/search-filters/search-filter/search-fixed-filter.service.ts +++ b/src/app/+search-page/search-filters/search-filter/search-fixed-filter.service.ts @@ -1,41 +1,20 @@ import { Injectable } from '@angular/core'; -import { debounceTime, distinctUntilChanged, flatMap, map } from 'rxjs/operators'; -import { SearchFiltersState, SearchFilterState } from './search-filter.reducer'; -import { createSelector, MemoizedSelector, Store } from '@ngrx/store'; +import { flatMap, map } from 'rxjs/operators'; import { Observable } from 'rxjs/Observable'; -import { - SearchFilterCollapseAction, - SearchFilterDecrementPageAction, SearchFilterExpandAction, - SearchFilterIncrementPageAction, - SearchFilterInitialCollapseAction, - SearchFilterInitialExpandAction, SearchFilterResetPageAction, - SearchFilterToggleAction -} from './search-filter.actions'; -import { hasValue, isEmpty, isNotEmpty, } from '../../../shared/empty.util'; -import { SearchFilterConfig } from '../../search-service/search-filter-config.model'; -import { SearchService } from '../../search-service/search.service'; import { RouteService } from '../../../shared/route.service'; -import ObjectExpression from 'rollup/dist/typings/ast/nodes/ObjectExpression'; -import { SortDirection, SortOptions } from '../../../core/cache/models/sort-options.model'; -import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model'; -import { SearchOptions } from '../../search-options.model'; -import { PaginatedSearchOptions } from '../../paginated-search-options.model'; -import { RemoteData } from '../../../core/data/remote-data'; -import { configureRequest } from '../../../core/shared/operators'; import { HALEndpointService } from '../../../core/shared/hal-endpoint.service'; import { GetRequest, RestRequest } from '../../../core/data/request.models'; -import { FacetConfigSuccessResponse } from '../../../core/cache/response-cache.models'; +import { FilteredDiscoveryQueryResponse } from '../../../core/cache/response-cache.models'; import { ResponseCacheEntry } from '../../../core/cache/response-cache.reducer'; import { RequestService } from '../../../core/data/request.service'; import { ResponseCacheService } from '../../../core/cache/response-cache.service'; -import { SearchResponseParsingService } from '../../../core/data/search-response-parsing.service'; import { ResponseParsingService } from '../../../core/data/parsing.service'; import { GenericConstructor } from '../../../core/shared/generic-constructor'; - +import { FilteredDiscoveryPageResponseParsingService } from '../../../core/data/filtered-discovery-page-response-parsing.service'; @Injectable() export class SearchFixedFilterService { - private queryByFilterPath = 'config/filtered-discovery-pages'; + private queryByFilterPath = 'filtered-discovery-pages'; constructor(private routeService: RouteService, protected requestService: RequestService, @@ -44,35 +23,31 @@ export class SearchFixedFilterService { } - getQueryByFilterName(filterName?: string): Observable> { + getQueryByFilterName(filterName?: string): Observable { const requestObs = this.halService.getEndpoint(this.queryByFilterPath).pipe( map((url: string) => { url += ('/' + filterName); const request = new GetRequest(this.requestService.generateRequestId(), url); return Object.assign(request, { getResponseParser(): GenericConstructor { - return FilterDiscoveryPageResponseParsingService; + return FilteredDiscoveryPageResponseParsingService; } }); }), ); - const requestEntryObs = requestObs.pipe( - flatMap((request: RestRequest) => this.requestService.getByHref(request.href)) - ); - const responseCacheObs = requestObs.pipe( flatMap((request: RestRequest) => this.responseCache.get(request.href)) ); // get search results from response cache - const fixedFilterConfigObs: Observable = responseCacheObs.pipe( + const filterQuery: Observable = responseCacheObs.pipe( map((entry: ResponseCacheEntry) => entry.response), - map((response: FacetConfigSuccessResponse) => - response.results.map((result: any) => Object.assign(new SearchFilterConfig(), result))) - ); + map((response: FilteredDiscoveryQueryResponse) => + response.filterQuery + )); - return this.rdb.toRemoteDataObservable(requestEntryObs, responseCacheObs, facetConfigObs); + return filterQuery; } } diff --git a/src/app/+search-page/search-options.model.ts b/src/app/+search-page/search-options.model.ts index 770d123f92..64a1df4d2d 100644 --- a/src/app/+search-page/search-options.model.ts +++ b/src/app/+search-page/search-options.model.ts @@ -8,9 +8,12 @@ export class SearchOptions { scope?: string; query?: string; filters?: any; + fixedFilter?: any; toRestUrl(url: string, args: string[] = []): string { - + if (isNotEmpty(this.fixedFilter)) { + args.push(this.fixedFilter); + } if (isNotEmpty(this.query)) { args.push(`query=${this.query}`); } diff --git a/src/app/+search-page/search-page-routing.module.ts b/src/app/+search-page/search-page-routing.module.ts index 65cca99a34..84cb0c31bb 100644 --- a/src/app/+search-page/search-page-routing.module.ts +++ b/src/app/+search-page/search-page-routing.module.ts @@ -2,11 +2,13 @@ import { NgModule } from '@angular/core'; import { RouterModule } from '@angular/router'; import { SearchPageComponent } from './search-page.component'; +import { FilteredSearchPageComponent } from './filtered-search-page.component'; @NgModule({ imports: [ RouterModule.forChild([ - { path: '', component: SearchPageComponent, data: { title: 'search.title' } } + { path: '', component: SearchPageComponent, data: { title: 'search.title' } }, + { path: ':filter', component: FilteredSearchPageComponent, data: { title: 'search.title.:filter' } } ]) ] }) diff --git a/src/app/+search-page/search-page.component.ts b/src/app/+search-page/search-page.component.ts index 4f50723ced..b38386d44c 100644 --- a/src/app/+search-page/search-page.component.ts +++ b/src/app/+search-page/search-page.component.ts @@ -14,6 +14,7 @@ import { SearchFilterService } from './search-filters/search-filter/search-filte import { SearchResult } from './search-result.model'; import { SearchService } from './search-service/search.service'; import { SearchSidebarService } from './search-sidebar/search-sidebar.service'; +import { RouteService } from '../shared/route.service'; /** * This component renders a simple item page. @@ -46,12 +47,14 @@ export class SearchPageComponent implements OnInit { query: '', scope: '' }; + title; - constructor(private service: SearchService, - private communityService: CommunityDataService, - private sidebarService: SearchSidebarService, - private windowService: HostWindowService, - private filterService: SearchFilterService) { + constructor(protected service: SearchService, + protected communityService: CommunityDataService, + protected sidebarService: SearchSidebarService, + protected windowService: HostWindowService, + protected filterService: SearchFilterService, + protected routeService: RouteService) { this.isMobileView$ = Observable.combineLatest( this.windowService.isXs(), this.windowService.isSm(), diff --git a/src/app/+search-page/search-page.module.ts b/src/app/+search-page/search-page.module.ts index 1468fe532e..206d18267d 100644 --- a/src/app/+search-page/search-page.module.ts +++ b/src/app/+search-page/search-page.module.ts @@ -21,6 +21,8 @@ import { SearchFiltersComponent } from './search-filters/search-filters.componen import { SearchFilterComponent } from './search-filters/search-filter/search-filter.component'; import { SearchFacetFilterComponent } from './search-filters/search-filter/search-facet-filter/search-facet-filter.component'; import { SearchFilterService } from './search-filters/search-filter/search-filter.service'; +import { FilteredSearchPageComponent } from './filtered-search-page.component'; +import { SearchFixedFilterService } from './search-filters/search-filter/search-fixed-filter.service'; const effects = [ SearchSidebarEffects @@ -36,6 +38,7 @@ const effects = [ ], declarations: [ SearchPageComponent, + FilteredSearchPageComponent, SearchResultsComponent, SearchSidebarComponent, SearchSettingsComponent, @@ -53,7 +56,8 @@ const effects = [ providers: [ SearchService, SearchSidebarService, - SearchFilterService + SearchFilterService, + SearchFixedFilterService ], entryComponents: [ ItemSearchResultListElementComponent, diff --git a/src/app/core/cache/response-cache.models.ts b/src/app/core/cache/response-cache.models.ts index 81fa9ac759..769027f204 100644 --- a/src/app/core/cache/response-cache.models.ts +++ b/src/app/core/cache/response-cache.models.ts @@ -108,4 +108,14 @@ export class ConfigSuccessResponse extends RestResponse { super(true, statusCode); } } + +export class FilteredDiscoveryQueryResponse extends RestResponse { + constructor( + public filterQuery: string, + public statusCode: string, + public pageInfo?: PageInfo + ) { + super(true, statusCode); + } +} /* tslint:enable:max-classes-per-file */ diff --git a/src/app/core/data/filtered-discovery-page-response-parsing.service.ts b/src/app/core/data/filtered-discovery-page-response-parsing.service.ts new file mode 100644 index 0000000000..ee87230214 --- /dev/null +++ b/src/app/core/data/filtered-discovery-page-response-parsing.service.ts @@ -0,0 +1,24 @@ +import { Inject, Injectable } from '@angular/core'; +import { FilteredDiscoveryQueryResponse, RestResponse } from '../cache/response-cache.models'; +import { ResponseParsingService } from './parsing.service'; +import { RestRequest } from './request.models'; +import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; +import { BaseResponseParsingService } from './base-response-parsing.service'; +import { ObjectCacheService } from '../cache/object-cache.service'; +import { GlobalConfig } from '../../../config/global-config.interface'; +import { GLOBAL_CONFIG } from '../../../config'; + +@Injectable() +export class FilteredDiscoveryPageResponseParsingService extends BaseResponseParsingService implements ResponseParsingService { + objectFactory = {}; + toCache = false; + constructor( + @Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig, + protected objectCache: ObjectCacheService, + ) { super(); + } + parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse { + const query = data.payload['discovery-query']; + return new FilteredDiscoveryQueryResponse(query, data.statusCode); + } +} diff --git a/src/app/shared/route.service.ts b/src/app/shared/route.service.ts index 716a77213b..3eb629e60c 100644 --- a/src/app/shared/route.service.ts +++ b/src/app/shared/route.service.ts @@ -28,6 +28,10 @@ export class RouteService { return this.route.params.map((params) => params[paramName]).distinctUntilChanged(); } + getRouteDataValue(datafield: string): Observable { + return this.route.data.map((data) => data[datafield]).distinctUntilChanged(); + } + getQueryParamsWithPrefix(prefix: string): Observable { return this.route.queryParamMap .map((map) => { From d06e8d93dc2f3b3639fb45992233f3182d0a1ef5 Mon Sep 17 00:00:00 2001 From: lotte Date: Wed, 4 Jul 2018 10:14:43 +0200 Subject: [PATCH 022/604] small fixes --- .../search-fixed-filter.service.ts | 48 ++++++++++--------- .../data/search-response-parsing.service.ts | 2 +- 2 files changed, 27 insertions(+), 23 deletions(-) diff --git a/src/app/+search-page/search-filters/search-filter/search-fixed-filter.service.ts b/src/app/+search-page/search-filters/search-filter/search-fixed-filter.service.ts index f562f4c680..252e4b101e 100644 --- a/src/app/+search-page/search-filters/search-filter/search-fixed-filter.service.ts +++ b/src/app/+search-page/search-filters/search-filter/search-fixed-filter.service.ts @@ -11,6 +11,7 @@ import { ResponseCacheService } from '../../../core/cache/response-cache.service import { ResponseParsingService } from '../../../core/data/parsing.service'; import { GenericConstructor } from '../../../core/shared/generic-constructor'; import { FilteredDiscoveryPageResponseParsingService } from '../../../core/data/filtered-discovery-page-response-parsing.service'; +import { hasValue } from '../../../shared/empty.util'; @Injectable() export class SearchFixedFilterService { @@ -23,31 +24,34 @@ export class SearchFixedFilterService { } - getQueryByFilterName(filterName?: string): Observable { - const requestObs = this.halService.getEndpoint(this.queryByFilterPath).pipe( - map((url: string) => { - url += ('/' + filterName); - const request = new GetRequest(this.requestService.generateRequestId(), url); - return Object.assign(request, { - getResponseParser(): GenericConstructor { - return FilteredDiscoveryPageResponseParsingService; - } - }); - }), - ); + getQueryByFilterName(filterName: string): Observable { + if (hasValue(filterName)) { + const requestObs = this.halService.getEndpoint(this.queryByFilterPath).pipe( + map((url: string) => { + url += ('/' + filterName); + const request = new GetRequest(this.requestService.generateRequestId(), url); + return Object.assign(request, { + getResponseParser(): GenericConstructor { + return FilteredDiscoveryPageResponseParsingService; + } + }); + }), + ); - const responseCacheObs = requestObs.pipe( - flatMap((request: RestRequest) => this.responseCache.get(request.href)) - ); + const responseCacheObs = requestObs.pipe( + flatMap((request: RestRequest) => this.responseCache.get(request.href)) + ); - // get search results from response cache - const filterQuery: Observable = responseCacheObs.pipe( - map((entry: ResponseCacheEntry) => entry.response), - map((response: FilteredDiscoveryQueryResponse) => - response.filterQuery - )); + // get search results from response cache + const filterQuery: Observable = responseCacheObs.pipe( + map((entry: ResponseCacheEntry) => entry.response), + map((response: FilteredDiscoveryQueryResponse) => + response.filterQuery + )); - return filterQuery; + return filterQuery; + } + return Observable.of(undefined); } } diff --git a/src/app/core/data/search-response-parsing.service.ts b/src/app/core/data/search-response-parsing.service.ts index ebefbe4ba4..6b0e8d050a 100644 --- a/src/app/core/data/search-response-parsing.service.ts +++ b/src/app/core/data/search-response-parsing.service.ts @@ -16,7 +16,7 @@ export class SearchResponseParsingService implements ResponseParsingService { } parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse { - const payload = data.payload._embedded.searchResult; + const payload = data.payload/*._embedded.searchResult*/; const hitHighlights = payload._embedded.objects .map((object) => object.hitHighlights) .map((hhObject) => { From cef2ea827b76cfeb2f3d320b07645d6247534401 Mon Sep 17 00:00:00 2001 From: lotte Date: Fri, 6 Jul 2018 13:59:02 +0200 Subject: [PATCH 023/604] finished entity search endpoints --- resources/i18n/en.json | 18 +++++++++++++++ .../filtered-search-page.guard.ts | 18 +++++++++++++++ .../search-filter.service.spec.ts | 9 +++++++- .../search-fixed-filter.service.ts | 3 +++ .../search-page-routing.module.ts | 3 ++- .../+search-page/search-page.component.html | 2 +- src/app/+search-page/search-page.component.ts | 3 ++- src/app/+search-page/search-page.module.ts | 4 +++- src/app/+search-page/search-result.model.ts | 1 - .../search-results.component.html | 2 +- .../search-results.component.ts | 12 +++++++++- src/app/core/core.module.ts | 2 ++ ...discovery-page-response-parsing.service.ts | 2 ++ src/app/shared/route.service.spec.ts | 9 ++++++-- src/app/shared/route.service.ts | 22 ++++++++++++++++--- 15 files changed, 97 insertions(+), 13 deletions(-) create mode 100644 src/app/+search-page/filtered-search-page.guard.ts diff --git a/resources/i18n/en.json b/resources/i18n/en.json index 5e30d08ed0..afd1c48e94 100644 --- a/resources/i18n/en.json +++ b/resources/i18n/en.json @@ -145,6 +145,24 @@ } }, "search": { + "journal": { + "title": "DSpace Angular :: Journal Search", + "results": { + "head": "Journal Search Results" + } + }, + "person": { + "title": "DSpace Angular :: Person Search", + "results": { + "head": "Person Search Results" + } + }, + "publication": { + "title": "DSpace Angular :: Publication Search", + "results": { + "head": "Publication Search Results" + } + }, "title": "DSpace Angular :: Search", "description": "", "form": { diff --git a/src/app/+search-page/filtered-search-page.guard.ts b/src/app/+search-page/filtered-search-page.guard.ts new file mode 100644 index 0000000000..7d022b81da --- /dev/null +++ b/src/app/+search-page/filtered-search-page.guard.ts @@ -0,0 +1,18 @@ +import { Injectable } from '@angular/core'; +import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router'; +import { Observable } from 'rxjs/Observable'; + +@Injectable() + +export class FilteredSearchPageGuard implements CanActivate { + canActivate( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot): Observable | Promise | boolean { + const filter = route.params.filter; + + const newTitle = route.data.title + filter + '.title'; + + route.data = { title: newTitle }; + return true; + } +} diff --git a/src/app/+search-page/search-filters/search-filter/search-filter.service.spec.ts b/src/app/+search-page/search-filters/search-filter/search-filter.service.spec.ts index 26eb961c53..6a112fef1c 100644 --- a/src/app/+search-page/search-filters/search-filter/search-filter.service.spec.ts +++ b/src/app/+search-page/search-filters/search-filter/search-filter.service.spec.ts @@ -10,6 +10,7 @@ import { import { SearchFiltersState } from './search-filter.reducer'; import { SearchFilterConfig } from '../../search-service/search-filter-config.model'; import { FilterType } from '../../search-service/filter-type.model'; +import { SearchFixedFilterService } from './search-fixed-filter.service'; describe('SearchFilterService', () => { let service: SearchFilterService; @@ -21,6 +22,12 @@ describe('SearchFilterService', () => { isOpenByDefault: false, pageSize: 2 }); + + const mockFixedFilterService: SearchFixedFilterService = { + getQueryByFilterName: (filter: string) => { + return Observable.of(undefined) + } + } as SearchFixedFilterService const value1 = 'random value'; // const value2 = 'another value'; const store: Store = jasmine.createSpyObj('store', { @@ -50,7 +57,7 @@ describe('SearchFilterService', () => { }; beforeEach(() => { - service = new SearchFilterService(store, routeServiceStub); + service = new SearchFilterService(store, routeServiceStub, mockFixedFilterService); }); describe('when the initialCollapse method is triggered', () => { diff --git a/src/app/+search-page/search-filters/search-filter/search-fixed-filter.service.ts b/src/app/+search-page/search-filters/search-filter/search-fixed-filter.service.ts index 252e4b101e..fe46417e59 100644 --- a/src/app/+search-page/search-filters/search-filter/search-fixed-filter.service.ts +++ b/src/app/+search-page/search-filters/search-filter/search-fixed-filter.service.ts @@ -12,6 +12,7 @@ import { ResponseParsingService } from '../../../core/data/parsing.service'; import { GenericConstructor } from '../../../core/shared/generic-constructor'; import { FilteredDiscoveryPageResponseParsingService } from '../../../core/data/filtered-discovery-page-response-parsing.service'; import { hasValue } from '../../../shared/empty.util'; +import { configureRequest } from '../../../core/shared/operators'; @Injectable() export class SearchFixedFilterService { @@ -30,12 +31,14 @@ export class SearchFixedFilterService { map((url: string) => { url += ('/' + filterName); const request = new GetRequest(this.requestService.generateRequestId(), url); + console.log(url); return Object.assign(request, { getResponseParser(): GenericConstructor { return FilteredDiscoveryPageResponseParsingService; } }); }), + configureRequest(this.requestService) ); const responseCacheObs = requestObs.pipe( diff --git a/src/app/+search-page/search-page-routing.module.ts b/src/app/+search-page/search-page-routing.module.ts index 84cb0c31bb..8c138c0d52 100644 --- a/src/app/+search-page/search-page-routing.module.ts +++ b/src/app/+search-page/search-page-routing.module.ts @@ -3,12 +3,13 @@ import { RouterModule } from '@angular/router'; import { SearchPageComponent } from './search-page.component'; import { FilteredSearchPageComponent } from './filtered-search-page.component'; +import { FilteredSearchPageGuard } from './filtered-search-page.guard'; @NgModule({ imports: [ RouterModule.forChild([ { path: '', component: SearchPageComponent, data: { title: 'search.title' } }, - { path: ':filter', component: FilteredSearchPageComponent, data: { title: 'search.title.:filter' } } + { path: ':filter', component: FilteredSearchPageComponent, canActivate: [FilteredSearchPageGuard], data: { title: 'search.' }} ]) ] }) diff --git a/src/app/+search-page/search-page.component.html b/src/app/+search-page/search-page.component.html index 1a1f379920..478403388e 100644 --- a/src/app/+search-page/search-page.component.html +++ b/src/app/+search-page/search-page.component.html @@ -30,7 +30,7 @@
+ [searchConfig]="searchOptions$ | async" [sortConfig]="sortConfig" [fixedFilter]="fixedFilter | async">
diff --git a/src/app/+search-page/search-page.component.ts b/src/app/+search-page/search-page.component.ts index b38386d44c..e347688072 100644 --- a/src/app/+search-page/search-page.component.ts +++ b/src/app/+search-page/search-page.component.ts @@ -47,7 +47,7 @@ export class SearchPageComponent implements OnInit { query: '', scope: '' }; - title; + fixedFilter; constructor(protected service: SearchService, protected communityService: CommunityDataService, @@ -68,6 +68,7 @@ export class SearchPageComponent implements OnInit { this.resultsRD$ = this.searchOptions$.pipe( flatMap((searchOptions) => this.service.search(searchOptions)) ); + this.fixedFilter = this.routeService.getRouteParameterValue('filter'); } public closeSidebar(): void { diff --git a/src/app/+search-page/search-page.module.ts b/src/app/+search-page/search-page.module.ts index 206d18267d..25d4b561c3 100644 --- a/src/app/+search-page/search-page.module.ts +++ b/src/app/+search-page/search-page.module.ts @@ -23,6 +23,7 @@ import { SearchFacetFilterComponent } from './search-filters/search-filter/searc import { SearchFilterService } from './search-filters/search-filter/search-filter.service'; import { FilteredSearchPageComponent } from './filtered-search-page.component'; import { SearchFixedFilterService } from './search-filters/search-filter/search-fixed-filter.service'; +import { FilteredSearchPageGuard } from './filtered-search-page.guard'; const effects = [ SearchSidebarEffects @@ -57,7 +58,8 @@ const effects = [ SearchService, SearchSidebarService, SearchFilterService, - SearchFixedFilterService + SearchFixedFilterService, + FilteredSearchPageGuard ], entryComponents: [ ItemSearchResultListElementComponent, diff --git a/src/app/+search-page/search-result.model.ts b/src/app/+search-page/search-result.model.ts index 2298f453e1..cc2bd8cd58 100644 --- a/src/app/+search-page/search-result.model.ts +++ b/src/app/+search-page/search-result.model.ts @@ -1,6 +1,5 @@ import { DSpaceObject } from '../core/shared/dspace-object.model'; import { Metadatum } from '../core/shared/metadatum.model'; -import { hasNoValue, isEmpty } from '../shared/empty.util'; import { ListableObject } from '../shared/object-collection/shared/listable-object.model'; export class SearchResult implements ListableObject { diff --git a/src/app/+search-page/search-results/search-results.component.html b/src/app/+search-page/search-results/search-results.component.html index ed6fc18d9c..a9b54a4601 100644 --- a/src/app/+search-page/search-results/search-results.component.html +++ b/src/app/+search-page/search-results/search-results.component.html @@ -1,4 +1,4 @@ -

{{ 'search.results.head' | translate }}

+

{{ getTitleKey() | translate }}

{ let service: RouteService; @@ -28,12 +30,15 @@ describe('RouteService', () => { queryParamMap: Observable.of(convertToParamMap(paramObject)) }, }, + { + provide: Router, useClass: RouterStub + } ] }); })); beforeEach(() => { - service = new RouteService(TestBed.get(ActivatedRoute)); + service = new RouteService(TestBed.get(ActivatedRoute), TestBed.get(Router)); }); describe('hasQueryParam', () => { diff --git a/src/app/shared/route.service.ts b/src/app/shared/route.service.ts index 3eb629e60c..f374f63432 100644 --- a/src/app/shared/route.service.ts +++ b/src/app/shared/route.service.ts @@ -1,11 +1,15 @@ import { Injectable } from '@angular/core'; import { Observable } from 'rxjs/Observable'; -import { ActivatedRoute, Params, } from '@angular/router'; +import { ActivatedRoute, NavigationEnd, Params, Router, } from '@angular/router'; +import { filter } from 'rxjs/operators'; @Injectable() export class RouteService { + params: Observable; + + constructor(private route: ActivatedRoute, private router: Router) { + this.subscribeToRouterParams(); - constructor(private route: ActivatedRoute) { } getQueryParameterValues(paramName: string): Observable { @@ -25,7 +29,7 @@ export class RouteService { } getRouteParameterValue(paramName: string): Observable { - return this.route.params.map((params) => params[paramName]).distinctUntilChanged(); + return this.params.map((params) => params[paramName]).distinctUntilChanged(); } getRouteDataValue(datafield: string): Observable { @@ -44,4 +48,16 @@ export class RouteService { return params; }).distinctUntilChanged(); } + + subscribeToRouterParams() { + this.router.events.pipe( + filter((event) => event instanceof NavigationEnd)) + .subscribe(() => { + let active = this.route; + while (active.firstChild) { + active = active.firstChild; + } + this.params = active.params; + }); + } } From b68ed37cb3ce5857b48662bfed81635ca1560612 Mon Sep 17 00:00:00 2001 From: lotte Date: Fri, 6 Jul 2018 14:07:11 +0200 Subject: [PATCH 024/604] 53029: removed console logs and small bugs --- .../search-filter/search-fixed-filter.service.ts | 1 - .../search-settings/search-settings.component.ts | 7 +++---- src/app/core/data/base-response-parsing.service.ts | 3 +-- .../filtered-discovery-page-response-parsing.service.ts | 2 -- 4 files changed, 4 insertions(+), 9 deletions(-) diff --git a/src/app/+search-page/search-filters/search-filter/search-fixed-filter.service.ts b/src/app/+search-page/search-filters/search-filter/search-fixed-filter.service.ts index fe46417e59..045c282fa3 100644 --- a/src/app/+search-page/search-filters/search-filter/search-fixed-filter.service.ts +++ b/src/app/+search-page/search-filters/search-filter/search-fixed-filter.service.ts @@ -31,7 +31,6 @@ export class SearchFixedFilterService { map((url: string) => { url += ('/' + filterName); const request = new GetRequest(this.requestService.generateRequestId(), url); - console.log(url); return Object.assign(request, { getResponseParser(): GenericConstructor { return FilteredDiscoveryPageResponseParsingService; diff --git a/src/app/+search-page/search-settings/search-settings.component.ts b/src/app/+search-page/search-settings/search-settings.component.ts index bd26663583..8c7925df9d 100644 --- a/src/app/+search-page/search-settings/search-settings.component.ts +++ b/src/app/+search-page/search-settings/search-settings.component.ts @@ -1,7 +1,6 @@ -import { Component, Input, OnInit, ChangeDetectionStrategy } from '@angular/core'; +import { Component, Input, OnInit } from '@angular/core'; import { SetViewMode } from '../../shared/view-mode'; import { SearchService } from '../search-service/search.service'; -import { SearchOptions} from '../search-options.model'; import { SortDirection } from '../../core/cache/models/sort-options.model'; import { ActivatedRoute, NavigationExtras, Router } from '@angular/router'; import { PaginatedSearchOptions } from '../paginated-search-options.model'; @@ -64,7 +63,7 @@ export class SearchSettingsComponent implements OnInit { pageSize: value }) }; - this.router.navigate([ '/search' ], navigationExtras); + this.router.navigate([ this.service.getSearchLink() ], navigationExtras); } reloadOrder(event: Event) { @@ -74,6 +73,6 @@ export class SearchSettingsComponent implements OnInit { sortDirection: value }) }; - this.router.navigate([ '/search' ], navigationExtras); + this.router.navigate([ this.service.getSearchLink() ], navigationExtras); } } diff --git a/src/app/core/data/base-response-parsing.service.ts b/src/app/core/data/base-response-parsing.service.ts index 3f64be7515..a9b399e827 100644 --- a/src/app/core/data/base-response-parsing.service.ts +++ b/src/app/core/data/base-response-parsing.service.ts @@ -6,14 +6,13 @@ import { ObjectCacheService } from '../cache/object-cache.service'; import { GlobalConfig } from '../../../config/global-config.interface'; import { GenericConstructor } from '../shared/generic-constructor'; import { PaginatedList } from './paginated-list'; -import { NormalizedObject } from '../cache/models/normalized-object.model'; function isObjectLevel(halObj: any) { return isNotEmpty(halObj._links) && hasValue(halObj._links.self); } function isPaginatedResponse(halObj: any) { - return isNotEmpty(halObj.page) /** && hasValue(halObj._embedded)**/; + return isNotEmpty(halObj.page) /* && hasValue(halObj._embedded)*/; } /* tslint:disable:max-classes-per-file */ diff --git a/src/app/core/data/filtered-discovery-page-response-parsing.service.ts b/src/app/core/data/filtered-discovery-page-response-parsing.service.ts index ec1f2fbe2c..ee87230214 100644 --- a/src/app/core/data/filtered-discovery-page-response-parsing.service.ts +++ b/src/app/core/data/filtered-discovery-page-response-parsing.service.ts @@ -18,8 +18,6 @@ export class FilteredDiscoveryPageResponseParsingService extends BaseResponsePar ) { super(); } parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse { - console.log('test'); - const query = data.payload['discovery-query']; return new FilteredDiscoveryQueryResponse(query, data.statusCode); } From 819610476ef91fcee5748d3f49788ce79e2c86c6 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Fri, 20 Jul 2018 16:04:02 +0200 Subject: [PATCH 025/604] 54472: Intermediate commit --- .../create-community-page.component.html | 0 .../create-community-page.component.ts | 9 +++++++++ 2 files changed, 9 insertions(+) create mode 100644 src/app/+community-page/create-community-page/create-community-page.component.html create mode 100644 src/app/+community-page/create-community-page/create-community-page.component.ts diff --git a/src/app/+community-page/create-community-page/create-community-page.component.html b/src/app/+community-page/create-community-page/create-community-page.component.html new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/+community-page/create-community-page/create-community-page.component.ts b/src/app/+community-page/create-community-page/create-community-page.component.ts new file mode 100644 index 0000000000..ffc6642cdf --- /dev/null +++ b/src/app/+community-page/create-community-page/create-community-page.component.ts @@ -0,0 +1,9 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'ds-create-community', + templateUrl: './create-community-page.component.html' +}) +export class CreateCommunityPageComponent { + +} From 0da645931cf5eea983abd805c5547189d10f0312 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Mon, 23 Jul 2018 14:29:32 +0200 Subject: [PATCH 026/604] 54472: Create Community page --- resources/i18n/en.json | 15 ++++++++++ .../community-form.component.html | 27 +++++++++++++++++ .../community-form.component.scss | 7 +++++ .../community-form.component.ts | 30 +++++++++++++++++++ .../community-page-routing.module.ts | 2 ++ .../+community-page/community-page.module.ts | 4 +++ .../create-community-page.component.html | 8 +++++ .../create-community-page.component.scss | 1 + .../create-community-page.component.ts | 5 ++++ src/app/core/comcol/comcol.service.ts | 0 10 files changed, 99 insertions(+) create mode 100644 src/app/+community-page/community-form/community-form.component.html create mode 100644 src/app/+community-page/community-form/community-form.component.scss create mode 100644 src/app/+community-page/community-form/community-form.component.ts create mode 100644 src/app/+community-page/create-community-page/create-community-page.component.scss create mode 100644 src/app/core/comcol/comcol.service.ts diff --git a/resources/i18n/en.json b/resources/i18n/en.json index ba70b87e12..c0a213fe1b 100644 --- a/resources/i18n/en.json +++ b/resources/i18n/en.json @@ -22,6 +22,21 @@ }, "sub-collection-list": { "head": "Collections of this Community" + }, + "edit": { + "name": "Name", + "description": "Short Description", + "introductory": "Introductory text (HTML)", + "copyright": "Copyright text (HTML)", + "news": "News (HTML)", + "submit": "Submit", + "cancel": "Cancel", + "required": { + "name": "Please enter a community name" + } + }, + "create": { + "head": "Create a Community" } }, "item": { diff --git a/src/app/+community-page/community-form/community-form.component.html b/src/app/+community-page/community-form/community-form.component.html new file mode 100644 index 0000000000..bc45b582ac --- /dev/null +++ b/src/app/+community-page/community-form/community-form.component.html @@ -0,0 +1,27 @@ +
+
+ + +
{{ 'community.edit.required.name' | translate }}
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
diff --git a/src/app/+community-page/community-form/community-form.component.scss b/src/app/+community-page/community-form/community-form.component.scss new file mode 100644 index 0000000000..d5811186e7 --- /dev/null +++ b/src/app/+community-page/community-form/community-form.component.scss @@ -0,0 +1,7 @@ +@import '../../../styles/variables.scss'; + +// temporary fix for bootstrap 4 beta btn color issue +.btn-secondary { + background-color: $input-bg; + color: $input-color; +} diff --git a/src/app/+community-page/community-form/community-form.component.ts b/src/app/+community-page/community-form/community-form.component.ts new file mode 100644 index 0000000000..64d08ab862 --- /dev/null +++ b/src/app/+community-page/community-form/community-form.component.ts @@ -0,0 +1,30 @@ +import { Component, EventEmitter, Output } from '@angular/core'; +import { isNotEmpty } from '../../shared/empty.util'; + +@Component({ + selector: 'ds-community-form', + styleUrls: ['./community-form.component.scss'], + templateUrl: './community-form.component.html' +}) +export class CommunityFormComponent { + + name: string; + description: string; + introductory: string; + copyright: string; + news: string; + + nameRequiredError = false; + + @Output() submitted: EventEmitter = new EventEmitter(); + + onSubmit(data: any) { + if (isNotEmpty(data.name)) { + this.submitted.emit(data); + this.nameRequiredError = false; + } else { + this.nameRequiredError = true; + } + } + +} diff --git a/src/app/+community-page/community-page-routing.module.ts b/src/app/+community-page/community-page-routing.module.ts index 6fd5cc8cb5..249b01ea18 100644 --- a/src/app/+community-page/community-page-routing.module.ts +++ b/src/app/+community-page/community-page-routing.module.ts @@ -2,10 +2,12 @@ import { NgModule } from '@angular/core'; import { RouterModule } from '@angular/router'; import { CommunityPageComponent } from './community-page.component'; +import { CreateCommunityPageComponent } from './create-community-page/create-community-page.component'; @NgModule({ imports: [ RouterModule.forChild([ + { path: 'create', component: CreateCommunityPageComponent }, { path: ':id', component: CommunityPageComponent, pathMatch: 'full' } ]) ] diff --git a/src/app/+community-page/community-page.module.ts b/src/app/+community-page/community-page.module.ts index e00c3910c5..292e6aaf9c 100644 --- a/src/app/+community-page/community-page.module.ts +++ b/src/app/+community-page/community-page.module.ts @@ -6,6 +6,8 @@ import { SharedModule } from '../shared/shared.module'; import { CommunityPageComponent } from './community-page.component'; import { CommunityPageSubCollectionListComponent } from './sub-collection-list/community-page-sub-collection-list.component'; import { CommunityPageRoutingModule } from './community-page-routing.module'; +import { CreateCommunityPageComponent } from './create-community-page/create-community-page.component'; +import { CommunityFormComponent } from './community-form/community-form.component'; @NgModule({ imports: [ @@ -16,6 +18,8 @@ import { CommunityPageRoutingModule } from './community-page-routing.module'; declarations: [ CommunityPageComponent, CommunityPageSubCollectionListComponent, + CreateCommunityPageComponent, + CommunityFormComponent ] }) export class CommunityPageModule { diff --git a/src/app/+community-page/create-community-page/create-community-page.component.html b/src/app/+community-page/create-community-page/create-community-page.component.html index e69de29bb2..ea270db92b 100644 --- a/src/app/+community-page/create-community-page/create-community-page.component.html +++ b/src/app/+community-page/create-community-page/create-community-page.component.html @@ -0,0 +1,8 @@ +
+
+
+ +
+
+ +
diff --git a/src/app/+community-page/create-community-page/create-community-page.component.scss b/src/app/+community-page/create-community-page/create-community-page.component.scss new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/src/app/+community-page/create-community-page/create-community-page.component.scss @@ -0,0 +1 @@ + diff --git a/src/app/+community-page/create-community-page/create-community-page.component.ts b/src/app/+community-page/create-community-page/create-community-page.component.ts index ffc6642cdf..db2e4f25fa 100644 --- a/src/app/+community-page/create-community-page/create-community-page.component.ts +++ b/src/app/+community-page/create-community-page/create-community-page.component.ts @@ -2,8 +2,13 @@ import { Component } from '@angular/core'; @Component({ selector: 'ds-create-community', + styleUrls: ['./create-community-page.component.scss'], templateUrl: './create-community-page.component.html' }) export class CreateCommunityPageComponent { + onSubmit(data: any) { + console.log('yay, made it with name: ' + data.name); + } + } diff --git a/src/app/core/comcol/comcol.service.ts b/src/app/core/comcol/comcol.service.ts new file mode 100644 index 0000000000..e69de29bb2 From 6f60cd68e282c6988ddb6c5ea2d5cf2d43619753 Mon Sep 17 00:00:00 2001 From: Giuseppe Date: Thu, 26 Jul 2018 18:36:36 +0200 Subject: [PATCH 027/604] Merged submission module code --- config/environment.default.js | 37 + package.json | 4 +- resources/i18n/en.json | 95 ++ src/app/+item-page/item-page.module.ts | 4 +- .../+login-page/login-page-routing.module.ts | 2 +- src/app/+login-page/login-page.component.ts | 49 +- .../submit-page-routing.module.ts | 20 + src/app/+submit-page/submit-page.module.ts | 17 + .../workflowitems-edit-page-routing.module.ts | 20 + .../workflowitems-edit-page.module.ts | 18 + ...workspaceitems-edit-page-routing.module.ts | 20 + .../workspaceitems-edit-page.module.ts | 18 + src/app/app-routing.module.ts | 3 + src/app/app.component.html | 2 - src/app/app.component.scss | 6 +- src/app/app.module.ts | 2 + .../models/normalized-collection.model.ts | 16 +- .../cache/models/normalized-license.model.ts | 21 + .../cache/models/normalized-object-factory.ts | 21 + .../normalized-resource-policy.model.ts | 45 + src/app/core/cache/response-cache.models.ts | 31 + .../submission-uploads-config.service.ts | 18 + src/app/core/core.effects.ts | 4 +- src/app/core/core.module.ts | 29 +- src/app/core/core.reducers.ts | 5 +- .../data/base-response-parsing.service.ts | 2 +- src/app/core/data/collection-data.service.ts | 1 + src/app/core/data/comcol-data.service.spec.ts | 1 + src/app/core/data/community-data.service.ts | 1 + .../data/config-response-parsing.service.ts | 2 +- src/app/core/data/data.service.ts | 111 +- src/app/core/data/item-data.service.ts | 1 + src/app/core/data/request.models.ts | 85 +- src/app/core/data/request.service.ts | 28 +- src/app/core/eperson/eperson-data.ts | 12 + .../core/eperson/eperson-object-factory.ts | 21 + .../eperson-response-parsing.service.ts | 47 + src/app/core/eperson/eperson-type.ts | 5 + src/app/core/eperson/eperson.service.ts | 53 + src/app/core/eperson/group-eperson.service.ts | 55 + .../eperson/models/NormalizedEperson.model.ts | 9 +- src/app/core/eperson/models/group.model.ts | 2 + .../models/authority-value.model.ts | 5 + .../json-patch-operation-path-combiner.ts | 45 + .../builder/json-patch-operations-builder.ts | 111 ++ .../json-patch-operations.actions.ts | 279 +++++ .../json-patch-operations.effects.ts | 20 + .../json-patch-operations.reducer.ts | 292 +++++ .../json-patch-operations.service.ts | 127 ++ src/app/core/json-patch/json-patch.model.ts | 14 + src/app/core/json-patch/selectors.ts | 34 + src/app/core/shared/collection.model.ts | 14 +- .../config-access-condition-option.model.ts | 8 + .../shared/config/config-object-factory.ts | 8 +- .../config/config-submission-section.model.ts | 5 +- .../config/config-submission-uploads.model.ts | 21 + src/app/core/shared/config/config-type.ts | 4 +- src/app/core/shared/dspace-object.model.ts | 6 +- src/app/core/shared/file.service.ts | 35 + src/app/core/shared/hal-endpoint.service.ts | 3 +- src/app/core/shared/item.model.ts | 2 + src/app/core/shared/license.model.ts | 14 + src/app/core/shared/operators.ts | 4 +- src/app/core/shared/patch-request.model.ts | 14 + src/app/core/shared/resource-policy.model.ts | 34 + src/app/core/shared/resource-type.ts | 4 + .../submit-data-response-definition.model.ts | 11 + .../core/submission/models/edititem.model.ts | 4 + .../models/normalized-edititem.model.ts | 47 + .../normalized-submission-object.model.ts | 8 + .../models/normalized-workflowitem.model.ts | 47 + .../models/normalized-workspaceitem.model.ts | 51 + .../models/submission-object.model.ts | 43 + ...sion-upload-file-access-condition.model.ts | 7 + .../submission/models/workflowitem.model.ts | 4 + ...rkspaceitem-section-deduplication.model.ts | 21 + .../workspaceitem-section-form.model.ts | 5 + .../workspaceitem-section-license.model.ts | 5 + .../workspaceitem-section-recycle.model.ts | 8 + ...workspaceitem-section-upload-file.model.ts | 15 + .../workspaceitem-section-upload.model.ts | 5 + .../models/workspaceitem-sections.model.ts | 65 ++ .../submission/models/workspaceitem.model.ts | 5 + .../normalized-submission-object-factory.ts | 69 ++ .../submission/submission-resource-type.ts | 24 + .../submission-response-parsing.service.ts | 99 ++ .../core/submission/submission-scope-type.ts | 5 + .../submission/workflowitem-data.service.ts | 35 + .../submission/workspaceitem-data.service.ts | 35 + src/app/shared/alerts/alerts.component.html | 9 + src/app/shared/alerts/alerts.component.scss | 3 + src/app/shared/alerts/alerts.component.ts | 44 + src/app/shared/alerts/aletrs-type.ts | 6 + src/app/shared/empty.util.ts | 45 + .../dynamic-group/dynamic-group.components.ts | 3 +- .../shared/form/builder/parsers/row-parser.ts | 3 +- src/app/shared/shared.module.ts | 4 +- .../truncatable-part.component.ts | 5 +- .../shared/uploader/uploader.component.scss | 2 +- .../edit/submission-edit.component.html | 7 + .../edit/submission-edit.component.scss | 0 .../edit/submission-edit.component.ts | 74 ++ .../submission-form-collection.component.html | 37 + .../submission-form-collection.component.scss | 17 + .../submission-form-collection.component.ts | 168 +++ .../submission-form-footer.component.html | 52 + .../submission-form-footer.component.scss | 0 .../submission-form-footer.component.ts | 70 ++ ...submission-form-section-add.component.html | 14 + ...submission-form-section-add.component.scss | 9 + .../submission-form-section-add.component.ts | 33 + .../form/submission-form.component.html | 36 + .../form/submission-form.component.scss | 21 + .../form/submission-form.component.ts | 139 +++ .../submission-upload-files.component.html | 7 + .../submission-upload-files.component.ts | 96 ++ .../objects/submission-objects.actions.ts | 1032 +++++++++++++++++ .../objects/submission-objects.effects.ts | 338 ++++++ .../objects/submission-objects.reducer.ts | 844 ++++++++++++++ .../section-container.component.html | 47 + .../section-container.component.scss | 11 + .../container/section-container.component.ts | 52 + .../deduplication/deduplication.service.ts | 51 + .../match/deduplication-match.component.html | 89 ++ .../match/deduplication-match.component.ts | 169 +++ .../section-deduplication.component.html | 39 + .../section-deduplication.component.ts | 73 ++ .../default/section-default.component.html | 6 + .../default/section-default.component.scss | 0 .../default/section-default.component.ts | 21 + .../sections/form/form-operations.service.ts | 242 ++++ .../sections/form/section-form.component.html | 9 + .../sections/form/section-form.component.scss | 1 + .../sections/form/section-form.component.ts | 248 ++++ .../license/section-license.component.html | 7 + .../license/section-license.component.scss | 0 .../license/section-license.component.ts | 139 +++ .../sections/license/section-license.model.ts | 17 + .../sections/models/section-data.model.ts | 15 + .../sections/models/section.model.ts | 24 + .../recycle/section-recycle.component.html | 39 + .../recycle/section-recycle.component.scss | 0 .../recycle/section-recycle.component.ts | 55 + .../submission/sections/sections-decorator.ts | 16 + src/app/submission/sections/sections-type.ts | 9 + .../submission/sections/sections.directive.ts | 155 +++ .../submission/sections/sections.service.ts | 154 +++ .../accessConditions.component.html | 9 + .../accessConditions.component.ts | 35 + .../upload/file/edit/file-edit.component.html | 8 + .../upload/file/edit/file-edit.component.ts | 259 +++++ .../upload/file/edit/files-edit.model.ts | 115 ++ .../sections/upload/file/file.component.html | 56 + .../sections/upload/file/file.component.ts | 181 +++ .../upload/file/view/file-view.component.html | 25 + .../upload/file/view/file-view.component.ts | 29 + .../upload/section-upload.component.html | 50 + .../upload/section-upload.component.scss | 0 .../upload/section-upload.component.ts | 185 +++ .../sections/upload/section-upload.service.ts | 60 + src/app/submission/selectors.ts | 60 + .../submission/server-submission.service.ts | 24 + src/app/submission/submission-rest.service.ts | 124 ++ src/app/submission/submission.effects.ts | 5 + src/app/submission/submission.module.ts | 90 ++ src/app/submission/submission.reducers.ts | 16 + src/app/submission/submission.service.ts | 223 ++++ .../submit/submission-submit.component.html | 8 + .../submit/submission-submit.component.scss | 0 .../submit/submission-submit.component.ts | 67 ++ .../utils/parseSectionErrorPaths.ts | 41 + .../submission/utils/parseSectionErrors.ts | 27 + src/config/global-config.interface.ts | 2 + src/config/submission-config.interface.ts | 16 + src/modules/app/browser-app.module.ts | 6 + src/modules/app/server-app.module.ts | 8 +- src/routes.ts | 5 + src/styles/_custom_variables.scss | 2 + yarn.lock | 14 +- 179 files changed, 9143 insertions(+), 77 deletions(-) create mode 100644 src/app/+submit-page/submit-page-routing.module.ts create mode 100644 src/app/+submit-page/submit-page.module.ts create mode 100644 src/app/+workflowitems-edit-page/workflowitems-edit-page-routing.module.ts create mode 100644 src/app/+workflowitems-edit-page/workflowitems-edit-page.module.ts create mode 100644 src/app/+workspaceitems-edit-page/workspaceitems-edit-page-routing.module.ts create mode 100644 src/app/+workspaceitems-edit-page/workspaceitems-edit-page.module.ts create mode 100644 src/app/core/cache/models/normalized-license.model.ts create mode 100644 src/app/core/cache/models/normalized-resource-policy.model.ts create mode 100644 src/app/core/config/submission-uploads-config.service.ts create mode 100644 src/app/core/eperson/eperson-data.ts create mode 100644 src/app/core/eperson/eperson-object-factory.ts create mode 100644 src/app/core/eperson/eperson-response-parsing.service.ts create mode 100644 src/app/core/eperson/eperson-type.ts create mode 100644 src/app/core/eperson/eperson.service.ts create mode 100644 src/app/core/eperson/group-eperson.service.ts create mode 100644 src/app/core/json-patch/builder/json-patch-operation-path-combiner.ts create mode 100644 src/app/core/json-patch/builder/json-patch-operations-builder.ts create mode 100644 src/app/core/json-patch/json-patch-operations.actions.ts create mode 100644 src/app/core/json-patch/json-patch-operations.effects.ts create mode 100644 src/app/core/json-patch/json-patch-operations.reducer.ts create mode 100644 src/app/core/json-patch/json-patch-operations.service.ts create mode 100644 src/app/core/json-patch/json-patch.model.ts create mode 100644 src/app/core/json-patch/selectors.ts create mode 100644 src/app/core/shared/config/config-access-condition-option.model.ts create mode 100644 src/app/core/shared/config/config-submission-uploads.model.ts create mode 100644 src/app/core/shared/file.service.ts create mode 100644 src/app/core/shared/license.model.ts create mode 100644 src/app/core/shared/patch-request.model.ts create mode 100644 src/app/core/shared/resource-policy.model.ts create mode 100644 src/app/core/shared/submit-data-response-definition.model.ts create mode 100644 src/app/core/submission/models/edititem.model.ts create mode 100644 src/app/core/submission/models/normalized-edititem.model.ts create mode 100644 src/app/core/submission/models/normalized-submission-object.model.ts create mode 100644 src/app/core/submission/models/normalized-workflowitem.model.ts create mode 100644 src/app/core/submission/models/normalized-workspaceitem.model.ts create mode 100644 src/app/core/submission/models/submission-object.model.ts create mode 100644 src/app/core/submission/models/submission-upload-file-access-condition.model.ts create mode 100644 src/app/core/submission/models/workflowitem.model.ts create mode 100644 src/app/core/submission/models/workspaceitem-section-deduplication.model.ts create mode 100644 src/app/core/submission/models/workspaceitem-section-form.model.ts create mode 100644 src/app/core/submission/models/workspaceitem-section-license.model.ts create mode 100644 src/app/core/submission/models/workspaceitem-section-recycle.model.ts create mode 100644 src/app/core/submission/models/workspaceitem-section-upload-file.model.ts create mode 100644 src/app/core/submission/models/workspaceitem-section-upload.model.ts create mode 100644 src/app/core/submission/models/workspaceitem-sections.model.ts create mode 100644 src/app/core/submission/models/workspaceitem.model.ts create mode 100644 src/app/core/submission/normalized-submission-object-factory.ts create mode 100644 src/app/core/submission/submission-resource-type.ts create mode 100644 src/app/core/submission/submission-response-parsing.service.ts create mode 100644 src/app/core/submission/submission-scope-type.ts create mode 100644 src/app/core/submission/workflowitem-data.service.ts create mode 100644 src/app/core/submission/workspaceitem-data.service.ts create mode 100644 src/app/shared/alerts/alerts.component.html create mode 100644 src/app/shared/alerts/alerts.component.scss create mode 100644 src/app/shared/alerts/alerts.component.ts create mode 100644 src/app/shared/alerts/aletrs-type.ts create mode 100644 src/app/submission/edit/submission-edit.component.html create mode 100644 src/app/submission/edit/submission-edit.component.scss create mode 100644 src/app/submission/edit/submission-edit.component.ts create mode 100644 src/app/submission/form/collection/submission-form-collection.component.html create mode 100644 src/app/submission/form/collection/submission-form-collection.component.scss create mode 100644 src/app/submission/form/collection/submission-form-collection.component.ts create mode 100644 src/app/submission/form/footer/submission-form-footer.component.html create mode 100644 src/app/submission/form/footer/submission-form-footer.component.scss create mode 100644 src/app/submission/form/footer/submission-form-footer.component.ts create mode 100644 src/app/submission/form/section-add/submission-form-section-add.component.html create mode 100644 src/app/submission/form/section-add/submission-form-section-add.component.scss create mode 100644 src/app/submission/form/section-add/submission-form-section-add.component.ts create mode 100644 src/app/submission/form/submission-form.component.html create mode 100644 src/app/submission/form/submission-form.component.scss create mode 100644 src/app/submission/form/submission-form.component.ts create mode 100644 src/app/submission/form/submission-upload-files/submission-upload-files.component.html create mode 100644 src/app/submission/form/submission-upload-files/submission-upload-files.component.ts create mode 100644 src/app/submission/objects/submission-objects.actions.ts create mode 100644 src/app/submission/objects/submission-objects.effects.ts create mode 100644 src/app/submission/objects/submission-objects.reducer.ts create mode 100644 src/app/submission/sections/container/section-container.component.html create mode 100644 src/app/submission/sections/container/section-container.component.scss create mode 100644 src/app/submission/sections/container/section-container.component.ts create mode 100644 src/app/submission/sections/deduplication/deduplication.service.ts create mode 100644 src/app/submission/sections/deduplication/match/deduplication-match.component.html create mode 100644 src/app/submission/sections/deduplication/match/deduplication-match.component.ts create mode 100644 src/app/submission/sections/deduplication/section-deduplication.component.html create mode 100644 src/app/submission/sections/deduplication/section-deduplication.component.ts create mode 100644 src/app/submission/sections/default/section-default.component.html create mode 100644 src/app/submission/sections/default/section-default.component.scss create mode 100644 src/app/submission/sections/default/section-default.component.ts create mode 100644 src/app/submission/sections/form/form-operations.service.ts create mode 100644 src/app/submission/sections/form/section-form.component.html create mode 100644 src/app/submission/sections/form/section-form.component.scss create mode 100644 src/app/submission/sections/form/section-form.component.ts create mode 100644 src/app/submission/sections/license/section-license.component.html create mode 100644 src/app/submission/sections/license/section-license.component.scss create mode 100644 src/app/submission/sections/license/section-license.component.ts create mode 100644 src/app/submission/sections/license/section-license.model.ts create mode 100644 src/app/submission/sections/models/section-data.model.ts create mode 100644 src/app/submission/sections/models/section.model.ts create mode 100644 src/app/submission/sections/recycle/section-recycle.component.html create mode 100644 src/app/submission/sections/recycle/section-recycle.component.scss create mode 100644 src/app/submission/sections/recycle/section-recycle.component.ts create mode 100644 src/app/submission/sections/sections-decorator.ts create mode 100644 src/app/submission/sections/sections-type.ts create mode 100644 src/app/submission/sections/sections.directive.ts create mode 100644 src/app/submission/sections/sections.service.ts create mode 100644 src/app/submission/sections/upload/accessConditions/accessConditions.component.html create mode 100644 src/app/submission/sections/upload/accessConditions/accessConditions.component.ts create mode 100644 src/app/submission/sections/upload/file/edit/file-edit.component.html create mode 100644 src/app/submission/sections/upload/file/edit/file-edit.component.ts create mode 100644 src/app/submission/sections/upload/file/edit/files-edit.model.ts create mode 100644 src/app/submission/sections/upload/file/file.component.html create mode 100644 src/app/submission/sections/upload/file/file.component.ts create mode 100644 src/app/submission/sections/upload/file/view/file-view.component.html create mode 100644 src/app/submission/sections/upload/file/view/file-view.component.ts create mode 100644 src/app/submission/sections/upload/section-upload.component.html create mode 100644 src/app/submission/sections/upload/section-upload.component.scss create mode 100644 src/app/submission/sections/upload/section-upload.component.ts create mode 100644 src/app/submission/sections/upload/section-upload.service.ts create mode 100644 src/app/submission/selectors.ts create mode 100644 src/app/submission/server-submission.service.ts create mode 100644 src/app/submission/submission-rest.service.ts create mode 100644 src/app/submission/submission.effects.ts create mode 100644 src/app/submission/submission.module.ts create mode 100644 src/app/submission/submission.reducers.ts create mode 100644 src/app/submission/submission.service.ts create mode 100644 src/app/submission/submit/submission-submit.component.html create mode 100644 src/app/submission/submit/submission-submit.component.scss create mode 100644 src/app/submission/submit/submission-submit.component.ts create mode 100644 src/app/submission/utils/parseSectionErrorPaths.ts create mode 100644 src/app/submission/utils/parseSectionErrors.ts create mode 100644 src/config/submission-config.interface.ts diff --git a/config/environment.default.js b/config/environment.default.js index a6ef738f41..1d121f5fbe 100644 --- a/config/environment.default.js +++ b/config/environment.default.js @@ -41,6 +41,43 @@ module.exports = { // NOTE: 'fade' | 'fromTop' | 'fromRight' | 'fromBottom' | 'fromLeft' | 'rotate' | 'scale' animate: 'scale' }, + // Submission settings + submission: { + autosave: { + // NOTE: which metadata trigger an autosave + metadata: ['dc.title', 'dc.identifier.doi', 'dc.identifier.pmid', 'dc.identifier.arxiv'], + // NOTE: every how many minutes submission is saved automatically + timer: 5 + }, + metadata: { + // NOTE: allow to set icons used to represent metadata belonging to a relation group + icons: [ + /** + * NOTE: example of configuration + * { + * // NOTE: metadata name + * name: 'dc.author', + * config: { + * // NOTE: used when metadata value has an authority + * withAuthority: { + * // NOTE: fontawesome (v4.x) icon classes and bootstrap color utility classes can be used + * style: 'fa-user' + * }, + * // NOTE: used when metadata value has not an authority + * withoutAuthority: { + * style: 'fa-user text-muted' + * } + * } + * } + */ + // default configuration + { + name: 'default', + config: {} + } + ] + } + }, // Angular Universal settings universal: { preboot: true, diff --git a/package.json b/package.json index 7ded007e83..c44674844e 100644 --- a/package.json +++ b/package.json @@ -79,7 +79,7 @@ "@angular/platform-server": "^5.2.5", "@angular/router": "^5.2.5", "@angularclass/bootloader": "1.0.1", - "@ng-bootstrap/ng-bootstrap": "^1.0.0", + "@ng-bootstrap/ng-bootstrap": "1.1.2", "@ng-dynamic-forms/core": "5.4.7", "@ng-dynamic-forms/ui-ng-bootstrap": "5.4.7", "@ngrx/effects": "^5.1.0", @@ -101,6 +101,7 @@ "core-js": "2.5.3", "express": "4.16.2", "express-session": "1.15.6", + "file-saver": "^1.3.8", "font-awesome": "4.7.0", "http-server": "0.11.1", "https": "1.0.0", @@ -133,6 +134,7 @@ "@types/deep-freeze": "0.1.1", "@types/express": "^4.11.1", "@types/express-serve-static-core": "4.11.1", + "@types/file-saver": "^1.3.0", "@types/hammerjs": "2.0.35", "@types/jasmine": "^2.8.6", "@types/js-cookie": "2.1.0", diff --git a/resources/i18n/en.json b/resources/i18n/en.json index ba70b87e12..c2c8d8c464 100644 --- a/resources/i18n/en.json +++ b/resources/i18n/en.json @@ -210,6 +210,11 @@ "license": { "notgranted": "You must grant this license to complete your submission. If you are unable to grant this license at this time you may save your work and return later or remove the submission." } + }, + "submission": { + "sections": { + "init-form-error": "An error occurred during section initialize, please check your input-form configuration. Details are below :

" + } } }, "form": { @@ -252,5 +257,95 @@ "errors": { "invalid-user": "Invalid email or password." } + }, + "submission": { + "general":{ + "cannot_submit": "You have not the privilege to make a new submission.", + "deposit": "Deposit", + "discard": { + "submit": "Discard", + "confirm": { + "cancel": "Cancel", + "submit": "Yes, I'm sure", + "title": "Discard submission", + "info": "This operation can't be undone. Are you sure?" + } + }, + "save": "Save", + "save-later": "Save for later" + }, + "submit": { + "title": "Submission" + }, + "edit": { + "title": "Edit Submission" + }, + "mydspace": { + + }, + "sections": { + + "general": { + "add-more": "Add more", + "sections_not_valid": "There are incomplete sections.", + "deposit_success_notice": "Submission deposited successfully.", + "deposit_error_notice": "There was an issue when submitting the item, please try again later.", + "discard_success_notice": "Submission discarded successfully.", + "discard_error_notice": "There was an issue when discarding the item, please try again later.", + "save_success_notice": "Submission saved successfully.", + "metadata-extracted": "New metadata have been extracted and added to the {{sectionId}} section.", + "metadata-extracted-new-section": "New {{sectionId}} section has been added to submission." + }, + "submit.progressbar.describe.stepone": "Describe", + "submit.progressbar.describe.steptwo": "Describe", + "submit.progressbar.describe.stepcustom": "Describe", + "submit.progressbar.describe.deduplication": "Potential duplicates", + "submit.progressbar.describe.recycle": "Recycle", + "submit.progressbar.upload": "Upload files", + "submit.progressbar.license": "Deposit license", + "submit.progressbar.cclicense": "Creative commons license", + + "upload": { + "info": "Here you will find all the files currently in the item. You can update the fle metadata and access conditions or upload additional files just dragging&dropping them everywhere in the page", + "drop-message": "Drop files to attach them to the item", + "upload-successful": "Upload successful", + "upload-failed": "Upload failed", + "header.policy.default.nolist": "Uploaded files in the {{collectionName}} collection will be accessible according to the following group(s):", + "header.policy.default.withlist": "Please note that uploaded files in the {{collectionName}} collection will be accessible, in addition to what is explicity decided for the single file, with the following group(s):", + "form": { + "access-condition-label": "Access condition type", + "from-label": "Access grant from", + "from-placeholder": "From", + "until-label": "Access grant until", + "until-placeholder": "Until", + "group-label": "Group" + } + }, + "deduplication": { + "duplicated": "It's a duplicate", + "not_duplicated": "It's not a duplicate", + "duplicated_ctrl": "Mark the record to merge", + "duplicated_help": "Click here if this is a duplicate of your item", + "not_duplicated_help": "Click here if this is not a duplicate of your item", + "note_help": "Please enter your reason for the duplication into the box below.", + "note_placeholder": "Describe the reason of duplication", + "clear_decision": "Undo", + "clear_decision_help": "Click for clear the decision about this pontential duplicate", + "your_decision": "Your choice:", + "submitter_decision": "Submitter choice:", + "disclaimer": "The system has identified some potential duplicates. Please carefully review the list and flag each occurency with the appropriate choice or discard this submission.", + "disclaimer_ctrl": "The system has identified some potential duplicates. Please carefully review the list and the submitter comments and perform the appropriate action." + }, + "recycle": { + "disclaimer": "The following existent information are not valid within the selected collection. Please copy them to the appropriate metadata if applicable. Use the discard button to remove these information when done." + } + } + }, + "uploader": { + "drag-message": "Drag & Drop your files here", + "or": ", or", + "browse": "browse", + "queue-lenght": "Queue length", + "processing": "Processing" } } diff --git a/src/app/+item-page/item-page.module.ts b/src/app/+item-page/item-page.module.ts index bd801923e3..d574681b21 100644 --- a/src/app/+item-page/item-page.module.ts +++ b/src/app/+item-page/item-page.module.ts @@ -1,7 +1,7 @@ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; -import { SharedModule } from './../shared/shared.module'; +import { SharedModule } from '../shared/shared.module'; import { ItemPageComponent } from './simple/item-page.component'; import { ItemPageRoutingModule } from './item-page-routing.module'; @@ -18,11 +18,13 @@ import { FileSectionComponent } from './simple/field-components/file-section/fil import { CollectionsComponent } from './field-components/collections/collections.component'; import { FullItemPageComponent } from './full/full-item-page.component'; import { FullFileSectionComponent } from './full/field-components/file-section/full-file-section.component'; +import { SubmissionModule } from '../submission/submission.module'; @NgModule({ imports: [ CommonModule, SharedModule, + SubmissionModule, ItemPageRoutingModule ], declarations: [ diff --git a/src/app/+login-page/login-page-routing.module.ts b/src/app/+login-page/login-page-routing.module.ts index 4e932c50ce..d3c6425dd3 100644 --- a/src/app/+login-page/login-page-routing.module.ts +++ b/src/app/+login-page/login-page-routing.module.ts @@ -6,7 +6,7 @@ import { LoginPageComponent } from './login-page.component'; @NgModule({ imports: [ RouterModule.forChild([ - { path: '', component: LoginPageComponent, data: { title: 'login.title' } } + { path: '', pathMatch: 'full', component: LoginPageComponent, data: { title: 'login.title' } } ]) ] }) diff --git a/src/app/+login-page/login-page.component.ts b/src/app/+login-page/login-page.component.ts index 2752973130..0c6f0a62bc 100644 --- a/src/app/+login-page/login-page.component.ts +++ b/src/app/+login-page/login-page.component.ts @@ -1,20 +1,61 @@ -import { Component, OnDestroy } from '@angular/core'; +import { Component, OnDestroy, OnInit } from '@angular/core'; import { Store } from '@ngrx/store'; import { AppState } from '../app.reducer'; -import { ResetAuthenticationMessagesAction } from '../core/auth/auth.actions'; +import { + AddAuthenticationMessageAction, + AuthenticatedAction, + AuthenticationSuccessAction, + ResetAuthenticationMessagesAction +} from '../core/auth/auth.actions'; +import { Subscription } from 'rxjs/Subscription'; +import { hasValue, isNotEmpty } from '../shared/empty.util'; +import { ActivatedRoute } from '@angular/router'; +import { AuthTokenInfo } from '../core/auth/models/auth-token-info.model'; +import { Observable } from 'rxjs/Observable'; +import { isAuthenticated } from '../core/auth/selectors'; @Component({ selector: 'ds-login-page', styleUrls: ['./login-page.component.scss'], templateUrl: './login-page.component.html' }) -export class LoginPageComponent implements OnDestroy { +export class LoginPageComponent implements OnDestroy, OnInit { + sub: Subscription; - constructor(private store: Store) {} + constructor(private route: ActivatedRoute, + private store: Store) {} + + ngOnInit() { + const queryParamsObs = this.route.queryParams; + const authenticated = this.store.select(isAuthenticated); + this.sub = Observable.combineLatest(queryParamsObs, authenticated) + .filter(([params, auth]) => isNotEmpty(params.token) || isNotEmpty(params.expired)) + .take(1) + .subscribe(([params, auth]) => { + const token = params.token; + let authToken: AuthTokenInfo; + if (!auth) { + if (isNotEmpty(token)) { + authToken = new AuthTokenInfo(token); + this.store.dispatch(new AuthenticatedAction(authToken)); + } else if (isNotEmpty(params.expired)) { + this.store.dispatch(new AddAuthenticationMessageAction('auth.messages.expired')); + } + } else { + if (isNotEmpty(token)) { + authToken = new AuthTokenInfo(token); + this.store.dispatch(new AuthenticationSuccessAction(authToken)); + } + } + }) + } ngOnDestroy() { + if (hasValue(this.sub)) { + this.sub.unsubscribe(); + } // Clear all authentication messages when leaving login page this.store.dispatch(new ResetAuthenticationMessagesAction()); } diff --git a/src/app/+submit-page/submit-page-routing.module.ts b/src/app/+submit-page/submit-page-routing.module.ts new file mode 100644 index 0000000000..c583db2cb6 --- /dev/null +++ b/src/app/+submit-page/submit-page-routing.module.ts @@ -0,0 +1,20 @@ +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { AuthenticatedGuard } from '../core/auth/authenticated.guard'; +import { SubmissionSubmitComponent } from '../submission/submit/submission-submit.component'; + +@NgModule({ + imports: [ + RouterModule.forChild([ + { + canActivate: [AuthenticatedGuard], + path: '', + pathMatch: 'full', + component: SubmissionSubmitComponent, + data: { title: 'submission.submit.title' } + } + ]) + ] +}) +export class SubmitPageRoutingModule { } diff --git a/src/app/+submit-page/submit-page.module.ts b/src/app/+submit-page/submit-page.module.ts new file mode 100644 index 0000000000..6c791d3d77 --- /dev/null +++ b/src/app/+submit-page/submit-page.module.ts @@ -0,0 +1,17 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { SharedModule } from '../shared/shared.module'; +import { SubmitPageRoutingModule } from './submit-page-routing.module'; +import { SubmissionModule } from '../submission/submission.module'; + +@NgModule({ + imports: [ + SubmitPageRoutingModule, + CommonModule, + SharedModule, + SubmissionModule, + ], +}) +export class SubmitPageModule { + +} diff --git a/src/app/+workflowitems-edit-page/workflowitems-edit-page-routing.module.ts b/src/app/+workflowitems-edit-page/workflowitems-edit-page-routing.module.ts new file mode 100644 index 0000000000..750f98435f --- /dev/null +++ b/src/app/+workflowitems-edit-page/workflowitems-edit-page-routing.module.ts @@ -0,0 +1,20 @@ +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { AuthenticatedGuard } from '../core/auth/authenticated.guard'; +import { SubmissionEditComponent } from '../submission/edit/submission-edit.component'; + +@NgModule({ + imports: [ + RouterModule.forChild([ + { path: '', redirectTo: '/home', pathMatch: 'full' }, + { + canActivate: [AuthenticatedGuard], + path: ':id/edit', + component: SubmissionEditComponent, + data: { title: 'submission.edit.title' } + } + ]) + ] +}) +export class WorkflowitemsEditPageRoutingModule { } diff --git a/src/app/+workflowitems-edit-page/workflowitems-edit-page.module.ts b/src/app/+workflowitems-edit-page/workflowitems-edit-page.module.ts new file mode 100644 index 0000000000..936816f2c3 --- /dev/null +++ b/src/app/+workflowitems-edit-page/workflowitems-edit-page.module.ts @@ -0,0 +1,18 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { SharedModule } from '../shared/shared.module'; +import { WorkflowitemsEditPageRoutingModule } from './workflowitems-edit-page-routing.module'; +import { SubmissionModule } from '../submission/submission.module'; + +@NgModule({ + imports: [ + WorkflowitemsEditPageRoutingModule, + CommonModule, + SharedModule, + SubmissionModule, + ], + declarations: [] +}) +export class WorkflowitemsEditPageModule { + +} diff --git a/src/app/+workspaceitems-edit-page/workspaceitems-edit-page-routing.module.ts b/src/app/+workspaceitems-edit-page/workspaceitems-edit-page-routing.module.ts new file mode 100644 index 0000000000..07d59dfdd3 --- /dev/null +++ b/src/app/+workspaceitems-edit-page/workspaceitems-edit-page-routing.module.ts @@ -0,0 +1,20 @@ +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { AuthenticatedGuard } from '../core/auth/authenticated.guard'; +import { SubmissionEditComponent } from '../submission/edit/submission-edit.component'; + +@NgModule({ + imports: [ + RouterModule.forChild([ + { path: '', redirectTo: '/home', pathMatch: 'full' }, + { + canActivate: [AuthenticatedGuard], + path: ':id/edit', + component: SubmissionEditComponent, + data: { title: 'submission.edit.title' } + } + ]) + ] +}) +export class WorkspaceitemsEditPageRoutingModule { } diff --git a/src/app/+workspaceitems-edit-page/workspaceitems-edit-page.module.ts b/src/app/+workspaceitems-edit-page/workspaceitems-edit-page.module.ts new file mode 100644 index 0000000000..611304f651 --- /dev/null +++ b/src/app/+workspaceitems-edit-page/workspaceitems-edit-page.module.ts @@ -0,0 +1,18 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { SharedModule } from '../shared/shared.module'; +import { WorkspaceitemsEditPageRoutingModule } from './workspaceitems-edit-page-routing.module'; +import { SubmissionModule } from '../submission/submission.module'; + +@NgModule({ + imports: [ + WorkspaceitemsEditPageRoutingModule, + CommonModule, + SharedModule, + SubmissionModule, + ], + declarations: [] +}) +export class WorkspaceitemsEditPageModule { + +} diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index 4bc8c43152..efb3499c01 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -15,6 +15,9 @@ import { PageNotFoundComponent } from './pagenotfound/pagenotfound.component'; { path: 'admin', loadChildren: './+admin/admin.module#AdminModule' }, { path: 'login', loadChildren: './+login-page/login-page.module#LoginPageModule' }, { path: 'logout', loadChildren: './+logout-page/logout-page.module#LogoutPageModule' }, + { path: 'submit', loadChildren: './+submit-page/submit-page.module#SubmitPageModule' }, + { path: 'workspaceitems', loadChildren: './+workspaceitems-edit-page/workspaceitems-edit-page.module#WorkspaceitemsEditPageModule' }, + { path: 'workflowitems', loadChildren: './+workflowitems-edit-page/workflowitems-edit-page.module#WorkflowitemsEditPageModule' }, { path: '**', pathMatch: 'full', component: PageNotFoundComponent }, ]) ], diff --git a/src/app/app.component.html b/src/app/app.component.html index d806bb8323..f040e740f6 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -13,5 +13,3 @@
- - diff --git a/src/app/app.component.scss b/src/app/app.component.scss index 00a3e56121..45c6e61045 100644 --- a/src/app/app.component.scss +++ b/src/app/app.component.scss @@ -27,7 +27,11 @@ body { } .main-content { - flex: 1 0 auto; + flex: 1 1 100%; margin-top: $content-spacing; margin-bottom: $content-spacing; } +.alert.hide { + padding: 0; + margin: 0; +} diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 528c84fd3b..f8bd852ee8 100755 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -31,6 +31,7 @@ import { DSpaceRouterStateSerializer } from './shared/ngrx/dspace-router-state-s import { NotificationsBoardComponent } from './shared/notifications/notifications-board/notifications-board.component'; import { NotificationComponent } from './shared/notifications/notification/notification.component'; import { SharedModule } from './shared/shared.module'; +import { ScrollToModule } from '@nicky-lenaers/ngx-scroll-to'; export function getConfig() { return ENV_CONFIG; @@ -58,6 +59,7 @@ if (!ENV_CONFIG.production) { HttpClientModule, AppRoutingModule, CoreModule.forRoot(), + ScrollToModule.forRoot(), NgbModule.forRoot(), TranslateModule.forRoot(), EffectsModule.forRoot(appEffects), diff --git a/src/app/core/cache/models/normalized-collection.model.ts b/src/app/core/cache/models/normalized-collection.model.ts index 22e0d20eaa..f7ed096287 100644 --- a/src/app/core/cache/models/normalized-collection.model.ts +++ b/src/app/core/cache/models/normalized-collection.model.ts @@ -1,4 +1,4 @@ -import { autoserialize, inheritSerialization, autoserializeAs } from 'cerialize'; +import { autoserialize, inheritSerialization } from 'cerialize'; import { NormalizedDSpaceObject } from './normalized-dspace-object.model'; import { Collection } from '../../shared/collection.model'; @@ -15,6 +15,20 @@ export class NormalizedCollection extends NormalizedDSpaceObject { @autoserialize handle: string; + /** + * The Bitstream that represents the license of this Collection + */ + @autoserialize + @relationship(ResourceType.License, false) + license: string; + + /** + * The Bitstream that represents the default Access Conditions of this Collection + */ + @autoserialize + @relationship(ResourceType.ResourcePolicy, false) + defaultAccessConditions: string; + /** * The Bitstream that represents the logo of this Collection */ diff --git a/src/app/core/cache/models/normalized-license.model.ts b/src/app/core/cache/models/normalized-license.model.ts new file mode 100644 index 0000000000..84f1671766 --- /dev/null +++ b/src/app/core/cache/models/normalized-license.model.ts @@ -0,0 +1,21 @@ +import { autoserialize, inheritSerialization } from 'cerialize'; +import { mapsTo } from '../builders/build-decorators'; +import { NormalizedDSpaceObject } from './normalized-dspace-object.model'; +import { License } from '../../shared/license.model'; + +@mapsTo(License) +@inheritSerialization(NormalizedDSpaceObject) +export class NormalizedLicense extends NormalizedDSpaceObject { + + /** + * Is the license custom? + */ + @autoserialize + custom: boolean; + + /** + * The text of the license + */ + @autoserialize + text: string; +} diff --git a/src/app/core/cache/models/normalized-object-factory.ts b/src/app/core/cache/models/normalized-object-factory.ts index 5b13d55ac8..095309f515 100644 --- a/src/app/core/cache/models/normalized-object-factory.ts +++ b/src/app/core/cache/models/normalized-object-factory.ts @@ -6,6 +6,12 @@ import { GenericConstructor } from '../../shared/generic-constructor'; import { NormalizedCommunity } from './normalized-community.model'; import { ResourceType } from '../../shared/resource-type'; import { NormalizedObject } from './normalized-object.model'; +import { NormalizedLicense } from './normalized-license.model'; +import { NormalizedResourcePolicy } from './normalized-resource-policy.model'; +import { NormalizedWorkspaceItem } from '../../submission/models/normalized-workspaceitem.model'; +import { NormalizedEpersonModel } from '../../eperson/models/NormalizedEperson.model'; +import { NormalizedGroupModel } from '../../eperson/models/NormalizedGroup.model'; +import { NormalizedWorkflowItem } from '../../submission/models/normalized-workflowitem.model'; export class NormalizedObjectFactory { public static getConstructor(type: ResourceType): GenericConstructor { @@ -25,6 +31,21 @@ export class NormalizedObjectFactory { case ResourceType.Community: { return NormalizedCommunity } + case ResourceType.License: { + return NormalizedLicense + } + case ResourceType.ResourcePolicy: { + return NormalizedResourcePolicy + } + case ResourceType.Workspaceitem: { + return NormalizedWorkspaceItem + } + case ResourceType.Eperson: { + return NormalizedEpersonModel + } + case ResourceType.Group: { + return NormalizedGroupModel + } default: { return undefined; } diff --git a/src/app/core/cache/models/normalized-resource-policy.model.ts b/src/app/core/cache/models/normalized-resource-policy.model.ts new file mode 100644 index 0000000000..14d4b00fcc --- /dev/null +++ b/src/app/core/cache/models/normalized-resource-policy.model.ts @@ -0,0 +1,45 @@ +import { mapsTo } from '../builders/build-decorators'; +import { autoserialize, inheritSerialization } from 'cerialize'; +import { NormalizedDSpaceObject } from './normalized-dspace-object.model'; +import { ResourcePolicy } from '../../shared/resource-policy.model'; + +@mapsTo(ResourcePolicy) +@inheritSerialization(NormalizedDSpaceObject) +export class NormalizedResourcePolicy extends NormalizedDSpaceObject { + + /** + * The action of the resource policy + */ + @autoserialize + action: string; + + /** + * The identifier of the resource policy + */ + @autoserialize + id: string; + + /** + * The group uuid bound to the resource policy + */ + @autoserialize + groupUUID: string; + + /** + * The end date of the resource policy + */ + @autoserialize + endDate: string; + + /** + * The start date of the resource policy + */ + @autoserialize + startDate: string; + + /** + * The type of the resource policy + */ + @autoserialize + rpType: string +} diff --git a/src/app/core/cache/response-cache.models.ts b/src/app/core/cache/response-cache.models.ts index 9b1b5b89eb..31ac797e5a 100644 --- a/src/app/core/cache/response-cache.models.ts +++ b/src/app/core/cache/response-cache.models.ts @@ -10,6 +10,7 @@ import { MetadataSchema } from '../metadata/metadataschema.model'; import { RegistryMetadatafieldsResponse } from '../registry/registry-metadatafields-response.model'; import { RegistryBitstreamformatsResponse } from '../registry/registry-bitstreamformats-response.model'; import { AuthStatus } from '../auth/models/auth-status.model'; +import { NormalizedObject } from './models/normalized-object.model'; /* tslint:disable:max-classes-per-file */ export class RestResponse { @@ -176,4 +177,34 @@ export class IntegrationSuccessResponse extends RestResponse { } } +export class PostPatchSuccessResponse extends RestResponse { + constructor( + public dataDefinition: any[], + public statusCode: string, + public pageInfo?: PageInfo + ) { + super(true, statusCode); + } +} + +export class SubmissionSuccessResponse extends RestResponse { + constructor( + public dataDefinition: Array, + public statusCode: string, + public pageInfo?: PageInfo + ) { + super(true, statusCode); + } +} + +export class EpersonSuccessResponse extends RestResponse { + constructor( + public epersonDefinition: NormalizedObject[], + public statusCode: string, + public pageInfo?: PageInfo + ) { + super(true, statusCode); + } +} + /* tslint:enable:max-classes-per-file */ diff --git a/src/app/core/config/submission-uploads-config.service.ts b/src/app/core/config/submission-uploads-config.service.ts new file mode 100644 index 0000000000..88b9de9182 --- /dev/null +++ b/src/app/core/config/submission-uploads-config.service.ts @@ -0,0 +1,18 @@ +import { Injectable } from '@angular/core'; +import { ConfigService } from './config.service'; +import { ResponseCacheService } from '../cache/response-cache.service'; +import { RequestService } from '../data/request.service'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; + +@Injectable() +export class SubmissionUploadsConfigService extends ConfigService { + protected linkPath = 'submissionuploads'; + protected browseEndpoint = ''; + + constructor( + protected responseCache: ResponseCacheService, + protected requestService: RequestService, + protected halService: HALEndpointService) { + super(); + } +} diff --git a/src/app/core/core.effects.ts b/src/app/core/core.effects.ts index bc534a36b0..bb6a96b7ae 100644 --- a/src/app/core/core.effects.ts +++ b/src/app/core/core.effects.ts @@ -4,11 +4,13 @@ import { ResponseCacheEffects } from './cache/response-cache.effects'; import { UUIDIndexEffects } from './index/index.effects'; import { RequestEffects } from './data/request.effects'; import { AuthEffects } from './auth/auth.effects'; +import { JsonPatchOperationsEffects } from './json-patch/json-patch-operations.effects'; export const coreEffects = [ ResponseCacheEffects, RequestEffects, ObjectCacheEffects, UUIDIndexEffects, - AuthEffects + AuthEffects, + JsonPatchOperationsEffects, ]; diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts index 8536169688..0e916511b9 100644 --- a/src/app/core/core.module.ts +++ b/src/app/core/core.module.ts @@ -1,9 +1,4 @@ -import { - NgModule, - Optional, - SkipSelf, - ModuleWithProviders -} from '@angular/core'; +import { ModuleWithProviders, NgModule, Optional, SkipSelf } from '@angular/core'; import { CommonModule } from '@angular/common'; import { StoreModule } from '@ngrx/store'; @@ -24,7 +19,9 @@ import { DSOResponseParsingService } from './data/dso-response-parsing.service'; import { SearchResponseParsingService } from './data/search-response-parsing.service'; import { DSpaceRESTv2Service } from './dspace-rest-v2/dspace-rest-v2.service'; import { FormBuilderService } from '../shared/form/builder/form-builder.service'; +import { FormOperationsService } from '../submission/sections/form/form-operations.service'; import { FormService } from '../shared/form/form.service'; +import { GroupEpersonService } from './eperson/group-eperson.service'; import { HostWindowService } from '../shared/host-window.service'; import { ItemDataService } from './data/item-data.service'; import { MetadataService } from './metadata/metadata.service'; @@ -43,8 +40,12 @@ import { RouteService } from '../shared/services/route.service'; import { SubmissionDefinitionsConfigService } from './config/submission-definitions-config.service'; import { SubmissionFormsConfigService } from './config/submission-forms-config.service'; import { SubmissionSectionsConfigService } from './config/submission-sections-config.service'; +import { SubmissionResponseParsingService } from './submission/submission-response-parsing.service'; +import { EpersonResponseParsingService } from './eperson/eperson-response-parsing.service'; +import { JsonPatchOperationsBuilder } from './json-patch/builder/json-patch-operations-builder'; import { AuthorityService } from './integration/authority.service'; import { IntegrationResponseParsingService } from './integration/integration-response-parsing.service'; +import { WorkspaceitemDataService } from './submission/workspaceitem-data.service'; import { UUIDService } from './shared/uuid.service'; import { AuthenticatedGuard } from './auth/authenticated.guard'; import { AuthRequestService } from './auth/auth-request.service'; @@ -60,8 +61,12 @@ import { RegistryMetadataschemasResponseParsingService } from './data/registry-m import { MetadataschemaParsingService } from './data/metadataschema-parsing.service'; import { RegistryMetadatafieldsResponseParsingService } from './data/registry-metadatafields-response-parsing.service'; import { RegistryBitstreamformatsResponseParsingService } from './data/registry-bitstreamformats-response-parsing.service'; +import { JsonPatchOperationsService } from './json-patch/json-patch-operations.service'; +import { WorkflowitemDataService } from './submission/workflowitem-data.service'; import { NotificationsService } from '../shared/notifications/notifications.service'; import { UploaderService } from '../shared/uploader/uploader.service'; +import { FileService } from './shared/file.service'; +import { SubmissionRestService } from '../submission/submission-rest.service'; const IMPORTS = [ CommonModule, @@ -90,7 +95,10 @@ const PROVIDERS = [ DynamicFormService, DynamicFormValidationService, FormBuilderService, + FormOperationsService, FormService, + EpersonResponseParsingService, + GroupEpersonService, HALEndpointService, HostWindowService, ItemDataService, @@ -119,11 +127,20 @@ const PROVIDERS = [ RouteService, SubmissionDefinitionsConfigService, SubmissionFormsConfigService, + SubmissionRestService, SubmissionSectionsConfigService, + SubmissionResponseParsingService, + JsonPatchOperationsBuilder, + JsonPatchOperationsService, AuthorityService, IntegrationResponseParsingService, UploaderService, UUIDService, + NotificationsService, + WorkspaceitemDataService, + WorkflowitemDataService, + UploaderService, + FileService, // register AuthInterceptor as HttpInterceptor { provide: HTTP_INTERCEPTORS, diff --git a/src/app/core/core.reducers.ts b/src/app/core/core.reducers.ts index c764a2acff..f4fe29c619 100644 --- a/src/app/core/core.reducers.ts +++ b/src/app/core/core.reducers.ts @@ -5,6 +5,7 @@ import { objectCacheReducer, ObjectCacheState } from './cache/object-cache.reduc import { indexReducer, IndexState } from './index/index.reducer'; import { requestReducer, RequestState } from './data/request.reducer'; import { authReducer, AuthState } from './auth/auth.reducer'; +import { jsonPatchOperationsReducer, JsonPatchOperationsState } from './json-patch/json-patch-operations.reducer'; export interface CoreState { 'data/object': ObjectCacheState, @@ -12,6 +13,7 @@ export interface CoreState { 'data/request': RequestState, 'index': IndexState, 'auth': AuthState, + 'json/patch': JsonPatchOperationsState } export const coreReducers: ActionReducerMap = { @@ -19,7 +21,8 @@ export const coreReducers: ActionReducerMap = { 'data/response': responseCacheReducer, 'data/request': requestReducer, 'index': indexReducer, - 'auth': authReducer + 'auth': authReducer, + 'json/patch': jsonPatchOperationsReducer }; export const coreSelector = createFeatureSelector('core'); diff --git a/src/app/core/data/base-response-parsing.service.ts b/src/app/core/data/base-response-parsing.service.ts index bde0857946..9932c51cbf 100644 --- a/src/app/core/data/base-response-parsing.service.ts +++ b/src/app/core/data/base-response-parsing.service.ts @@ -17,7 +17,7 @@ function isPaginatedResponse(halObj: any) { /* tslint:disable:max-classes-per-file */ -class ProcessRequestDTO { +export class ProcessRequestDTO { [key: string]: ObjectDomain[] } diff --git a/src/app/core/data/collection-data.service.ts b/src/app/core/data/collection-data.service.ts index 7d1e463dbe..c9f60b9709 100644 --- a/src/app/core/data/collection-data.service.ts +++ b/src/app/core/data/collection-data.service.ts @@ -15,6 +15,7 @@ import { HALEndpointService } from '../shared/hal-endpoint.service'; @Injectable() export class CollectionDataService extends ComColDataService { protected linkPath = 'collections'; + protected forceBypassCache = false; constructor( protected responseCache: ResponseCacheService, diff --git a/src/app/core/data/comcol-data.service.spec.ts b/src/app/core/data/comcol-data.service.spec.ts index b5727fb22f..a635066e72 100644 --- a/src/app/core/data/comcol-data.service.spec.ts +++ b/src/app/core/data/comcol-data.service.spec.ts @@ -22,6 +22,7 @@ class NormalizedTestObject extends NormalizedObject { } class TestService extends ComColDataService { + protected forceBypassCache = false; constructor( protected responseCache: ResponseCacheService, diff --git a/src/app/core/data/community-data.service.ts b/src/app/core/data/community-data.service.ts index 88ad3a5287..daa83ed150 100644 --- a/src/app/core/data/community-data.service.ts +++ b/src/app/core/data/community-data.service.ts @@ -16,6 +16,7 @@ import { HALEndpointService } from '../shared/hal-endpoint.service'; export class CommunityDataService extends ComColDataService { protected linkPath = 'communities'; protected cds = this; + protected forceBypassCache = false; constructor( protected responseCache: ResponseCacheService, diff --git a/src/app/core/data/config-response-parsing.service.ts b/src/app/core/data/config-response-parsing.service.ts index dfbbfc50c7..4713a9be8e 100644 --- a/src/app/core/data/config-response-parsing.service.ts +++ b/src/app/core/data/config-response-parsing.service.ts @@ -34,7 +34,7 @@ export class ConfigResponseParsingService extends BaseResponseParsingService imp return new ErrorResponse( Object.assign( new Error('Unexpected response from config endpoint'), - {statusText: data.statusCode} + { statusText: data.statusCode } ) ); } diff --git a/src/app/core/data/data.service.ts b/src/app/core/data/data.service.ts index f532ff05ba..ba447c32de 100644 --- a/src/app/core/data/data.service.ts +++ b/src/app/core/data/data.service.ts @@ -10,6 +10,7 @@ import { PaginatedList } from './paginated-list'; import { RemoteData } from './remote-data'; import { FindAllOptions, FindAllRequest, FindByIDRequest, GetRequest } from './request.models'; import { RequestService } from './request.service'; +import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service'; import { NormalizedObject } from '../cache/models/normalized-object.model'; export abstract class DataService { @@ -19,6 +20,7 @@ export abstract class DataService protected abstract store: Store; protected abstract linkPath: string; protected abstract halService: HALEndpointService; + protected abstract forceBypassCache = false; public abstract getScopedEndpoint(scope: string): Observable @@ -52,6 +54,36 @@ export abstract class DataService } } + protected getSearchByHref(endpoint, searchByLink, options: FindAllOptions = {}): Observable { + let result: Observable; + const args = []; + + if (hasValue(options.scopeID)) { + result = Observable.of(`${endpoint}/${searchByLink}?uuid=${options.scopeID}`); + } else { + result = Observable.of(endpoint); + } + + 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 */ + args.push(`page=${options.currentPage - 1}`); + } + + if (hasValue(options.elementsPerPage)) { + args.push(`size=${options.elementsPerPage}`); + } + + if (hasValue(options.sort)) { + args.push(`sort=${options.sort.field},${options.sort.direction}`); + } + + if (isNotEmpty(args)) { + return result.map((href: string) => new URLCombiner(href, `?${args.join('&')}`).toString()); + } else { + return result; + } + } + findAll(options: FindAllOptions = {}): Observable>> { const hrefObs = this.halService.getEndpoint(this.linkPath).filter((href: string) => isNotEmpty(href)) .flatMap((endpoint: string) => this.getFindAllHref(endpoint, options)); @@ -61,7 +93,7 @@ export abstract class DataService .take(1) .subscribe((href: string) => { const request = new FindAllRequest(this.requestService.generateRequestId(), href, options); - this.requestService.configure(request); + this.requestService.configure(request, this.forceBypassCache); }); return this.rdbService.buildList(hrefObs) as Observable>>; @@ -80,32 +112,69 @@ export abstract class DataService .take(1) .subscribe((href: string) => { const request = new FindByIDRequest(this.requestService.generateRequestId(), href, id); - this.requestService.configure(request); + this.requestService.configure(request, this.forceBypassCache); }); return this.rdbService.buildSingle(hrefObs); } - findByHref(href: string): Observable> { - this.requestService.configure(new GetRequest(this.requestService.generateRequestId(), href)); + findByHref(href: string, options?: HttpOptions): Observable> { + this.requestService.configure(new GetRequest(this.requestService.generateRequestId(), href, null, options), this.forceBypassCache); return this.rdbService.buildSingle(href); } - // TODO implement, after the structure of the REST server's POST response is finalized - // create(dso: DSpaceObject): Observable> { - // const postHrefObs = this.getEndpoint(); - // - // // TODO ID is unknown at this point - // const idHrefObs = postHrefObs.map((href: string) => this.getFindByIDHref(href, dso.id)); - // - // postHrefObs - // .filter((href: string) => hasValue(href)) - // .take(1) - // .subscribe((href: string) => { - // const request = new RestRequest(this.requestService.generateRequestId(), href, RestRequestMethod.Post, dso); - // this.requestService.configure(request); - // }); - // - // return this.rdbService.buildSingle(idHrefObs, this.normalizedResourceType); - // } + // TODO remove when search will be completed + public searchBySubmitter(options: FindAllOptions = {}): Observable>> { + return this.searchBy('submitter', options); + } + + // TODO remove when search will be completed + searchByUser(options: FindAllOptions = {}): Observable>> { + return this.searchBy('user', options); + } + + // TODO remove when search will be completed + protected searchBy(searchBy: string, options: FindAllOptions = {}): Observable>> { + let url = null; + switch (searchBy) { + case 'user': { + url = 'search/findByUser'; + break; + } + case 'submitter': { + url = 'search/findBySubmitter'; + break; + } + } + + const hrefObs = this.halService.getEndpoint(this.linkPath).filter((href: string) => isNotEmpty(href)) + .flatMap((endpoint: string) => this.getSearchByHref(endpoint, url, options)); + hrefObs + .filter((href: string) => hasValue(href)) + .take(1) + .subscribe((href: string) => { + const request = new FindAllRequest(this.requestService.generateRequestId(), href, options); + this.requestService.configure(request, this.forceBypassCache); + }); + + return this.rdbService.buildList(hrefObs) as Observable>>; + } + +// TODO implement, after the structure of the REST server's POST response is finalized +// create(dso: DSpaceObject): Observable> { +// const postHrefObs = this.getEndpoint(); +// +// // TODO ID is unknown at this point +// const idHrefObs = postHrefObs.map((href: string) => this.getFindByIDHref(href, dso.id)); +// +// postHrefObs +// .filter((href: string) => hasValue(href)) +// .take(1) +// .subscribe((href: string) => { +// const request = new RestRequest(this.requestService.generateRequestId(), href, RestRequestMethod.Post, dso); +// this.requestService.configure(request); +// }); +// +// return this.rdbService.buildSingle(idHrefObs, this.normalizedResourceType); +// } } diff --git a/src/app/core/data/item-data.service.ts b/src/app/core/data/item-data.service.ts index 6b0937d8e4..d0e8f360ae 100644 --- a/src/app/core/data/item-data.service.ts +++ b/src/app/core/data/item-data.service.ts @@ -19,6 +19,7 @@ import { HALEndpointService } from '../shared/hal-endpoint.service'; @Injectable() export class ItemDataService extends DataService { protected linkPath = 'items'; + protected forceBypassCache = false; constructor( protected responseCache: ResponseCacheService, diff --git a/src/app/core/data/request.models.ts b/src/app/core/data/request.models.ts index 7015b0b0f1..56d2109646 100644 --- a/src/app/core/data/request.models.ts +++ b/src/app/core/data/request.models.ts @@ -11,6 +11,8 @@ import { ConfigResponseParsingService } from './config-response-parsing.service' import { AuthResponseParsingService } from '../auth/auth-response-parsing.service'; import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service'; import { HttpHeaders } from '@angular/common/http'; +import { SubmissionResponseParsingService } from '../submission/submission-response-parsing.service'; +import { EpersonResponseParsingService } from '../eperson/eperson-response-parsing.service'; import { IntegrationResponseParsingService } from '../integration/integration-response-parsing.service'; /* tslint:disable:max-classes-per-file */ @@ -184,8 +186,8 @@ export class BrowseEntriesRequest extends GetRequest { } export class ConfigRequest extends GetRequest { - constructor(uuid: string, href: string) { - super(uuid, href); + constructor(uuid: string, href: string, public options?: HttpOptions) { + super(uuid, href, null, options); } getResponseParser(): GenericConstructor { @@ -222,6 +224,85 @@ export class IntegrationRequest extends GetRequest { return IntegrationResponseParsingService; } } + +export class SubmissionFindAllRequest extends GetRequest { + constructor(uuid: string, href: string, public body?: FindAllOptions) { + super(uuid, href); + } + + getResponseParser(): GenericConstructor { + return SubmissionResponseParsingService; + } +} + +export class SubmissionFindByIDRequest extends GetRequest { + constructor(uuid: string, + href: string, + public resourceID: string) { + super(uuid, href); + } + + getResponseParser(): GenericConstructor { + return SubmissionResponseParsingService; + } +} + +export class SubmissionRequest extends GetRequest { + constructor(uuid: string, href: string) { + super(uuid, href); + } + + getResponseParser(): GenericConstructor { + return SubmissionResponseParsingService; + } +} + +export class SubmissionDeleteRequest extends DeleteRequest { + constructor(public uuid: string, + public href: string) { + super(uuid, href); + } + + getResponseParser(): GenericConstructor { + return SubmissionResponseParsingService; + } +} + +export class SubmissionPatchRequest extends PatchRequest { + constructor(public uuid: string, + public href: string, + public body?: any) { + super(uuid, href, body); + } + + getResponseParser(): GenericConstructor { + return SubmissionResponseParsingService; + } +} + +export class SubmissionPostRequest extends PostRequest { + constructor(public uuid: string, + public href: string, + public body?: any, + public options?: HttpOptions) { + super(uuid, href, body, options); + } + + getResponseParser(): GenericConstructor { + return SubmissionResponseParsingService; + } +} + +export class EpersonRequest extends GetRequest { + constructor(uuid: string, href: string) { + super(uuid, href); + } + + getResponseParser(): GenericConstructor { + return EpersonResponseParsingService; + } +} + export class RequestError extends Error { statusText: string; } diff --git a/src/app/core/data/request.service.ts b/src/app/core/data/request.service.ts index 12933f83fc..6f836286ba 100644 --- a/src/app/core/data/request.service.ts +++ b/src/app/core/data/request.service.ts @@ -1,6 +1,8 @@ import { Injectable } from '@angular/core'; -import { createSelector, MemoizedSelector, Store } from '@ngrx/store'; +import { MemoizedSelector, Store } from '@ngrx/store'; + +import { remove } from 'lodash'; import { Observable } from 'rxjs/Observable'; import { hasValue } from '../../shared/empty.util'; @@ -67,12 +69,28 @@ export class RequestService { .flatMap((uuid: string) => this.getByUUID(uuid)); } - // TODO to review "overrideRequest" param when https://github.com/DSpace/dspace-angular/issues/217 will be fixed + private clearRequestsOnTheirWayToTheStore(href) { + this.getByHref(href) + .take(1) + .subscribe((re: RequestEntry) => { + if (!hasValue(re)) { + this.responseCache.remove(href); + } else if (!re.responsePending) { + this.responseCache.remove(href); + remove(this.requestsOnTheirWayToTheStore, (item) => item === href); + } + }); + } + + // TODO to review "forceBypassCache" param when https://github.com/DSpace/dspace-angular/issues/217 will be fixed configure(request: RestRequest, forceBypassCache: boolean = false): void { const isGetRequest = request.method === RestRequestMethod.Get; - if (!isGetRequest || !this.isCachedOrPending(request) || forceBypassCache) { + if (forceBypassCache) { + this.clearRequestsOnTheirWayToTheStore(request.href); + } + if (!isGetRequest || !this.isCachedOrPending(request) || (forceBypassCache && !this.isPending(request))) { this.dispatchRequest(request); - if (isGetRequest && !forceBypassCache) { + if (isGetRequest) { this.trackRequestsOnTheirWayToTheStore(request); } } @@ -121,7 +139,7 @@ export class RequestService { */ private trackRequestsOnTheirWayToTheStore(request: GetRequest) { this.requestsOnTheirWayToTheStore = [...this.requestsOnTheirWayToTheStore, request.href]; - this.store.select(this.entryFromUUIDSelector(request.href)) + this.getByHref(request.href) .filter((re: RequestEntry) => hasValue(re)) .take(1) .subscribe((re: RequestEntry) => { diff --git a/src/app/core/eperson/eperson-data.ts b/src/app/core/eperson/eperson-data.ts new file mode 100644 index 0000000000..a0d69a726f --- /dev/null +++ b/src/app/core/eperson/eperson-data.ts @@ -0,0 +1,12 @@ +import { PageInfo } from '../shared/page-info.model'; +import { NormalizedObject } from '../cache/models/normalized-object.model'; + +/** + * A class to represent the data retrieved by a Eperson service + */ +export class EpersonData { + constructor( + public pageInfo: PageInfo, + public payload: NormalizedObject[] + ) { } +} diff --git a/src/app/core/eperson/eperson-object-factory.ts b/src/app/core/eperson/eperson-object-factory.ts new file mode 100644 index 0000000000..e2d27d7164 --- /dev/null +++ b/src/app/core/eperson/eperson-object-factory.ts @@ -0,0 +1,21 @@ +import { EpersonType } from './eperson-type'; +import { GenericConstructor } from '../shared/generic-constructor'; +import { NormalizedEpersonModel } from './models/NormalizedEperson.model'; +import { NormalizedGroupModel } from './models/NormalizedGroup.model'; +import { NormalizedObject } from '../cache/models/normalized-object.model'; + +export class EpersonObjectFactory { + public static getConstructor(type): GenericConstructor { + switch (type) { + case EpersonType.EpersonsModel: { + return NormalizedEpersonModel + } + case EpersonType.GroupsModel: { + return NormalizedGroupModel + } + default: { + return undefined; + } + } + } +} diff --git a/src/app/core/eperson/eperson-response-parsing.service.ts b/src/app/core/eperson/eperson-response-parsing.service.ts new file mode 100644 index 0000000000..6972149798 --- /dev/null +++ b/src/app/core/eperson/eperson-response-parsing.service.ts @@ -0,0 +1,47 @@ +import { Inject, Injectable } from '@angular/core'; +import { RestRequest } from '../data/request.models'; +import { ResponseParsingService } from '../data/parsing.service'; +import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; +import { + EpersonSuccessResponse, ErrorResponse, + RestResponse +} from '../cache/response-cache.models'; +import { isNotEmpty } from '../../shared/empty.util'; +import { EpersonObjectFactory } from './eperson-object-factory'; +import { EpersonType } from './eperson-type'; + +import { BaseResponseParsingService } from '../data/base-response-parsing.service'; +import { GLOBAL_CONFIG } from '../../../config'; +import { GlobalConfig } from '../../../config/global-config.interface'; +import { ObjectCacheService } from '../cache/object-cache.service'; +import { DSpaceObject } from '../shared/dspace-object.model'; +import { NormalizedObject } from '../cache/models/normalized-object.model'; + +@Injectable() +export class EpersonResponseParsingService extends BaseResponseParsingService implements ResponseParsingService { + + protected objectFactory = EpersonObjectFactory; + protected toCache = false; + + constructor( + @Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig, + protected objectCache: ObjectCacheService, + ) { + super(); + } + + parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse { + if (isNotEmpty(data.payload) && isNotEmpty(data.payload._links)) { + const epersonDefinition = this.process(data.payload, request.href); + return new EpersonSuccessResponse(epersonDefinition[Object.keys(epersonDefinition)[0]], data.statusCode, this.processPageInfo(data.payload)); + } else { + return new ErrorResponse( + Object.assign( + new Error('Unexpected response from EPerson endpoint'), + {statusText: data.statusCode} + ) + ); + } + } + +} diff --git a/src/app/core/eperson/eperson-type.ts b/src/app/core/eperson/eperson-type.ts new file mode 100644 index 0000000000..ca0bbf04bd --- /dev/null +++ b/src/app/core/eperson/eperson-type.ts @@ -0,0 +1,5 @@ + +export enum EpersonType { + EpersonsModel = 'eperson', + GroupsModel = 'group', +} diff --git a/src/app/core/eperson/eperson.service.ts b/src/app/core/eperson/eperson.service.ts new file mode 100644 index 0000000000..bcd9448f5e --- /dev/null +++ b/src/app/core/eperson/eperson.service.ts @@ -0,0 +1,53 @@ +import { Observable } from 'rxjs/Observable'; +import { RequestService } from '../data/request.service'; +import { ResponseCacheService } from '../cache/response-cache.service'; +import { EpersonSuccessResponse, ErrorResponse, RestResponse } from '../cache/response-cache.models'; +import { EpersonRequest, GetRequest } from '../data/request.models'; +import { ResponseCacheEntry } from '../cache/response-cache.reducer'; +import { isNotEmpty } from '../../shared/empty.util'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { EpersonData } from './eperson-data'; + +export abstract class EpersonService { + protected request: EpersonRequest; + protected abstract responseCache: ResponseCacheService; + protected abstract requestService: RequestService; + protected abstract linkPath: string; + protected abstract browseEndpoint: string; + protected abstract halService: HALEndpointService; + + protected getEperson(request: GetRequest): Observable { + const [successResponse, errorResponse] = this.responseCache.get(request.href) + .map((entry: ResponseCacheEntry) => entry.response) + .partition((response: RestResponse) => response.isSuccessful); + return Observable.merge( + errorResponse.flatMap((response: ErrorResponse) => + Observable.throw(new Error(`Couldn't retrieve the EPerson`))), + successResponse + .filter((response: EpersonSuccessResponse) => isNotEmpty(response)) + .map((response: EpersonSuccessResponse) => new EpersonData(response.pageInfo, response.epersonDefinition)) + .distinctUntilChanged()); + } + + public getDataByHref(href: string): Observable { + const request = new EpersonRequest(this.requestService.generateRequestId(), href); + this.requestService.configure(request); + + return this.getEperson(request); + } + + public getDataByUuid(uuid: string): Observable { + return this.halService.getEndpoint(this.linkPath) + .map((endpoint: string) => this.getDataByIDHref(endpoint, uuid)) + .filter((href: string) => isNotEmpty(href)) + .distinctUntilChanged() + .map((endpointURL: string) => new EpersonRequest(this.requestService.generateRequestId(), endpointURL)) + .do((request: GetRequest) => this.requestService.configure(request)) + .flatMap((request: GetRequest) => this.getEperson(request)) + .distinctUntilChanged(); + } + + protected getDataByIDHref(endpoint, resourceID): string { + return `${endpoint}/${resourceID}`; + } +} diff --git a/src/app/core/eperson/group-eperson.service.ts b/src/app/core/eperson/group-eperson.service.ts new file mode 100644 index 0000000000..1f990ef5a0 --- /dev/null +++ b/src/app/core/eperson/group-eperson.service.ts @@ -0,0 +1,55 @@ +import { Injectable } from '@angular/core'; + +import { EpersonService } from './eperson.service'; +import { ResponseCacheService } from '../cache/response-cache.service'; +import { RequestService } from '../data/request.service'; +import { isNotEmpty } from '../../shared/empty.util'; +import { EpersonRequest, GetRequest } from '../data/request.models'; +import { EpersonData } from './eperson-data'; +import { EpersonSuccessResponse, ErrorResponse, RestResponse } from '../cache/response-cache.models'; +import { Observable } from 'rxjs/Observable'; +import { ResponseCacheEntry } from '../cache/response-cache.reducer'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { BrowseService } from '../browse/browse.service'; + +@Injectable() +export class GroupEpersonService extends EpersonService { + protected linkPath = 'groups'; + protected browseEndpoint = ''; + + constructor( + protected responseCache: ResponseCacheService, + protected requestService: RequestService, + protected bs: BrowseService, + protected halService: HALEndpointService) { + super(); + } + + protected getSearchHref(endpoint, groupName): string { + return `${endpoint}/search/isMemberOf?groupName=${groupName}`; + } + + isMemberOf(groupName: string) { + return this.halService.getEndpoint(this.linkPath) + .map((endpoint: string) => this.getSearchHref(endpoint, groupName)) + .filter((href: string) => isNotEmpty(href)) + .distinctUntilChanged() + .map((endpointURL: string) => new EpersonRequest(this.requestService.generateRequestId(), endpointURL)) + .do((request: GetRequest) => this.requestService.configure(request)) + .flatMap((request: GetRequest) => this.getSearch(request)) + .distinctUntilChanged(); + } + + protected getSearch(request: GetRequest): Observable { + const [successResponse, errorResponse] = this.responseCache.get(request.href) + .map((entry: ResponseCacheEntry) => entry.response) + .partition((response: RestResponse) => response.isSuccessful); + return Observable.merge( + errorResponse.flatMap((response: ErrorResponse) => + Observable.of(new EpersonData(undefined, undefined))), + successResponse + .filter((response: EpersonSuccessResponse) => isNotEmpty(response)) + .map((response: EpersonSuccessResponse) => new EpersonData(response.pageInfo, response.epersonDefinition)) + .distinctUntilChanged()); + } +} diff --git a/src/app/core/eperson/models/NormalizedEperson.model.ts b/src/app/core/eperson/models/NormalizedEperson.model.ts index 0c0b2490d6..c25581a11b 100644 --- a/src/app/core/eperson/models/NormalizedEperson.model.ts +++ b/src/app/core/eperson/models/NormalizedEperson.model.ts @@ -1,10 +1,12 @@ -import { autoserialize, inheritSerialization } from 'cerialize'; +import { autoserialize, autoserializeAs, inheritSerialization } from 'cerialize'; import { CacheableObject } from '../../cache/object-cache.reducer'; import { ListableObject } from '../../../shared/object-collection/shared/listable-object.model'; import { NormalizedDSpaceObject } from '../../cache/models/normalized-dspace-object.model'; import { Eperson } from './eperson.model'; import { mapsTo, relationship } from '../../cache/builders/build-decorators'; import { ResourceType } from '../../shared/resource-type'; +import { Group } from './group.model'; +import { NormalizedGroupModel } from './NormalizedGroup.model'; @mapsTo(Eperson) @inheritSerialization(NormalizedDSpaceObject) @@ -13,9 +15,8 @@ export class NormalizedEpersonModel extends NormalizedDSpaceObject implements Ca @autoserialize public handle: string; - @autoserialize - @relationship(ResourceType.Group, true) - groups: string[]; + @autoserializeAs(NormalizedGroupModel) + groups: Group[]; @autoserialize public netid: string; diff --git a/src/app/core/eperson/models/group.model.ts b/src/app/core/eperson/models/group.model.ts index cd41ce9e25..6ebedba48d 100644 --- a/src/app/core/eperson/models/group.model.ts +++ b/src/app/core/eperson/models/group.model.ts @@ -2,6 +2,8 @@ import { DSpaceObject } from '../../shared/dspace-object.model'; export class Group extends DSpaceObject { + public groups: Group[]; + public handle: string; public permanent: boolean; diff --git a/src/app/core/integration/models/authority-value.model.ts b/src/app/core/integration/models/authority-value.model.ts index e2ef9ce9db..82c8099e9f 100644 --- a/src/app/core/integration/models/authority-value.model.ts +++ b/src/app/core/integration/models/authority-value.model.ts @@ -1,5 +1,6 @@ import { IntegrationModel } from './integration.model'; import { autoserialize } from 'cerialize'; +import { isNotEmpty } from '../../../shared/empty.util'; export class AuthorityValueModel extends IntegrationModel { @@ -17,4 +18,8 @@ export class AuthorityValueModel extends IntegrationModel { @autoserialize language: string; + + hasValue(): boolean { + return isNotEmpty(this.value); + } } diff --git a/src/app/core/json-patch/builder/json-patch-operation-path-combiner.ts b/src/app/core/json-patch/builder/json-patch-operation-path-combiner.ts new file mode 100644 index 0000000000..1650f57d63 --- /dev/null +++ b/src/app/core/json-patch/builder/json-patch-operation-path-combiner.ts @@ -0,0 +1,45 @@ +/** + * Combines a variable number of strings representing parts + * of a relative REST URL in to a single, absolute REST URL + * + */ +import { isNotUndefined } from '../../../shared/empty.util'; + +export interface JsonPatchOperationPathObject { + rootElement: string; + subRootElement: string; + path: string; +} + +export class JsonPatchOperationPathCombiner { + private _rootElement: string; + private _subRootElement: string; + + constructor(rootElement, ...subRootElements: string[]) { + this._rootElement = rootElement; + this._subRootElement = subRootElements.join('/'); + } + + get rootElement(): string { + return this._rootElement; + } + + get subRootElement(): string { + return this._subRootElement; + } + + public getPath(fragment?: string|string[]): JsonPatchOperationPathObject { + if (isNotUndefined(fragment) && Array.isArray(fragment)) { + fragment = fragment.join('/'); + } + + let path; + if (isNotUndefined(fragment)) { + path = '/' + this._rootElement + '/' + this._subRootElement + '/' + fragment; + } else { + path = '/' + this._rootElement + '/' + this._subRootElement; + } + + return {rootElement: this._rootElement, subRootElement: this._subRootElement, path: path}; + } +} diff --git a/src/app/core/json-patch/builder/json-patch-operations-builder.ts b/src/app/core/json-patch/builder/json-patch-operations-builder.ts new file mode 100644 index 0000000000..1aa973f09f --- /dev/null +++ b/src/app/core/json-patch/builder/json-patch-operations-builder.ts @@ -0,0 +1,111 @@ +import { Store } from '@ngrx/store'; +import { CoreState } from '../../core.reducers'; +import { + NewPatchAddOperationAction, + NewPatchRemoveOperationAction, + NewPatchReplaceOperationAction +} from '../json-patch-operations.actions'; +import { JsonPatchOperationPathObject } from './json-patch-operation-path-combiner'; +import { Injectable } from '@angular/core'; +import { isEmpty, isNotEmpty } from '../../../shared/empty.util'; +import { dateToGMTString } from '../../../shared/date.util'; +import { AuthorityValueModel } from '../../integration/models/authority-value.model'; +import { FormFieldMetadataValueObject } from '../../../shared/form/builder/models/form-field-metadata-value.model'; +import { FormFieldLanguageValueObject } from '../../../shared/form/builder/models/form-field-language-value.model'; + +@Injectable() +export class JsonPatchOperationsBuilder { + + constructor(private store: Store) { + } + + add(path: JsonPatchOperationPathObject, value, first = false, plain = false) { + this.store.dispatch( + new NewPatchAddOperationAction( + path.rootElement, + path.subRootElement, + path.path, this.prepareValue(value, plain, first))); + } + + replace(path: JsonPatchOperationPathObject, value, plain = false) { + this.store.dispatch( + new NewPatchReplaceOperationAction( + path.rootElement, + path.subRootElement, + path.path, + this.prepareValue(value, plain, false))); + } + + remove(path: JsonPatchOperationPathObject) { + this.store.dispatch( + new NewPatchRemoveOperationAction( + path.rootElement, + path.subRootElement, + path.path)); + } + + protected prepareValue(value: any, plain: boolean, first: boolean) { + let operationValue: any = null; + if (isNotEmpty(value)) { + if (plain) { + operationValue = value; + } else { + if (Array.isArray(value)) { + operationValue = []; + value.forEach((entry) => { + if ((typeof entry === 'object')) { + operationValue.push(this.prepareObjectValue(entry)); + } else { + operationValue.push(new FormFieldMetadataValueObject(entry)); + // operationValue.push({value: entry}); + // operationValue.push(entry); + } + }); + } else if (typeof value === 'object') { + operationValue = this.prepareObjectValue(value); + } else { + operationValue = new FormFieldMetadataValueObject(value); + } + } + } + return (first && !Array.isArray(operationValue)) ? [operationValue] : operationValue; + } + + protected prepareObjectValue(value: any) { + let operationValue = Object.create({}); + if (isEmpty(value) || value instanceof FormFieldMetadataValueObject) { + operationValue = value; + } else if (value instanceof Date) { + operationValue = new FormFieldMetadataValueObject(dateToGMTString(value)); + } else if (value instanceof AuthorityValueModel) { + operationValue = this.prepareAuthorityValue(value); + } else if (value instanceof FormFieldLanguageValueObject) { + operationValue = new FormFieldMetadataValueObject(value.value, value.language); + } else if (value.hasOwnProperty('value')) { + operationValue = new FormFieldMetadataValueObject(value.value); + // operationValue = value; + } else { + Object.keys(value) + .forEach((key) => { + if (typeof value[key] === 'object') { + operationValue[key] = this.prepareObjectValue(value[key]); + } else { + operationValue[key] = value[key]; + } + }); + // operationValue = {value: value}; + } + return operationValue; + } + + protected prepareAuthorityValue(value: any) { + let operationValue: any = null; + if (isNotEmpty(value.id)) { + operationValue = new FormFieldMetadataValueObject(value.value, value.language, value.id); + } else { + operationValue = new FormFieldMetadataValueObject(value.value, value.language); + } + return operationValue; + } + +} diff --git a/src/app/core/json-patch/json-patch-operations.actions.ts b/src/app/core/json-patch/json-patch-operations.actions.ts new file mode 100644 index 0000000000..cb3e3b0d38 --- /dev/null +++ b/src/app/core/json-patch/json-patch-operations.actions.ts @@ -0,0 +1,279 @@ +import { Action } from '@ngrx/store'; + +import { type } from '../../shared/ngrx/type'; + +/** + * For each action type in an action group, make a simple + * enum object for all of this group's action types. + * + * The 'type' utility function coerces strings into string + * literal types and runs a simple check to guarantee all + * action types in the application are unique. + */ +export const JsonPatchOperationsActionTypes = { + NEW_JSON_PATCH_ADD_OPERATION: type('dspace/core/patch/NEW_JSON_PATCH_ADD_OPERATION'), + NEW_JSON_PATCH_COPY_OPERATION: type('dspace/core/patch/NEW_JSON_PATCH_COPY_OPERATION'), + NEW_JSON_PATCH_MOVE_OPERATION: type('dspace/core/patch/NEW_JSON_PATCH_MOVE_OPERATION'), + NEW_JSON_PATCH_REMOVE_OPERATION: type('dspace/core/patch/NEW_JSON_PATCH_REMOVE_OPERATION'), + NEW_JSON_PATCH_REPLACE_OPERATION: type('dspace/core/patch/NEW_JSON_PATCH_REPLACE_OPERATION'), + COMMIT_JSON_PATCH_OPERATIONS: type('dspace/core/patch/COMMIT_JSON_PATCH_OPERATIONS'), + ROLLBACK_JSON_PATCH_OPERATIONS: type('dspace/core/patch/ROLLBACK_JSON_PATCH_OPERATIONS'), + FLUSH_JSON_PATCH_OPERATIONS: type('dspace/core/patch/FLUSH_JSON_PATCH_OPERATIONS'), + START_TRANSACTION_JSON_PATCH_OPERATIONS: type('dspace/core/patch/START_TRANSACTION_JSON_PATCH_OPERATIONS'), +}; + +/* tslint:disable:max-classes-per-file */ + +/** + * An ngrx action to commit the current transaction + */ +export class CommitPatchOperationsAction implements Action { + type = JsonPatchOperationsActionTypes.COMMIT_JSON_PATCH_OPERATIONS; + payload: { + resourceType: string; + resourceId: string; + }; + + /** + * Create a new CommitPatchOperationsAction + * + * @param resourceType + * the resource's type + * @param resourceId + * the resource's ID + */ + constructor(resourceType: string, resourceId: string) { + this.payload = { resourceType, resourceId }; + } +} + +/** + * An ngrx action to rollback the current transaction + */ +export class RollbacktPatchOperationsAction implements Action { + type = JsonPatchOperationsActionTypes.ROLLBACK_JSON_PATCH_OPERATIONS; + payload: { + resourceType: string; + resourceId: string; + }; + + /** + * Create a new CommitPatchOperationsAction + * + * @param resourceType + * the resource's type + * @param resourceId + * the resource's ID + */ + constructor(resourceType: string, resourceId: string) { + this.payload = { resourceType, resourceId }; + } +} + +/** + * An ngrx action to initiate a transaction block + */ +export class StartTransactionPatchOperationsAction implements Action { + type = JsonPatchOperationsActionTypes.START_TRANSACTION_JSON_PATCH_OPERATIONS; + payload: { + resourceType: string; + resourceId: string; + startTime: number; + }; + + /** + * Create a new CommitPatchOperationsAction + * + * @param resourceType + * the resource's type + * @param resourceId + * the resource's ID + * @param startTime + * the start timestamp + */ + constructor(resourceType: string, resourceId: string, startTime: number) { + this.payload = { resourceType, resourceId, startTime }; + } +} + +/** + * An ngrx action to flush list of the JSON Patch operations + */ +export class FlushPatchOperationsAction implements Action { + type = JsonPatchOperationsActionTypes.FLUSH_JSON_PATCH_OPERATIONS; + payload: { + resourceType: string; + resourceId: string; + }; + + /** + * Create a new FlushPatchOperationsAction + * + * @param resourceType + * the resource's type + * @param resourceId + * the resource's ID + */ + constructor(resourceType: string, resourceId: string) { + this.payload = { resourceType, resourceId }; + } +} + +/** + * An ngrx action to Add new HTTP/PATCH ADD operations to state + */ +export class NewPatchAddOperationAction implements Action { + type = JsonPatchOperationsActionTypes.NEW_JSON_PATCH_ADD_OPERATION; + payload: { + resourceType: string; + resourceId: string; + path: string; + value: any + }; + + /** + * Create a new NewPatchAddOperationAction + * + * @param resourceType + * the resource's type where to add operation + * @param resourceId + * the resource's ID + * @param path + * the path of the operation + * @param value + * the operation's payload + */ + constructor(resourceType: string, resourceId: string, path: string, value: any) { + this.payload = { resourceType, resourceId, path, value }; + } +} + +/** + * An ngrx action to add new JSON Patch COPY operation to state + */ +export class NewPatchCopyOperationAction implements Action { + type = JsonPatchOperationsActionTypes.NEW_JSON_PATCH_COPY_OPERATION; + payload: { + resourceType: string; + resourceId: string; + from: string; + path: string; + }; + + /** + * Create a new NewPatchCopyOperationAction + * + * @param resourceType + * the resource's type + * @param resourceId + * the resource's ID + * @param from + * the path to copy the value from + * @param path + * the path where to copy the value + */ + constructor(resourceType: string, resourceId: string, from: string, path: string) { + this.payload = { resourceType, resourceId, from, path }; + } +} + +/** + * An ngrx action to Add new JSON Patch MOVE operation to state + */ +export class NewPatchMoveOperationAction implements Action { + type = JsonPatchOperationsActionTypes.NEW_JSON_PATCH_MOVE_OPERATION; + payload: { + resourceType: string; + resourceId: string; + from: string; + path: string; + }; + + /** + * Create a new NewPatchMoveOperationAction + * + * @param resourceType + * the resource's type + * @param resourceId + * the resource's ID + * @param from + * the path to move the value from + * @param path + * the path where to move the value + */ + constructor(resourceType: string, resourceId: string, from: string, path: string) { + this.payload = { resourceType, resourceId, from, path }; + } +} + +/** + * An ngrx action to Add new JSON Patch REMOVE operation to state + */ +export class NewPatchRemoveOperationAction implements Action { + type = JsonPatchOperationsActionTypes.NEW_JSON_PATCH_REMOVE_OPERATION; + payload: { + resourceType: string; + resourceId: string; + path: string; + }; + + /** + * Create a new NewPatchRemoveOperationAction + * + * @param resourceType + * the resource's type + * @param resourceId + * the resource's ID + * @param path + * the path of the operation + */ + constructor(resourceType: string, resourceId: string, path: string) { + this.payload = { resourceType, resourceId, path }; + } +} + +/** + * An ngrx action to add new JSON Patch REPLACE operation to state + */ +export class NewPatchReplaceOperationAction implements Action { + type = JsonPatchOperationsActionTypes.NEW_JSON_PATCH_REPLACE_OPERATION; + payload: { + resourceType: string; + resourceId: string; + path: string; + value: any + }; + + /** + * Create a new NewPatchReplaceOperationAction + * + * @param resourceType + * the resource's type + * @param resourceId + * the resource's ID + * @param path + * the path of the operation + * @param value + * the operation's payload + */ + constructor(resourceType: string, resourceId: string, path: string, value: any) { + this.payload = { resourceType, resourceId, path, value }; + } +} + +/* tslint:enable:max-classes-per-file */ + +/** + * Export a type alias of all actions in this action group + * so that reducers can easily compose action types + */ +export type PatchOperationsActions + = CommitPatchOperationsAction + | FlushPatchOperationsAction + | NewPatchAddOperationAction + | NewPatchCopyOperationAction + | NewPatchMoveOperationAction + | NewPatchRemoveOperationAction + | NewPatchReplaceOperationAction + | RollbacktPatchOperationsAction + | StartTransactionPatchOperationsAction diff --git a/src/app/core/json-patch/json-patch-operations.effects.ts b/src/app/core/json-patch/json-patch-operations.effects.ts new file mode 100644 index 0000000000..79fd63fafa --- /dev/null +++ b/src/app/core/json-patch/json-patch-operations.effects.ts @@ -0,0 +1,20 @@ +import { Injectable } from '@angular/core'; +import { Effect, Actions } from '@ngrx/effects'; + +import { + CommitPatchOperationsAction, FlushPatchOperationsAction, + JsonPatchOperationsActionTypes +} from './json-patch-operations.actions'; + +@Injectable() +export class JsonPatchOperationsEffects { + + @Effect() commit$ = this.actions$ + .ofType(JsonPatchOperationsActionTypes.COMMIT_JSON_PATCH_OPERATIONS) + .map((action: CommitPatchOperationsAction) => { + return new FlushPatchOperationsAction(action.payload.resourceType, action.payload.resourceId); + }); + + constructor(private actions$: Actions) {} + +} diff --git a/src/app/core/json-patch/json-patch-operations.reducer.ts b/src/app/core/json-patch/json-patch-operations.reducer.ts new file mode 100644 index 0000000000..4eb5a48d23 --- /dev/null +++ b/src/app/core/json-patch/json-patch-operations.reducer.ts @@ -0,0 +1,292 @@ +import { hasValue, isNotEmpty, isNotUndefined, isNull } from '../../shared/empty.util'; + +import { + FlushPatchOperationsAction, + PatchOperationsActions, + JsonPatchOperationsActionTypes, + NewPatchAddOperationAction, + NewPatchCopyOperationAction, + NewPatchMoveOperationAction, + NewPatchRemoveOperationAction, + NewPatchReplaceOperationAction, + CommitPatchOperationsAction, + StartTransactionPatchOperationsAction, + RollbacktPatchOperationsAction +} from './json-patch-operations.actions'; +import { JsonPatchOperationModel, JsonPatchOperationType } from './json-patch.model'; + +export interface JsonPatchOperationObject { + operation: JsonPatchOperationModel; + timeAdded: number; +} + +export interface JsonPatchOperationsEntry { + body: JsonPatchOperationObject[]; +} + +export interface JsonPatchOperationsResourceEntry { + children: { [resourceId: string]: JsonPatchOperationsEntry }; + transactionStartTime: number; + commitPending: boolean; +} + +/** + * The JSON patch operations State + * + * Consists of a map with a namespace as key, + * and an array of JsonPatchOperationModel as values + */ +export interface JsonPatchOperationsState { + [resourceType: string]: JsonPatchOperationsResourceEntry; +} + +const initialState: JsonPatchOperationsState = Object.create(null); + +export function jsonPatchOperationsReducer(state = initialState, action: PatchOperationsActions): JsonPatchOperationsState { + switch (action.type) { + + case JsonPatchOperationsActionTypes.COMMIT_JSON_PATCH_OPERATIONS: { + return commitOperations(state, action as CommitPatchOperationsAction); + } + + case JsonPatchOperationsActionTypes.FLUSH_JSON_PATCH_OPERATIONS: { + return flushOperation(state, action as FlushPatchOperationsAction); + } + + case JsonPatchOperationsActionTypes.NEW_JSON_PATCH_ADD_OPERATION: { + return newOperation(state, action as NewPatchAddOperationAction); + } + + case JsonPatchOperationsActionTypes.NEW_JSON_PATCH_COPY_OPERATION: { + return newOperation(state, action as NewPatchCopyOperationAction); + } + + case JsonPatchOperationsActionTypes.NEW_JSON_PATCH_MOVE_OPERATION: { + return newOperation(state, action as NewPatchMoveOperationAction); + } + + case JsonPatchOperationsActionTypes.NEW_JSON_PATCH_REMOVE_OPERATION: { + return newOperation(state, action as NewPatchRemoveOperationAction); + } + + case JsonPatchOperationsActionTypes.NEW_JSON_PATCH_REPLACE_OPERATION: { + return newOperation(state, action as NewPatchReplaceOperationAction); + } + + case JsonPatchOperationsActionTypes.ROLLBACK_JSON_PATCH_OPERATIONS: { + return rollbackOperations(state, action as RollbacktPatchOperationsAction); + } + + case JsonPatchOperationsActionTypes.START_TRANSACTION_JSON_PATCH_OPERATIONS: { + return startTransactionPatchOperations(state, action as StartTransactionPatchOperationsAction); + } + + default: { + return state; + } + } +} + +/** + * Set the transaction start time. + * + * @param state + * the current state + * @param action + * an StartTransactionPatchOperationsAction + * @return JsonPatchOperationsState + * the new state. + */ +function startTransactionPatchOperations(state: JsonPatchOperationsState, action: StartTransactionPatchOperationsAction): JsonPatchOperationsState { + if (hasValue(state[ action.payload.resourceType ]) + && isNull(state[ action.payload.resourceType ].transactionStartTime)) { + return Object.assign({}, state, { + [action.payload.resourceType]: Object.assign({}, state[ action.payload.resourceType ], { + children: state[ action.payload.resourceType ].children, + transactionStartTime: action.payload.startTime, + commitPending: true + }) + }); + } else { + return state; + } +} + +/** + * Set commit pending state. + * + * @param state + * the current state + * @param action + * an CommitPatchOperationsAction + * @return JsonPatchOperationsState + * the new state, with the section new validity status. + */ +function commitOperations(state: JsonPatchOperationsState, action: CommitPatchOperationsAction): JsonPatchOperationsState { + if (hasValue(state[ action.payload.resourceType ]) + && state[ action.payload.resourceType ].commitPending) { + return Object.assign({}, state, { + [action.payload.resourceType]: Object.assign({}, state[ action.payload.resourceType ], { + children: state[ action.payload.resourceType ].children, + transactionStartTime: state[ action.payload.resourceType ].transactionStartTime, + commitPending: false + }) + }); + } else { + return state; + } +} + +/** + * Set commit pending state. + * + * @param state + * the current state + * @param action + * an RollbacktPatchOperationsAction + * @return JsonPatchOperationsState + * the new state. + */ +function rollbackOperations(state: JsonPatchOperationsState, action: RollbacktPatchOperationsAction): JsonPatchOperationsState { + if (hasValue(state[ action.payload.resourceType ]) + && state[ action.payload.resourceType ].commitPending) { + return Object.assign({}, state, { + [action.payload.resourceType]: Object.assign({}, state[ action.payload.resourceType ], { + children: state[ action.payload.resourceType ].children, + transactionStartTime: null, + commitPending: false + }) + }); + } else { + return state; + } +} + +/** + * Add new JSON patch operation list. + * + * @param state + * the current state + * @param action + * an NewPatchAddOperationAction + * @return JsonPatchOperationsState + * the new state, with the section new validity status. + */ +function newOperation(state: JsonPatchOperationsState, action): JsonPatchOperationsState { + const newState = Object.assign({}, state); + const newBody = addOperationToList( + (hasValue(newState[ action.payload.resourceType ]) + && hasValue(newState[ action.payload.resourceType ].children) + && hasValue(newState[ action.payload.resourceType ].children[ action.payload.resourceId ]) + && isNotEmpty(newState[ action.payload.resourceType ].children[ action.payload.resourceId ].body)) + ? newState[ action.payload.resourceType ].children[ action.payload.resourceId ].body : Array.of(), + action.type, + action.payload.path, + hasValue(action.payload.value) ? action.payload.value : null); + + if (hasValue(newState[ action.payload.resourceType ]) + && hasValue(newState[ action.payload.resourceType ].children)) { + return Object.assign({}, state, { + [action.payload.resourceType]: Object.assign({}, state[ action.payload.resourceType ], { + children: Object.assign({}, state[ action.payload.resourceType ].children, { + [action.payload.resourceId]: { + body: newBody, + } + }), + transactionStartTime: state[ action.payload.resourceType ].transactionStartTime, + commitPending: isNotUndefined(state[ action.payload.resourceType ].commitPending) ? state[ action.payload.resourceType ].commitPending : false + }) + }); + } else { + return Object.assign({}, state, { + [action.payload.resourceType]: Object.assign({}, { + children: { + [action.payload.resourceId]: { + body: newBody, + } + }, + transactionStartTime: null, + commitPending: false + }) + }); + } +} + +/** + * Set the section validity. + * + * @param state + * the current state + * @param action + * an LoadSubmissionFormAction + * @return SubmissionObjectState + * the new state, with the section new validity status. + */ +function flushOperation(state: JsonPatchOperationsState, action: FlushPatchOperationsAction): JsonPatchOperationsState { + if (hasValue(state[ action.payload.resourceType ])) { + let newChildren; + if (isNotUndefined(action.payload.resourceId)) { + // flush only specified child's operations + if (hasValue(state[ action.payload.resourceType ].children) + && hasValue(state[ action.payload.resourceType ].children[ action.payload.resourceId ])) { + newChildren = Object.assign({}, state[ action.payload.resourceType ].children, { + [action.payload.resourceId]: { + body: state[ action.payload.resourceType ].children[ action.payload.resourceId ].body + .filter((entry) => entry.timeAdded > state[ action.payload.resourceType ].transactionStartTime) + } + }); + } else { + newChildren = state[ action.payload.resourceType ].children; + } + } else { + // flush all children's operations + newChildren = state[ action.payload.resourceType ].children; + Object.keys(newChildren) + .forEach((resourceId) => { + newChildren = Object.assign({}, newChildren, { + [resourceId]: { + body: newChildren[ resourceId ].body + .filter((entry) => entry.timeAdded > state[ action.payload.resourceType ].transactionStartTime) + } + }); + }) + } + return Object.assign({}, state, { + [action.payload.resourceType]: Object.assign({}, state[ action.payload.resourceType ], { + children: newChildren, + transactionStartTime: null, + commitPending: state[ action.payload.resourceType ].commitPending + }) + }); + } else { + return state; + } +} + +function addOperationToList(body: JsonPatchOperationObject[], actionType, targetPath, value?) { + const newBody = Array.from(body); + switch (actionType) { + case JsonPatchOperationsActionTypes.NEW_JSON_PATCH_ADD_OPERATION: + newBody.push(makeOperationEntry({ + op: JsonPatchOperationType.add, + path: targetPath, + value: value + })); + break; + case JsonPatchOperationsActionTypes.NEW_JSON_PATCH_REPLACE_OPERATION: + newBody.push(makeOperationEntry({ + op: JsonPatchOperationType.replace, + path: targetPath, + value: value + })); + break; + case JsonPatchOperationsActionTypes.NEW_JSON_PATCH_REMOVE_OPERATION: + newBody.push(makeOperationEntry({ op: JsonPatchOperationType.remove, path: targetPath })); + break; + } + return newBody; +} + +function makeOperationEntry(operation) { + return { operation: operation, timeAdded: new Date().getTime() }; +} diff --git a/src/app/core/json-patch/json-patch-operations.service.ts b/src/app/core/json-patch/json-patch-operations.service.ts new file mode 100644 index 0000000000..8328a6d4a7 --- /dev/null +++ b/src/app/core/json-patch/json-patch-operations.service.ts @@ -0,0 +1,127 @@ +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs/Observable'; +import { hasValue, isEmpty, isNotEmpty, isNotUndefined, isUndefined } from '../../shared/empty.util'; +import { ErrorResponse, PostPatchSuccessResponse, RestResponse } from '../cache/response-cache.models'; +import { ResponseCacheEntry } from '../cache/response-cache.reducer'; +import { ResponseCacheService } from '../cache/response-cache.service'; +import { PatchRequest, RestRequest, SubmissionPatchRequest } from '../data/request.models'; +import { RequestService } from '../data/request.service'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { CoreState } from '../core.reducers'; +import { Store } from '@ngrx/store'; +import { jsonPatchOperationsByResourceType } from './selectors'; +import { JsonPatchOperationsResourceEntry } from './json-patch-operations.reducer'; +import { + CommitPatchOperationsAction, + RollbacktPatchOperationsAction, + StartTransactionPatchOperationsAction +} from './json-patch-operations.actions'; +import { JsonPatchOperationModel } from './json-patch.model'; + +@Injectable() +export class JsonPatchOperationsService { + protected linkPath; + + constructor(protected responseCache: ResponseCacheService, + protected requestService: RequestService, + protected store: Store, + protected halService: HALEndpointService) { + } + + protected submitData(request: RestRequest): Observable { + const [successResponse, errorResponse] = this.responseCache.get(request.href) + .map((entry: ResponseCacheEntry) => entry.response) + .partition((response: RestResponse) => response.isSuccessful); + return Observable.merge( + errorResponse.flatMap((response: ErrorResponse) => + Observable.throw(new Error(`Couldn't send data to server`))), + successResponse + .filter((response: PostPatchSuccessResponse) => isNotEmpty(response)) + .map((response: PostPatchSuccessResponse) => response.dataDefinition) + .distinctUntilChanged()); + } + + protected submitJsonPatchOperations(hrefObs: Observable, resourceType: string, resourceId?: string) { + let startTransactionTime = null; + const [patchRequestObs, emptyRequestObs] = hrefObs + .flatMap((endpointURL: string) => { + return this.store.select(jsonPatchOperationsByResourceType(resourceType)) + .take(1) + .filter((operationsList: JsonPatchOperationsResourceEntry) => isUndefined(operationsList) || !(operationsList.commitPending)) + .do(() => startTransactionTime = new Date().getTime()) + .map((operationsList: JsonPatchOperationsResourceEntry) => { + const body: JsonPatchOperationModel[] = []; + if (isNotEmpty(operationsList)) { + if (isNotEmpty(resourceId)) { + if (isNotUndefined(operationsList.children[resourceId]) && isNotEmpty(operationsList.children[resourceId].body)) { + operationsList.children[resourceId].body.forEach((entry) => { + body.push(entry.operation); + }); + } + } else { + Object.keys(operationsList.children) + .filter((key) => operationsList.children.hasOwnProperty(key)) + .filter((key) => hasValue(operationsList.children[key])) + .filter((key) => hasValue(operationsList.children[key].body)) + .forEach((key) => { + operationsList.children[key].body.forEach((entry) => { + body.push(entry.operation); + }); + }) + } + } + return new SubmissionPatchRequest(this.requestService.generateRequestId(), endpointURL, body); + }); + }) + .partition((request: PatchRequest) => isNotEmpty(request.body)); + + return Observable.merge( + emptyRequestObs + .filter((request: PatchRequest) => isEmpty(request.body)) + .do(() => startTransactionTime = null) + .map(() => null), + patchRequestObs + .filter((request: PatchRequest) => isNotEmpty(request.body)) + .do(() => this.store.dispatch(new StartTransactionPatchOperationsAction(resourceType, resourceId, startTransactionTime))) + .do((request: PatchRequest) => this.requestService.configure(request, true)) + .flatMap((request: PatchRequest) => { + const [successResponse, errorResponse] = this.responseCache.get(request.href) + .filter((entry: ResponseCacheEntry) => startTransactionTime < entry.timeAdded) + .take(1) + .map((entry: ResponseCacheEntry) => entry.response) + .partition((response: RestResponse) => response.isSuccessful); + return Observable.merge( + errorResponse + .do(() => this.store.dispatch(new RollbacktPatchOperationsAction(resourceType, resourceId))) + .flatMap((response: ErrorResponse) => Observable.of(new Error(`Couldn't patch operations`))), + successResponse + .filter((response: PostPatchSuccessResponse) => isNotEmpty(response)) + .do(() => this.store.dispatch(new CommitPatchOperationsAction(resourceType, resourceId))) + .map((response: PostPatchSuccessResponse) => response.dataDefinition) + .distinctUntilChanged()); + }) + ); + } + + protected getEndpointByIDHref(endpoint, resourceID): string { + return isNotEmpty(resourceID) ? `${endpoint}/${resourceID}` : `${endpoint}`; + } + + public jsonPatchByResourceType(linkName: string, scopeId: string, resourceType: string,) { + const hrefObs = this.halService.getEndpoint(linkName) + .filter((href: string) => isNotEmpty(href)) + .distinctUntilChanged() + .map((endpointURL: string) => this.getEndpointByIDHref(endpointURL, scopeId)); + + return this.submitJsonPatchOperations(hrefObs, resourceType); + } + + public jsonPatchByResourceID(linkName: string, scopeId: string, resourceType: string, resourceId: string) { + const hrefObs = this.halService.getEndpoint(linkName) + .filter((href: string) => isNotEmpty(href)) + .distinctUntilChanged() + .map((endpointURL: string) => this.getEndpointByIDHref(endpointURL, scopeId)); + + return this.submitJsonPatchOperations(hrefObs, resourceType, resourceId); + } +} diff --git a/src/app/core/json-patch/json-patch.model.ts b/src/app/core/json-patch/json-patch.model.ts new file mode 100644 index 0000000000..c0553fde97 --- /dev/null +++ b/src/app/core/json-patch/json-patch.model.ts @@ -0,0 +1,14 @@ +export enum JsonPatchOperationType { + test = 'test', + remove = 'remove', + add = 'add', + replace = 'replace', + move = 'move', + copy = 'copy', +} + +export class JsonPatchOperationModel { + op: JsonPatchOperationType; + path: string; + value: any; +} diff --git a/src/app/core/json-patch/selectors.ts b/src/app/core/json-patch/selectors.ts new file mode 100644 index 0000000000..547d4b82e8 --- /dev/null +++ b/src/app/core/json-patch/selectors.ts @@ -0,0 +1,34 @@ +// @TODO: Merge with keySelector function present in 'src/app/core/shared/selectors.ts' +import { createSelector, MemoizedSelector, Selector } from '@ngrx/store'; +import { hasValue } from '../../shared/empty.util'; +import { coreSelector, CoreState } from '../core.reducers'; +import { JsonPatchOperationsEntry, JsonPatchOperationsResourceEntry } from './json-patch-operations.reducer'; + +export function keySelector(parentSelector: Selector, subState: string, key: string): MemoizedSelector { + return createSelector(parentSelector, (state: T) => { + if (hasValue(state[subState])) { + return state[subState][key]; + } else { + return undefined; + } + }); +} + +export function subStateSelector(parentSelector: Selector, subState: string): MemoizedSelector { + return createSelector(parentSelector, (state: T) => { + if (hasValue(state[subState])) { + return state[subState]; + } else { + return undefined; + } + }); +} + +export function jsonPatchOperationsByResourceType(resourceType: string): MemoizedSelector { + return keySelector(coreSelector,'json/patch', resourceType); +} + +export function jsonPatchOperationsByResourcId(resourceType: string, resourceId: string): MemoizedSelector { + const resourceTypeSelector = jsonPatchOperationsByResourceType(resourceType); + return subStateSelector(resourceTypeSelector, resourceId); +} diff --git a/src/app/core/shared/collection.model.ts b/src/app/core/shared/collection.model.ts index b2f8d90a65..c2287e39a7 100644 --- a/src/app/core/shared/collection.model.ts +++ b/src/app/core/shared/collection.model.ts @@ -3,6 +3,8 @@ import { Bitstream } from './bitstream.model'; import { Item } from './item.model'; import { RemoteData } from '../data/remote-data'; import { Observable } from 'rxjs/Observable'; +import { License } from './license.model'; +import { ResourcePolicy } from './resource-policy.model'; export class Collection extends DSpaceObject { @@ -39,7 +41,7 @@ export class Collection extends DSpaceObject { * The license of this Collection * Corresponds to the metadata field dc.rights.license */ - get license(): string { + get dcLicense(): string { return this.findMetadata('dc.rights.license'); } @@ -51,11 +53,21 @@ export class Collection extends DSpaceObject { return this.findMetadata('dc.description.tableofcontents'); } + /** + * The deposit license of this Collection + */ + license: Observable>; + /** * The Bitstream that represents the logo of this Collection */ logo: Observable>; + /** + * The default access conditions of this Collection + */ + defaultAccessConditions: Observable>; + /** * An array of Collections that are direct parents of this Collection */ diff --git a/src/app/core/shared/config/config-access-condition-option.model.ts b/src/app/core/shared/config/config-access-condition-option.model.ts new file mode 100644 index 0000000000..1f2e826af7 --- /dev/null +++ b/src/app/core/shared/config/config-access-condition-option.model.ts @@ -0,0 +1,8 @@ +export class AccessConditionOption { + name: string; + groupUUID: string; + hasStartDate: boolean; + hasEndDate: boolean; + maxStartDate: string; + maxEndDate: string; +} diff --git a/src/app/core/shared/config/config-object-factory.ts b/src/app/core/shared/config/config-object-factory.ts index 4cb5016983..b43d4456f4 100644 --- a/src/app/core/shared/config/config-object-factory.ts +++ b/src/app/core/shared/config/config-object-factory.ts @@ -1,5 +1,4 @@ - -import { GenericConstructor } from '../../shared/generic-constructor'; +import { GenericConstructor } from '../generic-constructor'; import { SubmissionSectionModel } from './config-submission-section.model'; import { SubmissionFormsModel } from './config-submission-forms.model'; @@ -7,6 +6,7 @@ import { SubmissionDefinitionsModel } from './config-submission-definitions.mode import { ConfigType } from './config-type'; import { ConfigObject } from './config.model'; import { ConfigAuthorityModel } from './config-authority.model'; +import { SubmissionUploadsModel } from './config-submission-uploads.model'; export class ConfigObjectFactory { public static getConstructor(type): GenericConstructor { @@ -23,6 +23,10 @@ export class ConfigObjectFactory { case ConfigType.SubmissionSections: { return SubmissionSectionModel } + case ConfigType.SubmissionUpload: + case ConfigType.SubmissionUploads: { + return SubmissionUploadsModel + } case ConfigType.Authority: { return ConfigAuthorityModel } diff --git a/src/app/core/shared/config/config-submission-section.model.ts b/src/app/core/shared/config/config-submission-section.model.ts index 0eb9daaeab..69b5af2d1a 100644 --- a/src/app/core/shared/config/config-submission-section.model.ts +++ b/src/app/core/shared/config/config-submission-section.model.ts @@ -1,5 +1,6 @@ -import { autoserialize, autoserializeAs, inheritSerialization } from 'cerialize'; +import { autoserialize, inheritSerialization } from 'cerialize'; import { ConfigObject } from './config.model'; +import { SectionsType } from '../../../submission/sections/sections-type'; @inheritSerialization(ConfigObject) export class SubmissionSectionModel extends ConfigObject { @@ -11,7 +12,7 @@ export class SubmissionSectionModel extends ConfigObject { mandatory: boolean; @autoserialize - sectionType: string; + sectionType: SectionsType; @autoserialize visibility: { diff --git a/src/app/core/shared/config/config-submission-uploads.model.ts b/src/app/core/shared/config/config-submission-uploads.model.ts new file mode 100644 index 0000000000..c970ff013f --- /dev/null +++ b/src/app/core/shared/config/config-submission-uploads.model.ts @@ -0,0 +1,21 @@ +import {autoserialize, autoserializeAs, inheritSerialization} from 'cerialize'; +import { ConfigObject } from './config.model'; +import { AccessConditionOption } from './config-access-condition-option.model'; +import {SubmissionFormsModel} from './config-submission-forms.model'; + +@inheritSerialization(ConfigObject) +export class SubmissionUploadsModel extends ConfigObject { + + @autoserialize + accessConditionOptions: AccessConditionOption[]; + + @autoserializeAs(SubmissionFormsModel) + metadata: SubmissionFormsModel[]; + + @autoserialize + required: boolean; + + @autoserialize + maxSize: number; + +} diff --git a/src/app/core/shared/config/config-type.ts b/src/app/core/shared/config/config-type.ts index 17ed099229..a240035eb9 100644 --- a/src/app/core/shared/config/config-type.ts +++ b/src/app/core/shared/config/config-type.ts @@ -2,7 +2,6 @@ * TODO replace with actual string enum after upgrade to TypeScript 2.4: * https://github.com/Microsoft/TypeScript/pull/15486 */ -import { ResourceType } from '../resource-type'; export enum ConfigType { SubmissionDefinitions = 'submissiondefinitions', @@ -11,5 +10,6 @@ export enum ConfigType { SubmissionForms = 'submissionforms', SubmissionSections = 'submissionsections', SubmissionSection = 'submissionsection', - Authority = 'authority' + SubmissionUploads = 'submissionuploads', + SubmissionUpload = 'submissionupload', } diff --git a/src/app/core/shared/dspace-object.model.ts b/src/app/core/shared/dspace-object.model.ts index 5e62e3e321..52a7925bab 100644 --- a/src/app/core/shared/dspace-object.model.ts +++ b/src/app/core/shared/dspace-object.model.ts @@ -60,9 +60,9 @@ export class DSpaceObject implements CacheableObject, ListableObject { * @return string */ findMetadata(key: string, language?: string): string { - const metadatum = this.metadata.find((m: Metadatum) => { + const metadatum = (this.metadata) ? this.metadata.find((m: Metadatum) => { return m.key === key && (isEmpty(language) || m.language === language) - }); + }) : null; if (isNotEmpty(metadatum)) { return metadatum.value; } else { @@ -81,7 +81,7 @@ export class DSpaceObject implements CacheableObject, ListableObject { * @return Array */ filterMetadata(keys: string[]): Metadatum[] { - return this.metadata.filter((metadatum: Metadatum) => { + return (this.metadata || []).filter((metadatum: Metadatum) => { return keys.some((key) => key === metadatum.key); }); } diff --git a/src/app/core/shared/file.service.ts b/src/app/core/shared/file.service.ts new file mode 100644 index 0000000000..75dbcefda8 --- /dev/null +++ b/src/app/core/shared/file.service.ts @@ -0,0 +1,35 @@ +import { Injectable } from '@angular/core'; +import { HttpHeaders } from '@angular/common/http'; + +import { DSpaceRESTv2Service, HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service'; +import { RestRequestMethod } from '../data/request.models'; +import { saveAs } from 'file-saver'; +import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; + +@Injectable() +export class FileService { + constructor( + private restService: DSpaceRESTv2Service + ) { } + + downloadFile(url: string) { + const headers = new HttpHeaders(); + const options: HttpOptions = Object.create({headers, responseType: 'blob'}); + return this.restService.request(RestRequestMethod.Get, url, null, options) + .subscribe((data) => { + saveAs(data.payload as Blob, this.getFileNameFromResponseContentDisposition(data)); + }); + } + + /** + * Derives file name from the http response + * by looking inside content-disposition + * @param res http DSpaceRESTV2Response + */ + getFileNameFromResponseContentDisposition(res: DSpaceRESTV2Response) { + const contentDisposition = res.headers.get('content-disposition') || ''; + const matches = /filename="([^;]+)"/ig.exec(contentDisposition) || []; + const fileName = (matches[1] || 'untitled').trim().replace(/\.[^/.]+$/, ''); + return fileName; + }; +} diff --git a/src/app/core/shared/hal-endpoint.service.ts b/src/app/core/shared/hal-endpoint.service.ts index 3bedeb9915..7ca8a1b8c7 100644 --- a/src/app/core/shared/hal-endpoint.service.ts +++ b/src/app/core/shared/hal-endpoint.service.ts @@ -55,8 +55,7 @@ export class HALEndpointService { return endpointMap[subPath]; } else { /*TODO remove if/else block once the rest response contains _links for facets*/ - currentPath += '/' + subPath; - return currentPath; + return currentPath + '/' + subPath; } }), ]) diff --git a/src/app/core/shared/item.model.ts b/src/app/core/shared/item.model.ts index dd60ad9b01..be22c28b91 100644 --- a/src/app/core/shared/item.model.ts +++ b/src/app/core/shared/item.model.ts @@ -88,8 +88,10 @@ export class Item extends DSpaceObject { */ getBitstreamsByBundleName(bundleName: string): Observable { return this.bitstreams + .filter((rd: RemoteData) => rd.hasSucceeded) .map((rd: RemoteData) => rd.payload) .filter((bitstreams: Bitstream[]) => hasValue(bitstreams)) + .first() .startWith([]) .map((bitstreams) => { return bitstreams diff --git a/src/app/core/shared/license.model.ts b/src/app/core/shared/license.model.ts new file mode 100644 index 0000000000..a04422242a --- /dev/null +++ b/src/app/core/shared/license.model.ts @@ -0,0 +1,14 @@ +import { DSpaceObject } from './dspace-object.model'; + +export class License extends DSpaceObject { + + /** + * Is the license custom? + */ + custom: boolean; + + /** + * The text of the license + */ + text: string; +} diff --git a/src/app/core/shared/operators.ts b/src/app/core/shared/operators.ts index c0b9be3fbf..e9b5d1ecbd 100644 --- a/src/app/core/shared/operators.ts +++ b/src/app/core/shared/operators.ts @@ -38,9 +38,9 @@ export const getResourceLinksFromResponse = () => map((entry: ResponseCacheEntry) => (entry.response as DSOSuccessResponse).resourceSelfLinks), ); -export const configureRequest = (requestService: RequestService) => +export const configureRequest = (requestService: RequestService, forceBypassCache?: boolean) => (source: Observable): Observable => - source.pipe(tap((request: RestRequest) => requestService.configure(request))); + source.pipe(tap((request: RestRequest) => requestService.configure(request, forceBypassCache))); export const getRemoteDataPayload = () => (source: Observable>): Observable => diff --git a/src/app/core/shared/patch-request.model.ts b/src/app/core/shared/patch-request.model.ts new file mode 100644 index 0000000000..ab99b17dfd --- /dev/null +++ b/src/app/core/shared/patch-request.model.ts @@ -0,0 +1,14 @@ +export enum PatchOperationType { + test = 'test', + remove = 'remove', + add = 'add', + replace = 'replace', + move = 'move', + copy = 'copy', +} + +export class PatchOperationModel { + op: PatchOperationType; + path: string; + value: any; +} diff --git a/src/app/core/shared/resource-policy.model.ts b/src/app/core/shared/resource-policy.model.ts new file mode 100644 index 0000000000..0cbcd8d883 --- /dev/null +++ b/src/app/core/shared/resource-policy.model.ts @@ -0,0 +1,34 @@ +import { DSpaceObject } from './dspace-object.model'; + +export class ResourcePolicy extends DSpaceObject { + + /** + * The action of the resource policy + */ + action: string; + + /** + * The identifier of the resource policy + */ + id: string; + + /** + * The group uuid bound to the resource policy + */ + groupUUID: string; + + /** + * The end date of the resource policy + */ + endDate: string; + + /** + * The start date of the resource policy + */ + startDate: string; + + /** + * The type of the resource policy + */ + rpType: string +} diff --git a/src/app/core/shared/resource-type.ts b/src/app/core/shared/resource-type.ts index b774188f63..38ac496f5d 100644 --- a/src/app/core/shared/resource-type.ts +++ b/src/app/core/shared/resource-type.ts @@ -8,4 +8,8 @@ export enum ResourceType { Community = 'community', Eperson = 'eperson', Group = 'group', + ResourcePolicy = 'resourcePolicy', + License = 'license', + Workflowitem = 'workflowitem', + Workspaceitem = 'workspaceitem', } diff --git a/src/app/core/shared/submit-data-response-definition.model.ts b/src/app/core/shared/submit-data-response-definition.model.ts new file mode 100644 index 0000000000..7220a82b7e --- /dev/null +++ b/src/app/core/shared/submit-data-response-definition.model.ts @@ -0,0 +1,11 @@ +import { autoserialize } from 'cerialize'; + +export class SubmitDataResponseDefinitionObject { + + @autoserialize + public name: string; + + @autoserialize + public type: string; + +} diff --git a/src/app/core/submission/models/edititem.model.ts b/src/app/core/submission/models/edititem.model.ts new file mode 100644 index 0000000000..9c8da2ab5a --- /dev/null +++ b/src/app/core/submission/models/edititem.model.ts @@ -0,0 +1,4 @@ +import { Workspaceitem } from './workspaceitem.model'; + +export class EditItem extends Workspaceitem { +} diff --git a/src/app/core/submission/models/normalized-edititem.model.ts b/src/app/core/submission/models/normalized-edititem.model.ts new file mode 100644 index 0000000000..49dc3e6bd4 --- /dev/null +++ b/src/app/core/submission/models/normalized-edititem.model.ts @@ -0,0 +1,47 @@ +import { autoserialize, autoserializeAs, inheritSerialization } from 'cerialize'; +import { mapsTo, relationship } from '../../cache/builders/build-decorators'; +import { NormalizedWorkspaceItem } from './normalized-workspaceitem.model'; +import { NormalizedSubmissionObject } from './normalized-submission-object.model'; +import { ResourceType } from '../../shared/resource-type'; +import { SubmissionDefinitionsModel } from '../../shared/config/config-submission-definitions.model'; +import { WorkspaceitemSectionsObject } from './workspaceitem-sections.model'; +import { SubmissionObjectError } from './submission-object.model'; +import { EditItem } from './edititem.model'; + +@mapsTo(EditItem) +@inheritSerialization(NormalizedWorkspaceItem) +export class NormalizedEditItem extends NormalizedSubmissionObject { + + /** + * The item identifier + */ + @autoserialize + id: string; + + /** + * The item last modified date + */ + @autoserialize + lastModified: Date; + + @autoserialize + @relationship(ResourceType.Collection, true) + collection: string[]; + + @autoserialize + @relationship(ResourceType.Item, true) + item: string[]; + + @autoserialize + sections: WorkspaceitemSectionsObject; + + @autoserializeAs(SubmissionDefinitionsModel) + submissionDefinition: SubmissionDefinitionsModel; + + @autoserialize + @relationship(ResourceType.Eperson, true) + submitter: string[]; + + @autoserialize + errors: SubmissionObjectError[] +} diff --git a/src/app/core/submission/models/normalized-submission-object.model.ts b/src/app/core/submission/models/normalized-submission-object.model.ts new file mode 100644 index 0000000000..771902dbc8 --- /dev/null +++ b/src/app/core/submission/models/normalized-submission-object.model.ts @@ -0,0 +1,8 @@ +import { NormalizedDSpaceObject } from '../../cache/models/normalized-dspace-object.model'; + +/** + * An abstract model class for a DSpaceObject. + */ +export abstract class NormalizedSubmissionObject extends NormalizedDSpaceObject { + +} diff --git a/src/app/core/submission/models/normalized-workflowitem.model.ts b/src/app/core/submission/models/normalized-workflowitem.model.ts new file mode 100644 index 0000000000..166e126f41 --- /dev/null +++ b/src/app/core/submission/models/normalized-workflowitem.model.ts @@ -0,0 +1,47 @@ +import { autoserialize, autoserializeAs, inheritSerialization } from 'cerialize'; +import { mapsTo, relationship } from '../../cache/builders/build-decorators'; +import { Workflowitem } from './workflowitem.model'; +import { NormalizedWorkspaceItem } from './normalized-workspaceitem.model'; +import { NormalizedSubmissionObject } from './normalized-submission-object.model'; +import { ResourceType } from '../../shared/resource-type'; +import { SubmissionDefinitionsModel } from '../../shared/config/config-submission-definitions.model'; +import { WorkspaceitemSectionsObject } from './workspaceitem-sections.model'; +import { SubmissionObjectError } from './submission-object.model'; + +@mapsTo(Workflowitem) +@inheritSerialization(NormalizedWorkspaceItem) +export class NormalizedWorkflowItem extends NormalizedSubmissionObject { + + /** + * The workspaceitem identifier + */ + @autoserialize + id: string; + + /** + * The workspaceitem last modified date + */ + @autoserialize + lastModified: Date; + + @autoserialize + @relationship(ResourceType.Collection, true) + collection: string[]; + + @autoserialize + @relationship(ResourceType.Item, true) + item: string[]; + + @autoserialize + sections: WorkspaceitemSectionsObject; + + @autoserializeAs(SubmissionDefinitionsModel) + submissionDefinition: SubmissionDefinitionsModel; + + @autoserialize + @relationship(ResourceType.Eperson, true) + submitter: string[]; + + @autoserialize + errors: SubmissionObjectError[] +} diff --git a/src/app/core/submission/models/normalized-workspaceitem.model.ts b/src/app/core/submission/models/normalized-workspaceitem.model.ts new file mode 100644 index 0000000000..33a0f2b262 --- /dev/null +++ b/src/app/core/submission/models/normalized-workspaceitem.model.ts @@ -0,0 +1,51 @@ +import { autoserialize, autoserializeAs, inheritSerialization } from 'cerialize'; + +import { Workspaceitem } from './workspaceitem.model'; +import { WorkspaceitemSectionsObject } from './workspaceitem-sections.model'; + +import { NormalizedSubmissionObject } from './normalized-submission-object.model'; +import { NormalizedDSpaceObject } from '../../cache/models/normalized-dspace-object.model'; +import { mapsTo, relationship } from '../../cache/builders/build-decorators'; +import { NormalizedCollection } from '../../cache/models/normalized-collection.model'; +import { ResourceType } from '../../shared/resource-type'; +import { SubmissionDefinitionsModel } from '../../shared/config/config-submission-definitions.model'; +import { Eperson } from '../../eperson/models/eperson.model'; +import { SubmissionObjectError } from './submission-object.model'; + +@mapsTo(Workspaceitem) +@inheritSerialization(NormalizedDSpaceObject) +export class NormalizedWorkspaceItem extends NormalizedSubmissionObject { + + /** + * The workspaceitem identifier + */ + @autoserialize + id: string; + + /** + * The workspaceitem last modified date + */ + @autoserialize + lastModified: Date; + + @autoserialize + @relationship(ResourceType.Collection, true) + collection: string[]; + + @autoserialize + @relationship(ResourceType.Item, true) + item: string[]; + + @autoserialize + sections: WorkspaceitemSectionsObject; + + @autoserializeAs(SubmissionDefinitionsModel) + submissionDefinition: SubmissionDefinitionsModel; + + @autoserialize + @relationship(ResourceType.Eperson, true) + submitter: string[]; + + @autoserialize + errors: SubmissionObjectError[] +} diff --git a/src/app/core/submission/models/submission-object.model.ts b/src/app/core/submission/models/submission-object.model.ts new file mode 100644 index 0000000000..f7140383af --- /dev/null +++ b/src/app/core/submission/models/submission-object.model.ts @@ -0,0 +1,43 @@ +import { CacheableObject } from '../../cache/object-cache.reducer'; +import { ListableObject } from '../../../shared/object-collection/shared/listable-object.model'; +import { DSpaceObject } from '../../shared/dspace-object.model'; +import { Eperson } from '../../eperson/models/eperson.model'; +import { RemoteData } from '../../data/remote-data'; +import { Collection } from '../../shared/collection.model'; +import { Item } from '../../shared/item.model'; +import { SubmissionDefinitionsModel } from '../../shared/config/config-submission-definitions.model'; +import { Observable } from 'rxjs/Observable'; +import { WorkspaceitemSectionsObject } from './workspaceitem-sections.model'; + +export interface SubmissionObjectError { + message: string, + paths: string[], +} + +/** + * An abstract model class for a DSpaceObject. + */ +export abstract class SubmissionObject extends DSpaceObject implements CacheableObject, ListableObject { + + /** + * The workspaceitem identifier + */ + id: string; + + /** + * The workspaceitem last modified date + */ + lastModified: Date; + + collection: Observable> | Collection[]; + + item: Observable> | Item[]; + + sections: WorkspaceitemSectionsObject; + + submissionDefinition: SubmissionDefinitionsModel; + + submitter: Observable> | Eperson[]; + + errors: SubmissionObjectError[]; +} diff --git a/src/app/core/submission/models/submission-upload-file-access-condition.model.ts b/src/app/core/submission/models/submission-upload-file-access-condition.model.ts new file mode 100644 index 0000000000..ca2f21de47 --- /dev/null +++ b/src/app/core/submission/models/submission-upload-file-access-condition.model.ts @@ -0,0 +1,7 @@ +export class SubmissionUploadFileAccessConditionObject { + id: string; + name: string; + groupUUID: string; + startDate: string; + endDate: string; +} diff --git a/src/app/core/submission/models/workflowitem.model.ts b/src/app/core/submission/models/workflowitem.model.ts new file mode 100644 index 0000000000..3df49c91f7 --- /dev/null +++ b/src/app/core/submission/models/workflowitem.model.ts @@ -0,0 +1,4 @@ +import { Workspaceitem } from './workspaceitem.model'; + +export class Workflowitem extends Workspaceitem { +} diff --git a/src/app/core/submission/models/workspaceitem-section-deduplication.model.ts b/src/app/core/submission/models/workspaceitem-section-deduplication.model.ts new file mode 100644 index 0000000000..d74191f87c --- /dev/null +++ b/src/app/core/submission/models/workspaceitem-section-deduplication.model.ts @@ -0,0 +1,21 @@ +import { FormFieldMetadataValueObject } from '../../../shared/form/builder/models/form-field-metadata-value.model'; +import { WorkspaceitemSectionUploadFileObject } from './workspaceitem-section-upload-file.model'; +import { FormFieldChangedObject } from '../../../shared/form/builder/models/form-field-unexpected-object.model'; + +import { DSpaceObject } from '../../shared/dspace-object.model'; + +export interface WorkspaceitemSectionDeduplicationObject { + matches: DeduplicationSchema[]; +} + +export interface DeduplicationSchema { + submitterDecision?: string; // [reject|verify] + submitterNote?: string; + submitterTime?: string; // (readonly) + + workflowDecision?: string; // [reject|verify] + workflowNote?: string; + workflowTime?: string; // (readonly) + + matchObject?: DSpaceObject; // item, workspaceItem, workflowItem +} diff --git a/src/app/core/submission/models/workspaceitem-section-form.model.ts b/src/app/core/submission/models/workspaceitem-section-form.model.ts new file mode 100644 index 0000000000..e0abad9130 --- /dev/null +++ b/src/app/core/submission/models/workspaceitem-section-form.model.ts @@ -0,0 +1,5 @@ +import { FormFieldMetadataValueObject } from '../../../shared/form/builder/models/form-field-metadata-value.model'; + +export interface WorkspaceitemSectionFormObject { + [metadata: string]: FormFieldMetadataValueObject; +} diff --git a/src/app/core/submission/models/workspaceitem-section-license.model.ts b/src/app/core/submission/models/workspaceitem-section-license.model.ts new file mode 100644 index 0000000000..4a86503a04 --- /dev/null +++ b/src/app/core/submission/models/workspaceitem-section-license.model.ts @@ -0,0 +1,5 @@ +export interface WorkspaceitemSectionLicenseObject { + url: string; + acceptanceDate: string; + granted: boolean; +} diff --git a/src/app/core/submission/models/workspaceitem-section-recycle.model.ts b/src/app/core/submission/models/workspaceitem-section-recycle.model.ts new file mode 100644 index 0000000000..760114e73a --- /dev/null +++ b/src/app/core/submission/models/workspaceitem-section-recycle.model.ts @@ -0,0 +1,8 @@ +import { FormFieldMetadataValueObject } from '../../../shared/form/builder/models/form-field-metadata-value.model'; +import { WorkspaceitemSectionUploadFileObject } from './workspaceitem-section-upload-file.model'; + +export interface WorkspaceitemSectionRecycleObject { + unexpected: any; + metadata: FormFieldMetadataValueObject[]; + files: WorkspaceitemSectionUploadFileObject[]; +} diff --git a/src/app/core/submission/models/workspaceitem-section-upload-file.model.ts b/src/app/core/submission/models/workspaceitem-section-upload-file.model.ts new file mode 100644 index 0000000000..a42a334b86 --- /dev/null +++ b/src/app/core/submission/models/workspaceitem-section-upload-file.model.ts @@ -0,0 +1,15 @@ +import { SubmissionUploadFileAccessConditionObject } from './submission-upload-file-access-condition.model'; +import { WorkspaceitemSectionFormObject } from './workspaceitem-section-form.model'; + +export class WorkspaceitemSectionUploadFileObject { + uuid: string; + metadata: WorkspaceitemSectionFormObject; + sizeBytes: number; + checkSum: { + checkSumAlgorithm: string; + value: string; + }; + url: string; + thumbnail: string; + accessConditions: SubmissionUploadFileAccessConditionObject[]; +} diff --git a/src/app/core/submission/models/workspaceitem-section-upload.model.ts b/src/app/core/submission/models/workspaceitem-section-upload.model.ts new file mode 100644 index 0000000000..b936b5d4d8 --- /dev/null +++ b/src/app/core/submission/models/workspaceitem-section-upload.model.ts @@ -0,0 +1,5 @@ +import { WorkspaceitemSectionUploadFileObject } from './workspaceitem-section-upload-file.model'; + +export interface WorkspaceitemSectionUploadObject { + files: WorkspaceitemSectionUploadFileObject[]; +} diff --git a/src/app/core/submission/models/workspaceitem-sections.model.ts b/src/app/core/submission/models/workspaceitem-sections.model.ts new file mode 100644 index 0000000000..3de9f9588e --- /dev/null +++ b/src/app/core/submission/models/workspaceitem-sections.model.ts @@ -0,0 +1,65 @@ +import { WorkspaceitemSectionFormObject } from './workspaceitem-section-form.model'; +import { WorkspaceitemSectionLicenseObject } from './workspaceitem-section-license.model'; +import { WorkspaceitemSectionUploadObject } from './workspaceitem-section-upload.model'; +import { isNotEmpty, isNotNull } from '../../../shared/empty.util'; +import { FormFieldLanguageValueObject } from '../../../shared/form/builder/models/form-field-language-value.model'; +import { WorkspaceitemSectionRecycleObject } from './workspaceitem-section-recycle.model'; +import { WorkspaceitemSectionDeduplicationObject } from './workspaceitem-section-deduplication.model'; +import { FormFieldMetadataValueObject } from '../../../shared/form/builder/models/form-field-metadata-value.model'; + +export class WorkspaceitemSectionsObject { + [name: string]: WorkspaceitemSectionDataType; + +} + +export function isServerFormValue(obj: any): boolean { + return (typeof obj === 'object' + && obj.hasOwnProperty('value') + && obj.hasOwnProperty('language') + && obj.hasOwnProperty('authority') + && obj.hasOwnProperty('confidence') + && obj.hasOwnProperty('place')) +} + +export function normalizeSectionData(obj: any) { + let result: any = obj; + if (isNotNull(obj)) { + // If is an Instance of FormFieldMetadataValueObject normalize it + if (typeof obj === 'object' && isServerFormValue(obj)) { + // If authority property is set normalize as a FormFieldMetadataValueObject object + /* NOTE: Data received from server could have authority property equal to null, but into form + field's model is required a FormFieldMetadataValueObject object as field value, so double-check in + field's parser and eventually instantiate it */ + // if (isNotEmpty(obj.authority)) { + // result = new FormFieldMetadataValueObject(obj.value, obj.language, obj.authority, (obj.display || obj.value), obj.place, obj.confidence); + // } else if (isNotEmpty(obj.language)) { + // const languageValue = new FormFieldLanguageValueObject(obj.value, obj.language); + // result = languageValue; + // } else { + // // Normalize as a string value + // result = obj.value; + // } + result = new FormFieldMetadataValueObject(obj.value, obj.language, obj.authority, (obj.display || obj.value), obj.place, obj.confidence); + } else if (Array.isArray(obj)) { + result = []; + obj.forEach((item, index) => { + result[index] = normalizeSectionData(item); + }); + } else if (typeof obj === 'object') { + result = Object.create({}); + Object.keys(obj) + .forEach((key) => { + result[key] = normalizeSectionData(obj[key]); + }); + } + } + return result; +} + +export type WorkspaceitemSectionDataType + = WorkspaceitemSectionUploadObject + | WorkspaceitemSectionFormObject + | WorkspaceitemSectionLicenseObject + | WorkspaceitemSectionRecycleObject + | WorkspaceitemSectionDeduplicationObject + | string; diff --git a/src/app/core/submission/models/workspaceitem.model.ts b/src/app/core/submission/models/workspaceitem.model.ts new file mode 100644 index 0000000000..e927431d71 --- /dev/null +++ b/src/app/core/submission/models/workspaceitem.model.ts @@ -0,0 +1,5 @@ +import { SubmissionObject } from './submission-object.model'; + +export class Workspaceitem extends SubmissionObject { + +} diff --git a/src/app/core/submission/normalized-submission-object-factory.ts b/src/app/core/submission/normalized-submission-object-factory.ts new file mode 100644 index 0000000000..724d90f19d --- /dev/null +++ b/src/app/core/submission/normalized-submission-object-factory.ts @@ -0,0 +1,69 @@ +import { SubmissionDefinitionsModel } from '../shared/config/config-submission-definitions.model'; +import { SubmissionFormsModel } from '../shared/config/config-submission-forms.model'; +import { SubmissionSectionModel } from '../shared/config/config-submission-section.model'; +import { GenericConstructor } from '../shared/generic-constructor'; +import { NormalizedBitstream } from '../cache/models/normalized-bitstream.model'; +import { NormalizedBundle } from '../cache/models/normalized-bundle.model'; +import { NormalizedCollection } from '../cache/models/normalized-collection.model'; +import { NormalizedCommunity } from '../cache/models/normalized-community.model'; +import { NormalizedItem } from '../cache/models/normalized-item.model'; +import { NormalizedLicense } from '../cache/models/normalized-license.model'; +import { NormalizedWorkspaceItem } from './models/normalized-workspaceitem.model'; +import { NormalizedObject } from '../cache/models/normalized-object.model'; +import { ConfigObject } from '../shared/config/config.model'; +import { SubmissionResourceType } from './submission-resource-type'; +import { NormalizedResourcePolicy } from '../cache/models/normalized-resource-policy.model'; +import { NormalizedWorkflowItem } from './models/normalized-workflowitem.model'; +import { NormalizedEditItem } from './models/normalized-edititem.model'; + +export class NormalizedSubmissionObjectFactory { + public static getConstructor(type: SubmissionResourceType): GenericConstructor { + switch (type) { + case SubmissionResourceType.Bitstream: { + return NormalizedBitstream + } + case SubmissionResourceType.Bundle: { + return NormalizedBundle + } + case SubmissionResourceType.Item: { + return NormalizedItem + } + case SubmissionResourceType.Collection: { + return NormalizedCollection + } + case SubmissionResourceType.Community: { + return NormalizedCommunity + } + case SubmissionResourceType.ResourcePolicy: { + return NormalizedResourcePolicy + } + case SubmissionResourceType.License: { + return NormalizedLicense + } + case SubmissionResourceType.WorkspaceItem: { + return NormalizedWorkspaceItem + } + case SubmissionResourceType.WorkflowItem: { + return NormalizedWorkflowItem + } + case SubmissionResourceType.EditItem: { + return NormalizedEditItem + } + case SubmissionResourceType.SubmissionDefinition: + case SubmissionResourceType.SubmissionDefinitions: { + return SubmissionDefinitionsModel + } + case SubmissionResourceType.SubmissionForm: + case SubmissionResourceType.SubmissionForms: { + return SubmissionFormsModel + } + case SubmissionResourceType.SubmissionSection: + case SubmissionResourceType.SubmissionSections: { + return SubmissionSectionModel + } + default: { + return undefined; + } + } + } +} diff --git a/src/app/core/submission/submission-resource-type.ts b/src/app/core/submission/submission-resource-type.ts new file mode 100644 index 0000000000..0f43968190 --- /dev/null +++ b/src/app/core/submission/submission-resource-type.ts @@ -0,0 +1,24 @@ +/** + * TODO replace with actual string enum after upgrade to TypeScript 2.4: + * https://github.com/Microsoft/TypeScript/pull/15486 + */ +export enum SubmissionResourceType { + Bundle = 'bundle', + Bitstream = 'bitstream', + BitstreamFormat = 'bitstreamformat', + Item = 'item', + Collection = 'collection', + Community = 'community', + ResourcePolicy = 'resourcePolicies', + License = 'license', + WorkspaceItem = 'workspaceitem', + WorkflowItem = 'workflowitem', + EditItem = 'edititem', + SubmissionDefinitions = 'submissiondefinitions', + SubmissionDefinition = 'submissiondefinition', + SubmissionForm = 'submissionform', + SubmissionForms = 'submissionforms', + SubmissionSections = 'submissionsections', + SubmissionSection = 'submissionsection', + Authority = 'authority' +} diff --git a/src/app/core/submission/submission-response-parsing.service.ts b/src/app/core/submission/submission-response-parsing.service.ts new file mode 100644 index 0000000000..f1879cdeb0 --- /dev/null +++ b/src/app/core/submission/submission-response-parsing.service.ts @@ -0,0 +1,99 @@ +import { Inject, Injectable } from '@angular/core'; + +import { ResponseParsingService } from '../data/parsing.service'; +import { RestRequest } from '../data/request.models'; +import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; +import { ErrorResponse, RestResponse, SubmissionSuccessResponse } from '../cache/response-cache.models'; +import { isEmpty, isNotEmpty, isNotNull } from '../../shared/empty.util'; + +import { ConfigObject } from '../shared/config/config.model'; +import { BaseResponseParsingService, ProcessRequestDTO } from '../data/base-response-parsing.service'; +import { GLOBAL_CONFIG } from '../../../config'; +import { GlobalConfig } from '../../../config/global-config.interface'; +import { ObjectCacheService } from '../cache/object-cache.service'; +import { NormalizedSubmissionObjectFactory } from './normalized-submission-object-factory'; +import { NormalizedObject } from '../cache/models/normalized-object.model'; +import { SubmissionResourceType } from './submission-resource-type'; +import { NormalizedWorkspaceItem } from './models/normalized-workspaceitem.model'; +import { normalizeSectionData } from './models/workspaceitem-sections.model'; +import { NormalizedWorkflowItem } from './models/normalized-workflowitem.model'; +import { NormalizedEditItem } from './models/normalized-edititem.model'; + +@Injectable() +export class SubmissionResponseParsingService extends BaseResponseParsingService implements ResponseParsingService { + + protected objectFactory = NormalizedSubmissionObjectFactory; + protected toCache = false; + + constructor(@Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig, + protected objectCache: ObjectCacheService,) { + super(); + } + + parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse { + if (isNotEmpty(data.payload) + && isNotEmpty(data.payload._links) + && (data.statusCode === '201' || data.statusCode === '200')) { + const dataDefinition = this.processResponse(data.payload, request.href); + return new SubmissionSuccessResponse(dataDefinition[Object.keys(dataDefinition)[0]], data.statusCode, this.processPageInfo(data.payload)); + } else if (isEmpty(data.payload) && data.statusCode === '204') { + // Response from a DELETE request + return new SubmissionSuccessResponse(null, data.statusCode); + } else { + return new ErrorResponse( + Object.assign( + new Error('Unexpected response from server'), + {statusText: data.statusCode} + ) + ); + } + } + + protected processResponse(data: any, requestHref: string): ProcessRequestDTO { + const dataDefinition = this.process(data, requestHref); + const normalizedDefinition = Object.create({}); + normalizedDefinition[Object.keys(dataDefinition)[0]] = []; + dataDefinition[Object.keys(dataDefinition)[0]].forEach((item, index) => { + let normalizedItem = Object.assign({}, item); + // In case data is an Instance of NormalizedWorkspaceItem normalize field value of all the section of type form + if (item instanceof NormalizedWorkspaceItem + || item instanceof NormalizedWorkflowItem + || item instanceof NormalizedEditItem) { + if (item.sections) { + const precessedSection = Object.create({}); + // Iterate over all workspaceitem's sections + Object.keys(item.sections) + .forEach((sectionId) => { + if (typeof item.sections[sectionId] === 'object' && isNotEmpty(item.sections[sectionId])) { + const normalizedSectionData = Object.create({}); + // Iterate over all sections property + Object.keys(item.sections[sectionId]) + .forEach((metdadataId) => { + const entry = item.sections[sectionId][metdadataId]; + // If entry is not an array, for sure is not a section of type form + if (isNotNull(entry) && Array.isArray(entry)) { + normalizedSectionData[metdadataId] = []; + entry.forEach((valueItem) => { + // Parse value and normalize it + const normValue = normalizeSectionData(valueItem); + if (isNotEmpty(normValue)) { + normalizedSectionData[metdadataId].push(normValue); + } + }); + } else { + normalizedSectionData[metdadataId] = entry; + } + }); + precessedSection[sectionId] = normalizedSectionData; + } + }); + normalizedItem = Object.assign({}, item, {sections: precessedSection}); + } + } + normalizedDefinition[Object.keys(dataDefinition)[0]][index] = normalizedItem; + }); + + return normalizedDefinition as ProcessRequestDTO; + } + +} diff --git a/src/app/core/submission/submission-scope-type.ts b/src/app/core/submission/submission-scope-type.ts new file mode 100644 index 0000000000..80d57c853f --- /dev/null +++ b/src/app/core/submission/submission-scope-type.ts @@ -0,0 +1,5 @@ +export enum SubmissionScopeType { + WorkspaceItem = 'WORKSPACE', + WorkflowItem = 'WORKFLOW', + EditItem = 'ITEM', +} diff --git a/src/app/core/submission/workflowitem-data.service.ts b/src/app/core/submission/workflowitem-data.service.ts new file mode 100644 index 0000000000..75abc93d6b --- /dev/null +++ b/src/app/core/submission/workflowitem-data.service.ts @@ -0,0 +1,35 @@ +import { Injectable } from '@angular/core'; + +import { Store } from '@ngrx/store'; +import { Observable } from 'rxjs/Observable'; +import { BrowseService } from '../browse/browse.service'; +import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { ResponseCacheService } from '../cache/response-cache.service'; +import { CoreState } from '../core.reducers'; + +import { DataService } from '../data/data.service'; +import { RequestService } from '../data/request.service'; +import { NormalizedWorkflowItem } from './models/normalized-workflowitem.model'; +import { Workflowitem } from './models/workflowitem.model'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; + +@Injectable() +export class WorkflowitemDataService extends DataService { + protected linkPath = 'workflowitems'; + protected forceBypassCache = true; + + constructor( + protected responseCache: ResponseCacheService, + protected requestService: RequestService, + protected rdbService: RemoteDataBuildService, + protected store: Store, + protected bs: BrowseService, + protected halService: HALEndpointService) { + super(); + } + + public getScopedEndpoint(scopeID: string): Observable { + return this.halService.getEndpoint(this.linkPath); + } + +} diff --git a/src/app/core/submission/workspaceitem-data.service.ts b/src/app/core/submission/workspaceitem-data.service.ts new file mode 100644 index 0000000000..fb052ef49a --- /dev/null +++ b/src/app/core/submission/workspaceitem-data.service.ts @@ -0,0 +1,35 @@ +import { Injectable } from '@angular/core'; + +import { Store } from '@ngrx/store'; +import { Observable } from 'rxjs/Observable'; +import { BrowseService } from '../browse/browse.service'; +import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { ResponseCacheService } from '../cache/response-cache.service'; +import { CoreState } from '../core.reducers'; + +import { DataService } from '../data/data.service'; +import { RequestService } from '../data/request.service'; +import { Workspaceitem } from './models/workspaceitem.model'; +import { NormalizedWorkspaceItem } from './models/normalized-workspaceitem.model'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; + +@Injectable() +export class WorkspaceitemDataService extends DataService { + protected linkPath = 'workspaceitems'; + protected forceBypassCache = true; + + constructor( + protected responseCache: ResponseCacheService, + protected requestService: RequestService, + protected rdbService: RemoteDataBuildService, + protected store: Store, + protected bs: BrowseService, + protected halService: HALEndpointService) { + super(); + } + + public getScopedEndpoint(scopeID: string): Observable { + return this.halService.getEndpoint(this.linkPath); + } + +} diff --git a/src/app/shared/alerts/alerts.component.html b/src/app/shared/alerts/alerts.component.html new file mode 100644 index 0000000000..ad2cd82bfd --- /dev/null +++ b/src/app/shared/alerts/alerts.component.html @@ -0,0 +1,9 @@ + diff --git a/src/app/shared/alerts/alerts.component.scss b/src/app/shared/alerts/alerts.component.scss new file mode 100644 index 0000000000..1a70081367 --- /dev/null +++ b/src/app/shared/alerts/alerts.component.scss @@ -0,0 +1,3 @@ +.close:focus { + outline: none !important; +} diff --git a/src/app/shared/alerts/alerts.component.ts b/src/app/shared/alerts/alerts.component.ts new file mode 100644 index 0000000000..c9fc0ec9cc --- /dev/null +++ b/src/app/shared/alerts/alerts.component.ts @@ -0,0 +1,44 @@ +import { ChangeDetectorRef, Component, EventEmitter, Input, Output, ViewEncapsulation } from '@angular/core'; +import { trigger } from '@angular/animations'; + +import { AlertType } from './aletrs-type'; +import { fadeOutLeave, fadeOutState } from '../animations/fade'; + +@Component({ + selector: 'ds-alert', + encapsulation: ViewEncapsulation.None, + animations: [ + trigger('enterLeave', [ + fadeOutLeave, fadeOutState, + ]) + ], + templateUrl: './alerts.component.html', + styleUrls: ['./alerts.component.scss'] +}) + +export class AlertsComponent { + + @Input() content: string; + @Input() dismissible = false; + @Input() type: AlertType; + @Output() close: EventEmitter = new EventEmitter(); + + public animate = 'fadeIn'; + public dismissed = false; + + constructor(private cdr: ChangeDetectorRef) { + } + + dismiss() { + if (this.dismissible) { + this.animate = 'fadeOut'; + this.cdr.detectChanges(); + setTimeout(() => { + this.dismissed = true; + this.close.emit(); + this.cdr.detectChanges(); + }, 300); + + } + } +} diff --git a/src/app/shared/alerts/aletrs-type.ts b/src/app/shared/alerts/aletrs-type.ts new file mode 100644 index 0000000000..aacfb451f9 --- /dev/null +++ b/src/app/shared/alerts/aletrs-type.ts @@ -0,0 +1,6 @@ +export enum AlertType { + Success = 'alert-success', + Error = 'alert-danger', + Info = 'alert-info', + Warning = 'alert-warning' +} diff --git a/src/app/shared/empty.util.ts b/src/app/shared/empty.util.ts index c1498d11af..e479989393 100644 --- a/src/app/shared/empty.util.ts +++ b/src/app/shared/empty.util.ts @@ -93,6 +93,51 @@ export const hasValueOperator = () => (source: Observable): Observable => source.pipe(filter((obj: T) => hasValue(obj))); +/** + * Returns true if the passed value is null or undefined. + * hasUndefinedValue(); // false + * hasUndefinedValue(null); // false + * hasUndefinedValue(undefined); // false + * hasUndefinedValue(''); // true + * hasUndefinedValue({undefined, obj}); // true + * hasUndefinedValue([undefined, val]); // true + */ +export function hasUndefinedValue(obj?: any): boolean { + let result = false; + + if (isUndefined(obj) || isNull(obj)) { + return false; + } + + const objectType = typeof obj; + + if (objectType === 'object') { + if (Object.keys(obj).length === 0) { + return false; + } + Object.entries(obj).forEach(([key, value]) => { + if (isUndefined(value)) { + result = true + } + }) + } + + return result; +} + +/** + * Returns true if the passed value is null or undefined. + * hasUndefinedValue(); // true + * hasUndefinedValue(null); // true + * hasUndefinedValue(undefined); // true + * hasUndefinedValue(''); // false + * hasUndefinedValue({undefined, obj}); // false + * hasUndefinedValue([undefined, val]); // false + */ +export function hasNoUndefinedValue(obj?: any): boolean { + return !hasUndefinedValue(obj); +} + /** * Verifies that a value is `null` or an empty string, empty array, * or empty function. diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/dynamic-group/dynamic-group.components.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/dynamic-group/dynamic-group.components.ts index a55e7aff9d..a954f58e99 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/dynamic-group/dynamic-group.components.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/dynamic-group/dynamic-group.components.ts @@ -89,7 +89,8 @@ export class DsDynamicGroupComponent implements OnDestroy, OnInit { this.chips = new Chips( initChipsValue, 'value', - this.model.mandatoryField); + this.model.mandatoryField, + this.EnvConfig.submission.metadata.icons); this.subs.push( this.chips.chipsItems .subscribe((subItems: any[]) => { diff --git a/src/app/shared/form/builder/parsers/row-parser.ts b/src/app/shared/form/builder/parsers/row-parser.ts index f315be451e..577845fab8 100644 --- a/src/app/shared/form/builder/parsers/row-parser.ts +++ b/src/app/shared/form/builder/parsers/row-parser.ts @@ -10,6 +10,7 @@ import { FormFieldModel } from '../models/form-field.model'; import { ParserType } from './parser-type'; import { ParserOptions } from './parser-options'; import { ParserFactory } from './parser-factory'; +import { TranslateService } from '@ngx-translate/core'; export const ROW_ID_PREFIX = 'df-row-group-config-'; @@ -49,7 +50,7 @@ export class RowParser { if (parserCo) { fieldModel = new parserCo(fieldData, this.initFormValues, parserOptions).parse(); } else { - throw new Error(`unknown form control model type defined with label "${fieldData.label}"`); + throw new Error(`unknown form control model type "${fieldData.input.type}" defined for Input field with label "${fieldData.label}".`, ); } if (fieldModel) { diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index 57ba7dec4d..9c23440dbf 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -55,8 +55,6 @@ import { DsDynamicFormComponent } from './form/builder/ds-dynamic-form-ui/ds-dyn import { DynamicFormsCoreModule } from '@ng-dynamic-forms/core'; import { DynamicFormsNGBootstrapUIModule } from '@ng-dynamic-forms/ui-ng-bootstrap'; import { TextMaskModule } from 'angular2-text-mask'; -import { NotificationComponent } from './notifications/notification/notification.component'; -import { NotificationsBoardComponent } from './notifications/notifications-board/notifications-board.component'; import { DragClickDirective } from './utils/drag-click.directive'; import { TruncatePipe } from './utils/truncate.pipe'; import { TruncatableComponent } from './truncatable/truncatable.component'; @@ -72,6 +70,7 @@ import { NumberPickerComponent } from './number-picker/number-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 { MockAdminGuard } from './mocks/mock-admin-guard.service'; +import { AlertsComponent } from './alerts/alerts.component'; const MODULES = [ // Do NOT include UniversalModule, HttpModule, or JsonpModule here @@ -104,6 +103,7 @@ const PIPES = [ const COMPONENTS = [ // put shared components here + AlertsComponent, AuthNavMenuComponent, ChipsComponent, ComcolPageContentComponent, diff --git a/src/app/shared/truncatable/truncatable-part/truncatable-part.component.ts b/src/app/shared/truncatable/truncatable-part/truncatable-part.component.ts index 0f695625ec..61fc031b56 100644 --- a/src/app/shared/truncatable/truncatable-part/truncatable-part.component.ts +++ b/src/app/shared/truncatable/truncatable-part/truncatable-part.component.ts @@ -1,5 +1,6 @@ import { Component, Input, OnDestroy, OnInit } from '@angular/core'; import { TruncatableService } from '../truncatable.service'; +import { hasValue } from '../../empty.util'; @Component({ selector: 'ds-truncatable-part', @@ -34,6 +35,8 @@ export class TruncatablePartComponent implements OnInit, OnDestroy { } ngOnDestroy(): void { - this.sub.unsubscribe(); + if (hasValue(this.sub)) { + this.sub.unsubscribe(); + } } } diff --git a/src/app/shared/uploader/uploader.component.scss b/src/app/shared/uploader/uploader.component.scss index 7e0f6fdd23..dd80a736ca 100644 --- a/src/app/shared/uploader/uploader.component.scss +++ b/src/app/shared/uploader/uploader.component.scss @@ -10,7 +10,7 @@ } .ds-base-drop-zone p { - height: 42px; + min-height: 42px; } .ds-document-drop-zone { diff --git a/src/app/submission/edit/submission-edit.component.html b/src/app/submission/edit/submission-edit.component.html new file mode 100644 index 0000000000..21b20997cf --- /dev/null +++ b/src/app/submission/edit/submission-edit.component.html @@ -0,0 +1,7 @@ +
+ +
diff --git a/src/app/submission/edit/submission-edit.component.scss b/src/app/submission/edit/submission-edit.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/submission/edit/submission-edit.component.ts b/src/app/submission/edit/submission-edit.component.ts new file mode 100644 index 0000000000..c4a9f52d33 --- /dev/null +++ b/src/app/submission/edit/submission-edit.component.ts @@ -0,0 +1,74 @@ +import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; +import { ActivatedRoute, ParamMap, Router } from '@angular/router'; + +import { Subscription } from 'rxjs/Subscription'; + +import { WorkspaceitemSectionsObject } from '../../core/submission/models/workspaceitem-sections.model'; +import { hasValue, isEmpty, isNotNull } from '../../shared/empty.util'; +import { SubmissionDefinitionsModel } from '../../core/shared/config/config-submission-definitions.model'; +import { SubmissionService } from '../submission.service'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { TranslateService } from '@ngx-translate/core'; +import { SubmissionObject } from '../../core/submission/models/submission-object.model'; + +@Component({ + selector: 'ds-submission-edit', + styleUrls: ['./submission-edit.component.scss'], + templateUrl: './submission-edit.component.html' +}) + +export class SubmissionEditComponent implements OnDestroy, OnInit { + public collectionId: string; + public sections: WorkspaceitemSectionsObject; + public selfUrl: string; + public submissionDefinition: SubmissionDefinitionsModel; + public submissionId: string; + + /** + * Array to track all subscriptions and unsubscribe them onDestroy + * @type {Array} + */ + private subs: Subscription[] = []; + + constructor(private changeDetectorRef: ChangeDetectorRef, + private notificationsService: NotificationsService, + private route: ActivatedRoute, + private router: Router, + private submissionService: SubmissionService, + private translate: TranslateService) { + } + + ngOnInit() { + this.subs.push(this.route.paramMap + .subscribe((params: ParamMap) => { + this.submissionId = params.get('id'); + this.subs.push( + this.submissionService.retrieveSubmission(this.submissionId) + .subscribe((submissionObject: SubmissionObject) => { + // NOTE new submission is retrieved on the browser side only + if (isNotNull(submissionObject)) { + if (isEmpty(submissionObject)) { + this.notificationsService.info(null, this.translate.get('submission.general.cannot_submit')); + this.router.navigate(['/mydspace']); + } else { + this.collectionId = submissionObject.collection[0].id; + this.selfUrl = submissionObject.self; + this.sections = submissionObject.sections; + this.submissionDefinition = submissionObject.submissionDefinition[0]; + this.changeDetectorRef.detectChanges(); + } + } + }) + ) + })); + } + + /** + * Method provided by Angular. Invoked when the instance is destroyed. + */ + ngOnDestroy() { + this.subs + .filter((sub) => hasValue(sub)) + .forEach((sub) => sub.unsubscribe()); + } +} diff --git a/src/app/submission/form/collection/submission-form-collection.component.html b/src/app/submission/form/collection/submission-form-collection.component.html new file mode 100644 index 0000000000..ebfd81233e --- /dev/null +++ b/src/app/submission/form/collection/submission-form-collection.component.html @@ -0,0 +1,37 @@ +
+
+
+ Collection +
+ + + +
+
diff --git a/src/app/submission/form/collection/submission-form-collection.component.scss b/src/app/submission/form/collection/submission-form-collection.component.scss new file mode 100644 index 0000000000..b72d04d3c9 --- /dev/null +++ b/src/app/submission/form/collection/submission-form-collection.component.scss @@ -0,0 +1,17 @@ +@import '../../../../styles/variables'; + +.scrollable-menu { + height: auto; + max-height: 200px; + overflow-x: hidden; +} + +.collection-item { + border-bottom: $dropdown-border-width solid $dropdown-border-color; +} + +#collectionControlsDropdownMenu { + outline: 0; + left: 0 !important; + box-shadow: $btn-focus-box-shadow; +} diff --git a/src/app/submission/form/collection/submission-form-collection.component.ts b/src/app/submission/form/collection/submission-form-collection.component.ts new file mode 100644 index 0000000000..efcebeebd8 --- /dev/null +++ b/src/app/submission/form/collection/submission-form-collection.component.ts @@ -0,0 +1,168 @@ +import { + ChangeDetectorRef, + Component, + EventEmitter, + HostListener, + Input, + OnChanges, + OnInit, + Output, + SimpleChanges +} from '@angular/core'; +import { FormControl } from '@angular/forms'; + +import { Store } from '@ngrx/store'; +import { Subscription } from 'rxjs/Subscription'; + +import { isNullOrUndefined } from 'util'; +import { Collection } from '../../../core/shared/collection.model'; +import { CommunityDataService } from '../../../core/data/community-data.service'; +import { Community } from '../../../core/shared/community.model'; +import { hasValue, isNotEmpty } from '../../../shared/empty.util'; +import { RemoteData } from '../../../core/data/remote-data'; +import { JsonPatchOperationPathCombiner } from '../../../core/json-patch/builder/json-patch-operation-path-combiner'; +import { JsonPatchOperationsBuilder } from '../../../core/json-patch/builder/json-patch-operations-builder'; +import { Workspaceitem } from '../../../core/submission/models/workspaceitem.model'; +import { PaginatedList } from '../../../core/data/paginated-list'; +import { JsonPatchOperationsService } from '../../../core/json-patch/json-patch-operations.service'; +import { SubmitDataResponseDefinitionObject } from '../../../core/shared/submit-data-response-definition.model'; +import { SubmissionService } from '../../submission.service'; +import { SubmissionState } from '../../submission.reducers'; +import { ChangeSubmissionCollectionAction } from '../../objects/submission-objects.actions'; +import { SubmissionObject } from '../../../core/submission/models/submission-object.model'; + +@Component({ + selector: 'ds-submission-form-collection', + styleUrls: ['./submission-form-collection.component.scss'], + templateUrl: './submission-form-collection.component.html' +}) +export class SubmissionFormCollectionComponent implements OnChanges, OnInit { + @Input() currentCollectionId: string; + @Input() currentDefinition: string; + @Input() submissionId; + + /** + * An event fired when a different collection is selected. + * Event's payload equals to new collection uuid. + */ + @Output() collectionChange: EventEmitter = new EventEmitter(); + + public disabled = true; + public listCollection = []; + public model: any; + public searchField: FormControl; + public searchListCollection = []; + public selectedCollectionId: string; + public selectedCollectionName: string; + + protected pathCombiner: JsonPatchOperationPathCombiner; + private scrollableBottom = false; + private scrollableTop = false; + private subs: Subscription[] = []; + + formatter = (x: { collection: string }) => x.collection; + + constructor(protected cdr: ChangeDetectorRef, + private communityDataService: CommunityDataService, + private operationsBuilder: JsonPatchOperationsBuilder, + private operationsService: JsonPatchOperationsService, + private store: Store, + private submissionService: SubmissionService) { + } + + @HostListener('mousewheel', ['$event']) onMousewheel(event) { + if (event.wheelDelta > 0 && this.scrollableTop) { + event.preventDefault(); + } + if (event.wheelDelta < 0 && this.scrollableBottom) { + event.preventDefault(); + } + } + + onScroll(event) { + this.scrollableBottom = (event.target.scrollTop + event.target.clientHeight === event.target.scrollHeight); + this.scrollableTop = (event.target.scrollTop === 0); + } + + ngOnChanges(changes: SimpleChanges) { + if (hasValue(changes.currentCollectionId) + && hasValue(changes.currentCollectionId.currentValue) + && !isNotEmpty(this.listCollection)) { + this.selectedCollectionId = this.currentCollectionId; + // @TODO replace with search/top browse endpoint + // @TODO implement community/subcommunity hierarchy + this.subs.push(this.communityDataService.findAll() + .filter((communities: RemoteData>) => isNotEmpty(communities.payload)) + .first() + .switchMap((communities: RemoteData>) => communities.payload.page) + .subscribe((communityData: Community) => { + + this.subs.push(communityData.collections + .filter((collections: RemoteData) => !collections.isResponsePending && collections.hasSucceeded) + .first() + .switchMap((collections: RemoteData) => collections.payload) + .filter((collectionData: Collection) => isNotEmpty(collectionData)) + .subscribe((collectionData: Collection) => { + if (collectionData.id === this.selectedCollectionId) { + this.selectedCollectionName = collectionData.name; + } + const collectionEntry = { + communities: [{id: communityData.id, name: communityData.name}], + collection: {id: collectionData.id, name: collectionData.name} + }; + this.listCollection.push(collectionEntry); + this.searchListCollection.push(collectionEntry); + this.disabled = false; + this.cdr.detectChanges(); + })) + })); + } + } + + ngOnInit() { + this.pathCombiner = new JsonPatchOperationPathCombiner('sections', 'collection'); + this.searchField = new FormControl(); + this.searchField.valueChanges + .debounceTime(200) + .distinctUntilChanged() + .subscribe((term) => { + this.search(term); + }); + } + + ngOnDestroy(): void { + this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe()); + } + + search(text: string) { + if (text === '' || isNullOrUndefined(text)) { + this.searchListCollection = this.listCollection; + } else { + this.searchListCollection = this.listCollection.filter((v) => v.collection.name.toLowerCase().indexOf(text.toLowerCase()) > -1).slice(0, 5); + } + } + + onSelect(event) { + this.searchField.reset(); + this.searchListCollection = this.listCollection; + this.disabled = true; + this.operationsBuilder.replace(this.pathCombiner.getPath(), event.collection.id, true); + this.operationsService.jsonPatchByResourceID( + this.submissionService.getSubmissionObjectLinkName(), + this.submissionId, + 'sections', + 'collection') + .subscribe((submissionObject: SubmissionObject[]) => { + this.selectedCollectionId = event.collection.id; + this.selectedCollectionName = event.collection.name; + this.collectionChange.emit(submissionObject[0]); + this.store.dispatch(new ChangeSubmissionCollectionAction(this.submissionId, event.collection.id)); + this.disabled = false; + this.cdr.detectChanges(); + }) + } + + onClose(event) { + this.searchField.reset(); + } +} diff --git a/src/app/submission/form/footer/submission-form-footer.component.html b/src/app/submission/form/footer/submission-form-footer.component.html new file mode 100644 index 0000000000..58017b320b --- /dev/null +++ b/src/app/submission/form/footer/submission-form-footer.component.html @@ -0,0 +1,52 @@ +
+
+ +
+
+ +
+
+
+
Saving...
+
Depositing...
+
+
+
+ + + +
+
+ + + + + + diff --git a/src/app/submission/form/footer/submission-form-footer.component.scss b/src/app/submission/form/footer/submission-form-footer.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/submission/form/footer/submission-form-footer.component.ts b/src/app/submission/form/footer/submission-form-footer.component.ts new file mode 100644 index 0000000000..86d4a502c5 --- /dev/null +++ b/src/app/submission/form/footer/submission-form-footer.component.ts @@ -0,0 +1,70 @@ +import { Component, Input, OnChanges, SimpleChanges } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { SubmissionRestService } from '../../submission-rest.service'; +import { SubmissionService } from '../../submission.service'; +import { SubmissionState } from '../../submission.reducers'; +import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; +import { + DepositSubmissionAction, DiscardSubmissionAction, + SaveAndDepositSubmissionAction, + SaveForLaterSubmissionFormAction, + SaveSubmissionFormAction +} from '../../objects/submission-objects.actions'; +import { Observable } from 'rxjs/Observable'; +import { SubmissionScopeType } from '../../../core/submission/submission-scope-type'; + +@Component({ + selector: 'ds-submission-form-footer', + styleUrls: ['./submission-form-footer.component.scss'], + templateUrl: './submission-form-footer.component.html' +}) +export class SubmissionFormFooterComponent implements OnChanges { + + @Input() submissionId; + + public processingDepositStatus: Observable; + public processingSaveStatus: Observable; + public showDepositAndDiscard: Observable; + private submissionIsInvalid = true; + + constructor(private modalService: NgbModal, + private restService: SubmissionRestService, + private submissionService: SubmissionService, + private store: Store) { + } + + ngOnChanges(changes: SimpleChanges) { + if (!!this.submissionId) { + this.submissionService.getSectionsState(this.submissionId) + .subscribe((isValid) => { + this.submissionIsInvalid = isValid === false; + }); + + this.processingSaveStatus = this.submissionService.getSubmissionSaveProcessingStatus(this.submissionId); + this.processingDepositStatus = this.submissionService.getSubmissionDepositProcessingStatus(this.submissionId); + this.showDepositAndDiscard = Observable.of(this.submissionService.getSubmissionScope() === SubmissionScopeType.WorkspaceItem); + } + } + + save(event) { + this.store.dispatch(new SaveSubmissionFormAction(this.submissionId)); + } + + saveLater(event) { + this.store.dispatch(new SaveForLaterSubmissionFormAction(this.submissionId)); + } + + public deposit(event) { + this.store.dispatch(new SaveAndDepositSubmissionAction(this.submissionId)); + } + + public confirmDiscard(content) { + this.modalService.open(content).result.then( + (result) => { + if (result === 'ok') { + this.store.dispatch(new DiscardSubmissionAction(this.submissionId)); + } + } + ); + } +} diff --git a/src/app/submission/form/section-add/submission-form-section-add.component.html b/src/app/submission/form/section-add/submission-form-section-add.component.html new file mode 100644 index 0000000000..4559abebba --- /dev/null +++ b/src/app/submission/form/section-add/submission-form-section-add.component.html @@ -0,0 +1,14 @@ +
+ +
+ + +
+
diff --git a/src/app/submission/form/section-add/submission-form-section-add.component.scss b/src/app/submission/form/section-add/submission-form-section-add.component.scss new file mode 100644 index 0000000000..628f0f5633 --- /dev/null +++ b/src/app/submission/form/section-add/submission-form-section-add.component.scss @@ -0,0 +1,9 @@ +@import '../../../../styles/variables'; + +.dropdown-toggle::after { + display:none +} + +.sections-dropdown-menu { + z-index: $submission-header-z-index; +} diff --git a/src/app/submission/form/section-add/submission-form-section-add.component.ts b/src/app/submission/form/section-add/submission-form-section-add.component.ts new file mode 100644 index 0000000000..105059dfc1 --- /dev/null +++ b/src/app/submission/form/section-add/submission-form-section-add.component.ts @@ -0,0 +1,33 @@ +import { Component, Input, OnInit, } from '@angular/core'; + +import { Observable } from 'rxjs/Observable'; + +import { SectionsService } from '../../sections/sections.service'; +import { HostWindowService } from '../../../shared/host-window.service'; +import { SubmissionService } from '../../submission.service'; +import { SectionDataObject } from '../../sections/models/section-data.model'; + +@Component({ + selector: 'ds-submission-form-section-add', + styleUrls: [ './submission-form-section-add.component.scss' ], + templateUrl: './submission-form-section-add.component.html' +}) +export class SubmissionFormSectionAddComponent implements OnInit { + @Input() collectionId: string; + @Input() submissionId: string; + + public sectionList: Observable; + + constructor(private sectionService: SectionsService, + private submissionService: SubmissionService, + public windowService: HostWindowService) { + } + + ngOnInit() { + this.sectionList = this.submissionService.getDisabledSectionsList(this.submissionId); + } + + addSection(sectionId) { + this.sectionService.addSection(this.submissionId, sectionId); + } +} diff --git a/src/app/submission/form/submission-form.component.html b/src/app/submission/form/submission-form.component.html new file mode 100644 index 0000000000..b17ca49bd5 --- /dev/null +++ b/src/app/submission/form/submission-form.component.html @@ -0,0 +1,36 @@ +
+
+ +
+
+
+
+ + +
+
+ + +
+
+ +
+ + + + +
+ +
diff --git a/src/app/submission/form/submission-form.component.scss b/src/app/submission/form/submission-form.component.scss new file mode 100644 index 0000000000..c9b6872146 --- /dev/null +++ b/src/app/submission/form/submission-form.component.scss @@ -0,0 +1,21 @@ +@import '../../../styles/variables'; + +.submission-form-header { + background-color: rgba($white, .97); + padding: ($spacer / 2) 0 ($spacer / 2) 0; + top: 0; + z-index: $submission-header-z-index; +} + +.submission-form-header-item { + flex-grow: 1; +} + +.submission-form-footer { + border-radius: $card-border-radius; + bottom: 0; + background-color: $gray-400; + padding: $spacer / 2; + z-index: $submission-footer-z-index; +} + diff --git a/src/app/submission/form/submission-form.component.ts b/src/app/submission/form/submission-form.component.ts new file mode 100644 index 0000000000..0bed9173c1 --- /dev/null +++ b/src/app/submission/form/submission-form.component.ts @@ -0,0 +1,139 @@ +import { ChangeDetectorRef, Component, Input, OnChanges, OnDestroy, SimpleChanges } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { + CancelSubmissionFormAction, + LoadSubmissionFormAction, + ResetSubmissionFormAction +} from '../objects/submission-objects.actions'; +import { hasValue, isNotEmpty, isNotUndefined } from '../../shared/empty.util'; +import { submissionObjectFromIdSelector } from '../selectors'; +import { SubmissionObjectEntry } from '../objects/submission-objects.reducer'; +import { WorkspaceitemSectionsObject } from '../../core/submission/models/workspaceitem-sections.model'; +import { SubmissionDefinitionsModel } from '../../core/shared/config/config-submission-definitions.model'; +import { SubmissionState } from '../submission.reducers'; +import { Workspaceitem } from '../../core/submission/models/workspaceitem.model'; +import { SubmissionService } from '../submission.service'; +import { Subscription } from 'rxjs/Subscription'; +import { AuthService } from '../../core/auth/auth.service'; +import { Observable } from 'rxjs/Observable'; +import { SectionDataObject } from '../sections/models/section-data.model'; +import { UploaderOptions } from '../../shared/uploader/uploader-options.model'; +import { HALEndpointService } from '../../core/shared/hal-endpoint.service'; + +@Component({ + selector: 'ds-submission-submit-form', + styleUrls: ['./submission-form.component.scss'], + templateUrl: './submission-form.component.html', +}) +export class SubmissionFormComponent implements OnChanges, OnDestroy { + @Input() collectionId: string; + @Input() sections: WorkspaceitemSectionsObject; + @Input() selfUrl: string; + @Input() submissionDefinition: SubmissionDefinitionsModel; + @Input() submissionId: string; + + public definitionId: string; + public test = true; + public loading: Observable = Observable.of(true); + public submissionSections: Observable; + public uploadFilesOptions: UploaderOptions = { + url: '', + authToken: null, + disableMultipart: false, + itemAlias: null + }; + + protected isActive: boolean; + protected subs: Subscription[] = []; + + constructor( + private authService: AuthService, + private changeDetectorRef: ChangeDetectorRef, + private halService: HALEndpointService, + private store: Store, + private submissionService: SubmissionService) { + this.isActive = true; + } + + ngOnChanges(changes: SimpleChanges) { + if (this.collectionId && this.submissionId) { + this.isActive = true; + this.submissionSections = this.store.select(submissionObjectFromIdSelector(this.submissionId)) + .filter((submission: SubmissionObjectEntry) => isNotUndefined(submission) && this.isActive) + .map((submission: SubmissionObjectEntry) => submission.isLoading) + .map((isLoading: boolean) => isLoading) + .distinctUntilChanged() + .flatMap((isLoading: boolean) => { + if (!isLoading) { + return this.getSectionsList(); + } else { + return Observable.of([]) + } + }); + + this.loading = this.store.select(submissionObjectFromIdSelector(this.submissionId)) + .filter((submission: SubmissionObjectEntry) => isNotUndefined(submission) && this.isActive) + .map((submission: SubmissionObjectEntry) => submission.isLoading) + .map((isLoading: boolean) => isLoading) + .distinctUntilChanged(); + + this.subs.push( + this.halService.getEndpoint('workspaceitems') + .filter((href: string) => isNotEmpty(href)) + .distinctUntilChanged() + .subscribe((endpointURL) => { + this.uploadFilesOptions.authToken = this.authService.buildAuthHeader(); + this.uploadFilesOptions.url = endpointURL.concat(`/${this.submissionId}`); + this.definitionId = this.submissionDefinition.name; + this.store.dispatch(new LoadSubmissionFormAction(this.collectionId, this.submissionId, this.selfUrl, this.submissionDefinition, this.sections, null)); + this.changeDetectorRef.detectChanges(); + }), + + // this.store.select(submissionObjectFromIdSelector(this.submissionId)) + // .filter((submission: SubmissionObjectEntry) => isNotUndefined(submission) && this.isActive) + // .subscribe((submission: SubmissionObjectEntry) => { + // if (this.loading !== submission.isLoading) { + // this.loading = submission.isLoading; + // this.changeDetectorRef.detectChanges(); + // } + // }) + ); + this.submissionService.startAutoSave(this.submissionId); + } + } + + ngOnDestroy() { + this.isActive = false; + this.submissionService.stopAutoSave(); + this.store.dispatch(new CancelSubmissionFormAction()); + this.subs + .filter((subscription) => hasValue(subscription)) + .forEach((subscription) => subscription.unsubscribe()); + } + + onCollectionChange(workspaceItemObject: Workspaceitem) { + this.collectionId = workspaceItemObject.collection[0].id; + if (this.definitionId !== workspaceItemObject.submissionDefinition[0].name) { + this.sections = workspaceItemObject.sections; + this.submissionDefinition = workspaceItemObject.submissionDefinition[0]; + this.definitionId = this.submissionDefinition.name; + this.store.dispatch(new ResetSubmissionFormAction(this.collectionId, this.submissionId, workspaceItemObject.self, this.sections, this.submissionDefinition)); + // this.submissionSections = this.getSectionsList(); + } else { + this.changeDetectorRef.detectChanges(); + } + } + + isLoading(): Observable { + // return isUndefined(this.loading) || this.loading === true; + return this.loading; + } + + protected getSectionsList(): Observable { + return this.submissionService.getSubmissionSections(this.submissionId) + .filter((sections: SectionDataObject[]) => isNotEmpty(sections)) + .map((sections: SectionDataObject[]) => { + return sections; + }); + } +} diff --git a/src/app/submission/form/submission-upload-files/submission-upload-files.component.html b/src/app/submission/form/submission-upload-files/submission-upload-files.component.html new file mode 100644 index 0000000000..cbd7dd3c9e --- /dev/null +++ b/src/app/submission/form/submission-upload-files/submission-upload-files.component.html @@ -0,0 +1,7 @@ + diff --git a/src/app/submission/form/submission-upload-files/submission-upload-files.component.ts b/src/app/submission/form/submission-upload-files/submission-upload-files.component.ts new file mode 100644 index 0000000000..7944dc05f6 --- /dev/null +++ b/src/app/submission/form/submission-upload-files/submission-upload-files.component.ts @@ -0,0 +1,96 @@ +import { Component, Input, OnChanges } from '@angular/core'; + +import { TranslateService } from '@ngx-translate/core'; +import { Observable } from 'rxjs/Observable'; + +import { SectionsService } from '../../sections/sections.service'; +import { hasValue, isEmpty, isNotEmpty } from '../../../shared/empty.util'; +import { Workspaceitem } from '../../../core/submission/models/workspaceitem.model'; +import { normalizeSectionData } from '../../../core/submission/models/workspaceitem-sections.model'; +import { JsonPatchOperationsService } from '../../../core/json-patch/json-patch-operations.service'; +import { SubmitDataResponseDefinitionObject } from '../../../core/shared/submit-data-response-definition.model'; +import { SubmissionService } from '../../submission.service'; +import { NotificationsService } from '../../../shared/notifications/notifications.service'; +import { UploaderOptions } from '../../../shared/uploader/uploader-options.model'; +import parseSectionErrors from '../../utils/parseSectionErrors'; + +@Component({ + selector: 'ds-submission-upload-files', + templateUrl: './submission-upload-files.component.html', +}) +export class SubmissionUploadFilesComponent implements OnChanges { + + @Input() collectionId; + @Input() submissionId; + @Input() sectionId; + @Input() uploadFilesOptions: UploaderOptions; + + public enableDragOverDocument = true; + public dropOverDocumentMsg = 'submission.sections.upload.drop-message'; + public dropMsg = 'submission.sections.upload.drop-message'; + + private subs = []; + private uploadEnabled: Observable = Observable.of(false); + + public onBeforeUpload = () => { + this.operationsService.jsonPatchByResourceType( + this.submissionService.getSubmissionObjectLinkName(), + this.submissionId, + 'sections') + .subscribe(); + }; + + constructor(private notificationsService: NotificationsService, + private operationsService: JsonPatchOperationsService, + private sectionService: SectionsService, + private submissionService: SubmissionService, + private translate: TranslateService) { + } + + ngOnChanges() { + this.uploadEnabled = this.sectionService.isSectionAvailable(this.submissionId, this.sectionId); + } + + public onCompleteItem(workspaceitem: Workspaceitem) { + // Checks if upload section is enabled so do upload + this.subs.push( + this.uploadEnabled + .first() + .subscribe((isUploadEnabled) => { + if (isUploadEnabled) { + + const {sections} = workspaceitem; + const {errors} = workspaceitem; + + const errorsList = parseSectionErrors(errors); + if (sections && isNotEmpty(sections)) { + Object.keys(sections) + .forEach((sectionId) => { + const sectionData = normalizeSectionData(sections[sectionId]); + const sectionErrors = errorsList[sectionId]; + if (sectionId === 'upload') { + // Look for errors on upload + if ((isEmpty(sectionErrors))) { + this.notificationsService.success(null, this.translate.get('submission.sections.upload.upload-successful')); + } else { + this.notificationsService.error(null, this.translate.get('submission.sections.upload.upload-failed')); + } + } + this.sectionService.updateSectionData(this.submissionId, sectionId, sectionData, sectionErrors) + }) + } + + } + }) + ); + } + + /** + * Method provided by Angular. Invoked when the instance is destroyed. + */ + ngOnDestroy() { + this.subs + .filter((subscription) => hasValue(subscription)) + .forEach((subscription) => subscription.unsubscribe()); + } +} diff --git a/src/app/submission/objects/submission-objects.actions.ts b/src/app/submission/objects/submission-objects.actions.ts new file mode 100644 index 0000000000..fba526dece --- /dev/null +++ b/src/app/submission/objects/submission-objects.actions.ts @@ -0,0 +1,1032 @@ +import { Action } from '@ngrx/store'; + +import { type } from '../../shared/ngrx/type'; +import { SectionVisibility, SubmissionSectionError } from './submission-objects.reducer'; +import { WorkspaceitemSectionUploadFileObject } from '../../core/submission/models/workspaceitem-section-upload-file.model'; +import { WorkspaceitemSectionFormObject } from '../../core/submission/models/workspaceitem-section-form.model'; +import { WorkspaceitemSectionLicenseObject } from '../../core/submission/models/workspaceitem-section-license.model'; +import { + WorkspaceitemSectionDataType, + WorkspaceitemSectionsObject +} from '../../core/submission/models/workspaceitem-sections.model'; +import { WorkspaceitemSectionUploadObject } from '../../core/submission/models/workspaceitem-section-upload.model'; +import { SubmissionObject } from '../../core/submission/models/submission-object.model'; +import { SubmissionDefinitionsModel } from '../../core/shared/config/config-submission-definitions.model'; +import { SectionsType } from '../sections/sections-type'; + +/** + * For each action type in an action group, make a simple + * enum object for all of this group's action types. + * + * The 'type' utility function coerces strings into string + * literal types and runs a simple check to guarantee all + * action types in the application are unique. + */ +export const SubmissionObjectActionTypes = { + // Section types + LOAD_SUBMISSION_FORM: type('dspace/submission/LOAD_SUBMISSION_FORM'), + RESET_SUBMISSION_FORM: type('dspace/submission/RESET_SUBMISSION_FORM'), + CANCEL_SUBMISSION_FORM: type('dspace/submission/CANCEL_SUBMISSION_FORM'), + INIT_SUBMISSION_FORM: type('dspace/submission/INIT_SUBMISSION_FORM'), + COMPLETE_INIT_SUBMISSION_FORM: type('dspace/submission/COMPLETE_INIT_SUBMISSION_FORM'), + SAVE_FOR_LATER_SUBMISSION_FORM: type('dspace/submission/SAVE_FOR_LATER_SUBMISSION_FORM'), + SAVE_FOR_LATER_SUBMISSION_FORM_SUCCESS: type('dspace/submission/SAVE_FOR_LATER_SUBMISSION_FORM_SUCCESS'), + SAVE_FOR_LATER_SUBMISSION_FORM_ERROR: type('dspace/submission/SAVE_FOR_LATER_SUBMISSION_FORM_ERROR'), + SAVE_SUBMISSION_FORM: type('dspace/submission/SAVE_SUBMISSION_FORM'), + SAVE_SUBMISSION_FORM_SUCCESS: type('dspace/submission/SAVE_SUBMISSION_FORM_SUCCESS'), + SAVE_SUBMISSION_FORM_ERROR: type('dspace/submission/SAVE_SUBMISSION_FORM_ERROR'), + SAVE_SUBMISSION_SECTION_FORM: type('dspace/submission/SAVE_SUBMISSION_SECTION_FORM'), + SAVE_SUBMISSION_SECTION_FORM_SUCCESS: type('dspace/submission/SAVE_SUBMISSION_SECTION_FORM_SUCCESS'), + SAVE_SUBMISSION_SECTION_FORM_ERROR: type('dspace/submission/SAVE_SUBMISSION_SECTION_FORM_ERROR'), + COMPLETE_SAVE_SUBMISSION_FORM: type('dspace/submission/COMPLETE_SAVE_SUBMISSION_FORM'), + CHANGE_SUBMISSION_COLLECTION: type('dspace/submission/CHANGE_SUBMISSION_COLLECTION'), + SET_ACTIVE_SECTION: type('dspace/submission/SET_ACTIVE_SECTION'), + INIT_SECTION: type('dspace/submission/INIT_SECTION'), + ENABLE_SECTION: type('dspace/submission/ENABLE_SECTION'), + DISABLE_SECTION: type('dspace/submission/DISABLE_SECTION'), + SECTION_STATUS_CHANGE: type('dspace/submission/SECTION_STATUS_CHANGE'), + SECTION_LOADING_STATUS_CHANGE: type('dspace/submission/SECTION_LOADING_STATUS_CHANGE'), + UPLOAD_SECTION_DATA: type('dspace/submission/UPLOAD_SECTION_DATA'), + REMOVE_SECTION_ERRORS: type('dspace/submission/REMOVE_SECTION_ERRORS'), + SAVE_AND_DEPOSIT_SUBMISSION: type('dspace/submission/SAVE_AND_DEPOSIT_SUBMISSION'), + DEPOSIT_SUBMISSION: type('dspace/submission/DEPOSIT_SUBMISSION'), + DEPOSIT_SUBMISSION_SUCCESS: type('dspace/submission/DEPOSIT_SUBMISSION_SUCCESS'), + DEPOSIT_SUBMISSION_ERROR: type('dspace/submission/DEPOSIT_SUBMISSION_ERROR'), + DISCARD_SUBMISSION: type('dspace/submission/DISCARD_SUBMISSION'), + DISCARD_SUBMISSION_SUCCESS: type('dspace/submission/DISCARD_SUBMISSION_SUCCESS'), + DISCARD_SUBMISSION_ERROR: type('dspace/submission/DISCARD_SUBMISSION_ERROR'), + SET_WORKSPACE_DUPLICATION: type('/sections/deduplication/SET_WORKSPACE_DUPLICATION'), + SET_WORKSPACE_DUPLICATION_SUCCESS: type('/sections/deduplication/SET_WORKSPACE_DUPLICATION_SUCCESS'), + SET_WORKSPACE_DUPLICATION_ERROR: type('/sections/deduplication/SET_WORKSPACE_DUPLICATION_ERROR'), + SET_WORKFLOW_DUPLICATION: type('/sections/deduplication/SET_WORKFLOW_DUPLICATION'), + SET_WORKFLOW_DUPLICATION_SUCCESS: type('/sections/deduplication/SET_WORKFLOW_DUPLICATION_SUCCESS'), + SET_WORKFLOW_DUPLICATION_ERROR: type('/sections/deduplication/SET_WORKFLOW_DUPLICATION_ERROR'), + + // Upload file types + NEW_FILE: type('dspace/submission/NEW_FILE'), + EDIT_FILE_DATA: type('dspace/submission/EDIT_FILE_DATA'), + DELETE_FILE: type('dspace/submission/DELETE_FILE'), + + // Errors + INSERT_ERRORS: type('dspace/submission/INSERT_ERRORS'), + DELETE_ERRORS: type('dspace/submission/DELETE_ERRORS'), + CLEAR_ERRORS: type('dspace/submission/CLEAR_ERRORS'), +}; + +/* tslint:disable:max-classes-per-file */ + +/** + * Insert a new error of type SubmissionSectionError into the given section + * @param {string} submissionId + * @param {string} sectionId + * @param {SubmissionSectionError} error + */ +export class InertSectionErrorsAction implements Action { + type: string = SubmissionObjectActionTypes.INSERT_ERRORS; + payload: { + submissionId: string; + sectionId: string; + error: SubmissionSectionError | SubmissionSectionError[]; + }; + + constructor(submissionId: string, sectionId: string, error: SubmissionSectionError | SubmissionSectionError[]) { + this.payload = { submissionId, sectionId, error }; + } +} + +/** + * Delete a SubmissionSectionError from the given section + * @param {string} submissionId + * @param {string} sectionId + * @param {string | SubmissionSectionError} error + */ +export class DeleteSectionErrorsAction implements Action { + type: string = SubmissionObjectActionTypes.DELETE_ERRORS; + payload: { + submissionId: string; + sectionId: string; + error: string | SubmissionSectionError | SubmissionSectionError[]; + }; + + constructor(submissionId: string, sectionId: string, error: string | SubmissionSectionError | SubmissionSectionError[]) { + this.payload = { submissionId, sectionId, error }; + } +} + +/** + * Clear all the errors from the given section + * @param {string} submissionId + * @param {string} sectionId + */ +export class ClearSectionErrorsAction implements Action { + type: string = SubmissionObjectActionTypes.CLEAR_ERRORS; + payload: { + submissionId: string; + sectionId: string; + }; + + constructor(submissionId: string, sectionId: string) { + this.payload = { submissionId, sectionId } + } +} + +// Section actions + +export class InitSectionAction implements Action { + type = SubmissionObjectActionTypes.INIT_SECTION; + payload: { + submissionId: string; + sectionId: string; + header: string; + config: string; + mandatory: boolean; + sectionType: SectionsType; + visibility: SectionVisibility; + enabled: boolean; + data: WorkspaceitemSectionDataType; + errors: SubmissionSectionError[]; + }; + + /** + * Create a new InitSectionAction + * + * @param submissionId + * the submission's ID to remove + * @param sectionId + * the section's ID to add + * @param header + * the section's header + * @param mandatory + * the section's mandatory + * @param sectionType + * the section's type + * @param visibility + * the section's visibility + * @param enabled + * the section's enabled state + * @param data + * the section's data + * @param errors + * the section's errors + */ + constructor(submissionId: string, + sectionId: string, + header: string, + config: string, + mandatory: boolean, + sectionType: SectionsType, + visibility: SectionVisibility, + enabled: boolean, + data: WorkspaceitemSectionDataType, + errors: SubmissionSectionError[]) { + this.payload = { submissionId, sectionId, header, config, mandatory, sectionType, visibility, enabled, data, errors }; + } +} + +export class EnableSectionAction implements Action { + type = SubmissionObjectActionTypes.ENABLE_SECTION; + payload: { + submissionId: string; + sectionId: string; + }; + + /** + * Create a new EnableSectionAction + * + * @param submissionId + * the submission's ID to remove + * @param sectionId + * the section's ID to add + */ + constructor(submissionId: string, + sectionId: string) { + this.payload = { submissionId, sectionId }; + } +} + +export class DisableSectionAction implements Action { + type = SubmissionObjectActionTypes.DISABLE_SECTION; + payload: { + submissionId: string; + sectionId: string; + }; + + /** + * Create a new DisableSectionAction + * + * @param submissionId + * the submission's ID to remove + * @param sectionId + * the section's ID to remove + */ + constructor(submissionId: string, sectionId: string) { + this.payload = { submissionId, sectionId }; + } +} + +export class UpdateSectionDataAction implements Action { + type = SubmissionObjectActionTypes.UPLOAD_SECTION_DATA; + payload: { + submissionId: string; + sectionId: string; + data: WorkspaceitemSectionDataType; + errors: SubmissionSectionError[]; + }; + + /** + * Create a new EnableSectionAction + * + * @param submissionId + * the submission's ID to remove + * @param sectionId + * the section's ID to add + * @param data + * the section's data + * @param errors + * the section's errors + */ + constructor(submissionId: string, + sectionId: string, + data: WorkspaceitemSectionDataType, + errors: SubmissionSectionError[]) { + this.payload = { submissionId, sectionId, data, errors }; + } +} + +export class RemoveSectionErrorsAction implements Action { + type = SubmissionObjectActionTypes.REMOVE_SECTION_ERRORS; + payload: { + submissionId: string; + sectionId: string; + }; + + /** + * Create a new RemoveSectionErrorsAction + * + * @param submissionId + * the submission's ID to remove + * @param sectionId + * the section's ID to add + */ + constructor(submissionId: string, sectionId: string) { + this.payload = { submissionId, sectionId }; + } +} + +export class InitSubmissionFormAction implements Action { + type = SubmissionObjectActionTypes.INIT_SUBMISSION_FORM; + payload: { + collectionId: string; + definitionId: string; + submissionId: string; + selfUrl: string; + sections: WorkspaceitemSectionsObject; + }; + + /** + * Create a new InitSubmissionFormAction + * + * @param collectionId + * the collection's Id where to deposit + * @param definitionId + * the definition's ID to use + * @param submissionId + * the submission's ID + * @param selfUrl + * the submission's self url + * @param sections + * the submission's sections + */ + constructor(collectionId: string, + definitionId: string, + submissionId: string, + selfUrl: string, + sections: WorkspaceitemSectionsObject) { + this.payload = { collectionId, definitionId, submissionId, selfUrl, sections }; + } +} + +// Submission actions + +export class CompleteInitSubmissionFormAction implements Action { + type = SubmissionObjectActionTypes.COMPLETE_INIT_SUBMISSION_FORM; + payload: { + submissionId: string; + }; + + /** + * Create a new CompleteInitSubmissionFormAction + * + * @param submissionId + * the submission's ID + */ + constructor(submissionId: string) { + this.payload = { submissionId }; + } +} + +export class LoadSubmissionFormAction implements Action { + type = SubmissionObjectActionTypes.LOAD_SUBMISSION_FORM; + payload: { + collectionId: string; + submissionId: string; + selfUrl: string; + submissionDefinition: SubmissionDefinitionsModel; + sections: WorkspaceitemSectionsObject; + errors: SubmissionSectionError[]; + }; + + /** + * Create a new LoadSubmissionFormAction + * + * @param collectionId + * the collection's Id where to deposit + * @param submissionId + * the submission's ID + * @param selfUrl + * the submission object url + * @param submissionDefinition + * the submission's sections definition + * @param sections + * the submission's sections + * @param errors + * the submission's sections errors + */ + constructor(collectionId: string, + submissionId: string, + selfUrl: string, + submissionDefinition: SubmissionDefinitionsModel, + sections: WorkspaceitemSectionsObject, + errors: SubmissionSectionError[]) { + this.payload = { collectionId, submissionId, selfUrl, submissionDefinition, sections, errors }; + } +} + +export class SaveForLaterSubmissionFormAction implements Action { + type = SubmissionObjectActionTypes.SAVE_FOR_LATER_SUBMISSION_FORM; + payload: { + submissionId: string; + }; + + /** + * Create a new SaveForLaterSubmissionFormAction + * + * @param submissionId + * the submission's ID + */ + constructor(submissionId: string) { + this.payload = { submissionId }; + } +} + +export class SaveForLaterSubmissionFormSuccessAction implements Action { + type = SubmissionObjectActionTypes.SAVE_FOR_LATER_SUBMISSION_FORM_SUCCESS; + payload: { + submissionId: string; + submissionObject: SubmissionObject[]; + }; + + /** + * Create a new SaveForLaterSubmissionFormSuccessAction + * + * @param submissionId + * the submission's ID + * @param submissionObjects + * the submission's Object + */ + constructor(submissionId: string, submissionObject: SubmissionObject[]) { + this.payload = { submissionId, submissionObject }; + } +} + +export class SaveForLaterSubmissionFormErrorAction implements Action { + type = SubmissionObjectActionTypes.SAVE_FOR_LATER_SUBMISSION_FORM_ERROR; + payload: { + submissionId: string; + }; + + /** + * Create a new SaveForLaterSubmissionFormErrorAction + * + * @param submissionId + * the submission's ID + */ + constructor(submissionId: string) { + this.payload = { submissionId }; + } +} + +export class SaveSubmissionFormAction implements Action { + type = SubmissionObjectActionTypes.SAVE_SUBMISSION_FORM; + payload: { + submissionId: string; + }; + + /** + * Create a new SaveSubmissionFormAction + * + * @param submissionId + * the submission's ID + */ + constructor(submissionId: string) { + this.payload = { submissionId }; + } +} + +export class SaveSubmissionFormSuccessAction implements Action { + type = SubmissionObjectActionTypes.SAVE_SUBMISSION_FORM_SUCCESS; + payload: { + submissionId: string; + submissionObject: SubmissionObject[]; + }; + + /** + * Create a new SaveSubmissionFormSuccessAction + * + * @param submissionId + * the submission's ID + * @param submissionObjects + * the submission's Object + */ + constructor(submissionId: string, submissionObject: SubmissionObject[]) { + this.payload = { submissionId, submissionObject }; + } +} + +export class SaveSubmissionFormErrorAction implements Action { + type = SubmissionObjectActionTypes.SAVE_SUBMISSION_FORM_ERROR; + payload: { + submissionId: string; + }; + + /** + * Create a new SaveSubmissionFormErrorAction + * + * @param submissionId + * the submission's ID + */ + constructor(submissionId: string) { + this.payload = { submissionId }; + } +} + +export class SaveSubmissionSectionFormAction implements Action { + type = SubmissionObjectActionTypes.SAVE_SUBMISSION_SECTION_FORM; + payload: { + submissionId: string; + sectionId: string; + }; + + /** + * Create a new SaveSubmissionSectionFormAction + * + * @param submissionId + * the submission's ID + * @param sectionId + * the section's ID + */ + constructor(submissionId: string, sectionId: string) { + this.payload = { submissionId, sectionId }; + } +} + +export class SaveSubmissionSectionFormSuccessAction implements Action { + type = SubmissionObjectActionTypes.SAVE_SUBMISSION_SECTION_FORM_SUCCESS; + payload: { + submissionId: string; + submissionObject: SubmissionObject[]; + }; + + /** + * Create a new SaveSubmissionSectionFormSuccessAction + * + * @param submissionId + * the submission's ID + * @param submissionObjects + * the submission's Object + */ + constructor(submissionId: string, submissionObject: SubmissionObject[]) { + this.payload = { submissionId, submissionObject }; + } +} + +export class SaveSubmissionSectionFormErrorAction implements Action { + type = SubmissionObjectActionTypes.SAVE_SUBMISSION_SECTION_FORM_ERROR; + payload: { + submissionId: string; + }; + + /** + * Create a new SaveSubmissionFormErrorAction + * + * @param submissionId + * the submission's ID + */ + constructor(submissionId: string) { + this.payload = { submissionId }; + } +} + +export class CompleteSaveSubmissionFormAction implements Action { + type = SubmissionObjectActionTypes.COMPLETE_SAVE_SUBMISSION_FORM; + payload: { + submissionId: string; + }; + + /** + * Create a new CompleteSaveSubmissionFormAction + * + * @param submissionId + * the submission's ID + */ + constructor(submissionId: string) { + this.payload = { submissionId }; + } +} + +export class ResetSubmissionFormAction implements Action { + type = SubmissionObjectActionTypes.RESET_SUBMISSION_FORM; + payload: { + collectionId: string; + submissionId: string; + selfUrl: string; + sections: WorkspaceitemSectionsObject; + submissionDefinition: SubmissionDefinitionsModel; + }; + + /** + * Create a new LoadSubmissionFormAction + * + * @param collectionId + * the collection's Id where to deposit + * @param submissionId + * the submission's ID + * @param selfUrl + * the submission object url + * @param sections + * the submission's sections + * @param submissionDefinition + * the submission's form definition + */ + constructor(collectionId: string, submissionId: string, selfUrl: string, sections: WorkspaceitemSectionsObject, submissionDefinition: SubmissionDefinitionsModel) { + this.payload = { collectionId, submissionId, selfUrl, sections, submissionDefinition }; + } +} + +export class CancelSubmissionFormAction implements Action { + type = SubmissionObjectActionTypes.CANCEL_SUBMISSION_FORM; +} + +export class ChangeSubmissionCollectionAction implements Action { + type = SubmissionObjectActionTypes.CHANGE_SUBMISSION_COLLECTION; + payload: { + submissionId: string; + collectionId: string; + }; + + /** + * Create a new ChangeSubmissionCollectionAction + * + * @param collectionId + * the new collection's ID + */ + constructor(submissionId: string, collectionId: string) { + this.payload = { submissionId, collectionId }; + } +} + +export class SaveAndDepositSubmissionAction implements Action { + type = SubmissionObjectActionTypes.SAVE_AND_DEPOSIT_SUBMISSION; + payload: { + submissionId: string; + }; + + /** + * Create a new SaveAndDepositSubmissionAction + * + * @param submissionId + * the submission's ID to deposit + */ + constructor(submissionId: string) { + this.payload = { submissionId }; + } +} + +export class DepositSubmissionAction implements Action { + type = SubmissionObjectActionTypes.DEPOSIT_SUBMISSION; + payload: { + submissionId: string; + }; + + /** + * Create a new DepositSubmissionAction + * + * @param submissionId + * the submission's ID to deposit + */ + constructor(submissionId: string) { + this.payload = { submissionId }; + } +} + +export class DepositSubmissionSuccessAction implements Action { + type = SubmissionObjectActionTypes.DEPOSIT_SUBMISSION_SUCCESS; + payload: { + submissionId: string; + }; + + /** + * Create a new DepositSubmissionSuccessAction + * + * @param submissionId + * the submission's ID to deposit + */ + constructor(submissionId: string) { + this.payload = { submissionId }; + } +} + +export class DepositSubmissionErrorAction implements Action { + type = SubmissionObjectActionTypes.DEPOSIT_SUBMISSION_ERROR; + payload: { + submissionId: string; + }; + + /** + * Create a new DepositSubmissionErrorAction + * + * @param submissionId + * the submission's ID to deposit + */ + constructor(submissionId: string) { + this.payload = { submissionId }; + } +} + +export class DiscardSubmissionAction implements Action { + type = SubmissionObjectActionTypes.DISCARD_SUBMISSION; + payload: { + submissionId: string; + }; + + /** + * Create a new DiscardSubmissionAction + * + * @param submissionId + * the submission's ID to discard + */ + constructor(submissionId: string) { + this.payload = { submissionId }; + } +} + +export class DiscardSubmissionSuccessAction implements Action { + type = SubmissionObjectActionTypes.DISCARD_SUBMISSION_SUCCESS; + payload: { + submissionId: string; + }; + + /** + * Create a new DiscardSubmissionSuccessAction + * + * @param submissionId + * the submission's ID to discard + */ + constructor(submissionId: string) { + this.payload = { submissionId }; + } +} + +export class DiscardSubmissionErrorAction implements Action { + type = SubmissionObjectActionTypes.DISCARD_SUBMISSION_ERROR; + payload: { + submissionId: string; + }; + + /** + * Create a new DiscardSubmissionErrorAction + * + * @param submissionId + * the submission's ID to discard + */ + constructor(submissionId: string) { + this.payload = { submissionId }; + } +} + +export class SectionStatusChangeAction implements Action { + type = SubmissionObjectActionTypes.SECTION_STATUS_CHANGE; + payload: { + submissionId: string; + sectionId: string; + status: boolean + }; + + /** + * Change the section validity status + * + * @param submissionId + * the submission's ID + * @param sectionId + * the section's ID to change + * @param status + * the section validity status (true if is valid) + */ + constructor(submissionId: string, sectionId: string, status: boolean) { + this.payload = { submissionId, sectionId, status }; + } +} + +export class SectionLoadingStatusChangeAction implements Action { + type = SubmissionObjectActionTypes.SECTION_LOADING_STATUS_CHANGE; + payload: { + submissionId: string; + sectionId: string; + loading: boolean + }; + + /** + * Change the section loading status + * + * @param submissionId + * the submission's ID + * @param sectionId + * the section's ID to change + * @param loading + * the section loading status (true if is loading) + */ + constructor(submissionId: string, sectionId: string, loading: boolean) { + this.payload = { submissionId, sectionId, loading }; + } +} + +export class SetActiveSectionAction implements Action { + type = SubmissionObjectActionTypes.SET_ACTIVE_SECTION; + payload: { + submissionId: string; + sectionId: string; + }; + + /** + * Create a new SetActiveSectionAction + * + * @param submissionId + * the submission's ID + * @param sectionId + * the section's ID to active + */ + constructor(submissionId: string, sectionId: string) { + this.payload = { submissionId, sectionId }; + } +} +// Upload file actions + +export class NewUploadedFileAction implements Action { + type = SubmissionObjectActionTypes.NEW_FILE; + payload: { + submissionId: string; + sectionId: string; + fileId: string; + data: WorkspaceitemSectionUploadFileObject; + }; + + /** + * Add a new uploaded file + * + * @param submissionId + * the submission's ID + * @param sectionId + * the section's ID + * @param fileId + * the file's ID + * @param data + * the metadata of the new bitstream + */ + constructor(submissionId: string, sectionId: string, fileId: string, data: WorkspaceitemSectionUploadFileObject) { + this.payload = { submissionId, sectionId, fileId: fileId, data }; + } +} + +export class EditFileDataAction implements Action { + type = SubmissionObjectActionTypes.EDIT_FILE_DATA; + payload: { + submissionId: string; + sectionId: string; + fileId: string; + data: WorkspaceitemSectionUploadFileObject; + }; + + /** + * Edit a file data + * + * @param submissionId + * the submission's ID + * @param sectionId + * the section's ID + * @param fileId + * the file's ID + * @param data + * the metadata of the new bitstream + */ + constructor(submissionId: string, sectionId: string, fileId: string, data: WorkspaceitemSectionUploadFileObject) { + this.payload = { submissionId, sectionId, fileId: fileId, data }; + } +} + +export class DeleteUploadedFileAction implements Action { + type = SubmissionObjectActionTypes.DELETE_FILE; + payload: { + submissionId: string; + sectionId: string; + fileId: string; + }; + + /** + * Delete a uploaded file + * + * @param submissionId + * the submission's ID + * @param sectionId + * the section's ID + * @param fileId + * the file's ID + */ + constructor(submissionId: string, sectionId: string, fileId: string) { + this.payload = { submissionId, sectionId, fileId }; + } +} + +export class SetWorkspaceDuplicatedAction implements Action { + type = SubmissionObjectActionTypes.SET_WORKSPACE_DUPLICATION; + payload: { + index: number; + decision: string; + note?: string + }; + + /** + * Create a new SetWorkspaceDuplicatedAction + * + * @param index + * the index in matches array + * @param decision + * the submitter's decision ('verify'|'reject'|null) + * @param note + * the submitter's note, for 'verify' decision only + */ + constructor(payload: any) { + this.payload = payload; + } +} + +export class SetWorkspaceDuplicatedSuccessAction implements Action { + type = SubmissionObjectActionTypes.SET_WORKSPACE_DUPLICATION_SUCCESS; + payload: { + index: number; + decision: string; + note?: string + }; + + /** + * Create a new SetWorkspaceDuplicatedSuccessAction + * + * @param index + * the index in matches array + * @param decision + * the submitter's decision ('verify'|'reject'|null) + * @param note + * the submitter's note, for 'verify' decision only + */ + constructor(payload: any) { + this.payload = payload; + } +} + +export class SetWorkspaceDuplicatedErrorAction implements Action { + type = SubmissionObjectActionTypes.SET_WORKSPACE_DUPLICATION_ERROR; + payload: { + index: number; + }; + + /** + * Create a new SetWorkspaceDuplicatedErrorAction + * + * @param index + * the index in matches array + */ + constructor(index: number) { + this.payload = { index }; + } +} + +export class SetWorkflowDuplicatedAction implements Action { + type = SubmissionObjectActionTypes.SET_WORKFLOW_DUPLICATION; + payload: { + index: number; + decision: string; + note?: string + }; + + /** + * Create a new SetWorkflowDuplicatedAction + * + * @param index + * the index in matches array + * @param decision + * the controller's decision ('verify'|'reject'|null) + * @param note + * the controller's note, for 'verify' decision only + */ + constructor(payload: any) { + this.payload = payload; + } +} + +export class SetWorkflowDuplicatedSuccessAction implements Action { + type = SubmissionObjectActionTypes.SET_WORKFLOW_DUPLICATION_SUCCESS; + payload: { + index: number; + decision: string; + note?: string + }; + + /** + * Create a new SetWorkflowDuplicatedSuccessAction + * + * @param index + * the index in matches array + * @param decision + * the controller's decision ('verify'|'reject'|null) + * @param note + * the controller's note, for 'verify' decision only + */ + constructor(payload: any) { + this.payload = payload; + } +} + +export class SetWorkflowDuplicatedErrorAction implements Action { + type = SubmissionObjectActionTypes.SET_WORKFLOW_DUPLICATION_ERROR; + payload: { + index: number; + }; + + /** + * Create a new SetWorkflowDuplicatedErrorAction + * + * @param index + * the index in matches array + */ + constructor(index: number) { + this.payload = { index }; + } +} + +/* tslint:enable:max-classes-per-file */ + +/** + * Export a type alias of all actions in this action group + * so that reducers can easily compose action types + */ +export type SubmissionObjectAction = DisableSectionAction + | InitSectionAction + | EnableSectionAction + | LoadSubmissionFormAction + | ResetSubmissionFormAction + | CancelSubmissionFormAction + | InitSubmissionFormAction + | CompleteInitSubmissionFormAction + | ChangeSubmissionCollectionAction + | SaveAndDepositSubmissionAction + | DepositSubmissionAction + | DepositSubmissionSuccessAction + | DepositSubmissionErrorAction + | DiscardSubmissionAction + | DiscardSubmissionSuccessAction + | DiscardSubmissionErrorAction + | SectionStatusChangeAction + | NewUploadedFileAction + | EditFileDataAction + | DeleteUploadedFileAction + | InertSectionErrorsAction + | DeleteSectionErrorsAction + | ClearSectionErrorsAction + | UpdateSectionDataAction + | RemoveSectionErrorsAction + | SaveForLaterSubmissionFormAction + | SaveForLaterSubmissionFormSuccessAction + | SaveForLaterSubmissionFormErrorAction + | SaveSubmissionFormAction + | SaveSubmissionFormSuccessAction + | SaveSubmissionFormErrorAction + | SaveSubmissionSectionFormAction + | SaveSubmissionSectionFormSuccessAction + | SaveSubmissionSectionFormErrorAction + | CompleteSaveSubmissionFormAction + | SetActiveSectionAction + | SetWorkspaceDuplicatedAction + | SetWorkspaceDuplicatedSuccessAction + | SetWorkspaceDuplicatedErrorAction + | SetWorkflowDuplicatedAction + | SetWorkflowDuplicatedSuccessAction + | SetWorkflowDuplicatedErrorAction; diff --git a/src/app/submission/objects/submission-objects.effects.ts b/src/app/submission/objects/submission-objects.effects.ts new file mode 100644 index 0000000000..7d0b0b2399 --- /dev/null +++ b/src/app/submission/objects/submission-objects.effects.ts @@ -0,0 +1,338 @@ +import { Injectable } from '@angular/core'; +import { Actions, Effect } from '@ngrx/effects'; + +import { union } from 'lodash'; + +import { + CompleteInitSubmissionFormAction, + DepositSubmissionAction, + DepositSubmissionErrorAction, + DepositSubmissionSuccessAction, + DiscardSubmissionErrorAction, + DiscardSubmissionSuccessAction, + InitSectionAction, + LoadSubmissionFormAction, + ResetSubmissionFormAction, + SaveAndDepositSubmissionAction, + SaveForLaterSubmissionFormAction, + SaveForLaterSubmissionFormSuccessAction, + SaveSubmissionFormAction, + SaveSubmissionFormErrorAction, + SaveSubmissionFormSuccessAction, + SaveSubmissionSectionFormAction, + SaveSubmissionSectionFormErrorAction, + SaveSubmissionSectionFormSuccessAction, + SetWorkflowDuplicatedAction, + SetWorkflowDuplicatedErrorAction, + SetWorkflowDuplicatedSuccessAction, + SetWorkspaceDuplicatedAction, + SetWorkspaceDuplicatedErrorAction, + SetWorkspaceDuplicatedSuccessAction, + SubmissionObjectActionTypes, + UpdateSectionDataAction +} from './submission-objects.actions'; +import { SectionsService } from '../sections/sections.service'; +import { isEmpty, isNotEmpty, isNotUndefined } from '../../shared/empty.util'; +import { Workspaceitem } from '../../core/submission/models/workspaceitem.model'; +import { Observable } from 'rxjs/Observable'; +import { JsonPatchOperationsService } from '../../core/json-patch/json-patch-operations.service'; +import { SubmitDataResponseDefinitionObject } from '../../core/shared/submit-data-response-definition.model'; +import { SubmissionService } from '../submission.service'; +import { Action, Store } from '@ngrx/store'; +import { Workflowitem } from '../../core/submission/models/workflowitem.model'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { SubmissionObject } from '../../core/submission/models/submission-object.model'; +import { TranslateService } from '@ngx-translate/core'; +import { DeduplicationService } from '../sections/deduplication/deduplication.service'; +import { SubmissionState } from '../submission.reducers'; +import { SubmissionObjectEntry } from './submission-objects.reducer'; +import { SubmissionSectionModel } from '../../core/shared/config/config-submission-section.model'; +import parseSectionErrors from '../utils/parseSectionErrors'; + +@Injectable() +export class SubmissionObjectEffects { + + @Effect() loadForm$ = this.actions$ + .ofType(SubmissionObjectActionTypes.LOAD_SUBMISSION_FORM) + .map((action: LoadSubmissionFormAction) => { + const definition = action.payload.submissionDefinition; + const mappedActions = []; + definition.sections.forEach((sectionDefinition: SubmissionSectionModel, index: number) => { + const sectionId = sectionDefinition._links.self.substr(sectionDefinition._links.self.lastIndexOf('/') + 1); + const config = sectionDefinition._links.config || ''; + const enabled = sectionDefinition.mandatory || (isNotEmpty(action.payload.sections) && action.payload.sections.hasOwnProperty(sectionId)); + const sectionData = (isNotUndefined(action.payload.sections) && isNotUndefined(action.payload.sections[sectionId])) ? action.payload.sections[sectionId] : Object.create(null); + const sectionErrors = null; + mappedActions.push( + new InitSectionAction( + action.payload.submissionId, + sectionId, + sectionDefinition.header, + config, + sectionDefinition.mandatory, + sectionDefinition.sectionType, + sectionDefinition.visibility, + enabled, + sectionData, + sectionErrors + ) + ) + }); + return {action: action, definition: definition, mappedActions: mappedActions}; + }) + .mergeMap((result) => { + return Observable.from( + result.mappedActions.concat( + new CompleteInitSubmissionFormAction(result.action.payload.submissionId) + )); + }); + + @Effect() resetForm$ = this.actions$ + .ofType(SubmissionObjectActionTypes.RESET_SUBMISSION_FORM) + .map((action: ResetSubmissionFormAction) => + new LoadSubmissionFormAction( + action.payload.collectionId, + action.payload.submissionId, + action.payload.selfUrl, + action.payload.submissionDefinition, + action.payload.sections, + null + )); + + @Effect() saveSubmission$ = this.actions$ + .ofType(SubmissionObjectActionTypes.SAVE_SUBMISSION_FORM) + .switchMap((action: SaveSubmissionFormAction) => { + return this.operationsService.jsonPatchByResourceType( + this.submissionService.getSubmissionObjectLinkName(), + action.payload.submissionId, + 'sections') + .map((response: SubmissionObject[]) => new SaveSubmissionFormSuccessAction(action.payload.submissionId, response)) + .catch(() => Observable.of(new SaveSubmissionFormErrorAction(action.payload.submissionId))); + }); + + @Effect() saveForLaterSubmission$ = this.actions$ + .ofType(SubmissionObjectActionTypes.SAVE_FOR_LATER_SUBMISSION_FORM) + .switchMap((action: SaveForLaterSubmissionFormAction) => { + return this.operationsService.jsonPatchByResourceType( + this.submissionService.getSubmissionObjectLinkName(), + action.payload.submissionId, + 'sections') + .map((response: SubmissionObject[]) => new SaveForLaterSubmissionFormSuccessAction(action.payload.submissionId, response)) + .catch(() => Observable.of(new SaveSubmissionFormErrorAction(action.payload.submissionId))); + }); + + @Effect() saveSubmissionSuccess$ = this.actions$ + .ofType(SubmissionObjectActionTypes.SAVE_SUBMISSION_FORM_SUCCESS, SubmissionObjectActionTypes.SAVE_SUBMISSION_SECTION_FORM_SUCCESS) + .withLatestFrom(this.store$) + .map(([action, currentState]: [SaveSubmissionFormSuccessAction | SaveSubmissionSectionFormSuccessAction, any]) => { + return this.parseSaveResponse((currentState.submission as SubmissionState).objects[action.payload.submissionId], action.payload.submissionObject, action.payload.submissionId); + }) + .mergeMap((actions) => { + return Observable.from(actions); + }); + + @Effect() saveSection$ = this.actions$ + .ofType(SubmissionObjectActionTypes.SAVE_SUBMISSION_SECTION_FORM) + .switchMap((action: SaveSubmissionSectionFormAction) => { + return this.operationsService.jsonPatchByResourceID( + this.submissionService.getSubmissionObjectLinkName(), + action.payload.submissionId, + 'sections', + action.payload.sectionId) + .map((response: SubmissionObject[]) => new SaveSubmissionSectionFormSuccessAction(action.payload.submissionId, response)) + .catch(() => Observable.of(new SaveSubmissionSectionFormErrorAction(action.payload.submissionId))); + }); + + @Effect() saveAndDepositSection$ = this.actions$ + .ofType(SubmissionObjectActionTypes.SAVE_AND_DEPOSIT_SUBMISSION) + .withLatestFrom(this.store$) + .switchMap(([action, currentState]: [SaveAndDepositSubmissionAction, any]) => { + return this.operationsService.jsonPatchByResourceType( + this.submissionService.getSubmissionObjectLinkName(), + action.payload.submissionId, + 'sections') + .map((response: SubmissionObject[]) => { + if (this.canDeposit(response)) { + return new DepositSubmissionAction(action.payload.submissionId); + } else { + this.notificationsService.warning(null, this.translate.get('submission.sections.general.sections_not_valid')); + return this.parseSaveResponse((currentState.submission as SubmissionState).objects[action.payload.submissionId], response, action.payload.submissionId); + } + }) + .catch(() => Observable.of(new SaveSubmissionSectionFormErrorAction(action.payload.submissionId))); + }); + + @Effect() depositSubmission$ = this.actions$ + .ofType(SubmissionObjectActionTypes.DEPOSIT_SUBMISSION) + .withLatestFrom(this.store$) + .switchMap(([action, state]: [DepositSubmissionAction, any]) => { + return this.submissionService.depositSubmission(state.submission.objects[action.payload.submissionId].selfUrl) + .map(() => new DepositSubmissionSuccessAction(action.payload.submissionId)) + .catch((e) => Observable.of(new DepositSubmissionErrorAction(action.payload.submissionId))); + }); + + @Effect({dispatch: false}) SaveForLaterSubmissionSuccess$ = this.actions$ + .ofType(SubmissionObjectActionTypes.SAVE_FOR_LATER_SUBMISSION_FORM_SUCCESS) + .do(() => this.notificationsService.success(null, this.translate.get('submission.sections.general.save_success_notice'))) + .do(() => this.submissionService.redirectToMyDSpace()); + + @Effect({dispatch: false}) depositSubmissionSuccess$ = this.actions$ + .ofType(SubmissionObjectActionTypes.DEPOSIT_SUBMISSION_SUCCESS) + .do(() => this.notificationsService.success(null, this.translate.get('submission.sections.general.deposit_success_notice'))) + .do(() => this.submissionService.redirectToMyDSpace()); + + @Effect({dispatch: false}) depositSubmissionError$ = this.actions$ + .ofType(SubmissionObjectActionTypes.DEPOSIT_SUBMISSION_ERROR) + .do(() => this.notificationsService.error(null, this.translate.get('submission.sections.general.deposit_error_notice'))); + + @Effect() discardSubmission$ = this.actions$ + .ofType(SubmissionObjectActionTypes.DISCARD_SUBMISSION) + .switchMap((action: DepositSubmissionAction) => { + return this.submissionService.discardSubmission(action.payload.submissionId) + .map(() => new DiscardSubmissionSuccessAction(action.payload.submissionId)) + .catch((e) => Observable.of(new DiscardSubmissionErrorAction(action.payload.submissionId))); + }); + + @Effect({dispatch: false}) discardSubmissionSuccess$ = this.actions$ + .ofType(SubmissionObjectActionTypes.DISCARD_SUBMISSION_SUCCESS) + .do(() => this.notificationsService.success(null, this.translate.get('submission.sections.general.discard_success_notice'))) + .do(() => this.submissionService.redirectToMyDSpace()); + + @Effect({dispatch: false}) discardSubmissionError$ = this.actions$ + .ofType(SubmissionObjectActionTypes.DISCARD_SUBMISSION_ERROR) + .do(() => this.notificationsService.error(null, this.translate.get('submission.sections.general.discard_error_notice'))); + + @Effect() + public wsDuplication: Observable = this.actions$ + .ofType(SubmissionObjectActionTypes.SET_WORKSPACE_DUPLICATION) + .map((action: SetWorkspaceDuplicatedAction) => { + // return this.deduplicationService.setWorkspaceDuplicated(action.payload) + // .first() + // .map((response) => { + console.log('Effect of SET_WORKSPACE_DUPLICATION'); + // TODO JSON PATCH + // const pathCombiner = new JsonPatchOperationPathCombiner('sections', 'deduplication'); + // const path = ''; // `metadata/${metadataKey}`; // TODO + // this.operationsBuilder.add(pathCombiner.getPath(path), action.payload, true); + return new SetWorkspaceDuplicatedSuccessAction(action.payload); + }) + .catch((error) => Observable.of(new SetWorkspaceDuplicatedErrorAction(error))); + + @Effect({dispatch: false}) + public wsDuplicationSuccess: Observable = this.actions$ + .ofType(SubmissionObjectActionTypes.SET_WORKSPACE_DUPLICATION_SUCCESS) + // TODO + .do((action: SetWorkspaceDuplicatedAction) => { + console.log('Effect of SET_WORKSPACE_DUPLICATION_SUCCESS'); + this.deduplicationService.setWorkspaceDuplicationSuccess(action.payload); + }); + + @Effect({dispatch: false}) + public wsDuplicationError: Observable = this.actions$ + .ofType(SubmissionObjectActionTypes.SET_WORKSPACE_DUPLICATION_ERROR) + .do((action: SetWorkspaceDuplicatedAction) => { + console.log('Effect of SET_WORKSPACE_DUPLICATION_ERROR'); + this.deduplicationService.setWorkspaceDuplicationError(action.payload); + }); + + @Effect() + public wfDuplication: Observable = this.actions$ + .ofType(SubmissionObjectActionTypes.SET_WORKFLOW_DUPLICATION) + .map((action: SetWorkflowDuplicatedAction) => { + // return this.deduplicationService.setWorkflowDuplicated(action.payload) + // .first() + // .map((response) => { + console.log('Effect of SET_WORKFLOW_DUPLICATION'); + // TODO JSON PATCH + // const pathCombiner = new JsonPatchOperationPathCombiner('sections', 'deduplication'); + // const path = ''; // `metadata/${metadataKey}`; // TODO + // this.operationsBuilder.add(pathCombiner.getPath(path), action.payload, true); + return new SetWorkflowDuplicatedSuccessAction(action.payload); + }) + .catch((error) => Observable.of(new SetWorkflowDuplicatedErrorAction(error))); + + @Effect({dispatch: false}) + public wfDuplicationSuccess: Observable = this.actions$ + .ofType(SubmissionObjectActionTypes.SET_WORKFLOW_DUPLICATION_SUCCESS) + // TODO + .do((action: SetWorkflowDuplicatedAction) => { + console.log('Effect of SET_WORKFLOW_DUPLICATION_SUCCESS'); + this.deduplicationService.setWorkflowDuplicationSuccess(action.payload); + }); + + @Effect({dispatch: false}) + public wfDuplicationError: Observable = this.actions$ + .ofType(SubmissionObjectActionTypes.SET_WORKFLOW_DUPLICATION_ERROR) + .do((action: SetWorkflowDuplicatedAction) => { + console.log('Effect of SET_WORKFLOW_DUPLICATION_ERROR'); + this.deduplicationService.setWorkflowDuplicationError(action.payload); + }); + + constructor(private actions$: Actions, + private notificationsService: NotificationsService, + private operationsService: JsonPatchOperationsService, + private sectionService: SectionsService, + private store$: Store, + private submissionService: SubmissionService, + private deduplicationService: DeduplicationService, + private translate: TranslateService) { + } + + protected canDeposit(response: SubmissionObject[]) { + let canDeposit = true; + + if (isNotEmpty(response)) { + response.forEach((item: Workspaceitem | Workflowitem) => { + const {errors} = item; + + if (errors && !isEmpty(errors)) { + canDeposit = false; + } + }); + } + return canDeposit; + } + + protected parseSaveResponse(currentState: SubmissionObjectEntry, response: SubmissionObject[], submissionId: string) { + const mappedActions = []; + + if (isNotEmpty(response)) { + this.notificationsService.success(null, this.translate.get('submission.sections.general.save_success_notice')); + + // to avoid dispatching an action for every error, create an array of errors per section + response.forEach((item: Workspaceitem | Workflowitem) => { + + let errorsList = Object.create({}); + const {errors} = item; + + if (errors && !isEmpty(errors)) { + errorsList = parseSectionErrors(errors); + this.notificationsService.warning(null, this.translate.get('submission.sections.general.sections_not_valid')); + } + + const sections = (item.sections && isNotEmpty(item.sections)) ? item.sections : {}; + + const sectionsKeys: string[] = union(Object.keys(sections), Object.keys(errorsList)); + + sectionsKeys + .forEach((sectionId) => { + const sectionErrors = errorsList[sectionId] || []; + const sectionData = sections[sectionId] || {}; + if (!currentState.sections[sectionId].enabled) { + this.translate.get('submission.sections.general.metadata-extracted-new-section', {sectionId}) + .take(1) + .subscribe((m) => { + this.notificationsService.info(null, m, null, true); + }); + } + mappedActions.push(new UpdateSectionDataAction(submissionId, sectionId, sectionData, sectionErrors)); + }); + + }); + + } + // mappedActions.push(new CompleteSaveSubmissionFormAction(submissionId)); + return mappedActions; + } + +} diff --git a/src/app/submission/objects/submission-objects.reducer.ts b/src/app/submission/objects/submission-objects.reducer.ts new file mode 100644 index 0000000000..1179591790 --- /dev/null +++ b/src/app/submission/objects/submission-objects.reducer.ts @@ -0,0 +1,844 @@ +import { hasValue, isNotEmpty, isNotNull, isNotUndefined } from '../../shared/empty.util'; +import { findKey, uniqWith, isEqual, differenceWith } from 'lodash'; + +import { + CompleteInitSubmissionFormAction, + DeleteUploadedFileAction, + DisableSectionAction, EditFileDataAction, + EnableSectionAction, NewUploadedFileAction, + LoadSubmissionFormAction, SectionStatusChangeAction, + SubmissionObjectAction, + SubmissionObjectActionTypes, ClearSectionErrorsAction, InertSectionErrorsAction, + DeleteSectionErrorsAction, ResetSubmissionFormAction, UpdateSectionDataAction, SaveSubmissionFormAction, + CompleteSaveSubmissionFormAction, SetActiveSectionAction, SaveSubmissionSectionFormAction, + DepositSubmissionAction, DepositSubmissionSuccessAction, DepositSubmissionErrorAction, + ChangeSubmissionCollectionAction, SaveSubmissionFormSuccessAction, SaveSubmissionFormErrorAction, + SaveSubmissionSectionFormSuccessAction, SaveSubmissionSectionFormErrorAction, SetWorkspaceDuplicatedAction, + SetWorkflowDuplicatedAction, InitSectionAction, RemoveSectionErrorsAction +} from './submission-objects.actions'; +import { deleteProperty } from '../../shared/object.util'; +import { WorkspaceitemSectionDataType } from '../../core/submission/models/workspaceitem-sections.model'; +import { WorkspaceitemSectionUploadObject } from '../../core/submission/models/workspaceitem-section-upload.model'; +import { SectionsType } from '../sections/sections-type'; + +export interface SectionVisibility { + main: any; + other: any; +} + +export interface SubmissionSectionObject { + header: string; + config: string; + mandatory: boolean; + sectionType: SectionsType; + visibility: SectionVisibility; + collapsed: boolean, + enabled: boolean; + data: WorkspaceitemSectionDataType; + errors: SubmissionSectionError[]; + isLoading: boolean; + isValid: boolean; +} + +export interface SubmissionSectionError { + path: string; + message: string; +} + +export interface SubmissionSectionEntry { + [sectionId: string]: SubmissionSectionObject; +} + +export interface SubmissionObjectEntry { + collection?: string, + definition?: string, + selfUrl?: string; + activeSection?: string; + sections?: SubmissionSectionEntry; + isLoading?: boolean; + savePending?: boolean; + depositPending?: boolean; +} + +/** + * The Submission State + * + * Consists of a map with submission's ID as key, + * and SubmissionObjectEntries as values + */ +export interface SubmissionObjectState { + [submissionId: string]: SubmissionObjectEntry; +} + +const initialState: SubmissionObjectState = Object.create({}); + +export function submissionObjectReducer(state = initialState, action: SubmissionObjectAction): SubmissionObjectState { + switch (action.type) { + + // submission form actions + case SubmissionObjectActionTypes.INIT_SUBMISSION_FORM: { + return state; + } + + case SubmissionObjectActionTypes.COMPLETE_INIT_SUBMISSION_FORM: { + return completeInit(state, action as CompleteInitSubmissionFormAction); + } + + case SubmissionObjectActionTypes.LOAD_SUBMISSION_FORM: { + return initSubmission(state, action as LoadSubmissionFormAction); + } + + case SubmissionObjectActionTypes.RESET_SUBMISSION_FORM: { + return resetSubmission(state, action as ResetSubmissionFormAction); + } + + case SubmissionObjectActionTypes.CANCEL_SUBMISSION_FORM: { + return initialState; + } + + case SubmissionObjectActionTypes.SAVE_SUBMISSION_FORM: + case SubmissionObjectActionTypes.SAVE_FOR_LATER_SUBMISSION_FORM: + case SubmissionObjectActionTypes.SAVE_AND_DEPOSIT_SUBMISSION: { + return saveSubmission(state, action as SaveSubmissionFormAction); + } + + case SubmissionObjectActionTypes.SAVE_SUBMISSION_FORM_SUCCESS: + case SubmissionObjectActionTypes.SAVE_FOR_LATER_SUBMISSION_FORM_SUCCESS: { + return completeSave(state, action as SaveSubmissionFormSuccessAction); + } + + case SubmissionObjectActionTypes.SAVE_SUBMISSION_FORM_ERROR: + case SubmissionObjectActionTypes.SAVE_FOR_LATER_SUBMISSION_FORM_ERROR: { + return completeSave(state, action as SaveSubmissionFormErrorAction); + } + + case SubmissionObjectActionTypes.SAVE_SUBMISSION_SECTION_FORM: { + return saveSubmission(state, action as SaveSubmissionSectionFormAction); + } + + case SubmissionObjectActionTypes.SAVE_SUBMISSION_SECTION_FORM_SUCCESS: { + return completeSave(state, action as SaveSubmissionSectionFormSuccessAction); + } + + case SubmissionObjectActionTypes.SAVE_SUBMISSION_SECTION_FORM_ERROR: { + return completeSave(state, action as SaveSubmissionSectionFormErrorAction); + } + + case SubmissionObjectActionTypes.CHANGE_SUBMISSION_COLLECTION: { + return changeCollection(state, action as ChangeSubmissionCollectionAction); + } + + case SubmissionObjectActionTypes.COMPLETE_SAVE_SUBMISSION_FORM: { + return completeSave(state, action as CompleteSaveSubmissionFormAction); + } + + case SubmissionObjectActionTypes.DEPOSIT_SUBMISSION: { + return startDeposit(state, action as DepositSubmissionAction); + } + + case SubmissionObjectActionTypes.DEPOSIT_SUBMISSION_SUCCESS: { + return initialState; + } + + case SubmissionObjectActionTypes.DEPOSIT_SUBMISSION_ERROR: { + return endDeposit(state, action as DepositSubmissionAction); + } + + case SubmissionObjectActionTypes.DISCARD_SUBMISSION: { + return state; + } + + case SubmissionObjectActionTypes.DISCARD_SUBMISSION_SUCCESS: { + return initialState; + } + + case SubmissionObjectActionTypes.DISCARD_SUBMISSION_ERROR: { + return state; + } + + case SubmissionObjectActionTypes.SET_ACTIVE_SECTION: { + return setActiveSection(state, action as SetActiveSectionAction); + } + + // Section actions + + case SubmissionObjectActionTypes.INIT_SECTION: { + return initSection(state, action as InitSectionAction); + } + + case SubmissionObjectActionTypes.ENABLE_SECTION: { + return changeSectionState(state, action as EnableSectionAction, true); + } + + case SubmissionObjectActionTypes.UPLOAD_SECTION_DATA: { + return updateSectionData(state, action as UpdateSectionDataAction); + } + + case SubmissionObjectActionTypes.REMOVE_SECTION_ERRORS: { + return removeSectionErrors(state, action as RemoveSectionErrorsAction); + } + + case SubmissionObjectActionTypes.DISABLE_SECTION: { + return changeSectionState(state, action as DisableSectionAction, false); + } + + case SubmissionObjectActionTypes.SECTION_STATUS_CHANGE: { + return setIsValid(state, action as SectionStatusChangeAction); + } + + // Files actions + case SubmissionObjectActionTypes.NEW_FILE: { + return newFile(state, action as NewUploadedFileAction); + } + + case SubmissionObjectActionTypes.EDIT_FILE_DATA: { + return editFileData(state, action as EditFileDataAction); + } + + case SubmissionObjectActionTypes.DELETE_FILE: { + return deleteFile(state, action as DeleteUploadedFileAction); + } + + // deduplication + case SubmissionObjectActionTypes.SET_WORKSPACE_DUPLICATION: { + return updateDeduplication(state, action as SetWorkspaceDuplicatedAction); + } + + case SubmissionObjectActionTypes.SET_WORKFLOW_DUPLICATION: { + return updateDeduplication(state, action as SetWorkflowDuplicatedAction); + } + + // errors actions + case SubmissionObjectActionTypes.INSERT_ERRORS: { + return insertError(state, action as InertSectionErrorsAction); + } + + case SubmissionObjectActionTypes.DELETE_ERRORS: { + return removeError(state, action as DeleteSectionErrorsAction); + } + + case SubmissionObjectActionTypes.CLEAR_ERRORS: { + return clearErrorsFromSection(state, action as ClearSectionErrorsAction); + } + + default: { + return state; + } + } +} + +// ------ Submission error functions ------ // + +const removeError = (state: SubmissionObjectState, action: DeleteSectionErrorsAction): SubmissionObjectState => { + const { submissionId, sectionId, error } = action.payload; + + if (hasValue(state[ submissionId ].sections[ sectionId ])) { + let errors = state[ submissionId ].sections[ sectionId ].errors.filter((currentError) => { + return currentError.message !== error && !isEqual(currentError, error); + }); + + if (action.payload.error instanceof Array) { + errors = differenceWith(errors, action.payload.error, isEqual); + } + + return Object.assign({}, state, { + [ submissionId ]: Object.assign({}, state[ submissionId ], { + sections: Object.assign({}, state[ submissionId ].sections, { + [ sectionId ]: Object.assign({}, state[ action.payload.submissionId ].sections [ action.payload.sectionId ], { + errors + }) + }), + }) + }); + } else { + return state; + } +}; + +const insertError = (state: SubmissionObjectState, action: InertSectionErrorsAction): SubmissionObjectState => { + const { submissionId, sectionId, error } = action.payload; + + if (hasValue(state[ submissionId ].sections[ sectionId ])) { + const errors = uniqWith(state[ submissionId ].sections[ sectionId ].errors.concat(error), isEqual); + + return Object.assign({}, state, { + [ submissionId ]: Object.assign({}, state[ submissionId ], { + activeSection: state[ action.payload.submissionId ].activeSection, sections: Object.assign({}, state[ submissionId ].sections, { + [ sectionId ]: Object.assign({}, state[ action.payload.submissionId ].sections [ action.payload.sectionId ], { + errors + }) + }), + }) + }); + } else { + return state; + } +}; + +const clearErrorsFromSection = (state: SubmissionObjectState, action: ClearSectionErrorsAction): SubmissionObjectState => { + const { submissionId, sectionId } = action.payload; + + if (hasValue(state[ submissionId ].sections[ sectionId ])) { + const errors = []; // clear the errors + + return Object.assign({}, state, { + [ submissionId ]: Object.assign({}, state[ submissionId ], { + sections: Object.assign({}, state[ submissionId ].sections, { + [ sectionId ]: Object.assign({}, state[ action.payload.submissionId ].sections [ action.payload.sectionId ], { + errors + }) + }), + }) + }); + } else { + return state; + } +}; + +// ------ Submission functions ------ // + +/** + * Init a SubmissionObjectState. + * + * @param state + * the current state + * @param action + * an LoadSubmissionFormAction + * @return SubmissionObjectState + * the new state, with the section removed. + */ +function initSubmission(state: SubmissionObjectState, action: LoadSubmissionFormAction | ResetSubmissionFormAction): SubmissionObjectState { + + const newState = Object.assign({}, state); + newState[ action.payload.submissionId ] = { + collection: action.payload.collectionId, + definition: action.payload.submissionDefinition.name, + selfUrl: action.payload.selfUrl, + activeSection: null, + sections: Object.create(null), + isLoading: true, + savePending: false, + depositPending: false, + }; + return newState; +} + +/** + * Reset submission. + * + * @param state + * the current state + * @param action + * an ResetSubmissionFormAction + * @return SubmissionObjectState + * the new state, with the section removed. + */ +function resetSubmission(state: SubmissionObjectState, action: ResetSubmissionFormAction): SubmissionObjectState { + if (hasValue(state[ action.payload.submissionId ])) { + return Object.assign({}, state, { + [ action.payload.submissionId ]: Object.assign({}, state[ action.payload.submissionId ], { + sections: Object.create(null), + isLoading: true + }) + }); + } else { + return state; + } +} + +/** + * Set submission loading to false. + * + * @param state + * the current state + * @param action + * an CompleteInitSubmissionFormAction + * @return SubmissionObjectState + * the new state, with the section removed. + */ +function completeInit(state: SubmissionObjectState, action: CompleteInitSubmissionFormAction): SubmissionObjectState { + if (hasValue(state[ action.payload.submissionId ])) { + return Object.assign({}, state, { + [ action.payload.submissionId ]: Object.assign({}, state[ action.payload.submissionId ], { + isLoading: false + }) + }); + } else { + return state; + } +} + +/** + * Set submission save flag to true + * + * @param state + * the current state + * @param action + * an SaveSubmissionFormAction + * @return SubmissionObjectState + * the new state, with the flag set to true. + */ +function saveSubmission(state: SubmissionObjectState, action: SaveSubmissionFormAction): SubmissionObjectState { + if (hasValue(state[ action.payload.submissionId ])) { + return Object.assign({}, state, { + [ action.payload.submissionId ]: Object.assign({}, state[ action.payload.submissionId ], { + activeSection: state[ action.payload.submissionId ].activeSection, + sections: state[ action.payload.submissionId ].sections, + isLoading: state[ action.payload.submissionId ].isLoading, + savePending: true, + }) + }); + } else { + return state; + } +} + +/** + * Set submission save flag to false. + * + * @param state + * the current state + * @param action + * an CompleteSaveSubmissionFormAction | SaveSubmissionFormSuccessAction | SaveSubmissionFormErrorAction + * @return SubmissionObjectState + * the new state, with the flag set to false. + */ +function completeSave(state: SubmissionObjectState, + action: CompleteSaveSubmissionFormAction + | SaveSubmissionFormSuccessAction + | SaveSubmissionFormErrorAction + | SaveSubmissionSectionFormSuccessAction + | SaveSubmissionSectionFormErrorAction): SubmissionObjectState { + if (hasValue(state[ action.payload.submissionId ])) { + return Object.assign({}, state, { + [ action.payload.submissionId ]: Object.assign({}, state[ action.payload.submissionId ], { + savePending: false, + }) + }); + } else { + return state; + } +} + +/** + * Set deposit flag to true + * + * @param state + * the current state + * @param action + * an DepositSubmissionAction + * @return SubmissionObjectState + * the new state, with the deposit flag changed. + */ +function startDeposit(state: SubmissionObjectState, action: DepositSubmissionAction): SubmissionObjectState { + if (hasValue(state[ action.payload.submissionId ])) { + return Object.assign({}, state, { + [ action.payload.submissionId ]: Object.assign({}, state[ action.payload.submissionId ], { + savePending: false, + depositPending: true, + }) + }); + } else { + return state; + } +} + +/** + * Set deposit flag to false + * + * @param state + * the current state + * @param action + * an DepositSubmissionSuccessAction or DepositSubmissionErrorAction + * @return SubmissionObjectState + * the new state, with the deposit flag changed. + */ +function endDeposit(state: SubmissionObjectState, action: DepositSubmissionSuccessAction | DepositSubmissionErrorAction): SubmissionObjectState { + if (hasValue(state[ action.payload.submissionId ])) { + return Object.assign({}, state, { + [ action.payload.submissionId ]: Object.assign({}, state[ action.payload.submissionId ], { + depositPending: false, + }) + }); + } else { + return state; + } +} + +/** + * Init a SubmissionObjectState. + * + * @param state + * the current state + * @param action + * an LoadSubmissionFormAction + * @return SubmissionObjectState + * the new state, with the section removed. + */ +function changeCollection(state: SubmissionObjectState, action: ChangeSubmissionCollectionAction): SubmissionObjectState { + return Object.assign({}, state, { + [ action.payload.submissionId ]: Object.assign({}, state[ action.payload.submissionId ], { + collection: action.payload.collectionId + }) + }); +} + +// ------ Section functions ------ // + +/** + * Set submission active section. + * + * @param state + * the current state + * @param action + * an SetActiveSectionAction + * @return SubmissionObjectState + * the new state, with the active section. + */ +function setActiveSection(state: SubmissionObjectState, action: SetActiveSectionAction): SubmissionObjectState { + if (hasValue(state[ action.payload.submissionId ])) { + return Object.assign({}, state, { + [ action.payload.submissionId ]: Object.assign({}, state[ action.payload.submissionId ], { + activeSection: action.payload.sectionId, + sections: state[ action.payload.submissionId ].sections, + isLoading: state[ action.payload.submissionId ].isLoading, + savePending: state[ action.payload.submissionId ].savePending, + }) + }); + } else { + return state; + } +} + +/** + * Set a section enabled. + * + * @param state + * the current state + * @param action + * an InitSectionAction + * @return SubmissionObjectState + * the new state, with the section removed. + */ +function initSection(state: SubmissionObjectState, action: InitSectionAction): SubmissionObjectState { + if (hasValue(state[ action.payload.submissionId ])) { + return Object.assign({}, state, { + [ action.payload.submissionId ]: Object.assign({}, state[ action.payload.submissionId ], { + sections: Object.assign({}, state[ action.payload.submissionId ].sections, { + [ action.payload.sectionId ]: { + header: action.payload.header, + config: action.payload.config, + mandatory: action.payload.mandatory, + sectionType: action.payload.sectionType, + visibility: action.payload.visibility, + collapsed: false, + enabled: action.payload.enabled, + data: action.payload.data, + errors: action.payload.errors || [], + isLoading: false, + isValid: false + } + }) + }) + }); + } else { + return state; + } +} + +/** + * Update section's data. + * + * @param state + * the current state + * @param action + * an UpdateSectionDataAction + * @return SubmissionObjectState + * the new state, with the section's data updated. + */ +function updateSectionData(state: SubmissionObjectState, action: UpdateSectionDataAction): SubmissionObjectState { + if (isNotEmpty(state[ action.payload.submissionId ]) + && isNotEmpty(state[ action.payload.submissionId ].sections[ action.payload.sectionId])) { + return Object.assign({}, state, { + [ action.payload.submissionId ]: Object.assign({}, state[ action.payload.submissionId ], { + sections: Object.assign({}, state[ action.payload.submissionId ].sections, { + [ action.payload.sectionId ]: Object.assign({}, state[ action.payload.submissionId ].sections [ action.payload.sectionId ], { + enabled: true, + data: action.payload.data, + errors: action.payload.errors + }) + }) + }) + }); + } else { + return state; + } +} + +/** + * Remove section's errors. + * + * @param state + * the current state + * @param action + * an RemoveSectionErrorsAction + * @return SubmissionObjectState + * the new state, with the section's errors updated. + */ +function removeSectionErrors(state: SubmissionObjectState, action: RemoveSectionErrorsAction): SubmissionObjectState { + if (isNotEmpty(state[ action.payload.submissionId ]) + && isNotEmpty(state[ action.payload.submissionId ].sections[ action.payload.sectionId])) { + return Object.assign({}, state, { + [ action.payload.submissionId ]: Object.assign({}, state[ action.payload.submissionId ], { + sections: Object.assign({}, state[ action.payload.submissionId ].sections, { + [ action.payload.sectionId ]: Object.assign({}, state[ action.payload.submissionId ].sections [ action.payload.sectionId ], { + errors: [] + }) + }) + }) + }); + } else { + return state; + } +} + +/** + * Set a section state. + * + * @param state + * the current state + * @param action + * an DisableSectionAction + * @param enabled + * enabled or disabled section. + * @return SubmissionObjectState + * the new state, with the section removed. + */ +function changeSectionState(state: SubmissionObjectState, action: EnableSectionAction | DisableSectionAction, enabled: boolean): SubmissionObjectState { + if (hasValue(state[ action.payload.submissionId ].sections[ action.payload.sectionId ])) { + return Object.assign({}, state, { + [ action.payload.submissionId ]: Object.assign({}, state[ action.payload.submissionId ], { + // sections: deleteProperty(state[ action.payload.submissionId ].sections, action.payload.sectionId), + sections: Object.assign({}, state[ action.payload.submissionId ].sections, { + [ action.payload.sectionId ]: Object.assign({}, state[ action.payload.submissionId ].sections [ action.payload.sectionId ], { + enabled + }) + }) + }) + }); + } else { + return state; + } +} + +/** + * Set the section validity. + * + * @param state + * the current state + * @param action + * an LoadSubmissionFormAction + * @return SubmissionObjectState + * the new state, with the section new validity status. + */ +function setIsValid(state: SubmissionObjectState, action: SectionStatusChangeAction): SubmissionObjectState { + if (hasValue(state[ action.payload.submissionId ].sections[ action.payload.sectionId ])) { + return Object.assign({}, state, { + [ action.payload.submissionId ]: Object.assign({}, state[ action.payload.submissionId ], { + sections: Object.assign({}, state[ action.payload.submissionId ].sections, + Object.assign({}, { + [ action.payload.sectionId ]: Object.assign({}, state[ action.payload.submissionId ].sections [ action.payload.sectionId ], { + isValid: action.payload.status + }) + }) + ) + }) + }); + } else { + return state; + } +} + +// ------ Upload file functions ------ // + +/** + * Set a new bitstream. + * + * @param state + * the current state + * @param action + * a NewUploadedFileAction action + * @return SubmissionObjectState + * the new state, with the new bitstream. + */ +function newFile(state: SubmissionObjectState, action: NewUploadedFileAction): SubmissionObjectState { + const filesData = state[ action.payload.submissionId ].sections[ action.payload.sectionId ].data as WorkspaceitemSectionUploadObject; + if (isNotUndefined(filesData.files) + && !hasValue(filesData.files[ action.payload.fileId ])) { + const newData = []; + newData[ action.payload.fileId ] = action.payload.data; + return Object.assign({}, state, { + [ action.payload.submissionId ]: Object.assign({}, state[ action.payload.submissionId ], { + activeSection: state[ action.payload.submissionId ].activeSection, + sections: Object.assign({}, state[ action.payload.submissionId ].sections, + Object.assign({}, { + [ action.payload.sectionId ]: { + data: Object.assign({}, state[ action.payload.submissionId ].sections[ action.payload.sectionId ].data, { + files: Object.assign({}, + filesData.files, + newData) + }), + isValid: state[ action.payload.submissionId ].sections[ action.payload.sectionId ].isValid, + errors: state[ action.payload.submissionId ].sections[ action.payload.sectionId ].errors + } + } + ) + ), + isLoading: state[ action.payload.submissionId ].isLoading, + savePending: state[ action.payload.submissionId ].savePending, + }) + }); + } else { + const newData = []; + newData[ action.payload.fileId ] = action.payload.data; + return Object.assign({}, state, { + [ action.payload.submissionId ]: Object.assign({}, state[ action.payload.submissionId ], { + activeSection: state[ action.payload.submissionId ].activeSection, + sections: Object.assign({}, state[ action.payload.submissionId ].sections, + Object.assign({}, { + [ action.payload.sectionId ]: { + data: Object.assign({}, state[ action.payload.submissionId ].sections[ action.payload.sectionId ].data, { + files: newData + }), + isValid: state[ action.payload.submissionId ].sections[ action.payload.sectionId ].isValid, + errors: state[ action.payload.submissionId ].sections[ action.payload.sectionId ].errors + } + }) + ), + isLoading: state[ action.payload.submissionId ].isLoading, + savePending: state[ action.payload.submissionId ].savePending, + }) + }); + } +} + +/** + * Edit a bitstream. + * + * @param state + * the current state + * @param action + * a EditFileDataAction action + * @return SubmissionObjectState + * the new state, with the edited bitstream. + */ +function editFileData(state: SubmissionObjectState, action: EditFileDataAction): SubmissionObjectState { + const filesData = state[ action.payload.submissionId ].sections[ action.payload.sectionId ].data as WorkspaceitemSectionUploadObject; + if (hasValue(filesData.files)) { + const fileIndex = findKey( + filesData.files, + { uuid: action.payload.fileId }); + if (isNotNull(fileIndex)) { + return Object.assign({}, state, { + [ action.payload.submissionId ]: Object.assign({}, state[ action.payload.submissionId ], { + activeSection: state[ action.payload.submissionId ].activeSection, + sections: Object.assign({}, state[ action.payload.submissionId ].sections, + Object.assign({}, { + [ action.payload.sectionId ]: Object.assign({}, state[ action.payload.submissionId ].sections [ action.payload.sectionId ], { + data: Object.assign({}, state[ action.payload.submissionId ].sections[ action.payload.sectionId ].data, { + files: Object.assign({}, + filesData.files, { + [ fileIndex ]: action.payload.data + }) + }) + }) + }) + // Object.assign({}, state[action.payload.submissionId].sections[action.payload.sectionId],{ + // [ action.payload.sectionId ]: { + // data: Object.assign({}, state[ action.payload.submissionId ].sections[ action.payload.sectionId ].data, { + // files: Object.assign({}, + // filesData.files, { + // [ fileIndex ]: action.payload.data + // }) + // }) + // } + // } + // ) + ), + isLoading: state[ action.payload.submissionId ].isLoading, + savePending: state[ action.payload.submissionId ].savePending, + }) + }); + } + } + return state; +} + +/** + * Delete a bitstream. + * + * @param state + * the current state + * @param action + * a DeleteUploadedFileAction action + * @return SubmissionObjectState + * the new state, with the bitstream removed. + */ +function deleteFile(state: SubmissionObjectState, action: DeleteUploadedFileAction): SubmissionObjectState { + const filesData = state[ action.payload.submissionId ].sections[ action.payload.sectionId ].data as WorkspaceitemSectionUploadObject; + if (hasValue(filesData.files)) { + const fileIndex = findKey( + filesData.files, + {uuid: action.payload.fileId}); + if (isNotNull(fileIndex)) { + return Object.assign({}, state, { + [ action.payload.submissionId ]: Object.assign({}, state[action.payload.submissionId], { + activeSection: state[ action.payload.submissionId ].activeSection, + sections: Object.assign({}, state[action.payload.submissionId].sections, + Object.assign({}, { + [ action.payload.sectionId ]: Object.assign({}, state[ action.payload.submissionId ].sections [ action.payload.sectionId ], { + data: Object.assign({}, state[ action.payload.submissionId ].sections[ action.payload.sectionId ].data, { + files: deleteProperty(filesData.files, fileIndex) + }) + }) + }) + // Object.assign({}, state[action.payload.submissionId].sections[action.payload.sectionId], { + // [ action.payload.sectionId ]: { + // data: Object.assign({}, state[action.payload.submissionId].sections[action.payload.sectionId].data, { + // files: deleteProperty(filesData.files, fileIndex) + // }) + // } + // } + // ) + ), + isLoading: state[action.payload.submissionId].isLoading, + savePending: state[action.payload.submissionId].savePending, + }) + }); + } + } + return state; +} + +/** + * Update a Workspace deduplication match. + * + * @param state + * the current state + * @param action + * a SetWorkspaceDuplicatedAction or SetWorkflowDuplicatedAction + * @return SubmissionObjectState + * the new state, with the match parameter changed. + */ +function updateDeduplication(state: SubmissionObjectState, action: SetWorkspaceDuplicatedAction|SetWorkflowDuplicatedAction): SubmissionObjectState { + const matches = Object.assign([], (state[(action.payload as any).submissionId].sections.deduplication.data as any).matches); + const newMatch = (action.payload as any).data; + matches.forEach( (match, i) => { + if (i === action.payload.index) { + matches.splice(i, 1, Object.assign({}, match, newMatch)); + return; + } + }); + // const updatedMatches = Object.assign({}, matches, newMatch); + return Object.assign({}, state, {[(action.payload as any).submissionId]: {sections: {deduplication: {data: {matches}}}}}); +} diff --git a/src/app/submission/sections/container/section-container.component.html b/src/app/submission/sections/container/section-container.component.html new file mode 100644 index 0000000000..898a4e5596 --- /dev/null +++ b/src/app/submission/sections/container/section-container.component.html @@ -0,0 +1,47 @@ +
+ + + + {{ 'submission.sections.'+sectionData.header | translate }} +
+ + + + + + + + + +
+
+ +
+ +
+
+ +
+
+
+
+
diff --git a/src/app/submission/sections/container/section-container.component.scss b/src/app/submission/sections/container/section-container.component.scss new file mode 100644 index 0000000000..f71765be88 --- /dev/null +++ b/src/app/submission/sections/container/section-container.component.scss @@ -0,0 +1,11 @@ +@import '../../../../styles/variables'; + +:host /deep/ .card { + margin-bottom: 0.5rem; +} + +.section-focus { + // box-shadow: $btn-focus-box-shadow; + border-radius: 0.25rem; + box-shadow: $btn-focus-box-shadow; +} diff --git a/src/app/submission/sections/container/section-container.component.ts b/src/app/submission/sections/container/section-container.component.ts new file mode 100644 index 0000000000..e97d348781 --- /dev/null +++ b/src/app/submission/sections/container/section-container.component.ts @@ -0,0 +1,52 @@ +import { Component, Injector, Input, OnInit, ViewChild } from '@angular/core'; + +import { Store } from '@ngrx/store'; + +import { SectionsDirective } from '../sections.directive'; +import { SectionDataObject } from '../models/section-data.model'; +import { SubmissionState } from '../../submission.reducers'; +import { rendersSectionType } from '../sections-decorator'; +import { SectionsType } from '../sections-type'; +import { AlertType } from '../../../shared/alerts/aletrs-type'; + +@Component({ + selector: 'ds-submission-form-section-container', + templateUrl: './section-container.component.html', + styleUrls: ['./section-container.component.scss'], +}) +export class SectionContainerComponent implements OnInit { + @Input() collectionId: string; + @Input() sectionData: SectionDataObject; + @Input() submissionId: string; + + public AlertTypeEnum = AlertType; + public active = true; + public objectInjector: Injector; + public sectionComponentType: SectionsType; + + @ViewChild('sectionRef') sectionRef: SectionsDirective; + + constructor(private injector: Injector, private store: Store) { + } + + ngOnInit() { + this.objectInjector = Injector.create({ + providers: [ + {provide: 'collectionIdProvider', useFactory: () => (this.collectionId), deps: []}, + {provide: 'sectionDataProvider', useFactory: () => (this.sectionData), deps: []}, + {provide: 'submissionIdProvider', useFactory: () => (this.submissionId), deps: []}, + ], + parent: this.injector + }); + } + + public removeSection(event) { + event.preventDefault(); + event.stopPropagation(); + this.sectionRef.removeSection(this.submissionId, this.sectionData.id); + } + + getSectionContent(): string { + return rendersSectionType(this.sectionData.sectionType); + } +} diff --git a/src/app/submission/sections/deduplication/deduplication.service.ts b/src/app/submission/sections/deduplication/deduplication.service.ts new file mode 100644 index 0000000000..5b0a029f2a --- /dev/null +++ b/src/app/submission/sections/deduplication/deduplication.service.ts @@ -0,0 +1,51 @@ +import { Store } from '@ngrx/store'; +import { SubmissionState } from '../../submission.reducers'; +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs/Observable'; +import { HttpHeaders } from '@angular/common/http'; +import { HttpOptions } from '../../../core/dspace-rest-v2/dspace-rest-v2.service'; + +@Injectable() +export class DeduplicationService { + + constructor(private store: Store) { + } + + // setWorkspaceDuplicated(payload: any): Observable { + // const options: HttpOptions = Object.create({}); + // let headers = new HttpHeaders(); + // headers = headers.append('Content-Type', 'application/json'); + // options.headers = headers; + // // TODO REST CALL + // // return this.restService.postToEndpoint('workspace/deduplication', payload, null, options); + // return Observable.of(payload); + // } + + setWorkspaceDuplicationSuccess(payload: any): void { + // TODO + + } + + setWorkspaceDuplicationError(payload: any): void { + // TODO + } + + // setWorkflowDuplicated(payload: any): Observable { + // const options: HttpOptions = Object.create({}); + // let headers = new HttpHeaders(); + // headers = headers.append('Content-Type', 'application/json'); + // options.headers = headers; + // // TODO REST CALL + // // return this.restService.postToEndpoint('workflow/deduplication', payload, null, options); + // return Observable.of(payload); + // } + + setWorkflowDuplicationSuccess(payload: any): void { + // TODO Update the redux store + + } + + setWorkflowDuplicationError(payload: any): void { + // TODO Update the redux store + } +} diff --git a/src/app/submission/sections/deduplication/match/deduplication-match.component.html b/src/app/submission/sections/deduplication/match/deduplication-match.component.html new file mode 100644 index 0000000000..2ec4385ddf --- /dev/null +++ b/src/app/submission/sections/deduplication/match/deduplication-match.component.html @@ -0,0 +1,89 @@ + + + + + + + + + + + + + +
+ +
+ + + + + + + + +
+ + + +
+ +
+ +
+ + + + + + + diff --git a/src/app/submission/sections/deduplication/match/deduplication-match.component.ts b/src/app/submission/sections/deduplication/match/deduplication-match.component.ts new file mode 100644 index 0000000000..a85239ef5c --- /dev/null +++ b/src/app/submission/sections/deduplication/match/deduplication-match.component.ts @@ -0,0 +1,169 @@ +import { Component, Input, OnInit } from '@angular/core'; +import { Item } from '../../../../core/shared/item.model'; +import { DeduplicationSchema } from '../../../../core/submission/models/workspaceitem-section-deduplication.model'; +import { SubmissionService } from '../../../submission.service'; +import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { Store } from '@ngrx/store'; +import { SubmissionState } from '../../../submission.reducers'; +import { DeduplicationService } from '../deduplication.service'; +import { JsonPatchOperationsBuilder } from '../../../../core/json-patch/builder/json-patch-operations-builder'; +import { JsonPatchOperationPathCombiner } from '../../../../core/json-patch/builder/json-patch-operation-path-combiner'; +import { TranslateService } from '@ngx-translate/core'; +import { Observable } from 'rxjs/Observable'; +import { SubmissionScopeType } from '../../../../core/submission/submission-scope-type'; + +@Component({ + selector: 'ds-deduplication-match', + templateUrl: 'deduplication-match.component.html', +}) + +export class DeduplicationMatchComponent implements OnInit { + @Input() + sectionId: string; + @Input() + match: DeduplicationSchema; + @Input() + submissionId: string; + @Input() + index: string; + + object = {hitHighlights: []}; + item: Item; + isWorkFlow = false; + showSubmitterDecision = false; + submitterDecisionTxt: string; + + decidedYet: boolean; + + closeResult: string; // for modal + rejectForm: FormGroup; + modalRef: NgbModalRef; + pathCombiner: JsonPatchOperationPathCombiner; + + duplicatedBtnLabel: Observable; + submitterDecisionLabel: Observable; + + constructor(private deduplicationService: DeduplicationService, + private submissionService: SubmissionService, + private modalService: NgbModal, + private formBuilder: FormBuilder, + private store: Store, + protected operationsBuilder: JsonPatchOperationsBuilder, + private translate: TranslateService) { + } + + ngOnInit(): void { + if ((this.match.matchObject as any).item) { + // WSI & WFI + this.item = Object.assign(new Item(), (this.match.matchObject as any).item); + } else { + // Item + this.item = Object.assign(new Item(), this.match.matchObject); + } + + this.isWorkFlow = this.submissionService.getSubmissionScope() === SubmissionScopeType.WorkflowItem; + + this.rejectForm = this.formBuilder.group({ + reason: ['', Validators.required] + }); + + this.decidedYet = this.isWorkFlow ? + this.match.workflowDecision !== null ? true : false + : this.match.submitterDecision !== null ? true : false; + + if (this.match.submitterDecision) { + if (this.match.submitterDecision === 'verify') { + this.submitterDecisionTxt = 'It\'s a duplicate'; + } else { + this.submitterDecisionTxt = 'It\'s not a duplicate'; + } + } else { + this.submitterDecisionTxt = 'Not decided'; + } + + this.pathCombiner = new JsonPatchOperationPathCombiner('sections', this.sectionId, 'matches', this.index); + + this.duplicatedBtnLabel = this.isWorkFlow ? + this.translate.get('submission.sections.deduplication.duplicated_ctrl') + : this.translate.get('submission.sections.deduplication.duplicated'); + + this.submitterDecisionLabel = this.isWorkFlow ? + this.translate.get('submission.sections.deduplication.submitter_decision') + : this.translate.get('submission.sections.deduplication.your_decision'); + + } + + setAsDuplicated() { + console.log('Setting item #' + this.item.uuid + ' as duplicated...'); + this.dispatchAction(true); + this.modalRef.dismiss(); + } + + setAsNotDuplicated() { + console.log('Setting item #' + this.item.uuid + ' as not duplicated...'); + this.dispatchAction(false); + } + + clearDecision() { + console.log('Clearing item #' + this.item.uuid + ' from previous decision...'); + + } + + private dispatchAction(duplicated: boolean, clear?: boolean): void { + const payload = { + submissionId: this.submissionId, + index: this.index, + data: {} as DeduplicationSchema + }; + + // Call workflow action + const decision = clear ? null : duplicated ? 'verify' : 'reject'; + const pathDecision = this.isWorkFlow ? 'workflowDecision' : 'submitterDecision'; + this.operationsBuilder.add(this.pathCombiner.getPath(pathDecision), decision, false, true); + + if (!clear && duplicated) { + const note = this.rejectForm.get('reason').value; + const pathNote = this.isWorkFlow ? 'workflowNote' : 'submitterNote'; + this.operationsBuilder.add(this.pathCombiner.getPath(pathNote), note, false, true); + } + + // const now = new Date(); + // const time = now.getUTCFullYear() + '/' + now.getUTCMonth() + 1 + '/' + now.getDay(); + + // if (this.isWorkFlow) { + // // Call workflow action + // payload.data.workflowDecision = clear ? null : duplicated ? 'verify' : 'reject'; + // // payload.data.workflowTime = time; + // if (!clear && duplicated) { + // const note = this.rejectForm.get('reason').value; + // payload.data.workflowNote = note; + // } + // // Dispatch WorkFLOW action + // // this.store.dispatch(new SetWorkflowDuplicatedAction(payload)); + // const path = 'workflowDecision' + // this.operationsBuilder.add(this.pathCombiner.getPath(path), payload.data.workflowDecision, false, true); + // + // } else { + // // Call workspace action + // payload.data.submitterDecision = clear ? null : duplicated ? 'verify' : 'reject'; + // // payload.data.submitterTime = time; + // if (!clear && duplicated) { + // const note = this.rejectForm.get('reason').value; + // payload.data.submitterNote = note; + // } + // // Dispatch workSPACE action + // this.store.dispatch(new SetWorkspaceDuplicatedAction(payload)); + // } + } + + toggleSubmitterDecision() { + this.showSubmitterDecision = !this.showSubmitterDecision; + } + + openModal(modal) { + this.rejectForm.reset(); + this.modalRef = this.modalService.open(modal); + } + +} diff --git a/src/app/submission/sections/deduplication/section-deduplication.component.html b/src/app/submission/sections/deduplication/section-deduplication.component.html new file mode 100644 index 0000000000..942d8a75a6 --- /dev/null +++ b/src/app/submission/sections/deduplication/section-deduplication.component.html @@ -0,0 +1,39 @@ + + + +
+
+

No duplicated yet.

+
+
+
+ + + + + + +
    +
  • + + +
  • +
+
+
diff --git a/src/app/submission/sections/deduplication/section-deduplication.component.ts b/src/app/submission/sections/deduplication/section-deduplication.component.ts new file mode 100644 index 0000000000..3d08ccc5c3 --- /dev/null +++ b/src/app/submission/sections/deduplication/section-deduplication.component.ts @@ -0,0 +1,73 @@ +import { SectionsType } from '../sections-type'; +import { ChangeDetectionStrategy, Component, Inject, OnInit } from '@angular/core'; +import { SectionModelComponent } from '../models/section.model'; +import { renderSectionFor } from '../sections-decorator'; +import { SectionDataObject } from '../models/section-data.model'; +import { SubmissionState } from '../../submission.reducers'; +import { Store } from '@ngrx/store'; +import { SortDirection, SortOptions } from '../../../core/cache/models/sort-options.model'; +import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model'; +import { submissionSectionDataFromIdSelector } from '../../selectors'; +import { Observable } from 'rxjs/Observable'; +import { isNotEmpty } from '../../../shared/empty.util'; +import { TranslateService } from '@ngx-translate/core'; +import { SubmissionService } from '../../submission.service'; +import { SubmissionScopeType } from '../../../core/submission/submission-scope-type'; + +@Component({ + selector: 'ds-deduplication-section', + // styleUrls: ['./section-deduplication.component.scss'], + templateUrl: './section-deduplication.component.html', + changeDetection: ChangeDetectionStrategy.Default +}) + +@renderSectionFor(SectionsType.Deduplication) +export class DeduplicationSectionComponent extends SectionModelComponent implements OnInit { + public isLoading = true; + public sectionDataObs: Observable; + public matches = []; + + config: PaginationComponentOptions; + sortConfig: SortOptions; + + isWorkFlow = false; + disclaimer: Observable; + + constructor(protected store: Store, + private translate: TranslateService, + private submissionService: SubmissionService, + @Inject('collectionIdProvider') public injectedCollectionId: string, + @Inject('sectionDataProvider') public injectedSectionData: SectionDataObject, + @Inject('submissionIdProvider') public injectedSubmissionId: string) { + super(injectedCollectionId, injectedSectionData, injectedSubmissionId); + } + + ngOnInit() { + this.config = new PaginationComponentOptions(); + this.config.id = 'duplicated_items'; + this.config.pageSize = 2; + this.sortConfig = new SortOptions('dc.title', SortDirection.ASC); + + this.sectionDataObs = this.store.select(submissionSectionDataFromIdSelector(this.submissionId, this.sectionData.id)) + .filter((sd) => isNotEmpty(sd)) + .startWith({matches: []}) + .distinctUntilChanged() + .map((sd) => { + return sd; + }); + + this.isWorkFlow = this.submissionService.getSubmissionScope() === SubmissionScopeType.WorkflowItem; + + this.disclaimer = this.isWorkFlow ? + this.translate.get('submission.sections.deduplication.disclaimer_ctrl') + : this.translate.get('submission.sections.deduplication.disclaimer'); + + this.isLoading = false; + } + + setPage(page) { + console.log('Select page #', page); + this.config.currentPage = page; + } + +} diff --git a/src/app/submission/sections/default/section-default.component.html b/src/app/submission/sections/default/section-default.component.html new file mode 100644 index 0000000000..62f5d179c7 --- /dev/null +++ b/src/app/submission/sections/default/section-default.component.html @@ -0,0 +1,6 @@ +Lorem ipsum dolor sit amet, consectetur adipisci elit, sed eiusmod tempor incidunt ut labore et dolore + magna aliqua. Ut enim ad minim veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut + aliquid ex ea commodi consequatur. Quis aute iure reprehenderit in voluptate velit esse cillum dolore eu fugiat + nulla pariatur. Excepteur sint obcaecat cupiditat non proident, sunt in culpa qui officia deserunt + mollit anim id est laborum. + diff --git a/src/app/submission/sections/default/section-default.component.scss b/src/app/submission/sections/default/section-default.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/submission/sections/default/section-default.component.ts b/src/app/submission/sections/default/section-default.component.ts new file mode 100644 index 0000000000..2a40ecb9a0 --- /dev/null +++ b/src/app/submission/sections/default/section-default.component.ts @@ -0,0 +1,21 @@ +import { Component, Inject } from '@angular/core'; +import { SectionModelComponent } from '../models/section.model'; +import { JsonPatchOperationsBuilder } from '../../../core/json-patch/builder/json-patch-operations-builder'; +import { SectionDataObject } from '../models/section-data.model'; + +@Component({ + selector: 'ds-submission-section-default', + styleUrls: ['./section-default.component.scss'], + templateUrl: './section-default.component.html', +}) +export class DefaultSectionComponent extends SectionModelComponent { + + protected operationsBuilder: JsonPatchOperationsBuilder; + + constructor(@Inject('collectionIdProvider') public injectedCollectionId: string, + @Inject('sectionDataProvider') public injectedSectionData: SectionDataObject, + @Inject('submissionIdProvider') public injectedSubmissionId: string) { + super(injectedCollectionId, injectedSectionData, injectedSubmissionId); + } + +} diff --git a/src/app/submission/sections/form/form-operations.service.ts b/src/app/submission/sections/form/form-operations.service.ts new file mode 100644 index 0000000000..2ba57e8657 --- /dev/null +++ b/src/app/submission/sections/form/form-operations.service.ts @@ -0,0 +1,242 @@ +import { Injectable } from '@angular/core'; + +import { isEqual, isObject } from 'lodash'; +import { + DYNAMIC_FORM_CONTROL_TYPE_ARRAY, + DYNAMIC_FORM_CONTROL_TYPE_GROUP, + DynamicFormArrayGroupModel, + DynamicFormControlEvent, + DynamicFormControlModel +} from '@ng-dynamic-forms/core'; + +import { isNotEmpty, isNotNull, isNotUndefined, isNull, isUndefined } from '../../../shared/empty.util'; +import { JsonPatchOperationPathCombiner } from '../../../core/json-patch/builder/json-patch-operation-path-combiner'; +import { FormFieldPreviousValueObject } from '../../../shared/form/builder/models/form-field-previous-value-object'; +import { JsonPatchOperationsBuilder } from '../../../core/json-patch/builder/json-patch-operations-builder'; +import { FormFieldLanguageValueObject } from '../../../shared/form/builder/models/form-field-language-value.model'; +import { DsDynamicInputModel } from '../../../shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-input.model'; +import { AuthorityValueModel } from '../../../core/integration/models/authority-value.model'; +import { FormBuilderService } from '../../../shared/form/builder/form-builder.service'; +import { FormFieldMetadataValueObject } from '../../../shared/form/builder/models/form-field-metadata-value.model'; +import { DynamicQualdropModel } from '../../../shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-qualdrop.model'; +import { DynamicGroupModel } from '../../../shared/form/builder/ds-dynamic-form-ui/models/dynamic-group/dynamic-group.model'; + +@Injectable() +export class FormOperationsService { + + constructor(private formBuilder: FormBuilderService, private operationsBuilder: JsonPatchOperationsBuilder) { + } + + dispatchOperationsFromEvent(pathCombiner: JsonPatchOperationPathCombiner, + event: DynamicFormControlEvent, + previousValue: FormFieldPreviousValueObject, + hasStoredValue: boolean) { + switch (event.type) { + case 'remove': + this.dispatchOperationsFromRemoveEvent(pathCombiner, event, previousValue); + break; + case 'change': + this.dispatchOperationsFromChangeEvent(pathCombiner, event, previousValue, hasStoredValue); + break; + default: + break; + } + } + + getArrayIndexFromEvent(event: DynamicFormControlEvent) { + let fieldIndex: number; + if (isNotEmpty(event)) { + if (isNull(event.context)) { + if (isNotNull(event.model.parent)) { + if ((event.model.parent as any).type === DYNAMIC_FORM_CONTROL_TYPE_GROUP) { + if ((event.model.parent as any).parent) { + if ((event.model.parent as any).parent.context) { + if ((event.model.parent as any).parent.context.type === DYNAMIC_FORM_CONTROL_TYPE_ARRAY) { + fieldIndex = (event.model.parent as any).parent.index; + } + } + } + } + } + } else { + fieldIndex = event.context.index; + } + } + return isNotUndefined(fieldIndex) ? fieldIndex : 0; + } + + public getComboboxMap(event): Map { + const metadataValueMap = new Map(); + + (event.model.parent.parent as DynamicFormArrayGroupModel).context.groups.forEach((arrayModel: DynamicFormArrayGroupModel) => { + const groupModel = arrayModel.group[0] as DynamicQualdropModel; + const metadataValueList = metadataValueMap.get(groupModel.qualdropId) ? metadataValueMap.get(groupModel.qualdropId) : []; + if (groupModel.value) { + metadataValueList.push(groupModel.value); + metadataValueMap.set(groupModel.qualdropId, metadataValueList); + } + }); + + return metadataValueMap; + } + + public getFieldPathFromChangeEvent(event: DynamicFormControlEvent) { + const fieldIndex = this.getArrayIndexFromEvent(event); + const fieldId = this.getFieldPathSegmentedFromChangeEvent(event); + return (isNotUndefined(fieldIndex)) ? fieldId + '/' + fieldIndex : fieldId; + } + + public getFieldPathSegmentedFromChangeEvent(event: DynamicFormControlEvent) { + let fieldId; + if (this.formBuilder.isQualdropGroup(event.model.parent as DynamicFormControlModel)) { + fieldId = (event.model.parent as any).qualdropId; + } else { + fieldId = this.formBuilder.getId(event.model); + } + return fieldId; + } + + public getFieldValueFromChangeEvent(event: DynamicFormControlEvent) { + let fieldValue; + const value = (event.model as any).value; + + if (this.formBuilder.isModelInCustomGroup(event.model)) { + fieldValue = (event.model.parent as any).value; + } else if (this.formBuilder.isRelationGroup(event.model)) { + fieldValue = (event.model as DynamicGroupModel).getGroupValue(); + } else if ((event.model as any).hasLanguages) { + const language = (event.model as any).language; + if ((event.model as DsDynamicInputModel).hasAuthority) { + if (Array.isArray(value)) { + value.forEach((authority, index) => { + authority = Object.assign(new AuthorityValueModel(), authority, {language}); + value[index] = authority; + }); + fieldValue = value; + } else { + fieldValue = Object.assign(new AuthorityValueModel(), value, {language}); + } + } else { + // Language without Authority (input, textArea) + fieldValue = new FormFieldMetadataValueObject(value, language); + } + } else if (value instanceof FormFieldLanguageValueObject || value instanceof AuthorityValueModel || isObject(value)) { + fieldValue = value; + } else { + fieldValue = new FormFieldMetadataValueObject(value); + } + + return fieldValue; + } + + public getValueMap(items: any[]): Map { + const metadataValueMap = new Map(); + + items.forEach((item) => { + Object.keys(item) + .forEach((key) => { + const metadataValueList = metadataValueMap.get(key) ? metadataValueMap.get(key) : []; + metadataValueList.push(item[key]); + metadataValueMap.set(key, metadataValueList); + }); + + }); + return metadataValueMap; + } + + protected dispatchOperationsFromRemoveEvent(pathCombiner: JsonPatchOperationPathCombiner, + event: DynamicFormControlEvent, + previousValue: FormFieldPreviousValueObject) { + const path = this.getFieldPathFromChangeEvent(event); + const value = this.getFieldValueFromChangeEvent(event); + if (this.formBuilder.isQualdropGroup(event.model.parent as DynamicFormControlModel)) { + this.dispatchOperationsFromMap(this.getComboboxMap(event), pathCombiner, event, previousValue); + } else if (isNotEmpty(value)) { + this.operationsBuilder.remove(pathCombiner.getPath(path)); + } + } + + protected dispatchOperationsFromChangeEvent(pathCombiner: JsonPatchOperationPathCombiner, + event: DynamicFormControlEvent, + previousValue: FormFieldPreviousValueObject, + hasStoredValue: boolean) { + const path = this.getFieldPathFromChangeEvent(event); + const segmentedPath = this.getFieldPathSegmentedFromChangeEvent(event); + const value = this.getFieldValueFromChangeEvent(event); + // Detect which operation must be dispatched + if (this.formBuilder.isQualdropGroup(event.model.parent as DynamicFormControlModel)) { + // It's a qualdrup model + this.dispatchOperationsFromMap(this.getComboboxMap(event), pathCombiner, event, previousValue); + } else if (this.formBuilder.isRelationGroup(event.model)) { + // It's a relation model + this.dispatchOperationsFromMap(this.getValueMap(value), pathCombiner, event, previousValue); + } else if (this.formBuilder.hasArrayGroupValue(event.model)) { + // Model has as value an array, so dispatch an add operation with entire block of values + this.operationsBuilder.add( + pathCombiner.getPath(segmentedPath), + value, true); + } else if (previousValue.isPathEqual(this.formBuilder.getPath(event.model)) || hasStoredValue) { + // Here model has a previous value changed or stored in the server + if (!value.hasValue()) { + // New value is empty, so dispatch a remove operation + if (this.getArrayIndexFromEvent(event) === 0) { + this.operationsBuilder.remove(pathCombiner.getPath(segmentedPath)); + } else { + this.operationsBuilder.remove(pathCombiner.getPath(path)); + } + } else { + // New value is not equal from the previous one, so dispatch a replace operation + this.operationsBuilder.replace( + pathCombiner.getPath(path), + value); + } + previousValue.delete(); + } else if (value.hasValue()) { + // Here model has no previous value but a new one + if (isUndefined(this.getArrayIndexFromEvent(event)) + || this.getArrayIndexFromEvent(event) === 0) { + // Model is single field or is part of an array model but is the first item, + // so dispatch an add operation that initialize the values of a specific metadata + this.operationsBuilder.add( + pathCombiner.getPath(segmentedPath), + value, true); + } else { + // Model is part of an array model but is not the first item, + // so dispatch an add operation that add a value to an existent metadata + this.operationsBuilder.add( + pathCombiner.getPath(path), + value); + } + } + } + + protected dispatchOperationsFromMap(valueMap: Map, + pathCombiner: JsonPatchOperationPathCombiner, + event: DynamicFormControlEvent, + previousValue: FormFieldPreviousValueObject) { + const currentValueMap = valueMap; + if (previousValue.isPathEqual(this.formBuilder.getPath(event.model))) { + previousValue.value.forEach((entry, index) => { + const currentValue = currentValueMap.get(index); + if (currentValue) { + if (!isEqual(entry, currentValue)) { + this.operationsBuilder.add(pathCombiner.getPath(index), currentValue, true); + } + currentValueMap.delete(index); + } else if (!currentValue) { + this.operationsBuilder.remove(pathCombiner.getPath(index)); + } + }); + } + currentValueMap.forEach((entry: any[], index) => { + if (entry.length === 1 && isNull(entry[0])) { + // The last item of the group has been deleted so make a remove op + this.operationsBuilder.remove(pathCombiner.getPath(index)); + } else { + this.operationsBuilder.add(pathCombiner.getPath(index), entry, true); + } + }); + + previousValue.delete(); + } +} diff --git a/src/app/submission/sections/form/section-form.component.html b/src/app/submission/sections/form/section-form.component.html new file mode 100644 index 0000000000..166e52675b --- /dev/null +++ b/src/app/submission/sections/form/section-form.component.html @@ -0,0 +1,9 @@ + + diff --git a/src/app/submission/sections/form/section-form.component.scss b/src/app/submission/sections/form/section-form.component.scss new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/src/app/submission/sections/form/section-form.component.scss @@ -0,0 +1 @@ + diff --git a/src/app/submission/sections/form/section-form.component.ts b/src/app/submission/sections/form/section-form.component.ts new file mode 100644 index 0000000000..b5dc2c3dee --- /dev/null +++ b/src/app/submission/sections/form/section-form.component.ts @@ -0,0 +1,248 @@ +import { ChangeDetectorRef, Component, Inject, OnDestroy, ViewChild } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { DynamicFormControlEvent, DynamicFormControlModel } from '@ng-dynamic-forms/core'; + +import { isEqual } from 'lodash'; + +import { FormBuilderService } from '../../../shared/form/builder/form-builder.service'; +import { FormComponent } from '../../../shared/form/form.component'; +import { FormService } from '../../../shared/form/form.service'; +import { SaveSubmissionFormAction, SectionStatusChangeAction, } from '../../objects/submission-objects.actions'; +import { SectionModelComponent } from '../models/section.model'; +import { SubmissionState } from '../../submission.reducers'; +import { SubmissionFormsConfigService } from '../../../core/config/submission-forms-config.service'; +import { hasValue, isNotEmpty, isNotUndefined, isUndefined } from '../../../shared/empty.util'; +import { ConfigData } from '../../../core/config/config-data'; +import { JsonPatchOperationPathCombiner } from '../../../core/json-patch/builder/json-patch-operation-path-combiner'; +import { submissionSectionDataFromIdSelector, submissionSectionFromIdSelector } from '../../selectors'; +import { SubmissionFormsModel } from '../../../core/shared/config/config-submission-forms.model'; +import { SubmissionSectionError, SubmissionSectionObject } from '../../objects/submission-objects.reducer'; +import { FormFieldPreviousValueObject } from '../../../shared/form/builder/models/form-field-previous-value-object'; +import { WorkspaceitemSectionDataType } from '../../../core/submission/models/workspaceitem-sections.model'; +import { Subscription } from 'rxjs/Subscription'; +import { GLOBAL_CONFIG } from '../../../../config'; +import { GlobalConfig } from '../../../../config/global-config.interface'; +import { SectionDataObject } from '../models/section-data.model'; +import { renderSectionFor } from '../sections-decorator'; +import { SectionsType } from '../sections-type'; +import { SubmissionService } from '../../submission.service'; +import { FormOperationsService } from './form-operations.service'; +import { NotificationsService } from '../../../shared/notifications/notifications.service'; +import { TranslateService } from '@ngx-translate/core'; +import { SectionsService } from '../sections.service'; +import { difference } from '../../../shared/object.util'; + +@Component({ + selector: 'ds-submission-section-form', + styleUrls: ['./section-form.component.scss'], + templateUrl: './section-form.component.html', +}) +@renderSectionFor(SectionsType.SubmissionForm) +export class FormSectionComponent extends SectionModelComponent implements OnDestroy { + + public formId; + public formModel: DynamicFormControlModel[]; + public isUpdating = false; + public isLoading = true; + + protected formConfig: SubmissionFormsModel; + protected formData: any = Object.create({}); + protected pathCombiner: JsonPatchOperationPathCombiner; + protected previousValue: FormFieldPreviousValueObject = new FormFieldPreviousValueObject(); + protected subs: Subscription[] = []; + + @ViewChild('formRef') private formRef: FormComponent; + + constructor(protected cdr: ChangeDetectorRef, + protected formBuilderService: FormBuilderService, + protected formOperationsService: FormOperationsService, + protected formService: FormService, + protected formConfigService: SubmissionFormsConfigService, + protected notificationsService: NotificationsService, + protected store: Store, + protected sectionService: SectionsService, + protected submissionService: SubmissionService, + protected translate: TranslateService, + @Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig, + @Inject('collectionIdProvider') public injectedCollectionId: string, + @Inject('sectionDataProvider') public injectedSectionData: SectionDataObject, + @Inject('submissionIdProvider') public injectedSubmissionId: string) { + super(injectedCollectionId, injectedSectionData, injectedSubmissionId); + } + + ngOnInit() { + this.pathCombiner = new JsonPatchOperationPathCombiner('sections', this.sectionData.id); + this.formConfigService.getConfigByHref(this.sectionData.config) + .flatMap((config: ConfigData) => config.payload) + .subscribe((config: SubmissionFormsModel) => { + this.formConfig = config; + this.formId = this.formService.getUniqueId(this.sectionData.id); + this.store.select(submissionSectionDataFromIdSelector(this.submissionId, this.sectionData.id)) + .take(1) + .subscribe((sectionData: WorkspaceitemSectionDataType) => { + if (isUndefined(this.formModel)) { + this.sectionData.errors = []; + // Is the first loading so init form + this.initForm(sectionData); + this.sectionData.data = sectionData; + this.subscriptions(); + this.isLoading = false; + this.cdr.detectChanges(); + } + }) + }); + } + + ngOnDestroy() { + this.subs + .filter((subscription) => hasValue(subscription)) + .forEach((subscription) => subscription.unsubscribe()); + } + + hasMetadataEnrichment(sectionData): boolean { + const diffResult = []; + + // compare current form data state with section data retrieved from store + const diffObj = difference(sectionData, this.formData); + + // iterate over differences to check whether they are actually different + Object.keys(diffObj) + .forEach((key) => { + diffObj[key].forEach((value) => { + if (value.hasOwnProperty('value')) { + diffResult.push(value); + } + }); + }); + return isNotEmpty(diffResult); + } + + initForm(sectionData: WorkspaceitemSectionDataType) { + try { + this.formModel = this.formBuilderService.modelFromConfiguration( + this.formConfig, + this.collectionId, + sectionData, + this.submissionService.getSubmissionScope()); + } catch (e) { + this.translate.get('error.submission.sections.init-form-error') + .subscribe((msg) => { + const sectionError: SubmissionSectionError = { + message: msg + e.toString(), + path: '/sections/' + this.sectionData.id + }; + this.sectionService.setSectionError(this.submissionId, this.sectionData.id, [sectionError]) + }) + + } + } + + updateForm(sectionData: WorkspaceitemSectionDataType, errors: SubmissionSectionError[]) { + + if (isNotEmpty(sectionData) && !isEqual(sectionData, this.sectionData.data) && this.hasMetadataEnrichment(sectionData)) { + this.translate.get('submission.sections.general.metadata-extracted', {sectionId: this.sectionData.id}) + .take(1) + .subscribe((m) => { + this.notificationsService.info(null, m, null, true); + }); + this.isUpdating = true; + this.formModel = null; + this.cdr.detectChanges(); + this.initForm(sectionData); + this.checksForErrors(errors); + this.sectionData.data = sectionData; + this.isUpdating = false; + this.cdr.detectChanges(); + } else if (isNotEmpty(errors) || isNotEmpty(this.sectionData.errors)) { + this.checksForErrors(errors); + } + + } + + checksForErrors(errors: SubmissionSectionError[]) { + this.formService.isFormInitialized(this.formId) + .filter((status: boolean) => status === true && !this.isUpdating) + .take(1) + .subscribe(() => { + this.sectionService.checkSectionErrors(this.submissionId, this.sectionData.id, this.formId, errors, this.sectionData.errors); + this.sectionData.errors = errors; + this.cdr.detectChanges(); + }); + } + + subscriptions() { + this.subs.push( + /** + * Subscribe to form status + */ + this.formService.isValid(this.formId) + .filter((formValid) => isNotUndefined(formValid)) + .filter((formValid) => formValid !== this.valid) + .subscribe((formState) => { + this.valid = formState; + this.store.dispatch(new SectionStatusChangeAction(this.submissionId, this.sectionData.id, this.valid)); + }), + /** + * Subscribe to form's data + */ + this.formService.getFormData(this.formId) + .distinctUntilChanged() + .subscribe((formData) => { + this.formData = formData; + }), + /** + * Subscribe to section state + */ + this.store.select(submissionSectionFromIdSelector(this.submissionId, this.sectionData.id)) + .filter((sectionState: SubmissionSectionObject) => { + return isNotEmpty(sectionState) && (isNotEmpty(sectionState.data) || isNotEmpty(sectionState.errors)) + }) + .distinctUntilChanged() + .subscribe((sectionState: SubmissionSectionObject) => { + this.updateForm(sectionState.data, sectionState.errors); + }) + ) + } + + onChange(event: DynamicFormControlEvent) { + this.formOperationsService.dispatchOperationsFromEvent( + this.pathCombiner, + event, + this.previousValue, + this.hasStoredValue(this.formBuilderService.getId(event.model), this.formOperationsService.getArrayIndexFromEvent(event))); + const metadata = this.formOperationsService.getFieldPathSegmentedFromChangeEvent(event); + const value = this.formOperationsService.getFieldValueFromChangeEvent(event); + + if (this.EnvConfig.submission.autosave.metadata.indexOf(metadata) !== -1 && isNotEmpty(value)) { + this.store.dispatch(new SaveSubmissionFormAction(this.submissionId)); + } + } + + onFocus(event: DynamicFormControlEvent) { + const value = this.formOperationsService.getFieldValueFromChangeEvent(event); + const path = this.formBuilderService.getPath(event.model); + if (this.formBuilderService.hasMappedGroupValue(event.model)) { + this.previousValue.path = path; + this.previousValue.value = this.formOperationsService.getComboboxMap(event); + } else if (isNotEmpty(value) && ((typeof value === 'object' && isNotEmpty(value.value)) || (typeof value === 'string'))) { + this.previousValue.path = path; + this.previousValue.value = value; + } + } + + onRemove(event: DynamicFormControlEvent) { + this.formOperationsService.dispatchOperationsFromEvent( + this.pathCombiner, + event, + this.previousValue, + this.hasStoredValue(this.formBuilderService.getId(event.model), this.formOperationsService.getArrayIndexFromEvent(event))); + } + + hasStoredValue(fieldId, index) { + if (isNotEmpty(this.sectionData.data) && isNotEmpty(this.sectionData.data[index])) { + return this.sectionData.data.hasOwnProperty(fieldId); + } else { + return false; + } + } +} diff --git a/src/app/submission/sections/license/section-license.component.html b/src/app/submission/sections/license/section-license.component.html new file mode 100644 index 0000000000..5388534d04 --- /dev/null +++ b/src/app/submission/sections/license/section-license.component.html @@ -0,0 +1,7 @@ +{{ licenseText }} +

+ diff --git a/src/app/submission/sections/license/section-license.component.scss b/src/app/submission/sections/license/section-license.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/submission/sections/license/section-license.component.ts b/src/app/submission/sections/license/section-license.component.ts new file mode 100644 index 0000000000..65cb99faf6 --- /dev/null +++ b/src/app/submission/sections/license/section-license.component.ts @@ -0,0 +1,139 @@ +import { ChangeDetectorRef, Component, Inject, OnDestroy, OnInit } from '@angular/core'; +import { SectionModelComponent } from '../models/section.model'; +import { Store } from '@ngrx/store'; +import { JsonPatchOperationsBuilder } from '../../../core/json-patch/builder/json-patch-operations-builder'; +import { CollectionDataService } from '../../../core/data/collection-data.service'; +import { Subscription } from 'rxjs/Subscription'; +import { hasValue, isNotEmpty, isNotNull, isNotUndefined } from '../../../shared/empty.util'; +import { License } from '../../../core/shared/license.model'; +import { RemoteData } from '../../../core/data/remote-data'; +import { Collection } from '../../../core/shared/collection.model'; +import { DynamicCheckboxModel, DynamicFormControlEvent, DynamicFormControlModel } from '@ng-dynamic-forms/core'; +import { SECTION_LICENSE_FORM_MODEL } from './section-license.model'; +import { FormBuilderService } from '../../../shared/form/builder/form-builder.service'; +import { RemoveSectionErrorsAction, SectionStatusChangeAction } from '../../objects/submission-objects.actions'; +import { FormService } from '../../../shared/form/form.service'; +import { SubmissionState } from '../../submission.reducers'; +import { JsonPatchOperationPathCombiner } from '../../../core/json-patch/builder/json-patch-operation-path-combiner'; +import { SectionsType } from '../sections-type'; +import { renderSectionFor } from '../sections-decorator'; +import { SectionDataObject } from '../models/section-data.model'; +import { WorkspaceitemSectionLicenseObject } from '../../../core/submission/models/workspaceitem-section-license.model'; +import { SubmissionService } from '../../submission.service'; +import { SectionsService } from '../sections.service'; +import { FormOperationsService } from '../form/form-operations.service'; +import { submissionSectionErrorsFromIdSelector } from '../../selectors'; + +@Component({ + selector: 'ds-submission-section-license', + styleUrls: ['./section-license.component.scss'], + templateUrl: './section-license.component.html', +}) +@renderSectionFor(SectionsType.License) +export class LicenseSectionComponent extends SectionModelComponent implements OnDestroy, OnInit { + + public formId; + public formModel: DynamicFormControlModel[]; + public displaySubmit = false; + public licenseText: string; + + protected pathCombiner: JsonPatchOperationPathCombiner; + protected subs: Subscription[] = []; + + constructor(protected changeDetectorRef: ChangeDetectorRef, + protected collectionDataService: CollectionDataService, + protected formBuilderService: FormBuilderService, + protected formOperationsService: FormOperationsService, + protected formService: FormService, + protected operationsBuilder: JsonPatchOperationsBuilder, + protected store: Store, + protected sectionService: SectionsService, + protected submissionService: SubmissionService, + @Inject('collectionIdProvider') public injectedCollectionId: string, + @Inject('sectionDataProvider') public injectedSectionData: SectionDataObject, + @Inject('submissionIdProvider') public injectedSubmissionId: string) { + super(injectedCollectionId, injectedSectionData, injectedSubmissionId); + } + + ngOnInit() { + this.pathCombiner = new JsonPatchOperationPathCombiner('sections', this.sectionData.id); + + this.subs.push( + this.collectionDataService.findById(this.collectionId) + .filter((collectionData: RemoteData) => isNotUndefined((collectionData.payload))) + .flatMap((collectionData: RemoteData) => collectionData.payload.license) + .filter((licenseData: RemoteData) => isNotUndefined((licenseData.payload))) + .take(1) + .subscribe((licenseData: RemoteData) => { + this.licenseText = licenseData.payload.text; + this.formId = this.formService.getUniqueId(this.sectionData.id); + this.formModel = this.formBuilderService.fromJSON(SECTION_LICENSE_FORM_MODEL); + const model = this.formBuilderService.findById('granted', this.formModel); + // Retrieve license accepted status + if ((this.sectionData.data as WorkspaceitemSectionLicenseObject).granted) { + (model as DynamicCheckboxModel).checked = true; + this.store.dispatch(new SectionStatusChangeAction(this.submissionId, this.sectionData.id, true)); + } + + // Disable checkbox whether it's in workflow or item scope + this.sectionService.isSectionReadOnly(this.submissionId, this.sectionData.id, this.submissionService.getSubmissionScope()) + .take(1) + .filter((isReadOnly) => isReadOnly) + .subscribe(() => { + model.disabled = true; + }); + this.changeDetectorRef.detectChanges(); + }), + this.store.select(submissionSectionErrorsFromIdSelector(this.submissionId, this.sectionData.id)) + .filter((errors) => isNotEmpty(errors)) + .distinctUntilChanged() + .subscribe((errors) => { + // parse errors + const newErrors = errors.map((error) => { + // When the error path is only on the section, + // replace it with the path to the form field to display error also on the form + if (error.path === '/sections/license') { + const model = this.formBuilderService.findById('granted', this.formModel); + // check whether license is not accepted + if (!(model as DynamicCheckboxModel).checked) { + return Object.assign({}, error, {path: '/sections/license/granted'}); + } else { + return null; + } + } else { + return error; + } + }).filter((error) => isNotNull(error)); + + if (isNotEmpty(newErrors)) { + this.sectionService.checkSectionErrors(this.submissionId, this.sectionData.id, this.formId, newErrors); + this.sectionData.errors = errors; + } else { + // Remove any section's errors + this.store.dispatch(new RemoveSectionErrorsAction(this.submissionId, this.sectionData.id)); + } + this.changeDetectorRef.detectChanges(); + }) + ); + } + + onChange(event: DynamicFormControlEvent) { + const path = this.formOperationsService.getFieldPathSegmentedFromChangeEvent(event); + const value = this.formOperationsService.getFieldValueFromChangeEvent(event); + this.store.dispatch(new SectionStatusChangeAction(this.submissionId, this.sectionData.id, value.value)); + if (value) { + this.operationsBuilder.add(this.pathCombiner.getPath(path), value.value.toString(), false, true); + // Remove any section's errors + this.store.dispatch(new RemoveSectionErrorsAction(this.submissionId, this.sectionData.id)); + } else { + this.operationsBuilder.remove(this.pathCombiner.getPath(path)); + } + } + + ngOnDestroy() { + this.subs + .filter((subscription) => hasValue(subscription)) + .forEach((subscription) => subscription.unsubscribe()); + } + +} diff --git a/src/app/submission/sections/license/section-license.model.ts b/src/app/submission/sections/license/section-license.model.ts new file mode 100644 index 0000000000..22f6c544c4 --- /dev/null +++ b/src/app/submission/sections/license/section-license.model.ts @@ -0,0 +1,17 @@ + +export const SECTION_LICENSE_FORM_MODEL = [ + { + id: 'granted', + label: 'I confirm the license above', + required: true, + value: false, + validators: { + required: null + }, + errorMessages: { + required: 'You must accept the license', + notgranted: 'You must accept the license' + }, + type: 'CHECKBOX', + } +]; diff --git a/src/app/submission/sections/models/section-data.model.ts b/src/app/submission/sections/models/section-data.model.ts new file mode 100644 index 0000000000..230b36eb94 --- /dev/null +++ b/src/app/submission/sections/models/section-data.model.ts @@ -0,0 +1,15 @@ +import { SubmissionSectionError } from '../../objects/submission-objects.reducer'; +import { WorkspaceitemSectionDataType } from '../../../core/submission/models/workspaceitem-sections.model'; +import { SectionsType } from '../sections-type'; + +export interface SectionDataObject { + config: string; + data: WorkspaceitemSectionDataType; + errors: SubmissionSectionError[]; + header: string; + id: string; + mandatory: boolean; + sectionType: SectionsType; + + [propName: string]: any; +} diff --git a/src/app/submission/sections/models/section.model.ts b/src/app/submission/sections/models/section.model.ts new file mode 100644 index 0000000000..7925cc54ec --- /dev/null +++ b/src/app/submission/sections/models/section.model.ts @@ -0,0 +1,24 @@ +import { Inject } from '@angular/core'; +import { SectionDataObject } from './section-data.model'; + +export interface SectionDataModel { + sectionData: SectionDataObject +} + +/** + * An abstract model class for a submission edit form section. + */ +export abstract class SectionModelComponent implements SectionDataModel { + collectionId: string; + sectionData: SectionDataObject; + submissionId: string; + protected valid: boolean; + + public constructor(@Inject('collectionIdProvider') public injectedCollectionId: string, + @Inject('sectionDataProvider') public injectedSectionData: SectionDataObject, + @Inject('submissionIdProvider') public injectedSubmissionId: string) { + this.collectionId = injectedCollectionId; + this.sectionData = injectedSectionData; + this.submissionId = injectedSubmissionId; + } +} diff --git a/src/app/submission/sections/recycle/section-recycle.component.html b/src/app/submission/sections/recycle/section-recycle.component.html new file mode 100644 index 0000000000..a3bc8d52bb --- /dev/null +++ b/src/app/submission/sections/recycle/section-recycle.component.html @@ -0,0 +1,39 @@ + + + +
+
+

No recycled elements yet.

+
+
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/src/app/submission/sections/recycle/section-recycle.component.scss b/src/app/submission/sections/recycle/section-recycle.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/submission/sections/recycle/section-recycle.component.ts b/src/app/submission/sections/recycle/section-recycle.component.ts new file mode 100644 index 0000000000..9890a59d69 --- /dev/null +++ b/src/app/submission/sections/recycle/section-recycle.component.ts @@ -0,0 +1,55 @@ +import { SectionsType } from '../sections-type'; +import { Component, Inject } from '@angular/core'; +import { SectionModelComponent } from '../models/section.model'; +import { renderSectionFor } from '../sections-decorator'; +import { SectionDataObject } from '../models/section-data.model'; +import { SubmissionState } from '../../submission.reducers'; +import { Store } from '@ngrx/store'; +import { WorkspaceitemSectionRecycleObject } from '../../../core/submission/models/workspaceitem-section-recycle.model'; +import { submissionSectionDataFromIdSelector } from '../../selectors'; +import { isNotEmpty } from '../../../shared/empty.util'; +import { Observable } from 'rxjs/Observable'; + +@Component({ + selector: 'ds-recycle-section', + styleUrls: ['./section-recycle.component.scss'], + templateUrl: './section-recycle.component.html', +}) + +@renderSectionFor(SectionsType.Recycle) +export class RecycleSectionComponent extends SectionModelComponent { + public sectionDataObs: Observable; + public isLoading = true; + + public unexpected: any[]; // FormFieldChangedObject[]; + public metadata: any[]; // FormFieldMetadataValueObject[]; + public files: any[]; // WorkspaceitemSectionUploadFileObject[]; + + constructor(protected store: Store, + @Inject('collectionIdProvider') public injectedCollectionId: string, + @Inject('sectionDataProvider') public injectedSectionData: SectionDataObject, + @Inject('submissionIdProvider') public injectedSubmissionId: string) { + super(injectedCollectionId, injectedSectionData, injectedSubmissionId); + } + + ngOnInit() { + + this.sectionDataObs = this.store.select(submissionSectionDataFromIdSelector(this.submissionId, this.sectionData.id)) + .filter((sd) => isNotEmpty(sd)) + // .startWith( {metadata:[]}) + .distinctUntilChanged() + .map( (sd) => { + console.log('sectionData for recycle...'); + console.log(sd); + console.log('sectionData for recycle end'); + return sd; + }); + + this.unexpected = this.sectionData.unexpected; + this.metadata = this.sectionData.metadata; + this.files = this.sectionData.files; + + this.isLoading = false; + } + +} diff --git a/src/app/submission/sections/sections-decorator.ts b/src/app/submission/sections/sections-decorator.ts new file mode 100644 index 0000000000..7e7840adfd --- /dev/null +++ b/src/app/submission/sections/sections-decorator.ts @@ -0,0 +1,16 @@ + +import { SectionsType } from './sections-type'; + +const submissionSectionsMap = new Map(); +export function renderSectionFor(sectionType: SectionsType) { + return function decorator(objectElement: any) { + if (!objectElement) { + return; + } + submissionSectionsMap.set(sectionType, objectElement); + }; +} + +export function rendersSectionType(sectionType: SectionsType) { + return submissionSectionsMap.get(sectionType); +} diff --git a/src/app/submission/sections/sections-type.ts b/src/app/submission/sections/sections-type.ts new file mode 100644 index 0000000000..a21bdf6cd4 --- /dev/null +++ b/src/app/submission/sections/sections-type.ts @@ -0,0 +1,9 @@ +export enum SectionsType { + SubmissionForm = 'submission-form', + Upload = 'upload', + License = 'license', + CcLicense = 'cclicense', + collection = 'collection', + Recycle = 'recycle', + Deduplication = 'deduplication' +} diff --git a/src/app/submission/sections/sections.directive.ts b/src/app/submission/sections/sections.directive.ts new file mode 100644 index 0000000000..aa61ca5e88 --- /dev/null +++ b/src/app/submission/sections/sections.directive.ts @@ -0,0 +1,155 @@ +import { ChangeDetectorRef, Directive, Input, OnDestroy, OnInit } from '@angular/core'; + +import { Store } from '@ngrx/store'; +import { Observable } from 'rxjs/Observable'; +import { Subscription } from 'rxjs/Subscription'; +import { isEmpty, uniq } from 'lodash'; + +import { SectionsService } from './sections.service'; +import { hasValue, isNotEmpty, isNotNull, isNotUndefined } from '../../shared/empty.util'; +import { submissionSectionFromIdSelector } from '../selectors'; +import { SubmissionState } from '../submission.reducers'; +import { SubmissionSectionError, SubmissionSectionObject } from '../objects/submission-objects.reducer'; +import parseSectionErrorPaths, { SectionErrorPath } from '../utils/parseSectionErrorPaths'; +import { + DeleteSectionErrorsAction, + SaveSubmissionSectionFormAction, + SetActiveSectionAction +} from '../objects/submission-objects.actions'; +import { SubmissionService } from '../submission.service'; + +@Directive({ + selector: '[dsSection]', + exportAs: 'sectionRef' +}) +export class SectionsDirective implements OnDestroy, OnInit { + @Input() mandatory = true; + @Input() sectionId; + @Input() submissionId; + public sectionErrors: string[] = []; + private active = true; + private animation = !this.mandatory; + private enabled: Observable; + private sectionState = this.mandatory; + private subs: Subscription[] = []; + private valid: Observable; + + constructor(private changeDetectorRef: ChangeDetectorRef, + private store: Store, + private submissionService: SubmissionService, + private sectionService: SectionsService) { + } + + ngOnInit() { + this.valid = this.sectionService.isSectionValid(this.submissionId, this.sectionId) + .map((valid: boolean) => { + if (valid) { + this.resetErrors(); + } + return valid; + }); + + this.subs.push( + this.store.select(submissionSectionFromIdSelector(this.submissionId, this.sectionId)) + .filter((state: SubmissionSectionObject) => isNotUndefined(state)) + .map((state: SubmissionSectionObject) => state.errors) + // .filter((errors: SubmissionSectionError[]) => isNotEmpty(errors)) + .subscribe((errors: SubmissionSectionError[]) => { + if (isNotEmpty(errors)) { + errors.forEach((errorItem: SubmissionSectionError) => { + const parsedErrors: SectionErrorPath[] = parseSectionErrorPaths(errorItem.path); + + parsedErrors.forEach((error: SectionErrorPath) => { + if (!error.fieldId) { + this.sectionErrors = uniq(this.sectionErrors.concat(errorItem.message)); + } + }); + }); + } else { + this.resetErrors(); + } + }), + this.submissionService.getActiveSectionId(this.submissionId) + .subscribe((activeSectionId) => { + const previousActive = this.active; + this.active = (activeSectionId === this.sectionId); + if (previousActive !== this.active) { + this.changeDetectorRef.detectChanges(); + // If section is no longer active dispatch save action + if (!this.active && isNotNull(activeSectionId)) { + this.store.dispatch(new SaveSubmissionSectionFormAction(this.submissionId, this.sectionId)); + } + } + }) + ); + + this.enabled = this.sectionService.isSectionEnabled(this.submissionId, this.sectionId); + } + + ngOnDestroy() { + this.subs + .filter((subscription) => hasValue(subscription)) + .forEach((subscription) => subscription.unsubscribe()); + } + + public sectionChange(event) { + this.sectionState = event.nextState; + } + + public isOpen() { + return (this.sectionState) ? true : false; + } + + public isMandatory() { + return this.mandatory; + } + + public isAnimationsActive() { + return this.animation; + } + + public isSectionActive(): boolean { + return this.active; + } + + public isEnabled(): Observable { + return this.enabled; + } + + public isValid(): Observable { + return this.valid; + } + + public removeSection(submissionId, sectionId) { + this.sectionService.removeSection(submissionId, sectionId) + } + + public hasErrors() { + return this.sectionErrors && this.sectionErrors.length > 0 + } + + public getErrors() { + return this.sectionErrors; + } + + public setFocus(event) { + if (!this.active) { + this.store.dispatch(new SetActiveSectionAction(this.submissionId, this.sectionId)); + } + } + + public removeError(index) { + this.sectionErrors.splice(index); + } + + public resetErrors() { + this.sectionErrors + .forEach((errorItem) => { + // because it has been deleted, remove the error from the state + const removeAction = new DeleteSectionErrorsAction(this.submissionId, this.sectionId, errorItem); + this.store.dispatch(removeAction); + }) + this.sectionErrors = []; + + } +} diff --git a/src/app/submission/sections/sections.service.ts b/src/app/submission/sections/sections.service.ts new file mode 100644 index 0000000000..b01f58751e --- /dev/null +++ b/src/app/submission/sections/sections.service.ts @@ -0,0 +1,154 @@ +import { Injectable } from '@angular/core'; + +import { Observable } from 'rxjs/Observable'; +import { Store } from '@ngrx/store'; +import { SubmissionState } from '../submission.reducers'; +import { isEqual } from 'lodash'; + +import { hasValue, isEmpty, isNotEmpty, isNotUndefined } from '../../shared/empty.util'; +import { + DeleteSectionErrorsAction, + DisableSectionAction, + EnableSectionAction, + InertSectionErrorsAction, + UpdateSectionDataAction +} from '../objects/submission-objects.actions'; +import { + SubmissionObjectEntry, + SubmissionSectionError, + SubmissionSectionObject +} from '../objects/submission-objects.reducer'; +import { submissionObjectFromIdSelector, submissionSectionFromIdSelector } from '../selectors'; +import { ScrollToConfigOptions, ScrollToService } from '@nicky-lenaers/ngx-scroll-to'; +import { SubmissionScopeType } from '../../core/submission/submission-scope-type'; +import parseSectionErrorPaths, { SectionErrorPath } from '../utils/parseSectionErrorPaths'; +import { FormAddError, FormClearErrorsAction, FormRemoveErrorAction } from '../../shared/form/form.actions'; +import { TranslateService } from '@ngx-translate/core'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; + +@Injectable() +export class SectionsService { + + constructor(private notificationsService: NotificationsService, + private scrollToService: ScrollToService, + private store: Store, + private translate: TranslateService) { + } + + public checkSectionErrors(submissionId, sectionId, formId, currentErrors, prevErrors = []) { + if (isEmpty(currentErrors)) { + this.store.dispatch(new DeleteSectionErrorsAction(submissionId, sectionId, currentErrors)); + this.store.dispatch(new FormClearErrorsAction(formId)); + } else if (!isEqual(currentErrors, prevErrors)) { + const dispatchedErrors = []; + currentErrors.forEach((error: SubmissionSectionError) => { + const errorPaths: SectionErrorPath[] = parseSectionErrorPaths(error.path); + + errorPaths.forEach((path: SectionErrorPath) => { + if (path.fieldId) { + const fieldId = path.fieldId.replace(/\./g, '_'); + + // Dispatch action to the form state; + const formAddErrorAction = new FormAddError(formId, fieldId, path.fieldIndex, error.message); + this.store.dispatch(formAddErrorAction); + dispatchedErrors.push(fieldId); + } + }); + }); + + prevErrors.forEach((error: SubmissionSectionError) => { + const errorPaths: SectionErrorPath[] = parseSectionErrorPaths(error.path); + + errorPaths.forEach((path: SectionErrorPath) => { + if (path.fieldId) { + const fieldId = path.fieldId.replace(/\./g, '_'); + + if (!dispatchedErrors.includes(fieldId)) { + const formRemoveErrorAction = new FormRemoveErrorAction(formId, fieldId, path.fieldIndex); + this.store.dispatch(formRemoveErrorAction); + } + } + }); + }); + } + } + + public getSectionState(submissionId, sectionId): Observable { + return this.store.select(submissionSectionFromIdSelector(submissionId, sectionId)) + .filter((sectionObj) => hasValue(sectionObj)) + .map((sectionObj: SubmissionSectionObject) => sectionObj) + .distinctUntilChanged(); + } + + public isSectionValid(submissionId, sectionId): Observable { + return this.store.select(submissionSectionFromIdSelector(submissionId, sectionId)) + .filter((sectionObj) => hasValue(sectionObj)) + .map((sectionObj: SubmissionSectionObject) => sectionObj.isValid) + .distinctUntilChanged(); + } + + public isSectionEnabled(submissionId, sectionId): Observable { + return this.store.select(submissionSectionFromIdSelector(submissionId, sectionId)) + .filter((sectionObj) => hasValue(sectionObj)) + .map((sectionObj: SubmissionSectionObject) => sectionObj.enabled) + .distinctUntilChanged(); + } + + public isSectionReadOnly(submissionId: string, sectionId: string, submissionScope: SubmissionScopeType): Observable { + return this.store.select(submissionSectionFromIdSelector(submissionId, sectionId)) + .filter((sectionObj) => hasValue(sectionObj)) + .map((sectionObj: SubmissionSectionObject) => { + return sectionObj.visibility.other === 'READONLY' && submissionScope !== SubmissionScopeType.WorkspaceItem + }) + .distinctUntilChanged(); + } + + public isSectionAvailable(submissionId, sectionId): Observable { + return this.store.select(submissionObjectFromIdSelector(submissionId)) + .filter((submissionState: SubmissionObjectEntry) => isNotUndefined(submissionState)) + .map((submissionState: SubmissionObjectEntry) => { + return isNotUndefined(submissionState.sections) && isNotUndefined(submissionState.sections[sectionId]); + }) + .distinctUntilChanged(); + } + + public addSection(submissionId: string, + sectionId: string) { + this.store.dispatch(new EnableSectionAction(submissionId, sectionId)); + const config: ScrollToConfigOptions = { + target: sectionId, + offset: -70 + }; + + this.scrollToService.scrollTo(config); + } + + public removeSection(submissionId, sectionId) { + this.store.dispatch(new DisableSectionAction(submissionId, sectionId)) + } + + public updateSectionData(submissionId, sectionId, data, errors = []) { + if (isNotEmpty(data)) { + const isAvailable$ = this.isSectionAvailable(submissionId, sectionId); + const isEnabled$ = this.isSectionEnabled(submissionId, sectionId); + + Observable.combineLatest(isAvailable$, isEnabled$) + .take(1) + .filter(([available, enabled]: [boolean, boolean]) => available) + .subscribe(([available, enabled]: [boolean, boolean]) => { + if (!enabled) { + this.translate.get('submission.sections.general.metadata-extracted-new-section', {sectionId}) + .take(1) + .subscribe((m) => { + this.notificationsService.info(null, m, null, true); + }); + } + this.store.dispatch(new UpdateSectionDataAction(submissionId, sectionId, data, errors)); + }); + } + } + + public setSectionError(submissionId: string, sectionId: string, errors: SubmissionSectionError[]) { + this.store.dispatch(new InertSectionErrorsAction(submissionId, sectionId, errors)); + } +} diff --git a/src/app/submission/sections/upload/accessConditions/accessConditions.component.html b/src/app/submission/sections/upload/accessConditions/accessConditions.component.html new file mode 100644 index 0000000000..ae4c74e2eb --- /dev/null +++ b/src/app/submission/sections/upload/accessConditions/accessConditions.component.html @@ -0,0 +1,9 @@ + + + {{accessCondition.name}} {{accessCondition.startDate}} {{accessCondition.endDate}} + + {{accessCondition.name}} + {{accessCondition.name}} from {{accessCondition.endDate}} + {{accessCondition.name}} until {{accessCondition.startDate}} +
+
diff --git a/src/app/submission/sections/upload/accessConditions/accessConditions.component.ts b/src/app/submission/sections/upload/accessConditions/accessConditions.component.ts new file mode 100644 index 0000000000..c89886d69d --- /dev/null +++ b/src/app/submission/sections/upload/accessConditions/accessConditions.component.ts @@ -0,0 +1,35 @@ +import { Component, Input, OnInit } from '@angular/core'; +import { GroupEpersonService } from '../../../../core/eperson/group-eperson.service'; +import { ResourcePolicy } from '../../../../core/shared/resource-policy.model'; +import { isEmpty } from '../../../../shared/empty.util'; +import { EpersonData } from '../../../../core/eperson/eperson-data'; +import { Group } from '../../../../core/eperson/models/group.model'; + +@Component({ + selector: 'ds-access-conditions', + templateUrl: './accessConditions.component.html', +}) +export class AccessConditionsComponent implements OnInit { + + @Input() accessConditions: ResourcePolicy[]; + + public accessConditionsList = []; + + constructor(private groupService: GroupEpersonService) {} + + ngOnInit() { + this.accessConditions.forEach((accessCondition: ResourcePolicy) => { + if (isEmpty(accessCondition.name)) { + this.groupService.getDataByUuid(accessCondition.groupUUID) + .subscribe((data: EpersonData) => { + const group = data.payload[0] as any; + const accessConditionEntry = Object.assign({}, accessCondition); + accessConditionEntry.name = group.name; + this.accessConditionsList.push(accessConditionEntry); + }) + } else { + this.accessConditionsList.push(accessCondition); + } + }) + } +} diff --git a/src/app/submission/sections/upload/file/edit/file-edit.component.html b/src/app/submission/sections/upload/file/edit/file-edit.component.html new file mode 100644 index 0000000000..bfb322052c --- /dev/null +++ b/src/app/submission/sections/upload/file/edit/file-edit.component.html @@ -0,0 +1,8 @@ +
+ +
diff --git a/src/app/submission/sections/upload/file/edit/file-edit.component.ts b/src/app/submission/sections/upload/file/edit/file-edit.component.ts new file mode 100644 index 0000000000..fcc323047f --- /dev/null +++ b/src/app/submission/sections/upload/file/edit/file-edit.component.ts @@ -0,0 +1,259 @@ +import { ChangeDetectorRef, Component, Input, OnChanges } from '@angular/core'; + +import { WorkspaceitemSectionUploadFileObject } from '../../../../../core/submission/models/workspaceitem-section-upload-file.model'; +import { + DYNAMIC_FORM_CONTROL_TYPE_DATEPICKER, + DynamicDateControlModel, + DynamicDatePickerModel, + DynamicFormArrayGroupModel, + DynamicFormArrayModel, + DynamicFormControlEvent, + DynamicFormControlModel, + DynamicFormGroupModel, + DynamicSelectModel +} from '@ng-dynamic-forms/core'; +import { FormBuilderService } from '../../../../../shared/form/builder/form-builder.service'; +import { + BITSTREAM_ACCESS_CONDITIONS_FORM_ARRAY_LAYOUT, + BITSTREAM_ACCESS_CONDITIONS_FORM_ARRAY_CONFIG, + BITSTREAM_FORM_ACCESS_CONDITION_END_DATE_LAYOUT, + BITSTREAM_FORM_ACCESS_CONDITION_END_DATE_CONFIG, + BITSTREAM_FORM_ACCESS_CONDITION_GROUPS_LAYOUT, + BITSTREAM_FORM_ACCESS_CONDITION_GROUPS_CONFIG, + BITSTREAM_FORM_ACCESS_CONDITION_START_DATE_LAYOUT, + BITSTREAM_FORM_ACCESS_CONDITION_START_DATE_CONFIG, + BITSTREAM_FORM_ACCESS_CONDITION_TYPE_LAYOUT, + BITSTREAM_FORM_ACCESS_CONDITION_TYPE_CONFIG, + BITSTREAM_METADATA_FORM_GROUP_LAYOUT, + BITSTREAM_METADATA_FORM_GROUP_CONFIG +} from './files-edit.model'; +import { POLICY_DEFAULT_WITH_LIST } from '../../section-upload.component'; +import { isNotEmpty, isNotUndefined } from '../../../../../shared/empty.util'; +import { SubmissionFormsModel } from '../../../../../core/shared/config/config-submission-forms.model'; +import { FormFieldModel } from '../../../../../shared/form/builder/models/form-field.model'; +import { AccessConditionOption } from '../../../../../core/shared/config/config-access-condition-option.model'; +import { SubmissionService } from '../../../../submission.service'; + +@Component({ + selector: 'ds-submission-upload-section-file-edit', + templateUrl: './file-edit.component.html', +}) +export class UploadSectionFileEditComponent implements OnChanges { + + @Input() availableAccessConditionOptions: any[]; + @Input() availableAccessConditionGroups: Map; + @Input() collectionId; + @Input() collectionPolicyType; + @Input() configMetadataForm: SubmissionFormsModel; + @Input() fileData: WorkspaceitemSectionUploadFileObject; + @Input() fileId; + @Input() fileIndex; + @Input() formId; + @Input() sectionId; + @Input() submissionId; + + public formModel: DynamicFormControlModel[]; + + constructor(private cdr: ChangeDetectorRef, + private formBuilderService: FormBuilderService, + private submissionService: SubmissionService) { + } + + ngOnChanges() { + if (this.fileData && this.formId) { + this.formModel = this.buildFileEditForm(); + this.cdr.detectChanges(); + } + } + + protected buildFileEditForm() { + // TODO check in the rest server configuration whether dc.description may be repeatable + const configDescr: FormFieldModel = Object.assign({}, this.configMetadataForm.rows[ 0 ].fields[ 0 ]); + configDescr.repeatable = false; + const configForm = Object.assign({}, this.configMetadataForm, { + fields: Object.assign([], this.configMetadataForm.rows[ 0 ].fields[ 0 ], [ + this.configMetadataForm.rows[ 0 ].fields[ 0 ], + configDescr + ]) + }); + const formModel: DynamicFormControlModel[] = []; + const metadataGroupModelConfig = Object.assign({}, BITSTREAM_METADATA_FORM_GROUP_CONFIG); + metadataGroupModelConfig.group = this.formBuilderService.modelFromConfiguration( + configForm, + this.collectionId, + this.fileData.metadata, + this.submissionService.getSubmissionScope() + ); + formModel.push(new DynamicFormGroupModel(metadataGroupModelConfig, BITSTREAM_METADATA_FORM_GROUP_LAYOUT)); + const accessConditionTypeModelConfig = Object.assign({}, BITSTREAM_FORM_ACCESS_CONDITION_TYPE_CONFIG); + const accessConditionsArrayConfig = Object.assign({}, BITSTREAM_ACCESS_CONDITIONS_FORM_ARRAY_CONFIG); + const accessConditionTypeOptions = []; + + if (this.collectionPolicyType === POLICY_DEFAULT_WITH_LIST) { + for (const accessCondition of this.availableAccessConditionOptions) { + accessConditionTypeOptions.push( + { + label: accessCondition.name, + value: accessCondition.name + } + ); + } + accessConditionTypeModelConfig.options = accessConditionTypeOptions; + + // Dynamic assign of relation in config. For startdate, endDate, groups. + const hasStart = []; + const hasEnd = []; + const hasGroups = []; + this.availableAccessConditionOptions.forEach((condition) => { + const showStart: boolean = condition.hasStartDate === true; + const showEnd: boolean = condition.hasEndDate === true; + const showGroups: boolean = showStart || showEnd; + if (showStart) { + hasStart.push({ id: 'name', value: condition.name }); + } + if (showEnd) { + hasEnd.push({ id: 'name', value: condition.name }); + } + if (showGroups) { + hasGroups.push({ id: 'name', value: condition.name }); + } + }); + const confStart = { relation: [ { action: 'ENABLE', connective: 'OR', when: hasStart } ] }; + const confEnd = { relation: [ { action: 'ENABLE', connective: 'OR', when: hasEnd } ] }; + const confGroup = { relation: [ { action: 'ENABLE', connective: 'OR', when: hasGroups } ] }; + + accessConditionsArrayConfig.groupFactory = () => { + const type = new DynamicSelectModel(accessConditionTypeModelConfig, BITSTREAM_FORM_ACCESS_CONDITION_TYPE_LAYOUT); + const startDateConfig = Object.assign({}, BITSTREAM_FORM_ACCESS_CONDITION_START_DATE_CONFIG, confStart); + const endDateConfig = Object.assign({}, BITSTREAM_FORM_ACCESS_CONDITION_END_DATE_CONFIG, confEnd); + const groupsConfig = Object.assign({}, BITSTREAM_FORM_ACCESS_CONDITION_GROUPS_CONFIG, confGroup); + + const startDate = new DynamicDatePickerModel(startDateConfig, BITSTREAM_FORM_ACCESS_CONDITION_START_DATE_LAYOUT); + const endDate = new DynamicDatePickerModel(endDateConfig, BITSTREAM_FORM_ACCESS_CONDITION_END_DATE_LAYOUT); + const groups = new DynamicSelectModel(groupsConfig, BITSTREAM_FORM_ACCESS_CONDITION_GROUPS_LAYOUT); + + return [ type, startDate, endDate, groups ]; + }; + + // Number of access conditions blocks in form + accessConditionsArrayConfig.initialCount = isNotEmpty(this.fileData.accessConditions) ? this.fileData.accessConditions.length : 1; + formModel.push( + new DynamicFormArrayModel(accessConditionsArrayConfig, BITSTREAM_ACCESS_CONDITIONS_FORM_ARRAY_LAYOUT) + ); + + } + this.initModelData(formModel); + return formModel; + } + + public initModelData(formModel: DynamicFormControlModel[]) { + this.fileData.accessConditions.forEach((accessCondition, index) => { + Array.of('name', 'groupUUID', 'startDate', 'endDate') + .filter((key) => accessCondition.hasOwnProperty(key)) + .forEach((key) => { + const metadataModel: any = this.formBuilderService.findById(key, formModel, index); + if (metadataModel) { + if (key === 'groupUUID') { + this.availableAccessConditionGroups.forEach((group) => { + metadataModel.options.push({ + label: group.name, + value: group.uuid + }) + }); + } + if (metadataModel.type === DYNAMIC_FORM_CONTROL_TYPE_DATEPICKER) { + const date = new Date(accessCondition[key]); + metadataModel.value = { + year: date.getFullYear(), + month: date.getMonth() + 1, + day: date.getDate() + } + } else { + metadataModel.value = accessCondition[key]; + } + } + }); + }); + } + + public onChange(event: DynamicFormControlEvent) { + if (event.model.id === 'name') { + this.setOptions(event.model, event.control); + } + } + + public setOptions(model, control) { + let accessCondition: AccessConditionOption = null; + this.availableAccessConditionOptions.filter((element) => element.name === control.value) + .forEach((element) => accessCondition = element); + if (isNotEmpty(accessCondition)) { + const showGroups: boolean = accessCondition.hasStartDate === true || accessCondition.hasEndDate === true; + + const groupControl = control.parent.get('groupUUID'); + const startDateControl = control.parent.get('startDate'); + const endDateControl = control.parent.get('endDate'); + + // Clear previous values + if (showGroups) { + groupControl.setValue(null); + } else { + groupControl.setValue(accessCondition.groupUUID); + } + startDateControl.setValue(null); + control.parent.markAsDirty(); + endDateControl.setValue(null); + + if (showGroups) { + if (isNotUndefined(accessCondition.groupUUID)) { + + const groupOptions = []; + if (isNotUndefined(this.availableAccessConditionGroups.get(accessCondition.groupUUID))) { + const groupModel = this.formBuilderService.findById( + 'groupUUID', + (model.parent as DynamicFormArrayGroupModel).group) as DynamicSelectModel; + + this.availableAccessConditionGroups.forEach((group) => { + groupOptions.push({ + label: group.name, + value: group.uuid + }) + }); + + // Due to a bug can't dynamically change the select options, so replace the model with a new one + const confGroup = { relation: groupModel.relation }; + const groupsConfig = Object.assign({}, BITSTREAM_FORM_ACCESS_CONDITION_GROUPS_CONFIG, confGroup); + groupsConfig.options = groupOptions; + model.parent.group.pop(); + model.parent.group.push(new DynamicSelectModel(groupsConfig, BITSTREAM_FORM_ACCESS_CONDITION_GROUPS_LAYOUT)); + } + + } + if (accessCondition.hasStartDate) { + const startDateModel = this.formBuilderService.findById( + 'startDate', + (model.parent as DynamicFormArrayGroupModel).group) as DynamicDateControlModel; + + const min = new Date(accessCondition.maxStartDate); + startDateModel.max = { + year: min.getFullYear(), + month: min.getMonth() + 1, + day: min.getDate() + }; + } + if (accessCondition.hasEndDate) { + const endDateModel = this.formBuilderService.findById( + 'endDate', + (model.parent as DynamicFormArrayGroupModel).group) as DynamicDateControlModel; + + const max = new Date(accessCondition.maxEndDate); + endDateModel.max = { + year: max.getFullYear(), + month: max.getMonth() + 1, + day: max.getDate() + }; + } + } + } + } + +} diff --git a/src/app/submission/sections/upload/file/edit/files-edit.model.ts b/src/app/submission/sections/upload/file/edit/files-edit.model.ts new file mode 100644 index 0000000000..fb36e8c54a --- /dev/null +++ b/src/app/submission/sections/upload/file/edit/files-edit.model.ts @@ -0,0 +1,115 @@ +import { + DynamicDatePickerModelConfig, + DynamicFormArrayModelConfig, + DynamicSelectModelConfig, + DynamicFormGroupModelConfig, DynamicFormControlLayout, +} from '@ng-dynamic-forms/core'; + +export const BITSTREAM_METADATA_FORM_GROUP_CONFIG: DynamicFormGroupModelConfig = { + id: 'metadata', + group: [] +}; +export const BITSTREAM_METADATA_FORM_GROUP_LAYOUT: DynamicFormControlLayout = { + element: { + container: 'form-group', + label: 'col-form-label' + }, + grid: { + label: 'col-sm-3' + } +}; + +export const BITSTREAM_ACCESS_CONDITIONS_FORM_ARRAY_CONFIG: DynamicFormArrayModelConfig = { + id: 'accessConditions', + groupFactory: null, +}; +export const BITSTREAM_ACCESS_CONDITIONS_FORM_ARRAY_LAYOUT: DynamicFormControlLayout = { + grid: { + group: 'form-row' + } +}; + +export const BITSTREAM_FORM_ACCESS_CONDITION_TYPE_CONFIG: DynamicSelectModelConfig = { + id: 'name', + label: 'submission.sections.upload.form.access-condition-label', + options: [] +}; +export const BITSTREAM_FORM_ACCESS_CONDITION_TYPE_LAYOUT: DynamicFormControlLayout = { + element: { + container: 'p-0', + label: 'col-form-label' + }, + grid: { + host: 'col-md-10' + } +}; + +export const BITSTREAM_FORM_ACCESS_CONDITION_START_DATE_CONFIG: DynamicDatePickerModelConfig = { + id: 'startDate', + label: 'submission.sections.upload.form.from-label', + placeholder: 'submission.sections.upload.form.from-placeholder', + inline: false, + toggleIcon: 'fa fa-calendar', + relation: [ + { + action: 'ENABLE', + connective: 'OR', + when: [] + } + ] +}; +export const BITSTREAM_FORM_ACCESS_CONDITION_START_DATE_LAYOUT: DynamicFormControlLayout = { + element: { + container: 'p-0', + label: 'col-form-label' + }, + grid: { + host: 'col-md-4' + } +}; + +export const BITSTREAM_FORM_ACCESS_CONDITION_END_DATE_CONFIG: DynamicDatePickerModelConfig = { + id: 'endDate', + label: 'submission.sections.upload.form.until-label', + placeholder: 'submission.sections.upload.form.until-placeholder', + inline: false, + toggleIcon: 'fa fa-calendar', + relation: [ + { + action: 'ENABLE', + connective: 'OR', + when: [] + } + ] +}; +export const BITSTREAM_FORM_ACCESS_CONDITION_END_DATE_LAYOUT: DynamicFormControlLayout = { + element: { + container: 'p-0', + label: 'col-form-label' + }, + grid: { + host: 'col-md-4' + } +}; + +export const BITSTREAM_FORM_ACCESS_CONDITION_GROUPS_CONFIG: DynamicSelectModelConfig = { + id: 'groupUUID', + label: 'submission.sections.upload.form.group-label', + options: [], + relation: [ + { + action: 'ENABLE', + connective: 'OR', + when: [] + } + ] +}; +export const BITSTREAM_FORM_ACCESS_CONDITION_GROUPS_LAYOUT: DynamicFormControlLayout = { + element: { + container: 'p-0', + label: 'col-form-label' + }, + grid: { + host: 'col-sm-10' + } +}; diff --git a/src/app/submission/sections/upload/file/file.component.html b/src/app/submission/sections/upload/file/file.component.html new file mode 100644 index 0000000000..ffbaefaa0e --- /dev/null +++ b/src/app/submission/sections/upload/file/file.component.html @@ -0,0 +1,56 @@ + +
+
+ + +
+
+
+

{{fileName}} ({{fileData?.sizeBytes | dsFileSize}})

+
+
+ + + + + + + + + + + +
+
+ + +
+
+
+ + + + + + diff --git a/src/app/submission/sections/upload/file/file.component.ts b/src/app/submission/sections/upload/file/file.component.ts new file mode 100644 index 0000000000..f522f6c3b7 --- /dev/null +++ b/src/app/submission/sections/upload/file/file.component.ts @@ -0,0 +1,181 @@ +import { ChangeDetectorRef, Component, Input, OnChanges, OnInit } from '@angular/core'; +import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; +import { SectionUploadService } from '../section-upload.service'; +import { isNotEmpty, isNotNull, isNotUndefined } from '../../../../shared/empty.util'; +import { DynamicFormControlModel, } from '@ng-dynamic-forms/core'; + +import { FormService } from '../../../../shared/form/form.service'; +import { JsonPatchOperationsBuilder } from '../../../../core/json-patch/builder/json-patch-operations-builder'; +import { JsonPatchOperationPathCombiner } from '../../../../core/json-patch/builder/json-patch-operation-path-combiner'; + +import { WorkspaceitemSectionUploadFileObject } from '../../../../core/submission/models/workspaceitem-section-upload-file.model'; +import { SubmissionFormsModel } from '../../../../core/shared/config/config-submission-forms.model'; +import { deleteProperty } from '../../../../shared/object.util'; +import { dateToGMTString } from '../../../../shared/date.util'; +import { JsonPatchOperationsService } from '../../../../core/json-patch/json-patch-operations.service'; +import { SubmitDataResponseDefinitionObject } from '../../../../core/shared/submit-data-response-definition.model'; +import { SubmissionService } from '../../../submission.service'; +import { FileService } from '../../../../core/shared/file.service'; +import { HALEndpointService } from '../../../../core/shared/hal-endpoint.service'; + +@Component({ + selector: 'ds-submission-upload-section-file', + templateUrl: './file.component.html', +}) +export class UploadSectionFileComponent implements OnChanges, OnInit { + + @Input() availableAccessConditionOptions: any[]; + @Input() availableAccessConditionGroups: Map; + @Input() collectionId; + @Input() collectionPolicyType; + @Input() configMetadataForm: SubmissionFormsModel; + @Input() fileId; + @Input() fileIndex; + @Input() fileName + @Input() sectionId; + @Input() submissionId; + + public fileData: WorkspaceitemSectionUploadFileObject; + public formId; + public formState; + public readMode; + public formModel: DynamicFormControlModel[]; + + protected pathCombiner: JsonPatchOperationPathCombiner; + protected subscriptions = []; + + constructor(private cdr: ChangeDetectorRef, + private fileService: FileService, + private formService: FormService, + private halService: HALEndpointService, + private modalService: NgbModal, + private operationsBuilder: JsonPatchOperationsBuilder, + private operationsService: JsonPatchOperationsService, + private submissionService: SubmissionService, + private uploadService: SectionUploadService) { + this.readMode = true; + } + + ngOnChanges() { + if (this.availableAccessConditionOptions && this.availableAccessConditionGroups) { + // Retrieve file state + this.subscriptions.push( + this.uploadService + .getFileData(this.submissionId, this.sectionId, this.fileId) + .filter((bitstream) => isNotUndefined(bitstream)) + .subscribe((bitstream) => { + this.fileData = bitstream; + } + ) + ); + } + } + + ngOnInit() { + this.formId = this.formService.getUniqueId(this.fileId); + this.pathCombiner = new JsonPatchOperationPathCombiner('sections', this.sectionId, 'files', this.fileIndex); + } + + protected deleteFile() { + this.uploadService.removeUploadedFile(this.submissionId, this.sectionId, this.fileId); + this.operationsBuilder.remove(this.pathCombiner.getPath()); + this.operationsService.jsonPatchByResourceID( + this.submissionService.getSubmissionObjectLinkName(), + this.submissionId, + this.pathCombiner.rootElement, + this.pathCombiner.subRootElement) + .subscribe(); + } + + public confirmDelete(content) { + this.modalService.open(content).result.then( + (result) => { + if (result === 'ok') { + this.deleteFile(); + } + } + ); + } + + public downloadBitstreamFile() { + this.halService.getEndpoint('bitstreams') + .first() + .subscribe((url) => { + const fileUrl = `${url}/${this.fileData.uuid}/content`; + this.fileService.downloadFile(fileUrl); + }); + } + + public saveBitstreamData(event) { + event.preventDefault(); + this.subscriptions.push( + this.formService.getFormData(this.formId) + .take(1) + .subscribe((formData: any) => { + Object.keys((formData.metadata)) + .filter((key) => isNotEmpty(formData.metadata[key])) + .forEach((key) => { + const metadataKey = key.replace(/_/g, '.'); + const path = `metadata/${metadataKey}`; + this.operationsBuilder.add(this.pathCombiner.getPath(path), formData.metadata[key], true); + }); + const accessConditionsToSave = []; + formData.accessConditions + .filter((accessCondition) => isNotEmpty(accessCondition)) + .forEach((accessCondition, index) => { + let accessConditionOpt; + + this.availableAccessConditionOptions + .filter((element) => isNotNull(accessCondition.name) && element.name === accessCondition.name[0].value) + .forEach((element) => accessConditionOpt = element); + + if (accessConditionOpt) { + const path = `accessConditions/${index}`; + if (accessConditionOpt.hasStartDate !== true && accessConditionOpt.hasEndDate !== true) { + accessConditionOpt = deleteProperty(accessConditionOpt, 'hasStartDate'); + + accessConditionOpt = deleteProperty(accessConditionOpt, 'hasEndDate'); + accessConditionsToSave.push(accessConditionOpt); + } else { + accessConditionOpt = Object.assign({}, accessCondition); + accessConditionOpt.name = Array.isArray(accessCondition.name) ? accessCondition.name[0].value : accessCondition.name.value; + accessConditionOpt.groupUUID = Array.isArray(accessCondition.groupUUID) ? accessCondition.groupUUID[0].value : accessCondition.groupUUID.value; + if (accessCondition.startDate) { + const startDate = Array.isArray(accessCondition.startDate) ? accessCondition.startDate[0].value : accessCondition.startDate.value; + accessConditionOpt.startDate = dateToGMTString(startDate); + accessConditionOpt = deleteProperty(accessConditionOpt, 'endDate'); + } + if (accessCondition.endDate) { + const endDate = Array.isArray(accessCondition.endDate) ? accessCondition.endDate[0].value : accessCondition.endDate.value; + accessConditionOpt.endDate = dateToGMTString(endDate); + accessConditionOpt = deleteProperty(accessConditionOpt, 'startDate'); + } + accessConditionsToSave.push(accessConditionOpt); + } + } + }); + this.operationsBuilder.add(this.pathCombiner.getPath('accessConditions'), accessConditionsToSave, true); + this.operationsService.jsonPatchByResourceID( + this.submissionService.getSubmissionObjectLinkName(), + this.submissionId, + this.pathCombiner.rootElement, + this.pathCombiner.subRootElement) + .subscribe((result) => { + Object.keys(result[0].sections.upload.files) + .filter((key) => result[0].sections.upload.files[key].uuid === this.fileId) + .forEach((key) => this.uploadService.updateFileData( + this.submissionId, + this.sectionId, + this.fileId, + result[0].sections.upload.files[key])); + this.switchMode(); + }); + }) + ); + } + + public switchMode() { + this.readMode = !this.readMode; + this.cdr.detectChanges(); + } +} diff --git a/src/app/submission/sections/upload/file/view/file-view.component.html b/src/app/submission/sections/upload/file/view/file-view.component.html new file mode 100644 index 0000000000..e8a2ea4469 --- /dev/null +++ b/src/app/submission/sections/upload/file/view/file-view.component.html @@ -0,0 +1,25 @@ +
+ + + +
+ {{entry.value}} +
+ + {{entry.value | dsTruncate:[150]}} + +
+ +
+ No {{entry.key}} +
+ + No {{entry.key}} + +
+
+
+ + + +
diff --git a/src/app/submission/sections/upload/file/view/file-view.component.ts b/src/app/submission/sections/upload/file/view/file-view.component.ts new file mode 100644 index 0000000000..0136b70c05 --- /dev/null +++ b/src/app/submission/sections/upload/file/view/file-view.component.ts @@ -0,0 +1,29 @@ +import { Component, Input, OnInit } from '@angular/core'; +import { WorkspaceitemSectionUploadFileObject } from '../../../../../core/submission/models/workspaceitem-section-upload-file.model'; +import { Metadatum } from '../../../../../core/shared/metadatum.model'; +import { isNotEmpty } from '../../../../../shared/empty.util'; + +@Component({ + selector: 'ds-submission-upload-section-file-view', + templateUrl: './file-view.component.html', +}) +export class UploadSectionFileViewComponent implements OnInit { + @Input() fileData: WorkspaceitemSectionUploadFileObject; + + public metadata: Metadatum[] = []; + + ngOnInit() { + if (isNotEmpty(this.fileData.metadata)) { + this.metadata.push({ + key: 'Title', + language: (this.fileData.metadata.hasOwnProperty('dc.title') ? this.fileData.metadata['dc.title'][0].language : ''), + value: (this.fileData.metadata.hasOwnProperty('dc.title') ? this.fileData.metadata['dc.title'][0].value : '') + }); + this.metadata.push({ + key: 'Description', + language: (this.fileData.metadata.hasOwnProperty('dc.description') ? this.fileData.metadata['dc.description'][0].language : ''), + value: (this.fileData.metadata.hasOwnProperty('dc.description') ? this.fileData.metadata['dc.description'][0].value : '') + }); + } + } +} diff --git a/src/app/submission/sections/upload/section-upload.component.html b/src/app/submission/sections/upload/section-upload.component.html new file mode 100644 index 0000000000..3939688e2f --- /dev/null +++ b/src/app/submission/sections/upload/section-upload.component.html @@ -0,0 +1,50 @@ + + + +
+
+

No file uploaded yet.

+
+
+
+ + + +
+
+ + + + + {{ 'submission.sections.upload.header.policy.default.nolist' | translate:{ "collectionName": collectionName } }} + + + + {{ 'submission.sections.upload.header.policy.default.withlist' | translate:{ "collectionName": collectionName } }} + + + + +
+
+ + + +
+
+
+
+
+
+
diff --git a/src/app/submission/sections/upload/section-upload.component.scss b/src/app/submission/sections/upload/section-upload.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/submission/sections/upload/section-upload.component.ts b/src/app/submission/sections/upload/section-upload.component.ts new file mode 100644 index 0000000000..c392c52d8f --- /dev/null +++ b/src/app/submission/sections/upload/section-upload.component.ts @@ -0,0 +1,185 @@ +import { ChangeDetectorRef, Component, Inject, OnInit } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { SectionModelComponent } from '../models/section.model'; +import { hasValue, isNotEmpty, isNotUndefined, isUndefined } from '../../../shared/empty.util'; +import { SectionUploadService } from './section-upload.service'; +import { SectionStatusChangeAction } from '../../objects/submission-objects.actions'; +import { SubmissionState } from '../../submission.reducers'; +import { CollectionDataService } from '../../../core/data/collection-data.service'; +import { GroupEpersonService } from '../../../core/eperson/group-eperson.service'; +import { SubmissionUploadsConfigService } from '../../../core/config/submission-uploads-config.service'; +import { SubmissionUploadsModel } from '../../../core/shared/config/config-submission-uploads.model'; +import { Observable } from 'rxjs/Observable'; +import { EpersonData } from '../../../core/eperson/eperson-data'; +import { SubmissionFormsModel } from '../../../core/shared/config/config-submission-forms.model'; +import { SectionsType } from '../sections-type'; +import { renderSectionFor } from '../sections-decorator'; +import { SectionDataObject } from '../models/section-data.model'; +import { submissionObjectFromIdSelector } from '../../selectors'; +import { SubmissionObjectEntry } from '../../objects/submission-objects.reducer'; +import { AlertType } from '../../../shared/alerts/aletrs-type'; + +export const POLICY_DEFAULT_NO_LIST = 1; // Banner1 +export const POLICY_DEFAULT_WITH_LIST = 2; // Banner2 + +@Component({ + selector: 'ds-submission-section-upload', + styleUrls: ['./section-upload.component.scss'], + templateUrl: './section-upload.component.html', +}) +@renderSectionFor(SectionsType.Upload) +export class UploadSectionComponent extends SectionModelComponent implements OnInit { + + public AlertTypeEnum = AlertType; + public fileIndexes = []; + public fileList = []; + public fileNames = []; + + public collectionName: string; + + /* + * Default access conditions of this collection + */ + public collectionDefaultAccessConditions: any[] = []; + + /* + * The collection access conditions policy + */ + public collectionPolicyType; + + public configMetadataForm: SubmissionFormsModel; + + /* + * List of available access conditions that could be setted to files + */ + public availableAccessConditionOptions: any[]; // List of accessConditions that an user can select + + /* + * List of Groups available for every access condition + */ + protected availableGroups: Map; // Groups for any policy + + protected subs = []; + + constructor(private bitstreamService: SectionUploadService, + private changeDetectorRef: ChangeDetectorRef, + private collectionDataService: CollectionDataService, + private groupService: GroupEpersonService, + private store: Store, + private uploadsConfigService: SubmissionUploadsConfigService, + @Inject('sectionDataProvider') public injectedSectionData: SectionDataObject, + @Inject('submissionIdProvider') public injectedSubmissionId: string) { + super(undefined, injectedSectionData, injectedSubmissionId); + } + + ngOnInit() { + this.subs.push( + this.store.select(submissionObjectFromIdSelector(this.submissionId)) + .filter((submissionObject: SubmissionObjectEntry) => isNotUndefined(submissionObject) && !submissionObject.isLoading) + .filter((submissionObject: SubmissionObjectEntry) => isUndefined(this.collectionId) || this.collectionId !== submissionObject.collection) + .subscribe((submissionObject: SubmissionObjectEntry) => { + this.collectionId = submissionObject.collection; + this.collectionDataService.findById(this.collectionId) + .filter((collectionData) => isNotUndefined((collectionData.payload))) + .take(1) + .subscribe((collectionData) => { + this.collectionName = collectionData.payload.name; + + // Default Access Conditions + this.subs.push(collectionData.payload.defaultAccessConditions + .filter((accessConditions) => isNotUndefined((accessConditions.payload))) + .take(1) + .subscribe((defaultAccessConditions) => { + + if (isNotEmpty(defaultAccessConditions.payload)) { + this.collectionDefaultAccessConditions = Array.isArray(defaultAccessConditions.payload) + ? defaultAccessConditions.payload : [defaultAccessConditions.payload]; + } + + // Edit Form Configuration, access policy list + this.subs.push(this.uploadsConfigService.getConfigByHref(this.sectionData.config) + .flatMap((config) => config.payload) + .take(1) + .subscribe((config: SubmissionUploadsModel) => { + this.availableAccessConditionOptions = isNotEmpty(config.accessConditionOptions) ? config.accessConditionOptions : []; + + this.configMetadataForm = config.metadata[0]; + this.collectionPolicyType = this.availableAccessConditionOptions.length > 0 + ? POLICY_DEFAULT_WITH_LIST + : POLICY_DEFAULT_NO_LIST; + + this.availableGroups = new Map(); + const groupsObs = []; + // Retrieve Groups for accessConditionPolicies + this.availableAccessConditionOptions.forEach((accessCondition) => { + if (accessCondition.hasEndDate === true || accessCondition.hasStartDate === true) { + groupsObs.push(this.groupService.getDataByUuid(accessCondition.groupUUID) + ); + } + }); + let obsCounter = 1; + Observable.merge(groupsObs) + .flatMap((group) => group) + .take(groupsObs.length) + .subscribe((data: EpersonData) => { + const group = data.payload[0] as any; + if (isUndefined(this.availableGroups.get(group.uuid))) { + if (Array.isArray(group.groups)) { + const groupArrayData = []; + for (const groupData of group.groups) { + groupArrayData.push({name: groupData.name, uuid: groupData.uuid}); + } + this.availableGroups.set(group.uuid, groupArrayData); + } else { + this.availableGroups.set(group.uuid, {name: group.name, uuid: group.uuid}); + } + } + if (obsCounter++ === groupsObs.length) { + this.changeDetectorRef.detectChanges(); + } + }) + }) + ); + }) + ); + }) + }) + , + this.bitstreamService + .getUploadedFileList(this.submissionId, this.sectionData.id) + .filter((bitstreamList) => isNotUndefined(bitstreamList)) + .distinctUntilChanged() + .subscribe((fileList) => { + let sectionStatus = false; + this.fileList = []; + this.fileIndexes = []; + this.fileNames = []; + if (isNotUndefined(fileList) && Object.keys(fileList).length > 0) { + Object.keys(fileList) + .forEach((key) => { + this.fileList.push(fileList[key]); + this.fileIndexes.push(fileList[key].uuid); + const fileName = fileList[key].metadata['dc.title'][0].display || fileList[key].uuid; + this.fileNames.push(fileName); + }); + sectionStatus = true; + } + this.store.dispatch(new SectionStatusChangeAction(this.submissionId, + this.sectionData.id, + sectionStatus)); + this.changeDetectorRef.detectChanges(); + } + ) + ); + } + + /** + * Method provided by Angular. Invoked when the instance is destroyed. + */ + ngOnDestroy() { + this.subs + .filter((subscription) => hasValue(subscription)) + .forEach((subscription) => subscription.unsubscribe()); + } + +} diff --git a/src/app/submission/sections/upload/section-upload.service.ts b/src/app/submission/sections/upload/section-upload.service.ts new file mode 100644 index 0000000000..3e0a1735ef --- /dev/null +++ b/src/app/submission/sections/upload/section-upload.service.ts @@ -0,0 +1,60 @@ +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs/Observable'; +import { Store } from '@ngrx/store'; +import { SubmissionState } from '../../submission.reducers'; +import { DeleteUploadedFileAction, EditFileDataAction, NewUploadedFileAction } from '../../objects/submission-objects.actions'; +import { + submissionUploadedFileFromUuidSelector, + submissionUploadedFilesFromIdSelector +} from '../../selectors'; +import { isUndefined } from '../../../shared/empty.util'; +import { WorkspaceitemSectionUploadFileObject } from '../../../core/submission/models/workspaceitem-section-upload-file.model'; + +@Injectable() +export class SectionUploadService { + + constructor(private store: Store) {} + + public getUploadedFileList(submissionId: string, sectionId: string): Observable { + return this.store.select(submissionUploadedFilesFromIdSelector(submissionId, sectionId)) + .map((state) => state) + .distinctUntilChanged(); + } + + public getFileData(submissionId: string, sectionId: string, fileUuid: string): Observable { + return this.store.select(submissionUploadedFilesFromIdSelector(submissionId, sectionId)) + .filter((state) => !isUndefined(state)) + .map((state) => { + let fileState; + Object.keys(state) + .filter((key) => state[key].uuid === fileUuid) + .forEach((key) => fileState = state[key]); + return fileState; + }) + .distinctUntilChanged(); + } + + public getDefaultPolicies(submissionId: string, sectionId: string, fileId: string): Observable { + return this.store.select(submissionUploadedFileFromUuidSelector(submissionId, sectionId, fileId)) + .map((state) => state) + .distinctUntilChanged(); + } + + public addUploadedFile(submissionId: string, sectionId: string, fileId: string, data: WorkspaceitemSectionUploadFileObject) { + this.store.dispatch( + new NewUploadedFileAction(submissionId, sectionId, fileId, data) + ); + } + + public updateFileData(submissionId: string, sectionId: string, fileId: string, data: WorkspaceitemSectionUploadFileObject) { + this.store.dispatch( + new EditFileDataAction(submissionId, sectionId, fileId, data) + ); + } + + public removeUploadedFile(submissionId: string, sectionId: string, fileId: string) { + this.store.dispatch( + new DeleteUploadedFileAction(submissionId, sectionId, fileId) + ); + } +} diff --git a/src/app/submission/selectors.ts b/src/app/submission/selectors.ts new file mode 100644 index 0000000000..b52c44b7b1 --- /dev/null +++ b/src/app/submission/selectors.ts @@ -0,0 +1,60 @@ +import { createSelector, MemoizedSelector, Selector } from '@ngrx/store'; + +import { hasValue } from '../shared/empty.util'; +import { submissionSelector, SubmissionState } from './submission.reducers'; +import { SubmissionObjectEntry, SubmissionSectionObject } from './objects/submission-objects.reducer'; + +// @TODO: Merge with keySelector function present in 'src/app/core/shared/selectors.ts' +export function keySelector(parentSelector: Selector, subState: string, key: string): MemoizedSelector { + return createSelector(parentSelector, (state: T) => { + if (hasValue(state) && hasValue(state[subState])) { + return state[subState][key]; + } else { + return undefined; + } + }); +} + +export function subStateSelector(parentSelector: Selector, subState: string): MemoizedSelector { + return createSelector(parentSelector, (state: T) => { + if (hasValue(state) && hasValue(state[subState])) { + return state[subState]; + } else { + return undefined; + } + }); +} + +export function submissionObjectFromIdSelector(submissionId: string): MemoizedSelector { + return keySelector(submissionSelector, 'objects', submissionId); +} + +export function submissionObjectSectionsFromIdSelector(submissionId: string): MemoizedSelector { + const submissionObjectSelector = submissionObjectFromIdSelector(submissionId); + return subStateSelector(submissionObjectSelector, 'sections'); +} + +export function submissionUploadedFilesFromIdSelector(submissionId: string, sectionId: string): MemoizedSelector { + const sectionDataSelector = submissionSectionDataFromIdSelector(submissionId, sectionId); + return subStateSelector(sectionDataSelector, 'files'); +} + +export function submissionUploadedFileFromUuidSelector(submissionId: string, sectionId: string, uuid: string): MemoizedSelector { + const filesSelector = submissionSectionDataFromIdSelector(submissionId, sectionId); + return keySelector(filesSelector, 'files', uuid); +} + +export function submissionSectionFromIdSelector(submissionId: string, sectionId: string): MemoizedSelector { + const submissionIdSelector = submissionObjectFromIdSelector(submissionId); + return keySelector(submissionIdSelector, 'sections', sectionId); +} + +export function submissionSectionDataFromIdSelector(submissionId: string, sectionId: string): MemoizedSelector { + const submissionIdSelector = submissionSectionFromIdSelector(submissionId, sectionId); + return subStateSelector(submissionIdSelector, 'data'); +} + +export function submissionSectionErrorsFromIdSelector(submissionId: string, sectionId: string): MemoizedSelector { + const submissionIdSelector = submissionSectionFromIdSelector(submissionId, sectionId); + return subStateSelector(submissionIdSelector, 'errors'); +} diff --git a/src/app/submission/server-submission.service.ts b/src/app/submission/server-submission.service.ts new file mode 100644 index 0000000000..cf3500fb94 --- /dev/null +++ b/src/app/submission/server-submission.service.ts @@ -0,0 +1,24 @@ +import { Injectable } from '@angular/core'; +import { SubmissionService } from './submission.service'; +import { SubmissionObject } from '../core/submission/models/submission-object.model'; +import { Observable } from 'rxjs/Observable'; + +@Injectable() +export class ServerSubmissionService extends SubmissionService { + + createSubmission(): Observable { + return Observable.of(null); + } + + retrieveSubmission(submissionId): Observable { + return Observable.of(null); + } + + startAutoSave(submissionId) { + return; + } + + stopAutoSave() { + return; + } +} diff --git a/src/app/submission/submission-rest.service.ts b/src/app/submission/submission-rest.service.ts new file mode 100644 index 0000000000..019b377fdd --- /dev/null +++ b/src/app/submission/submission-rest.service.ts @@ -0,0 +1,124 @@ +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs/Observable'; +import { Store } from '@ngrx/store'; + +import { ResponseCacheService } from '../core/cache/response-cache.service'; +import { RequestService } from '../core/data/request.service'; +import { ResponseCacheEntry } from '../core/cache/response-cache.reducer'; +import { + ErrorResponse, + PostPatchSuccessResponse, + RestResponse, + SubmissionSuccessResponse +} from '../core/cache/response-cache.models'; +import { isNotEmpty } from '../shared/empty.util'; +import { + ConfigRequest, + DeleteRequest, + PostRequest, + RestRequest, + SubmissionDeleteRequest, + SubmissionPatchRequest, + SubmissionPostRequest, + SubmissionRequest +} from '../core/data/request.models'; +import { SubmitDataResponseDefinitionObject } from '../core/shared/submit-data-response-definition.model'; +import { CoreState } from '../core/core.reducers'; +import { HttpOptions } from '../core/dspace-rest-v2/dspace-rest-v2.service'; +import { HALEndpointService } from '../core/shared/hal-endpoint.service'; + +@Injectable() +export class SubmissionRestService { + protected linkPath = 'workspaceitems'; + protected overrideRequest = true; + + constructor( + protected responseCache: ResponseCacheService, + protected requestService: RequestService, + protected store: Store, + protected halService: HALEndpointService) { + } + + protected submitData(request: RestRequest): Observable { + const [successResponse, errorResponse] = this.responseCache.get(request.href) + .map((entry: ResponseCacheEntry) => entry.response) + .partition((response: RestResponse) => response.isSuccessful); + return Observable.merge( + errorResponse.flatMap((response: ErrorResponse) => + Observable.throw(new Error(`Couldn't send data to server`))), + successResponse + .filter((response: PostPatchSuccessResponse) => isNotEmpty(response)) + .map((response: PostPatchSuccessResponse) => response.dataDefinition) + .distinctUntilChanged()); + } + + protected fetchRequest(request: RestRequest): Observable { + const [successResponse, errorResponse] = this.responseCache.get(request.href) + .map((entry: ResponseCacheEntry) => entry.response) + .do(() => this.responseCache.remove(request.href)) + .partition((response: RestResponse) => response.isSuccessful); + return Observable.merge( + errorResponse.flatMap((response: ErrorResponse) => + Observable.throw(new Error(`Couldn't retrieve the data`))), + successResponse + .filter((response: SubmissionSuccessResponse) => isNotEmpty(response)) + .map((response: SubmissionSuccessResponse) => response.dataDefinition) + .distinctUntilChanged()); + } + + protected getEndpointByIDHref(endpoint, resourceID): string { + return isNotEmpty(resourceID) ? `${endpoint}/${resourceID}` : `${endpoint}`; + } + + public deleteById(scopeId: string, linkName?: string): Observable { + return this.halService.getEndpoint(linkName || this.linkPath) + .filter((href: string) => isNotEmpty(href)) + .distinctUntilChanged() + .map((endpointURL: string) => this.getEndpointByIDHref(endpointURL, scopeId)) + .map((endpointURL: string) => new SubmissionDeleteRequest(this.requestService.generateRequestId(), endpointURL)) + .do((request: DeleteRequest) => this.requestService.configure(request)) + .flatMap((request: DeleteRequest) => this.submitData(request)) + .distinctUntilChanged(); + } + + public getDataByHref(href: string, options?: HttpOptions): Observable { + const request = new ConfigRequest(this.requestService.generateRequestId(), href, options); + this.requestService.configure(request, true); + + return this.fetchRequest(request); + } + + public getDataById(linkName: string, id: string): Observable { + return this.halService.getEndpoint(linkName) + .map((endpointURL: string) => this.getEndpointByIDHref(endpointURL, id)) + .filter((href: string) => isNotEmpty(href)) + .distinctUntilChanged() + .map((endpointURL: string) => new SubmissionRequest(this.requestService.generateRequestId(), endpointURL)) + .do((request: RestRequest) => this.requestService.configure(request, true)) + .flatMap((request: RestRequest) => this.fetchRequest(request)) + .distinctUntilChanged(); + } + + public postToEndpoint(linkName: string, body: any, scopeId?: string, options?: HttpOptions): Observable { + return this.halService.getEndpoint(linkName) + .filter((href: string) => isNotEmpty(href)) + .map((endpointURL: string) => this.getEndpointByIDHref(endpointURL, scopeId)) + .distinctUntilChanged() + .map((endpointURL: string) => new SubmissionPostRequest(this.requestService.generateRequestId(), endpointURL, body, options)) + .do((request: PostRequest) => this.requestService.configure(request, true)) + .flatMap((request: PostRequest) => this.submitData(request)) + .distinctUntilChanged(); + } + + public patchToEndpoint(linkName: string, body: any, scopeId?: string): Observable { + return this.halService.getEndpoint(linkName) + .filter((href: string) => isNotEmpty(href)) + .map((endpointURL: string) => this.getEndpointByIDHref(endpointURL, scopeId)) + .distinctUntilChanged() + .map((endpointURL: string) => new SubmissionPatchRequest(this.requestService.generateRequestId(), endpointURL, body)) + .do((request: PostRequest) => this.requestService.configure(request, true)) + .flatMap((request: PostRequest) => this.submitData(request)) + .distinctUntilChanged(); + } + +} diff --git a/src/app/submission/submission.effects.ts b/src/app/submission/submission.effects.ts new file mode 100644 index 0000000000..30e01451d1 --- /dev/null +++ b/src/app/submission/submission.effects.ts @@ -0,0 +1,5 @@ +import { SubmissionObjectEffects } from './objects/submission-objects.effects'; + +export const submissionEffects = [ + SubmissionObjectEffects +]; diff --git a/src/app/submission/submission.module.ts b/src/app/submission/submission.module.ts new file mode 100644 index 0000000000..d1a802e0f8 --- /dev/null +++ b/src/app/submission/submission.module.ts @@ -0,0 +1,90 @@ +import { NgModule } from '@angular/core'; +import { TranslateModule } from '@ngx-translate/core'; +import { CoreModule } from '../core/core.module'; +import { SharedModule } from '../shared/shared.module'; + +import { FormSectionComponent } from './sections/form/section-form.component'; +import { SectionsDirective } from './sections/sections.directive'; +import { SectionsService } from './sections/sections.service'; +import { DefaultSectionComponent } from './sections/default/section-default.component'; +import { SubmissionFormCollectionComponent } from './form/collection/submission-form-collection.component'; +import { SubmissionFormFooterComponent } from './form/footer/submission-form-footer.component'; +import { SubmissionFormComponent } from './form/submission-form.component'; +import { SubmissionFormSectionAddComponent } from './form/section-add/submission-form-section-add.component'; +import { SectionContainerComponent } from './sections/container/section-container.component'; +import { CommonModule } from '@angular/common'; +import { StoreModule } from '@ngrx/store'; +import { EffectsModule } from '@ngrx/effects'; +import { submissionReducers } from './submission.reducers'; +import { submissionEffects } from './submission.effects'; +import { UploadSectionComponent } from './sections/upload/section-upload.component'; +import { SectionUploadService } from './sections/upload/section-upload.service'; +import { SubmissionUploadFilesComponent } from './form/submission-upload-files/submission-upload-files.component'; +import { SubmissionRestService } from './submission-rest.service'; +import { LicenseSectionComponent } from './sections/license/section-license.component'; +import { SubmissionUploadsConfigService } from '../core/config/submission-uploads-config.service'; +import { SubmissionEditComponent } from './edit/submission-edit.component'; +import { UploadSectionFileComponent } from './sections/upload/file/file.component'; +import { UploadSectionFileEditComponent } from './sections/upload/file/edit/file-edit.component'; +import { UploadSectionFileViewComponent } from './sections/upload/file/view/file-view.component'; +import { AccessConditionsComponent } from './sections/upload/accessConditions/accessConditions.component'; +import { RecycleSectionComponent } from './sections/recycle/section-recycle.component'; +import { DeduplicationSectionComponent } from './sections/deduplication/section-deduplication.component'; +import { DeduplicationMatchComponent } from './sections/deduplication/match/deduplication-match.component'; +import { DeduplicationService } from './sections/deduplication/deduplication.service'; +import { SubmissionSubmitComponent } from './submit/submission-submit.component'; + +@NgModule({ + imports: [ + CommonModule, + CoreModule, + SharedModule, + StoreModule.forFeature('submission', submissionReducers, {}), + EffectsModule.forFeature(submissionEffects), + TranslateModule + ], + declarations: [ + AccessConditionsComponent, + DefaultSectionComponent, + UploadSectionComponent, + FormSectionComponent, + LicenseSectionComponent, + SectionsDirective, + SectionContainerComponent, + SubmissionEditComponent, + SubmissionFormSectionAddComponent, + SubmissionFormCollectionComponent, + SubmissionFormComponent, + SubmissionFormFooterComponent, + SubmissionSubmitComponent, + SubmissionUploadFilesComponent, + UploadSectionFileComponent, + UploadSectionFileEditComponent, + UploadSectionFileViewComponent, + RecycleSectionComponent, + DeduplicationSectionComponent, + DeduplicationMatchComponent, + ], + entryComponents: [ + DefaultSectionComponent, + UploadSectionComponent, + FormSectionComponent, + LicenseSectionComponent, + SectionContainerComponent, + RecycleSectionComponent, + DeduplicationSectionComponent], + exports: [ + SubmissionEditComponent, + SubmissionFormComponent, + SubmissionSubmitComponent + ], + providers: [ + SectionUploadService, + SectionsService, + SubmissionRestService, + SubmissionUploadsConfigService, + DeduplicationService + ] +}) +export class SubmissionModule { +} diff --git a/src/app/submission/submission.reducers.ts b/src/app/submission/submission.reducers.ts new file mode 100644 index 0000000000..39069b2917 --- /dev/null +++ b/src/app/submission/submission.reducers.ts @@ -0,0 +1,16 @@ +import { ActionReducerMap, createFeatureSelector } from '@ngrx/store'; + +import { + submissionObjectReducer, + SubmissionObjectState +} from './objects/submission-objects.reducer'; + +export interface SubmissionState { + 'objects': SubmissionObjectState +} + +export const submissionReducers: ActionReducerMap = { + objects: submissionObjectReducer, +}; + +export const submissionSelector = createFeatureSelector('submission'); diff --git a/src/app/submission/submission.service.ts b/src/app/submission/submission.service.ts new file mode 100644 index 0000000000..cef39c9a52 --- /dev/null +++ b/src/app/submission/submission.service.ts @@ -0,0 +1,223 @@ +import { Inject, Injectable } from '@angular/core'; +import { Observable } from 'rxjs/Observable'; +import { Subscription } from 'rxjs/Subscription'; +import { Store } from '@ngrx/store'; + +import { submissionSelector, SubmissionState } from './submission.reducers'; +import { hasValue, isEmpty, isNotUndefined } from '../shared/empty.util'; +import { SaveSubmissionFormAction } from './objects/submission-objects.actions'; +import { + SubmissionObjectEntry, + SubmissionSectionEntry, + SubmissionSectionObject +} from './objects/submission-objects.reducer'; +import { submissionObjectFromIdSelector } from './selectors'; +import { GlobalConfig } from '../../config/global-config.interface'; +import { GLOBAL_CONFIG } from '../../config'; +import { HttpHeaders } from '@angular/common/http'; +import { HttpOptions } from '../core/dspace-rest-v2/dspace-rest-v2.service'; +import { SubmissionRestService } from './submission-rest.service'; +import { Router } from '@angular/router'; +import { SectionDataObject } from './sections/models/section-data.model'; +import { SubmissionScopeType } from '../core/submission/submission-scope-type'; +import { SubmissionObject } from '../core/submission/models/submission-object.model'; + +@Injectable() +export class SubmissionService { + + protected autoSaveSub: Subscription; + protected timerObs: Observable; + + constructor(@Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig, + protected restService: SubmissionRestService, + protected router: Router, + protected store: Store) { + } + + createSubmission(): Observable { + return this.restService.postToEndpoint('workspaceitems', {}) + .map((workspaceitems) => workspaceitems[0]) + .catch(() => Observable.of({})) + } + + depositSubmission(selfUrl: string): Observable { + const options: HttpOptions = Object.create({}); + let headers = new HttpHeaders(); + headers = headers.append('Content-Type', 'text/uri-list'); + options.headers = headers; + return this.restService.postToEndpoint('workflowitems', selfUrl, null, options); + } + + discardSubmission(submissionId: string): Observable { + return this.restService.deleteById(submissionId); + } + + getActiveSectionId(submissionId: string): Observable { + return this.getSubmissionObject(submissionId) + .map((submission: SubmissionObjectEntry) => submission.activeSection); + } + + getSubmissionObject(submissionId: string): Observable { + return this.store.select(submissionObjectFromIdSelector(submissionId)) + .filter((submission: SubmissionObjectEntry) => isNotUndefined(submission)) + } + + getSubmissionSections(submissionId: string): Observable { + return this.getSubmissionObject(submissionId) + .filter((submission: SubmissionObjectEntry) => isNotUndefined(submission.sections) && !submission.isLoading) + .take(1) + .map((submission: SubmissionObjectEntry) => submission.sections) + .map((sections: SubmissionSectionEntry) => { + const availableSections: SectionDataObject[] = []; + Object.keys(sections) + .filter((sectionId) => !this.isSectionHidden(sections[sectionId] as SubmissionSectionObject)) + .forEach((sectionId) => { + const sectionObject: SectionDataObject = Object.create({}); + sectionObject.config = sections[sectionId].config; + sectionObject.mandatory = sections[sectionId].mandatory; + sectionObject.data = sections[sectionId].data; + sectionObject.errors = sections[sectionId].errors; + sectionObject.header = sections[sectionId].header; + sectionObject.id = sectionId; + sectionObject.sectionType = sections[sectionId].sectionType; + availableSections.push(sectionObject); + }); + return availableSections; + }) + .startWith([]) + .distinctUntilChanged(); + } + + getDisabledSectionsList(submissionId: string): Observable { + return this.getSubmissionObject(submissionId) + .filter((submission: SubmissionObjectEntry) => isNotUndefined(submission.sections) && !submission.isLoading) + .map((submission: SubmissionObjectEntry) => submission.sections) + .map((sections: SubmissionSectionEntry) => { + const disabledSections: SectionDataObject[] = []; + Object.keys(sections) + .filter((sectionId) => !this.isSectionHidden(sections[sectionId] as SubmissionSectionObject)) + .filter((sectionId) => !sections[sectionId].enabled) + .forEach((sectionId) => { + const sectionObject: SectionDataObject = Object.create({}); + sectionObject.header = sections[sectionId].header; + sectionObject.id = sectionId; + disabledSections.push(sectionObject); + }); + return disabledSections; + }) + .startWith([]) + .distinctUntilChanged(); + } + + isSectionHidden(sectionData: SubmissionSectionObject) { + return (isNotUndefined(sectionData.visibility) + && sectionData.visibility.main === 'HIDDEN' + && sectionData.visibility.other === 'HIDDEN'); + + } + + isSubmissionLoading(submissionId: string): Observable { + return this.getSubmissionObject(submissionId) + .map((submission: SubmissionObjectEntry) => submission.isLoading) + .distinctUntilChanged() + } + + getSubmissionObjectLinkName(): string { + const url = this.router.routerState.snapshot.url; + if (url.startsWith('/workspaceitems') || url.startsWith('/submit')) { + return 'workspaceitems'; + } else if (url.startsWith('/workflowitems')) { + return 'workflowitems'; + } else { + return 'edititems'; + } + } + + getSubmissionScope(): SubmissionScopeType { + let scope: SubmissionScopeType; + switch (this.getSubmissionObjectLinkName()) { + case 'workspaceitems': + scope = SubmissionScopeType.WorkspaceItem; + break; + case 'workflowitems': + scope = SubmissionScopeType.WorkflowItem; + break; + case 'edititems': + scope = SubmissionScopeType.EditItem; + break; + } + return scope; + } + + getSectionsState(submissionId: string): Observable { + return this.store.select(submissionSelector) + .map((submissions: SubmissionState) => submissions.objects[submissionId]) + .filter((item) => isNotUndefined(item) && isNotUndefined(item.sections)) + .map((item) => item.sections) + .map((sections) => { + const states = []; + + if (isNotUndefined(sections)) { + Object.keys(sections) + .filter((sectionId) => sections.hasOwnProperty(sectionId)) + .filter((sectionId) => !this.isSectionHidden(sections[sectionId] as SubmissionSectionObject)) + .filter((sectionId) => sections[sectionId].enabled) + .filter((sectionId) => sections[sectionId].isValid === false) + .forEach((sectionId) => { + states.push(sections[sectionId].isValid); + }); + } + + return !isEmpty(sections) && isEmpty(states); + }) + .distinctUntilChanged() + .startWith(false); + } + + getSubmissionSaveProcessingStatus(submissionId: string): Observable { + return this.store.select(submissionObjectFromIdSelector(submissionId)) + .filter((state: SubmissionObjectEntry) => isNotUndefined(state)) + .map((state: SubmissionObjectEntry) => state.savePending) + .distinctUntilChanged() + .startWith(false); + } + + getSubmissionDepositProcessingStatus(submissionId: string): Observable { + return this.store.select(submissionObjectFromIdSelector(submissionId)) + .filter((state: SubmissionObjectEntry) => isNotUndefined(state)) + .map((state: SubmissionObjectEntry) => state.depositPending) + .distinctUntilChanged() + .startWith(false); + } + + redirectToMyDSpace() { + this.router.navigate(['/mydspace']); + } + + retrieveSubmission(submissionId): Observable { + return this.restService.getDataById(this.getSubmissionObjectLinkName(), submissionId) + .filter((submissionObjects: SubmissionObject[]) => isNotUndefined(submissionObjects)) + .take(1) + .map((submissionObjects: SubmissionObject[]) => submissionObjects[0]); + } + + startAutoSave(submissionId) { + this.stopAutoSave(); + console.log('AUTOSAVE ON!!!'); + // AUTOSAVE submission + // Retrieve interval from config and convert to milliseconds + const duration = this.EnvConfig.submission.autosave.timer * (1000 * 60); + // Dispatch save action after given duration + this.timerObs = Observable.timer(duration, duration); + this.autoSaveSub = this.timerObs + .subscribe(() => this.store.dispatch(new SaveSubmissionFormAction(submissionId))); + } + + stopAutoSave() { + if (hasValue(this.autoSaveSub)) { + console.log('AUTOSAVE OFFF!!!'); + this.autoSaveSub.unsubscribe(); + this.autoSaveSub = null; + } + } +} diff --git a/src/app/submission/submit/submission-submit.component.html b/src/app/submission/submit/submission-submit.component.html new file mode 100644 index 0000000000..7cd58069ac --- /dev/null +++ b/src/app/submission/submit/submission-submit.component.html @@ -0,0 +1,8 @@ +
+
+ +
+
diff --git a/src/app/submission/submit/submission-submit.component.scss b/src/app/submission/submit/submission-submit.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/submission/submit/submission-submit.component.ts b/src/app/submission/submit/submission-submit.component.ts new file mode 100644 index 0000000000..f761a9b193 --- /dev/null +++ b/src/app/submission/submit/submission-submit.component.ts @@ -0,0 +1,67 @@ +import { ChangeDetectorRef, Component, OnDestroy, OnInit, ViewContainerRef } from '@angular/core'; +import { Router } from '@angular/router'; + +import { Subscription } from 'rxjs/Subscription'; + +import { hasValue, isEmpty, isNotNull } from '../../shared/empty.util'; +import { SubmissionDefinitionsModel } from '../../core/shared/config/config-submission-definitions.model'; +import { TranslateService } from '@ngx-translate/core'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { SubmissionService } from '../submission.service'; +import { SubmissionObject } from '../../core/submission/models/submission-object.model'; + +@Component({ + selector: 'ds-submit-page', + styleUrls: ['./submission-submit.component.scss'], + templateUrl: './submission-submit.component.html' +}) +export class SubmissionSubmitComponent implements OnDestroy, OnInit { + + public collectionId: string; + public model: any; + public selfUrl: string; + public submissionDefinition: SubmissionDefinitionsModel; + public submissionId: string; + + protected subs: Subscription[] = []; + + constructor(private changeDetectorRef: ChangeDetectorRef, + private notificationsService: NotificationsService, + private router: Router, + private submissioService: SubmissionService, + private translate: TranslateService, + private viewContainerRef: ViewContainerRef) { + } + + ngOnInit() { + // NOTE execute the code on the browser side only, otherwise it is executed twice + this.subs.push( + this.submissioService.createSubmission() + .subscribe((submissionObject: SubmissionObject) => { + // NOTE new submission is created on the browser side only + if (isNotNull(submissionObject)) { + if (isEmpty(submissionObject)) { + this.notificationsService.info(null, this.translate.get('submission.general.cannot_submit')); + this.router.navigate(['/mydspace']); + } else { + this.collectionId = submissionObject.collection[0].id; + this.selfUrl = submissionObject.self; + this.submissionDefinition = submissionObject.submissionDefinition[0]; + this.submissionId = submissionObject.id; + this.changeDetectorRef.detectChanges(); + } + } + }) + ) + } + + ngOnDestroy() { + this.subs + .filter((subscription) => hasValue(subscription)) + .forEach((subscription) => subscription.unsubscribe()); + + this.viewContainerRef.clear(); + this.changeDetectorRef.markForCheck(); + } + +} diff --git a/src/app/submission/utils/parseSectionErrorPaths.ts b/src/app/submission/utils/parseSectionErrorPaths.ts new file mode 100644 index 0000000000..b47b9d0b05 --- /dev/null +++ b/src/app/submission/utils/parseSectionErrorPaths.ts @@ -0,0 +1,41 @@ +import { hasValue } from '../../shared/empty.util'; + +export interface SectionErrorPath { + sectionId: string; + fieldId?: string; + fieldIndex?: number; + originalPath: string; +} + +const regex = /([^\/]+)/g; +// const regex = /\/sections\/(.*)\/(.*)\/(.*)/; +const regexShort = /\/sections\/(.*)/; + +/** + * the following method accept an array of section path strings and return a path object + * @param {string | string[]} path + * @returns {SectionErrorPath[]} + */ +const parseSectionErrorPaths = (path: string | string[]): SectionErrorPath[] => { + const paths = typeof path === 'string' ? [path] : path; + + return paths.map((item) => { + if (item.match(regex) && item.match(regex).length > 2) { + return { + sectionId: item.match(regex)[1], + fieldId: item.match(regex)[2], + fieldIndex: hasValue(item.match(regex)[3]) ? +item.match(regex)[3] : 0, + originalPath: item, + }; + } else { + return { + sectionId: item.match(regexShort)[1], + originalPath: item, + }; + } + + } + ); +}; + +export default parseSectionErrorPaths; diff --git a/src/app/submission/utils/parseSectionErrors.ts b/src/app/submission/utils/parseSectionErrors.ts new file mode 100644 index 0000000000..5f2867c8b8 --- /dev/null +++ b/src/app/submission/utils/parseSectionErrors.ts @@ -0,0 +1,27 @@ +import { SubmissionObjectError } from '../../core/submission/models/submission-object.model'; +import { default as parseSectionErrorPaths, SectionErrorPath } from './parseSectionErrorPaths'; + +/** + * the following method accept an array of SubmissionObjectError and return a section errors object + * @param {errors: SubmissionObjectError[]} errors + * @returns {any} + */ +const parseSectionErrors = (errors: SubmissionObjectError[] = []): any => { + const errorsList = Object.create({}); + + errors.forEach((error: SubmissionObjectError) => { + const paths: SectionErrorPath[] = parseSectionErrorPaths(error.paths); + + paths.forEach((path: SectionErrorPath) => { + const sectionError = {path: path.originalPath, message: error.message}; + if (!errorsList[path.sectionId]) { + errorsList[path.sectionId] = []; + } + errorsList[path.sectionId].push(sectionError); + }); + }); + + return errorsList; +}; + +export default parseSectionErrors; diff --git a/src/config/global-config.interface.ts b/src/config/global-config.interface.ts index b623a4bf8c..3062d0bd44 100644 --- a/src/config/global-config.interface.ts +++ b/src/config/global-config.interface.ts @@ -3,6 +3,7 @@ import { ServerConfig } from './server-config.interface'; import { CacheConfig } from './cache-config.interface'; import { UniversalConfig } from './universal-config.interface'; import { INotificationBoardOptions } from './notifications-config.interfaces'; +import { SubmissionConfig } from './submission-config.interface'; import { FormConfig } from './form-config.interfaces'; export interface GlobalConfig extends Config { @@ -12,6 +13,7 @@ export interface GlobalConfig extends Config { cache: CacheConfig; form: FormConfig; notifications: INotificationBoardOptions; + submission: SubmissionConfig; universal: UniversalConfig; gaTrackingId: string; logDirectory: string; diff --git a/src/config/submission-config.interface.ts b/src/config/submission-config.interface.ts new file mode 100644 index 0000000000..2def1dbf1e --- /dev/null +++ b/src/config/submission-config.interface.ts @@ -0,0 +1,16 @@ +import { Config } from './config.interface'; +import { MetadataIconsConfig } from '../app/shared/chips/models/chips.model'; + +interface AutosaveConfig extends Config { + metadata: string[], + timer: number +} + +interface MetadataConfig extends Config { + icons: MetadataIconsConfig[] +} + +export interface SubmissionConfig extends Config { + autosave: AutosaveConfig, + metadata: MetadataConfig +} diff --git a/src/modules/app/browser-app.module.ts b/src/modules/app/browser-app.module.ts index a7a59dc837..b20894880b 100644 --- a/src/modules/app/browser-app.module.ts +++ b/src/modules/app/browser-app.module.ts @@ -20,6 +20,8 @@ import { CookieService } from '../../app/shared/services/cookie.service'; import { AuthService } from '../../app/core/auth/auth.service'; import { Angulartics2Module } from 'angulartics2'; import { Angulartics2GoogleAnalytics } from 'angulartics2/ga'; +import { ServerSubmissionService } from '../../app/submission/server-submission.service'; +import { SubmissionService } from '../../app/submission/submission.service'; export const REQ_KEY = makeStateKey('req'); @@ -71,6 +73,10 @@ export function getRequest(transferState: TransferState): any { { provide: CookieService, useClass: ClientCookieService + }, + { + provide: SubmissionService, + useClass: SubmissionService } ] }) diff --git a/src/modules/app/server-app.module.ts b/src/modules/app/server-app.module.ts index 10285e75f5..e8c03e6fd8 100644 --- a/src/modules/app/server-app.module.ts +++ b/src/modules/app/server-app.module.ts @@ -22,6 +22,8 @@ import { ServerAuthService } from '../../app/core/auth/server-auth.service'; import { Angulartics2GoogleAnalytics } from 'angulartics2/ga'; import { AngularticsMock } from '../../app/shared/mocks/mock-angulartics.service'; +import { SubmissionService } from '../../app/submission/submission.service'; +import { ServerSubmissionService } from '../../app/submission/server-submission.service'; export function createTranslateLoader() { return new TranslateUniversalLoader('dist/assets/i18n/', '.json'); @@ -57,7 +59,11 @@ export function createTranslateLoader() { { provide: CookieService, useClass: ServerCookieService - } + }, + { + provide: SubmissionService, + useClass: ServerSubmissionService + }, ] }) export class ServerAppModule { diff --git a/src/routes.ts b/src/routes.ts index 392d3925a5..f3e963b25a 100644 --- a/src/routes.ts +++ b/src/routes.ts @@ -1,10 +1,15 @@ export const ROUTES: string[] = [ 'home', 'items/:id', + 'login', + 'logout', 'collections/:id', 'communities/:id', 'login', 'logout', 'search', + 'submit', + 'workspaceitems/:id/edit', + 'workflowitems/:id/edit', '**' ]; diff --git a/src/styles/_custom_variables.scss b/src/styles/_custom_variables.scss index f378c2b7c9..f6f68eaec1 100644 --- a/src/styles/_custom_variables.scss +++ b/src/styles/_custom_variables.scss @@ -8,4 +8,6 @@ $drop-zone-area-z-index: 1025; $drop-zone-area-inner-z-index: 1021; $login-logo-height:72px; $login-logo-width:72px; +$submission-header-z-index: 1001; +$submission-footer-z-index: 1000; diff --git a/yarn.lock b/yarn.lock index 3207959415..48296826cc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -77,9 +77,9 @@ version "1.0.1" resolved "https://registry.yarnpkg.com/@angularclass/bootloader/-/bootloader-1.0.1.tgz#75de7cf3901b445900a419c2aeca44181d465060" -"@ng-bootstrap/ng-bootstrap@^1.0.0": - version "1.0.0" - resolved "https://registry.yarnpkg.com/@ng-bootstrap/ng-bootstrap/-/ng-bootstrap-1.0.0.tgz#8f2ae70db2fe1dcbf5e0acb49dc2b1bbba2be8d2" +"@ng-bootstrap/ng-bootstrap@1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@ng-bootstrap/ng-bootstrap/-/ng-bootstrap-1.1.2.tgz#276b1c488687ca3e53d1694b63835fd57ca552ca" "@ng-dynamic-forms/core@5.4.7": version "5.4.7" @@ -193,6 +193,10 @@ "@types/express-serve-static-core" "*" "@types/serve-static" "*" +"@types/file-saver@^1.3.0": + version "1.3.0" + resolved "https://registry.yarnpkg.com/@types/file-saver/-/file-saver-1.3.0.tgz#0ef213077e704fc3f4e7a86cfd31c9de4f4f47a7" + "@types/fs-extra@4.0.0": version "4.0.0" resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-4.0.0.tgz#1dd742ad5c9bce308f7a52d02ebc01421bc9102f" @@ -3114,6 +3118,10 @@ fd-slicer@~1.0.1: dependencies: pend "~1.2.0" +file-saver@^1.3.8: + version "1.3.8" + resolved "https://registry.yarnpkg.com/file-saver/-/file-saver-1.3.8.tgz#e68a30c7cb044e2fb362b428469feb291c2e09d8" + file-uri-to-path@1: version "1.0.0" resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd" From f1f11bcdaf9cf6e6c53253f4ebff759505a471f7 Mon Sep 17 00:00:00 2001 From: Giuseppe Date: Thu, 26 Jul 2018 18:47:11 +0200 Subject: [PATCH 028/604] Removed unused model --- .../shared/config/config-authority.model.ts | 23 ------------------- .../shared/config/config-object-factory.ts | 4 ---- 2 files changed, 27 deletions(-) delete mode 100644 src/app/core/shared/config/config-authority.model.ts diff --git a/src/app/core/shared/config/config-authority.model.ts b/src/app/core/shared/config/config-authority.model.ts deleted file mode 100644 index bbb8605bcc..0000000000 --- a/src/app/core/shared/config/config-authority.model.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { autoserialize, autoserializeAs, inheritSerialization } from 'cerialize'; -import { ConfigObject } from './config.model'; -import { SubmissionSectionModel } from './config-submission-section.model'; - -@inheritSerialization(ConfigObject) -export class ConfigAuthorityModel extends ConfigObject { - - @autoserialize - id: string; - - @autoserialize - display: string; - - @autoserialize - value: string; - - @autoserialize - otherInformation: any; - - @autoserialize - language: string; - -} diff --git a/src/app/core/shared/config/config-object-factory.ts b/src/app/core/shared/config/config-object-factory.ts index b43d4456f4..68e2ccbcb3 100644 --- a/src/app/core/shared/config/config-object-factory.ts +++ b/src/app/core/shared/config/config-object-factory.ts @@ -5,7 +5,6 @@ import { SubmissionFormsModel } from './config-submission-forms.model'; import { SubmissionDefinitionsModel } from './config-submission-definitions.model'; import { ConfigType } from './config-type'; import { ConfigObject } from './config.model'; -import { ConfigAuthorityModel } from './config-authority.model'; import { SubmissionUploadsModel } from './config-submission-uploads.model'; export class ConfigObjectFactory { @@ -27,9 +26,6 @@ export class ConfigObjectFactory { case ConfigType.SubmissionUploads: { return SubmissionUploadsModel } - case ConfigType.Authority: { - return ConfigAuthorityModel - } default: { return undefined; } From 9424116b419e5504e2b889f21b2fc43ad6ea832a Mon Sep 17 00:00:00 2001 From: Giuseppe Date: Fri, 27 Jul 2018 14:35:33 +0200 Subject: [PATCH 029/604] Fixed tests --- .../metadata-schema.component.spec.ts | 15 ++--- src/app/+item-page/item-page.module.ts | 2 - .../+login-page/login-page.component.spec.ts | 10 +++- src/app/core/browse/browse.service.spec.ts | 6 +- src/app/core/data/request.service.spec.ts | 60 +++++++++++-------- .../core/metadata/metadata.service.spec.ts | 33 ++++++---- src/app/core/shared/item.model.ts | 4 +- src/app/core/shared/operators.spec.ts | 4 +- .../dynamic-group.component.spec.ts | 10 ++++ src/app/shared/form/form.service.spec.ts | 1 - 10 files changed, 88 insertions(+), 57 deletions(-) diff --git a/src/app/+admin/admin-registries/metadata-schema/metadata-schema.component.spec.ts b/src/app/+admin/admin-registries/metadata-schema/metadata-schema.component.spec.ts index 7e6064ddff..148dba5b88 100644 --- a/src/app/+admin/admin-registries/metadata-schema/metadata-schema.component.spec.ts +++ b/src/app/+admin/admin-registries/metadata-schema/metadata-schema.component.spec.ts @@ -1,16 +1,17 @@ import { MetadataSchemaComponent } from './metadata-schema.component'; import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + import { Observable } from 'rxjs/Observable'; +import 'rxjs/add/observable/of'; + import { RemoteData } from '../../../core/data/remote-data'; import { PaginatedList } from '../../../core/data/paginated-list'; import { MetadataSchema } from '../../../core/metadata/metadataschema.model'; -import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; +import { TranslateModule } from '@ngx-translate/core'; import { CommonModule } from '@angular/common'; import { ActivatedRoute, Router } from '@angular/router'; import { By } from '@angular/platform-browser'; -import { MockTranslateLoader } from '../../../shared/testing/mock-translate-loader'; import { RegistryService } from '../../../core/registry/registry.service'; -import { SharedModule } from '../../../shared/shared.module'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; import { EnumKeysPipe } from '../../../shared/utils/enum-keys-pipe'; import { PaginationComponent } from '../../../shared/pagination/pagination.component'; @@ -86,10 +87,10 @@ describe('MetadataSchemaComponent', () => { imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule.forRoot()], declarations: [MetadataSchemaComponent, PaginationComponent, EnumKeysPipe], providers: [ - { provide: RegistryService, useValue: registryServiceStub }, - { provide: ActivatedRoute, useValue: activatedRouteStub }, - { provide: HostWindowService, useValue: new HostWindowServiceStub(0) }, - { provide: Router, useValue: new RouterStub() } + {provide: RegistryService, useValue: registryServiceStub}, + {provide: ActivatedRoute, useValue: activatedRouteStub}, + {provide: HostWindowService, useValue: new HostWindowServiceStub(0)}, + {provide: Router, useValue: new RouterStub()} ] }).compileComponents(); })); diff --git a/src/app/+item-page/item-page.module.ts b/src/app/+item-page/item-page.module.ts index d574681b21..0ba82b882a 100644 --- a/src/app/+item-page/item-page.module.ts +++ b/src/app/+item-page/item-page.module.ts @@ -18,13 +18,11 @@ import { FileSectionComponent } from './simple/field-components/file-section/fil import { CollectionsComponent } from './field-components/collections/collections.component'; import { FullItemPageComponent } from './full/full-item-page.component'; import { FullFileSectionComponent } from './full/field-components/file-section/full-file-section.component'; -import { SubmissionModule } from '../submission/submission.module'; @NgModule({ imports: [ CommonModule, SharedModule, - SubmissionModule, ItemPageRoutingModule ], declarations: [ diff --git a/src/app/+login-page/login-page.component.spec.ts b/src/app/+login-page/login-page.component.spec.ts index 609cf47794..b700621665 100644 --- a/src/app/+login-page/login-page.component.spec.ts +++ b/src/app/+login-page/login-page.component.spec.ts @@ -1,5 +1,6 @@ import { NO_ERRORS_SCHEMA } from '@angular/core'; import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { ActivatedRoute } from '@angular/router'; import { Store } from '@ngrx/store'; import { TranslateModule } from '@ngx-translate/core'; @@ -7,10 +8,14 @@ import { Observable } from 'rxjs/Observable'; import 'rxjs/add/observable/of'; import { LoginPageComponent } from './login-page.component'; +import { ActivatedRouteStub } from '../shared/testing/active-router-stub'; describe('LoginPageComponent', () => { let comp: LoginPageComponent; let fixture: ComponentFixture; + const activatedRouteStub = Object.assign(new ActivatedRouteStub(), { + params: Observable.of({}) + }); const store: Store = jasmine.createSpyObj('store', { /* tslint:disable:no-empty */ @@ -26,9 +31,8 @@ describe('LoginPageComponent', () => { ], declarations: [LoginPageComponent], providers: [ - { - provide: Store, useValue: store - } + { provide: ActivatedRoute, useValue: activatedRouteStub }, + { provide: Store, useValue: store } ], schemas: [NO_ERRORS_SCHEMA] }).compileComponents(); diff --git a/src/app/core/browse/browse.service.spec.ts b/src/app/core/browse/browse.service.spec.ts index 5118ea7ecc..1bbea199e7 100644 --- a/src/app/core/browse/browse.service.spec.ts +++ b/src/app/core/browse/browse.service.spec.ts @@ -123,7 +123,7 @@ describe('BrowseService', () => { scheduler.schedule(() => service.getBrowseDefinitions().subscribe()); scheduler.flush(); - expect(requestService.configure).toHaveBeenCalledWith(expected); + expect(requestService.configure).toHaveBeenCalledWith(expected, undefined); }); it('should call RemoteDataBuildService to create the RemoteData Observable', () => { @@ -163,7 +163,7 @@ describe('BrowseService', () => { scheduler.schedule(() => service.getBrowseEntriesFor(browseDefinitions[1].id).subscribe()); scheduler.flush(); - expect(requestService.configure).toHaveBeenCalledWith(expected); + expect(requestService.configure).toHaveBeenCalledWith(expected, undefined); }); it('should call RemoteDataBuildService to create the RemoteData Observable', () => { @@ -179,7 +179,7 @@ describe('BrowseService', () => { it('should throw an Error', () => { const definitionID = 'invalidID'; - const expected = cold('--#-', undefined, new Error(`No metadata browse definition could be found for id '${definitionID}'`)) + const expected = cold('--#-', undefined, new Error(`No metadata browse definition could be found for id '${definitionID}'`)); expect(service.getBrowseEntriesFor(definitionID)).toBeObservable(expected); }); diff --git a/src/app/core/data/request.service.spec.ts b/src/app/core/data/request.service.spec.ts index aa9954f680..d5b8268397 100644 --- a/src/app/core/data/request.service.spec.ts +++ b/src/app/core/data/request.service.spec.ts @@ -1,7 +1,7 @@ import { Store } from '@ngrx/store'; import { cold, hot } from 'jasmine-marbles'; import { Observable } from 'rxjs/Observable'; -import 'rxjs/add/observable/of'; +import 'rxjs/add/observable/of' import { getMockObjectCacheService } from '../../shared/mocks/mock-object-cache.service'; import { getMockResponseCacheService } from '../../shared/mocks/mock-response-cache.service'; import { getMockStore } from '../../shared/mocks/mock-store'; @@ -235,6 +235,7 @@ describe('RequestService', () => { service.configure(request); expect(serviceAsAny.trackRequestsOnTheirWayToTheStore).toHaveBeenCalledWith(request); }); + describe('and it isn\'t cached or pending', () => { beforeEach(() => { spyOn(serviceAsAny, 'isCachedOrPending').and.returnValue(false); @@ -277,28 +278,6 @@ describe('RequestService', () => { service.configure(testPatchRequest); expect(serviceAsAny.dispatchRequest).toHaveBeenCalledWith(testPatchRequest); }); - - it('shouldn\'t track it on it\'s way to the store', () => { - spyOn(serviceAsAny, 'trackRequestsOnTheirWayToTheStore'); - - serviceAsAny.dispatchRequest(testPostRequest); - expect(serviceAsAny.trackRequestsOnTheirWayToTheStore).not.toHaveBeenCalled(); - - serviceAsAny.dispatchRequest(testPutRequest); - expect(serviceAsAny.trackRequestsOnTheirWayToTheStore).not.toHaveBeenCalled(); - - serviceAsAny.dispatchRequest(testDeleteRequest); - expect(serviceAsAny.trackRequestsOnTheirWayToTheStore).not.toHaveBeenCalled(); - - serviceAsAny.dispatchRequest(testOptionsRequest); - expect(serviceAsAny.trackRequestsOnTheirWayToTheStore).not.toHaveBeenCalled(); - - serviceAsAny.dispatchRequest(testHeadRequest); - expect(serviceAsAny.trackRequestsOnTheirWayToTheStore).not.toHaveBeenCalled(); - - serviceAsAny.dispatchRequest(testPatchRequest); - expect(serviceAsAny.trackRequestsOnTheirWayToTheStore).not.toHaveBeenCalled(); - }); }); }); @@ -409,6 +388,30 @@ describe('RequestService', () => { serviceAsAny.dispatchRequest(request); expect(store.dispatch).toHaveBeenCalledWith(new RequestExecuteAction(request.uuid)); }); + + describe('when it\'s not a GET request', () => { + it('shouldn\'t track it', () => { + spyOn(serviceAsAny, 'trackRequestsOnTheirWayToTheStore'); + + serviceAsAny.dispatchRequest(testPostRequest); + expect(serviceAsAny.trackRequestsOnTheirWayToTheStore).not.toHaveBeenCalled(); + + serviceAsAny.dispatchRequest(testPutRequest); + expect(serviceAsAny.trackRequestsOnTheirWayToTheStore).not.toHaveBeenCalled(); + + serviceAsAny.dispatchRequest(testDeleteRequest); + expect(serviceAsAny.trackRequestsOnTheirWayToTheStore).not.toHaveBeenCalled(); + + serviceAsAny.dispatchRequest(testOptionsRequest); + expect(serviceAsAny.trackRequestsOnTheirWayToTheStore).not.toHaveBeenCalled(); + + serviceAsAny.dispatchRequest(testHeadRequest); + expect(serviceAsAny.trackRequestsOnTheirWayToTheStore).not.toHaveBeenCalled(); + + serviceAsAny.dispatchRequest(testPatchRequest); + expect(serviceAsAny.trackRequestsOnTheirWayToTheStore).not.toHaveBeenCalled(); + }); + }); }); describe('trackRequestsOnTheirWayToTheStore', () => { @@ -427,9 +430,18 @@ describe('RequestService', () => { }); describe('when the request is added to the store', () => { + beforeEach(() => { + spyOn(service, 'getByHref').and.returnValue(Observable.of({ + request, + requestPending: false, + responsePending: true, + completed: false + })); + }); + it('should stop tracking the request', () => { - (store.select as any).and.returnValues(Observable.of({ request })); serviceAsAny.trackRequestsOnTheirWayToTheStore(request); + expect(service.getByHref).toHaveBeenCalledWith(request.href); expect(serviceAsAny.requestsOnTheirWayToTheStore.includes(request.href)).toBeFalsy(); }); }); diff --git a/src/app/core/metadata/metadata.service.spec.ts b/src/app/core/metadata/metadata.service.spec.ts index f8f36a358e..9998b18677 100644 --- a/src/app/core/metadata/metadata.service.spec.ts +++ b/src/app/core/metadata/metadata.service.spec.ts @@ -1,17 +1,17 @@ -import { ComponentFixture, TestBed, async, fakeAsync, inject, tick } from '@angular/core/testing'; +import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing'; import { RouterTestingModule } from '@angular/router/testing'; -import { Location, CommonModule } from '@angular/common'; +import { CommonModule, Location } from '@angular/common'; import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; -import { By, Meta, MetaDefinition, Title } from '@angular/platform-browser'; +import { Meta, MetaDefinition, Title } from '@angular/platform-browser'; import { ActivatedRoute, Router } from '@angular/router'; -import { TranslateModule, TranslateLoader } from '@ngx-translate/core'; +import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; import { Store, StoreModule } from '@ngrx/store'; import { Observable } from 'rxjs/Observable'; -import { RemoteDataError } from '../data/remote-data-error'; +import 'rxjs/add/observable/of'; import { UUIDService } from '../shared/uuid.service'; import { MetadataService } from './metadata.service'; @@ -27,8 +27,8 @@ import { RemoteDataBuildService } from '../cache/builders/remote-data-build.serv import { RequestService } from '../data/request.service'; import { ResponseCacheService } from '../cache/response-cache.service'; -import { RemoteData } from '../../core/data/remote-data'; -import { Item } from '../../core/shared/item.model'; +import { RemoteData } from '../data/remote-data'; +import { Item } from '../shared/item.model'; import { MockItem } from '../../shared/mocks/mock-item'; import { MockTranslateLoader } from '../../shared/mocks/mock-translate-loader'; @@ -45,13 +45,15 @@ class TestComponent { } } -@Component({ template: '' }) class DummyItemComponent { +@Component({ template: '' }) +class DummyItemComponent { constructor(private route: ActivatedRoute, private items: ItemDataService, private metadata: MetadataService) { this.route.params.subscribe((params) => { this.metadata.processRemoteData(this.items.findById(params.id)); }); } } + /* tslint:enable:max-classes-per-file */ describe('MetadataService', () => { @@ -101,7 +103,12 @@ describe('MetadataService', () => { }), RouterTestingModule.withRoutes([ { path: 'items/:id', component: DummyItemComponent, pathMatch: 'full' }, - { path: 'other', component: DummyItemComponent, pathMatch: 'full', data: { title: 'Dummy Title', description: 'This is a dummy item component for testing!' } } + { + path: 'other', + component: DummyItemComponent, + pathMatch: 'full', + data: { title: 'Dummy Title', description: 'This is a dummy item component for testing!' } + } ]) ], declarations: [ @@ -114,7 +121,7 @@ describe('MetadataService', () => { { provide: RequestService, useValue: requestService }, { provide: RemoteDataBuildService, useValue: remoteDataBuildService }, { provide: GLOBAL_CONFIG, useValue: ENV_CONFIG }, - { provide: HALEndpointService, useValue: {}}, + { provide: HALEndpointService, useValue: {} }, Meta, Title, ItemDataService, @@ -172,7 +179,7 @@ describe('MetadataService', () => { spyOn(itemDataService, 'findById').and.returnValue(mockRemoteData(MockItem)); router.navigate(['/items/0ec7ff22-f211-40ab-a69e-c819b0b1f357']); tick(); - expect(tagStore.size).toBeGreaterThan(0) + expect(tagStore.size).toBeGreaterThan(0); router.navigate(['/other']); tick(); expect(tagStore.size).toEqual(2); @@ -189,7 +196,7 @@ describe('MetadataService', () => { undefined, MockItem )); - } + }; const mockType = (mockItem: Item, type: string): Item => { const typedMockItem = Object.assign(new Item(), mockItem) as Item; @@ -200,7 +207,7 @@ describe('MetadataService', () => { } } return typedMockItem; - } + }; const mockPublisher = (mockItem: Item): Item => { const publishedMockItem = Object.assign(new Item(), mockItem) as Item; diff --git a/src/app/core/shared/item.model.ts b/src/app/core/shared/item.model.ts index be22c28b91..8250c73af1 100644 --- a/src/app/core/shared/item.model.ts +++ b/src/app/core/shared/item.model.ts @@ -88,10 +88,10 @@ export class Item extends DSpaceObject { */ getBitstreamsByBundleName(bundleName: string): Observable { return this.bitstreams - .filter((rd: RemoteData) => rd.hasSucceeded) + .filter((rd: RemoteData) => !rd.isResponsePending) .map((rd: RemoteData) => rd.payload) .filter((bitstreams: Bitstream[]) => hasValue(bitstreams)) - .first() + .take(1) .startWith([]) .map((bitstreams) => { return bitstreams diff --git a/src/app/core/shared/operators.spec.ts b/src/app/core/shared/operators.spec.ts index bb2fc263fd..4f3245088b 100644 --- a/src/app/core/shared/operators.spec.ts +++ b/src/app/core/shared/operators.spec.ts @@ -4,7 +4,7 @@ import { getMockRequestService } from '../../shared/mocks/mock-request.service'; import { getMockResponseCacheService } from '../../shared/mocks/mock-response-cache.service'; import { ResponseCacheEntry } from '../cache/response-cache.reducer'; import { ResponseCacheService } from '../cache/response-cache.service'; -import { GetRequest, RestRequest } from '../data/request.models'; +import { GetRequest } from '../data/request.models'; import { RequestEntry } from '../data/request.reducer'; import { RequestService } from '../data/request.service'; import { @@ -133,7 +133,7 @@ describe('Core Module - RxJS Operators', () => { scheduler.schedule(() => source.pipe(configureRequest(requestService)).subscribe()); scheduler.flush(); - expect(requestService.configure).toHaveBeenCalledWith(testRequest) + expect(requestService.configure).toHaveBeenCalledWith(testRequest, undefined) }); }); diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/dynamic-group/dynamic-group.component.spec.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/dynamic-group/dynamic-group.component.spec.ts index d1e6f67287..6d6f6fcb54 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/dynamic-group/dynamic-group.component.spec.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/dynamic-group/dynamic-group.component.spec.ts @@ -82,6 +82,16 @@ describe('DsDynamicGroupComponent test suite', () => { required: 'required', regex: 'pattern' } + }, + submission: { + metadata: { + icons: [ + { + name: 'default', + config: {} + } + ] + } } } as any; let testComp: TestComponent; diff --git a/src/app/shared/form/form.service.spec.ts b/src/app/shared/form/form.service.spec.ts index 06125c9034..794b362909 100644 --- a/src/app/shared/form/form.service.spec.ts +++ b/src/app/shared/form/form.service.spec.ts @@ -13,7 +13,6 @@ import { FormService } from './form.service'; import { FormBuilderService } from './builder/form-builder.service'; import { AppState } from '../../app.reducer'; import { formReducer } from './form.reducer'; -import { GlobalConfig } from '../../../config/global-config.interface'; describe('FormService test suite', () => { const config = { From ba1337ec8f72750bb88d076fbd2a53ffe894d3ef Mon Sep 17 00:00:00 2001 From: Giuseppe Date: Tue, 31 Jul 2018 17:51:10 +0200 Subject: [PATCH 030/604] Improved redirect after 'Save for later' in Submission edit page --- src/app/app.component.spec.ts | 9 +++++- src/app/app.component.ts | 6 +++- src/app/shared/chips/models/chips.model.ts | 11 ++++++- src/app/shared/mocks/mock-router.ts | 4 +++ src/app/shared/services/route.service.spec.ts | 7 +++-- src/app/shared/services/route.service.ts | 29 +++++++++++++++---- src/app/submission/submission.service.ts | 12 ++++++-- 7 files changed, 65 insertions(+), 13 deletions(-) diff --git a/src/app/app.component.spec.ts b/src/app/app.component.spec.ts index cbab798f1e..6b8ec5a4bc 100644 --- a/src/app/app.component.spec.ts +++ b/src/app/app.component.spec.ts @@ -34,6 +34,10 @@ import { Angulartics2GoogleAnalytics } from 'angulartics2/ga'; import { AngularticsMock } from './shared/mocks/mock-angulartics.service'; import { AuthServiceMock } from './shared/mocks/mock-auth.service'; import { AuthService } from './core/auth/auth.service'; +import { RouteService } from './shared/services/route.service'; +import { ActivatedRoute, Router } from '@angular/router'; +import { MockActivatedRoute } from './shared/mocks/mock-active-router'; +import { MockRouter } from './shared/mocks/mock-router'; let comp: AppComponent; let fixture: ComponentFixture; @@ -62,7 +66,10 @@ describe('App component', () => { { provide: MetadataService, useValue: new MockMetadataService() }, { provide: Angulartics2GoogleAnalytics, useValue: new AngularticsMock() }, { provide: AuthService, useValue: new AuthServiceMock() }, - AppComponent + { provide: ActivatedRoute, useValue: new MockActivatedRoute() }, + { provide: Router, useValue: new MockRouter() }, + AppComponent, + RouteService ], schemas: [CUSTOM_ELEMENTS_SCHEMA] }) diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 1c1a47cf12..f98f0185df 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -13,6 +13,7 @@ import { NativeWindowRef, NativeWindowService } from './shared/services/window.s import { isAuthenticated } from './core/auth/selectors'; import { AuthService } from './core/auth/auth.service'; import { Angulartics2GoogleAnalytics } from 'angulartics2/ga'; +import { RouteService } from './shared/services/route.service'; @Component({ selector: 'ds-app', @@ -30,7 +31,8 @@ export class AppComponent implements OnInit { private store: Store, private metadata: MetadataService, private angulartics2GoogleAnalytics: Angulartics2GoogleAnalytics, - private authService: AuthService + private authService: AuthService, + private routeService: RouteService ) { // this language will be used as a fallback when a translation isn't found in the current language translate.setDefaultLang('en'); @@ -39,6 +41,8 @@ export class AppComponent implements OnInit { metadata.listenForRouteChange(); + routeService.saveRouting(); + if (config.debug) { console.info(config); } diff --git a/src/app/shared/chips/models/chips.model.ts b/src/app/shared/chips/models/chips.model.ts index e133a416f4..92a1b18fb9 100644 --- a/src/app/shared/chips/models/chips.model.ts +++ b/src/app/shared/chips/models/chips.model.ts @@ -2,6 +2,7 @@ import { findIndex, isEqual, isObject } from 'lodash'; import { BehaviorSubject } from 'rxjs/BehaviorSubject'; import { ChipsItem, ChipsItemIcon } from './chips-item.model'; import { hasValue, isNotEmpty } from '../../empty.util'; +import { PLACEHOLDER_PARENT_METADATA } from '../../form/builder/ds-dynamic-form-ui/models/dynamic-group/dynamic-group.model'; export interface IconsConfig { withAuthority?: { @@ -83,6 +84,14 @@ export class Chips { return this._items.length > 0; } + private hasPlaceholder(value) { + if (isObject(value)) { + return value.value === PLACEHOLDER_PARENT_METADATA; + } else { + return value === PLACEHOLDER_PARENT_METADATA; + } + } + public remove(chipsItem: ChipsItem): void { const index = findIndex(this._items, {id: chipsItem.id}); this._items.splice(index, 1); @@ -119,7 +128,7 @@ export class Chips { config = (configIndex !== -1) ? this.iconsConfig[configIndex].config : defaultConfig; - if (hasValue(value) && isNotEmpty(config)) { + if (hasValue(value) && isNotEmpty(config) && !this.hasPlaceholder(value)) { let icon: ChipsItemIcon; const hasAuthority: boolean = !!(isObject(value) && ((value.hasOwnProperty('authority') && value.authority) || (value.hasOwnProperty('id') && value.id))); diff --git a/src/app/shared/mocks/mock-router.ts b/src/app/shared/mocks/mock-router.ts index 054c63d4c0..cf9a522f07 100644 --- a/src/app/shared/mocks/mock-router.ts +++ b/src/app/shared/mocks/mock-router.ts @@ -1,4 +1,8 @@ +import { Observable } from 'rxjs/Observable'; + export class MockRouter { + public events = Observable.of({}); + // noinspection TypeScriptUnresolvedFunction navigate = jasmine.createSpy('navigate'); } diff --git a/src/app/shared/services/route.service.spec.ts b/src/app/shared/services/route.service.spec.ts index b134771b3e..6ec9ef8d53 100644 --- a/src/app/shared/services/route.service.spec.ts +++ b/src/app/shared/services/route.service.spec.ts @@ -1,7 +1,9 @@ import { RouteService } from './route.service'; import { async, TestBed } from '@angular/core/testing'; -import { ActivatedRoute, convertToParamMap, Params } from '@angular/router'; +import { ActivatedRoute, convertToParamMap, Params, Router } from '@angular/router'; import { Observable } from 'rxjs/Observable'; +import 'rxjs/add/observable/of' +import { MockRouter } from '../mocks/mock-router'; describe('RouteService', () => { let service: RouteService; @@ -28,12 +30,13 @@ describe('RouteService', () => { queryParamMap: Observable.of(convertToParamMap(paramObject)) }, }, + { provide: Router, useValue: new MockRouter() }, ] }); })); beforeEach(() => { - service = new RouteService(TestBed.get(ActivatedRoute)); + service = new RouteService(TestBed.get(ActivatedRoute), TestBed.get(Router)); }); describe('hasQueryParam', () => { diff --git a/src/app/shared/services/route.service.ts b/src/app/shared/services/route.service.ts index aa683a6403..5b6ad44aba 100644 --- a/src/app/shared/services/route.service.ts +++ b/src/app/shared/services/route.service.ts @@ -1,15 +1,14 @@ import { Injectable } from '@angular/core'; import { Observable } from 'rxjs/Observable'; -import { - ActivatedRoute, convertToParamMap, NavigationExtras, Params, - Router, -} from '@angular/router'; -import { isNotEmpty } from '../empty.util'; +import { ActivatedRoute, NavigationEnd, Params, Router } from '@angular/router'; +import { filter } from 'rxjs/operators'; @Injectable() export class RouteService { - constructor(private route: ActivatedRoute) { + private history = []; + + constructor(private route: ActivatedRoute, private router: Router) { } getQueryParameterValues(paramName: string): Observable { @@ -40,4 +39,22 @@ export class RouteService { return params; }).distinctUntilChanged(); } + + public saveRouting(): void { + this.router.events + .pipe(filter((event) => event instanceof NavigationEnd)) + .subscribe(({urlAfterRedirects}: NavigationEnd) => { + this.history = [...this.history, urlAfterRedirects]; + console.log(this.history); + }); + } + + public getHistory(): string[] { + return this.history; + } + + public getPreviousUrl(): string { + return this.history[this.history.length - 2] || ''; + } + } diff --git a/src/app/submission/submission.service.ts b/src/app/submission/submission.service.ts index cef39c9a52..7eedd55ab6 100644 --- a/src/app/submission/submission.service.ts +++ b/src/app/submission/submission.service.ts @@ -1,4 +1,6 @@ import { Inject, Injectable } from '@angular/core'; +import { Router } from '@angular/router'; + import { Observable } from 'rxjs/Observable'; import { Subscription } from 'rxjs/Subscription'; import { Store } from '@ngrx/store'; @@ -17,10 +19,10 @@ import { GLOBAL_CONFIG } from '../../config'; import { HttpHeaders } from '@angular/common/http'; import { HttpOptions } from '../core/dspace-rest-v2/dspace-rest-v2.service'; import { SubmissionRestService } from './submission-rest.service'; -import { Router } from '@angular/router'; import { SectionDataObject } from './sections/models/section-data.model'; import { SubmissionScopeType } from '../core/submission/submission-scope-type'; import { SubmissionObject } from '../core/submission/models/submission-object.model'; +import { RouteService } from '../shared/services/route.service'; @Injectable() export class SubmissionService { @@ -31,6 +33,7 @@ export class SubmissionService { constructor(@Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig, protected restService: SubmissionRestService, protected router: Router, + protected routeService: RouteService, protected store: Store) { } @@ -191,7 +194,12 @@ export class SubmissionService { } redirectToMyDSpace() { - this.router.navigate(['/mydspace']); + const previousUrl = this.routeService.getPreviousUrl(); + if (isEmpty(previousUrl)) { + this.router.navigate(['/mydspace']); + } else { + this.router.navigateByUrl(previousUrl); + } } retrieveSubmission(submissionId): Observable { From 7d08e5813f75fa490c0fa7e8ff4f3be5dff61a01 Mon Sep 17 00:00:00 2001 From: Giuseppe Date: Wed, 1 Aug 2018 16:27:49 +0200 Subject: [PATCH 031/604] Fixed chips tooltip and tag fields --- src/app/shared/chips/chips.component.html | 9 +++++++-- src/app/shared/chips/chips.component.ts | 6 +++++- .../models/tag/dynamic-tag.component.html | 4 +++- .../models/tag/dynamic-tag.component.scss | 1 + 4 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/app/shared/chips/chips.component.html b/src/app/shared/chips/chips.component.html index 21ce99ecdb..3e10839f22 100644 --- a/src/app/shared/chips/chips.component.html +++ b/src/app/shared/chips/chips.component.html @@ -3,8 +3,13 @@ {{tipText}}
diff --git a/src/app/+collection-page/collection-form/collection-form.component.ts b/src/app/+collection-page/collection-form/collection-form.component.ts index c51633dfb6..4238eb3811 100644 --- a/src/app/+collection-page/collection-form/collection-form.component.ts +++ b/src/app/+collection-page/collection-form/collection-form.component.ts @@ -1,5 +1,6 @@ import { Component, EventEmitter, Output } from '@angular/core'; import { isNotEmpty } from '../../shared/empty.util'; +import { Location } from '@angular/common'; @Component({ selector: 'ds-collection-form', @@ -20,6 +21,10 @@ export class CollectionFormComponent { @Output() submitted: EventEmitter = new EventEmitter(); + public constructor(private location: Location) { + + } + onSubmit(data: any) { if (isNotEmpty(data.name)) { this.submitted.emit(data); @@ -29,4 +34,8 @@ export class CollectionFormComponent { } } + cancel() { + this.location.back(); + } + } diff --git a/src/app/+community-page/community-form/community-form.component.html b/src/app/+community-page/community-form/community-form.component.html index bc45b582ac..1bb5e97ec1 100644 --- a/src/app/+community-page/community-form/community-form.component.html +++ b/src/app/+community-page/community-form/community-form.component.html @@ -21,7 +21,7 @@
- +
diff --git a/src/app/+community-page/community-form/community-form.component.ts b/src/app/+community-page/community-form/community-form.component.ts index 64d08ab862..d7797028b7 100644 --- a/src/app/+community-page/community-form/community-form.component.ts +++ b/src/app/+community-page/community-form/community-form.component.ts @@ -1,5 +1,6 @@ import { Component, EventEmitter, Output } from '@angular/core'; import { isNotEmpty } from '../../shared/empty.util'; +import { Location } from '@angular/common'; @Component({ selector: 'ds-community-form', @@ -18,6 +19,10 @@ export class CommunityFormComponent { @Output() submitted: EventEmitter = new EventEmitter(); + public constructor(private location: Location) { + + } + onSubmit(data: any) { if (isNotEmpty(data.name)) { this.submitted.emit(data); @@ -27,4 +32,8 @@ export class CommunityFormComponent { } } + cancel() { + this.location.back(); + } + } From dfa42acab5f691904c71bde7c41470a72510cea8 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Mon, 20 Aug 2018 16:09:42 +0200 Subject: [PATCH 038/604] 54472: Create community/collection with parent community + error messages --- resources/i18n/en.json | 6 ++-- .../create-collection-page.component.html | 9 ++++- .../create-collection-page.component.ts | 34 +++++++++++++++++-- .../create-community-page.component.html | 9 ++++- .../create-community-page.component.ts | 34 +++++++++++++++++-- src/app/core/data/collection-data.service.ts | 4 --- src/app/core/data/comcol-data.service.ts | 28 ++++++++++----- src/app/core/data/community-data.service.ts | 4 --- 8 files changed, 103 insertions(+), 25 deletions(-) diff --git a/resources/i18n/en.json b/resources/i18n/en.json index 963dfae80f..af9d1a08c8 100644 --- a/resources/i18n/en.json +++ b/resources/i18n/en.json @@ -29,7 +29,8 @@ } }, "create": { - "head": "Create a Collection" + "head": "Create a Collection", + "sub-head": "Create a Collection for Community {{ parent }}" } }, "community": { @@ -53,7 +54,8 @@ } }, "create": { - "head": "Create a Community" + "head": "Create a Community", + "sub-head": "Create a Sub-Community for Community {{ parent }}" } }, "item": { diff --git a/src/app/+collection-page/create-collection-page/create-collection-page.component.html b/src/app/+collection-page/create-collection-page/create-collection-page.component.html index 53a55571f6..9cf01f7c88 100644 --- a/src/app/+collection-page/create-collection-page/create-collection-page.component.html +++ b/src/app/+collection-page/create-collection-page/create-collection-page.component.html @@ -1,8 +1,15 @@
- + + +

{{ 'collection.create.sub-head' | translate:{ parent: community.name } }}

+
+
+ {{error.statusCode}} +

{{error.errorMessage}}

+
diff --git a/src/app/+collection-page/create-collection-page/create-collection-page.component.ts b/src/app/+collection-page/create-collection-page/create-collection-page.component.ts index 689bd30dfc..2e8d9b557d 100644 --- a/src/app/+collection-page/create-collection-page/create-collection-page.component.ts +++ b/src/app/+collection-page/create-collection-page/create-collection-page.component.ts @@ -5,6 +5,13 @@ import { NormalizedCommunity } from '../../core/cache/models/normalized-communit import { CommunityDataService } from '../../core/data/community-data.service'; import { CollectionDataService } from '../../core/data/collection-data.service'; import { Collection } from '../../core/shared/collection.model'; +import { RouteService } from '../../shared/services/route.service'; +import { Router } from '@angular/router'; +import { DSOSuccessResponse, ErrorResponse } from '../../core/cache/response-cache.models'; +import { Observable } from 'rxjs/Observable'; +import { ResponseCacheEntry } from '../../core/cache/response-cache.reducer'; +import { map } from 'rxjs/operators'; +import { RemoteData } from '../../core/data/remote-data'; @Component({ selector: 'ds-create-collection', @@ -13,15 +20,38 @@ import { Collection } from '../../core/shared/collection.model'; }) export class CreateCollectionPageComponent { - public constructor(private collectionDataService: CollectionDataService) { + private error$: Observable; + private parentUUID$: Observable; + private communityRDObs: Observable>; + public constructor(private collectionDataService: CollectionDataService, private communityDataService: CommunityDataService, private routeService: RouteService, private router: Router) { + this.parentUUID$ = this.routeService.getQueryParameterValue('parent'); + this.parentUUID$.subscribe((uuid: string) => { + this.communityRDObs = this.communityDataService.findById(uuid); + }); } onSubmit(data: any) { const collection = Object.assign(new Collection(), { name: data.name }); - this.collectionDataService.create(collection); + this.parentUUID$.subscribe((uuid: string) => { + let response$: Observable; + if (uuid) { + response$ = this.collectionDataService.create(collection, uuid); + } else { + response$ = this.collectionDataService.create(collection); + } + this.error$ = response$.pipe( + map((response: ResponseCacheEntry) => { + if (!response.response.isSuccessful && response.response instanceof ErrorResponse) { + return response.response; + } else if (response.response instanceof DSOSuccessResponse) { + this.router.navigateByUrl(''); + } + }) + ); + }); } } diff --git a/src/app/+community-page/create-community-page/create-community-page.component.html b/src/app/+community-page/create-community-page/create-community-page.component.html index ea270db92b..53ca4f5020 100644 --- a/src/app/+community-page/create-community-page/create-community-page.component.html +++ b/src/app/+community-page/create-community-page/create-community-page.component.html @@ -1,8 +1,15 @@
- + + +

{{ 'community.create.sub-head' | translate:{ parent: community.name } }}

+
+
+ {{error.statusCode}} +

{{error.errorMessage}}

+
diff --git a/src/app/+community-page/create-community-page/create-community-page.component.ts b/src/app/+community-page/create-community-page/create-community-page.component.ts index 255d88c21b..1da0299e66 100644 --- a/src/app/+community-page/create-community-page/create-community-page.component.ts +++ b/src/app/+community-page/create-community-page/create-community-page.component.ts @@ -3,6 +3,13 @@ import { Community } from '../../core/shared/community.model'; import { ComColDataService } from '../../core/data/comcol-data.service'; import { NormalizedCommunity } from '../../core/cache/models/normalized-community.model'; import { CommunityDataService } from '../../core/data/community-data.service'; +import { ResponseCacheEntry } from '../../core/cache/response-cache.reducer'; +import { DSOSuccessResponse, ErrorResponse, RestResponse } from '../../core/cache/response-cache.models'; +import { Observable } from 'rxjs/Observable'; +import { map } from 'rxjs/operators'; +import { RouteService } from '../../shared/services/route.service'; +import { Router } from '@angular/router'; +import { RemoteData } from '../../core/data/remote-data'; @Component({ selector: 'ds-create-community', @@ -11,15 +18,38 @@ import { CommunityDataService } from '../../core/data/community-data.service'; }) export class CreateCommunityPageComponent { - public constructor(private communityDataService: CommunityDataService) { + private error$: Observable; + private parentUUID$: Observable; + private communityRDObs: Observable>; + public constructor(private communityDataService: CommunityDataService, private routeService: RouteService, private router: Router) { + this.parentUUID$ = this.routeService.getQueryParameterValue('parent'); + this.parentUUID$.subscribe((uuid: string) => { + this.communityRDObs = this.communityDataService.findById(uuid); + }); } onSubmit(data: any) { const community = Object.assign(new Community(), { name: data.name }); - this.communityDataService.create(community); + this.parentUUID$.subscribe((uuid: string) => { + let response$: Observable; + if (uuid) { + response$ = this.communityDataService.create(community, uuid); + } else { + response$ = this.communityDataService.create(community); + } + this.error$ = response$.pipe( + map((response: ResponseCacheEntry) => { + if (!response.response.isSuccessful && response.response instanceof ErrorResponse) { + return response.response; + } else if (response.response instanceof DSOSuccessResponse) { + this.router.navigateByUrl(''); + } + }) + ); + }); } } diff --git a/src/app/core/data/collection-data.service.ts b/src/app/core/data/collection-data.service.ts index 765cba0a8b..70c11ca5fc 100644 --- a/src/app/core/data/collection-data.service.ts +++ b/src/app/core/data/collection-data.service.ts @@ -31,8 +31,4 @@ export class CollectionDataService extends ComColDataService { + return this.halService.getEndpoint(this.linkPath).pipe( isNotEmptyOperator(), distinctUntilChanged(), map((endpointURL: string) => { @@ -75,12 +75,22 @@ export abstract class ComColDataService request.href), + getResponseFromSelflink(this.responseCache) + ); } - abstract buildCreateParams(comcol: TDomain): string; + public buildCreateParams(comcol: TDomain, parentUUID?: string): string { + if (comcol instanceof Community || comcol instanceof Collection) { + let urlParams = '?name=' + comcol.name; + if (parentUUID) { + urlParams += '&parent=' + parentUUID; + } + return urlParams; + } + } } diff --git a/src/app/core/data/community-data.service.ts b/src/app/core/data/community-data.service.ts index acc915c093..9d6af5ee6f 100644 --- a/src/app/core/data/community-data.service.ts +++ b/src/app/core/data/community-data.service.ts @@ -34,8 +34,4 @@ export class CommunityDataService extends ComColDataService Date: Wed, 22 Aug 2018 16:10:55 +0200 Subject: [PATCH 039/604] 54472: Authorization + tests (intermediate) --- .../collection-page-routing.module.ts | 3 +- .../community-page-routing.module.ts | 3 +- .../create-community-page.component.spec.ts | 69 +++++++++++ src/app/core/data/comcol-data.service.spec.ts | 110 +++++++++++++++--- src/app/core/data/comcol-data.service.ts | 2 +- 5 files changed, 168 insertions(+), 19 deletions(-) create mode 100644 src/app/+community-page/create-community-page/create-community-page.component.spec.ts diff --git a/src/app/+collection-page/collection-page-routing.module.ts b/src/app/+collection-page/collection-page-routing.module.ts index 1bd53dd2b3..35faae7b02 100644 --- a/src/app/+collection-page/collection-page-routing.module.ts +++ b/src/app/+collection-page/collection-page-routing.module.ts @@ -3,11 +3,12 @@ import { RouterModule } from '@angular/router'; import { CollectionPageComponent } from './collection-page.component'; import { CreateCollectionPageComponent } from './create-collection-page/create-collection-page.component'; +import { AuthenticatedGuard } from '../core/auth/authenticated.guard'; @NgModule({ imports: [ RouterModule.forChild([ - { path: 'create', component: CreateCollectionPageComponent }, + { path: 'create', component: CreateCollectionPageComponent, canActivate: [AuthenticatedGuard] }, { path: ':id', component: CollectionPageComponent, pathMatch: 'full' } ]) ] diff --git a/src/app/+community-page/community-page-routing.module.ts b/src/app/+community-page/community-page-routing.module.ts index 249b01ea18..13d75628b9 100644 --- a/src/app/+community-page/community-page-routing.module.ts +++ b/src/app/+community-page/community-page-routing.module.ts @@ -3,11 +3,12 @@ import { RouterModule } from '@angular/router'; import { CommunityPageComponent } from './community-page.component'; import { CreateCommunityPageComponent } from './create-community-page/create-community-page.component'; +import { AuthenticatedGuard } from '../core/auth/authenticated.guard'; @NgModule({ imports: [ RouterModule.forChild([ - { path: 'create', component: CreateCommunityPageComponent }, + { path: 'create', component: CreateCommunityPageComponent, canActivate: [AuthenticatedGuard] }, { path: ':id', component: CommunityPageComponent, pathMatch: 'full' } ]) ] diff --git a/src/app/+community-page/create-community-page/create-community-page.component.spec.ts b/src/app/+community-page/create-community-page/create-community-page.component.spec.ts new file mode 100644 index 0000000000..990a9895b9 --- /dev/null +++ b/src/app/+community-page/create-community-page/create-community-page.component.spec.ts @@ -0,0 +1,69 @@ +import { CreateCommunityPageComponent } from './create-community-page.component'; +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { CommunityDataService } from '../../core/data/community-data.service'; +import { RouteService } from '../../shared/services/route.service'; +import { Router } from '@angular/router'; +import { TranslateModule } from '@ngx-translate/core'; +import { Observable } from 'rxjs/Observable'; +import { RemoteData } from '../../core/data/remote-data'; +import { Community } from '../../core/shared/community.model'; +import { DSOSuccessResponse } from '../../core/cache/response-cache.models'; +import { BrowserModule } from '@angular/platform-browser'; +import { SharedModule } from '../../shared/shared.module'; +import { CommonModule } from '@angular/common'; +import { CommunityFormComponent } from '../community-form/community-form.component'; + +describe('CreateCommunityPageComponent', () => { + let comp: CreateCommunityPageComponent; + let fixture: ComponentFixture; + let communityDataService: CommunityDataService; + let routeService: RouteService; + let router: any = {}; + + const community = Object.assign(new Community(), { + uuid: 'a20da287-e174-466a-9926-f66b9300d347', + name: 'test community' + }); + + const communityDataServiceStub = { + findById: (uuid) => Observable.of(new RemoteData(false, false, true, null, Object.assign(new Community(), { + uuid: uuid, + name: community.name + }))), + create: (com, uuid?) => Observable.of({ + response: new DSOSuccessResponse(null,'200',null) + }) + }; + const routeServiceStub = { + getQueryParameterValue: (param) => Observable.of(community.uuid) + }; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [TranslateModule.forRoot(), SharedModule, CommonModule], + declarations: [CreateCommunityPageComponent, CommunityFormComponent], + providers: [ + { provide: CommunityDataService, useValue: communityDataServiceStub }, + { provide: RouteService, useValue: routeServiceStub }, + { provide: Router, useValue: router } + ] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(CreateCommunityPageComponent); + comp = fixture.componentInstance; + fixture.detectChanges(); + communityDataService = (comp as any).communityDataService; + routeService = (comp as any).routeService; + router = (comp as any).router; + }); + + it('should navigate on successful submit', () => { + spyOn(router, 'navigateByUrl'); + comp.onSubmit({ + name: 'test' + }); + expect(router.navigateByUrl).toHaveBeenCalled(); + }); +}); diff --git a/src/app/core/data/comcol-data.service.spec.ts b/src/app/core/data/comcol-data.service.spec.ts index b5727fb22f..5f440dd442 100644 --- a/src/app/core/data/comcol-data.service.spec.ts +++ b/src/app/core/data/comcol-data.service.spec.ts @@ -1,6 +1,6 @@ import { Store } from '@ngrx/store'; import { cold, getTestScheduler, hot } from 'jasmine-marbles'; -import { TestScheduler } from 'rxjs/Rx'; +import { Observable, TestScheduler } from 'rxjs/Rx'; import { GlobalConfig } from '../../../config'; import { getMockRequestService } from '../../shared/mocks/mock-request.service'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; @@ -14,6 +14,8 @@ import { FindByIDRequest } from './request.models'; import { RequestService } from './request.service'; import { NormalizedObject } from '../cache/models/normalized-object.model'; import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { Community } from '../shared/community.model'; +import { AuthService } from '../auth/auth.service'; const LINK_NAME = 'test'; @@ -32,6 +34,7 @@ class TestService extends ComColDataService { protected cds: CommunityDataService, protected objectCache: ObjectCacheService, protected halService: HALEndpointService, + protected authService: AuthService, protected linkPath: string ) { super(); @@ -46,7 +49,8 @@ describe('ComColDataService', () => { let requestService: RequestService; let cds: CommunityDataService; let objectCache: ObjectCacheService; - const halService: any = {}; + let authService: AuthService; + let halService: any = {}; const rdbService = {} as RemoteDataBuildService; const store = {} as Store; @@ -57,6 +61,11 @@ describe('ComColDataService', () => { const communityEndpoint = `${communitiesEndpoint}/${scopeID}`; const scopedEndpoint = `${communityEndpoint}/${LINK_NAME}`; const serviceEndpoint = `https://rest.api/core/${LINK_NAME}`; + const authHeader = 'Bearer eyJhbGciOiJIUzI1NiJ9.eyJlaWQiOiJhNjA4NmIzNC0zOTE4LTQ1YjctOGRkZC05MzI5YTcwMmEyNmEiLCJzZyI6W10sImV4cCI6MTUzNDk0MDcyNX0.RV5GAtiX6cpwBN77P_v16iG9ipeyiO7faNYSNMzq_sQ'; + + const mockHalService = { + getEndpoint: (linkPath) => Observable.of(communitiesEndpoint) + }; function initMockCommunityDataService(): CommunityDataService { return jasmine.createSpyObj('responseCache', { @@ -85,6 +94,14 @@ describe('ComColDataService', () => { }); } + function initMockAuthService(): AuthService { + return jasmine.createSpyObj('authService', { + buildAuthHeader: cold('c-', { + c: authHeader + }) + }); + } + function initTestService(): TestService { return new TestService( responseCache, @@ -95,22 +112,27 @@ describe('ComColDataService', () => { cds, objectCache, halService, + authService, LINK_NAME ); } + beforeEach(() => { + cds = initMockCommunityDataService(); + requestService = getMockRequestService(); + objectCache = initMockObjectCacheService(); + responseCache = initMockResponseCacheService(true); + halService = mockHalService; + authService = initMockAuthService(); + service = initTestService(); + }); + describe('getScopedEndpoint', () => { beforeEach(() => { scheduler = getTestScheduler(); }); it('should configure a new FindByIDRequest for the scope Community', () => { - cds = initMockCommunityDataService(); - requestService = getMockRequestService(); - objectCache = initMockObjectCacheService(); - responseCache = initMockResponseCacheService(true); - service = initTestService(); - const expected = new FindByIDRequest(requestService.generateRequestId(), communityEndpoint, scopeID); scheduler.schedule(() => service.getScopedEndpoint(scopeID).subscribe()); @@ -120,14 +142,6 @@ describe('ComColDataService', () => { }); describe('if the scope Community can be found', () => { - beforeEach(() => { - cds = initMockCommunityDataService(); - requestService = getMockRequestService(); - objectCache = initMockObjectCacheService(); - responseCache = initMockResponseCacheService(true); - service = initTestService(); - }); - it('should fetch the scope Community from the cache', () => { scheduler.schedule(() => service.getScopedEndpoint(scopeID).subscribe()); scheduler.flush(); @@ -160,4 +174,68 @@ describe('ComColDataService', () => { }); }); + + describe('create', () => { + let community: Community; + const name = 'test community'; + + beforeEach(() => { + community = Object.assign(new Community(), { + name: name + }); + spyOn(service, 'buildCreateParams'); + }); + + describe('when creating a top-level community', () => { + it('should build params without parent UUID', () => { + scheduler.schedule(() => service.create(community).subscribe()); + scheduler.flush(); + expect(service.buildCreateParams).toHaveBeenCalledWith(community); + }); + }); + + describe('when creating a community part of another community', () => { + let parentCommunity: Community; + const parentName = 'test parent community'; + const parentUUID = 'a20da287-e174-466a-9926-f66b9300d347'; + + beforeEach(() => { + parentCommunity = Object.assign(new Community(), { + id: parentUUID, + uuid: parentUUID, + name: parentName + }); + }); + + it('should build params with parent UUID', () => { + scheduler.schedule(() => service.create(community, parentUUID).subscribe()); + scheduler.flush(); + expect(service.buildCreateParams).toHaveBeenCalledWith(community, parentUUID); + }); + }); + }); + + describe('buildCreateParams', () => { + let community: Community; + const name = 'test community'; + let parentCommunity: Community; + const parentName = 'test parent community'; + const parentUUID = 'a20da287-e174-466a-9926-f66b9300d347'; + + beforeEach(() => { + community = Object.assign(new Community(), { + name: name + }); + parentCommunity = Object.assign(new Community(), { + id: parentUUID, + uuid: parentUUID, + name: parentName + }); + }); + + it('should return the correct url parameters', () => { + expect(service.buildCreateParams(community, parentUUID)).toEqual('?name=' + name + '&parent=' + parentUUID); + }); + }); + }); diff --git a/src/app/core/data/comcol-data.service.ts b/src/app/core/data/comcol-data.service.ts index b7a87913de..a57f5fa910 100644 --- a/src/app/core/data/comcol-data.service.ts +++ b/src/app/core/data/comcol-data.service.ts @@ -75,7 +75,7 @@ export abstract class ComColDataService request.href), From 3a0de2865a753eabddc29a6427639ab4812de73e Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Thu, 23 Aug 2018 11:03:06 +0200 Subject: [PATCH 040/604] 54472: CreateCommunityPageComponent and CreateCollectionPageComponent tests --- .../create-collection-page.component.spec.ts | 99 +++++++++++++++++++ .../create-community-page.component.spec.ts | 38 +++++-- 2 files changed, 129 insertions(+), 8 deletions(-) create mode 100644 src/app/+collection-page/create-collection-page/create-collection-page.component.spec.ts diff --git a/src/app/+collection-page/create-collection-page/create-collection-page.component.spec.ts b/src/app/+collection-page/create-collection-page/create-collection-page.component.spec.ts new file mode 100644 index 0000000000..efe0e3da97 --- /dev/null +++ b/src/app/+collection-page/create-collection-page/create-collection-page.component.spec.ts @@ -0,0 +1,99 @@ +import { SharedModule } from '../../shared/shared.module'; +import { Community } from '../../core/shared/community.model'; +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { DSOSuccessResponse, ErrorResponse } from '../../core/cache/response-cache.models'; +import { CommonModule } from '@angular/common'; +import { CreateCommunityPageComponent } from '../../+community-page/create-community-page/create-community-page.component'; +import { Router } from '@angular/router'; +import { TranslateModule } from '@ngx-translate/core'; +import { CommunityFormComponent } from '../../+community-page/community-form/community-form.component'; +import { Observable } from 'rxjs/Observable'; +import { CommunityDataService } from '../../core/data/community-data.service'; +import { RequestError } from '../../core/data/request.models'; +import { RouteService } from '../../shared/services/route.service'; +import { RemoteData } from '../../core/data/remote-data'; +import { CreateCollectionPageComponent } from './create-collection-page.component'; +import { CollectionDataService } from '../../core/data/collection-data.service'; +import { Collection } from '../../core/shared/collection.model'; +import { RouterTestingModule } from '@angular/router/testing'; +import { CollectionFormComponent } from '../collection-form/collection-form.component'; + +describe('CreateCollectionPageComponent', () => { + let comp: CreateCollectionPageComponent; + let fixture: ComponentFixture; + let collectionDataService: CollectionDataService; + let communityDataService: CommunityDataService; + let routeService: RouteService; + let router: Router; + + const community = Object.assign(new Community(), { + uuid: 'a20da287-e174-466a-9926-f66b9300d347', + name: 'test community' + }); + + const collectionDataServiceStub = { + create: (com, uuid?) => Observable.of({ + response: new DSOSuccessResponse(null,'200',null) + }) + }; + const communityDataServiceStub = { + findById: (uuid) => Observable.of(new RemoteData(false, false, true, null, Object.assign(new Community(), { + uuid: uuid, + name: community.name + }))) + }; + const routeServiceStub = { + getQueryParameterValue: (param) => Observable.of(community.uuid) + }; + const routerStub = { + navigateByUrl: (url) => url + }; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [TranslateModule.forRoot(), SharedModule, CommonModule, RouterTestingModule], + declarations: [CreateCollectionPageComponent, CollectionFormComponent], + providers: [ + { provide: CollectionDataService, useValue: collectionDataServiceStub }, + { provide: CommunityDataService, useValue: communityDataServiceStub }, + { provide: RouteService, useValue: routeServiceStub }, + { provide: Router, useValue: routerStub } + ] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(CreateCollectionPageComponent); + comp = fixture.componentInstance; + fixture.detectChanges(); + collectionDataService = (comp as any).collectionDataService; + communityDataService = (comp as any).communityDataService; + routeService = (comp as any).routeService; + router = (comp as any).router; + }); + + describe('onSubmit', () => { + const data = { + name: 'test' + }; + + it('should navigate when successful', () => { + spyOn(router, 'navigateByUrl'); + comp.onSubmit(data); + fixture.detectChanges(); + expect(router.navigateByUrl).toHaveBeenCalled(); + }); + + it('should not navigate on failure', () => { + spyOn(router, 'navigateByUrl'); + spyOn(collectionDataService, 'create').and.returnValue(Observable.of({ + response: Object.assign(new ErrorResponse(new RequestError()), { + isSuccessful: false + }) + })); + comp.onSubmit(data); + fixture.detectChanges(); + expect(router.navigateByUrl).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/src/app/+community-page/create-community-page/create-community-page.component.spec.ts b/src/app/+community-page/create-community-page/create-community-page.component.spec.ts index 990a9895b9..3c4c7648da 100644 --- a/src/app/+community-page/create-community-page/create-community-page.component.spec.ts +++ b/src/app/+community-page/create-community-page/create-community-page.component.spec.ts @@ -7,18 +7,20 @@ import { TranslateModule } from '@ngx-translate/core'; import { Observable } from 'rxjs/Observable'; import { RemoteData } from '../../core/data/remote-data'; import { Community } from '../../core/shared/community.model'; -import { DSOSuccessResponse } from '../../core/cache/response-cache.models'; +import { DSOSuccessResponse, ErrorResponse } from '../../core/cache/response-cache.models'; import { BrowserModule } from '@angular/platform-browser'; import { SharedModule } from '../../shared/shared.module'; import { CommonModule } from '@angular/common'; import { CommunityFormComponent } from '../community-form/community-form.component'; +import { RouterTestingModule } from '@angular/router/testing'; +import { RequestError } from '../../core/data/request.models'; describe('CreateCommunityPageComponent', () => { let comp: CreateCommunityPageComponent; let fixture: ComponentFixture; let communityDataService: CommunityDataService; let routeService: RouteService; - let router: any = {}; + let router: Router; const community = Object.assign(new Community(), { uuid: 'a20da287-e174-466a-9926-f66b9300d347', @@ -37,15 +39,18 @@ describe('CreateCommunityPageComponent', () => { const routeServiceStub = { getQueryParameterValue: (param) => Observable.of(community.uuid) }; + const routerStub = { + navigateByUrl: (url) => url + }; beforeEach(async(() => { TestBed.configureTestingModule({ - imports: [TranslateModule.forRoot(), SharedModule, CommonModule], + imports: [TranslateModule.forRoot(), SharedModule, CommonModule, RouterTestingModule], declarations: [CreateCommunityPageComponent, CommunityFormComponent], providers: [ { provide: CommunityDataService, useValue: communityDataServiceStub }, { provide: RouteService, useValue: routeServiceStub }, - { provide: Router, useValue: router } + { provide: Router, useValue: routerStub } ] }).compileComponents(); })); @@ -59,11 +64,28 @@ describe('CreateCommunityPageComponent', () => { router = (comp as any).router; }); - it('should navigate on successful submit', () => { - spyOn(router, 'navigateByUrl'); - comp.onSubmit({ + describe('onSubmit', () => { + const data = { name: 'test' + }; + + it('should navigate when successful', () => { + spyOn(router, 'navigateByUrl'); + comp.onSubmit(data); + fixture.detectChanges(); + expect(router.navigateByUrl).toHaveBeenCalled(); + }); + + it('should not navigate on failure', () => { + spyOn(router, 'navigateByUrl'); + spyOn(communityDataService, 'create').and.returnValue(Observable.of({ + response: Object.assign(new ErrorResponse(new RequestError()), { + isSuccessful: false + }) + })); + comp.onSubmit(data); + fixture.detectChanges(); + expect(router.navigateByUrl).not.toHaveBeenCalled(); }); - expect(router.navigateByUrl).toHaveBeenCalled(); }); }); From 7bb264b8e6d3cebfa74aee2e63f1533866897e04 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Thu, 23 Aug 2018 11:42:36 +0200 Subject: [PATCH 041/604] 54472: CommunityFormComponent tests --- .../community-form.component.html | 4 +- .../community-form.component.spec.ts | 70 +++++++++++++++++++ 2 files changed, 72 insertions(+), 2 deletions(-) create mode 100644 src/app/+community-page/community-form/community-form.component.spec.ts diff --git a/src/app/+community-page/community-form/community-form.component.html b/src/app/+community-page/community-form/community-form.component.html index 1bb5e97ec1..578e2a0d6a 100644 --- a/src/app/+community-page/community-form/community-form.component.html +++ b/src/app/+community-page/community-form/community-form.component.html @@ -21,7 +21,7 @@
- - + +
diff --git a/src/app/+community-page/community-form/community-form.component.spec.ts b/src/app/+community-page/community-form/community-form.component.spec.ts new file mode 100644 index 0000000000..b7b18dc70b --- /dev/null +++ b/src/app/+community-page/community-form/community-form.component.spec.ts @@ -0,0 +1,70 @@ +import { CommunityFormComponent } from './community-form.component'; +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { SharedModule } from '../../shared/shared.module'; +import { TranslateModule } from '@ngx-translate/core'; +import { CommonModule } from '@angular/common'; +import { RouterTestingModule } from '@angular/router/testing'; +import { By } from '@angular/platform-browser'; +import { DebugElement } from '@angular/core'; + +fdescribe('CommunityFormComponent', () => { + let comp: CommunityFormComponent; + let fixture: ComponentFixture + let location: Location; + + /* tslint:disable:no-empty */ + const locationStub = { + back: () => {} + }; + /* tslint:enable:no-empty */ + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [TranslateModule.forRoot(), SharedModule, CommonModule, RouterTestingModule], + declarations: [CommunityFormComponent], + providers: [ + { provide: Location, useValue: locationStub } + ] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(CommunityFormComponent); + comp = fixture.componentInstance; + fixture.detectChanges(); + location = (comp as any).location; + }); + + describe('when submitting', () => { + let input: DebugElement; + let submit: DebugElement; + let cancel: DebugElement; + let error: DebugElement; + + beforeEach(() => { + input = fixture.debugElement.query(By.css('input#community-name')); + submit = fixture.debugElement.query(By.css('button#community-submit')); + cancel = fixture.debugElement.query(By.css('button#community-cancel')); + error = fixture.debugElement.query(By.css('div.invalid-feedback')); + }); + + it('should display an error when leaving name empty', () => { + const el = input.nativeElement; + + el.value = ''; + el.dispatchEvent(new Event('input')); + submit.nativeElement.click(); + fixture.detectChanges(); + + expect(error.nativeElement.style.display).not.toEqual('none'); + }); + + it('should navigate back when pressing cancel', () => { + spyOn(location, 'back'); + cancel.nativeElement.click(); + fixture.detectChanges(); + + expect(location.back).toHaveBeenCalled(); + }); + }) +}); From def2cfab3a05a4bd1ab2de69a01e5da6c3bdcc4b Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Thu, 23 Aug 2018 12:59:33 +0200 Subject: [PATCH 042/604] 54472: CollectionFormComponent tests --- .../collection-form.component.html | 4 +- .../collection-form.component.spec.ts | 71 +++++++++++++++++++ .../community-form.component.spec.ts | 4 +- 3 files changed, 75 insertions(+), 4 deletions(-) create mode 100644 src/app/+collection-page/collection-form/collection-form.component.spec.ts diff --git a/src/app/+collection-page/collection-form/collection-form.component.html b/src/app/+collection-page/collection-form/collection-form.component.html index 808a2793c4..ea99c58c6a 100644 --- a/src/app/+collection-page/collection-form/collection-form.component.html +++ b/src/app/+collection-page/collection-form/collection-form.component.html @@ -29,7 +29,7 @@
- - + +
diff --git a/src/app/+collection-page/collection-form/collection-form.component.spec.ts b/src/app/+collection-page/collection-form/collection-form.component.spec.ts new file mode 100644 index 0000000000..9c65b7c305 --- /dev/null +++ b/src/app/+collection-page/collection-form/collection-form.component.spec.ts @@ -0,0 +1,71 @@ +import { CommunityFormComponent } from './community-form.component'; +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { SharedModule } from '../../shared/shared.module'; +import { TranslateModule } from '@ngx-translate/core'; +import { CommonModule } from '@angular/common'; +import { RouterTestingModule } from '@angular/router/testing'; +import { By } from '@angular/platform-browser'; +import { DebugElement } from '@angular/core'; +import { CollectionFormComponent } from './collection-form.component'; + +describe('CommunityFormComponent', () => { + let comp: CollectionFormComponent; + let fixture: ComponentFixture + let location: Location; + + /* tslint:disable:no-empty */ + const locationStub = { + back: () => {} + }; + /* tslint:enable:no-empty */ + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [TranslateModule.forRoot(), SharedModule, CommonModule, RouterTestingModule], + declarations: [CollectionFormComponent], + providers: [ + { provide: Location, useValue: locationStub } + ] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(CollectionFormComponent); + comp = fixture.componentInstance; + fixture.detectChanges(); + location = (comp as any).location; + }); + + describe('when submitting', () => { + let input: DebugElement; + let submit: DebugElement; + let cancel: DebugElement; + let error: DebugElement; + + beforeEach(() => { + input = fixture.debugElement.query(By.css('input#collection-name')); + submit = fixture.debugElement.query(By.css('button#collection-submit')); + cancel = fixture.debugElement.query(By.css('button#collection-cancel')); + error = fixture.debugElement.query(By.css('div.invalid-feedback')); + }); + + it('should display an error when leaving name empty', () => { + const el = input.nativeElement; + + el.value = ''; + el.dispatchEvent(new Event('input')); + submit.nativeElement.click(); + fixture.detectChanges(); + + expect(error.nativeElement.style.display).not.toEqual('none'); + }); + + it('should navigate back when pressing cancel', () => { + spyOn(location, 'back'); + cancel.nativeElement.click(); + fixture.detectChanges(); + + expect(location.back).toHaveBeenCalled(); + }); + }) +}); diff --git a/src/app/+community-page/community-form/community-form.component.spec.ts b/src/app/+community-page/community-form/community-form.component.spec.ts index b7b18dc70b..d8eab13f20 100644 --- a/src/app/+community-page/community-form/community-form.component.spec.ts +++ b/src/app/+community-page/community-form/community-form.component.spec.ts @@ -7,9 +7,9 @@ import { RouterTestingModule } from '@angular/router/testing'; import { By } from '@angular/platform-browser'; import { DebugElement } from '@angular/core'; -fdescribe('CommunityFormComponent', () => { +describe('CommunityFormComponent', () => { let comp: CommunityFormComponent; - let fixture: ComponentFixture + let fixture: ComponentFixture; let location: Location; /* tslint:disable:no-empty */ From 870c545357f07cf7eb9f704ade62b516452f0c0d Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Thu, 23 Aug 2018 13:56:16 +0200 Subject: [PATCH 043/604] 54472: Added metadata to community/collection creation --- .../collection-form/collection-form.component.html | 4 ++-- .../create-collection-page.component.ts | 9 ++++++++- .../create-community-page.component.ts | 8 +++++++- src/app/core/data/comcol-data.service.ts | 5 +++++ 4 files changed, 22 insertions(+), 4 deletions(-) diff --git a/src/app/+collection-page/collection-form/collection-form.component.html b/src/app/+collection-page/collection-form/collection-form.component.html index ea99c58c6a..85135af15d 100644 --- a/src/app/+collection-page/collection-form/collection-form.component.html +++ b/src/app/+collection-page/collection-form/collection-form.component.html @@ -22,11 +22,11 @@
- +
- +
diff --git a/src/app/+collection-page/create-collection-page/create-collection-page.component.ts b/src/app/+collection-page/create-collection-page/create-collection-page.component.ts index 2e8d9b557d..92314a53c7 100644 --- a/src/app/+collection-page/create-collection-page/create-collection-page.component.ts +++ b/src/app/+collection-page/create-collection-page/create-collection-page.component.ts @@ -33,7 +33,14 @@ export class CreateCollectionPageComponent { onSubmit(data: any) { const collection = Object.assign(new Collection(), { - name: data.name + name: data.name, + metadata: [ + { key: 'dc.description', value: data.introductory }, + { key: 'dc.description.abstract', value: data.description }, + { key: 'dc.rights', value: data.copyright }, + { key: 'dc.rights.license', value: data.license } + // TODO: metadata for news and provenance + ] }); this.parentUUID$.subscribe((uuid: string) => { let response$: Observable; diff --git a/src/app/+community-page/create-community-page/create-community-page.component.ts b/src/app/+community-page/create-community-page/create-community-page.component.ts index 1da0299e66..edb332d807 100644 --- a/src/app/+community-page/create-community-page/create-community-page.component.ts +++ b/src/app/+community-page/create-community-page/create-community-page.component.ts @@ -31,7 +31,13 @@ export class CreateCommunityPageComponent { onSubmit(data: any) { const community = Object.assign(new Community(), { - name: data.name + name: data.name, + metadata: [ + { key: 'dc.description', value: data.introductory }, + { key: 'dc.description.abstract', value: data.description }, + { key: 'dc.rights', value: data.copyright } + // TODO: metadata for news + ] }); this.parentUUID$.subscribe((uuid: string) => { let response$: Observable; diff --git a/src/app/core/data/comcol-data.service.ts b/src/app/core/data/comcol-data.service.ts index a57f5fa910..745879be1f 100644 --- a/src/app/core/data/comcol-data.service.ts +++ b/src/app/core/data/comcol-data.service.ts @@ -89,6 +89,11 @@ export abstract class ComColDataService { let comp: CollectionFormComponent; diff --git a/src/app/+collection-page/create-collection-page/create-collection-page.component.ts b/src/app/+collection-page/create-collection-page/create-collection-page.component.ts index 92314a53c7..4f57482cbd 100644 --- a/src/app/+collection-page/create-collection-page/create-collection-page.component.ts +++ b/src/app/+collection-page/create-collection-page/create-collection-page.component.ts @@ -20,9 +20,9 @@ import { RemoteData } from '../../core/data/remote-data'; }) export class CreateCollectionPageComponent { - private error$: Observable; - private parentUUID$: Observable; - private communityRDObs: Observable>; + public error$: Observable; + public parentUUID$: Observable; + public communityRDObs: Observable>; public constructor(private collectionDataService: CollectionDataService, private communityDataService: CommunityDataService, private routeService: RouteService, private router: Router) { this.parentUUID$ = this.routeService.getQueryParameterValue('parent'); diff --git a/src/app/+community-page/community-form/community-form.component.spec.ts b/src/app/+community-page/community-form/community-form.component.spec.ts index d8eab13f20..ea17d15942 100644 --- a/src/app/+community-page/community-form/community-form.component.spec.ts +++ b/src/app/+community-page/community-form/community-form.component.spec.ts @@ -6,6 +6,7 @@ import { CommonModule } from '@angular/common'; import { RouterTestingModule } from '@angular/router/testing'; import { By } from '@angular/platform-browser'; import { DebugElement } from '@angular/core'; +import { Location } from '@angular/common'; describe('CommunityFormComponent', () => { let comp: CommunityFormComponent; diff --git a/src/app/+community-page/create-community-page/create-community-page.component.ts b/src/app/+community-page/create-community-page/create-community-page.component.ts index edb332d807..8f2f61ab80 100644 --- a/src/app/+community-page/create-community-page/create-community-page.component.ts +++ b/src/app/+community-page/create-community-page/create-community-page.component.ts @@ -18,9 +18,9 @@ import { RemoteData } from '../../core/data/remote-data'; }) export class CreateCommunityPageComponent { - private error$: Observable; - private parentUUID$: Observable; - private communityRDObs: Observable>; + public error$: Observable; + public parentUUID$: Observable; + public communityRDObs: Observable>; public constructor(private communityDataService: CommunityDataService, private routeService: RouteService, private router: Router) { this.parentUUID$ = this.routeService.getQueryParameterValue('parent'); From 46daefd16e761a12ef4275725c61fed38eadf347 Mon Sep 17 00:00:00 2001 From: lotte Date: Mon, 27 Aug 2018 14:36:04 +0200 Subject: [PATCH 045/604] 55407: updated home page --- resources/images/atmire-logo.svg | 37 +++++++++++++++++++ .../home-news/home-news.component.html | 13 +++---- src/app/footer/footer.component.html | 1 + src/app/footer/footer.component.scss | 4 ++ 4 files changed, 47 insertions(+), 8 deletions(-) create mode 100644 resources/images/atmire-logo.svg diff --git a/resources/images/atmire-logo.svg b/resources/images/atmire-logo.svg new file mode 100644 index 0000000000..5a416693ce --- /dev/null +++ b/resources/images/atmire-logo.svg @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/app/+home-page/home-news/home-news.component.html b/src/app/+home-page/home-news/home-news.component.html index ffc88be574..c7943bb7f5 100644 --- a/src/app/+home-page/home-news/home-news.component.html +++ b/src/app/+home-page/home-news/home-news.component.html @@ -2,19 +2,16 @@
- +
-

Welcome to DSpace

-

DSpace is an open source software platform that enables organisations to:

+

DSpace 7 entities prototype

+

This is the demo site of the prototype implementation of a flexible data model for DSpace 7:

    -
  • capture and describe digital material using a submission workflow module, or a variety of programmatic ingest options -
  • -
  • distribute an organisation's digital assets over the web through a search and retrieval system -
  • -
  • preserve digital assets over the long term
  • +
  • The functionality is governed and discussed by the DSpace 7 Entities Working Group, if you would like to participate, please visit the wiki
  • +
  • Documentation on this prototype implementation and links to the source code can be found here
diff --git a/src/app/footer/footer.component.html b/src/app/footer/footer.component.html index c7f41a07a3..fec75b2fd3 100644 --- a/src/app/footer/footer.component.html +++ b/src/app/footer/footer.component.html @@ -1,5 +1,6 @@