From 2e8b4bacf7d7fcf0ded58c5a578f2da3a544634f Mon Sep 17 00:00:00 2001 From: Richard Gustin Date: Fri, 29 Aug 2014 16:21:06 +1000 Subject: [PATCH 01/14] chore(es5): Got rid of warnings about ES5 --- .jshintrc | 173 +++++++++++++++++++++++++++--------------------------- 1 file changed, 86 insertions(+), 87 deletions(-) diff --git a/.jshintrc b/.jshintrc index 0f7896f..0aceda7 100644 --- a/.jshintrc +++ b/.jshintrc @@ -1,87 +1,86 @@ -{ - // Settings - "maxerr": 100, // Maximum error before stopping. - "passfail": false, // Stop on first error. - "smarttabs": true, // Suppress warnings about mixed tabs and spaces when the latter are used for alignmnent only - - // Predefined globals whom JSHint will ignore. - "browser": true, // Standard browser globals e.g. `window`, `document`. - "predef": [ - // Grunt - "__dirname", - // requireJS - "define", - // JASMINE - "describe", - "it", - "xit", - "beforeEach", - "afterEach", - "expect", - "spyOn", - "runs", - "waits", - "waitsFor" - ], - "globals": { - "require": true, - "module": true, - "sinon": true, - "S": true, - "steal": true, - "test": true, - "equal": true, - "ok": true, - "console": true, - "process": true - }, - - // Development - "debug": false, // Allow debugger statements e.g. browser breakpoints. - "devel": false, // Allow developments statements e.g. `console.log();`. - - // EcmaScript 5 - "es5": true, // Allow EcmaScript 5 syntax. - "esnext": true, // Allow ECMAScript 6 specific syntax - "strict": true, // Require `use strict` pragma in every file. - "globalstrict": true, // Allow global "use strict" (also enables 'strict'). - - // Bug prevention - "asi": false, // Tolerate Automatic Semicolon Insertion (no semicolons). - "laxbreak": false, // Tolerate unsafe line breaks e.g. `return [\n] x` without semicolons. - "laxcomma": true, // Tolerate comma first - "bitwise": true, // Prohibit bitwise operators (&, |, ^, etc.). - "boss": false, // Tolerate assignments inside if, for & while. Usually conditions & loops are for comparison, not assignments. - "curly": true, // Require {} for every new block or scope. - "eqeqeq": true, // Require triple equals i.e. `===`. - "eqnull": false, // Tolerate use of `== null`. - "evil": false, // Tolerate use of `eval`. - "expr": true, // Tolerate `ExpressionStatement` as Programs. - "forin": false, // Tolerate `for in` loops without `hasOwnPrototype`. - "immed": true, // Require immediate invocations to be wrapped in parens e.g. `( function(){}() );` - "latedef": true, // Prohibit variable use before definition. - "loopfunc": false, // Allow functions to be defined within loops. - "maxdepth": 3, // Max number of block nesting levels - "maxparams": false, // Max number of formal parameters allowed per function - "multistr": false, // Suppress warnings about multi-line strings - "noarg": true, // Prohibit use of `arguments.caller` and `arguments.callee`. - "regexp": true, // Prohibit `.` and `[^...]` in regular expressions. - "shadow": false, // Allows re-define variables later in code e.g. `var x=1; x=2;`. - "supernew": false, // Tolerate `new function () { ... };` and `new Object;`. - "undef": true, // Require all non-global variables be declared before they are used. - "unused": true, // Warn when variable is defined but never used - - // Styling prefrences. - "quotmark": "single", // Enforce the consistency of quotation marks - "camelcase": true, // Force all variable names to use either camelCase style or UPPER_CASE with underscores - "indent": 2, // Enforce specific tab width - "newcap": true, // Require capitalization of all constructor functions e.g. `new F()`. - "noempty": false, // Prohibit use of empty blocks. - "nonew": true, // Prohibit use of constructors for side-effects. - "nomen": false, // Prohibit use of initial or trailing underscores in names. - "onevar": false, // Allow only one `var` statement per function. - "plusplus": false, // Prohibit use of `++` & `--`. - "sub": false, // Tolerate all forms of subscript notation besides dot notation e.g. `dict['key']` instead of `dict.key`. - "trailing": true, // Prohibit trailing whitespaces. - "white": false // Enforce Douglas Crockford's JavaScript coding style for white space -} +{ + // Settings + "maxerr": 100, // Maximum error before stopping. + "passfail": false, // Stop on first error. + "smarttabs": true, // Suppress warnings about mixed tabs and spaces when the latter are used for alignmnent only + + // Predefined globals whom JSHint will ignore. + "browser": true, // Standard browser globals e.g. `window`, `document`. + "predef": [ + // Grunt + "__dirname", + // requireJS + "define", + // JASMINE + "describe", + "it", + "xit", + "beforeEach", + "afterEach", + "expect", + "spyOn", + "runs", + "waits", + "waitsFor" + ], + "globals": { + "require": true, + "module": true, + "sinon": true, + "S": true, + "steal": true, + "test": true, + "equal": true, + "ok": true, + "console": true, + "process": true + }, + + // Development + "debug": false, // Allow debugger statements e.g. browser breakpoints. + "devel": false, // Allow developments statements e.g. `console.log();`. + + // EcmaScript 5 + "esnext": true, // Allow ECMAScript 6 specific syntax + "strict": true, // Require `use strict` pragma in every file. + "globalstrict": true, // Allow global "use strict" (also enables 'strict'). + + // Bug prevention + "asi": false, // Tolerate Automatic Semicolon Insertion (no semicolons). + "laxbreak": false, // Tolerate unsafe line breaks e.g. `return [\n] x` without semicolons. + "laxcomma": true, // Tolerate comma first + "bitwise": true, // Prohibit bitwise operators (&, |, ^, etc.). + "boss": false, // Tolerate assignments inside if, for & while. Usually conditions & loops are for comparison, not assignments. + "curly": true, // Require {} for every new block or scope. + "eqeqeq": true, // Require triple equals i.e. `===`. + "eqnull": false, // Tolerate use of `== null`. + "evil": false, // Tolerate use of `eval`. + "expr": true, // Tolerate `ExpressionStatement` as Programs. + "forin": false, // Tolerate `for in` loops without `hasOwnPrototype`. + "immed": true, // Require immediate invocations to be wrapped in parens e.g. `( function(){}() );` + "latedef": true, // Prohibit variable use before definition. + "loopfunc": false, // Allow functions to be defined within loops. + "maxdepth": 3, // Max number of block nesting levels + "maxparams": false, // Max number of formal parameters allowed per function + "multistr": false, // Suppress warnings about multi-line strings + "noarg": true, // Prohibit use of `arguments.caller` and `arguments.callee`. + "regexp": true, // Prohibit `.` and `[^...]` in regular expressions. + "shadow": false, // Allows re-define variables later in code e.g. `var x=1; x=2;`. + "supernew": false, // Tolerate `new function () { ... };` and `new Object;`. + "undef": true, // Require all non-global variables be declared before they are used. + "unused": true, // Warn when variable is defined but never used + + // Styling prefrences. + "quotmark": "single", // Enforce the consistency of quotation marks + "camelcase": true, // Force all variable names to use either camelCase style or UPPER_CASE with underscores + "indent": 2, // Enforce specific tab width + "newcap": true, // Require capitalization of all constructor functions e.g. `new F()`. + "noempty": false, // Prohibit use of empty blocks. + "nonew": true, // Prohibit use of constructors for side-effects. + "nomen": false, // Prohibit use of initial or trailing underscores in names. + "onevar": false, // Allow only one `var` statement per function. + "plusplus": false, // Prohibit use of `++` & `--`. + "sub": false, // Tolerate all forms of subscript notation besides dot notation e.g. `dict['key']` instead of `dict.key`. + "trailing": true, // Prohibit trailing whitespaces. + "white": false // Enforce Douglas Crockford's JavaScript coding style for white space +} From ec9b6c4cb39b1758cd49ae5847fb29a8f8229621 Mon Sep 17 00:00:00 2001 From: Richard Gustin Date: Fri, 29 Aug 2014 16:21:43 +1000 Subject: [PATCH 02/14] feat(app): Adding in default images --- app/images/brand.png | Bin 0 -> 1701 bytes app/images/fade-to-white.png | Bin 0 -> 208 bytes .../hvd_fonts_-_plutosanslight-webfont.eot | Bin 0 -> 34718 bytes app/images/icon-close.png | Bin 0 -> 1179 bytes app/images/logobig.png | Bin 0 -> 14040 bytes app/images/modal-header-bg.png | Bin 0 -> 235 bytes app/images/nav-app-bg.png | Bin 0 -> 305 bytes app/images/nav-app-item-bg.png | Bin 0 -> 200 bytes app/images/nav-main-bg.png | Bin 0 -> 252 bytes 9 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 app/images/brand.png create mode 100644 app/images/fade-to-white.png create mode 100644 app/images/hvd_fonts_-_plutosanslight-webfont.eot create mode 100644 app/images/icon-close.png create mode 100644 app/images/logobig.png create mode 100644 app/images/modal-header-bg.png create mode 100644 app/images/nav-app-bg.png create mode 100644 app/images/nav-app-item-bg.png create mode 100644 app/images/nav-main-bg.png diff --git a/app/images/brand.png b/app/images/brand.png new file mode 100644 index 0000000000000000000000000000000000000000..37b0c4c86b076e66a4aecda0ca28b4eb1dba2c92 GIT binary patch literal 1701 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≶7~8_Ub-Ke=_vp4A=LgKmQJB#JjhGsb)1Wi* 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 z z0yIs4@2N3h#rzr$z}S^c)V_MNIYptqCWKiOOHO|~qj+H0iiBvVxI-Aj0i=L^krjZ9 zh&dUGLlB`Fu1vQ_lT4oc^rq>bn}U3->k67JW8^u#i9{79GG5$-Nn@c1(<;S-wEPJR zXj)|Fo{kbu(L_4+=B7c=oSl5*q%(Wu$P7o$Gez-c4^yI!ba+zC@+SX95EX#hOgav= z8qI-`9GiKq;$^kvE_G>+Gm9!n?s=ZdeyyCN>D%~RawuAykZOfcQ~Af~<~xMVXSr2* zf=W<4EC8IiIOWO%L}UhqAlve#vSL!%3lb}*o-}7u#ltokZAy*~+ah)Vz+lsB3aX>g z!#8n9kYmMEk9=Z~s{MA)BIawT_$Ry!&oUTc&wHYy(v~)oT$qFm1|`YSz)+CS-UMGJ zmxL2H%vHEDn%YYLIw_jg9wvBCLwZtUg8`OI+(KK8IMp2WqpyRbQ%ydk6U`#H>o~6u z5$_x!I4C+CvUI+j^3(NgSeYEt`eGhL-<}?N$AtF~ZOBU4Tt>w3vnCC{k(6|xz#|&Ofc(RYoTj zmixc83S5Q7armhQnxWhgN+TuQ1TpXM;S(_%Abm@{aR5N@WW+J>=G#Gu2zz@nh&BYp zU*4+}hoj371kuJ}7U?l?h$~8{jEaJji!d?*UbsF>U85Xo@v5Bs!D9~dGAf5?_Sl-j zkzv5oW&{YcgFHr_*~>)p7%y=<1m@-`$#4`1K+6y(8sOoXb}mu-fknL;k+?xgoM9Kr z9`>4i(;qpP5m?brqEQyq1ScgrtKS;+6LN+tewXO6f`nmzI0hdfjlom((9}vW@a&E;5|^Pj`{(Y z5>p$zF5d_4<`X!UU&5?E1)dPBSu+&i27ncJeKEXbMq`kkx|~_UE?5+txKJ{#Em>Pe zWuD5~JLo7R`ms0|Cl1opc*mRte-M2pJXiz)CjLW=_stU7Bb+e!&wSwrb9UZcllMOc zCik%cZ}8cI9E;tyVCji&`JtQ1Ck8osA{LB-LAJzsynysPy^GF#86EtHZ+PR0wl*Em zV*X97oO>pmYtgD%ykHSy zUd1pOs7$r-Ah3~>0w-E~wmKNmXmD4yav|5;sGwj80S+UEkV3QZfyopIfdB(9R0fR+ zh`q!+s_J;%7)qcAYI_M>RJA0Y@)Co(^mt)h4lHITQ=pGa>cs}2SO>Hk$|#IfHSSl! zY$1gzgTbZI3vx8Y%l|oO_Gh!{wwOSS-@XSDoh^8i2#v~HLUn}iT@>&00d1{2y_DcV*gWge~q@n zcuLULl}(x=V+|o-3I)b2o z7t}76F;rSIDafF0*QDq~PrxOkN14I{*fC@h(xh{S{jw#rgGkZDq*yUc?sddzzKh#B z@Uh$mh|Io`s8CF6Vl-vm;bB+0aX0`)MQMs7G0YO+M<>Iix7e~o7Y+sHVk@5@%_)Tv zI&V^L3L}vimGIEsVUhwfFo=xI1LH+g77*kv%#L80>Na+uNWs2JuN+PujqU=*gPoBK z2=Np&crQBI9g$=f;G_d1HCn_JRnRH`kf8LtSU{q!XHtc6w#1gMzWYgcQ>2uE&4@5o2$Yl40&H zC(Ok@>^41H&dGu-y?aqw&0%RzP*6I6JU}w^Z>%9%S$=BIr3F6rSRAjgF3*L;*9{2H z@l=cih_JSFrECzy0UXqXi1;*oF}<8D6F1C3z#BJa=VeDW%|_a(PA>L^LKzg<;uUYO($H`q!{yvJziD_bT< zTv+>Imf_2A0J=bK(e#$~kCd&yXs+lU(I&X}(~D4*SNN_g3sP>fS)ze)H+)jsJDi{A zLh1fwOL#tLjds{|OCnuB0?z8k_H-0^qor@kzs-U0Fb6G`{h{Z^S~OQ0h9G2&qOl;O zIM9m)$l(_Is<|LSeZ_q*mqQ>AncxlmZPE81-#fOw37!hC*dy2br+=!tYozzezB}zM zevY@4RaS5ca2itoL9Dwek8*Qv0)rYC7iuS@Zi97^v|%xtdr(A)geXUC8e3@TR&J>) z$L5G>*NoH>DBFWq2#C)291x{AMRamZd(l_*IuiI!&z#eDmaK4R0j;SBo!AAaH4#sL;hlF;uuh!|DNSWG)qOVAS4DnGe>)^N8tIA%$t~-q!yJfpegtt>U4oCu}CHssq!=`q}a%L6EM;yI3_cB{G!2-pHp!F zScL0wjEH!|E5MB7&}3Yi zU?a?&;4^)*T@61`uuwyzT**|^wb8x97yvS-S2PGXC|@=KJLB^%5)RU`FoqJS)Vt|A z{APMqGn$_-*VJ|+>su~I`6g#C?R`~S_>u?tT?qSiK|qVo;ebko6P`nmJpUz&AIt z9YAh?AK8M8;QW^4NK2Luh)GMun}b|+l1%I)l!A*q;<_)xwByd`O_?_ycpk z^=Ca*zeCIcbw}gXHCJ}mAOeUx z*u|>H?jQ+?^<--@0F%M{l<8yjmZ%oqy0CL;dlD!e69KQlq5;4ml&cwmp{BkVkRWxC zk=gQ!izro;amr+Z#26{NYZe?27D4jJZScUDSaDarSPW?V<3MOw`0yUr>4ih}P;7WI zsbC%u`YZI4t;hvY`1a6~2ktZbWnHa`%z{NoRMtvwJ^CZNF?Bl~EyrzkHEkXJ47df( zWFTYekJzGeJ|u&uS%hHlL5%nR<4Ry*GHs>6IwN9b3K>EGQU_s3zW^wh$Io@PbXmMm zRJwE_Od%BMZ*PKd4Gq^!{YcpJM*_35$s3TMTib_*2rX6cf)1uYs<0qt#Dy$q6CZ%< zXb?0@JP0pf z=Zstsa0-VBPOmEuHCdRb76lI9_4OW2D*nx&#xM_?V+U6l2jU{S8FBUn9|%Y!$mcl7 zrG=yk4LiLzh}TIn^`uTenLg9kWm9dPc2r;uco$*Kmv)_ z$f_oU67}2b5QYbUPmuyNF%UXp=8qUaB8@?SQWOauzhUJffZlSk(_m5)pI+TT6Hg3| zkoMnTvOKViTss?n+@`?9I`xGa$Pug*IO|S=X#Uqf{@w)&9gSD~w<&ffqxA zi791%A>L?>@g|qEw1@qfvq{%D4I%m`wDML#70fTm@hUbXa+ioomP*It%(>Cieom}O zsCWijd2e7P4M~VV3Z=>2k`-hS_hqG-h5RC1uEPhrWZQ^MM~D!p+5jFQ(^0F1H9mPk z2}2KMWw5~FH3g9r$uK&@M8X>qf3RlINwu1LCaxxRfPalAK-aoVzH(MBiNGR*Mr8ZB zpfjoy__~qG!J!3yy!LKJEN&n1-e#y2ZCGc8KeQA0v!U z-Q{v5F6he=EuK@Io8?6(CVEXV%g75UK^4vL0l<*Gnsayr%Wc#6p5P`ihE3iuNtiQ` zP7M*t3}!e*tNlJaDAIsQw&@E-f^&2f?*I^GxB^3>{MWE!V3KSpybqk-v61Q^EXL8g_Ah(+6Oxj*0_4pAr zLelx;fFu^&oG5iJT%^6;8^StMHHIplB1b}K!{Rev4ID8v25WBIXQtlDG@i$94A3wL zg(ap@$Pe!!JSYzNaULcBU5)R^gpuUG0zh5CHL;p8TEd-@*6nR9U+01XJ~zrBnS%@> zml8cN=*nPH;Ge~mo?=Wt zbt%2)#&LtE(1wZ*pX{tV1&C#`UJmpw4j|5?i?y;3ouX2Xw349^D(U|Rh`y3caOU~7 zk3L>(reK_)5Mw~YtLV=32{{Ny$j4?o{)uyT3K~QKYh*HDsu3-(qwYk@U>HjnmmzC;em|@PjdbkBw--$=&@&9nY5G2qC;`NaGa;a2FQZF>wR(lmr1xNtNKVKL?u-i|VJ06R|8q0^*Y2?ge{ACYe5D^>uA%24hR$Mv zP9_39N_^u z`CXFLqzW4_kLra7$wh{s97~;Ym5LC?R4)`w=Ev6cvhN)_n%|J?xd#FrI8x~Koxq|2 zf-GWywA})<>42TPc2oL5zyvFn)hUzbIC+^z(cF*uyaI@Bc#9u0@HYqr4ggzN3S>DLd<%2=A2q2IqrNSU;TKPy$AUBv*JJud4hH$;u$$(4pihbeeL!5`8Gp zoqd~fasnEG&YuL|Mv@@BVm{`y5KD;6A*QObH3e_OK*-)c<;QhI_)%%{C7X2RfP(Pe z089mi^VV{pZ4h#T?(9PkU( z4%0so4y|XQMQC=gL1+mNAoX#E;e@E-M9&?Pq2K7bC_}})HlgZXDZ|00Ot6_l?Z{Ga z*<4SCisJPBj@MqohT6zKXP83I+!k^J-qZ@iJSG&h5p`SnqnpH@~hd4p^1*7)Qktxy2#q z-NWoSq&@x>tD;trhWvL7Fs%oa=(Sntho#9#*_u;YhY~3hnrzj8Q;N6Xt;Yp)!9&0FmDel?5ZrOZQ0hj+BIZ z%*h1=BzYWhM#F*vm{J1*R;JLvXoNOGmkdcq%`>n<_Oh=DSPhnxo)b`y7%T&1@PhV< zv*sNJs&a7mGeLhXpgyVU>FB;HC_7hZ9~5$D@D0082PeTRX_;kt08!Djts_vBdL3RU zqcqGuz_em&tv{qX1@n!Mh#WUO#Dv3#Vg8V+f1<0#(G?2YBjL zOUd&1%YWGbW8IV@N8aS=HQjn7C}!nm+5jjsN&4GxRYU=+j7b%TsA?7&1p;G?y<<8X7kaC{&?Dxr+fZ6p|>pZ18gjZuSn z>Ba(;>(N=3lk<=)3Bu+HwexJSAglUtaluptIK+(#M2sf}ur%S|oIXzj zQCyrw>L4SWH85J>{?j`SEEOCe!;Z1#m;i`;z!kIx&p3z_Ip2hs0%K4&S{Z@o*oT9H z^*BWg0lS!Tf^mUKl5rMCv{MCDQ+)BAF{jPlvq4rhe4ZM2xrywpNV6DsaMu2^FQj4H ztp4ZC1M8e*_J*cexJ3p^s!4GEmjQK}V}w)EK0oaDc-@m@gwe3;aGEwX0W{Fh`Gs`k z(?OX(sih01lsOK_USM3-CYr`u5fkmLVi(^+c=e0{djo|0&NzJBw{8K^F-1z+<2Sd!cAmGhWB&5x`4TQdkOFkh$Aiea<>*pHU5u2 zlM!zyk0Kv%PX=br1JfKIjYMF@L^<(>S#ghMyJ_?aGCy+=Np5i#ej^4eeF*iO(B~@| zs)6w=l+e~OQ4iXsnAm7zYJY&k1s`j|G|;QB67^Rd!4-~HNpb*NYXZg{2Mm5(8^}*# z9EfcKNVmuimmw1zwWUPE9(NO{LM%oUp>H7!4b9w}%}9Ma%)m52gs6;T{U z-BFog{GhM>`$L$|Xo+e0M9&&QBvfP-WHQbO|F&?p4ix_U0r3#=G z)$Fw|A{At*-QfUW6tPA)#6_|}?=l=29%R@_qoP0ar45JKlgB<9fE=w)*PH?8!zG?Y zi&-37QXwlTq;4RNeGupWv~CoDE)sIQc! z+Rb7^FRVFek^|MWYlenB)@rTTz=Ih4-4iWI(N}PcA?;Pv?}1-rx~H8|16z%Uuow#)G_mozD(svlef7qyj}{ReQi335 CESRPM literal 0 HcmV?d00001 diff --git a/app/images/icon-close.png b/app/images/icon-close.png new file mode 100644 index 0000000000000000000000000000000000000000..77c6a984a7b6b3c7744d7bdee9d79cca783a8e22 GIT binary patch literal 1179 zcmeAS@N?(olHy`uVBq!ia0vp^JRr=$1|-8uW1a&k$r9IylHmNblJdl&R0hYC{G?O` z&)mfH)S%SFl*+=BsWuD@%u1Od5hW46K32*3xq68pHF_1f1wh>l3^w)^1&PVosU-?Y zsp*+{wo31J?^jaDOtDo8H}y5}EpSfF$n>ZxN)4{^3rViZPPR-@vbR&PsjvbXkegbP zs8ErclUHn2VXFi-*9yo63F|81#=KlDb#X~hD#E>34K5C;EJ)Q4 zN-fSWElLJPT$(b-ssbzLqSVBa{GyQj{2W*)24v)y2=9ZF3nBND}m`vLFjeGsTY(KatnYqyQCInmZhe+73JqDfW2&$iQ6qsIL(9V zO~LIJXPkQVfsWA!MJ!T8!-RmT2gHOYTObFX@Kf`Esl5o8tmkn2{LH|>$nWXm7*cU7 z>CgZF_RO9P%NRYZ89#a|h`CM{Z|$-^Xv(nBQlOsc(f|4O_4}%yoDkGpF^5ASo~`r$ z&(F_acM7ZDNsv1C&z(u$u4YHs>ub8ID^3=C`1kksZ3AAT`}=CEn{+r$K3W_7KRsRl zd`(TwqX%_6Dn2gy_5J<*`2WY58rt@Dc64|!X%#0p@GZT*KK^&&WrNC3Pofr>oVXlw zVj)Y>XW+8#n$;U%Bv|XBQo}c&Y=g*`2Ykz-Zi+XXl z={U2-oP|yNYu2n$YM2)N_U`U*DG_~zkN5Z2*RNi=QnTgv!(+YDbqd0{IXOy=G0_TX q%$>}06?wa+zkB!2OW``N!T|>PkhbsNC(o|{6&IeaelF{r5}E+5exODG literal 0 HcmV?d00001 diff --git a/app/images/logobig.png b/app/images/logobig.png new file mode 100644 index 0000000000000000000000000000000000000000..963ee0458288fd86bd50e8821535c6a773889d10 GIT binary patch literal 14040 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 literal 0 HcmV?d00001 diff --git a/app/images/modal-header-bg.png b/app/images/modal-header-bg.png new file mode 100644 index 0000000000000000000000000000000000000000..071179d3261b476c2ea36c7e15281982798a6a5e GIT binary patch literal 235 zcmeAS@N?(olHy`uVBq!ia0vp^Ahs3<8<6apcd8pmu@pObhHwBu4M$1`kk47*5n0T@ zz;_sg8IR|$NC67Ul(C#5R5WfrBD=NDxcD>w(67H)lP22>{o zQs-Qhnpl#VpQjL#nVZUBXrOOwpl@g_di3vApbBSC7sn8b-sCIBTYjE*U`a?!NKtW< zWfl%xB_SaZAtBjeJh9cIbc?~n#t7j~lL=~J{7ceT%+pwr`)I|Xz}CfF?em=&Ubr&J UKNh{|2sDnt)78&qol`;+0A24!lK=n! literal 0 HcmV?d00001 diff --git a/app/images/nav-app-bg.png b/app/images/nav-app-bg.png new file mode 100644 index 0000000000000000000000000000000000000000..820a2725be2dcfb468c42df477e613c3d66e179f GIT binary patch literal 305 zcmeAS@N?(olHy`uVBq!ia0vp^Q-PR^gAGXTog{M;NU;<Lgc;eV1m6V;%9OZ9lmzFem6RtIr7}3CmX#0vRXP srvh3U?E#LKEnKG*Ljw2~HWlUl^LAVOy!y>EpraT(UHx3vIVCg!0L#{G+W-In literal 0 HcmV?d00001 diff --git a/app/images/nav-app-item-bg.png b/app/images/nav-app-item-bg.png new file mode 100644 index 0000000000000000000000000000000000000000..c52b19c30b73ead7fb710211321dd0394375fbac GIT binary patch literal 200 zcmeAS@N?(olHy`uVBq!ia0vp^j6f{R!3HD)xziFVdQ&MBb@0Q8zO-2eap literal 0 HcmV?d00001 diff --git a/app/images/nav-main-bg.png b/app/images/nav-main-bg.png new file mode 100644 index 0000000000000000000000000000000000000000..9f74f666a1c744878f0aaaf6ed9967a6b253c446 GIT binary patch literal 252 zcmeAS@N?(olHy`uVBq!ia0vp^Aa)7|8<5<8X5n-o#Zv6#8NvYsH5@4&Kt5-IM`SSr z1K(i~W;~w1A_XWYR^l2_;#`!PSdy8arx22vo62Blpl@WUZ)hC4cJ_Oq3K@`!;QX|b z^2DN42FH~Aq*MjB%%art{G#k)1?OPX!mW?Zfa*d#T^vIqTHjtb<|MO&%HNRG5 l-{7hJdOTyFZLuzkxf84GcEuAn?1APpc)I$ztaD0e0sv?LQ5^sP literal 0 HcmV?d00001 From e1cd1ee4fab6ece4ce3fe6113f7135d4596bda84 Mon Sep 17 00:00:00 2001 From: Richard Gustin Date: Fri, 29 Aug 2014 16:22:02 +1000 Subject: [PATCH 03/14] refactor(index): New structure for index --- app/index.html | 40 +++++++++++++++++++++++++++++++++++----- 1 file changed, 35 insertions(+), 5 deletions(-) diff --git a/app/index.html b/app/index.html index 791f66a..c610149 100644 --- a/app/index.html +++ b/app/index.html @@ -6,21 +6,30 @@ - ngSeed + CleverStack - + + + + + + + + + + - + - + -
+
+
+
+
+ From a53be41051ca076d03cfefa98104f3f875d1e262 Mon Sep 17 00:00:00 2001 From: Richard Gustin Date: Fri, 29 Aug 2014 16:23:08 +1000 Subject: [PATCH 04/14] refactor(app): New ng-seed v1.2 app module --- .../app/controllers/ApplicationController.js | 18 +++ app/modules/app/controllers/HomeController.js | 11 ++ app/modules/app/module.js | 110 ++++++++++++++++++ .../tests/unit/controllers/home_controller.js | 0 app/modules/app/views/home.html | 10 ++ app/modules/application/config.js | 45 ------- app/modules/application/main.js | 23 ---- app/modules/application/module.js | 52 --------- .../scripts/application_controller.js | 22 ---- .../application/scripts/helpers_provider.js | 60 ---------- .../application/scripts/home_controller.js | 13 --- .../application/scripts/navbar_controller.js | 98 ---------------- .../application/scripts/navbar_directive.js | 37 ------ .../application/styles/less/dropdown.less | 21 ---- app/modules/application/views/home.html | 4 - .../application/views/partials/navbar.html | 33 ------ 16 files changed, 149 insertions(+), 408 deletions(-) create mode 100644 app/modules/app/controllers/ApplicationController.js create mode 100644 app/modules/app/controllers/HomeController.js create mode 100644 app/modules/app/module.js rename app/modules/{application => app}/tests/unit/controllers/home_controller.js (100%) create mode 100644 app/modules/app/views/home.html delete mode 100644 app/modules/application/config.js delete mode 100644 app/modules/application/main.js delete mode 100644 app/modules/application/module.js delete mode 100644 app/modules/application/scripts/application_controller.js delete mode 100644 app/modules/application/scripts/helpers_provider.js delete mode 100644 app/modules/application/scripts/home_controller.js delete mode 100644 app/modules/application/scripts/navbar_controller.js delete mode 100644 app/modules/application/scripts/navbar_directive.js delete mode 100644 app/modules/application/styles/less/dropdown.less delete mode 100644 app/modules/application/views/home.html delete mode 100644 app/modules/application/views/partials/navbar.html diff --git a/app/modules/app/controllers/ApplicationController.js b/app/modules/app/controllers/ApplicationController.js new file mode 100644 index 0000000..a342657 --- /dev/null +++ b/app/modules/app/controllers/ApplicationController.js @@ -0,0 +1,18 @@ +define( [ 'angular', 'app' ], function( ng ) { + 'use strict'; + + ng + .module( 'app.controllers' ) + .controller( 'ApplicationController', function( $scope, Helpers, Template, Session ) { + $scope.helpers = Helpers; + $scope.tpl = Template; + + $scope.currentUser = null; + + $scope.$watch( Session.getCurrentUser, function( user ) { + $scope.currentUser = user || false; + }); + } + ); + +}); diff --git a/app/modules/app/controllers/HomeController.js b/app/modules/app/controllers/HomeController.js new file mode 100644 index 0000000..651e38d --- /dev/null +++ b/app/modules/app/controllers/HomeController.js @@ -0,0 +1,11 @@ +define( [ 'angular', 'app' ], function( ng ) { + 'use strict'; + + ng + .module( 'app.controllers' ) + .controller( 'HomeController', function( $scope, Helpers ) { + $scope.welcome = 'Welcome to the CleverStack AngularJS Front-end!'; + $scope.helpers = Helpers; + }); + +}); diff --git a/app/modules/app/module.js b/app/modules/app/module.js new file mode 100644 index 0000000..1fd92f4 --- /dev/null +++ b/app/modules/app/module.js @@ -0,0 +1,110 @@ +define( [ 'angular' ], function( ng ) { + 'use strict'; + + ng.module( 'app.controllers', [] ); + + var module = ng.module( 'app', [ + 'cs_common', + 'cs_messenger', + 'cs_modal', + 'auth', + 'roles', + 'app.controllers' + ]); + + module.config( function( $routeProvider, $locationProvider, TemplateProvider, HttpOptionsProvider, SessionProvider, NavbarProvider ) { + + // Turn on HTML5 PushState URL's + $locationProvider.html5Mode( true ); + + // Set the service that will be used for Sessions + SessionProvider.setSessionService( 'SessionService' ); + + // Set the domain of the API + switch( window.location.host.match( '([^:]+):?(.*)' )[ 1 ] ) { + + case 'localhost': + case '127.0.0.1': + case 'local': + HttpOptionsProvider.setDomain( 'http://localhost:8080' ); + break; + default: + HttpOptionsProvider.setDomain( 'http://54.84.54.95' ); + break; + } + + NavbarProvider.extend({ + app: [ + { + label: 'Home', + href: '/', + class: 'fa-2x fa-home', + order: 10, + display: function() { + return true; + } + }, + { + label: 'Add New...', + class: 'fa-2x fa-plus-circle', + requiresSignIn: true, + order: 20, + subMenu: [ + { + label: 'User', + href: '/user/new', + class: 'fa-user', + requiresSignIn: true, + order: 1, + click: function( $scope, $event ) { + $scope.helpers.openUserModal( false ); + $event.preventDefault(); + } + }, + { + label: 'Role', + href: '/role/new', + class: 'fa-group', + requiresSignIn: true, + order: 2, + click: function( $scope, $event ) { + $scope.helpers.openRoleModal( false ); + $event.preventDefault(); + } + }, + { + label: 'Permission', + href: '/permission/new', + class: 'fa-legal', + requiresSignIn: true, + order: 3, + click: function( $scope, $event ) { + $scope.helpers.openPermissionModal( false ); + $event.preventDefault(); + } + } + ] + }, + { + label: 'Help', + href: '/help', + class: 'fa-2x fa-question-circle', + order: 40, + display: function() { + return true; + } + } + ] + }); + + $routeProvider + .when( '/', { + templateUrl: TemplateProvider.view( 'app', 'home' ), + controller: 'HomeController', + public: true + }); + + }); + + return module; +}); diff --git a/app/modules/application/tests/unit/controllers/home_controller.js b/app/modules/app/tests/unit/controllers/home_controller.js similarity index 100% rename from app/modules/application/tests/unit/controllers/home_controller.js rename to app/modules/app/tests/unit/controllers/home_controller.js diff --git a/app/modules/app/views/home.html b/app/modules/app/views/home.html new file mode 100644 index 0000000..d006240 --- /dev/null +++ b/app/modules/app/views/home.html @@ -0,0 +1,10 @@ +
+
+
+

Homepage

+

{{welcome}}


+ +

To find helpful documentation please go to cleverstack.io

+
+
+
diff --git a/app/modules/application/config.js b/app/modules/application/config.js deleted file mode 100644 index 46821ad..0000000 --- a/app/modules/application/config.js +++ /dev/null @@ -1,45 +0,0 @@ -require.config({ - baseUrl: '/modules/application', - packages: [ - { - name: 'cs_account', - location: '../cs_account' - }, - { - name: 'cs_common', - location: '../cs_common' - }, - { - name: 'cs_session', - location: '../cs_session' - }, - { - name: 'users', - location: '../users' - } - ], - paths: { - jquery: '../../components/jquery/jquery', - angular: '../../components/angular/angular', - ngResource: '../../components/angular-resource/angular-resource', - ngRoute: '../../components/angular-route/angular-route', - ngSanitize: '../../components/angular-sanitize/angular-sanitize', - }, - shim: { - jquery: { - exports: '$' - }, - angular: { - exports: 'angular' - }, - ngResource: { - deps: ['angular'] - }, - ngRoute: { - deps: ['angular'] - }, - ngSanitize: { - deps: ['angular'] - } - } -}); diff --git a/app/modules/application/main.js b/app/modules/application/main.js deleted file mode 100644 index efe3c33..0000000 --- a/app/modules/application/main.js +++ /dev/null @@ -1,23 +0,0 @@ -define([ - 'jquery', - 'angular', - './module', - 'cs_account', - 'cs_common', - 'cs_session', - 'users', - - // Controllers - './scripts/application_controller', - './scripts/home_controller', - './scripts/navbar_controller', - - // Providers - './scripts/helpers_provider', - - // Directives - './scripts/navbar_directive' - -], function() { - -}); diff --git a/app/modules/application/module.js b/app/modules/application/module.js deleted file mode 100644 index dcb635e..0000000 --- a/app/modules/application/module.js +++ /dev/null @@ -1,52 +0,0 @@ -define(['angular'], function (ng) { - 'use strict'; - - ng.module('app.providers', []); - ng.module('app.controllers', []); - ng.module('app.services', []); - ng.module('app.directives', []); - - var module = ng.module('app', [ - 'cs_account', - 'cs_common', - 'cs_session', - 'users', - 'app.providers', - 'app.controllers', - 'app.services', - 'app.directives' - ]); - - module.config([ - '$routeProvider', - '$locationProvider', - 'CSTemplateProvider', - 'CSHttpOptionsProvider', - 'CSSessionProvider', - 'HelpersProvider', - function ($routeProvider, $locationProvider, CSTemplateProvider, CSHttpOptionsProvider, CSSessionProvider, HelpersProvider) { - - HelpersProvider.extend('CSCommonHelpers'); - - CSTemplateProvider.setPath('/modules/application/views'); - - // CSHttpOptionsProvider.setDomain('/api'); - - CSSessionProvider.setSessionService('CSSessionService'); - - $locationProvider.html5Mode(true); - - $routeProvider - .when('/', { - templateUrl: CSTemplateProvider.view('home'), - controller: 'HomeController', - public: true - }); - - } - - ]); - - return module; - -}); diff --git a/app/modules/application/scripts/application_controller.js b/app/modules/application/scripts/application_controller.js deleted file mode 100644 index 89e9324..0000000 --- a/app/modules/application/scripts/application_controller.js +++ /dev/null @@ -1,22 +0,0 @@ -define(['angular', 'application'], function (ng) { - 'use strict'; - - ng.module('app.controllers') - .controller('AppCtrl', [ - '$scope', - 'Helpers', - 'CSTemplate', - 'CSSession', - function ($scope, HelpersProvider, CSTemplateProvider, CSSessionProvider) { - - $scope.helpers = HelpersProvider; - $scope.tpl = CSTemplateProvider; - - $scope.$watch(CSSessionProvider.getCurrentUser, function (user) { - $scope.currentUser = user || false; - }); - - } - - ]); -}); diff --git a/app/modules/application/scripts/helpers_provider.js b/app/modules/application/scripts/helpers_provider.js deleted file mode 100644 index a46405c..0000000 --- a/app/modules/application/scripts/helpers_provider.js +++ /dev/null @@ -1,60 +0,0 @@ -define(['angular', 'application'], function (ng) { - 'use strict'; - - ng.module('app.providers') - .provider('Helpers', [ - function () { - - var helpers = {}; - var inheritedProvider; - - /** - * @description - * The actual service. - */ - return { - $get: [ - '$injector', - function ($injector) { - - if(inheritedProvider){ - var provider = $injector.get(inheritedProvider); - if(!provider){ - throw new Error('Unable to inject "' + inheritedProvider + '"'); - } - ng.copy(provider, helpers); - } - - /** - * Define your own helper functions here - * - * helpers.uppercase = function(string){ - * return string.toUpperCase(); - * } - */ - - return helpers; - - } - ], - - /** - * @ngdoc function - * @methodOf ngSeed.providers:CSAccountProvider - * @name setAccountService - * @param {String} serviceName the account service name - */ - extend: function (providerName) { - if(typeof providerName !== 'string') { - throw new Error('CSHelpersProvider: extend method expects a string (name of the helpers provider)'); - } - inheritedProvider = providerName; - } - - }; - - } - - ]); - -}); diff --git a/app/modules/application/scripts/home_controller.js b/app/modules/application/scripts/home_controller.js deleted file mode 100644 index 9c2b030..0000000 --- a/app/modules/application/scripts/home_controller.js +++ /dev/null @@ -1,13 +0,0 @@ -define(['angular', 'application'], function (ng) { - 'use strict'; - - ng.module('app.controllers') - .controller('HomeController', [ - '$scope', - 'Helpers', - function ($scope, Helpers) { - $scope.welcome = 'Hello clever!'; - } - - ]); -}); diff --git a/app/modules/application/scripts/navbar_controller.js b/app/modules/application/scripts/navbar_controller.js deleted file mode 100644 index 326a266..0000000 --- a/app/modules/application/scripts/navbar_controller.js +++ /dev/null @@ -1,98 +0,0 @@ -define(['angular', 'application'], function (ng) { - 'use strict'; - - ng.module('app.controllers') - .controller('NavbarCtrl', [ - '$rootScope', - '$scope', - 'Helpers', - 'CSTemplate', - 'CSSession', - '$location', - '$route', - function ($rootScope, $scope, HelpersProvider, CSTemplateProvider, CSSessionProvider, $location, $route) { - - $scope.$watch(CSSessionProvider.getCurrentUser, function (user) { - $scope.currentUser = user || false; - }); - - //navbar menu items - //glyphs: http://getbootstrap.com/components/#glyphicons - $scope.navbarItems = { - - //these items appear in the navbar on the left - "left": [ - { - "label": "Users", - "href": "/users", - "glyph": "user", - "requiresLogin": true, - "order": 1 - } - ], - - //these items appear on the navbar on the right - "right": [ - { - "label": "Login", - "href": "/login", - "glyph": "log-in", - "requiresLogin": false, - "order": 1 - }, - { - "label": "Logout", - "href": "/logout", - "glyph": "log-out", - "requiresLogin": true, - "order": 1 - }, - { - "label": "Register", - "href": "/register", - "glyph": "list-alt", - "requiresLogin": false, - "order": 2 - } - ] - }; - - /* sub menu (dropdown) example - - { - "label": "Menu Item 2", - "href": "", - "glyph": "list-alt", - "requiresLogin": false, - "order": 2, - "subMenu": [ - { - "label": "Sub Menu Item 1", - "href": "", - "glyph": "list-alt", - "requiresLogin": false, - "order": 1 - }, - { - "label": "Sub Menu Item 2", - "href": "", - "glyph": "list-alt", - "requiresLogin": false, - "order": 2 - }, - { - "label": "Sub Menu Item 3", - "href": "", - "glyph": "list-alt", - "requiresLogin": false, - "order": 3 - } - ] - } - - */ - - } - - ]); -}); diff --git a/app/modules/application/scripts/navbar_directive.js b/app/modules/application/scripts/navbar_directive.js deleted file mode 100644 index 049c83d..0000000 --- a/app/modules/application/scripts/navbar_directive.js +++ /dev/null @@ -1,37 +0,0 @@ -define(['angular', '../module'], function (ng) { - 'use strict'; - - /** - * @ngdoc directive - * @name ngSeed.directives:navbar - * @description - * Sets the current navbar option in focus. - */ - ng.module('app.directives') - .directive('navbar', function($location) { - return { - restrict: 'E', - replace: true, - transclude: true, - templateUrl: 'modules/application/views/partials/navbar.html', - scope: { - heading: '@' - }, - controller: 'NavbarCtrl', - link: function($scope, $element, $attrs, Navbar) { - - //add the active class to the current address == item href - $scope.activeHref = function(item) { - return ($location.path() == item.href) ? 'active' : ''; - } - - //show if the item requires login and the user is logged in - $scope.showItem = function(item) { - return angular.isDefined($scope.currentUser) && angular.isDefined($scope.currentUser.id) === item.requiresLogin; - } - - } - } - }); - -}); diff --git a/app/modules/application/styles/less/dropdown.less b/app/modules/application/styles/less/dropdown.less deleted file mode 100644 index 5821945..0000000 --- a/app/modules/application/styles/less/dropdown.less +++ /dev/null @@ -1,21 +0,0 @@ -/* dynamic drop down submenu styles */ -.navbar-nav ul.navbar-nav { - - list-style: none; - padding: 0; - - a { - - padding: 15px; - display: inline-block !important; - text-decoration: none; - - &:hover { - - text-decoration: none; - - } - - } - -} diff --git a/app/modules/application/views/home.html b/app/modules/application/views/home.html deleted file mode 100644 index 01c34de..0000000 --- a/app/modules/application/views/home.html +++ /dev/null @@ -1,4 +0,0 @@ -
-

Homepage

-

{{welcome}}

-
diff --git a/app/modules/application/views/partials/navbar.html b/app/modules/application/views/partials/navbar.html deleted file mode 100644 index ef98c95..0000000 --- a/app/modules/application/views/partials/navbar.html +++ /dev/null @@ -1,33 +0,0 @@ - From e61173726d1a18d061523888b8455f9a3f9fcf87 Mon Sep 17 00:00:00 2001 From: Richard Gustin Date: Fri, 29 Aug 2014 16:24:09 +1000 Subject: [PATCH 05/14] feat(auth): New auth module for ng-seed 1.2 --- app/modules/app/main.js | 15 ++ .../PasswordResetRequestController.js | 34 ++++ .../PasswordResetSubmitController.js | 36 ++++ .../RegistrationConfirmController.js | 30 +++ .../auth/controllers/SignInController.js | 40 ++++ .../auth/controllers/SignOutController.js | 10 + .../auth/controllers/SignUpController.js | 51 +++++ .../auth/controllers/UserEditController.js | 41 ++++ .../auth/controllers/UsersListController.js | 66 +++++++ app/modules/auth/filters/LastLoginFilter.js | 18 ++ app/modules/auth/main.js | 35 ++++ app/modules/auth/models/AccountModel.js | 10 + app/modules/auth/models/UserModel.js | 23 +++ app/modules/auth/module.js | 138 ++++++++++++++ app/modules/auth/providers/AccountProvider.js | 138 ++++++++++++++ .../auth/providers/AuthHelpersProvider.js | 78 ++++++++ app/modules/auth/services/AccountService.js | 23 +++ app/modules/auth/services/SessionService.js | 37 ++++ app/modules/auth/services/UserService.js | 24 +++ app/modules/auth/views/account/form.html | 69 +++++++ app/modules/auth/views/partials/signOut.html | 2 + .../auth/views/password_reset_request.html | 20 ++ .../auth/views/password_reset_submit.html | 66 +++++++ .../auth/views/registration_confirm.html | 120 ++++++++++++ app/modules/auth/views/signIn.html | 38 ++++ app/modules/auth/views/signUp.html | 125 ++++++++++++ app/modules/auth/views/users/form.html | 178 ++++++++++++++++++ app/modules/auth/views/users/list.html | 20 ++ app/modules/auth/views/users/show.html | 55 ++++++ .../auth/views/users/table_actions.html | 43 +++++ app/modules/cs_account/main.js | 18 -- app/modules/cs_account/module.js | 39 ---- .../scripts/cs_account_create_controller.js | 39 ---- .../scripts/cs_account_helpers_provider.js | 60 ------ .../cs_account/scripts/cs_account_provider.js | 147 --------------- .../cs_account/scripts/cs_account_service.js | 30 --- .../cs_account/tests/e2e/register.test.js | 26 --- .../cs_account/views/registration.html | 45 ----- 38 files changed, 1583 insertions(+), 404 deletions(-) create mode 100644 app/modules/app/main.js create mode 100644 app/modules/auth/controllers/PasswordResetRequestController.js create mode 100644 app/modules/auth/controllers/PasswordResetSubmitController.js create mode 100644 app/modules/auth/controllers/RegistrationConfirmController.js create mode 100644 app/modules/auth/controllers/SignInController.js create mode 100644 app/modules/auth/controllers/SignOutController.js create mode 100644 app/modules/auth/controllers/SignUpController.js create mode 100644 app/modules/auth/controllers/UserEditController.js create mode 100644 app/modules/auth/controllers/UsersListController.js create mode 100644 app/modules/auth/filters/LastLoginFilter.js create mode 100644 app/modules/auth/main.js create mode 100644 app/modules/auth/models/AccountModel.js create mode 100644 app/modules/auth/models/UserModel.js create mode 100644 app/modules/auth/module.js create mode 100644 app/modules/auth/providers/AccountProvider.js create mode 100644 app/modules/auth/providers/AuthHelpersProvider.js create mode 100644 app/modules/auth/services/AccountService.js create mode 100644 app/modules/auth/services/SessionService.js create mode 100644 app/modules/auth/services/UserService.js create mode 100644 app/modules/auth/views/account/form.html create mode 100644 app/modules/auth/views/partials/signOut.html create mode 100644 app/modules/auth/views/password_reset_request.html create mode 100644 app/modules/auth/views/password_reset_submit.html create mode 100644 app/modules/auth/views/registration_confirm.html create mode 100644 app/modules/auth/views/signIn.html create mode 100644 app/modules/auth/views/signUp.html create mode 100644 app/modules/auth/views/users/form.html create mode 100644 app/modules/auth/views/users/list.html create mode 100644 app/modules/auth/views/users/show.html create mode 100644 app/modules/auth/views/users/table_actions.html delete mode 100644 app/modules/cs_account/main.js delete mode 100644 app/modules/cs_account/module.js delete mode 100644 app/modules/cs_account/scripts/cs_account_create_controller.js delete mode 100644 app/modules/cs_account/scripts/cs_account_helpers_provider.js delete mode 100644 app/modules/cs_account/scripts/cs_account_provider.js delete mode 100644 app/modules/cs_account/scripts/cs_account_service.js delete mode 100644 app/modules/cs_account/tests/e2e/register.test.js delete mode 100644 app/modules/cs_account/views/registration.html diff --git a/app/modules/app/main.js b/app/modules/app/main.js new file mode 100644 index 0000000..74d55b5 --- /dev/null +++ b/app/modules/app/main.js @@ -0,0 +1,15 @@ +define([ + + // This modules definition + './module', + + // CleverStack modules + 'cs_common', + + // Controllers + './controllers/ApplicationController', + './controllers/HomeController' + +], function() { + +}); diff --git a/app/modules/auth/controllers/PasswordResetRequestController.js b/app/modules/auth/controllers/PasswordResetRequestController.js new file mode 100644 index 0000000..6191e0e --- /dev/null +++ b/app/modules/auth/controllers/PasswordResetRequestController.js @@ -0,0 +1,34 @@ +define( [ 'angular', '../module' ], function( ng ) { + 'use strict'; + + ng + .module( 'auth.controllers' ) + .controller( 'PasswordResetRequestController', function( $scope, $injector, Session, Helpers ) { + var messenger = $injector.has( 'Messenger' ) ? $injector.get( 'Messenger' ) : $injector.get( '$log' ); + + $scope.helpers = Helpers; + + $scope.submit = function() { + if( $scope.form && $scope.form.$invalid ){ + return; + } + $scope.submitted = false; + $scope.processing = true; + + Session.requestPasswordReset( { email: $scope.email } ); + }; + + $scope.$on( 'SessionProvider:requestPasswordResetSuccess', function() { + $scope.success = true; + + messenger.success( 'You will shortly receive a confirmation email. Please follow it to reset your password.' ); + }); + + $scope.$on( 'SessionProvider:requestPasswordResetFailure', function( err ) { + $scope.processing = false; + $scope.success = false; + + messenger.error( err || 'Unknown email address.' ); + }); + }); +}); diff --git a/app/modules/auth/controllers/PasswordResetSubmitController.js b/app/modules/auth/controllers/PasswordResetSubmitController.js new file mode 100644 index 0000000..41a9b6f --- /dev/null +++ b/app/modules/auth/controllers/PasswordResetSubmitController.js @@ -0,0 +1,36 @@ +define( [ 'angular', 'underscore', '../module' ], function( ng, _ ) { + 'use strict'; + + ng + .module( 'auth.controllers' ) + .controller( 'PasswordResetSubmitController', function( $scope, $routeParams, $location, $injector, Session, Helpers ) { + var messenger = $injector.has( 'Messenger' ) ? $injector.get( 'Messenger' ) : $injector.get( '$log' ); + + $scope.helpers = Helpers; + $scope.credentials = { + token: $routeParams.t, + user: $routeParams.u + }; + + $scope.submit = function() { + if( $scope.form && $scope.form.$invalid ){ + return; + } + + $scope.processing = true; + $scope.submitted = false; + + Session.submitPasswordReset( _( $scope.credentials ).omit( 'passwordConfirmation' ) ); + }; + + $scope.$on( 'SessionProvider:submitPasswordResetSuccess', function() { + messenger.success( 'Your new password is saved. You may now sign in with your new password.' ); + }); + + $scope.$on( 'SessionProvider:submitPasswordResetFailure', function() { + $scope.processing = false; + messenger.error( 'Unable to store new password.' ); + }); + + }); +}); diff --git a/app/modules/auth/controllers/RegistrationConfirmController.js b/app/modules/auth/controllers/RegistrationConfirmController.js new file mode 100644 index 0000000..2f83c9c --- /dev/null +++ b/app/modules/auth/controllers/RegistrationConfirmController.js @@ -0,0 +1,30 @@ +define( [ 'angular', '../module' ], function( ng ) { + 'use strict'; + + ng + .module( 'auth.controllers' ) + .controller( 'RegistrationConfirmController', [ + '$scope', + '$auth', + 'AuthHelpers', + '$routeParams', + '$location', + function ($scope, $auth, AuthHelpers, $routeParams, $location) { + $scope.$on('$auth:registrationConfirmationSuccess', function(){ + if($auth.getCurrentUser()){ + $location.url('/dashboard'); + } else { + $location.url('/login'); + } + $scope.success = true; + }); + + $scope.$on('$auth:registrationConfirmationFailure', function(){ + $scope.failure = true; + }); + + $auth.confirmRegistration({accountId: $routeParams.accountId, token: $routeParams.token}); + + } + ]); +}); diff --git a/app/modules/auth/controllers/SignInController.js b/app/modules/auth/controllers/SignInController.js new file mode 100644 index 0000000..49d1f85 --- /dev/null +++ b/app/modules/auth/controllers/SignInController.js @@ -0,0 +1,40 @@ +define( [ 'angular', '../module' ], function( ng ) { + 'use strict'; + + ng + .module( 'auth.controllers' ) + .controller( 'SignInController', function( $scope, $log, $location, $injector, Session, Helpers ) { + var messenger = $injector.has( 'Messenger' ) ? $injector.get( 'Messenger' ) : $log; + + $scope.helpers = Helpers; + $scope.processing = false; + $scope.submitted = false; + + $scope.signIn = function() { + if ( $scope.form && $scope.form.$invalid ) { + messenger.warn( 'Fix form errors and try again.' ); + return; + } + + if( $scope.processing ){ + return; + } + + $scope.processing = true; + + Session.signIn( $scope.credentials ); + + $scope.$on( 'SessionProvider:signInFailure', function( event, data ) { + $scope.processing = false; + messenger.error( data.message ? data.message : data ); + }); + + $scope.$on( 'SessionProvider:signInSuccess', function( event ) { + $scope.processing = false; + messenger.success( 'User ' + event.currentScope.credentials.username + ' signed in.' ); + $location.path( '/pages' ); + }); + }; + + }); +}); diff --git a/app/modules/auth/controllers/SignOutController.js b/app/modules/auth/controllers/SignOutController.js new file mode 100644 index 0000000..783acf8 --- /dev/null +++ b/app/modules/auth/controllers/SignOutController.js @@ -0,0 +1,10 @@ +define( [ 'angular', '../module' ], function( ng ) { + 'use strict'; + + ng + .module( 'auth.controllers' ) + .controller( 'SignOutController', function( $injector, $log, Session ) { + $log.log( 'SignOutController: signOut()' ); + Session.signOut(); + }); +}); \ No newline at end of file diff --git a/app/modules/auth/controllers/SignUpController.js b/app/modules/auth/controllers/SignUpController.js new file mode 100644 index 0000000..c48a2ad --- /dev/null +++ b/app/modules/auth/controllers/SignUpController.js @@ -0,0 +1,51 @@ +define( [ 'angular', 'underscore', '../module' ], function( ng, _ ) { + 'use strict'; + + ng + .module( 'auth.controllers' ) + .controller( 'SignUpController', function( $rootScope, $scope, $injector, $log, Account, Helpers, Session, $location ) { + var messenger = $injector.has( 'Messenger' ) ? $injector.get( 'Messenger' ) : $log; + + $scope.helpers = Helpers; + $scope.credentials = {}; + + $scope.processing = false; + + $scope.signUp = function() { + if ( $scope.form && $scope.form.$invalid ) { + messenger.warn( 'Fix form errors and try again.' ); + return; + } + + if ( $scope.processing ) { + return; + } + $scope.processing = true; + + var credentials = _( $scope.credentials ).omit( 'passwordConfirmation' ); + credentials.username = credentials.email.split( '@' )[ 0 ]; + + Session.signUp( credentials ); + }; + + $rootScope.$on( 'SessionProvider:signUpSuccess', function( event, user ) { + $scope.success = true; + Session.authenticate( user ); + $location.path( '/pages' ); + }); + + $rootScope.$on( 'SessionProvider:signUpFailure', function( event, data ) { + $scope.success = false; + $scope.processing = false; + messenger.error( data && data.message ? data.message : 'Unable to Sign Up. Please try again. (or later)' ); + }); + + $scope.checkUrl = function( $event ) { + if ( !/(https?:\/\/)/ig.test( $event.target.value ) ) { + $event.target.value = 'http://' + $event.target.value.replace( RegExp.$1, '' ); + $scope.credentials[ $event.target.name ] = $event.target.value; + } + }; + } + ); +}); diff --git a/app/modules/auth/controllers/UserEditController.js b/app/modules/auth/controllers/UserEditController.js new file mode 100644 index 0000000..54f1362 --- /dev/null +++ b/app/modules/auth/controllers/UserEditController.js @@ -0,0 +1,41 @@ +define( [ 'angular', '../module' ], function( ng ) { + 'use strict'; + + ng + .module( 'auth.controllers' ) + .controller( 'UserEditController', function( $rootScope, $scope, $modalInstance, Helpers, Messenger, UserService, user, roles, currentUser ) { + $scope.helpers = Helpers; + + $scope.user = user; + $scope.roles = roles; + $scope.currentUser = currentUser; + + $scope.save = function() { + var promise; + + if ( this.form && this.form.$invalid ) { + Messenger.warn( 'Fix form errors and try again.' ); + return; + } + + if ( !!$scope.user.id ) { + promise = $scope.user.$save(); + } else { + promise = UserService.create( $scope.user ); + } + + promise + .then( function() { + Messenger.success( 'User ' + $scope.user.fullName + ' (' + $scope.user.email + ') successfully ' + ( !!$scope.user.id ? 'updated.' : 'created.' ) ); + $modalInstance.close( $scope ); + }) + .catch( function( err ) { + Messenger.error( 'Unable to ' + ( !!$scope.user.id ? 'update' : 'create' ) + ' user ' + $scope.user.fullName + ' (' + $scope.user.email + ') due to error (' + err + ')' ); + }); + }; + + $scope.cancel = function () { + $modalInstance.dismiss( 'cancel' ); + }; + }); +}); diff --git a/app/modules/auth/controllers/UsersListController.js b/app/modules/auth/controllers/UsersListController.js new file mode 100644 index 0000000..e92d61b --- /dev/null +++ b/app/modules/auth/controllers/UsersListController.js @@ -0,0 +1,66 @@ +define( [ 'angular', '../module' ], function( ng ) { + 'use strict'; + + ng + .module( 'auth.controllers' ) + .controller( 'UsersListController', function( $scope, Helpers, dateFilter ) { + $scope.helpers = Helpers; + $scope.welcome = 'This page lists all of the Users available in your account, you can add as many as you want'; + $scope.actionsTemplate = '/modules/auth/views/users/table_actions.html'; + + $scope.columns = [ + // { + // name: 'id', + // title: 'Id', + // filter: true, + // filterType: 'text', + // glyph: 'barcode', + // width: 40, + // }, + { + name: 'firstname', + title: 'First Name', + filter: true, + filterType: 'text', + glyph: 'user' + }, + { + name: 'lastname', + title: 'Last Name', + filter: true, + filterType: 'text', + glyph: 'user' + }, + { + name: 'email', + title: 'Email', + filter: true, + filterType: 'text', + glyph: 'envelope' + }, + { + name: 'accessedAt', + title: 'Last Login', + filter: false, + display: function( val ) { + if ( val ) { + return dateFilter( val, 'short' ); + } + return 'Never'; + } + }, + { + name: 'createdAt', + title: 'Date Registered', + filter: true, + filterType: 'text', + glyph: 'calendar', + width: 100, + display: function( val ) { + return dateFilter( val, 'short' ); + } + } + ]; + + }); +}); diff --git a/app/modules/auth/filters/LastLoginFilter.js b/app/modules/auth/filters/LastLoginFilter.js new file mode 100644 index 0000000..379ba13 --- /dev/null +++ b/app/modules/auth/filters/LastLoginFilter.js @@ -0,0 +1,18 @@ +define( [ 'angular', '../module' ], function( ng ) { + 'use strict'; + + ng + .module( 'auth.filters' ) + .filter( 'lastLogin', [ + 'dateFilter', + function( dateFilter ) { + return function( input, dateFormat ) { + if ( input ) { + return dateFilter( input, dateFormat || 'short' ); + } + return 'Not Active'; + }; + } + ]); + +}); diff --git a/app/modules/auth/main.js b/app/modules/auth/main.js new file mode 100644 index 0000000..a0029a5 --- /dev/null +++ b/app/modules/auth/main.js @@ -0,0 +1,35 @@ +define([ + 'cs_common', + + './module', + + // Helpers + './providers/AuthHelpersProvider', + + // Providers + './providers/SessionProvider', + './providers/AccountProvider', + + './models/AccountModel', + './models/UserModel', + + // Services + './services/SessionService', + './services/AccountService', + './services/UserService', + + // Filters + './filters/LastLoginFilter', + + // Controllers + './controllers/SignUpController', + './controllers/SignInController', + './controllers/SignOutController', + './controllers/UsersListController', + './controllers/UserEditController', + './controllers/PasswordResetRequestController', + './controllers/PasswordResetSubmitController' + +], function() { + +}); diff --git a/app/modules/auth/models/AccountModel.js b/app/modules/auth/models/AccountModel.js new file mode 100644 index 0000000..bc4282d --- /dev/null +++ b/app/modules/auth/models/AccountModel.js @@ -0,0 +1,10 @@ +define( [ 'angular', '../module' ], function( ng ) { + 'use strict'; + + ng + .module( 'auth.models' ) + .factory( 'AccountModel', function( ResourceFactory ) { + return new ResourceFactory( '/account', {}, {} ); + }); + +}); diff --git a/app/modules/auth/models/UserModel.js b/app/modules/auth/models/UserModel.js new file mode 100644 index 0000000..8fd39e6 --- /dev/null +++ b/app/modules/auth/models/UserModel.js @@ -0,0 +1,23 @@ +define( [ 'angular', '../module' ], function( ng ) { + 'use strict'; + + ng + .module( 'auth.models' ) + .factory( 'UserModel', function( ResourceFactory ) { + return new ResourceFactory( '/auth/user', { id: '@id' }, { + resend: { + params: { + action: 'resend' + }, + method: 'POST' + }, + confirm: { + params: { + action: 'confirm' + }, + method: 'POST' + } + }); + }); + +}); diff --git a/app/modules/auth/module.js b/app/modules/auth/module.js new file mode 100644 index 0000000..3dcd57a --- /dev/null +++ b/app/modules/auth/module.js @@ -0,0 +1,138 @@ +define( [ 'angular' ], function( ng ) { + 'use strict'; + + ng.module( 'auth.providers', [] ); + ng.module( 'auth.controllers', [] ); + ng.module( 'auth.services', [] ); + ng.module( 'auth.filters', [] ); + ng.module( 'auth.models', [] ); + + var module = ng.module( 'auth', [ + 'cs_common', + 'auth.providers', + 'auth.filters', + 'auth.controllers', + 'auth.models', + 'auth.services' + ]); + + module.config( function( $routeProvider, $injector, TemplateProvider, SessionProvider, HelpersProvider, NavbarProvider ) { + + SessionProvider.setSessionService( 'SessionService' ); + HelpersProvider.extend( 'AuthHelpers' ); + + NavbarProvider.extend({ + right: [ + { + label: 'Sign Up', + href: '/signUp', + class: 'fa-list-alt', + requiresSignIn: false, + order: 1, + display: function( $scope ) { + return !$scope.currentUser; + } + }, + { + label: 'Settings', + href: '', + class: 'fa-cogs', + requiresSignIn: true, + order: 1, + subMenu: [ + { + label: 'Account', + href: '/settings/account', + class: 'fa-institution', + requiresSignIn: true, + order: 3 + }, + { + label: 'Users', + href: '/settings/users', + class: 'fa-user', + requiresSignIn: true, + order: 4 + } + ] + }, + { + label: 'My Account', + click: function( $scope, $event ) { + $scope.helpers.openUserModal( $scope.currentUser, $scope.currentUser, true ); + $event.preventDefault(); + }, + class: 'fa-user', + order: 2, + display: function( $scope ) { + return !!$scope.currentUser; + } + }, + { + label: 'Sign In', + href: '/signIn', + class: 'fa-sign-in', + order: 2, + display: function( $scope ) { + return !$scope.currentUser; + } + }, + { + label: 'Sign Out', + href: '/signOut', + class: 'fa-sign-out', + order: 3, + display: function( $scope ) { + return !!$scope.currentUser; + } + } + ] + }); + + $routeProvider + .when( '/signUp', { + templateUrl: TemplateProvider.view( 'auth', 'signUp' ), + controller: 'SignUpController', + public: true + }) + .when('/registration_confirm/:accountId/:token', { + templateUrl: TemplateProvider.view( 'auth', 'registration_confirm' ), + controller: 'RegistrationConfirm', + public: true + }) + .when( '/signIn', { + templateUrl: TemplateProvider.view( 'auth', 'signIn' ), + controller: 'SignInController', + public: true + }) + .when( '/signOut', { + templateUrl: TemplateProvider.view( 'auth', 'partials/signOut' ), + controller: 'SignOutController', + template: ' ', + public: true + }) + .when('/password_reset_request', { + templateUrl: TemplateProvider.view( 'auth', 'password_reset_request' ), + controller: 'PasswordResetRequestController', + public: true + }) + .when('/password_reset_submit', { + templateUrl: TemplateProvider.view( 'auth', 'password_reset_submit' ), + controller: 'PasswordResetSubmitController', + public: true + }) + .when( '/settings/users', { + templateUrl: TemplateProvider.view( 'auth', 'users/list' ), + controller: 'UsersListController', + public: false + }) + .when('/account_confirm', { + templateUrl: TemplateProvider.view( 'auth', 'account_confirm' ), + controller: 'ActivationConfirm', + public: true + }); + + }); + + return module; +}); diff --git a/app/modules/auth/providers/AccountProvider.js b/app/modules/auth/providers/AccountProvider.js new file mode 100644 index 0000000..b0c0020 --- /dev/null +++ b/app/modules/auth/providers/AccountProvider.js @@ -0,0 +1,138 @@ +define( [ 'angular', '../module' ], function( ng ) { + 'use strict'; + + ng + .module( 'auth.providers' ) + .provider( 'Account', function() { + /** + * @name accountService + * @type {Object} + * @propertyOf ngSeed.providers:AccountProvider + * @description + * The user service. + */ + var accountService = null; + + /** + * @name accountServiceName + * @type {String} + * @propertyOf ngSeed.providers:AccountProvider + * @description + * The name of the service to $inject. + */ + var accountServiceName = 'AccountService'; + + /** + * @name handlers + * @type {Object} + * @propertyOf ngSeed.providers:AccountProvider + * @description + * The handlers object. + */ + var handlers = { + registrationSuccess: null, + registrationFailure: null + }; + + /** + * @description + * The actual service. + */ + return { + + $get: function( $injector, $rootScope, $log ) { + if( !accountService && accountServiceName ) { + accountService = $injector.get( accountServiceName ); + } + + if ( !handlers.registrationSuccess ) { + $log.log( 'AccountProvider: using default registrationSuccess method' ); + } + + if ( !handlers.registrationFailure ) { + $log.log( 'AccountProvider: using default registrationFailure method' ); + } + + /** + * @ngdoc function + * @name handlers.registrationSuccess + * @propertyOf ngSeed.providers:AccountProvider + * @description + */ + handlers.registrationSuccess = handlers.registrationSuccess || function( user ) { + $rootScope.$broadcast( 'AccountProvider:registrationSuccess', user ); + }; + + /** + * @ngdoc function + * @name handlers.registrationFailure + * @propertyOf ngSeed.providers:AccountProvider + * @description + */ + handlers.registrationFailure = handlers.registrationFailure || function( response ) { + $rootScope.$broadcast( 'AccountProvider:registrationFailure', response ); + }; + + + return { + + /** + * @name register + * @ngdoc function + * @methodOf ngSeed.services:CSSession + * @param {Object} credentials the user credentials + * @return {Promise} the promise your user service returns on registration. + */ + register: function( credentials ) { + return accountService + .register( credentials ) + .then( function( user ) { + handlers.registrationSuccess( user ); + $rootScope.$broadcast( 'AccountProvider:registrationSuccess', user ); + }, + handlers.registrationFailure + ); + } + + }; + + }, + + /** + * @ngdoc function + * @methodOf ngSeed.providers:AccountProvider + * @name setAccountService + * @param {String} serviceName the account service name + */ + setAccountService: function( serviceName ) { + if ( typeof serviceName !== 'string' ) { + throw new Error( 'AccountProvider: setAccountService expects a string for the account service' ); + } + accountServiceName = serviceName; + }, + + /** + * @ngdoc function + * @methodOf ngSeed.providers:AccountProvider + * @name setHandler + * @param {String} key - the handler name + * @param {Function} foo - the handler function + * @description + * Replaces one of the default handlers. + */ + setHandler: function( key, foo ) { + + if ( !handlers.hasOwnProperty( key ) ) { + throw new Error( 'AccountProvider: handle name "' + key + '" is not a valid property.' ); + } + + if ( typeof foo !== 'function' ) { + throw new Error( 'AccountProvider: ' + foo + ' is not a function.' ); + } + + handlers[ key ] = foo; + } + + }; + }); +}); \ No newline at end of file diff --git a/app/modules/auth/providers/AuthHelpersProvider.js b/app/modules/auth/providers/AuthHelpersProvider.js new file mode 100644 index 0000000..93732ed --- /dev/null +++ b/app/modules/auth/providers/AuthHelpersProvider.js @@ -0,0 +1,78 @@ +define( [ 'angular', '../module' ], function( ng ) { + 'use strict'; + + ng + .module( 'auth.providers' ) + .provider( 'AuthHelpers', [ function() { + var helpers = {}; + var inheritedProviders = []; + + return { + $get: function( $injector, UserService ) { + var RoleService = $injector.has( 'RoleService' ) ? $injector.get( 'RoleService' ) : false + , ModalFactory = $injector.has( 'ModalFactory' ) ? $injector.get( 'ModalFactory' ) : false; + + if ( inheritedProviders ) { + inheritedProviders.forEach( function( inheritedProvider ) { + var provider = $injector.get( inheritedProvider ); + if ( !provider ) { + throw new Error( 'Unable to inject "' + inheritedProvider + '"' ); + } + ng.extend( helpers, provider ); + }); + } + + helpers.openUserModal = function( user, currentUser ) { + ModalFactory.open( user, '/modules/auth/views/users/form.html', { + controller: 'UserEditController', + resolve: { + user: function() { + if ( typeof user === 'object' ) { + return user; + } else if ( user !== false && user !== undefined ) { + return UserService + .get( { id: user } ) + .then( function( user ) { + return user; + }); + } else { + return {}; + } + }, + currentUser: function() { + return currentUser; + }, + roles: function() { + if ( !!RoleService ) { + return RoleService + .list() + .then(function( roles ) { + return roles; + }); + } else { + return []; + } + } + } + }); + }; + + return helpers; + }, + + /** + * @ngdoc function + * @methodOf ngSeed.providers:CSAccountProvider + * @name setAccountService + * @param {String} serviceName the account service name + */ + extend: function( providerName ) { + if ( typeof providerName !== 'string' ) { + throw new Error( 'Helpers: extend method expects a string (name of the helpers provider)' ); + } + inheritedProviders.push( providerName ); + } + }; + + }]); +}); diff --git a/app/modules/auth/services/AccountService.js b/app/modules/auth/services/AccountService.js new file mode 100644 index 0000000..3f54bfc --- /dev/null +++ b/app/modules/auth/services/AccountService.js @@ -0,0 +1,23 @@ +define( [ 'angular', '../module' ], function( ng ) { + 'use strict'; + + ng + .module( 'auth.services' ) + .factory( 'AccountService', function( AccountModel ) { + return { + model: AccountModel, + + list: function( findOptions ) { + return AccountModel.list( findOptions ).$promise; + }, + + get: function( findOptions ) { + return AccountModel.get( findOptions ).$promise; + }, + + create: function( data ) { + return AccountModel.create( data ).$promise; + } + }; + }); +}); diff --git a/app/modules/auth/services/SessionService.js b/app/modules/auth/services/SessionService.js new file mode 100644 index 0000000..cddc021 --- /dev/null +++ b/app/modules/auth/services/SessionService.js @@ -0,0 +1,37 @@ +define( [ 'angular', '../module' ], function( ng ) { + 'use strict'; + + ng + .module( 'auth.services' ) + .service( 'SessionService', function( $resource ) { + + return $resource( '/auth/:action', {}, { + signIn: { + method: 'POST', + params: { + action: 'signIn' + } + }, + signOut: { + method: 'POST', + params: { + action: 'signOut' + } + }, + session: { + method: 'GET', + params: { + action: 'session' + } + }, + requestPasswordReset: { + method: 'POST', + params: { + action: 'recover' + }, + url: '/auth/users/:action' + } + }); + + }); +}); diff --git a/app/modules/auth/services/UserService.js b/app/modules/auth/services/UserService.js new file mode 100644 index 0000000..c30f5ed --- /dev/null +++ b/app/modules/auth/services/UserService.js @@ -0,0 +1,24 @@ +define( [ 'angular', '../module' ], function( ng ) { + 'use strict'; + + ng + .module( 'auth.services' ) + .factory( 'UserService', function( UserModel ) { + return { + model: UserModel, + + list: function( findOptions ) { + return UserModel.list( findOptions ).$promise; + }, + + get: function( findOptions ) { + return UserModel.get( findOptions ).$promise; + }, + + create: function( data ) { + return UserModel.create( data ).$promise; + } + }; + }); + +}); diff --git a/app/modules/auth/views/account/form.html b/app/modules/auth/views/account/form.html new file mode 100644 index 0000000..6126832 --- /dev/null +++ b/app/modules/auth/views/account/form.html @@ -0,0 +1,69 @@ +
+ +
+ +
+ +
+ + Company name is required +
+
+ +
+ +
+ +
+
+ +
+ +
+ + Invalid email address +
+
+ +
+ +
+

+
+
+ +
+ +
+

+

Use this email address to forward new prospects into your resume pool without relating them to a specific job posting.

+
+
+ +
+
+ + or Cancel +
+ +
\ No newline at end of file diff --git a/app/modules/auth/views/partials/signOut.html b/app/modules/auth/views/partials/signOut.html new file mode 100644 index 0000000..33e360a --- /dev/null +++ b/app/modules/auth/views/partials/signOut.html @@ -0,0 +1,2 @@ +

Signing Out

+

You will be redirected after sign out...

\ No newline at end of file diff --git a/app/modules/auth/views/password_reset_request.html b/app/modules/auth/views/password_reset_request.html new file mode 100644 index 0000000..3feaa6e --- /dev/null +++ b/app/modules/auth/views/password_reset_request.html @@ -0,0 +1,20 @@ +
+
+
+ +

Password Recovery

+

Please enter your email address used to sign up for your account

+

We will send you confirmation link.

+ +
+
+ + Invalid email address + Please enter your email address +
+ +
+ +
+
+
\ No newline at end of file diff --git a/app/modules/auth/views/password_reset_submit.html b/app/modules/auth/views/password_reset_submit.html new file mode 100644 index 0000000..e9c3ade --- /dev/null +++ b/app/modules/auth/views/password_reset_submit.html @@ -0,0 +1,66 @@ +
+
+
+
+ +
+
+
+
\ No newline at end of file diff --git a/app/modules/auth/views/registration_confirm.html b/app/modules/auth/views/registration_confirm.html new file mode 100644 index 0000000..5fc2d4f --- /dev/null +++ b/app/modules/auth/views/registration_confirm.html @@ -0,0 +1,120 @@ +
+
+
+
+

Start Your 30-Day Free Trial

+
+
+
+
+

Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s.

+
+
+
+
+ +
+ +
+
+
+
+
+ +
+
+
+ +
+
+ +
+
+ +
+
+
+
+
+
+

+ Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. +

+
+
+
+
+
+
+
    +
  • Lorem Ipsum is simply dummy text
  • +
  • Lorem Ipsum is simply dummy text
  • +
  • Lorem Ipsum is simply dummy text
  • +
+
+
+
    +
  • Lorem Ipsum is simply dummy text
  • +
  • Lorem Ipsum is simply dummy text
  • +
  • Lorem Ipsum is simply dummy text
  • +
+
+
+
    +
  • Lorem Ipsum is simply dummy text
  • +
  • Lorem Ipsum is simply dummy text
  • +
  • Lorem Ipsum is simply dummy text
  • +
+
+
+
+
+
+
+ +
+
+ +
+ +
+ +
+
+
+
+
+
+
+
+ Sign Up For A Free Trial +
+
+
+
+

Congratulations!

+

Your account is now active.

+ +
+
+
+
+
SAMPLE VIDEO
+
+
+
+
+
+
+

Sorry, but something's not right.

+

We were unable to authorize your account.

+
+
+
+
+
+
+
+
diff --git a/app/modules/auth/views/signIn.html b/app/modules/auth/views/signIn.html new file mode 100644 index 0000000..7b5bc3d --- /dev/null +++ b/app/modules/auth/views/signIn.html @@ -0,0 +1,38 @@ +
+
+
+
+
+ + + Invalid email + Email is required +
+
+ + + Password is required + Invalid password +
+
+
+
+ +
+ +
+
+
+
+
+
\ No newline at end of file diff --git a/app/modules/auth/views/signUp.html b/app/modules/auth/views/signUp.html new file mode 100644 index 0000000..fe706b1 --- /dev/null +++ b/app/modules/auth/views/signUp.html @@ -0,0 +1,125 @@ +
+
+
+
+

Start Your Free Trial

+

today with our free 30 day trial.

+
+
+
+ +
+
+
+
+ +
+
+
+
+ +
+
+
+
+ +
+
+
+
+ +
+
+
+
+
+ + Password is required + + Password must contain at least 1 uppercase letter, 1 lower case letter and 1 number or special character. + No spaces or double quotes or more than 4 consecutive identical characters are allowed. Minimum length is 8 characters. + +
+
+
+
+
+
+ + Passwords must match +
+
+
+
+
+

By Creating an Account, You Agree to Our Terms Of Use

+
+
+
+
+ +
+
+
+
+

Almost Done!

+

You should be receiving an email shortly with instructions on how to activate your account.

+
+
+
diff --git a/app/modules/auth/views/users/form.html b/app/modules/auth/views/users/form.html new file mode 100644 index 0000000..e4931c5 --- /dev/null +++ b/app/modules/auth/views/users/form.html @@ -0,0 +1,178 @@ + + + + + diff --git a/app/modules/auth/views/users/list.html b/app/modules/auth/views/users/list.html new file mode 100644 index 0000000..82d5e70 --- /dev/null +++ b/app/modules/auth/views/users/list.html @@ -0,0 +1,20 @@ +
+
+

Users
{{welcome}}

+
+
+ +
+ +
+ +
+
diff --git a/app/modules/auth/views/users/show.html b/app/modules/auth/views/users/show.html new file mode 100644 index 0000000..b451669 --- /dev/null +++ b/app/modules/auth/views/users/show.html @@ -0,0 +1,55 @@ +
+ +
+
+ +
+ +
+ +
+ +
+ +
+

INACTIVE

+
+
+ +
+ +
+

+
+
+ +
+ +
+

+
+
+ +
+ +
+

+
+
+ +
+ +
+

+
+
+ + +
+ +
+ +
+
+
+
\ No newline at end of file diff --git a/app/modules/auth/views/users/table_actions.html b/app/modules/auth/views/users/table_actions.html new file mode 100644 index 0000000..44046d0 --- /dev/null +++ b/app/modules/auth/views/users/table_actions.html @@ -0,0 +1,43 @@ +
+ + + + + + +
diff --git a/app/modules/cs_account/main.js b/app/modules/cs_account/main.js deleted file mode 100644 index ca91d0c..0000000 --- a/app/modules/cs_account/main.js +++ /dev/null @@ -1,18 +0,0 @@ -define([ - './module', - - 'cs_common', - - // Controllers - './scripts/cs_account_create_controller', - // 'controllers/cs_account_confirm_controller', - - // Providers - './scripts/cs_account_provider', - './scripts/cs_account_helpers_provider', - - // Services - './scripts/cs_account_service' -], function() { - -}); diff --git a/app/modules/cs_account/module.js b/app/modules/cs_account/module.js deleted file mode 100644 index f8d12cb..0000000 --- a/app/modules/cs_account/module.js +++ /dev/null @@ -1,39 +0,0 @@ -define(['angular'], function (ng) { - 'use strict'; - - ng.module('cs_account.controllers', []); - ng.module('cs_account.providers', []); - ng.module('cs_account.controllers', []); - ng.module('cs_account.services', []); - - var module = ng.module('cs_account', [ - 'cs_common', - 'cs_account.controllers', - 'cs_account.providers', - 'cs_account.controllers', - 'cs_account.services' - ]); - - module.config([ - '$routeProvider', - 'CSTemplateProvider', - 'CSAccountHelpersProvider', - function ($routeProvider, CSTemplate, CSAccountHelpersProvider) { - - CSAccountHelpersProvider.extend('CSCommonHelpers'); - - CSTemplate.setPath('/modules/cs_account/views'); - - $routeProvider - .when('/register', { - templateUrl: CSTemplate.view('registration'), - controller: 'CSAccountCreateController', - public: true - }); - } - - ]); - - return module; - -}); diff --git a/app/modules/cs_account/scripts/cs_account_create_controller.js b/app/modules/cs_account/scripts/cs_account_create_controller.js deleted file mode 100644 index 221004a..0000000 --- a/app/modules/cs_account/scripts/cs_account_create_controller.js +++ /dev/null @@ -1,39 +0,0 @@ -define(['angular', 'underscore', '../module'], function (ng, _) { - 'use strict'; - - ng.module('cs_account.controllers') - .controller('CSAccountCreateController', [ - '$scope', - '$log', - 'CSAccount', - 'CSAccountHelpers', - 'CSSession', - '$location', - function ($scope, $log, CSAccountProvider, CSAccountHelpersProvider, CSSessionProvider, $location) { - $scope.helpers = CSAccountHelpersProvider; - - $scope.register = function () { - if($scope.form && $scope.form.$invalid){ - $log.error('Please fix form errors and try again.'); - return; - } - - var credentials = _($scope.credentials).omit('passwordConfirmation'); - credentials.username = credentials.email.split('@')[0]; - CSAccountProvider.register(credentials); - }; - - $scope.$on('CSAccountProvider:registrationSuccess', function(event, user){ - CSSessionProvider.authenticate(user); - $location.url('/users'); - }); - - $scope.$on('CSAccountProvider:registrationFailure', function (event, data) { - $log.log('CSAccountCreateController:', data); - }); - - } - - ]); - -}); diff --git a/app/modules/cs_account/scripts/cs_account_helpers_provider.js b/app/modules/cs_account/scripts/cs_account_helpers_provider.js deleted file mode 100644 index dff593f..0000000 --- a/app/modules/cs_account/scripts/cs_account_helpers_provider.js +++ /dev/null @@ -1,60 +0,0 @@ -define(['angular', '../module'], function (ng) { - 'use strict'; - - ng.module('cs_account.providers') - .provider('CSAccountHelpers', [ - function () { - - var helpers = {}; - var inheritedProvider; - - /** - * @description - * The actual service. - */ - return { - $get: [ - '$injector', - function ($injector) { - - if(inheritedProvider){ - var provider = $injector.get(inheritedProvider); - if(!provider){ - throw new Error('Unable to inject "' + inheritedProvider + '"'); - } - ng.copy(provider, helpers); - } - - /** - * Define your own helper functions here - * - * helpers.uppercase = function(string){ - * return string.toUpperCase(); - * } - */ - - return helpers; - - } - ], - - /** - * @ngdoc function - * @methodOf ngSeed.providers:CSAccountProvider - * @name setAccountService - * @param {String} serviceName the account service name - */ - extend: function (providerName) { - if(typeof providerName !== 'string') { - throw new Error('CSHelpersProvider: extend method expects a string (name of the helpers provider)'); - } - inheritedProvider = providerName; - } - - }; - - } - - ]); - -}); diff --git a/app/modules/cs_account/scripts/cs_account_provider.js b/app/modules/cs_account/scripts/cs_account_provider.js deleted file mode 100644 index 3e36a7a..0000000 --- a/app/modules/cs_account/scripts/cs_account_provider.js +++ /dev/null @@ -1,147 +0,0 @@ -define(['angular', '../module'], function (ng) { - 'use strict'; - - ng.module('cs_account.providers') - .provider('CSAccount', [ - function () { - - /** - * @name accountService - * @type {Object} - * @propertyOf ngSeed.providers:CSAccountProvider - * @description - * The user service. - */ - var accountService = null; - - /** - * @name accountServiceName - * @type {String} - * @propertyOf ngSeed.providers:CSAccountProvider - * @description - * The name of the service to $inject. - */ - var accountServiceName = 'CSAccountService'; - - /** - * @name handlers - * @type {Object} - * @propertyOf ngSeed.providers:CSAccountProvider - * @description - * The handlers object. - */ - var handlers = { - registrationSuccess: null, - registrationFailure: null - }; - - /** - * @description - * The actual service. - */ - return { - - $get: [ - '$injector', - '$rootScope', - function ($injector, $rootScope) { - - if(!accountService && accountServiceName) { - accountService = $injector.get(accountServiceName); - } - - if (!handlers.registrationSuccess) { - console.log('CSAccountProvider: using default registrationSuccess method'); - } - - if (!handlers.registrationFailure) { - console.log('CSAccountProvider: using default registrationFailure method'); - } - - /** - * @ngdoc function - * @name handlers.registrationSuccess - * @propertyOf ngSeed.providers:CSAccountProvider - * @description - */ - handlers.registrationSuccess = handlers.registrationSuccess || function (user) { - console.log(user); - $rootScope.$broadcast('CSAccountProvider:registrationSuccess', user); - }; - - /** - * @ngdoc function - * @name handlers.registrationFailure - * @propertyOf ngSeed.providers:CSAccountProvider - * @description - */ - handlers.registrationFailure = handlers.registrationFailure || function (response) { - console.log(response); - $rootScope.$broadcast('CSAccountProvider:registrationFailure', response); - }; - - - return { - - /** - * @name register - * @ngdoc function - * @methodOf ngSeed.services:CSSession - * @param {Object} credentials the user credentials - * @return {Promise} the promise your user service returns on registration. - */ - register: function (credentials) { - return accountService.register(credentials) - .then(function(user){ - handlers.registrationSuccess(user); - $rootScope.$broadcast('CSAccountProvider:registrationSuccess', user); - }, handlers.registrationFailure); - } - - }; - - } - ], - - /** - * @ngdoc function - * @methodOf ngSeed.providers:CSAccountProvider - * @name setAccountService - * @param {String} serviceName the account service name - */ - setAccountService: function (serviceName) { - if(typeof serviceName !== 'string') { - throw new Error('CSAccountProvider: setAccountService expects a string for the account service'); - } - accountServiceName = serviceName; - }, - - /** - * @ngdoc function - * @methodOf ngSeed.providers:CSAccountProvider - * @name setHandler - * @param {String} key - the handler name - * @param {Function} foo - the handler function - * @description - * Replaces one of the default handlers. - */ - setHandler: function (key, foo) { - - if (!handlers.hasOwnProperty(key)) { - throw new Error('CSAccountProvider: handle name "' + key + '" is not a valid property.'); - } - - if (typeof foo !== 'function') { - throw new Error('CSAccountProvider: ' + foo + ' is not a function.'); - } - - handlers[key] = foo; - } - - }; - - } - - ]); - -}); diff --git a/app/modules/cs_account/scripts/cs_account_service.js b/app/modules/cs_account/scripts/cs_account_service.js deleted file mode 100644 index 1089ab9..0000000 --- a/app/modules/cs_account/scripts/cs_account_service.js +++ /dev/null @@ -1,30 +0,0 @@ -define(['angular', '../module'], function (ng) { - 'use strict'; - - ng.module('cs_account.services') - .service('CSAccountService', [ - '$http', - function ($http) { - - return { - - list: function () { - return $http.get('/user').then(function (res) { - return res.data; - }); - }, - - register: function (credentials) { - return $http.post('/user', credentials) - .then(function(response){ - return response.data; - }); - } - - }; - - } - - ]); - -}); diff --git a/app/modules/cs_account/tests/e2e/register.test.js b/app/modules/cs_account/tests/e2e/register.test.js deleted file mode 100644 index 8c65899..0000000 --- a/app/modules/cs_account/tests/e2e/register.test.js +++ /dev/null @@ -1,26 +0,0 @@ -// test if the register is working against a running back-end -describe( 'e2e: register', function() { - - var ptor; - beforeEach(function() { - ptor = protractor.getInstance(); - ptor.get( '/register' ); - }); - - it( 'should login with the default user account', function() { - var email = element( by.model( 'credentials.email' ) ); - var password = element( by.model( 'credentials.password' ) ); - var passwordConfirmation = element( by.model( 'credentials.passwordConfirmation' ) ); - - email.sendKeys( 'user1234@email.com' ); - password.sendKeys( '1234' ); - passwordConfirmation.sendKeys( '1234' ); - - ptor.findElement( protractor.By.css( 'button[type="submit"]' ) ).click(); - - expect( ptor.findElement( protractor.By.tagName( 'h1' ) ).getText() ) - .toEqual( 'Users' ); - - }); - -}); diff --git a/app/modules/cs_account/views/registration.html b/app/modules/cs_account/views/registration.html deleted file mode 100644 index fd66ab1..0000000 --- a/app/modules/cs_account/views/registration.html +++ /dev/null @@ -1,45 +0,0 @@ -
- -
- - - Invalid email - Email is required -
- -
- - - Password is required - - Password must contain at least 1 uppercase letter, 1 lower case letter and 1 number or special character. - No spaces or double quotes or more than 4 consecutive identical characters are allowed. Minimum length is 8 characters. - -
- -
- - - Passwords must match -
- -
- -
- -
From 0156c82e10d2f9a214df64e343ef2c368e0ab99a Mon Sep 17 00:00:00 2001 From: Richard Gustin Date: Fri, 29 Aug 2014 16:25:23 +1000 Subject: [PATCH 06/14] feat(common): New common module for mg-seed v1.2 --- .../controllers/CleverTableController.js | 122 +++++++ .../cs_common/controllers/NavbarController.js | 18 + .../directives/CleverTableDirective.js | 15 + .../directives/ColorPickerDirective.js | 31 ++ .../directives/FocusedOnDirective.js | 32 ++ .../cs_common/directives/LoadingDirective.js | 36 ++ .../directives/MustBeEqualToDirective.js | 33 ++ .../cs_common/directives/NavbarDirective.js | 45 +++ .../cs_common/directives/NgBlurDirective.js | 25 ++ .../cs_common/directives/NgFocusDirective.js | 25 ++ .../StringToNumberDirective.js} | 21 +- .../directives/WarnUnsavedChangesDirective.js | 73 ++++ .../StartsWithFilter.js} | 161 ++++----- .../cs_common/filters/TimeAgoFilter.js | 22 ++ app/modules/cs_common/main.js | 38 ++- app/modules/cs_common/module.js | 50 +-- .../cs_common/providers/HelpersProvider.js | 95 ++++++ .../providers/HttpOptionsProvider.js | 163 +++++++++ .../cs_common/providers/NavbarProvider.js | 61 ++++ .../providers/ResourceFactoryProvider.js | 104 ++++++ .../cs_common/providers/TemplateProvider.js | 190 +++++++++++ .../cs_common/scripts/cs_browser_detect.js | 193 ----------- .../scripts/cs_common_helpers_provider.js | 91 ----- .../scripts/cs_http_options_provider.js | 150 -------- .../cs_common/scripts/cs_resource_factory.js | 66 ---- .../cs_common/scripts/cs_template_provider.js | 155 --------- .../cs_common/scripts/must_equal_to.js | 32 -- .../styles/jqueryResizableColumns.css | 19 ++ app/modules/cs_common/styles/navbar.less | 321 ++++++++++++++++++ app/modules/cs_common/views/navbar.html | 134 ++++++++ .../cs_common/views/page_not_found.html | 4 + app/modules/cs_common/views/table.html | 85 +++++ 32 files changed, 1797 insertions(+), 813 deletions(-) create mode 100644 app/modules/cs_common/controllers/CleverTableController.js create mode 100644 app/modules/cs_common/controllers/NavbarController.js create mode 100644 app/modules/cs_common/directives/CleverTableDirective.js create mode 100644 app/modules/cs_common/directives/ColorPickerDirective.js create mode 100644 app/modules/cs_common/directives/FocusedOnDirective.js create mode 100644 app/modules/cs_common/directives/LoadingDirective.js create mode 100644 app/modules/cs_common/directives/MustBeEqualToDirective.js create mode 100644 app/modules/cs_common/directives/NavbarDirective.js create mode 100644 app/modules/cs_common/directives/NgBlurDirective.js create mode 100644 app/modules/cs_common/directives/NgFocusDirective.js rename app/modules/cs_common/{scripts/string_to_number.js => directives/StringToNumberDirective.js} (66%) create mode 100644 app/modules/cs_common/directives/WarnUnsavedChangesDirective.js rename app/modules/cs_common/{scripts/starts_with.js => filters/StartsWithFilter.js} (84%) create mode 100644 app/modules/cs_common/filters/TimeAgoFilter.js create mode 100644 app/modules/cs_common/providers/HelpersProvider.js create mode 100644 app/modules/cs_common/providers/HttpOptionsProvider.js create mode 100644 app/modules/cs_common/providers/NavbarProvider.js create mode 100644 app/modules/cs_common/providers/ResourceFactoryProvider.js create mode 100644 app/modules/cs_common/providers/TemplateProvider.js delete mode 100644 app/modules/cs_common/scripts/cs_browser_detect.js delete mode 100644 app/modules/cs_common/scripts/cs_common_helpers_provider.js delete mode 100644 app/modules/cs_common/scripts/cs_http_options_provider.js delete mode 100644 app/modules/cs_common/scripts/cs_resource_factory.js delete mode 100644 app/modules/cs_common/scripts/cs_template_provider.js delete mode 100644 app/modules/cs_common/scripts/must_equal_to.js create mode 100644 app/modules/cs_common/styles/jqueryResizableColumns.css create mode 100644 app/modules/cs_common/styles/navbar.less create mode 100644 app/modules/cs_common/views/navbar.html create mode 100644 app/modules/cs_common/views/page_not_found.html create mode 100644 app/modules/cs_common/views/table.html diff --git a/app/modules/cs_common/controllers/CleverTableController.js b/app/modules/cs_common/controllers/CleverTableController.js new file mode 100644 index 0000000..d1ebfac --- /dev/null +++ b/app/modules/cs_common/controllers/CleverTableController.js @@ -0,0 +1,122 @@ +define( [ 'angular', 'underscore', 'selectn', '../module' ], function( ng, _, selectn ) { + 'use strict'; + + ng + .module( 'cs_common.controllers' ) + .controller( 'CleverTableController', function( $scope, $element, $attrs, $rootScope, $injector, $log, $filter, ngTableParams ) { + var NgTableParams = ngTableParams; + + $scope.service = $injector.has( $attrs.service ) ? $injector.get( $attrs.service ) : false; + if ( !$scope.service ) { + throw 'You didn\'t provide a service to use with this clever-table'; + } + + $scope.data = []; + $scope.filters = {}; + $scope.messenger = $injector.has( 'Messenger' ) ? $injector.get( 'Messenger' ) : $log; + $scope.columnTitles = ''; + + $scope.columns.forEach( function( column ) { + $scope.columnTitles += column.title ? column.title : column.name; + $scope.columnTitles += ','; + $scope.filters[ column.name ] = ''; + }); + $scope.columnTitles.substr( 0, $scope.columnTitles.length - 1 ); + + $scope.sorting = { id: 'asc' }; + $scope.page = 1; + $scope.count = 10; + + $scope.filterClass = function( column ) { + if ( $scope.tableParams.isSortBy( column, 'asc' ) ) { + return 'sort-asc'; + } else if ( $scope.tableParams.isSortBy( column, 'desc' ) ) { + return 'sort-desc'; + } + return ''; + }; + + $scope.setSorting = function( column ) { + var sort = {}; + sort[ column ] = $scope.tableParams.isSortBy(column, 'asc') ? 'desc' : 'asc'; + $scope.tableParams.sorting( sort ); + }; + + $scope.resetFilters = function() { + $scope.filters = {}; + $scope.tableParams.filter($scope.filters); + }; + + $scope.outputRow = function( row, columnName ) { + return selectn( columnName, row ); + }; + + $scope.selectFilter = function( column ) { + var values = []; + $scope.data.forEach( function( row ) { + values.push( { id: selectn( column.name, row ), title: selectn( column.name, row ) } ); + }); + return values; + }; + + $scope.$on( 'reload', function() { + $scope.tableParams.reload(); + }); + + $scope.filters = {}; + + // Directive tableParams + $scope._params = { + page: $scope.page, + count: $scope.count, + sorting: $scope.sorting, + filter: $scope.filters + }; + + $scope.tableParams = new NgTableParams( + $scope._params, + { + // debugMode: true, + data: $scope.data, + getData: function( $defer, params ) { + + $scope.service + .list( $scope.getDataOptions ? $scope.getDataOptions() : _.extend( { /* _page: params.page(), _pageSize: params.count(), _sorting: params.sorting()*/ } /*, params.filter() */ ) ) + .then( function( data ) { + if ( params.sorting() ) { + data = $filter( 'orderBy' )( data, params.orderBy() ); + } + + if ( Object.keys( params.filter() ).length ) { + var _filters = {}; + + Object.keys( params.filter() ).forEach( function( filterName ) { + if ( params.filter()[ filterName ] !== undefined && params.filter()[ filterName ] !== '' ) { + var fil = selectn( filterName, _filters ); + fil = selectn( filterName, params.filter() ); + } + }); + + if ( Object.keys( _filters ).length ) { + data = _.where( data, _filters ); + } + } + + params.total( data.length ); + + data = data.slice((params.page() - 1) * params.count(), params.page() * params.count()); + $scope.data = data; + + $defer.resolve( data ); + }, function( err ) { + throw err; + }) + .catch( function( err ) { + $defer.reject( err ); + $scope.messenger.error( ' ' + err.message ? err.message : err ); + }); + } + } + ); + }); +}); diff --git a/app/modules/cs_common/controllers/NavbarController.js b/app/modules/cs_common/controllers/NavbarController.js new file mode 100644 index 0000000..cd6359e --- /dev/null +++ b/app/modules/cs_common/controllers/NavbarController.js @@ -0,0 +1,18 @@ +define( [ 'angular', '../module' ], function( ng ) { + 'use strict'; + + ng + .module( 'cs_common.controllers' ) + .controller( 'NavbarController', function( $rootScope, $scope, Session, Helpers, Navbar ) { + $scope.helpers = Helpers; + + $scope.$watch( Session.getCurrentUser, function( user ) { + $scope.currentUser = user || false; + }); + + $scope.$watch( Navbar.getNavbar, function( navbarItems ) { + $scope.navbarItems = navbarItems || false; + }); + } + ); +}); diff --git a/app/modules/cs_common/directives/CleverTableDirective.js b/app/modules/cs_common/directives/CleverTableDirective.js new file mode 100644 index 0000000..4bc85ea --- /dev/null +++ b/app/modules/cs_common/directives/CleverTableDirective.js @@ -0,0 +1,15 @@ +define( [ 'angular', '../module' ], function( ng ) { + 'use strict'; + + ng + .module( 'cs_common.directives' ) + .directive( 'cleverTable', function() { + return { + restrict: 'A', + scope: true, + templateUrl: '/modules/cs_common/views/table.html', + controller: 'CleverTableController' + }; + }); + +}); \ No newline at end of file diff --git a/app/modules/cs_common/directives/ColorPickerDirective.js b/app/modules/cs_common/directives/ColorPickerDirective.js new file mode 100644 index 0000000..6330052 --- /dev/null +++ b/app/modules/cs_common/directives/ColorPickerDirective.js @@ -0,0 +1,31 @@ +define( [ 'angular', 'jqueryMinicolors', '../module' ], function( ng ){ + 'use strict'; + + ng.module( 'cs_common.directives' ) + .directive( 'colorPicker', + [ + function(){ + return { + require: '?ngModel', + link: function( $scope, $element, $attrs, ngModelCtrl ) { + + ngModelCtrl.$render = function() { + $element.minicolors( 'value', ngModelCtrl.$modelValue ); + }; + + $element.minicolors({ + theme: 'bootstrap', + letterCase: 'uppercase', + position: 'top right', + change: function( hex ) { + ngModelCtrl.$setViewValue( hex ); + !$scope.$$phase && $scope.$apply(); + } + }); + } + }; + } + ] + ); + +}); diff --git a/app/modules/cs_common/directives/FocusedOnDirective.js b/app/modules/cs_common/directives/FocusedOnDirective.js new file mode 100644 index 0000000..ed71c31 --- /dev/null +++ b/app/modules/cs_common/directives/FocusedOnDirective.js @@ -0,0 +1,32 @@ +define( [ 'angular', 'underscore', '../module' ], function( ng, _ ) { + 'use strict'; + + ng.module( 'cs_common.directives' ) + .directive( 'focusedOn', [ + '$timeout' + ,function( $timeout ) { + return function( $scope, $element, $attrs ) { + function focus() { + $timeout( function() { + $element.focus(); + }, 20); + } + + if( _( $attrs.focusedOn ).isEmpty() ) { + return focus(); + } + + $scope.$watch( $attrs.focusedOn, function( newVal ) { + if( newVal ) { + focus(); + } + }); + + }; + + } + ] + + ); + +}); diff --git a/app/modules/cs_common/directives/LoadingDirective.js b/app/modules/cs_common/directives/LoadingDirective.js new file mode 100644 index 0000000..4c7141b --- /dev/null +++ b/app/modules/cs_common/directives/LoadingDirective.js @@ -0,0 +1,36 @@ +define( [ 'angular', '../module' ], function( ng ) { + 'use strict'; + + ng + .module( 'cs_common.directives' ) + .directive( 'loading', + [ + '$timeout', + function( $timeout ) { + + return { + restrict: 'C', + link: function( $scope, $element ) { + var promise; + + $scope.$on( '$routeChangeStart', function() { + promise = $timeout( function() { + $element.show(); + }, 400); + }); + + function removeLoader() { + $timeout.cancel( promise ); + $element.hide(); + } + + $scope.$on('SessionProvider:signInRequired', removeLoader); + $scope.$on('$routeChangeSuccess', removeLoader); + } + }; + } + ] + + ); + +}); diff --git a/app/modules/cs_common/directives/MustBeEqualToDirective.js b/app/modules/cs_common/directives/MustBeEqualToDirective.js new file mode 100644 index 0000000..d2a486b --- /dev/null +++ b/app/modules/cs_common/directives/MustBeEqualToDirective.js @@ -0,0 +1,33 @@ +define( [ 'angular', '../module' ], function( ng ) { + 'use strict'; + + ng + .module( 'cs_common.directives' ) + .directive( 'mustBeEqualTo', function() { + var link = function( $scope, $element, $attrs, ctrl ) { + + function validate( value ) { + ctrl.$setValidity( 'mustBeEqualTo', value === $scope.$eval( $attrs.mustBeEqualTo ) ); + return value; + } + + $scope.$watch( $attrs.mustBeEqualTo, function( equalTo ) { + if ( ng.isUndefined( equalTo ) ) { + return; + } + $scope.equalTo = equalTo; + return validate( $element.val() ); + }); + + ctrl.$parsers.unshift( validate ); + + }; + + return { + require: 'ngModel', + link: link + }; + + }); + +}); diff --git a/app/modules/cs_common/directives/NavbarDirective.js b/app/modules/cs_common/directives/NavbarDirective.js new file mode 100644 index 0000000..613de18 --- /dev/null +++ b/app/modules/cs_common/directives/NavbarDirective.js @@ -0,0 +1,45 @@ +define( [ 'angular', '../module' ], function( ng ) { + 'use strict'; + + /** + * @ngdoc directive + * @name ngSeed.directives:navbar + * @description + * Sets the current navbar option in focus. + */ + ng + .module( 'cs_common.directives' ) + .directive( 'navbar', function() { + return { + restrict: 'E', + replace: true, + transclude: true, + templateUrl: 'modules/cs_common/views/navbar.html', + scope: { + heading: '@' + }, + controller: 'NavbarController', + link: function( $scope ) { + //show if the item requires signIn and the user is logged in + $scope.showItem = function( item ) { + if ( item.display && typeof item.display === 'function' ) { + return item.display( $scope ); + } else { + return ng.isDefined( $scope.currentUser ) && ng.isDefined( $scope.currentUser.id ) === item.requiresSignIn; + } + }; + + $scope.classHandler = function( item ) { + var css = ''; + if ( item.class ) { + css = 'fa fa-fw ' + item.class; + } else if ( item.glyph ) { + css = 'glyphicon glyphicon-' + item.glyph; + } + return css; + }; + + } + }; + }); +}); diff --git a/app/modules/cs_common/directives/NgBlurDirective.js b/app/modules/cs_common/directives/NgBlurDirective.js new file mode 100644 index 0000000..370a3ca --- /dev/null +++ b/app/modules/cs_common/directives/NgBlurDirective.js @@ -0,0 +1,25 @@ +define( [ 'angular', '../module' ], function( ng ) { + 'use strict'; + + /** + * Angular v1.1.5 still doesn't support ngBlur so we build our own + */ + ng + .module( 'cs_common.directives' ) + .directive( 'ngBlur', [ + '$parse', + function( $parse ) { + return function( scope, element, attr ) { + var fn = $parse( attr.ngBlur ); + element.bind( 'blur', function( event ) { + scope.$apply( function() { + fn( scope, { $event: event} ); + }); + }); + }; + } + + ] + ); + +}); diff --git a/app/modules/cs_common/directives/NgFocusDirective.js b/app/modules/cs_common/directives/NgFocusDirective.js new file mode 100644 index 0000000..4d65969 --- /dev/null +++ b/app/modules/cs_common/directives/NgFocusDirective.js @@ -0,0 +1,25 @@ +define( [ 'angular', '../module' ], function( ng ) { + 'use strict'; + + /** + * Angular v1.1.5 still doesn't support ngFocus so we build our own + */ + ng + .module( 'cs_common.directives' ) + .directive( 'ngFocus', [ + '$parse', + function( $parse ) { + return function( scope, element, attr ) { + var fn = $parse( attr.ngFocus ); + element.bind( 'focus', function( event ) { + scope.$apply( function() { + fn( scope, { $event: event } ); + }); + }); + }; + } + + ] + ); + +}); diff --git a/app/modules/cs_common/scripts/string_to_number.js b/app/modules/cs_common/directives/StringToNumberDirective.js similarity index 66% rename from app/modules/cs_common/scripts/string_to_number.js rename to app/modules/cs_common/directives/StringToNumberDirective.js index bdd803e..0274cdd 100644 --- a/app/modules/cs_common/scripts/string_to_number.js +++ b/app/modules/cs_common/directives/StringToNumberDirective.js @@ -1,4 +1,4 @@ -define(['angular', '../module'], function (ng) { +define( [ 'angular', '../module' ], function( ng ) { 'use strict'; /** @@ -8,13 +8,14 @@ define(['angular', '../module'], function (ng) { * Converts a string to a number. Useful in type="number" input * elements that bind to a stringified number model. */ - ng.module('cs_common.directives') - .directive('string2number', function() { + ng + .module( 'cs_common.directives' ) + .directive( 'stringToNumber', function() { return { restrict: 'A', require: 'ngModel', - link: function(scope, element, attr, ngModel) { + link: function( scope, element, attr, ngModel ) { /** * @ngdoc method @@ -23,8 +24,8 @@ define(['angular', '../module'], function (ng) { * @param {Number, String} number Just the number that has been input. * @return {Number} The number. */ - function fromField(number) { - return Number(number); + function fromField( number ) { + return Number( number ); } /** @@ -34,12 +35,12 @@ define(['angular', '../module'], function (ng) { * @param {Number, String} number Just the number that has been input. * @return {Number} The number or 0. */ - function toField(text) { - return Number(text || 0); + function toField( text ) { + return Number( text || 0 ); } - ngModel.$parsers.push(fromField); - ngModel.$formatters.push(toField); + ngModel.$parsers.push( fromField ); + ngModel.$formatters.push( toField ); } }; diff --git a/app/modules/cs_common/directives/WarnUnsavedChangesDirective.js b/app/modules/cs_common/directives/WarnUnsavedChangesDirective.js new file mode 100644 index 0000000..49c1353 --- /dev/null +++ b/app/modules/cs_common/directives/WarnUnsavedChangesDirective.js @@ -0,0 +1,73 @@ +define( [ 'angular', 'underscore', '../module' ], function( angular, _ ) { + 'use strict'; + + angular + .module( 'cs_common.directives' ) + .directive( 'warnUnsavedChanges', [ + '$window', + function( $window ) { + return function( $scope, $element, $attrs ) { + var master, cancelWatch; + + function applyWatch( model ) { + master = angular.copy( model ); + attachDirtyFormWarningHandler( $scope ); + cancelWatch(); + } + + cancelWatch = $scope.$watch( $attrs.warnUnsavedChanges, function( model ) { + if ( model && _( model ).has( '$resolved' ) ) { + if ( model.$resolved === true ) { + applyWatch( model ); + } + } else { + applyWatch( model ); + } + }, true); + + function attachDirtyFormWarningHandler( $scope ){ + + function dirtyFormWarningHandler( event ){ + if ( isDirty() ){ + if ( !$window.confirm( 'There are unsaved changes on this form. Are you sure you want to leave this page?' ) ) { + event.preventDefault(); + } + } + } + + function isDirty() { + return $scope.form.$dirty && !angular.equals( master, $scope[ $attrs.warnUnsavedChanges ] ); + } + + $scope.$on( '$locationChangeStart', dirtyFormWarningHandler ); + + // function showWarning() { + // return 'There\'s some unsaved changes.'; + // } + + // var attached; + // $scope.$watch(isDirty, function(dirty) { + // if(dirty && !attached) { + // angular.element($window).on('beforeunload', showWarning); + // attached = true; + // } else { + // angular.element($window).off('beforeunload', showWarning); + // attached = false; + // } + // }); + + $scope.$watch( 'processing', function( processing, previousValue ) { + if( processing === false && previousValue === true ) { + master = angular.copy( $scope[ $attrs.warnUnsavedChanges ] ); + } + }); + + } + + }; + } + ] + + ); + +}); \ No newline at end of file diff --git a/app/modules/cs_common/scripts/starts_with.js b/app/modules/cs_common/filters/StartsWithFilter.js similarity index 84% rename from app/modules/cs_common/scripts/starts_with.js rename to app/modules/cs_common/filters/StartsWithFilter.js index 33e3d47..539d86c 100644 --- a/app/modules/cs_common/scripts/starts_with.js +++ b/app/modules/cs_common/filters/StartsWithFilter.js @@ -1,80 +1,81 @@ -define(['angular', '../module'], function (ng) { - 'use strict'; - - /** - * @ngdoc filter - * @name ngSeed.filters:startsWith - * @description - * - * Filters a collection with a simple regex. - * - * @example - - - -
-
- - - - - - - - -
Name
{{friend.name}}
-
-
- - it('should be reverse ordered by aged', function() { - expect(binding('predicate')).toBe('-age'); - expect(repeater('table.friend', 'friend in friends').column('friend.age')). - toEqual(['35', '29', '21', '19', '10']); - expect(repeater('table.friend', 'friend in friends').column('friend.name')). - toEqual(['Adam', 'Julie', 'Mike', 'Mary', 'John']); - }); - - it('should reorder the table when user selects different predicate', function() { - element('.doc-example-live a:contains("Name")').click(); - expect(repeater('table.friend', 'friend in friends').column('friend.name')). - toEqual(['Adam', 'John', 'Julie', 'Mary', 'Mike']); - expect(repeater('table.friend', 'friend in friends').column('friend.age')). - toEqual(['35', '10', '29', '19', '21']); - - element('.doc-example-live a:contains("Phone")').click(); - expect(repeater('table.friend', 'friend in friends').column('friend.phone')). - toEqual(['555-9876', '555-8765', '555-5678', '555-4321', '555-1212']); - expect(repeater('table.friend', 'friend in friends').column('friend.name')). - toEqual(['Mary', 'Julie', 'Adam', 'Mike', 'John']); - }); - -
- */ - ng.module('cs_common.filters') - .filter('startsWith', function() { - - return function(str, letter, prop){ - letter = letter || undefined; - if(!letter){ - return str; - } - var filtered = []; - str.forEach(function (i) { - if((new RegExp('^['+letter.toLowerCase()+letter.toUpperCase()+']')).test(i[prop])) { - filtered.push(i); - } - }); - return filtered; - }; - }); - -}); +define( [ 'angular', '../module' ], function( ng ) { + 'use strict'; + + /** + * @ngdoc filter + * @name ngSeed.filters:startsWith + * @description + * + * Filters a collection with a simple regex. + * + * @example + + + +
+
+ + + + + + + + +
Name
{{friend.name}}
+
+
+ + it('should be reverse ordered by aged', function() { + expect(binding('predicate')).toBe('-age'); + expect(repeater('table.friend', 'friend in friends').column('friend.age')). + toEqual(['35', '29', '21', '19', '10']); + expect(repeater('table.friend', 'friend in friends').column('friend.name')). + toEqual(['Adam', 'Julie', 'Mike', 'Mary', 'John']); + }); + + it('should reorder the table when user selects different predicate', function() { + element('.doc-example-live a:contains("Name")').click(); + expect(repeater('table.friend', 'friend in friends').column('friend.name')). + toEqual(['Adam', 'John', 'Julie', 'Mary', 'Mike']); + expect(repeater('table.friend', 'friend in friends').column('friend.age')). + toEqual(['35', '10', '29', '19', '21']); + + element('.doc-example-live a:contains("Phone")').click(); + expect(repeater('table.friend', 'friend in friends').column('friend.phone')). + toEqual(['555-9876', '555-8765', '555-5678', '555-4321', '555-1212']); + expect(repeater('table.friend', 'friend in friends').column('friend.name')). + toEqual(['Mary', 'Julie', 'Adam', 'Mike', 'John']); + }); + +
+ */ + ng + .module( 'cs_common.filters' ) + .filter( 'startsWith', function() { + + return function( str, letter, prop ) { + letter = letter || undefined; + if ( !letter ) { + return str; + } + var filtered = []; + str.forEach( function( i ) { + if ( ( new RegExp( '^[' + letter.toLowerCase() + letter.toUpperCase() + ']' ) ).test( i[ prop ] ) ) { + filtered.push( i ); + } + }); + return filtered; + }; + }); + +}); diff --git a/app/modules/cs_common/filters/TimeAgoFilter.js b/app/modules/cs_common/filters/TimeAgoFilter.js new file mode 100644 index 0000000..6a7456b --- /dev/null +++ b/app/modules/cs_common/filters/TimeAgoFilter.js @@ -0,0 +1,22 @@ +define( [ 'angular', 'moment', '../module' ], function( ng, moment ) { + 'use strict'; + + ng + .module( 'cs_common.filters' ) + .filter( 'timeAgo', [ + '$log', + function( $log ) { + return function( date ) { + var dt = moment( date ); + if( !dt.isValid() ){ + $log.warn( 'timeAgoFilter: Invalid date input' ); + return date; + } + + return dt.fromNow(); + + }; + } + ]); + +}); diff --git a/app/modules/cs_common/main.js b/app/modules/cs_common/main.js index 0364762..d3f8a8a 100644 --- a/app/modules/cs_common/main.js +++ b/app/modules/cs_common/main.js @@ -4,24 +4,38 @@ define([ 'ngResource', 'ngRoute', 'ngSanitize', + 'ngTable', + 'ngTableResizableColumns', + 'selectn', + 'jqueryMinicolors', './module', // Providers - './scripts/cs_common_helpers_provider', - './scripts/cs_template_provider', - './scripts/cs_http_options_provider', - - 'http-auth-interceptor', + './providers/HelpersProvider', + './providers/TemplateProvider', + './providers/HttpOptionsProvider', + './providers/NavbarProvider', + './providers/ResourceFactoryProvider', // Directives - './scripts/string_to_number', - './scripts/must_equal_to', + './directives/CleverTableDirective', + './directives/ColorPickerDirective', + './directives/FocusedOnDirective', + './directives/LoadingDirective', + './directives/MustBeEqualToDirective', + './directives/NavbarDirective', + './directives/NgBlurDirective', + './directives/NgFocusDirective', + './directives/StringToNumberDirective', + './directives/WarnUnsavedChangesDirective', + + // Controllers + './controllers/CleverTableController', + './controllers/NavbarController', // Filters - './scripts/starts_with', + './filters/StartsWithFilter', + './filters/TimeAgoFilter' - // Services - './scripts/cs_browser_detect', - './scripts/cs_resource_factory' -], function(){}); +], function() {}); diff --git a/app/modules/cs_common/module.js b/app/modules/cs_common/module.js index 2cc2056..83a47ec 100644 --- a/app/modules/cs_common/module.js +++ b/app/modules/cs_common/module.js @@ -1,17 +1,21 @@ -define(['angular'], function (ng) { +define( [ 'angular' ], function( ng ) { 'use strict'; - ng.module('cs_common.providers', []); - ng.module('cs_common.controllers', []); - ng.module('cs_common.services', []); - ng.module('cs_common.directives', []); - ng.module('cs_common.filters', []); + ng.module( 'cs_common.providers', [] ); + ng.module( 'cs_common.controllers', [] ); + ng.module( 'cs_common.services', [] ); + ng.module( 'cs_common.directives', [] ); + ng.module( 'cs_common.filters', [] ); - var module = ng.module('cs_common', [ + var module = ng.module( 'cs_common', [ 'ngCookies', 'ngResource', 'ngRoute', 'ngSanitize', + 'ngTable', + 'ngTableResizableColumns', + 'ui.bootstrap', + 'ui', 'http-auth-interceptor', 'cs_common.providers', 'cs_common.controllers', @@ -20,26 +24,24 @@ define(['angular'], function (ng) { 'cs_common.filters' ]); - module.config([ - '$routeProvider', - 'CSTemplateProvider', - function ($routeProvider, CSTemplate) { + module.config( function( $routeProvider, TemplateProvider ) { - CSTemplate.setPath('/modules/cs_common/views'); + TemplateProvider.setPath( '/modules/:moduleName/views' ); - $routeProvider - .when('/error', { - templateUrl: CSTemplate.view('error'), - public: true - }) - .otherwise({ - redirectTo: '/' - }); + $routeProvider + .when( '/error/page_not_found', { + templateUrl: TemplateProvider.view( 'cs_common', 'page_not_found' ), + public: true + }) + .when( '/error', { + templateUrl: TemplateProvider.view( 'cs_common', 'error' ), + public: true + }) + .otherwise({ + redirectTo: '/error/page_not_found' + }); - } - - ]); + }); return module; - }); diff --git a/app/modules/cs_common/providers/HelpersProvider.js b/app/modules/cs_common/providers/HelpersProvider.js new file mode 100644 index 0000000..aec1e81 --- /dev/null +++ b/app/modules/cs_common/providers/HelpersProvider.js @@ -0,0 +1,95 @@ +define( [ 'angular', '../module' ], function( ng ) { + 'use strict'; + + ng + .module( 'cs_common.providers' ) + .provider( 'Helpers', [ + function Helpers() { + + var helpers = {}; + var inheritedProviders = []; + + /** + * @description + * The actual service. + */ + return { + $get: [ '$injector', '$location', '$log', + function( $injector, $location, $log ) { + + if ( inheritedProviders ) { + inheritedProviders.forEach( function( inheritedProvider ) { + var provider = $injector.get( inheritedProvider ); + if ( !provider ) { + throw new Error( 'Unable to inject "' + inheritedProvider + '"' ); + } + ng.extend( helpers, provider ); + }); + } + + if ( $injector.has( 'ModalFactory' ) ) { + helpers.modal = $injector.get( 'ModalFactory' ).open; + } + + helpers.hasError = function( $scope, fieldName, validation, validateOnSubmit ) { + if ( !$scope.form ) { + throw new Error( 'Expected $scope.form' ); + } + var field = $scope.form[ fieldName ]; + if ( !field ) { + throw new Error('Expected $scope.form to have a field \'' + fieldName + '\''); + } + + if ( validateOnSubmit && !$scope.submitted ) { + return false; + } + + if ( validation ) { + return ( field.$dirty && field.$error[ validation ] ) || ( $scope.submitted && field.$error[ validation ] ); + } + return !!( ( field.$dirty && field.$invalid ) || ( $scope.submitted && field.$invalid ) ); + }; + + helpers.isActive = function( path ) { + return ( new RegExp( path, 'gi' ) ).test( $location.path() ); + }; + + helpers.redirect = function( path, $event ) { + if ( $event ) { + $event.stopPropagation(); + $event.preventDefault(); + } + + if ( !path ) { + return $log.error( 'Path not provided' ); + } + + $location.url( path ); + return false; + }; + + return helpers; + + } + ], + + /** + * @ngdoc function + * @methodOf ngSeed.providers:Helpers + * @name setAccountService + * @param {String} serviceName the account service name + */ + extend: function( providerName ) { + if ( typeof providerName !== 'string' ) { + throw new Error( 'Helpers: extend method expects a string (name of the helpers provider)' ); + } + inheritedProviders.push( providerName ); + } + + }; + + } + + ]); + +}); diff --git a/app/modules/cs_common/providers/HttpOptionsProvider.js b/app/modules/cs_common/providers/HttpOptionsProvider.js new file mode 100644 index 0000000..f8ccbee --- /dev/null +++ b/app/modules/cs_common/providers/HttpOptionsProvider.js @@ -0,0 +1,163 @@ +define( [ 'angular', '../module' ], function( ng ) { + 'use strict'; + + /** + * @ngdoc service + * @name ngSeed.services:HttpOptions + * @description + * This options can be plugged in the $httpProvider or + * the $http object to set up application wide configuration + * for HTTP requests. + * + * ### Examples + * ```js + * myApp.controller('Test', ['$scope', 'HttpOptions', function ($scope, HttpOptions) { + * if( /localhost/.test(HttpOptions.domain) ) { + * // you are working with your local server + * } + * }]); + * ``` + */ + + /** + * @ngdoc service + * @name ngSeed.providers:HttpOptionsProvider + * @description + * The provider to configure the domain, port, headers and + * interceptors for all the requests made with $http. + * + * ### Examples + * ```js + * myApp.config(['HttpOptionsProvider', function (HttpOptionsProvider) { + * HttpOptionsProvider.setDomain('http://www.google.com'); + * HttpOptionsProvider.setWithCredentials(false); + * }]); + * ``` + */ + + ng + .module( 'cs_common.providers' ) + .provider('HttpOptions', function( $httpProvider, $injector ) { + /** + * @ngdoc property + * @name withCredentials + * @propertyOf ngSeed.providers:HttpOptionsProvider + * @description + * Should it send session information with every request? + */ + var withCredentials = true; + + /** + * @ngdoc property + * @name domain + * @propertyOf ngSeed.providers:HttpOptionsProvider + * @description + * For testing purposes this is extremely useful + * it allows you to test the app against a aws server + * running either in dev, staging or production + */ + var domain = 'http://localhost:8080'; + + /** + * If the clever-messenger module is installed use it to send messages to the user + * @type {Function|Boolean} + */ + var messenger = $injector.has( 'Messenger' ) ? $injector.get( 'Messenger' ) : false; + + // Update the $httpProvider with the default settings + $httpProvider.defaults.withCredentials = withCredentials; + + /** + * @ngdoc method + * @name interceptors.addDomain + * @methodOf ngSeed.providers:HttpOptionsProvider + * @description + * Interceptor. + * + * Add the domain to all the HTTP requests that are not + * templates. And doesn't add it to absolute urls. + * + * Note: Hookup the $templates service to get the proper + * regex. This works for now as is what we are using. + */ + $httpProvider.interceptors.push( function( $rootScope, $timeout ) { + return { + request: function( config ) { + if( ! /(.*).html$/.test( config.url ) && ! /^(http|https:\/\/)/i.test( config.url ) ) { + config.url = domain + config.url; + } + return config; + }, + responseError: function( res ) { + if ( res.status === 401 && !/auth\/session/.test( res.config.url) ) { + $rootScope.$broadcast( 'SessionProvider:signInRequired' ); + if ( !!messenger ) { + $timeout( function() { + messenger.warning( 'You are required to login to view that page.' ); + }); + } + } + throw res.data; + } + }; + }); + + /* + * @name Unauthorized + * @methodOf ngSeed.services:HttpOptions + * @param {Object} $q the promise service + * @return {Object} An object with the handlers. + * @description + * Add the domain to all the HTTP requests that are not + * templates. + * + * Note: Hookup the $templates service to get the proper + * regex. This works for now as is what we are using. + */ + // $httpProvider.interceptors.push(['$rootScope', '$q',function ($rootScope, $q) { + // return { + // response: function (res) { + // if(res.status === 401) { + // $rootScope.$broadcast('$auth:signInRequired'); + // } + // return res.data; + // } + // } + // }]); + + return { + $get: function () { + return { + domain: domain, + withCredentials: withCredentials + }; + }, + + /** + * @ngdoc method + * @name setDomain + * @methodOf ngSeed.providers:HttpOptionsProvider + * @param {String} uri The domain. + */ + setDomain: function( uri ) { + if ( typeof uri !== 'string' ) { + throw new Error( 'HttpOptions: expecting string for domain' ); + } + domain = uri; + }, + + /** + * @ngdoc method + * @name setWithCredentials + * @methodOf ngSeed.providers:HttpOptionsProvider + * @param {Boolean} value Should it send credentials? + */ + setWithCredentials: function( value ) { + if ( typeof value !== 'boolean' ) { + throw new Error( 'HttpOptions: expecting value to be boolean' ); + } + withCredentials = value; + } + }; + }); +}); diff --git a/app/modules/cs_common/providers/NavbarProvider.js b/app/modules/cs_common/providers/NavbarProvider.js new file mode 100644 index 0000000..c701f69 --- /dev/null +++ b/app/modules/cs_common/providers/NavbarProvider.js @@ -0,0 +1,61 @@ +define( [ 'angular', 'underscore', '../module' ], function( ng, _ ) { + 'use strict'; + + ng + .module( 'cs_common.providers' ) + .provider( 'Navbar', [ + function () { + + var Navbar = { + left: [], + right: [], + app: [] + }; + + /** + * @description + * The actual service. + */ + return { + $get: [ + '$injector', + function() { + return { + getNavbar: function() { + return Navbar; + } + }; + } + ], + + /** + * @ngdoc function + * @methodOf cleverStackNgSeed.providers:NavbarProvider + * @name setTemplate + * @param {String} theTemplate the template you want to use + */ + setNavbar: function( navbar ) { + Navbar = navbar; + }, + + getNavbar: function() { + return Navbar; + }, + + extend: function( navbarItems ) { + if ( !ng.isObject( navbarItems ) ) { + throw new Error( 'NavbarProvider: extend method expects an object (your menu structure for your navbar)' ); + } + + Object.keys( navbarItems ).forEach( function( position ) { + Navbar[ position ] = _.union( ( Navbar[ position ] || [] ), navbarItems[ position ] ); + }); + } + + }; + + } + + ]); + +}); diff --git a/app/modules/cs_common/providers/ResourceFactoryProvider.js b/app/modules/cs_common/providers/ResourceFactoryProvider.js new file mode 100644 index 0000000..1cb7e53 --- /dev/null +++ b/app/modules/cs_common/providers/ResourceFactoryProvider.js @@ -0,0 +1,104 @@ +define( [ 'angular', 'underscore', 'inflection', '../module' ], function( ng, _, inflection ) { + 'use strict'; + + ng + .module( 'cs_common.providers' ) + .provider( 'ResourceFactory', [ + function () { + + /** + * @ngdoc service + * @name ngSeed.services:$resourceFactory + * @description + * A service used to generate and register resources. + */ + + /** + * @ngdoc function + * @name ngSeed.services:ResourceFactory + * @methodOf ngSeed.services:$resourceFactory + * @param {String} resourceName The name of the resource. + * @description + * Generates a factory function that uses ngResource + * to talk to a REST resource that can be directly used + * with factory. + * + * Example of use with factory: + * ```js + * services + * .factory('ResourceService', ResourceFactory('resource')); + * ``` + */ + function ResourceFactory( resourceUrl, defaultParams, actions ) { + + /** + * @ngdoc function + * @name builder + * @methodOf ngSeed.services:$resourceFactory + * @description + * + * Generates an HTTP Resource. + * + * @param {$resource} $resource ngResource + * @param {$httpOptions} $httpOptions The HTTP Options object. + * + * @return {resource} The actual resource for resourceName. + */ + var builder = function ( $resource, HttpOptions ) { + var singularUrl = HttpOptions.domain + inflection.singularize( resourceUrl ) + '/:id/:action' + , pluralUrl = HttpOptions.domain + inflection.pluralize( resourceUrl ) + '/:action'; + + actions = _.extend( + { + list: { + method: 'GET', + url: pluralUrl, + isArray: true + }, + get: { + method: 'GET' + }, + save: { + method: 'PUT' + }, + create: { + method: 'POST' + }, + destroy: { + method: 'DELETE' + } + }, + actions + ); + + return $resource( singularUrl, defaultParams, actions ); + }; + + return [ '$resource', 'HttpOptions', builder ]; + } + + /** + * @description + * The actual service. + */ + return { + $get: [ + '$injector', + function( $injector ) { + return function( resourceUrl, defaultParams, actions ) { + + // ng + // .module( 'app.resources' ) + // .factory( resourceName, ResourceFactory( resourceName ) ); + + return $injector.instantiate( new ResourceFactory( resourceUrl, defaultParams, actions ) ); + }; + } + ] + }; + + } + + ]); + +}); diff --git a/app/modules/cs_common/providers/TemplateProvider.js b/app/modules/cs_common/providers/TemplateProvider.js new file mode 100644 index 0000000..2df8fc6 --- /dev/null +++ b/app/modules/cs_common/providers/TemplateProvider.js @@ -0,0 +1,190 @@ +define( [ 'angular', '../module' ], function( ng ) { + 'use strict'; + + /** + * @ngdoc service + * @name ngSeed.services:Template + * @description + * This service facilitates the loading of templates (not yet extended to + * ng-include) thru two functions. + * + * It can be configured using the TemplateProvider. + * + * ### Examples + * ```js + * Template.view('home'); + * // will look for /views/home.html + * + * Template.partial('navbar'); + * // will look for /views/partials/navbar.html + * ``` + * This path can be changed using `TemplateProvider.setPath`. + * The extension can be changed using `TemplateProvider.setExtension`. + */ + + ng + .module( 'cs_common.providers' ) + .provider( 'Template', [ + function() { + var basePath = ''; + try { + basePath = document.getElementsByTagName( 'base' )[ 0 ].href; + } catch( e ) { + console && console.warn && console.warn( 'Unable to read href attr from base tag' ); + } + + if( basePath.length ) { + if( basePath[ basePath.length - 1 ] === '/' ) { + basePath = basePath.slice( 0, -1 ); + } + } + + /** + * @name viewsPath + * @type {String} + * @propertyOf ngSeed.providers:TemplateProvider + * @description + * The path to the views. + */ + var viewsPath = basePath + '/:moduleName/views/'; + + /** + * @name partialsPath + * @type {String} + * @propertyOf ngSeed.providers:$templatesProvider + * @description + * The path to the partials. + */ + var partialsPath = viewsPath + '/:section/partials/'; + + /** + * @name extension + * @type {String} + * @propertyOf ngSeed.providers:TemplateProvider + * @description + * The extension of the partials. + */ + var extension = '.html'; + + /** + * @ngdoc method + * @methodOf ngSeed.services:CSTemplate + * @name view + * @param {String} viewName The name of the view. + * @return {String} The path to the view. + * @description + * Utility functions to get the path of a view. + * + * This method is also available from TemplateProvider + */ + function view( moduleName, viewName ) { + var templatePath; + + if ( arguments.length === 1 ) { + viewName = moduleName; + moduleName = 'app'; + } + + templatePath = viewsPath.replace( ':moduleName', moduleName ) + viewName + extension; + // console && console.log && console.log( 'Template.view( ' + templatePath + ' )' ); + + return templatePath; + } + + /** + * @ngdoc method + * @methodOf ngSeed.services:$templates + * @name partial + * @param {String} section The folder path to look into. + * @param {String} partialName The name of the partial. + * @return {String} The path to the partial. + * @description + * Utility functions to get the path of a partial. + * + * This method is also available from $templatesProvider + */ + function partial( moduleName, section, partialName ) { + var partialPath; + + if ( arguments.length !== 3 ) { + partialName = section; + section = moduleName + '/'; + moduleName = 'app'; + + } else if ( arguments.length === 1 ) { + partialName = moduleName; + moduleName = 'app'; + section = ''; + } + + partialPath = partialsPath.replace( ':moduleName', moduleName ).replace( ':section/', section ) + partialName + extension; + // console && console.log && console.log( 'Template.partial( ' + partialPath + ' )' ); + + return partialPath; + } + + + /** + * @description + * The actual service. + */ + return { + + $get: function() { + return { + view: view, + partial: partial + }; + }, + + view: view, + partial: partial, + + /** + * @ngdoc method + * @methodOf ngSeed.providers:TemplateProvider + * @name setPath + * @param {String} path the path + * @description + * Changes the path of the views folder. + */ + setPath: function( path ) { + if ( typeof path !== 'string') { + throw new Error( 'Template: expecting a string for path.' ); + } + + if ( path[ path.length - 1 ] !== '/' ) { + path += '/'; + } + + viewsPath = basePath + path; + partialsPath = viewsPath + '/:section/partials/'; + }, + + /** + * @ngdoc method + * @methodOf ngSeed.providers:TemplateProvider + * @name setExtension + * @param {String} ext the extension + * @description + * Changes the extension of the views files. + */ + setExtension: function( ext ) { + if ( typeof ext !== 'string') { + throw new Error( 'Template: expecting a string for extension.' ); + } + + if( ext.substr( 0 ) !== '.' ) { + ext = '.' + ext; + } + + extension = ext; + + } + + }; + + } + ]); + +}); diff --git a/app/modules/cs_common/scripts/cs_browser_detect.js b/app/modules/cs_common/scripts/cs_browser_detect.js deleted file mode 100644 index 6d28038..0000000 --- a/app/modules/cs_common/scripts/cs_browser_detect.js +++ /dev/null @@ -1,193 +0,0 @@ -define(['angular', '../module'], function (ng) { - 'use strict'; - /** - * @ngdoc service - * @name ngSeed.services:$browserDetect - * @description - * Easy check for Browser, version and OS with this service. - * - * ### Example - * ```js - * myApp.controller('Test', ['$scope', '$browserDetect', function ($scope, $browserDetect) { - * if( $browserDetect.browser !== 'Chrome' ) { - * // do something with non chrome browsers - * } - * - * if( $browserDetect.browser === 'Explorer' && $browserDetect.version < 10 ) { - * // please upgrade your browser - * } - * }]); - * ``` - */ - ng.module('cs_common.services') - .factory('CSBrowserDetect', function () { - var browserDetect = { - - /** - * @ngdoc function - * @name init - * @methodOf ngSeed.services:$browserDetect - * @description - * Initializes the properties and error messages. - * - * @return {Boolean} True if no errors. False if errors. - */ - init: function () { - // Errors array - this.errors = []; - // Copy of the navigator access - this.navigator = navigator; - - this.browser = this.searchString(this.dataBrowser) || undefined; - // this.browser = 'Explorer'; - - if(!this.browser) { - this.errors.push('Unknown browser'); - } - - this.version = this.searchVersion(navigator.userAgent) || this.searchVersion(navigator.appVersion) || undefined; - // this.version = 9; - - if(!this.version) { - this.errors.push('Unknown version'); - } - - this.OS = this.searchString(this.dataOS) || undefined; - - if(!this.OS) { - this.errors.push('Unkown OS'); - } - - return !this.errors.length; - }, - - searchString: function (data) { - for (var i = 0; i < data.length; i++) { - var dataString = data[i].string; - var dataProp = data[i].prop; - this.versionSearchString = data[i].versionSearch || data[i].identity; - if (dataString) { - if (dataString.indexOf(data[i].subString) !== -1){ - return data[i].identity; - } - } - else if (dataProp){ - return data[i].identity; - } - } - }, - - searchVersion: function (dataString) { - var index = dataString.indexOf(this.versionSearchString); - if (index === -1) { - return; - } - return parseFloat(dataString.substring(index + this.versionSearchString.length + 1)); - }, - - - /** - * @ngdoc property - * @name dataBrowser - * @propertyOf ngSeed.services:$browserDetect - * - * @description - * Array of known Browsers. - * - * @type {Array} - */ - dataBrowser: [ - { - string: navigator.userAgent, - subString: 'Chrome', - identity: 'Chrome' - }, - { - string: navigator.userAgent, - subString: 'OmniWeb', - versionSearch: 'OmniWeb/', - identity: 'OmniWeb' - }, - { - string: navigator.vendor, - subString: 'Apple', - identity: 'Safari', - versionSearch: 'Version' - }, - { - prop: window.opera, - identity: 'Opera', - versionSearch: 'Version' - }, - { - string: navigator.vendor, - subString: 'iCab', - identity: 'iCab' - }, - { - string: navigator.vendor, - subString: 'KDE', - identity: 'Konqueror' - }, - { - string: navigator.userAgent, - subString: 'Firefox', - identity: 'Firefox' - }, - { - string: navigator.vendor, - subString: 'Camino', - identity: 'Camino' - }, - { // for newer Netscapes (6+) - string: navigator.userAgent, - subString: 'Netscape', - identity: 'Netscape' - }, - { - string: navigator.userAgent, - subString: 'MSIE', - identity: 'Explorer', - versionSearch: 'MSIE' - }, - { - string: navigator.userAgent, - subString: 'Gecko', - identity: 'Mozilla', - versionSearch: 'rv' - }, - { // for older Netscapes (4-) - string: navigator.userAgent, - subString: 'Mozilla', - identity: 'Netscape', - versionSearch: 'Mozilla' - } - ], - dataOS: [ - { - string: navigator.platform, - subString: 'Win', - identity: 'Windows' - }, - { - string: navigator.platform, - subString: 'Mac', - identity: 'Mac' - }, - { - string: navigator.userAgent, - subString: 'iPhone', - identity: 'iPhone/iPod' - }, - { - string: navigator.platform, - subString: 'Linux', - identity: 'Linux' - } - ] - }; - - browserDetect.init(); - return browserDetect; - }); -}); diff --git a/app/modules/cs_common/scripts/cs_common_helpers_provider.js b/app/modules/cs_common/scripts/cs_common_helpers_provider.js deleted file mode 100644 index 719b550..0000000 --- a/app/modules/cs_common/scripts/cs_common_helpers_provider.js +++ /dev/null @@ -1,91 +0,0 @@ -define(['angular', '../module'], function (ng) { - 'use strict'; - - ng.module('cs_common.providers') - .provider('CSCommonHelpers', [ - function () { - - var helpers = {}; - var inheritedProvider; - - /** - * @description - * The actual service. - */ - return { - $get: [ - '$injector', - '$location', - '$log', - function ($injector, $location, $log) { - - if(inheritedProvider){ - var provider = $injector.get(inheritedProvider); - if(!provider){ - throw new Error('Unable to inject "' + inheritedProvider + '"'); - } - ng.copy(provider, helpers); - } - - helpers.hasError = function($scope, fieldName, validation, validateOnSubmit){ - if(!$scope.form){ - throw new Error('Expected scope to have a formController registered at scope.form'); - } - var field = $scope.form[fieldName]; - if(!field){ - throw new Error('Expected scope.form to have a field \'' + fieldName + '\''); - } - - if(validateOnSubmit && !$scope.submitted){ - return false; - } - - if(validation){ - return (field.$dirty && field.$error[validation]) || ($scope.submitted && field.$error[validation]); - } - return !!( (field.$dirty && field.$invalid) || ($scope.submitted && field.$invalid) ); - }; - - helpers.isActive = function(path){ - return (new RegExp(path, 'gi')).test($location.path()); - }; - - helpers.redirect = function(path, $event){ - if($event){ - $event.stopPropagation(); - $event.preventDefault(); - } - - if(!path){ - return $log.error('Path not provided'); - } - - $location.url(path); - return false; - }; - - return helpers; - - } - ], - - /** - * @ngdoc function - * @methodOf ngSeed.providers:CSAccountProvider - * @name setAccountService - * @param {String} serviceName the account service name - */ - extend: function (providerName) { - if(typeof serviceName !== 'string') { - throw new Error('CSHelpersProvider: extend method expects a string (name of the helpers provider)'); - } - inheritedProvider = providerName; - } - - }; - - } - - ]); - -}); diff --git a/app/modules/cs_common/scripts/cs_http_options_provider.js b/app/modules/cs_common/scripts/cs_http_options_provider.js deleted file mode 100644 index da1da82..0000000 --- a/app/modules/cs_common/scripts/cs_http_options_provider.js +++ /dev/null @@ -1,150 +0,0 @@ -define(['angular', '../module'], function (ng) { - 'use strict'; - - /** - * @ngdoc service - * @name ngSeed.services:CSHttpOptions - * @description - * This options can be plugged in the $httpProvider or - * the $http object to set up application wide configuration - * for HTTP requests. - * - * ### Examples - * ```js - * myApp.controller('Test', ['$scope', 'CSHttpOptions', function ($scope, CSHttpOptions) { - * if( /localhost/.test(CSHttpOptions.domain) ) { - * // you are working with your local server - * } - * }]); - * ``` - */ - - /** - * @ngdoc service - * @name ngSeed.providers:CSHttpOptionsProvider - * @description - * The provider to configure the domain, port, headers and - * interceptors for all the requests made with $http. - * - * ### Examples - * ```js - * myApp.config(['CSHttpOptionsProvider', function (CSHttpOptionsProvider) { - * CSHttpOptionsProvider.setDomain('http://www.google.com'); - * CSHttpOptionsProvider.setWithCredentials(false); - * }]); - * ``` - */ - - ng.module('cs_common.providers') - .provider('CSHttpOptions', [ - '$httpProvider', - function ($httpProvider) { - /** - * @ngdoc property - * @name withCredentials - * @propertyOf ngSeed.providers:CSHttpOptionsProvider - * @description - * Should it send session information with every request? - */ - var withCredentials = true; - - /** - * @ngdoc property - * @name domain - * @propertyOf ngSeed.providers:CSHttpOptionsProvider - * @description - * For testing purposes this is extremely useful - * it allows you to test the app against a aws server - * running either in dev, staging or production - */ - var domain = 'http://localhost:8080'; - - $httpProvider.defaults.withCredentials = withCredentials; - - /** - * @ngdoc method - * @name interceptors.addDomain - * @methodOf ngSeed.providers:CSHttpOptionsProvider - * @description - * Interceptor. - * - * Add the domain to all the HTTP requests that are not - * templates. And doesn't add it to absolute urls. - * - * Note: Hookup the $templates service to get the proper - * regex. This works for now as is what we are using. - */ - $httpProvider.interceptors.push(function () { - return { - request: function (config) { - if( ! /views\/(.*).html$/.test( config.url ) && ! /^(http|https:\/\/)/i.test( config.url ) ) { - config.url = domain + config.url; - } - return config; - } - }; - }); - - /* - * @name Unauthorized - * @methodOf ngSeed.services:CSHttpOptions - * @param {Object} $q the promise service - * @return {Object} An object with the handlers. - * @description - * Add the domain to all the HTTP requests that are not - * templates. - * - * Note: Hookup the $templates service to get the proper - * regex. This works for now as is what we are using. - */ - // $httpProvider.interceptors.push(['$rootScope', '$q',function ($rootScope, $q) { - // return { - // response: function (res) { - // if(res.status === 401) { - // $rootScope.$broadcast('$auth:loginRequired'); - // } - // return res.data; - // } - // } - // }]); - - return { - $get: function () { - return { - withCredentials: withCredentials, - domain: domain - }; - }, - - /** - * @ngdoc method - * @name setDomain - * @methodOf ngSeed.providers:CSHttpOptionsProvider - * @param {String} uri The domain. - */ - setDomain: function (uri) { - if(typeof uri !== 'string') { - throw new Error('CSHttpOptions: expecting string for domain'); - } - domain = uri; - }, - - /** - * @ngdoc method - * @name setWithCredentials - * @methodOf ngSeed.providers:CSHttpOptionsProvider - * @param {Boolean} value Should it send credentials? - */ - setWithCredentials: function (value) { - if(typeof value !== 'boolean') { - throw new Error('CSHttpOptions: expecting value to be boolean'); - } - withCredentials = value; - } - }; - - } - - ]); - -}); diff --git a/app/modules/cs_common/scripts/cs_resource_factory.js b/app/modules/cs_common/scripts/cs_resource_factory.js deleted file mode 100644 index 8b72d63..0000000 --- a/app/modules/cs_common/scripts/cs_resource_factory.js +++ /dev/null @@ -1,66 +0,0 @@ -define(['angular', '../module'], function (ng) { - 'use strict'; - - /** - * @ngdoc service - * @name ngSeed.services:$resourceFactory - * @description - * A service used to generate and register resources. - */ - - /** - * @ngdoc function - * @name ngSeed.services:ResourceFactory - * @methodOf ngSeed.services:$resourceFactory - * @param {String} resourceName The name of the resource. - * @description - * Generates a factory function that uses ngResource - * to talk to a REST resource that can be directly used - * with factory. - * - * Example of use with factory: - * ```js - * services - * .factory('ResourceService', ResourceFactory('resource')); - * ``` - */ - function ResourceFactory ( resourceName ) { - - /** - * @ngdoc function - * @name builder - * @methodOf ngSeed.services:$resourceFactory - * @description - * - * Generates an HTTP Resource. - * - * @param {$resource} $resource ngResource - * @param {$httpOptions} $httpOptions The HTTP Options object. - * - * @return {resource} The actual resource for resourceName. - */ - var builder = function ( $resource, $httpOptions ) { - var url = $httpOptions.domain+'/'+resourceName+'/:id'; - var defaults = {}; - var actions = { - 'get': {method:'GET'}, - 'query': {method:'GET`', isArray:true}, - 'save': {method:'PUT'}, - 'create': {method:'POST'}, - 'destroy':{method:'DELETE'}, - }; - return $resource(url, defaults, actions); - }; - - return ['$resource', '$httpOptions', builder]; - } - - ng.module('cs_common.services') - .service('CSResourceFactory',function () { - return function (resourceName) { - ng - .module('app.resources') /* TODO: Convert this to provider and make module name configurable */ - .factory(resourceName, ResourceFactory(resourceName)); - }; - }); -}); diff --git a/app/modules/cs_common/scripts/cs_template_provider.js b/app/modules/cs_common/scripts/cs_template_provider.js deleted file mode 100644 index 302f1de..0000000 --- a/app/modules/cs_common/scripts/cs_template_provider.js +++ /dev/null @@ -1,155 +0,0 @@ -define(['angular', '../module'], function (ng) { - 'use strict'; - - /** - * @ngdoc service - * @name ngSeed.services:CSTemplate - * @description - * This service facilitates the loading of templates (not yet extended to - * ng-include) thru two functions. - * - * It can be configured using the CSTemplateProvider. - * - * ### Examples - * ```js - * CSTemplate.view('home'); - * // will look for /views/home.html - * - * CSTemplate.partial('navbar'); - * // will look for /views/partials/navbar.html - * ``` - * This path can be changed using `CSTemplateProvider.setPath`. - * The extension can be changed using `CSTemplateProvider.setExtension`. - */ - - /** - * @ngdoc service - * @name ngSeed.providers:CSTemplateProvider - * @description - * This provider can be used to configure the templates retrieved with the - * CSTemplate service. - * - * ### Examples - * ```js - * CSTemplateProvider.setPath('/templates'); - * CSTemplateProvider.setExtension('.ejs'); - * // now calls like CSTemplate.view('home'); - * // will go to /templates/home.ejs - * // and calls like CSTemplate.partial('navbar'); - * // will go to /templates/partials/navbar.ejs - * ``` - */ - - ng.module('cs_common.providers') - .provider('CSTemplate', [ - function(){ - var basePath = ''; - try { - basePath = document.getElementsByTagName('base')[0].href; - } catch(e){ - console.warn('Unable to read href attr from base tag'); - } - - if(basePath.length) { - if(basePath[basePath.length - 1] === '/') { - basePath = basePath.slice(0, -1); - } - } - - /** - * @name viewsPath - * @type {String} - * @propertyOf ngSeed.providers:CSTemplateProvider - * @description - * The path to the views. - */ - var viewsPath = basePath + '/views/'; - - /** - * @name extension - * @type {String} - * @propertyOf ngSeed.providers:CSTemplateProvider - * @description - * The extension of the partials. - */ - var extension = '.html'; - - /** - * @ngdoc method - * @methodOf ngSeed.services:CSTemplate - * @name view - * @param {String} viewName The name of the view. - * @return {String} The path to the view. - * @description - * Utility functions to get the path of a view. - * - * This method is also available from CSTemplateProvider - */ - function view (viewName) { - // console.log('CSTemplate:', viewsPath + viewName + extension); - return viewsPath + viewName + extension; - } - - /** - * @description - * The actual service. - */ - return { - - $get: function () { - return { - view: view - }; - }, - - view: view, - - /** - * @ngdoc method - * @methodOf ngSeed.providers:CSTemplateProvider - * @name setPath - * @param {String} path the path - * @description - * Changes the path of the views folder. - */ - setPath: function (path) { - if ( typeof path !== 'string') { - throw new Error('CSTemplate: expecting a string for path.'); - } - - if(path[path.length - 1] !== '/') { - path += '/'; - } - - viewsPath = basePath + path; - - // console.log('CSTemplate: path set to ', viewsPath); - }, - - /** - * @ngdoc method - * @methodOf ngSeed.providers:CSTemplateProvider - * @name setExtension - * @param {String} ext the extension - * @description - * Changes the extension of the views files. - */ - setExtension: function (ext) { - if ( typeof ext !== 'string') { - throw new Error('CSTemplate: expecting a string for extension.'); - } - - if(ext.substr(0) !== '.') { - ext = '.' + ext; - } - - extension = ext; - - } - - }; - - } - ]); - -}); diff --git a/app/modules/cs_common/scripts/must_equal_to.js b/app/modules/cs_common/scripts/must_equal_to.js deleted file mode 100644 index 1b5720f..0000000 --- a/app/modules/cs_common/scripts/must_equal_to.js +++ /dev/null @@ -1,32 +0,0 @@ -define(['angular', '../module'], function (ng) { - 'use strict'; - - ng.module('cs_common.directives') - .directive('mustEqualTo', function() { - var link = function($scope, $element, $attrs, ctrl) { - - function validate(value){ - ctrl.$setValidity('mustEqualTo', value === $scope.$eval($attrs.mustEqualTo)); - return value; - } - - $scope.$watch($attrs.mustEqualTo, function(equalTo){ - if(ng.isUndefined(equalTo)){ - return; - } - $scope.equalTo = equalTo; - return validate($element.val()); - }); - - ctrl.$parsers.unshift(validate); - - }; - - return { - require: 'ngModel', - link: link - }; - - }); - -}); diff --git a/app/modules/cs_common/styles/jqueryResizableColumns.css b/app/modules/cs_common/styles/jqueryResizableColumns.css new file mode 100644 index 0000000..3e04b27 --- /dev/null +++ b/app/modules/cs_common/styles/jqueryResizableColumns.css @@ -0,0 +1,19 @@ +/* jQuery Resizable Columns v0.1.0 | http://dobtco.github.io/jquery-resizable-columns/ | Licensed MIT | Built Wed Apr 30 2014 14:24:25 */ +.rc-handle-container { + position: relative; +} +.rc-handle { + position: absolute; + width: 7px; + cursor: ew-resize; + margin-left: -3px; + z-index: 2; +} +table.rc-table-resizing { + cursor: ew-resize; +} +table.rc-table-resizing thead, +table.rc-table-resizing thead > th, +table.rc-table-resizing thead > th > a { + cursor: ew-resize; +} diff --git a/app/modules/cs_common/styles/navbar.less b/app/modules/cs_common/styles/navbar.less new file mode 100644 index 0000000..73cf967 --- /dev/null +++ b/app/modules/cs_common/styles/navbar.less @@ -0,0 +1,321 @@ +@import "./../../../styles/less/bootstrap/variables.less"; +@import "./../../../styles/less/bootstrap/mixins.less"; + +// ---------------------------------------------------------- +// Custom Navigation Elements +// ---------------------------------------------------------- + +.pill-tabs { + li { + > a { color:@trim-color; cursor:pointer; font-weight:bold; } + &.active, &.active:hover { + > a { background-color:@trim-color; color:#FFF; } + } + } +} +.dropdown-header { background:none; color:#BBB; text-transform:uppercase; border-top:1px solid #E2E2E2; margin-top:10px; padding-top:12px; padding-bottom:6px; } +.dropdown-header:first-child { margin-top:0; border-top:none; } + +.navbar { + .brand-logo { + height: 45px; + margin: 10px; + } +} +.navbar { height:67px; background:#FFF url('../images/nav-main-bg.png') left bottom repeat-x; .box-shadow(0 3px 8px -2px rgba(0,0,0,0.25)); + .navbar-header { + .navbar-brand img { max-height:36px; max-width:160px; } + } + .navbar-text { margin-top:22px; font-size:12px; } + .btn-group { margin-top:12px; + .btn { font-size:12px; } + } + .navbar-toggle { margin-top:14px; + .icon-bar { background:#000; } + } +} + +.navbar-app { height: 100%; width: 92px; position: fixed; left: 0px; top: 67px; bottom: 0px; border-right: 1px solid #95969B; + padding: 0; background: #3F4452 url('../images/nav-app-bg.png') right top repeat-y; z-index: 1020; + + ul.nav { list-style:none; height:100%; + > li { display:block; border-bottom:1px solid #42454E; margin:0; padding:0; font-size:12px; background:transparent url('../images/nav-app-item-bg.png') left bottom repeat-x; + > a { margin:0; padding:16px 5px 14px; text-align:center; color:#C0C1C4; background:transparent; } + span { display:block; margin-top:6px; } + &:hover, &.open { background:#353944; + > a { color:#FFF; background:none; } + } + &.active { + > a { color:@trim-color; } + } + &:last-child { border-bottom:none; } + } + li.dropdown { position:static; + &:hover > ul.dropdown-menu { display:block; } + } + .dropdown-menu { top:-2px; left:91px; border-radius:0; border:0; width:188px; height:100%; .box-shadow(none); background:#353944; padding:12px 0; + > li { + a { color:#FFF; font-size:12px; padding:12px 16px 13px 26px; background:none; + &:hover, &:active { background:#3D4049; } + } + } + .divider { opacity:0.12; } + } + } +} + +.content { + padding-bottom: 20px; + margin-left: 97px; + padding-right: 10px; + + .pagination { + margin: 0; + } + .add-button { + padding-right : 20px; + margin-top : 10px; + } + .clear-filters { + margin-bottom : 5px; + margin-right : 25px; + margin-top : -40px; + } + .table-responsive { + margin-left : 28px; + width : 100%; + } + .header-area { + .header-buttons + { margin:24px 8px 0 0; position:relative; + .btn { margin-right:5px; } + .fa-fw { text-align:left; } + .dropdown-menu + { margin-top:45px; font-size:12px; padding:10px 0; + >li > a { padding:8px 18px; } + } + } + } +} + +/* Mobile @1200px +────────────────────────────────────────────────────────────*/ +@media ( max-width : @screen-lg ) { + .navbar { + .navbar-right { + padding-top : 5px; + position : relative; + z-index : 9999; + } + } + .content { + padding-left : 7px; + + .ng-table { + .actions { + width : 140px; + margin : 0; + padding : 0; + margin-left : auto; + margin-right: auto; + } + } + + .table-responsive { + padding-left : 28px; + padding-right : 28px; + max-width : 1080px; + width : 100%; + } + + .add-button { + padding-right : 20px; + margin-top : 20px; + } + .clear-filters { + margin-bottom : 5px; + margin-right : 25px; + margin-top : -40px; + } + } +} + +/* Desktop @992px +────────────────────────────────────────────────────────────*/ +@media ( min-width: ( @screen-sm + 1 ) ) and ( max-width: @screen-md ) { + .navbar { + .navbar-right { + padding-top : 5px; + position : relative; + z-index : 9999; + } + } + .content { + margin-left : 98px; + + .ng-table { + .actions { + width : 140px; + margin : 0; + padding : 0; + margin-left : auto; + margin-right: auto; + } + + } + + .table-responsive { + margin-left : 28px; + max-width : 850px; + width : 100%; + } + + .add-button { + padding-right : 0; + margin-top : 20px; + } + .clear-filters { + margin-bottom : 5px; + margin-right : 0; + margin-top : -40px; + } + } +} + +/* Mobile @768px +────────────────────────────────────────────────────────────*/ +@media ( min-width: ( @screen-xs + 1 ) ) and ( max-width: @screen-sm ) { + .navbar { + .navbar-right { + padding-top : 5px; + position : relative; + z-index : 9999; + } + } + .content { + margin-left : 94px; + + .ng-table { + .actions { + width : 140px; + margin : 0; + padding : 0; + margin-left : auto; + margin-right: auto; + } + + } + + .table-responsive { + margin-left : 28px; + max-width : 640px; + width : 100%; + } + + .add-button { + padding-right : 20px; + margin-top : 20px; + } + .clear-filters { + margin-bottom : 5px; + margin-right : 20px; + margin-top : -30px; + } + } +} + +/* Mobile @480px +────────────────────────────────────────────────────────────*/ +@media ( max-width : @screen-xs ) { + body { + padding-top : 50px; + } + .content { + margin-left : 55px; + padding-right : 10px; + padding-bottom : 20px; + font-size : 12px; + + .btn { + .button-size(@padding-xs-vertical; @padding-xs-horizontal; @font-size-small; @line-height-small; @border-radius-small); + } + + .ng-table { + button b span { + display : none; + } + + .actions { + width : 48px; + margin : 0; + padding : 0; + margin-left : auto; + margin-right: auto; + } + + } + + .table-responsive { + margin-left : 28px; + max-width : 390px; + width : 100%; + } + + .pull-left h3, + .pull-left.h3 { + font-size : @font-size-h4; + } + + .add-button { + padding-right : 0; + margin-top : 20px; + } + .clear-filters { + margin-bottom : 5px; + margin-right : 10px; + margin-top : -27px; + } + .header-area { + .header-buttons { + position : absolute; + top : 0; + right : 12px; + margin-top : 12px; + } + } + } + .navbar { + height : 47px; + + .navbar-toggle { + margin-top : 5px; + } + + .brand-logo { + height : 28px; + padding : 0; + padding-top : 5px; + } + + .navbar-right { + padding-top : 5px; + position : relative; + z-index : 9999; + } + } + .navbar-app { + width : 50px; + top : 50px; + + ul.nav { + > li { + i { + font-size: 1.6em; + } + span { + display : none; + } + } + } + } +} diff --git a/app/modules/cs_common/views/navbar.html b/app/modules/cs_common/views/navbar.html new file mode 100644 index 0000000..bd8748f --- /dev/null +++ b/app/modules/cs_common/views/navbar.html @@ -0,0 +1,134 @@ +
+ diff --git a/app/modules/cs_common/views/page_not_found.html b/app/modules/cs_common/views/page_not_found.html new file mode 100644 index 0000000..f561130 --- /dev/null +++ b/app/modules/cs_common/views/page_not_found.html @@ -0,0 +1,4 @@ +
+

Page Not Found (404)

+

Whoops it looks like something went wrong during your request, please check the url and try again.

+
\ No newline at end of file diff --git a/app/modules/cs_common/views/table.html b/app/modules/cs_common/views/table.html new file mode 100644 index 0000000..d7c5f2d --- /dev/null +++ b/app/modules/cs_common/views/table.html @@ -0,0 +1,85 @@ +
+ +
+ + + +
+
+ + + + + + + + + + + + + + + + +
+ +
+ {{column.title}} +
+
+ + + + + Actions +
+ +
{{ outputRow( row, column.name ) }}
+ +
+
+
+
+
From 9d2ded7293127d7c4e5600444794d9a1624229c6 Mon Sep 17 00:00:00 2001 From: Richard Gustin Date: Fri, 29 Aug 2014 16:25:53 +1000 Subject: [PATCH 07/14] refactor(session): Removed old cs_session module --- app/modules/auth/providers/SessionProvider.js | 588 ++++++++++++++++++ .../tests/e2e/signIn.test.js} | 8 +- app/modules/cs_session/main.js | 17 - app/modules/cs_session/module.js | 45 -- .../cs_session/scripts/cs_login_controller.js | 27 - .../scripts/cs_logout_controller.js | 15 - .../scripts/cs_session_helpers_provider.js | 60 -- .../cs_session/scripts/cs_session_provider.js | 391 ------------ .../cs_session/scripts/cs_session_service.js | 46 -- app/modules/cs_session/views/login.html | 11 - 10 files changed, 592 insertions(+), 616 deletions(-) create mode 100644 app/modules/auth/providers/SessionProvider.js rename app/modules/{cs_session/tests/e2e/login.test.js => auth/tests/e2e/signIn.test.js} (70%) delete mode 100644 app/modules/cs_session/main.js delete mode 100644 app/modules/cs_session/module.js delete mode 100644 app/modules/cs_session/scripts/cs_login_controller.js delete mode 100644 app/modules/cs_session/scripts/cs_logout_controller.js delete mode 100644 app/modules/cs_session/scripts/cs_session_helpers_provider.js delete mode 100644 app/modules/cs_session/scripts/cs_session_provider.js delete mode 100644 app/modules/cs_session/scripts/cs_session_service.js delete mode 100644 app/modules/cs_session/views/login.html diff --git a/app/modules/auth/providers/SessionProvider.js b/app/modules/auth/providers/SessionProvider.js new file mode 100644 index 0000000..17cae87 --- /dev/null +++ b/app/modules/auth/providers/SessionProvider.js @@ -0,0 +1,588 @@ +define( [ 'angular', '../module' ], function( ng ) { + 'use strict'; + + /** + * @ngdoc service + * @name ngSeed.services:CSSession + * @description + * A set of functions to easily signIn/signOut, register new users, and + * retrieving the user session from the server. + * + * ### Example + * ```js + * myApp.controller('Test', ['$scope', 'CSSession', function ($scope, CSSession) { + * $scope.$watch(CSSession.getCurrentUser, function() { + * // do something as soon as the user changes + * // and by this I mean logs in or out + * }); + * + * if (CSSession.isLoggedIn()) { + * // do something if the user is logged in + * // thou this is not necessary on non-public pages + * // on public ones you might want to use it + * // to do some other logic + * } + * }]); + * ``` + */ + + /** + * @ngdoc service + * @name ngSeed.providers:SessionProvider + * @description + * Dead-easy auth checking. + * + * Please note that custom signIn requiring logic, on-location-change auth + * checking, and default signIn success behaviour can be configured + * using the authProvider on a config block. + * + * ### Configuring SessionProvider: + * This is the default value, feel free to change it to something else if your app requires it: + * + * ```js + * SessionProvider.setSessionService('sessionService'); + * + * SessionProvider.setHandler('handleSignInStart', function (redirect) { + * $('#mysignInModal').open(); + * }); + * + * SessionProvider.setHandler('handleSignInSuccess', function () { + * $('#mysignInModal').close(); + * }); + * ``` + * + * ### Securing Routes: + * Add a `public: false` property or a `public: true` property to your routes. In fact, + * any falsy value will end up requiring signIn. For instance: + * + * ```js + * $routeProvider + * .when('/', { + * templateUrl: view('home'), + * controller: 'HomeCtrl', + * public: true + * }) + * .when('/users', { + * templateUrl: view('users'), + * controller: 'UserCtrl', + * }) + * .when('/error', { + * templateUrl: partial('error'), + * public: true + * }) + * .otherwise({ + * redirectTo: '/' + * }); + * ``` + * + * This will give you a public home and error routes. If you try to access `/users`, you will + * immediately be prompted for authentication. + */ + + ng + .module( 'auth.providers' ) + .provider( 'Session', [ + function() { + /** + * @name currentUser + * @type {Object} + * @propertyOf ngSeed.providers:SessionProvider + * @description + * the logged in user or undefined + */ + var currentUser = null; + + /** + * @name sessionService + * @type {Object} + * @propertyOf ngSeed.providers:SessionProvider + * @description + * The user service. + */ + var sessionService = null; + + /** + * @name sessionServiceName + * @type {String} + * @propertyOf ngSeed.providers:SessionProvider + * @description + * The name of the service to $inject. + */ + var sessionServiceName = 'SessionService'; + + /** + * @name sessionService + * @type {Object} + * @propertyOf ngSeed.providers:SessionProvider + * @description + * The user service. + */ + var accountService = null; + + /** + * @name sessionServiceName + * @type {String} + * @propertyOf ngSeed.providers:SessionProvider + * @description + * The name of the service to $inject. + */ + var accountServiceName = 'AccountService'; + + /** + * @name handlers + * @type {Object} + * @propertyOf ngSeed.providers:SessionProvider + * @description + * The handlers object. + */ + var handlers = { + signUpSuccess: null, + signUpFailure: null, + signInStart: null, + signInSuccess: null, + signOutSuccess: null, + locationChange: null + }; + + function switchRouteMatcher(on, when, whenProperties) { + // TODO(i): this code is convoluted and inefficient, we should construct the route matching + // regex only once and then reuse it + + // Escape regexp special characters. + when = '^' + when.replace(/[-\/\\^$:*+?.()|[\]{}]/g, '\\$&') + '$'; + + var regex = '', + params = [], + dst = {}; + + var re = /\\([:*])(\w+)/g, + paramMatch, + lastMatchedIndex = 0; + + while ((paramMatch = re.exec(when)) !== null) { + // Find each :param in `when` and replace it with a capturing group. + // Append all other sections of when unchanged. + regex += when.slice(lastMatchedIndex, paramMatch.index); + switch(paramMatch[1]) { + case ':': + regex += '([^\\/]*)'; + break; + case '*': + regex += '(.*)'; + break; + } + params.push(paramMatch[2]); + lastMatchedIndex = re.lastIndex; + } + // Append trailing path part. + regex += when.substr(lastMatchedIndex); + + var match = on.match(new RegExp(regex, whenProperties.caseInsensitiveMatch ? 'i' : '')); + if (match) { + ng.forEach(params, function(name, index) { + dst[name] = match[index + 1]; + }); + } + return match ? dst : null; + } + + /** + * @description + * The actual service. + */ + return { + + $get: [ '$rootScope', '$location', '$route', '$injector', '$log', + function( $rootScope, $location, $route, $injector, $log ) { + var messenger = $injector.has( 'Messenger' ) ? $injector.get( 'Messenger' ) : $log; + + if ( !sessionService && sessionServiceName ) { + sessionService = $injector.get( sessionServiceName ); + } + + if ( !sessionService ) { + throw new Error( 'SessionProvider: please configure a sessionService' ); + } + + if ( !accountService && accountServiceName ) { + accountService = $injector.get( accountServiceName ); + } + + if ( !accountService ) { + throw new Error( 'SessionProvider: please configure a accountService' ); + } + + if ( !handlers.signInStart ) { + $log.log( 'SessionProvider: using default signInStart method' ); + } + + if ( !handlers.signInSuccess ) { + $log.log( 'SessionProvider: using default signInSuccess method' ); + } + + if ( !handlers.locationChange ) { + $log.log( 'SessionProvider: using default locationChange method' ); + } + + /** + * @ngdoc function + * @name handlers.signInStart + * @propertyOf ngSeed.providers:SessionProvider + * @description + * Default signIn starting logic. + */ + handlers.signInStart = handlers.signInStart || function( redirect ) { + $log.log( 'SessionProvider: redirecting to /signIn' ); + + $location.path( '/signIn' ); + $location.search({ + redirect: encodeURIComponent( redirect ) + }); + return; + }; + + /** + * @ngdoc function + * @name handlers.signInSuccess + * @propertyOf ngSeed.providers:SessionProvider + * @description + * This method redirects the user to the redirect search term if + * it exists. + */ + handlers.signInSuccess = handlers.signInSuccess || function() { + if ( $location.search().redirect ) { + $log.log( 'SessionProvider: redirecting to', $location.search().redirect ); + + $location.path( $location.search().redirect ); + $location.search( {} ); + } else { + $location.path( '/' ); + } + }; + + /* + * @ngdoc function + * @name handlers.signInSuccess + * @propertyOf ngSeed.providers:SessionProvider + * @description + * This method redirects the user to the redirect search term if + * it exists. + */ + handlers.signOutSuccess = handlers.signOutSuccess || function() { + messenger.success( 'User has successfully signed out.' ); + $location.path( '/' ); + }; + + /** + * @name signUpSuccess + * @description + * This method is a success callback for signUp function + */ + handlers.signUpSuccess = handlers.signUpSuccess || function( user ) { + if ( !user || !user.id ) { + $rootScope.$broadcast( 'SessionProvider:signUpFailure', user ); + } else { + $rootScope.$broadcast( 'SessionProvider:signUpSuccess', user ); + } + }; + + /** + * @name signUpFailure + * @description + * This method is an eror callback for signUp function + */ + handlers.signUpFailure = handlers.signUpFailure || function( error ) { + currentUser = null; + $rootScope.$broadcast('SessionProvider:signUpFailure', error ); + }; + + /** + * @name userReload + * @description + * Re-fetches the user object from the DB + */ + handlers.userReload = handlers.userReload || function () { + accountService.getCurrentUser(true).then(function (user) { + currentUser = user; + }); + }; + + /** + * @ngdoc function + * @name handlers.locationChange + * @propertyOf ngSeed.providers:SessionProvider + * @description + * This method takes a user navigating, does a quick auth check + * and if everything is alright proceeds. + */ + handlers.locationChange = handlers.locationChange || function( event, next ) { + next = '/' + next.split( '/' ).splice( 3 ).join( '/' ).split( '?' )[ 0 ]; + if( next.length > 1 && next.substr(-1) === '/' ){ + next = next.substr(0, next.length - 1); + } + + if ( currentUser === null || !currentUser.id ){ + var route; + ng.forEach($route.routes, function(when, pathTemplate){ + if(switchRouteMatcher(next, pathTemplate, when)){ + route = route || when; + } + }); + + $log.log( 'SessionProvider: Guest access to', next ); + $log.log( 'SessionProvider:', next, 'is', route.public ? 'public' : 'private' ); + + if ( route && !route.public ) { + $rootScope.$broadcast( 'SessionProvider:signInStart' ); + handlers.signInStart( next.substr( 1 ) ); + } + } else { + $log.log( 'SessionProvider: proceeding to load', next ); + } + }; + + /** + * @description + * $rootScope hookups + */ + $rootScope.$on( '$locationChangeStart', function( event, next, current ) { + if ( !$route.current ) { + $log.log( 'SessionProvider: Welcome newcomer!' ); + $log.log( 'SessionProvider: Checking your session...' ); + + sessionService + .session().$promise + .then( function( user ) { + if ( user.id ) { + $log.log( 'SessionProvider: we got', user ); + + currentUser = user; + if ( ng.isFunction( handlers.locationChange ) ) { + handlers.locationChange( event, next, current ); + } + } else { + throw user; + } + }) + .catch( function( err ) { + $log.log( 'SessionProvider: request failed' + ( err.message ? err.message : err ) ); + $log.log( 'SessionProvider: proceeding as guest.' ); + + if ( ng.isFunction( handlers.locationChange ) ) { + handlers.locationChange( event, next, current ); + } + }); + } else { + if ( ng.isFunction( handlers.locationChange ) ) { + handlers.locationChange( event, next, current ); + } + } + }); + + $rootScope.$on( 'SessionProvider:signInSuccess', function() { + if ( ng.isFunction( handlers.signInSuccess ) ) { + handlers.signInSuccess(); + } + }); + + $rootScope.$on( 'SessionProvider:signOutSuccess', function() { + if ( ng.isFunction( handlers.signOutSuccess ) ) { + handlers.signOutSuccess(); + } + }); + + $rootScope.$on( 'SessionProvider:signInRequired', function() { + messenger.error( 'User is not authenticated, please sign in to continue.' ); + $location.path( '/signIn' ); + }); + + return { + /** + * @name getCurrentUser + * @ngdoc function + * @methodOf ngSeed.services:CSSession + * @return {Object} the current user + */ + getCurrentUser: function() { + return currentUser; + }, + + /** + * @name isLoggedIn + * @ngdoc function + * @methodOf ngSeed.services:CSSession + * @return {Boolean} true or false if there is or not a current user + */ + isLoggedIn: function() { + return !!currentUser; + // return (currentUser === null || !currentUser.id) ? false : true; + }, + + + /** + * @name signIn + * @ngdoc function + * @methodOf ngSeed.services:CSSession + * @param {Object} credentials the credentials to be passed to the signIn service + * @return {Promise} the promise your signIn service returns on signIn + */ + signIn: function( credentials ) { + return sessionService + .signIn( credentials ).$promise + .then( function( user ) { + if ( user.id ) { + currentUser = user; + $rootScope.$broadcast( 'SessionProvider:signInSuccess' ); + } else { + throw user; + } + }) + .catch( function( err ) { + $rootScope.$broadcast( 'SessionProvider:signInFailure', { + message: ( !!err.message ? err.message : err ) + }); + }); + }, + + /** + * @name signOut + * @ngdoc function + * @methodOf ngSeed.services:CSSession + * @return {Promise} the promise your signIn service returns on signOut + */ + signOut: function() { + $rootScope.$broadcast( 'SessionProvider:signOutSuccess' ); + if ( currentUser && currentUser.id ) { + return sessionService.signOut().$promise.then( function() { + currentUser = null; + }); + } + }, + + /** + * @name authenticate + * @ngdoc function + * @methodOf ngSeed.services:CSSession + * @return {Promise} the promise your signIn service returns on signOut + */ + authenticate: function( user ) { + if ( !user || !user.id ){ + throw new Error( 'Unable to authenticate with', user ); + } + currentUser = user; + $rootScope.$broadcast( 'SessionProvider:signInSuccess' ); + $rootScope.$broadcast( 'SessionProvider:authenticated' ); + }, + + /** + * @name signUp + * @param {Object} credentials the user (and account) credentials + * @return {Promise} the promise your account service returns on signUp. + */ + signUp: function( credentials ) { + return accountService + .create( credentials ).$promise + .then( function( response ){ + return handlers.signUpSuccess( response, currentUser ); + }) + .catch( function( err ) { + return handlers.signUpFailure( err, currentUser ); + }); + }, + + /** + * @name confirmSignUp + * @param {string} token as received from the confirmation email link + * @return {Promise} the promise your user service returns on 'signUpRegistration' + */ + confirmSignUp: function( data ) { + return accountService + .confirm( data ).$promise + .then( function() { + $rootScope.$broadcast( 'SessionProvider:signUpConfirmationSuccess' ); + }) + .catch( function( err ) { + $rootScope.$broadcast( 'SessionProvider:signUpConfirmationFailure', { err: err } ); + }); + }, + + /** + * @name requestPasswordReset + * @param {string} email, as received from the reset password form + * @return {Promise} the promise your user service returns on 'requestPasswordReset' + */ + requestPasswordReset: function( email ) { + return sessionService + .requestPasswordReset( email ).$promise + .then( function() { + $rootScope.$broadcast( 'SessionProvider:requestPasswordResetSuccess' ); + }) + .catch( function( err ) { + $rootScope.$broadcast( 'SessionProvider:requestPasswordResetFailure', !!err.message ? err.message : err ); + }); + }, + + /** + * @name submitPasswordReset + * @param {string} email, as received from the reset password form + * @return {Promise} the promise your user service returns on 'requestPasswordReset' + */ + submitPasswordReset: function( data ) { + return sessionService + .submitPasswordReset( data ) + .then( function() { + $rootScope.$broadcast( 'SessionProvider:submitPasswordResetSuccess' ); + }) + .catch( function( err ) { + $rootScope.$broadcast( 'SessionProvider:submitPasswordResetFailure', !!err.message ? err.message : err ); + }); + }, + + + }; + + }], + + /** + * @ngdoc function + * @methodOf ngSeed.providers:SessionProvider + * @name setSessionService + * @param {String} usr the user service name + */ + setSessionService: function( serviceName ) { + if ( !ng.isString( serviceName ) ) { + throw new Error( 'SessionProvider: setSessionService expects a string to use $injector upon instantiation' ); + } + sessionServiceName = serviceName; + }, + + /** + * @ngdoc function + * @methodOf ngSeed.providers:SessionProvider + * @name setHandler + * @param {String} key the handler name + * @param {Function} foo the handler function + * @description + * Replaces one of the default handlers. + */ + setHandler: function( key, foo ) { + if ( key.substr( 0, 6 ) !== 'handle' ) { + throw new Error( 'SessionProvider: Expecting a handler name that starts with \'handle\'.' ); + } + + if ( !handlers.hasOwnProperty( key ) ) { + throw new Error( 'SessionProvider: handle name "' + key + '" is not a valid property.' ); + } + + if ( !ng.isFunction( foo ) ) { + throw new Error( 'SessionProvider: foo is not a function.' ); + } + + handlers[ key ] = foo; + } + + }; + + } + ]); +}); diff --git a/app/modules/cs_session/tests/e2e/login.test.js b/app/modules/auth/tests/e2e/signIn.test.js similarity index 70% rename from app/modules/cs_session/tests/e2e/login.test.js rename to app/modules/auth/tests/e2e/signIn.test.js index dcc7017..4b34f8c 100644 --- a/app/modules/cs_session/tests/e2e/login.test.js +++ b/app/modules/auth/tests/e2e/signIn.test.js @@ -1,13 +1,13 @@ -// test if the login is working against a running back-end -describe('e2e: login', function() { +// test if the signIn is working against a running back-end +describe('e2e: signIn', function() { var ptor; beforeEach(function() { ptor = protractor.getInstance(); - ptor.get( '/login?redirect=users' ); + ptor.get( '/signIn?redirect=users' ); }); - it('should login with the default user account', function() { + it('should signIn with the default user account', function() { var username = element( by.model( 'credentials.username' ) ); var password = element( by.model( 'credentials.password' ) ); diff --git a/app/modules/cs_session/main.js b/app/modules/cs_session/main.js deleted file mode 100644 index 975d712..0000000 --- a/app/modules/cs_session/main.js +++ /dev/null @@ -1,17 +0,0 @@ -define([ - './module', - - 'cs_common', - 'underscore', - - // Controllers - './scripts/cs_login_controller', - './scripts/cs_logout_controller', - - // Providers - './scripts/cs_session_provider', - './scripts/cs_session_helpers_provider', - - // Services - './scripts/cs_session_service' -], function() {}); diff --git a/app/modules/cs_session/module.js b/app/modules/cs_session/module.js deleted file mode 100644 index f7185f6..0000000 --- a/app/modules/cs_session/module.js +++ /dev/null @@ -1,45 +0,0 @@ -define(['angular'], function (ng) { - 'use strict'; - - ng.module('cs_session.providers', []); - ng.module('cs_session.controllers', []); - ng.module('cs_session.services', []); - - var module = ng.module('cs_session', [ - 'cs_common', - 'cs_session.providers', - 'cs_session.controllers', - 'cs_session.services' - ]); - - module.config([ - '$routeProvider', - 'CSTemplateProvider', - 'CSSessionHelpersProvider', - 'CSSessionProvider', - function ($routeProvider, CSTemplate, CSSessionHelpersProvider, CSSessionProvider) { - - CSSessionHelpersProvider.extend('CSCommonHelpers'); - - CSTemplate.setPath('/modules/cs_session/views'); - - CSSessionProvider.setSessionService('CSSessionService'); - - $routeProvider - .when('/login', { - templateUrl: CSTemplate.view('login'), - controller: 'CSLoginController', - public: true - }) - .when('/logout', { - controller: 'CSLogoutController', - template: ' ', - public: true - }); - } - - ]); - - return module; - -}); diff --git a/app/modules/cs_session/scripts/cs_login_controller.js b/app/modules/cs_session/scripts/cs_login_controller.js deleted file mode 100644 index f496ce6..0000000 --- a/app/modules/cs_session/scripts/cs_login_controller.js +++ /dev/null @@ -1,27 +0,0 @@ -define(['angular', '../module'], function (ng) { - 'use strict'; - - ng.module('cs_session.controllers') - .controller('CSLoginController', [ - '$scope', - 'CSSession', - 'CSSessionHelpers', - '$log', - function ($scope, CSSessionProvider, CSSessionHelpersProvider, $log) { - $scope.helpers = CSSessionHelpersProvider; - - $scope.login = function () { - CSSessionProvider.login($scope.credentials); - }; - - $scope.$on('CSSessionProvider:loginFailure', function (event, data) { - $log.log('CSLoginController:', event, data); - if(data === '403') { - $log.error('Invalid username/password'); - } - }); - - } - ]); - -}); diff --git a/app/modules/cs_session/scripts/cs_logout_controller.js b/app/modules/cs_session/scripts/cs_logout_controller.js deleted file mode 100644 index 7b9e8ba..0000000 --- a/app/modules/cs_session/scripts/cs_logout_controller.js +++ /dev/null @@ -1,15 +0,0 @@ -define(['angular', '../module'], function (ng) { - 'use strict'; - - ng.module('cs_session.controllers') - .controller('CSLogoutController', [ - '$scope', - 'CSSession', - function ($scope, CSSessionProvider) { - console.log('currentUser', $scope.currentUser); - CSSessionProvider.logout(); - - } - ]); - -}); diff --git a/app/modules/cs_session/scripts/cs_session_helpers_provider.js b/app/modules/cs_session/scripts/cs_session_helpers_provider.js deleted file mode 100644 index 6dd1b90..0000000 --- a/app/modules/cs_session/scripts/cs_session_helpers_provider.js +++ /dev/null @@ -1,60 +0,0 @@ -define(['angular', '../module'], function (ng) { - 'use strict'; - - ng.module('cs_session.providers') - .provider('CSSessionHelpers', [ - function () { - - var helpers = {}; - var inheritedProvider; - - /** - * @description - * The actual service. - */ - return { - $get: [ - '$injector', - function ($injector) { - - if(inheritedProvider){ - var provider = $injector.get(inheritedProvider); - if(!provider){ - throw new Error('Unable to inject "' + inheritedProvider + '"'); - } - ng.copy(provider, helpers); - } - - /** - * Define your own helper functions here - * - * helpers.uppercase = function(string){ - * return string.toUpperCase(); - * } - */ - - return helpers; - - } - ], - - /** - * @ngdoc function - * @methodOf ngSeed.providers:CSAccountProvider - * @name setAccountService - * @param {String} serviceName the account service name - */ - extend: function (providerName) { - if(typeof providerName !== 'string') { - throw new Error('CSHelpersProvider: extend method expects a string (name of the helpers provider)'); - } - inheritedProvider = providerName; - } - - }; - - } - - ]); - -}); diff --git a/app/modules/cs_session/scripts/cs_session_provider.js b/app/modules/cs_session/scripts/cs_session_provider.js deleted file mode 100644 index 47b36e3..0000000 --- a/app/modules/cs_session/scripts/cs_session_provider.js +++ /dev/null @@ -1,391 +0,0 @@ -define(['angular', '../module'], function (ng) { - 'use strict'; - - /** - * @ngdoc service - * @name ngSeed.services:CSSession - * @description - * A set of functions to easily login/logout, register new users, and - * retrieving the user session from the server. - * - * ### Example - * ```js - * myApp.controller('Test', ['$scope', 'CSSession', function ($scope, CSSession) { - * $scope.$watch(CSSession.getCurrentUser, function() { - * // do something as soon as the user changes - * // and by this I mean logs in or out - * }); - * - * if(CSSession.isLoggedIn()) { - * // do something if the user is logged in - * // thou this is not necessary on non-public pages - * // on public ones you might want to use it - * // to do some other logic - * } - * }]); - * ``` - */ - - /** - * @ngdoc service - * @name ngSeed.providers:CSSessionProvider - * @description - * Dead-easy auth checking. - * - * Please note that custom login requiring logic, on-location-change auth - * checking, and default login success behaviour can be configured - * using the authProvider on a config block. - * - * ### Configuring CSSessionProvider: - * This is the default value, feel free to change it to something else if your app requires it: - * - * ```js - * CSSessionProvider.setSessionService('sessionService'); - * - * CSSessionProvider.setHandler('handleLoginStart', function (redirect) { - * $('#myLoginModal').open(); - * }); - * - * CSSessionProvider.setHandler('handleLoginSuccess', function () { - * $('#myLoginModal').close(); - * }); - * ``` - * - * ### Securing Routes: - * Add a `public: false` property or a `public: true` property to your routes. In fact, - * any falsy value will end up requiring login. For instance: - * - * ```js - * $routeProvider - * .when('/', { - * templateUrl: view('home'), - * controller: 'HomeCtrl', - * public: true - * }) - * .when('/users', { - * templateUrl: view('users'), - * controller: 'UserCtrl', - * }) - * .when('/error', { - * templateUrl: partial('error'), - * public: true - * }) - * .otherwise({ - * redirectTo: '/' - * }); - * ``` - * - * This will give you a public home and error routes. If you try to access `/users`, you will - * immediately be prompted for authentication. - */ - - ng.module('cs_session.providers') - .provider('CSSession', [ - function () { - /** - * @name currentUser - * @type {Object} - * @propertyOf ngSeed.providers:CSSessionProvider - * @description - * the logged in user or undefined - */ - var currentUser = null; - - /** - * @name sessionService - * @type {Object} - * @propertyOf ngSeed.providers:CSSessionProvider - * @description - * The user service. - */ - var sessionService = null; - - /** - * @name sessionServiceName - * @type {String} - * @propertyOf ngSeed.providers:CSSessionProvider - * @description - * The name of the service to $inject. - */ - var sessionServiceName = 'CSSessionService'; - - /** - * @name handlers - * @type {Object} - * @propertyOf ngSeed.providers:CSSessionProvider - * @description - * The handlers object. - */ - var handlers = { - loginStart: null, - loginSuccess: null, - logoutSuccess: null, - locationChange: null, - }; - - /** - * @description - * The actual service. - */ - return { - - $get: ['$rootScope', '$location', '$route', '$injector', - function ($rootScope, $location, $route, $injector) { - - if(!sessionService && sessionServiceName) { - sessionService = $injector.get(sessionServiceName); - } - - if (!sessionService) { - throw new Error('CSSessionProvider: please configure a sessionService'); - } - - if (!handlers.loginStart) { - console.log('CSSessionProvider: using default loginStart method'); - } - - if (!handlers.loginSuccess) { - console.log('CSSessionProvider: using default loginSuccess method'); - } - - if (!handlers.locationChange) { - console.log('CSSessionProvider: using default locationChange method'); - } - - /** - * @ngdoc function - * @name handlers.loginStart - * @propertyOf ngSeed.providers:CSSessionProvider - * @description - * Default login starting logic. - */ - handlers.loginStart = handlers.loginStart || function (redirect) { - console.log('CSSessionProvider: redirecting to /login'); - $location.path('/login'); - $location.search({ - redirect: encodeURIComponent(redirect) - }); - return; - }; - - /** - * @ngdoc function - * @name handlers.loginSuccess - * @propertyOf ngSeed.providers:CSSessionProvider - * @description - * This method redirects the user to the redirect search term if - * it exists. - */ - handlers.loginSuccess = handlers.loginSuccess || function () { - if($location.search().redirect) { - console.log('CSSessionProvider: redirecting to', $location.search().redirect); - $location.path($location.search().redirect); - $location.search(false); - } else { - $location.path('/'); - } - }; - - /** - * @ngdoc function - * @name handlers.loginSuccess - * @propertyOf ngSeed.providers:CSSessionProvider - * @description - * This method redirects the user to the redirect search term if - * it exists. - */ - handlers.logoutSuccess = handlers.logoutSuccess || function () { - console.log('CSSessionProvider: redirecting to /'); - $location.path('/'); - }; - - /** - * @ngdoc function - * @name handlers.locationChange - * @propertyOf ngSeed.providers:CSSessionProvider - * @description - * This method takes a user navigating, does a quick auth check - * and if everything is alright proceeds. - */ - handlers.locationChange = handlers.locationChange || function (event, next, current) { - next = '/' + next.split('/').splice(3).join('/').split('?')[0]; - if(currentUser === null || !currentUser.id){ - var route = $route.routes[next] || false; - console.log('CSSessionProvider: Guest access to', next); - console.log('CSSessionProvider:', next, 'is', route.public ? 'public' : 'private'); - if(route && !route.public) { - $rootScope.$broadcast('CSSessionProvider:loginStart'); - handlers.loginStart(next.substr(1)); - } - } else { - console.log('CSSessionProvider: proceeding to load', next); - } - }; - - /** - * @description - * $rootScope hookups - */ - $rootScope.$on('$locationChangeStart', function (event, next, current) { - if(!$route.current) { - console.log('CSSessionProvider: Welcome newcomer!'); - console.log('CSSessionProvider: Checking your session...'); - sessionService.getCurrentUser().then(function (user) { - currentUser = user; - console.log('CSSessionProvider: we got', user); - if (angular.isFunction(handlers.locationChange)) { - handlers.locationChange(event, next, current); - } - }, function (err) { - console.log('CSSessionProvider: request failed'); - console.log('CSSessionProvider: proceeding as guest.'); - if(angular.isFunction(handlers.locationChange)) { - handlers.locationChange(event, next, current); - } - }); - } else { - if(angular.isFunction(handlers.locationChange)) { - handlers.locationChange(event, next, current); - } - } - }); - - $rootScope.$on('CSSessionProvider:loginSuccess', function () { - if(angular.isFunction(handlers.loginSuccess)) { - handlers.loginSuccess(); - } - }); - - $rootScope.$on('CSSessionProvider:logoutSuccess', function () { - if(angular.isFunction(handlers.logoutSuccess)) { - handlers.logoutSuccess(); - } - }); - - $rootScope.$on('CSSessionProvider:loginRequired', function () { - console.log('CSSessionProvider: login was required'); - $location.path('/login'); - }); - - return { - /** - * @name getCurrentUser - * @ngdoc function - * @methodOf ngSeed.services:CSSession - * @return {Object} the current user - */ - getCurrentUser: function () { - return currentUser; - }, - - /** - * @name isLoggedIn - * @ngdoc function - * @methodOf ngSeed.services:CSSession - * @return {Boolean} true or false if there is or not a current user - */ - isLoggedIn: function () { - return !!currentUser; - // return (currentUser === null || !currentUser.id) ? false : true; - }, - - - /** - * @name login - * @ngdoc function - * @methodOf ngSeed.services:CSSession - * @param {Object} credentials the credentials to be passed to the login service - * @return {Promise} the promise your login service returns on login - */ - login: function (credentials) { - return sessionService.login(credentials).then(function (user) { - if(user.id) { - currentUser = user; - $rootScope.$broadcast('CSSessionProvider:loginSuccess'); - } else { - $rootScope.$broadcast('CSSessionProvider:loginFailure'); - } - }, function() { - currentUser = null; - $rootScope.$broadcast('CSSessionProvider:loginFailure'); - }); - }, - - /** - * @name logout - * @ngdoc function - * @methodOf ngSeed.services:CSSession - * @return {Promise} the promise your login service returns on logout - */ - logout: function () { - $rootScope.$broadcast('CSSessionProvider:logoutSuccess'); - if(currentUser && currentUser.id) { - return sessionService.logout().then(function () { - currentUser = null; - }); - } - }, - - /** - * @name authenticate - * @ngdoc function - * @methodOf ngSeed.services:CSSession - * @return {Promise} the promise your login service returns on logout - */ - authenticate: function (user) { - if(!user || !user.id){ - throw new Error('Unable to authenticate with', user); - } - currentUser = user; - $rootScope.$broadcast('CSSessionProvider:loginSuccess'); - $rootScope.$broadcast('CSSessionProvider:authenticated'); - } - - - }; - - }], - - /** - * @ngdoc function - * @methodOf ngSeed.providers:CSSessionProvider - * @name setSessionService - * @param {String} usr the user service name - */ - setSessionService: function (serviceName) { - if (!angular.isString(serviceName)) { - throw new Error('CSSessionProvider: setSessionService expects a string to use $injector upon instantiation'); - } - sessionServiceName = serviceName; - }, - - /** - * @ngdoc function - * @methodOf ngSeed.providers:CSSessionProvider - * @name setHandler - * @param {String} key the handler name - * @param {Function} foo the handler function - * @description - * Replaces one of the default handlers. - */ - setHandler: function (key, foo) { - if( key.substr(0, 6) !== 'handle' ) { - throw new Error('CSSessionProvider: Expecting a handler name that starts with \'handle\'.'); - } - - if ( !handlers.hasOwnProperty(key) ) { - throw new Error('CSSessionProvider: handle name "' + key + '" is not a valid property.'); - } - - if (!angular.isFunction(foo)) { - throw new Error('CSSessionProvider: foo is not a function.'); - } - - handlers[key] = foo; - } - }; - } - - ]); - -}); diff --git a/app/modules/cs_session/scripts/cs_session_service.js b/app/modules/cs_session/scripts/cs_session_service.js deleted file mode 100644 index b4cda5f..0000000 --- a/app/modules/cs_session/scripts/cs_session_service.js +++ /dev/null @@ -1,46 +0,0 @@ -define(['angular', 'underscore', '../module'], function (ng, _) { - 'use strict'; - - ng.module('cs_session.services') - .service('CSSessionService', [ - '$http', - '$q', - function ($http, $q) { - - return { - - login: function (credentials) { - return $http.post('/auth/signIn', credentials) - .then(function(response){ - return response.data; - }); - }, - - logout: function () { - return $http.get('/auth/signOut'); - }, - - getCurrentUser: function () { - var def = $q.defer(); - - $http.get('/auth/session') - .then(function (response) { - if(_(response.data).has('id')) { - def.resolve(response.data); - } else { - def.reject(); - } - }, function (err) { - def.reject(err); - }); - - return def.promise; - } - - }; - - } - - ]); - -}); diff --git a/app/modules/cs_session/views/login.html b/app/modules/cs_session/views/login.html deleted file mode 100644 index 40e5558..0000000 --- a/app/modules/cs_session/views/login.html +++ /dev/null @@ -1,11 +0,0 @@ -
-
- - -
-
- - -
- -
From f89de19493ea2287a95df7d0d5295e71f4d2f356 Mon Sep 17 00:00:00 2001 From: Richard Gustin Date: Fri, 29 Aug 2014 16:26:38 +1000 Subject: [PATCH 08/14] feat(roles): New role module for ng-seed v1.2 --- .../controllers/PermissionEditController.js | 40 +++ .../controllers/PermissionListController.js | 39 +++ .../roles/controllers/RoleEditController.js | 41 +++ .../roles/controllers/RoleListController.js | 37 +++ app/modules/roles/main.js | 23 ++ app/modules/roles/models/PermissionModel.js | 18 ++ app/modules/roles/models/RoleModel.js | 18 ++ app/modules/roles/module.js | 76 +++++ .../roles/providers/RoleHelpersProvider.js | 272 ++++++++++++++++++ .../roles/services/PermissionService.js | 24 ++ app/modules/roles/services/RoleService.js | 24 ++ app/modules/roles/views/permission/form.html | 85 ++++++ app/modules/roles/views/permission/list.html | 20 ++ .../roles/views/permission/tableActions.html | 23 ++ app/modules/roles/views/role/form.html | 104 +++++++ app/modules/roles/views/role/list.html | 20 ++ .../roles/views/role/tableActions.html | 23 ++ app/modules/users/main.js | 9 - app/modules/users/module.js | 35 --- app/modules/users/scripts/users_controller.js | 17 -- app/modules/users/views/index.html | 26 -- 21 files changed, 887 insertions(+), 87 deletions(-) create mode 100644 app/modules/roles/controllers/PermissionEditController.js create mode 100644 app/modules/roles/controllers/PermissionListController.js create mode 100644 app/modules/roles/controllers/RoleEditController.js create mode 100644 app/modules/roles/controllers/RoleListController.js create mode 100644 app/modules/roles/main.js create mode 100644 app/modules/roles/models/PermissionModel.js create mode 100644 app/modules/roles/models/RoleModel.js create mode 100644 app/modules/roles/module.js create mode 100644 app/modules/roles/providers/RoleHelpersProvider.js create mode 100644 app/modules/roles/services/PermissionService.js create mode 100644 app/modules/roles/services/RoleService.js create mode 100644 app/modules/roles/views/permission/form.html create mode 100644 app/modules/roles/views/permission/list.html create mode 100644 app/modules/roles/views/permission/tableActions.html create mode 100644 app/modules/roles/views/role/form.html create mode 100644 app/modules/roles/views/role/list.html create mode 100644 app/modules/roles/views/role/tableActions.html delete mode 100644 app/modules/users/main.js delete mode 100644 app/modules/users/module.js delete mode 100644 app/modules/users/scripts/users_controller.js delete mode 100644 app/modules/users/views/index.html diff --git a/app/modules/roles/controllers/PermissionEditController.js b/app/modules/roles/controllers/PermissionEditController.js new file mode 100644 index 0000000..35f68d9 --- /dev/null +++ b/app/modules/roles/controllers/PermissionEditController.js @@ -0,0 +1,40 @@ +define( [ 'angular', '../module' ], function( ng ) { + 'use strict'; + + ng + .module( 'roles.controllers' ) + .controller( 'PermissionEditController', function( $scope, Helpers, Messenger, PermissionService, $modalInstance, permission, roles ) { + $scope.helpers = Helpers; + + $scope.permission = permission; + $scope.roles = roles; + + $scope.save = function() { + var promise; + + if ( this.form && this.form.$invalid ) { + Messenger.warn( 'Fix form errors and try again.' ); + return; + } + + if ( !!$scope.permission.id ) { + promise = $scope.permission.$save(); + } else { + promise = PermissionService.create( $scope.permission ); + } + + promise + .then( function() { + Messenger.success( 'Permission ' + $scope.permission.action + ' successfully ' + ( !!$scope.permission.id ? 'updated.' : 'created.' ) ); + $modalInstance.close( $scope ); + }) + .catch( function( err ) { + Messenger.error( 'Unable to ' + ( !!$scope.permission.id ? 'update' : 'create' ) + ' permission ' + $scope.permission.action + ' due to error (' + err + ')' ); + }); + }; + + $scope.cancel = function () { + $modalInstance.dismiss( 'cancel' ); + }; + }); +}); diff --git a/app/modules/roles/controllers/PermissionListController.js b/app/modules/roles/controllers/PermissionListController.js new file mode 100644 index 0000000..2d123d8 --- /dev/null +++ b/app/modules/roles/controllers/PermissionListController.js @@ -0,0 +1,39 @@ +define( [ 'angular', '../module' ], function( ng ) { + 'use strict'; + + ng + .module( 'roles.controllers' ) + .controller( 'PermissionListController', function( $scope, Helpers ) { + $scope.welcome = 'Viewing all Permissions available in your account, you can define your own custom ones by clicking the "Add Permission" Button.'; + $scope.helpers = Helpers; + $scope.actionsTemplate = '/modules/roles/views/permission/tableActions.html'; + + $scope.sorting = { action: 'asc' }; + + $scope.columns = [ + // { + // name: 'id', + // title: 'ID/#', + // filter: true, + // filterType: 'text', + // glyph: 'barcode', + // width: 40, + // }, + { + name: 'action', + title: 'Action Name', + filter: true, + filterType: 'text', + glyph: 'user' + }, + { + name: 'description', + title: 'Description', + filter: true, + filterType: 'text', + glyph: 'list' + } + ]; + + }); +}); diff --git a/app/modules/roles/controllers/RoleEditController.js b/app/modules/roles/controllers/RoleEditController.js new file mode 100644 index 0000000..a0f552e --- /dev/null +++ b/app/modules/roles/controllers/RoleEditController.js @@ -0,0 +1,41 @@ +define( [ 'angular', '../module' ], function( ng ) { + 'use strict'; + + ng + .module( 'roles.controllers' ) + .controller( 'RoleEditController', function( $scope, Helpers, Messenger, RoleService, $modalInstance, role, users, permissions ) { + $scope.helpers = Helpers; + + $scope.role = role; + $scope.users = users; + $scope.permissions = permissions; + + $scope.save = function() { + var promise; + + if ( this.form && this.form.$invalid ) { + Messenger.warn( 'Fix form errors and try again.' ); + return; + } + + if ( !!$scope.role.id ) { + promise = $scope.role.$save(); + } else { + promise = RoleService.create( $scope.role ); + } + + promise + .then( function() { + Messenger.success( 'Role ' + $scope.role.name + ' successfully ' + ( !!$scope.role.id ? 'updated.' : 'created.' ) ); + $modalInstance.close( $scope ); + }) + .catch( function( err ) { + Messenger.error( 'Unable to ' + ( !!$scope.role.id ? 'update' : 'create' ) + ' role ' + $scope.role.name + ' due to error (' + err + ')' ); + }); + }; + + $scope.cancel = function () { + $modalInstance.dismiss( 'cancel' ); + }; + }); +}); diff --git a/app/modules/roles/controllers/RoleListController.js b/app/modules/roles/controllers/RoleListController.js new file mode 100644 index 0000000..524c122 --- /dev/null +++ b/app/modules/roles/controllers/RoleListController.js @@ -0,0 +1,37 @@ +define( [ 'angular', '../module' ], function( ng ) { + 'use strict'; + + ng + .module( 'roles.controllers' ) + .controller( 'RoleListController', function( $scope, Helpers ) { + $scope.welcome = 'This page lists all of the Roles available in your account, you can add as many as you want'; + $scope.helpers = Helpers; + $scope.actionsTemplate = '/modules/roles/views/role/tableActions.html'; + + $scope.columns = [ + // { + // name: 'id', + // title: 'ID/#', + // filter: true, + // filterType: 'text', + // glyph: 'barcode', + // width: 40, + // }, + { + name: 'name', + title: 'Name', + filter: true, + filterType: 'text', + glyph: 'user' + }, + { + name: 'description', + title: 'Description', + filter: true, + filterType: 'text', + glyph: 'list' + } + ]; + + }); +}); diff --git a/app/modules/roles/main.js b/app/modules/roles/main.js new file mode 100644 index 0000000..a5f846a --- /dev/null +++ b/app/modules/roles/main.js @@ -0,0 +1,23 @@ +define([ + './module', + + // Providers + './providers/RoleHelpersProvider', + + // Controllers + './controllers/RoleListController', + './controllers/RoleEditController', + './controllers/PermissionListController', + './controllers/PermissionEditController', + + + // Directives + + // Models + './models/RoleModel', + './models/PermissionModel', + + // Services + './services/RoleService', + './services/PermissionService' +], function() {}); diff --git a/app/modules/roles/models/PermissionModel.js b/app/modules/roles/models/PermissionModel.js new file mode 100644 index 0000000..f35db66 --- /dev/null +++ b/app/modules/roles/models/PermissionModel.js @@ -0,0 +1,18 @@ +define( [ 'angular', '../module' ], function( ng ) { + 'use strict'; + + ng + .module( 'roles.models' ) + .factory( 'PermissionModel', function( $rootScope, Session, ResourceFactory ) { + var defaultParams = { + id: '@id' + }; + + $rootScope.$watch( Session.getCurrentUser, function( user ) { + defaultParams.accountId = user ? user.AccountId : null; + }); + + return new ResourceFactory( '/account/:accountId/permissions', defaultParams, {} ); + }); + +}); diff --git a/app/modules/roles/models/RoleModel.js b/app/modules/roles/models/RoleModel.js new file mode 100644 index 0000000..9ff1af6 --- /dev/null +++ b/app/modules/roles/models/RoleModel.js @@ -0,0 +1,18 @@ +define( [ 'angular', '../module' ], function( ng ) { + 'use strict'; + + ng + .module( 'roles.models' ) + .factory( 'RoleModel', function( $rootScope, Session, ResourceFactory ) { + var defaultParams = { + id: '@id' + }; + + $rootScope.$watch( Session.getCurrentUser, function( user ) { + defaultParams.accountId = user ? user.AccountId : null; + }); + + return new ResourceFactory( '/account/:accountId/roles', defaultParams, {} ); + }); + +}); diff --git a/app/modules/roles/module.js b/app/modules/roles/module.js new file mode 100644 index 0000000..d84ffbd --- /dev/null +++ b/app/modules/roles/module.js @@ -0,0 +1,76 @@ +define( [ 'angular', 'underscore' ], function( ng, _ ) { + 'use strict'; + + ng.module( 'roles.providers', [] ); + ng.module( 'roles.controllers', [] ); + ng.module( 'roles.models', [] ); + ng.module( 'roles.services', [] ); + ng.module( 'roles.directives', [] ); + ng.module( 'roles.filters', [] ); + + var module = ng.module( 'roles', [ + 'cs_common', + 'cs_messenger', + 'cs_modal', + 'auth', + 'ui.bootstrap', + 'ui', + 'roles.providers', + 'roles.controllers', + 'roles.models', + 'roles.services', + 'roles.directives', + 'roles.filters' + ]); + + module.config( function( $routeProvider, TemplateProvider, HelpersProvider, NavbarProvider ) { + var settingsMenu; + + // Create helpers for this module + HelpersProvider.extend( 'RoleHelpers' ); + + // Define menu structure for this module + if ( ( settingsMenu = _.findWhere( NavbarProvider.getNavbar().right, { label: 'Settings' } ) ) !== undefined ) { + settingsMenu.subMenu.push({ + label: 'Roles', + href: '/settings/roles', + class: 'fa-group', + requiresSignIn: true, + order: 5 + }); + + settingsMenu.subMenu.push({ + label: 'Permissions', + href: '/settings/permissions', + class: 'fa-legal', + requiresSignIn: true, + order: 6 + }); + } + + // Define routes provided by this module + $routeProvider + .when( '/settings/roles', { + templateUrl: TemplateProvider.view( 'roles', 'role/list' ), + controller: 'RoleListController', + public: false + }) + .when( '/settings/role/:id', { + templateUrl: TemplateProvider.view( 'roles', 'role/form' ), + controller: 'RoleEditController', + public: false + }) + .when( '/settings/permissions', { + templateUrl: TemplateProvider.view( 'roles', 'permission/list' ), + controller: 'PermissionListController', + public: false + }) + .when( '/settings/permission/:id', { + templateUrl: TemplateProvider.view( 'roles', 'permission/form' ), + controller: 'PermissionEditController', + public: false + }); + }); + + return module; +}); diff --git a/app/modules/roles/providers/RoleHelpersProvider.js b/app/modules/roles/providers/RoleHelpersProvider.js new file mode 100644 index 0000000..39b3ca5 --- /dev/null +++ b/app/modules/roles/providers/RoleHelpersProvider.js @@ -0,0 +1,272 @@ +define( [ 'angular', '../module' ], function( ng ) { + 'use strict'; + + ng + .module( 'roles.providers' ) + .provider( 'RoleHelpers', [ function() { + var helpers = {} + , inheritedProviders = []; + + return { + $get: function( $injector, $rootScope, $timeout, $location ) { + var Messenger = $injector.has( 'Messenger' ) ? $injector.get( 'Messenger' ) : $injector.get( '$log' ) + , RoleService = $injector.has( 'RoleService' ) ? $injector.get( 'RoleService' ) : false + , PermissionService = $injector.has( 'PermissionService' ) ? $injector.get( 'PermissionService' ) : false + , UserService = $injector.has( 'UserService' ) ? $injector.get( 'UserService' ) : false + , ModalFactory = $injector.has( 'ModalFactory' ) ? $injector.get( 'ModalFactory' ) : false + , Session = $injector.get( 'Session' ); + + if ( inheritedProviders ) { + inheritedProviders.forEach( function( inheritedProvider ) { + var provider = $injector.get( inheritedProvider ); + if ( !provider ) { + throw new Error( 'Unable to inject "' + inheritedProvider + '"' ); + } + ng.extend( helpers, provider ); + }); + } + + helpers.openRoleModal = function( role ) { + ModalFactory.open( role, '/modules/roles/views/role/form.html', { + controller: 'RoleEditController', + resolve: { + role: function() { + if ( typeof role === 'object' ) { + return role; + } else if ( role !== false && role !== undefined ) { + return RoleService + .get( { id: role, _include: 'Permission,User' } ) + .then( function( role ) { + + role.users = role.users.map( function( user ) { + return user.id; + }); + role.permissions = role.permissions.map( function( role ) { + return role.id; + }); + + return role; + }); + } else { + return { permissions: [], users: [] }; + } + }, + + users: function() { + return UserService + .list() + .then( function( users ) { + return users; + }) + .catch( function( err ) { + Messenger.error( 'Unable to resolve "users" to a value because of (' + err + ')' ); + }); + }, + + permissions: function() { + return PermissionService + .list() + .then( function( permissions ) { + return permissions; + }) + .catch( function( err ) { + Messenger.error( 'Unable to resolve "permissions" to a value because of (' + err + ')' ); + }); + } + } + }); + }; + + helpers.openDeleteRoleModal = function( role ) { + ModalFactory.open( {}, '/modules/cs_modal/views/confirmModal.html', { + resolve: { + title: function() { + return 'Delete the "' + role.name + '" Role?'; + }, + warning: function() { + return { + title: 'Warning:', + message: 'If you delete a role that has users their account\'s may no longer work, you will need to assign them each a new role' + }; + }, + message: function() { + return 'Are you sure you want to delete this role?'; + }, + confirm: function() { + return 'Yes, delete this role'; + }, + helpers: function() { + return helpers; + } + }, + methods: { + submit: function() { + var $scope = this.$parent; + + role + .$destroy() + .then( function() { + $scope.next(); + }) + .catch( function( err ) { + Messenger.error( 'Unable to delete role because of error (' + err + ')' ); + }); + } + } + }, + function() { + + }); + }; + + helpers.openPermissionModal = function( permission ) { + ModalFactory.open( permission, '/modules/roles/views/permission/form.html', { + controller: 'PermissionEditController', + resolve: { + permission: function() { + if ( typeof permission === 'object' ) { + return permission; + } else if ( permission !== false && permission !== undefined ) { + return PermissionService + .get( { id: permission, _include: 'Role' } ) + .then( function( permission ) { + + permission.roles = permission.roles.map( function( role ) { + return role.id; + }); + + return permission; + }); + } else { + return { roles: [] }; + } + }, + + roles: function() { + return RoleService + .list() + .then( function( roles ) { + return roles; + }) + .catch( function( err ) { + Messenger.error( 'Unable to resolve "roles" to a value because of (' + err + ')' ); + }); + } + } + }); + }; + + helpers.openDeletePermissionModal = function( permission ) { + ModalFactory.open( {}, '/modules/cs_modal/views/confirmModal.html', { + resolve: { + title: function() { + return 'Delete the "' + permission.action + '" Permission?'; + }, + warning: function() { + return { + title: 'Warning:', + message: 'If you delete a permission that has been assigned to a role and subsequently users their account\'s may no longer work.' + }; + }, + message: function() { + return 'Are you sure you want to delete this permission?'; + }, + confirm: function() { + return 'Yes, delete this permission'; + }, + helpers: function() { + return helpers; + } + }, + methods: { + submit: function() { + var $scope = this.$parent; + + permission + .$destroy() + .then( function() { + $scope.next(); + }) + .catch( function( err ) { + Messenger.error( 'Unable to delete permission because of error (' + err + ')' ); + }); + } + } + }, + function() { + + }); + }; + + helpers.ensurePermission = function( permission ) { + var hasPerm; + var scope = $rootScope.$new(true); + var clearWatcher = scope.$watch('user', function(user){ + if(!user){ + return; + } + + if (user && user.role && user.role.permissions) { + user.role.permissions.every(function(perm) { + if (perm.action === permission) { + hasPerm = true; + return false; + } + return true; + }); + } + + if (!hasPerm) { + $timeout(function(){ + Messenger.error('You do not have the correct permissions'); + $location.url('/'); + }); + } + $timeout(function(){ + clearWatcher(); + scope.$destroy(); + }); + + }); + + scope.user = Session.getCurrentUser(); + }; + + helpers.hasPermission = function( requiredPermission, user ) { + var currentUser = user || Session.getCurrentUser() + , role = currentUser && currentUser.role + , hasPerm = false; + + if ( role && role.permissions ) { + role.permissions.every( function( permission ) { + if (permission.action === requiredPermission) { + hasPerm = true; + return false; + } + + return true; + }); + } + + return hasPerm; + }; + + return helpers; + }, + + /** + * @ngdoc function + * @methodOf ngSeed.providers:CSAccountProvider + * @name setAccountService + * @param {String} serviceName the account service name + */ + extend: function( providerName ) { + if ( typeof providerName !== 'string' ) { + throw new Error( 'Helpers: extend method expects a string (name of the helpers provider)' ); + } + inheritedProviders.push( providerName ); + } + }; + + }]); +}); diff --git a/app/modules/roles/services/PermissionService.js b/app/modules/roles/services/PermissionService.js new file mode 100644 index 0000000..3654857 --- /dev/null +++ b/app/modules/roles/services/PermissionService.js @@ -0,0 +1,24 @@ +define( [ 'angular', '../module' ], function( ng ) { + 'use strict'; + + ng + .module( 'roles.services' ) + .factory( 'PermissionService', function( PermissionModel ) { + return { + model: PermissionModel, + + list: function( findOptions ) { + return PermissionModel.list( findOptions ).$promise; + }, + + get: function( findOptions ) { + return PermissionModel.get( findOptions ).$promise; + }, + + create: function( data ) { + return PermissionModel.create( data ).$promise; + } + }; + }); + +}); diff --git a/app/modules/roles/services/RoleService.js b/app/modules/roles/services/RoleService.js new file mode 100644 index 0000000..ce49ab8 --- /dev/null +++ b/app/modules/roles/services/RoleService.js @@ -0,0 +1,24 @@ +define( [ 'angular', '../module' ], function( ng ) { + 'use strict'; + + ng + .module( 'roles.services' ) + .factory( 'RoleService', function( RoleModel ) { + return { + model: RoleModel, + + list: function( findOptions ) { + return RoleModel.list( findOptions ).$promise; + }, + + get: function( findOptions ) { + return RoleModel.get( findOptions ).$promise; + }, + + create: function( data ) { + return RoleModel.create( data ).$promise; + } + }; + }); + +}); diff --git a/app/modules/roles/views/permission/form.html b/app/modules/roles/views/permission/form.html new file mode 100644 index 0000000..ef7c15b --- /dev/null +++ b/app/modules/roles/views/permission/form.html @@ -0,0 +1,85 @@ + + + + + diff --git a/app/modules/roles/views/permission/list.html b/app/modules/roles/views/permission/list.html new file mode 100644 index 0000000..1aacf0b --- /dev/null +++ b/app/modules/roles/views/permission/list.html @@ -0,0 +1,20 @@ +
+
+

Permissions
{{welcome}}

+
+
+ +
+ +
+ +
+
diff --git a/app/modules/roles/views/permission/tableActions.html b/app/modules/roles/views/permission/tableActions.html new file mode 100644 index 0000000..e53854d --- /dev/null +++ b/app/modules/roles/views/permission/tableActions.html @@ -0,0 +1,23 @@ +
+ + + +
diff --git a/app/modules/roles/views/role/form.html b/app/modules/roles/views/role/form.html new file mode 100644 index 0000000..f7ef4b7 --- /dev/null +++ b/app/modules/roles/views/role/form.html @@ -0,0 +1,104 @@ + + + + + diff --git a/app/modules/roles/views/role/list.html b/app/modules/roles/views/role/list.html new file mode 100644 index 0000000..11771fa --- /dev/null +++ b/app/modules/roles/views/role/list.html @@ -0,0 +1,20 @@ +
+
+

Roles
{{welcome}}

+
+
+ +
+ +
+ +
+
diff --git a/app/modules/roles/views/role/tableActions.html b/app/modules/roles/views/role/tableActions.html new file mode 100644 index 0000000..69e77a1 --- /dev/null +++ b/app/modules/roles/views/role/tableActions.html @@ -0,0 +1,23 @@ +
+ + + +
diff --git a/app/modules/users/main.js b/app/modules/users/main.js deleted file mode 100644 index 5d0f264..0000000 --- a/app/modules/users/main.js +++ /dev/null @@ -1,9 +0,0 @@ -define([ - 'angular', - './module', - 'cs_common', - - // Controllers - './scripts/users_controller' - -], function() {}); diff --git a/app/modules/users/module.js b/app/modules/users/module.js deleted file mode 100644 index 4f536ae..0000000 --- a/app/modules/users/module.js +++ /dev/null @@ -1,35 +0,0 @@ -define(['angular'], function (ng) { - 'use strict'; - - ng.module('users.providers', []); - ng.module('users.controllers', []); - ng.module('users.services', []); - - var module = ng.module('users', [ - 'cs_common', - 'users.providers', - 'users.controllers', - 'users.services' - ]); - - module.config([ - '$routeProvider', - 'CSTemplateProvider', - function ($routeProvider, CSTemplateProvider) { - - CSTemplateProvider.setPath('/modules/users/views'); - - $routeProvider - .when('/users', { - templateUrl: CSTemplateProvider.view('index'), - controller: 'UsersController', - public: false - }); - - } - - ]); - - return module; - -}); diff --git a/app/modules/users/scripts/users_controller.js b/app/modules/users/scripts/users_controller.js deleted file mode 100644 index 297ff36..0000000 --- a/app/modules/users/scripts/users_controller.js +++ /dev/null @@ -1,17 +0,0 @@ -define(['angular', '../module'], function (ng) { - 'use strict'; - - ng.module('users.controllers') - .controller('UsersController', [ - '$scope', - 'CSAccountService', - function ($scope, CSAccountService) { - $scope.welcome = 'This is a private area.'; - $scope.users = []; - CSAccountService.list().then(function (users) { - $scope.users = users; - }); - } - - ]); -}); diff --git a/app/modules/users/views/index.html b/app/modules/users/views/index.html deleted file mode 100644 index dd4a97c..0000000 --- a/app/modules/users/views/index.html +++ /dev/null @@ -1,26 +0,0 @@ -

Users

-

{{welcome}}

- - - - - - - - - - - - - - - - - - - - - - - -
IDUsernameEmailRegistered On
There are currently no registered users
{{user.id}}{{user.username}}{{user.email}}{{user.createdAt.split('T')[0]}}
From 02e0df58a1e7a7e6106b11bc438444a9ad40a16d Mon Sep 17 00:00:00 2001 From: Richard Gustin Date: Fri, 29 Aug 2014 16:26:56 +1000 Subject: [PATCH 09/14] refactor(require): Main js file cleaned up for 1.2 --- app/modules/main.js | 123 +++++++++++++++++++++++++++++++++----------- 1 file changed, 93 insertions(+), 30 deletions(-) diff --git a/app/modules/main.js b/app/modules/main.js index ec0853b..c7a5798 100644 --- a/app/modules/main.js +++ b/app/modules/main.js @@ -1,66 +1,129 @@ require.config({ baseUrl: 'modules', packages: [ - 'application', - 'cs_account', + 'app', 'cs_common', - 'cs_session', - 'users' + 'cs_messenger', + 'cs_modal', + 'auth', + 'roles' ], paths: { - angular: '../components/angular/angular', - jquery: '../components/jquery/jquery', - underscore: '../components/underscore/underscore', - ngCookies: '../components/angular-cookies/angular-cookies', - ngResource: '../components/angular-resource/angular-resource', - ngRoute: '../components/angular-route/angular-route', - ngSanitize: '../components/angular-sanitize/angular-sanitize', - 'http-auth-interceptor': '../components/angular-http-auth/src/http-auth-interceptor', - bootstrap: '../scripts/bootstrap' + angular: '../components/angular/angular', + ngCookies: '../components/angular-cookies/angular-cookies', + ngResource: '../components/angular-resource/angular-resource', + ngRoute: '../components/angular-route/angular-route', + ngSanitize: '../components/angular-sanitize/angular-sanitize', + ngTable: '../components/ng-table/ng-table', + ngTableResizableColumns: '../components/ng-table-resizable-columns/ng-table-resizable-columns.src', + ngUi: '../components/angular-ui/build/angular-ui', + ngUiBootstrap: '../components/angular-bootstrap/ui-bootstrap-tpls', + httpAuthInterceptor: '../components/angular-http-auth/src/http-auth-interceptor', + bootstrap: '../scripts/bootstrap', + jquery: '../components/jquery/jquery', + jqueryMinicolors: '../components/jquery-minicolors/jquery.minicolors', + underscore: '../components/underscore/underscore', + selectn: '../components/selectn/selectn', + inflection: '../components/inflection/lib/inflection', + select2: '../components/select2/select2', + 'ui.select2': '../components/angular-ui-select2/src/select2', + moment: '../components/momentjs/moment' }, shim: { angular: { - exports: 'angular' + deps: [ 'jquery' ], + exports: 'angular' }, ngCookies: { - deps: ['angular'] + deps: [ 'angular' ] }, ngResource: { - deps: ['angular'] + deps: [ 'angular' ] }, ngRoute: { - deps: ['angular'] + deps: [ 'angular' ] }, ngSanitize: { - deps: ['angular'] + deps: [ 'angular' ] }, - 'http-auth-interceptor': { - deps: ['angular'] + ngTable: { + deps: [ 'angular' ], + exports: 'ngTable' + }, + ngTableResizableColumns: { + deps: [ 'angular' ], + exports: 'ngTableResizableColumns' + }, + ngUi: { + deps: [ 'angular' ] + }, + ngUiBootstrap: { + deps: [ 'angular' ] + }, + httpAuthInterceptor: { + deps: [ 'angular' ] }, bootstrap: { - deps: ['jquery'] + deps: [ 'jquery' ] }, underscore: { - exports: '_' + exports: '_' + }, + selectn: { + deps: [ 'bootstrap' ] + }, + inflection: { + exports: 'inflection' + }, + select2: { + deps: [ 'jquery' ] + }, + 'ui.select2': { + deps: [ 'angular', 'select2' ] + }, + moment: { + exports: 'moment' + }, + jqueryMinicolors: { + deps: [ 'jquery' ] } - } + }, + waitSeconds: 15 }); require([ 'angular', + 'bootstrap', + 'ngUi', + 'ngUiBootstrap', 'ngRoute', 'ngResource', 'ngSanitize', - 'http-auth-interceptor', - 'bootstrap', + 'ngTable', + 'ngTableResizableColumns', + 'httpAuthInterceptor', + + 'ui.select2', + 'selectn', + 'inflection', + + // CleverStack modules + 'cs_common', + 'cs_messenger', + 'cs_modal', + 'auth', + 'roles', + + // Main app module + 'app', + + // Custom modules - // Init - 'application', -], function (angular) { +], function( angular ) { 'use strict'; - angular.element(document).ready(function () { - angular.bootstrap(document, ['app']); + angular.element( document ).ready( function() { + angular.bootstrap( document, [ 'app' ] ); }); }); From 4666e881b3e42a019960733f4cf5d1e01cea1b56 Mon Sep 17 00:00:00 2001 From: Richard Gustin Date: Fri, 29 Aug 2014 16:27:25 +1000 Subject: [PATCH 10/14] feat(styles): Much better starting point for 1.2 --- app/styles/application.css | 622 ++++++++++++++++++++--- app/styles/less/application.less | 119 ++++- app/styles/less/bootstrap/variables.less | 146 +++--- app/styles/preloader.css | 86 ++++ 4 files changed, 836 insertions(+), 137 deletions(-) create mode 100644 app/styles/preloader.css diff --git a/app/styles/application.css b/app/styles/application.css index 2a3707d..35714f2 100644 --- a/app/styles/application.css +++ b/app/styles/application.css @@ -1,4 +1,5 @@ -/* Use this file to add LESS for your application. */ +/* Bootstrap Theme +────────────────────────────────────────────────────────────*/ .btn-default, .btn-primary, .btn-success, @@ -29,60 +30,60 @@ background-image: none; } .btn-default { - background-image: -webkit-linear-gradient(top, #ffffff 0%, #e0e0e0 100%); - background-image: linear-gradient(to bottom, #ffffff 0%, #e0e0e0 100%); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe0e0e0', GradientType=0); + background-image: -webkit-linear-gradient(top, #fafafa 0%, #dbdbdb 100%); + background-image: linear-gradient(to bottom, #fafafa 0%, #dbdbdb 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffafafa', endColorstr='#ffdbdbdb', GradientType=0); filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); background-repeat: repeat-x; - border-color: #dbdbdb; + border-color: #d6d6d6; text-shadow: 0 1px 0 #fff; border-color: #ccc; } .btn-default:hover, .btn-default:focus { - background-color: #e0e0e0; + background-color: #dbdbdb; background-position: 0 -15px; } .btn-default:active, .btn-default.active { - background-color: #e0e0e0; - border-color: #dbdbdb; + background-color: #dbdbdb; + border-color: #d6d6d6; } .btn-primary { - background-image: -webkit-linear-gradient(top, #428bca 0%, #2d6ca2 100%); - background-image: linear-gradient(to bottom, #428bca 0%, #2d6ca2 100%); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff2d6ca2', GradientType=0); + background-image: -webkit-linear-gradient(top, #8e9bb7 0%, #697a9f 100%); + background-image: linear-gradient(to bottom, #8e9bb7 0%, #697a9f 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff8e9bb7', endColorstr='#ff697a9f', GradientType=0); filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); background-repeat: repeat-x; - border-color: #2b669a; + border-color: #63759b; } .btn-primary:hover, .btn-primary:focus { - background-color: #2d6ca2; + background-color: #697a9f; background-position: 0 -15px; } .btn-primary:active, .btn-primary.active { - background-color: #2d6ca2; - border-color: #2b669a; + background-color: #697a9f; + border-color: #63759b; } .btn-success { - background-image: -webkit-linear-gradient(top, #5cb85c 0%, #419641 100%); - background-image: linear-gradient(to bottom, #5cb85c 0%, #419641 100%); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff419641', GradientType=0); + background-image: -webkit-linear-gradient(top, #8cc63f 0%, #6c9a2e 100%); + background-image: linear-gradient(to bottom, #8cc63f 0%, #6c9a2e 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff8cc63f', endColorstr='#ff6c9a2e', GradientType=0); filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); background-repeat: repeat-x; - border-color: #3e8f3e; + border-color: #66922b; } .btn-success:hover, .btn-success:focus { - background-color: #419641; + background-color: #6c9a2e; background-position: 0 -15px; } .btn-success:active, .btn-success.active { - background-color: #419641; - border-color: #3e8f3e; + background-color: #6c9a2e; + border-color: #66922b; } .btn-warning { background-image: -webkit-linear-gradient(top, #f0ad4e 0%, #eb9316 100%); @@ -145,20 +146,20 @@ } .dropdown-menu > li > a:hover, .dropdown-menu > li > a:focus { - background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%); - background-image: linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%); + background-image: -webkit-linear-gradient(top, #8cc63f 0%, #7fb636 100%); + background-image: linear-gradient(to bottom, #8cc63f 0%, #7fb636 100%); background-repeat: repeat-x; - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0); - background-color: #e8e8e8; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff8cc63f', endColorstr='#ff7fb636', GradientType=0); + background-color: #7fb636; } .dropdown-menu > .active > a, .dropdown-menu > .active > a:hover, .dropdown-menu > .active > a:focus { - background-image: -webkit-linear-gradient(top, #428bca 0%, #357ebd 100%); - background-image: linear-gradient(to bottom, #428bca 0%, #357ebd 100%); + background-image: -webkit-linear-gradient(top, #8cc63f 0%, #7fb636 100%); + background-image: linear-gradient(to bottom, #8cc63f 0%, #7fb636 100%); background-repeat: repeat-x; - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff357ebd', GradientType=0); - background-color: #357ebd; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff8cc63f', endColorstr='#ff7fb636', GradientType=0); + background-color: #7fb636; } .navbar-default { background-image: -webkit-linear-gradient(top, #ffffff 0%, #f8f8f8 100%); @@ -226,18 +227,18 @@ border-color: #9acfea; } .alert-warning { - background-image: -webkit-linear-gradient(top, #fcf8e3 0%, #f8efc0 100%); - background-image: linear-gradient(to bottom, #fcf8e3 0%, #f8efc0 100%); + background-image: -webkit-linear-gradient(top, #ffcccc 0%, #ffa6a6 100%); + background-image: linear-gradient(to bottom, #ffcccc 0%, #ffa6a6 100%); background-repeat: repeat-x; - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fff8efc0', GradientType=0); - border-color: #f5e79e; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffcccc', endColorstr='#ffffa6a6', GradientType=0); + border-color: #ff8080; } .alert-danger { - background-image: -webkit-linear-gradient(top, #f2dede 0%, #e7c3c3 100%); - background-image: linear-gradient(to bottom, #f2dede 0%, #e7c3c3 100%); + background-image: -webkit-linear-gradient(top, #e14031 0%, #ce2d1e 100%); + background-image: linear-gradient(to bottom, #e14031 0%, #ce2d1e 100%); background-repeat: repeat-x; - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffe7c3c3', GradientType=0); - border-color: #dca7a7; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe14031', endColorstr='#ffce2d1e', GradientType=0); + border-color: #ac2619; } .progress { background-image: -webkit-linear-gradient(top, #ebebeb 0%, #f5f5f5 100%); @@ -246,16 +247,16 @@ filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff5f5f5', GradientType=0); } .progress-bar { - background-image: -webkit-linear-gradient(top, #428bca 0%, #3071a9 100%); - background-image: linear-gradient(to bottom, #428bca 0%, #3071a9 100%); + background-image: -webkit-linear-gradient(top, #8cc63f 0%, #71a230 100%); + background-image: linear-gradient(to bottom, #8cc63f 0%, #71a230 100%); background-repeat: repeat-x; - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff3071a9', GradientType=0); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff8cc63f', endColorstr='#ff71a230', GradientType=0); } .progress-bar-success { - background-image: -webkit-linear-gradient(top, #5cb85c 0%, #449d44 100%); - background-image: linear-gradient(to bottom, #5cb85c 0%, #449d44 100%); + background-image: -webkit-linear-gradient(top, #8cc63f 0%, #71a230 100%); + background-image: linear-gradient(to bottom, #8cc63f 0%, #71a230 100%); background-repeat: repeat-x; - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff449d44', GradientType=0); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff8cc63f', endColorstr='#ff71a230', GradientType=0); } .progress-bar-info { background-image: -webkit-linear-gradient(top, #5bc0de 0%, #31b0d5 100%); @@ -283,28 +284,28 @@ .list-group-item.active, .list-group-item.active:hover, .list-group-item.active:focus { - text-shadow: 0 -1px 0 #3071a9; - background-image: -webkit-linear-gradient(top, #428bca 0%, #3278b3 100%); - background-image: linear-gradient(to bottom, #428bca 0%, #3278b3 100%); + text-shadow: 0 -1px 0 #71a230; + background-image: -webkit-linear-gradient(top, #8cc63f 0%, #78ac33 100%); + background-image: linear-gradient(to bottom, #8cc63f 0%, #78ac33 100%); background-repeat: repeat-x; - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff3278b3', GradientType=0); - border-color: #3278b3; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff8cc63f', endColorstr='#ff78ac33', GradientType=0); + border-color: #78ac33; } .panel { -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); } .panel-default > .panel-heading { - background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%); - background-image: linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%); + background-image: -webkit-linear-gradient(top, #ffffff 0%, #f2f2f2 100%); + background-image: linear-gradient(to bottom, #ffffff 0%, #f2f2f2 100%); background-repeat: repeat-x; - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff2f2f2', GradientType=0); } .panel-primary > .panel-heading { - background-image: -webkit-linear-gradient(top, #428bca 0%, #357ebd 100%); - background-image: linear-gradient(to bottom, #428bca 0%, #357ebd 100%); + background-image: -webkit-linear-gradient(top, #8cc63f 0%, #7fb636 100%); + background-image: linear-gradient(to bottom, #8cc63f 0%, #7fb636 100%); background-repeat: repeat-x; - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff357ebd', GradientType=0); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff8cc63f', endColorstr='#ff7fb636', GradientType=0); } .panel-success > .panel-heading { background-image: -webkit-linear-gradient(top, #dff0d8 0%, #d0e9c6 100%); @@ -319,16 +320,16 @@ filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffc4e3f3', GradientType=0); } .panel-warning > .panel-heading { - background-image: -webkit-linear-gradient(top, #fcf8e3 0%, #faf2cc 100%); - background-image: linear-gradient(to bottom, #fcf8e3 0%, #faf2cc 100%); + background-image: -webkit-linear-gradient(top, #ffcccc 0%, #ffb3b3 100%); + background-image: linear-gradient(to bottom, #ffcccc 0%, #ffb3b3 100%); background-repeat: repeat-x; - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fffaf2cc', GradientType=0); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffcccc', endColorstr='#ffffb3b3', GradientType=0); } .panel-danger > .panel-heading { - background-image: -webkit-linear-gradient(top, #f2dede 0%, #ebcccc 100%); - background-image: linear-gradient(to bottom, #f2dede 0%, #ebcccc 100%); + background-image: -webkit-linear-gradient(top, #e14031 0%, #d92f20 100%); + background-image: linear-gradient(to bottom, #e14031 0%, #d92f20 100%); background-repeat: repeat-x; - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffebcccc', GradientType=0); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe14031', endColorstr='#ffd92f20', GradientType=0); } .well { background-image: -webkit-linear-gradient(top, #e8e8e8 0%, #f5f5f5 100%); @@ -339,17 +340,500 @@ -webkit-box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.05), 0 1px 0 rgba(255, 255, 255, 0.1); box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.05), 0 1px 0 rgba(255, 255, 255, 0.1); } +/* Global Styles +────────────────────────────────────────────────────────────*/ +body { + padding-top: 67px; +} +body a { + color: #143468; +} +.jumbotron { + font-size: 14px; + background: none; + border: 1px solid #D7D7D7; +} +.well { + border-radius: 0; + background: none; + border: 0; + border-left: 7px solid #8cc63f; + -webkit-box-shadow: none; + box-shadow: none; +} +.badge { + background: #e4e7e7; + color: #2c333d; + border-radius: 5px; + padding: 5px 8px; +} +.btn-lg { + border-radius: 0; +} +.pagination > .active > a, +.pagination > .active > span, +.pagination > .active > a:hover, +.pagination > .active > span:hover, +.pagination > .active > a:focus, +.pagination > .active > span:focus { + background: #8cc63f; + border-color: #71a230; +} +html > .no-js > div.js-warning { + display: none; +} +/* Success/Error Message Custom Styling +────────────────────────────────────────────────────────────*/ +.has-error .help-block { + font-size: 11px; +} +#app_message { + display: none; + top: 45%; +} +#app_message .alert { + font-size: 13px; + font-weight: bold; + padding: 16px; + border-radius: 2px; +} +#app_message .alert-warning { + color: #000; +} +#app_message .alert-danger { + color: #FFF; +} +/* Form Custom Styling +────────────────────────────────────────────────────────────*/ +.wrapper-form { + max-width: 900px; +} +.form-horizontal label { + font-size: 12px; +} +.select2-container .select2-choices .select2-search-field input, +.select2-container .select2-choice, +.select2-container .select2-choices { + border-radius: 0px; +} +/* Mobile @1200px +────────────────────────────────────────────────────────────*/ +@media (max-width: 1200px) { + +} +/* Desktop @992px +────────────────────────────────────────────────────────────*/ +@media (max-width: 992px) { + +} +/* Mobile @768px +────────────────────────────────────────────────────────────*/ +@media (max-width: 768px) { + +} +/* Mobile @480px +────────────────────────────────────────────────────────────*/ +@media (max-width: 480px) { + +} -/* dynamic drop down submenu styles */ -.navbar-nav ul.navbar-nav { +.pill-tabs li > a { + color: #8cc63f; + cursor: pointer; + font-weight: bold; +} +.pill-tabs li.active > a, +.pill-tabs li.active:hover > a { + background-color: #8cc63f; + color: #FFF; +} +.dropdown-header { + background: none; + color: #BBB; + text-transform: uppercase; + border-top: 1px solid #E2E2E2; + margin-top: 10px; + padding-top: 12px; + padding-bottom: 6px; +} +.dropdown-header:first-child { + margin-top: 0; + border-top: none; +} +.navbar .brand-logo { + height: 45px; + margin: 10px; +} +.navbar { + height: 67px; + background: #ffffff url('../images/nav-main-bg.png') left bottom repeat-x; + -webkit-box-shadow: 0 3px 8px -2px rgba(0, 0, 0, 0.25); + box-shadow: 0 3px 8px -2px rgba(0, 0, 0, 0.25); +} +.navbar .navbar-header .navbar-brand img { + max-height: 36px; + max-width: 160px; +} +.navbar .navbar-text { + margin-top: 22px; + font-size: 12px; +} +.navbar .btn-group { + margin-top: 12px; +} +.navbar .btn-group .btn { + font-size: 12px; +} +.navbar .navbar-toggle { + margin-top: 14px; +} +.navbar .navbar-toggle .icon-bar { + background: #000; +} +.navbar-app { + height: 100%; + width: 92px; + position: fixed; + left: 0px; + top: 67px; + bottom: 0px; + border-right: 1px solid #95969B; + padding: 0; + background: #3f4452 url('../images/nav-app-bg.png') right top repeat-y; + z-index: 1020; +} +.navbar-app ul.nav { list-style: none; + height: 100%; +} +.navbar-app ul.nav > li { + display: block; + border-bottom: 1px solid #42454E; + margin: 0; padding: 0; + font-size: 12px; + background: transparent url('../images/nav-app-item-bg.png') left bottom repeat-x; +} +.navbar-app ul.nav > li > a { + margin: 0; + padding: 16px 5px 14px; + text-align: center; + color: #C0C1C4; + background: transparent; +} +.navbar-app ul.nav > li span { + display: block; + margin-top: 6px; +} +.navbar-app ul.nav > li:hover, +.navbar-app ul.nav > li.open { + background: #353944; +} +.navbar-app ul.nav > li:hover > a, +.navbar-app ul.nav > li.open > a { + color: #FFF; + background: none; } -.navbar-nav ul.navbar-nav a { - padding: 15px; - display: inline-block !important; - text-decoration: none; +.navbar-app ul.nav > li.active > a { + color: #8cc63f; } -.navbar-nav ul.navbar-nav a:hover { - text-decoration: none; +.navbar-app ul.nav > li:last-child { + border-bottom: none; +} +.navbar-app ul.nav li.dropdown { + position: static; +} +.navbar-app ul.nav li.dropdown:hover > ul.dropdown-menu { + display: block; +} +.navbar-app ul.nav .dropdown-menu { + top: -2px; + left: 91px; + border-radius: 0; + border: 0; + width: 188px; + height: 100%; + -webkit-box-shadow: none; + box-shadow: none; + background: #353944; + padding: 12px 0; +} +.navbar-app ul.nav .dropdown-menu > li a { + color: #FFF; + font-size: 12px; + padding: 12px 16px 13px 26px; + background: none; +} +.navbar-app ul.nav .dropdown-menu > li a:hover, +.navbar-app ul.nav .dropdown-menu > li a:active { + background: #3D4049; +} +.navbar-app ul.nav .dropdown-menu .divider { + opacity: 0.12; +} +.content { + padding-bottom: 20px; + margin-left: 97px; + padding-right: 10px; +} +.content .pagination { + margin: 0; +} +.content .add-button { + padding-right: 20px; + margin-top: 10px; +} +.content .clear-filters { + margin-bottom: 5px; + margin-right: 25px; + margin-top: -40px; +} +.content .table-responsive { + margin-left: 28px; + width: 100%; +} +.content .header-area .header-buttons { + margin: 24px 8px 0 0; + position: relative; +} +.content .header-area .header-buttons .btn { + margin-right: 5px; +} +.content .header-area .header-buttons .fa-fw { + text-align: left; +} +.content .header-area .header-buttons .dropdown-menu { + margin-top: 45px; + font-size: 12px; + padding: 10px 0; +} +.content .header-area .header-buttons .dropdown-menu > li > a { + padding: 8px 18px; +} +/* Mobile @1200px +────────────────────────────────────────────────────────────*/ +@media (max-width: 1200px) { + .navbar .navbar-right { + padding-top: 5px; + position: relative; + z-index: 9999; + } + .content { + padding-left: 7px; + } + .content .ng-table .actions { + width: 140px; + margin: 0; + padding: 0; + margin-left: auto; + margin-right: auto; + } + .content .table-responsive { + padding-left: 28px; + padding-right: 28px; + max-width: 1080px; + width: 100%; + } + .content .add-button { + padding-right: 20px; + margin-top: 20px; + } + .content .clear-filters { + margin-bottom: 5px; + margin-right: 25px; + margin-top: -40px; + } +} +/* Desktop @992px +────────────────────────────────────────────────────────────*/ +@media (min-width: 769px) and (max-width: 992px) { + .navbar .navbar-right { + padding-top: 5px; + position: relative; + z-index: 9999; + } + .content { + margin-left: 98px; + } + .content .ng-table .actions { + width: 140px; + margin: 0; + padding: 0; + margin-left: auto; + margin-right: auto; + } + .content .table-responsive { + margin-left: 28px; + max-width: 850px; + width: 100%; + } + .content .add-button { + padding-right: 0; + margin-top: 20px; + } + .content .clear-filters { + margin-bottom: 5px; + margin-right: 0; + margin-top: -40px; + } +} +/* Mobile @768px +────────────────────────────────────────────────────────────*/ +@media (min-width: 481px) and (max-width: 768px) { + .navbar .navbar-right { + padding-top: 5px; + position: relative; + z-index: 9999; + } + .content { + margin-left: 94px; + } + .content .ng-table .actions { + width: 140px; + margin: 0; + padding: 0; + margin-left: auto; + margin-right: auto; + } + .content .table-responsive { + margin-left: 28px; + max-width: 640px; + width: 100%; + } + .content .add-button { + padding-right: 20px; + margin-top: 20px; + } + .content .clear-filters { + margin-bottom: 5px; + margin-right: 20px; + margin-top: -30px; + } +} +/* Mobile @480px +────────────────────────────────────────────────────────────*/ +@media (max-width: 480px) { + body { + padding-top: 50px; + } + .content { + margin-left: 55px; + padding-right: 10px; + padding-bottom: 20px; + font-size: 12px; + } + .content .btn { + padding: 1px 5px; + font-size: 12px; + line-height: 1.5; + border-radius: 3px; + } + .content .ng-table button b span { + display: none; + } + .content .ng-table .actions { + width: 48px; + margin: 0; + padding: 0; + margin-left: auto; + margin-right: auto; + } + .content .table-responsive { + margin-left: 28px; + max-width: 390px; + width: 100%; + } + .content .pull-left h3, + .content .pull-left.h3 { + font-size: 18px; + } + .content .add-button { + padding-right: 0; + margin-top: 20px; + } + .content .clear-filters { + margin-bottom: 5px; + margin-right: 10px; + margin-top: -27px; + } + .content .header-area .header-buttons { + position: absolute; + top: 0; + right: 12px; + margin-top: 12px; + } + .navbar { + height: 47px; + } + .navbar .navbar-toggle { + margin-top: 5px; + } + .navbar .brand-logo { + height: 28px; + padding: 0; + padding-top: 5px; + } + .navbar .navbar-right { + padding-top: 5px; + position: relative; + z-index: 9999; + } + .navbar-app { + width: 50px; + top: 50px; + } + .navbar-app ul.nav > li i { + font-size: 1.6em; + } + .navbar-app ul.nav > li span { + display: none; + } +} + +.modal .modal-header { + background: #343844 url('../images/modal-header-bg.png') left top repeat-x; + color: #FFF; +} +.modal .modal-header .close { + color: #FFF; +} +.modal .modal-body { + border-left: 7px solid #8cc63f; +} +.modal .modal-body .container { + width: 560px; +} +.modal .modal-helpertext { + background: #EFEFF1; + border-left: 7px solid #8cc63f; + padding: 20px 20px 20px 30px; + font-size: 12px; +} +.modal-backdrop { + background-image: -webkit-radial-gradient(circle, #ffffff, #343743); + background-image: radial-gradient(circle, #ffffff, #343743); + background-repeat: no-repeat; +} +.modal-backdrop.in { + opacity: 0.6; +} + +/* jQuery Resizable Columns v0.1.0 | http://dobtco.github.io/jquery-resizable-columns/ | Licensed MIT | Built Wed Apr 30 2014 14:24:25 */ +.rc-handle-container { + position: relative; +} +.rc-handle { + position: absolute; + width: 7px; + cursor: ew-resize; + margin-left: -3px; + z-index: 2; +} +table.rc-table-resizing { + cursor: ew-resize; +} +table.rc-table-resizing thead, +table.rc-table-resizing thead > th, +table.rc-table-resizing thead > th > a { + cursor: ew-resize; } diff --git a/app/styles/less/application.less b/app/styles/less/application.less index 052912a..6d1e2b7 100644 --- a/app/styles/less/application.less +++ b/app/styles/less/application.less @@ -1,3 +1,120 @@ -/* Use this file to add LESS for your application. */ +/* Bootstrap Theme +────────────────────────────────────────────────────────────*/ @import "bootstrap/theme.less"; +@import "bootstrap/variables.less"; + + +/* Global Styles +────────────────────────────────────────────────────────────*/ +body { + padding-top : 67px; + + a { + color : @brand-second-color; + } +} +.jumbotron { + font-size : 14px; + background : none; + border : 1px solid #D7D7D7; +} +.well { + border-radius : 0; + background : none; + border : 0; + border-left : @trim-border; + .box-shadow( none ); +} +.badge { + background : #e4e7e7; + color : #2c333d; + border-radius : 5px; + padding : 5px 8px; +} +.btn-lg { + border-radius : 0; +} +.pagination > .active > a, +.pagination > .active > span, +.pagination > .active > a:hover, +.pagination > .active > span:hover, +.pagination > .active > a:focus, +.pagination > .active > span:focus { + background : @trim-color; + border-color : darken( @trim-color, 10%); +} +html > .no-js > div.js-warning { + display : none; +} + +/* Success/Error Message Custom Styling +────────────────────────────────────────────────────────────*/ +.has-error .help-block { + font-size : 11px; +} + +#app_message { + display : none; + top : 45%; + .alert { + font-size : 13px; + font-weight : bold; + padding : 16px; + border-radius : 2px; + } + .alert-warning { + color : #000; + } + .alert-danger { + color : #FFF; + } +} + +/* Form Custom Styling +────────────────────────────────────────────────────────────*/ +.wrapper-form { + max-width : 900px; +} +.form-horizontal { + label { + font-size : 12px; + } +} +.select2-container .select2-choices .select2-search-field input, +.select2-container .select2-choice, .select2-container .select2-choices { + border-radius : 0px; +} + + +/* Mobile @1200px +────────────────────────────────────────────────────────────*/ +@media ( max-width : @screen-lg ) { + .content { + + } +} + +/* Desktop @992px +────────────────────────────────────────────────────────────*/ +@media ( max-width : @screen-md ) { + .content { + + } +} + +/* Mobile @768px +────────────────────────────────────────────────────────────*/ +@media ( max-width : @screen-sm ) { + .content { + + } +} + +/* Mobile @480px +────────────────────────────────────────────────────────────*/ +@media ( max-width : @screen-xs ) { + .content { + + } +} diff --git a/app/styles/less/bootstrap/variables.less b/app/styles/less/bootstrap/variables.less index 8b5f218..ed78f06 100644 --- a/app/styles/less/bootstrap/variables.less +++ b/app/styles/less/bootstrap/variables.less @@ -18,31 +18,48 @@ // Brand colors // ------------------------- -@brand-primary: #428bca; -@brand-success: #5cb85c; +@trim-color: #8CC63F; +@trim-border: 7px solid @trim-color; + +@brand-color: #8CC63F; +@brand-primary: @brand-color; +@brand-secondary: #8CC63F; +@brand-second-color: #143468; + +@brand-success: #8CC63F; @brand-warning: #f0ad4e; @brand-danger: #d9534f; @brand-info: #5bc0de; +// Workflow Colours +// ------------------------- + +@wf-red: #DC1D01; +@wf-yellow: #39B3D7; +@wf-green: #339900; + // Scaffolding // ------------------------- @body-bg: #fff; -@text-color: @gray-dark; +@text-color: #1D1F32; // Links // ------------------------- -@link-color: @brand-primary; +@link-color: #254373; @link-hover-color: darken(@link-color, 15%); // Typography // ------------------------- -@font-family-sans-serif: "Helvetica Neue", Helvetica, Arial, sans-serif; +@font-family-sans-serif: "Lato", "Helvetica Neue", Helvetica, Arial, sans-serif; @font-family-serif: Georgia, "Times New Roman", Times, serif; -@font-family-monospace: Menlo, Monaco, Consolas, "Courier New", monospace; +@font-family-monospace: Monaco, Menlo, Consolas, "Courier New", monospace; + @font-family-base: @font-family-sans-serif; +@font-family-secondary: "Doppio One", "Helvetica Neue", Helvetica, Arial, sans-serif; + @font-size-base: 14px; @font-size-large: ceil(@font-size-base * 1.25); // ~18px @@ -63,7 +80,6 @@ @headings-line-height: 1.1; @headings-color: inherit; - // Iconography // ------------------------- @@ -100,6 +116,7 @@ @caret-width-base: 4px; @caret-width-large: 5px; + // Tables // ------------------------- @@ -120,11 +137,11 @@ @btn-font-weight: normal; @btn-default-color: #333; -@btn-default-bg: #fff; +@btn-default-bg: #FAFAFA; @btn-default-border: #ccc; @btn-primary-color: #fff; -@btn-primary-bg: @brand-primary; +@btn-primary-bg: #8E9BB7; @btn-primary-border: darken(@btn-primary-bg, 5%); @btn-success-color: #fff; @@ -155,12 +172,12 @@ @input-color: @gray; @input-border: #ccc; @input-border-radius: @border-radius-base; -@input-border-focus: #66afe9; +@input-border-focus: lighten(#343944, 50%); @input-color-placeholder: @gray-light; @input-height-base: (@line-height-computed + (@padding-base-vertical * 2) + 2); -@input-height-large: (ceil(@font-size-large * @line-height-large) + (@padding-large-vertical * 2) + 2); +@input-height-large: (floor(@font-size-large * @line-height-large) + (@padding-large-vertical * 2) + 2); @input-height-small: (floor(@font-size-small * @line-height-small) + (@padding-small-vertical * 2) + 2); @legend-color: @gray-dark; @@ -178,17 +195,19 @@ @dropdown-fallback-border: #ccc; @dropdown-divider-bg: #e5e5e5; -@dropdown-link-color: @gray-dark; -@dropdown-link-hover-color: darken(@gray-dark, 5%); -@dropdown-link-hover-bg: #f5f5f5; - -@dropdown-link-active-color: @component-active-color; +@dropdown-link-active-color: #fff; @dropdown-link-active-bg: @component-active-bg; +@dropdown-link-color: @gray-dark; +@dropdown-link-hover-color: #fff; +@dropdown-link-hover-bg: @dropdown-link-active-bg; + @dropdown-link-disabled-color: @gray-light; @dropdown-header-color: @gray-light; +@dropdown-caret-color: #000; + // COMPONENT VARIABLES // -------------------------------------------------- @@ -211,33 +230,25 @@ // -------------------------------------------------- // Extra small screen / phone -// Note: Deprecated @screen-xs and @screen-phone as of v3.0.1 @screen-xs: 480px; -@screen-xs-min: @screen-xs; -@screen-phone: @screen-xs-min; +@screen-phone: @screen-xs; // Small screen / tablet -// Note: Deprecated @screen-sm and @screen-tablet as of v3.0.1 @screen-sm: 768px; -@screen-sm-min: @screen-sm; -@screen-tablet: @screen-sm-min; +@screen-tablet: @screen-sm; // Medium screen / desktop -// Note: Deprecated @screen-md and @screen-desktop as of v3.0.1 @screen-md: 992px; -@screen-md-min: @screen-md; -@screen-desktop: @screen-md-min; +@screen-desktop: @screen-md; // Large screen / wide desktop -// Note: Deprecated @screen-lg and @screen-lg-desktop as of v3.0.1 @screen-lg: 1200px; -@screen-lg-min: @screen-lg; -@screen-lg-desktop: @screen-lg-min; +@screen-lg-desktop: @screen-lg; // So media queries don't overlap when required, provide a maximum -@screen-xs-max: (@screen-sm-min - 1); -@screen-sm-max: (@screen-md-min - 1); -@screen-md-max: (@screen-lg-min - 1); +@screen-xs-max: (@screen-sm - 1); +@screen-sm-max: (@screen-md - 1); +@screen-md-max: (@screen-lg - 1); // Grid system @@ -247,13 +258,8 @@ @grid-columns: 12; // Padding, to be divided by two and applied to the left and right of all columns @grid-gutter-width: 30px; - -// Navbar collapse - -// Point at which the navbar becomes uncollapsed -@grid-float-breakpoint: @screen-sm-min; -// Point at which the navbar begins collapsing -@grid-float-breakpoint-max: (@grid-float-breakpoint - 1); +// Point at which the navbar stops collapsing +@grid-float-breakpoint: @screen-phone; // Navbar @@ -262,13 +268,12 @@ // Basics of a navbar @navbar-height: 50px; @navbar-margin-bottom: @line-height-computed; -@navbar-border-radius: @border-radius-base; -@navbar-padding-horizontal: floor(@grid-gutter-width / 2); -@navbar-padding-vertical: ((@navbar-height - @line-height-computed) / 2); - @navbar-default-color: #777; @navbar-default-bg: #f8f8f8; @navbar-default-border: darken(@navbar-default-bg, 6.5%); +@navbar-border-radius: @border-radius-base; +@navbar-padding-horizontal: floor(@grid-gutter-width / 2); +@navbar-padding-vertical: ((@navbar-height - @line-height-computed) / 2); // Navbar links @navbar-default-link-color: #777; @@ -281,7 +286,7 @@ // Navbar brand label @navbar-default-brand-color: @navbar-default-link-color; -@navbar-default-brand-hover-color: darken(@navbar-default-brand-color, 10%); +@navbar-default-brand-hover-color: darken(@navbar-default-link-color, 10%); @navbar-default-brand-hover-bg: transparent; // Navbar toggle @@ -311,6 +316,13 @@ @navbar-inverse-brand-hover-color: #fff; @navbar-inverse-brand-hover-bg: transparent; +// Inverted navbar search +// Normal navbar needs no special styles or vars +@navbar-inverse-search-bg: lighten(@navbar-inverse-bg, 25%); +@navbar-inverse-search-bg-focus: #fff; +@navbar-inverse-search-border: @navbar-inverse-bg; +@navbar-inverse-search-placeholder-color: #ccc; + // Inverted navbar toggle @navbar-inverse-toggle-hover-bg: #333; @navbar-inverse-toggle-icon-bar-bg: #fff; @@ -327,6 +339,7 @@ @nav-disabled-link-hover-color: @gray-light; @nav-open-link-hover-color: #fff; +@nav-open-caret-border-color: #fff; // Tabs @nav-tabs-border-color: #ddd; @@ -341,9 +354,8 @@ @nav-tabs-justified-active-link-border-color: @body-bg; // Pills -@nav-pills-border-radius: @border-radius-base; @nav-pills-active-link-hover-bg: @component-active-bg; -@nav-pills-active-link-hover-color: @component-active-color; +@nav-pills-active-link-hover-color: #fff; // Pagination @@ -373,29 +385,29 @@ @jumbotron-padding: 30px; @jumbotron-color: inherit; @jumbotron-bg: @gray-lighter; + @jumbotron-heading-color: inherit; -@jumbotron-font-size: ceil(@font-size-base * 1.5); // Form states and alerts // ------------------------- -@state-success-text: #3c763d; +@state-warning-text: #000000; +@state-warning-bg: #FFCCCC; +@state-warning-border: darken(spin(@state-warning-bg, -10), 3%); + +@state-danger-text: #E14031; +@state-danger-bg: #E14031; +@state-danger-border: darken(spin(@state-danger-bg, -10), 3%); + +@state-success-text: #468847; @state-success-bg: #dff0d8; @state-success-border: darken(spin(@state-success-bg, -10), 5%); -@state-info-text: #31708f; +@state-info-text: #3a87ad; @state-info-bg: #d9edf7; @state-info-border: darken(spin(@state-info-bg, -10), 7%); -@state-warning-text: #8a6d3b; -@state-warning-bg: #fcf8e3; -@state-warning-border: darken(spin(@state-warning-bg, -10), 5%); - -@state-danger-text: #a94442; -@state-danger-bg: #f2dede; -@state-danger-border: darken(spin(@state-danger-bg, -10), 5%); - // Tooltips // ------------------------- @@ -496,7 +508,7 @@ @list-group-border-radius: @border-radius-base; @list-group-hover-bg: #f5f5f5; -@list-group-active-color: @component-active-color; +@list-group-active-color: #fff; @list-group-active-bg: @component-active-bg; @list-group-active-border: @list-group-active-bg; @@ -513,7 +525,7 @@ @panel-default-text: @gray-dark; @panel-default-border: #ddd; -@panel-default-heading-bg: #f5f5f5; +@panel-default-heading-bg: #fff; @panel-primary-text: #fff; @panel-primary-border: @brand-primary; @@ -571,7 +583,6 @@ @breadcrumb-bg: #f5f5f5; @breadcrumb-color: #ccc; @breadcrumb-active-color: @gray-light; -@breadcrumb-separator: "/"; // Carousel @@ -592,8 +603,8 @@ // Close // ------------------------ -@close-font-weight: bold; @close-color: #000; +@close-font-weight: bold; @close-text-shadow: 0 1px 0 #fff; @@ -619,9 +630,6 @@ // Miscellaneous // ------------------------- -// Hr border color -@hr-border: @gray-lighter; - // Horizontal forms & lists @component-offset-horizontal: 180px; @@ -629,14 +637,18 @@ // Container sizes // -------------------------------------------------- +// Very Small screen / tablet +@container-phone: ((@screen-phone - @grid-gutter-width)); +@container-xs: @container-phone; + // Small screen / tablet -@container-tablet: ((720px + @grid-gutter-width)); +@container-tablet: ((@screen-tablet - @grid-gutter-width)); @container-sm: @container-tablet; // Medium screen / desktop -@container-desktop: ((940px + @grid-gutter-width)); +@container-desktop: ((@screen-desktop - @grid-gutter-width)); @container-md: @container-desktop; // Large screen / wide desktop -@container-large-desktop: ((1140px + @grid-gutter-width)); -@container-lg: @container-large-desktop; +@container-lg-desktop: ((@screen-lg-desktop - @grid-gutter-width)); +@container-lg: @container-lg-desktop; diff --git a/app/styles/preloader.css b/app/styles/preloader.css new file mode 100644 index 0000000..e383a71 --- /dev/null +++ b/app/styles/preloader.css @@ -0,0 +1,86 @@ +/*!_________ .__ _________ __ __ + \_ ___ \ | | ____ ___ __ ____ _______ / _____/_/ |_ _____ ____ | | __ + / \ \/ | | _/ __ \ \ \/ /_/ __ \ \_ __ \ \_____ \ \ __\\__ \ _/ ___\ | |/ / + \ \____| |__\ ___/ \ / \ ___/ | | \/ / \ | | / __ \_\ \___ | < + \______ /|____/ \___ > \_/ \___ > |__| /_______ / |__| (____ / \___ >|__|_ \ + \/ \/ \/ \/ \/ \/ \/ */ +.pace { + -webkit-pointer-events: none; + pointer-events: none; + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; +} + +.pace-inactive { + display: none; +} + +.pace .pace-progress { + background: #3F4452; + position: fixed; + z-index: 2000; + top: 0; + left: 0; + height: 2px; + + -webkit-transition: width 1s; + -moz-transition: width 1s; + -o-transition: width 1s; + transition: width 1s; +} + +.pace .pace-progress-inner { + display: block; + position: absolute; + right: 0px; + width: 100px; + height: 100%; + box-shadow: 0 0 10px #3F4452, 0 0 5px #3F4452; + opacity: 1.0; + -webkit-transform: rotate(3deg) translate(0px, -4px); + -moz-transform: rotate(3deg) translate(0px, -4px); + -ms-transform: rotate(3deg) translate(0px, -4px); + -o-transform: rotate(3deg) translate(0px, -4px); + transform: rotate(3deg) translate(0px, -4px); +} + +.pace .pace-activity { + display: block; + position: fixed; + z-index: 2000; + top: 15px; + right: 15px; + width: 14px; + height: 14px; + border: solid 2px transparent; + border-top-color: #3F4452; + border-left-color: #3F4452; + border-radius: 10px; + -webkit-animation: pace-spinner 400ms linear infinite; + -moz-animation: pace-spinner 400ms linear infinite; + -ms-animation: pace-spinner 400ms linear infinite; + -o-animation: pace-spinner 400ms linear infinite; + animation: pace-spinner 400ms linear infinite; +} + +@-webkit-keyframes pace-spinner { + 0% { -webkit-transform: rotate(0deg); transform: rotate(0deg); } + 100% { -webkit-transform: rotate(360deg); transform: rotate(360deg); } +} +@-moz-keyframes pace-spinner { + 0% { -moz-transform: rotate(0deg); transform: rotate(0deg); } + 100% { -moz-transform: rotate(360deg); transform: rotate(360deg); } +} +@-o-keyframes pace-spinner { + 0% { -o-transform: rotate(0deg); transform: rotate(0deg); } + 100% { -o-transform: rotate(360deg); transform: rotate(360deg); } +} +@-ms-keyframes pace-spinner { + 0% { -ms-transform: rotate(0deg); transform: rotate(0deg); } + 100% { -ms-transform: rotate(360deg); transform: rotate(360deg); } +} +@keyframes pace-spinner { + 0% { transform: rotate(0deg); transform: rotate(0deg); } + 100% { transform: rotate(360deg); transform: rotate(360deg); } +} From 7059df98ec994180c5d4c652148586dd9440d6fa Mon Sep 17 00:00:00 2001 From: Richard Gustin Date: Fri, 29 Aug 2014 16:28:01 +1000 Subject: [PATCH 11/14] chore(reps): Updated bower config with new modules --- bower.json | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/bower.json b/bower.json index 7dc68aa..faeed06 100644 --- a/bower.json +++ b/bower.json @@ -27,7 +27,20 @@ "jquery": "~1.10.2", "json3": "~3.2.4", "requirejs": "~2.1.9", - "underscore": "~1.5.2" + "underscore": "~1.5.2", + "pace": "~0.5.5", + "ng-table": "~0.3.3", + "fontawesome": "~4.1.0", + "ng-table-resizable-columns": "https://github.com/esvit/ng-table-resizable-columns.git", + "ui-bootstrap": "~0.11.0", + "angular-bootstrap": "~0.11.0", + "selectn": "~0.9.6", + "select2": "~3.4.8", + "angular-ui-select2": "~0.0.5", + "angular-ui": "~0.4.0", + "inflection": "~1.3.8", + "momentjs": "~2.8.1", + "jquery-minicolors": "~2.1.6" }, "devDependencies": { "angular-mocks": "1.2.6", From 943fa6088f853a7fcf6dc7c68a77925015b4715f Mon Sep 17 00:00:00 2001 From: Richard Gustin Date: Fri, 29 Aug 2014 16:28:20 +1000 Subject: [PATCH 12/14] refactor(Gruntfile): Huge refactor of the grunt file for 1.2 --- Gruntfile.js | 601 ++++++++++++++++++++------------------------------- 1 file changed, 236 insertions(+), 365 deletions(-) diff --git a/Gruntfile.js b/Gruntfile.js index a9664b4..a7dec34 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -1,40 +1,31 @@ -'use strict'; +var fs = require( 'fs' ) + , path = require( 'path' ) + , lrSnippet = require( 'grunt-contrib-livereload/lib/utils' ).livereloadSnippet; -var fs = require('fs'); +var mountFolder = function( connect, dir ) { + 'use strict'; -var lrSnippet = require('grunt-contrib-livereload/lib/utils').livereloadSnippet; - -var mountFolder = function (connect, dir) { - return connect.static(require('path').resolve(dir)); + return connect.static( path.resolve( dir ) ); }; -var fallbackToTest = function (connect) { - return connect().use(function (req, res, next) { - fs.exists(__dirname + req.url, function (exists) { - if(exists) { - fs.createReadStream(req.url).pipe(res); - } else { - fs.createReadStream(__dirname + '/test/e2e/test-index.html').pipe(res); - } - }); - }); -}; +var fallbackToIndex = function( connect, index, file ) { + 'use strict'; -var fallbackToIndex = function (connect, index, file) { - return connect().use(function (req, res, next) { - if(req.url === file) { + return connect().use( function( req, res, next ) { + if( req.url === file ) { return next(); } - if(/views\/(.*).html$/.test(req.url)) { - res.end( fs.readFileSync(index) ); + if( /views\/(.*).html$/.test( req.url ) ) { + res.end( fs.readFileSync( index ) ); } - res.end( fs.readFileSync(index) ); + res.end( fs.readFileSync( index ) ); }); }; module.exports = function (grunt) { + 'use strict'; // grunt helpers require('time-grunt')(grunt); @@ -43,65 +34,21 @@ module.exports = function (grunt) { require('load-grunt-tasks')(grunt); // configurations - var appConfig = { - - // Development Server - "dev": { - "port": "9000", // default 9000 - "path": "./app", // the development directory of your app - "liveReloadPort": "35729", // default 35729 - "hostname": "localhost" // using 0.0.0.0 will make the server accessible from anywhere - }, - - // Unit Testing Server - "test": { - "unit": { - "port": "9090", // default 9090 - "path": "./test", // if you change this it must reflect in your test-unit.conf.js - "coverage": { - "port": "5555", // default 5555 - "path": "./test/coverage/unit/" // browsable directory for unit testing code coverage reports - }, - "conf": "./test-unit.conf.js" - }, - "e2e": { - "seleniumPort": "4444", // default 4444 - "path": "./test", // if you change this it must reflect in your test-e2e.conf.js - "conf": "./test-e2e.conf.js", - "coverage": { - "port": "7776", // default 5555 - "path": "./test/coverage/e2e/" // browsable directory for e2e testing code coverage reports - }, - "report": { - "port": "7777" - }, - "instrumented": { - "path": './test/coverage/e2e/instrumented/' - } - } - }, - - // Production Preview Server - "dist": { - "port": "9009", // default 9009 - "path": "./dist" // directory where you want your production builds to go - } - - }; + var appConfig = require( path.resolve( path.join( __dirname, 'config', 'global.json' ) ) ); grunt.initConfig({ - appConfig: appConfig, + appConfig: appConfig, watch: { livereload: { options: { - livereload: true + livereload: true }, files: [ '<%= appConfig.dev.path %>/components/bootstrap/{,*/}*.css', '<%= appConfig.dev.path %>/styles/{,*/}*.css', '<%= appConfig.dev.path %>/{,*/}*.html', - '<%= appConfig.dev.path %>/modules/**/{,*/}*.{css,js,less,html}', - '{.tmp,<%= appConfig.dev.path %>}/styles/{,*/}*.css', + '<%= appConfig.dev.path %>/modules/**/{,*/}*.{css,js,html}', + '{.tmp,<%= appConfig.dev.path %>}/styles/*.css', '{.tmp,<%= appConfig.dev.path %>}/views/{,*/}*.html', '{.tmp,<%= appConfig.dev.path %>}/scripts/{,*/}*.js', '<%= appConfig.dev.path %>/images/{,*/}*.{png,jpg,jpeg,gif,webp,svg}' @@ -111,9 +58,11 @@ module.exports = function (grunt) { files: [ '<%= appConfig.dev.path %>/components/bootstrap/less/*.less', '<%= appConfig.dev.path %>/styles/less/**/*.less', - '<%= appConfig.dev.path %>/modules/**/styles/less/*.less' + '<%= appConfig.dev.path %>/modules/**/styles/**/*.less' ], - tasks: ['less:development'] + tasks: [ + 'less:css' + ] }, unitTests: { files: [ @@ -131,253 +80,240 @@ module.exports = function (grunt) { }, connect: { options: { - port: '<%= appConfig.dev.port %>', - // Change this to '0.0.0.0' to access the server from outside. - hostname: '<%= appConfig.dev.hostname %>', - livereload: '<%= appConfig.dev.liveReloadPort %>' + port: '<%= appConfig.dev.port %>', + hostname: '<%= appConfig.dev.hostname %>', + livereload: '<%= appConfig.dev.liveReloadPort %>' }, livereload: { options: { - middleware: function (connect) { + middleware: function( connect ) { return [ lrSnippet, - mountFolder(connect, '.tmp'), - mountFolder(connect, appConfig.dev.path), - fallbackToIndex(connect, 'app/index.html', '/index.html') + mountFolder( connect, '.tmp' ), + mountFolder( connect, appConfig.dev.path ), + fallbackToIndex( connect, 'app/index.html', '/index.html' ) ]; } } }, - test: { + dist: { options: { - port: '<%= appConfig.test.unit.port %>', - base: __dirname, - livereload: false, - middleware: function (connect) { + livereload: false, + port: '<%= appConfig.dist.port %>', + base: '<%= appConfig.dist.path %>', + middleware: function( connect ) { return [ - mountFolder(connect, '.'), - fallbackToTest(connect) + connect.compress(), + mountFolder( connect, appConfig.dist.path ), + fallbackToIndex( connect, appConfig.dist.path + '/index.html', '/index.html' ) ]; } } }, - dist: { - options: { - livereload: false, - port: '<%= appConfig.dist.port %>', - base: '<%= appConfig.dist.path %>' - } - }, coverage: { options: { - base: '<%= appConfig.test.unit.coverage.path %>', - directory: '<%= appConfig.test.unit.coverage.path %>', - port: '<%= appConfig.test.unit.coverage.port %>', - keepalive: true, - livereload: false + base: '<%= appConfig.test.unit.coverage.path %>', + directory: '<%= appConfig.test.unit.coverage.path %>', + port: '<%= appConfig.test.unit.coverage.port %>', + keepalive: true, + livereload: false } }, coverageE2E: { options: { - port: '<%= appConfig.test.e2e.coverage.port %>', - middleware: function (connect) { + port: '<%= appConfig.test.e2e.coverage.port %>', + middleware: function( connect ) { return [ mountFolder( connect, appConfig.test.e2e.instrumented.path + 'app' ), fallbackToIndex( connect, appConfig.test.e2e.instrumented.path + 'app/index.html', '/index.html' ) ]; }, - livereload: false, - debug: false + livereload: false, + debug: false } }, coverageE2EReport: { options: { - port: '<%= appConfig.test.e2e.report.port %>', - middleware: function (connect) { + port: '<%= appConfig.test.e2e.report.port %>', + middleware: function( connect ) { return [ mountFolder( connect, appConfig.test.e2e.coverage.path + '/reports' ), fallbackToIndex( connect, appConfig.test.e2e.coverage.path + 'reports/index.html', '/index.html' ) ]; }, - keepalive: true, - livereload: false + keepalive: true, + livereload: false } }, coverageE2EReportNoWait: { options: { - port: '<%= appConfig.test.e2e.report.port %>', - middleware: function (connect) { + port: '<%= appConfig.test.e2e.report.port %>', + middleware: function( connect ) { return [ mountFolder( connect, appConfig.test.e2e.coverage.path + '/reports' ), fallbackToIndex( connect, appConfig.test.e2e.coverage.path + 'reports/index.html', '/index.html' ) ]; }, - keepalive: false, - livereload: false + keepalive: false, + livereload: false } } }, clean: { + server: '.tmp', dist: { files: [{ - dot: true, - src: [ - '.tmp', - '<%= appConfig.dist.path %>/*', - '!<%= appConfig.dist.path %>/.git*' - ] + dot: true, + src: [ '.tmp', '<%= appConfig.dist.path %>/*', '!<%= appConfig.dist.path %>/.git*' ] }] }, - server: '.tmp', coverage: { files: [{ - dot: true, - src: [ - '<%= appConfig.test.unit.coverage.path %>*' - ] + dot: true, + src: [ '<%= appConfig.test.unit.coverage.path %>*' ] }] }, coverageE2E: { files: [{ - dot: true, - src: [ - '<%= appConfig.test.e2e.coverage.path %>*' - ] + dot: true, + src: [ '<%= appConfig.test.e2e.coverage.path %>*' ] }] }, }, jshint: { options: { - jshintrc: '.jshintrc' + jshintrc: '.jshintrc', + reporter: require( 'jshint-stylish' ), + ignores: [ + '<%= appConfig.dev.path %>/modules/**/test*/**/*.js' + ] }, - all: [ + all: [ 'Gruntfile.js', - '<%= appConfig.dev.path %>/scripts/{,*/}*.js' - ] - }, - concat: { - dist: { - files: { - '<%= appConfig.dist.path %>/scripts/scripts.js': [ - '.tmp/scripts/{,*/}*.js', - '<%= appConfig.dev.path %>/scripts/{,*/}*.js' - ] - } - } + '<%= appConfig.dev.path %>/modules/**/*.js' + ], }, useminPrepare: { - html: '<%= appConfig.dev.path %>/index.html', + html: '<%= appConfig.dev.path %>/index.html', options: { - dest: '<%= appConfig.dist.path %>' + root: '<%= appConfig.dev.path %>', + dest: '<%= appConfig.dist.path %>' } }, usemin: { - html: ['<%= appConfig.dist.path %>/index.html', '<%= appConfig.dist.path %>/views/**/*.html'], - css: ['<%= appConfig.dist.path %>/styles/**/*.css'], + html: [ '<%= appConfig.dist.path %>/index.html', '<%= appConfig.dist.path %>/modules/**/views/**/*.html' ], + css: [ '<%= appConfig.dist.path %>/styles/**/*.css' ], options: { - dirs: ['<%= appConfig.dist.path %>'] + dirs: [ '<%= appConfig.dist.path %>' ] } }, imagemin: { dist: { files: [{ - expand: true, - cwd: '<%= appConfig.dev.path %>/images', - src: '**/*.{png,jpg,jpeg}', - dest: '<%= appConfig.dist.path %>/images' + expand: true, + cwd: '<%= appConfig.dev.path %>/images', + src: '**/*.{png,jpg,jpeg}', + dest: '<%= appConfig.dist.path %>/images' + }, + { + expand: true, + cwd: '<%= appConfig.dev.path %>/components', + src: '**/*.{png,jpg,jpeg}', + dest: '<%= appConfig.dist.path %>/components' }] } }, less: { - development: { + css: { options: { - paths: ['<%= appConfig.dist.path %>/styles'] + paths: [ '<%= appConfig.dist.path %>/styles' ] }, - files: [{ - dest: '<%= appConfig.dev.path %>/styles/application.css', - src: [ - '<%= appConfig.dev.path %>/styles/less/application.less', - '<%= appConfig.dev.path %>/modules/**/styles/less/*.less' - ] - }] - }, - production: { - options: { - paths: ['<%= appConfig.dist.path %>/styles'], - cleancss: true - }, - files: [{ - dest: '<%= appConfig.dev.path %>/styles/application.css', - src: [ - '<%= appConfig.dev.path %>/styles/less/application.less', - '<%= appConfig.dev.path %>/modules/**/styles/less/*.less' - ] - }] - }, + files: [ + { + dest: '<%= appConfig.dev.path %>/styles/application.css', + src: [ + + '<%= appConfig.dev.path %>/styles/less/application.less', + '<%= appConfig.dev.path %>/modules/**/styles/**/*.less', + '<%= appConfig.dev.path %>/modules/**/styles/**/*.css' + ] + } + ] + } }, cssmin: { + options: { + root: './app', + relativeTo: '/' + }, dist: { files: { '<%= appConfig.dist.path %>/styles/screen.css': [ - '.tmp/styles/{,*/}*.css', - '<%= appConfig.dev.path %>/styles/{,*/}*.css' + '<%= appConfig.dev.path %>/styles/preloader.css', + '<%= appConfig.dev.path %>/styles/bootstrap.css', + '<%= appConfig.dev.path %>/components/fontawesome/css/font-awesome.min.css', + '<%= appConfig.dev.path %>/components/angular-ui/build/angular-ui.min.css', + '<%= appConfig.dev.path %>/components/ng-table/ng-table.min.css', + '<%= appConfig.dev.path %>/components/jquery-minicolors/jquery.minicolors.css', + '<%= appConfig.dev.path %>/components/select2/select2.css', + '<%= appConfig.dev.path %>/components/select2/select2-bootstrap.css', + '<%= appConfig.dev.path %>/styles/application.css' ] } - } + }, }, htmlmin: { dist: { options: { - /*removeCommentsFromCDATA: true, - // https://github.com/appConfig/grunt-usemin/issues/44 - //collapseWhitespace: true, - collapseBooleanAttributes: true, - removeAttributeQuotes: true, - removeRedundantAttributes: true, - useShortDoctype: true, - removeEmptyAttributes: true, - removeOptionalTags: true*/ + collapseBooleanAttributes: true, + collapseWhitespace: true, + removeAttributeQuotes: true, + removeComments: true, + removeEmptyAttributes: true, + removeRedundantAttributes: true, + removeScriptTypeAttributes: true, + removeStyleLinkTypeAttributes: true }, files: [{ - expand: true, - cwd: '<%= appConfig.dev.path %>', - src: ['*.html', 'views/**/*.html', 'views/**/partials/*.html'], - dest: '<%= appConfig.dist.path %>' - }] - } - }, - ngmin: { - dist: { - files: [{ - expand: true, - cwd: '<%= appConfig.dist.path %>/scripts', - src: '*.js', - dest: '<%= appConfig.dist.path %>/scripts' + expand: true, + cwd: '<%= appConfig.dist.path %>', + src: [ 'index.html', 'modules/**/views/**/*.html' ], + dest: '<%= appConfig.dist.path %>' }] } }, - uglify: { + ngAnnotate: { + options: { + singleQuotes: true, + regexp: '^(ng\n?[\\ ]+(.*)|(module.*))$' + }, dist: { - files: { - '<%= appConfig.dist.path %>/scripts/scripts.js': [ - '<%= appConfig.dist.path %>/scripts/scripts.js' - ] - } + files: [ { add: true, src: 'dist/modules/**/*.js' } ] } }, requirejs: { compile: { options: { - name: 'main', - baseUrl: '<%= appConfig.dist.path %>/modules', - out: '<%= appConfig.dist.path %>/scripts/scripts.js', - findNestedDependencies: true, - uglify: { - beautify: false, - overwrite: true, - verbose: true, - no_mangle: true, - copyright: true + name: 'main', + baseUrl: '<%= appConfig.dist.path %>/modules', + out: '<%= appConfig.dist.path %>/scripts/scripts.js', + findNestedDependencies: true, + preserveLicenseComments: false, + generateSourceMaps: false, + optimize: 'uglify2', + uglify2: { + mangle: true, + compress: { + 'drop_console': true, + 'drop_debugger': true, + 'dead_code': true, + 'join_vars': true, + 'if_return': true, + 'negate_iife': true, + booleans: true, + loops: true, + unused: true + } } } } @@ -385,7 +321,7 @@ module.exports = function (grunt) { rev: { dist: { files: { - src: [ + src: [ '<%= appConfig.dist.path %>/scripts/{,*/}*.js', '<%= appConfig.dist.path %>/styles/{,*/}*.css', '<%= appConfig.dist.path %>/images/{,*/}*.{png,jpg,jpeg,gif,webp,svg}', @@ -397,11 +333,11 @@ module.exports = function (grunt) { copy: { dist: { files: [{ - expand: true, - dot: true, - cwd: '<%= appConfig.dev.path %>', - dest: '<%= appConfig.dist.path %>', - src: [ + expand: true, + dot: true, + cwd: '<%= appConfig.dev.path %>', + dest: '<%= appConfig.dist.path %>', + src: [ '*.{ico,txt}', '.htaccess', 'components/**/*.{js,css,eot,svg,ttf,woff,png,jpg,jpeg,gif,webp}', @@ -411,17 +347,18 @@ module.exports = function (grunt) { 'scripts/**/*', 'fonts/**/*', 'home/**/*', - 'modules/**/*' + 'modules/**/*', + 'index.html' ] }] }, coverageE2E: { - files: [{ - expand: true, - dot: true, - cwd: '<%= appConfig.dev.path %>', - dest: '<%= appConfig.test.e2e.instrumented.path %>app', - src: [ + files: [{ + expand: true, + dot: true, + cwd: '<%= appConfig.dev.path %>', + dest: '<%= appConfig.test.e2e.instrumented.path %>app', + src: [ '*.{ico,txt}', '.htaccess', 'components/**/*.{js,css,eot,svg,ttf,woff,png,jpg,jpeg,gif,webp}', @@ -441,146 +378,132 @@ module.exports = function (grunt) { }] }, bootstrap: { - files: [ + files: [ { - expand: true, - filter: 'isFile', - cwd: '<%= appConfig.dev.path %>/components/bootstrap/dist/js', - dest: '<%= appConfig.dev.path %>/scripts', - src: [ - 'bootstrap.js' - ] + expand: true, + filter: 'isFile', + cwd: '<%= appConfig.dev.path %>/components/bootstrap/dist/js', + dest: '<%= appConfig.dev.path %>/scripts', + src: [ 'bootstrap.js' ] }, { - expand: true, - filter: 'isFile', - cwd: '<%= appConfig.dev.path %>/components/bootstrap/dist', - dest: '<%= appConfig.dev.path %>', - src: [ - 'fonts/*' - ] + expand: true, + filter: 'isFile', + cwd: '<%= appConfig.dev.path %>/components/bootstrap/dist', + dest: '<%= appConfig.dev.path %>', + src: [ 'fonts/*' ] }, { - expand: true, - filter: 'isFile', - cwd: '<%= appConfig.dev.path %>/components/bootstrap/dist/css', - dest: '<%= appConfig.dev.path %>/styles', - src: [ - 'bootstrap.css' - ] + expand: true, + filter: 'isFile', + cwd: '<%= appConfig.dev.path %>/components/bootstrap/dist/css', + dest: '<%= appConfig.dev.path %>/styles', + src: [ 'bootstrap.css' ] }, { - expand: true, - filter: 'isFile', - cwd: '<%= appConfig.dev.path %>/components/bootstrap/less', - dest: '<%= appConfig.dev.path %>/styles/less/bootstrap', - src: [ - 'variables.less', - 'mixins.less', - 'theme.less' - ] + expand: true, + filter: 'isFile', + cwd: '<%= appConfig.dev.path %>/components/bootstrap/less', + dest: '<%= appConfig.dev.path %>/styles/less/bootstrap', + src: [ + '*.less' + ] } ] } }, - // Concurrent config concurrent: { watch: { - tasks: [ + tasks: [ 'watch:livereload', 'watch:less' ], - options: { - logConcurrentOutput: true + options: { + logConcurrentOutput: true } } }, open: { server: { - url: 'http://localhost:<%= connect.options.port %>' + url: 'http://localhost:<%= connect.options.port %>' }, test: { - url: 'http://localhost:<%= connect.test.options.port %>' + url: 'http://localhost:<%= connect.test.options.port %>' }, dist: { - url: 'http://localhost:<%= connect.dist.options.port %>' + url: 'http://localhost:<%= connect.dist.options.port %>' }, coverage: { - url: 'http://localhost:<%= appConfig.test.unit.coverage.port %>' + url: 'http://localhost:<%= appConfig.test.unit.coverage.port %>' }, coverageE2E: { - url: 'http://localhost:<%= appConfig.test.e2e.report.port %>' + url: 'http://localhost:<%= appConfig.test.e2e.report.port %>' } }, - // unit testing config karma: { unit: { - configFile: '<%= appConfig.test.unit.conf %>', - autoWatch: false, - singleRun: true + configFile: '<%= appConfig.test.unit.conf %>', + autoWatch: false, + singleRun: true }, unitAuto: { - configFile: '<%= appConfig.test.unit.conf %>', - autoWatch: true, - singleRun: false + configFile: '<%= appConfig.test.unit.conf %>', + autoWatch: true, + singleRun: false }, unitCoverage: { - configFile: '<%= appConfig.test.unit.conf %>', - autoWatch: false, - singleRun: true, - reporters: ['progress', 'coverage'], + configFile: '<%= appConfig.test.unit.conf %>', + autoWatch: false, + singleRun: true, + reporters: [ 'progress', 'coverage' ], preprocessors: { - './app/modules/**/*.js': ['coverage'] + './app/modules/**/*.js': [ 'coverage' ] }, coverageReporter: { - type : 'html', - dir : '<%= appConfig.test.unit.coverage.path %>' + type: 'html', + dir: '<%= appConfig.test.unit.coverage.path %>' } }, travis: { - configFile: '<%= appConfig.test.unit.conf %>', - autoWatch: false, - singleRun: true, - browsers: ['PhantomJS'] + configFile: '<%= appConfig.test.unit.conf %>', + autoWatch: false, + singleRun: true, + browsers: [ 'PhantomJS' ] } }, - - // e2e protractor testing config protractor: { options: { - configFile: '<%= appConfig.test.e2e.conf %>', + configFile: '<%= appConfig.test.e2e.conf %>', args: { seleniumPort: '<%= appConfig.test.e2e.seleniumPort %>' } }, singlerun: { - keepAlive: false + keepAlive: false }, auto: { - keepAlive: true + keepAlive: true } }, - - // e2e protractor coverage - protractor_coverage: { + 'protractor_coverage': { local: { options: { - configFile: '<%= appConfig.test.e2e.conf %>', - keepAlive: true, - noColor: false, - coverageDir: '<%= appConfig.test.e2e.instrumented.path %>', + configFile: '<%= appConfig.test.e2e.conf %>', + keepAlive: true, + noColor: false, + coverageDir: '<%= appConfig.test.e2e.instrumented.path %>', args: { - baseUrl: 'http://localhost:<%= appConfig.test.e2e.coverage.port %>' + baseUrl: 'http://localhost:<%= appConfig.test.e2e.coverage.port %>' } } } }, instrument: { options: { - lazy: true, - basePath: '<%= appConfig.test.e2e.instrumented.path %>' + lazy: true, + basePath: '<%= appConfig.test.e2e.instrumented.path %>' }, - files: [ + files: [ 'app/modules/**/scripts/*.js', 'app/modules/**/controllers/*.js', 'app/modules/**/directives/*.js', @@ -589,46 +512,11 @@ module.exports = function (grunt) { ] }, makeReport: { - src: '<%= appConfig.test.e2e.instrumented.path %>*.json', + src: '<%= appConfig.test.e2e.instrumented.path %>*.json', options: { - type: 'html', - dir: '<%= appConfig.test.e2e.coverage.path %>reports', - print: 'detail' - } - }, - run: { - wbDriverUpdate: { - args: [ './node_modules/protractor/bin/webdriver-manager', 'update', '--out_dir=./scripts/' ], - options: { - passArgs: [ - 'ie', - 'chrome', - 'standalone', - 'seleniumPort' - ] - } - }, - wbDriverStatus: { - args: [ './node_modules/protractor/bin/webdriver-manager', 'status' ], - options: { - passArgs: [ - 'ie', - 'chrome', - 'standalone', - 'seleniumPort' - ] - } - }, - wbDriverStart: { - args: [ './node_modules/protractor/bin/webdriver-manager', 'start', '--out_dir=./scripts/' ], - options: { - passArgs: [ - 'ie', - 'chrome', - 'standalone', - 'seleniumPort' - ] - } + type: 'html', + dir: '<%= appConfig.test.e2e.coverage.path %>reports', + print: 'detail' } } }); @@ -637,12 +525,11 @@ module.exports = function (grunt) { grunt.registerTask('bootstrap', ['copy:bootstrap']); //for less compilation force - grunt.option("force", true); + grunt.option( 'force', true ); grunt.registerTask('server', [ 'clean:server', 'connect:livereload', - 'connect:test', 'connect:dist', 'concurrent:watch' ]); @@ -650,32 +537,16 @@ module.exports = function (grunt) { grunt.registerTask('build', [ 'clean:dist', 'useminPrepare', + 'jshint', 'imagemin', 'less', 'cssmin', - 'htmlmin', 'copy:dist', - 'ngmin', + 'ngAnnotate:dist', 'requirejs', 'rev', - 'usemin' - ]); - - grunt.registerTask( 'webdriver', [ - 'run:wbDriverStatus', - 'run:wbDriverUpdate' - ]); - - grunt.registerTask( 'webdriver:update', [ - 'run:wbDriverUpdate' - ]); - - grunt.registerTask( 'webdriver:status', [ - 'run:wbDriverStatus' - ]); - - grunt.registerTask( 'webdriver:start', [ - 'run:wbDriverStart' + 'usemin', + 'htmlmin' ]); /* -- TEST TASKS ------------------------------------------------ */ @@ -731,7 +602,7 @@ module.exports = function (grunt) { ]); grunt.registerTask('test:e2e', 'Single run of end to end (e2e) tests using protractor.', [ - 'connect:dist', + 'connect:livereload', 'protractor:singlerun' ]); From a98d91f88e6587c4896f52890dd36b236a4ccd77 Mon Sep 17 00:00:00 2001 From: Richard Gustin Date: Fri, 29 Aug 2014 16:29:09 +1000 Subject: [PATCH 13/14] chore(package): Get ready for release 1.2.0-beta-1 --- package.json | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 75bce2f..3c821dd 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "private": true, "name": "angular-seed", "description": "Cleverstack Angular-Seed", - "version": "1.1.5", + "version": "1.2.0-beta-1", "author": { "name": "CleverStack", "email": "admin@cleverstack.io", @@ -31,6 +31,7 @@ "license": "BSD-2-Clause", "dependencies": {}, "devDependencies": { + "grunt-angular-templates": "^0.5.7", "grunt-concurrent": "~0.4.1", "grunt-contrib-clean": "~0.5.0", "grunt-contrib-concat": "~0.3.0", @@ -46,15 +47,15 @@ "grunt-contrib-watch": "~0.5.2", "grunt-coveralls": "^0.3.0", "grunt-istanbul": "^0.3.0", - "grunt-karma": "0.7.2", - "grunt-ngmin": "~0.0.2", + "grunt-karma": "~0.6.2", + "grunt-ng-annotate": "^0.3.2", "grunt-open": "~0.2.2", "grunt-protractor-coverage": "^0.2.8", "grunt-protractor-runner": "~0.1.6", "grunt-rev": "~0.1.0", - "grunt-run": "~0.2.2", "grunt-usemin": "~0.1.12", - "karma": "~0.11.11", + "jshint-stylish": "^0.4.0", + "karma": "~0.10.8", "karma-browserstack-launcher": "~0.0.7", "karma-chrome-launcher": "~0.1.1", "karma-coverage": "~0.1.0", @@ -68,9 +69,9 @@ "karma-phantomjs-launcher": "~0.1.1", "karma-requirejs": "~0.2.0", "karma-safari-launcher": "~0.1.1", - "karma-sauce-launcher": "~0.2.8", "karma-script-launcher": "~0.1.0", "load-grunt-tasks": "~0.2.0", + "ng-annotate": "^0.9.11", "phantomjs": "~1.9.2", "protractor": "~0.24.2", "time-grunt": "~0.2.1" From 2f90364792419d6762b2a5f87c831f5d08103e49 Mon Sep 17 00:00:00 2001 From: Richard Gustin Date: Fri, 29 Aug 2014 16:29:29 +1000 Subject: [PATCH 14/14] feat(config): Global json configuration file --- config/global.json | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 config/global.json diff --git a/config/global.json b/config/global.json new file mode 100644 index 0000000..9f528f2 --- /dev/null +++ b/config/global.json @@ -0,0 +1,42 @@ +{ + "dev": { + "port": "9000", + "path": "./app", + "liveReloadPort": "35729", + "hostname": "0.0.0.0" + }, + + + "test": { + "unit": { + "port": "9090", + "path": "./test", + "coverage": { + "port": "5555", + "path": "./test/coverage/unit/" + }, + "conf": "./test-unit.conf.js" + }, + "e2e": { + "seleniumPort": "4444", + "path": "./test", + "conf": "./test-e2e.conf.js", + "coverage": { + "port": "7776", + "path": "./test/coverage/e2e/" + }, + "report": { + "port": "7777" + }, + "instrumented": { + "path": "./test/coverage/e2e/instrumented/" + } + } + }, + + + "dist": { + "port": "9009", + "path": "./dist" + } +}