Compare commits

...

183 Commits

Author SHA1 Message Date
Lorenz Hilpert
a27a0c8b9c Zxing ask for camera permission 2022-05-31 15:55:04 +02:00
Lorenz Hilpert
acb76c1384 zxing scanner poc 2022-05-31 13:22:31 +02:00
Lorenz Hilpert
5882655862 Scanner performance update 2022-05-16 17:36:49 +02:00
Lorenz Hilpert
5b440da557 Scandit aktiviert 2022-05-16 16:11:29 +02:00
Lorenz Hilpert
be91a0f1b4 Scan performance test 2022-05-16 15:53:57 +02:00
Lorenz Hilpert
65e3d5d4f1 Update Gitignore 2022-05-16 15:28:02 +02:00
Lorenz Hilpert
a0e7614fa0 Remove Scandit Folder Content 2022-05-16 15:25:25 +02:00
Lorenz Hilpert
094c881e7f Scanner Performance Update 2022-05-16 15:20:10 +02:00
Lorenz Hilpert
d3cd6f415b Ipad 6 fix scandit 2022-04-19 20:39:58 +02:00
Lorenz Hilpert
e733396c63 Fix Engine Location 2022-04-19 19:08:02 +02:00
Lorenz Hilpert
e88795f96b Fix Reader 2022-04-19 15:56:35 +02:00
Lorenz Hilpert
fd276b6553 scandit typo 2022-04-19 15:21:21 +02:00
Lorenz Hilpert
22c3b057d7 scandit licence 2022-04-19 15:20:14 +02:00
Lorenz Hilpert
b7feea46f7 scandit implementation 2022-04-19 14:58:15 +02:00
Andreas Schickinger
9052fe25db Merged PR 1182: #2756 WE WA Edit Anmerkungsfeld mehrzeilig
#2756 WE WA Edit Anmerkungsfeld mehrzeilig

Related work items: #2756
2022-04-14 08:26:45 +00:00
Andreas Schickinger
776115fbed Merged PR 1181: #3023 Remission Required Capacities Department wird mitgegeben
#3023 Remission Required Capacities Department wird mitgegeben

Related work items: #3023
2022-04-13 15:29:13 +00:00
Andreas Schickinger
962f0bc2c6 Merged PR 1180: #3005 AHF Zubuchen innerhalb einer Kundennummer
#3005 AHF Zubuchen innerhalb einer Kundennummer

Related work items: #3005
2022-04-12 15:36:44 +00:00
Lorenz Hilpert
c184df717d #3021 - Remission offene WBS werden nicht angezeigt 2022-04-12 16:07:38 +02:00
Lorenz Hilpert
26a7348d25 Dashboard Styling 2022-04-12 14:55:59 +02:00
Lorenz Hilpert
cf5052cbe5 Merge branch 'develop' of https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend into develop 2022-04-12 14:48:02 +02:00
Lorenz Hilpert
be848123a1 Dashboard Product Navigation 2022-04-12 14:47:53 +02:00
Lorenz Hilpert
04232b85a7 Dasboard Product Navigation 2022-04-12 14:47:39 +02:00
Nino Righi
09bb84c34e Merged PR 1173: #2992 TK Fix PDF Viewer Content Sizing on Mobile
#2992 TK Fix PDF Viewer Content Sizing on Mobile
2022-04-12 08:21:36 +00:00
Lorenz Hilpert
4449992442 Scanner Fix Test 2022-04-11 17:06:45 +02:00
Andreas Schickinger
39c40f2e13 Merged PR 1179: #2742 AHF Zubuchen Button mit Zusatz anzeigen
#2742 AHF Zubuchen Button mit Zusatz anzeigen

Related work items: #2742
2022-04-11 13:37:50 +00:00
Nino Righi
9813575584 Merged PR 1178: #3004 Fix Ui Slider Show Scroll Arrow
#3004 Fix Ui Slider Show Scroll Arrow
2022-04-11 13:35:53 +00:00
Andreas Schickinger
b0af718cbc Merged PR 1176: #3001 Remission Grund Dropdown über Popup legen
#3001 Remission Grund Dropdown über Popup legen

Related work items: #3001
2022-04-11 06:53:51 +00:00
Andreas Schickinger
550544cbe4 Merged PR 1177: #972 Vorgang schließen Styling angepasst und Rechtschreibfehler behoben
#972 Vorgang schließen Styling angepasst und Rechtschreibfehler behoben

Related work items: #972
2022-04-11 06:53:22 +00:00
Andreas Schickinger
3ec3cf0e5b Merged PR 1175: Merge release in develop
Merge release in develop

Related work items: #2666, #2927, #2928, #2979, #2996
2022-04-07 14:06:57 +00:00
Nino Righi
0b79464e52 Merged PR 1174: #2999 Fix Customer Search Box Display Hint
#2999 Fix Customer Search Box Display Hint
2022-04-07 14:05:10 +00:00
Andreas Schickinger
a1642c5749 Merged PR 1172: #2996 Hotfix Listenbestellung Checkboxen bei Filialwechsel
#2996 Hotfix Listenbestellung Checkboxen bei Filialwechsel

Related work items: #2996
2022-04-06 16:22:41 +00:00
Andreas Schickinger
148aab1495 Merged PR 1171: #2928 Listenbestellung Download CanAdd Prüfung
#2928 Listenbestellung Download CanAdd Prüfung

Related work items: #2928
2022-04-06 14:18:20 +00:00
Lorenz Hilpert
2f78f2685f Prozessid aus Navigation für Artikelsuche und Kunden entfernt 2022-04-06 15:59:30 +02:00
Nino Righi
6dde5682a9 Merged PR 1170: #2995 Build Error Fix
#2995 Build Error Fix
2022-04-06 11:59:57 +00:00
Lorenz Hilpert
af8a3d89af Merged PR 1167: #2989 Scanner Refactoring
#2989  Scanner Refactoring
2022-04-06 10:18:20 +00:00
Lorenz Hilpert
ec97d1e84a CI Fix 2022-04-06 11:12:36 +02:00
Lorenz Hilpert
eea412abd3 #2993 WA WE - ORD: in Breadcrumb 2022-04-06 09:58:03 +02:00
Nino Righi
cf65ba3e6b Merged PR 1168: #2988 Fix Remission Close Add Product Modal if quantity changed to 0
#2988 Fix Remission Close Add Product Modal if quantity changed to 0
2022-04-06 07:39:00 +00:00
Nino Righi
3d4445bb46 Merged PR 1169: #2990 Fix Customer Search Cache Filter Settings
#2990 Fix Customer Search Cache Filter Settings
2022-04-06 07:37:12 +00:00
Andreas Schickinger
94753ceac4 Merged PR 1166: #2666 Hotfix Listenbestellung CanAddItems angepasst
#2666 Hotfix Listenbestellung CanAddItems angepasst

Related work items: #2666
2022-04-05 15:12:34 +00:00
Nino Righi
af16542ce5 Merged PR 1165: #2971 Fix Remission unique WBS
#2971 Fix Remission unique WBS
2022-04-05 15:12:08 +00:00
Lorenz Hilpert
e4c82441b8 #2987 Remission - Fehler bei Artikel hinzufügen 2022-04-05 16:05:00 +02:00
Lorenz Hilpert
8783802483 #1025 Schatten um Action-Button 2022-04-05 08:45:37 +02:00
Nino Righi
8ad28a16b0 Merged PR 1164: #2974 Fix CanSetCustomer - Customer Search Setting Filter from Route
#2974 Fix CanSetCustomer - Customer Search Setting Filter from Route
2022-04-04 16:14:21 +00:00
Andreas Schickinger
f9403649e6 Merged PR 1163: #626 Remission Hinweis Farbe bei Artikel hinzufügen
#626 Remission Hinweis Farbe bei Artikel hinzufügen

Related work items: #626
2022-04-04 15:51:18 +00:00
Andreas Schickinger
698af2ecc3 Merged PR 1162: #2666 Listenbestellung Sichtbarkeit der Radio-Buttons angepasst
#2666 Listenbestellung Sichtbarkeit der Radio-Buttons angepasst

Related work items: #2666
2022-04-04 14:39:53 +00:00
Nino Righi
c92649a8d4 Merged PR 1157: #2977 Fix Clear Cart after Process gets closed and reopened via Footer Proces...
#2977 Fix Clear Cart after Process gets closed and reopened via Footer Process generation
2022-04-04 12:30:41 +00:00
Nino Righi
dfd1847a2d Merged PR 1160: #2984 Added Card Feature To Navigation
#2984 Added Card Feature To Navigation
2022-04-04 12:30:03 +00:00
Andreas Schickinger
eeaafec80a Merged PR 1161: #2979 Hotfix Listenbestellung Fehlermeldung und Anpassungen
#2979 Listenbestellung Fehlermeldung und Anpassungen

Related work items: #2979
2022-04-04 12:19:37 +00:00
Nino Righi
896e91d4d9 Merged PR 1159: #2954 Fix Remission Result List no longer horizontally scrollable
#2954 Fix Remission Result List no longer horizontally scrollable
2022-04-04 11:46:06 +00:00
Nino Righi
d6507b428f Merged PR 1158: #2976 Added Option to Reset Filter Settings to TK, Customer and Remission Area
#2976 Added Option to Reset Filter Settings to TK, Customer and Remission Area
2022-04-04 11:44:39 +00:00
Lorenz Hilpert
6ff4d204c2 #2962 IPAd Login Fix 2022-04-04 13:43:17 +02:00
Nino Righi
8145436d1d Merged PR 1156: #2972 Fix Remission Start Remission Popup Updated if supplier is ZL
#2972 Fix Remission Start Remission Popup Updated if supplier is ZL
2022-03-30 11:34:09 +00:00
Nino Righi
25f22b46c5 Merged PR 1155: #2967 Fix Remission Placementtype Wording inside remit Popup
#2967 Fix Remission Placementtype Wording inside remit Popup
2022-03-30 11:33:42 +00:00
Nino Righi
558d846812 Merged PR 1154: #2969 Fix Remission IPAD Scan Popup Navigation
#2969 Fix Remission IPAD Scan Popup Navigation
2022-03-30 10:37:48 +00:00
Nino Righi
05916031ad Merged PR 1153: #2970 Fix Remission Display CTAs on List Elements
#2970 Fix Remission Display CTAs on List Elements
2022-03-30 10:08:51 +00:00
Andreas Schickinger
ffad6aa939 Merged PR 1152: #2755 OLA Nachbestellen Grund
#2755 OLA Nachbestellen Grund

Related work items: #2755
2022-03-30 10:02:35 +00:00
Nino Righi
cb22a39ffc Merged PR 1151: #2912 Ipad Reset Focus Changes
#2912 Ipad Reset Focus Changes
2022-03-29 16:21:20 +00:00
Nino Righi
c6130dcffb Merged PR 1150: #2912 Focus
#2912 Focus
2022-03-29 15:49:30 +00:00
Nino Righi
c9f4143204 Merged PR 1149: #2912 IPADs Fix Autofocus Searchbox
#2912 IPADs Fix Autofocus Searchbox
2022-03-29 15:19:11 +00:00
Nino Righi
fa0e1d7d60 Merged PR 1148: Code auskommentiert der zur Anmelde Dauerschleife auf den Ipads führt
Code auskommentiert der zur Anmelde Dauerschleife auf den Ipads führt
2022-03-29 15:04:09 +00:00
Andreas Schickinger
621b545e34 Merged PR 1143: #2666 Hotfix Listenbestellung Hindernismeldung
#2666 Hotfix Listenbestellung Hindernismeldung

Related work items: #2666, #2927
2022-03-29 14:59:34 +00:00
Nino Righi
53e3d90064 Merged PR 1147: #2962 #2956 Ipad Keycard Login Fix
#2962 #2956 Ipad Keycard Login Fix
2022-03-29 14:09:44 +00:00
Nino Righi
c175826fd3 Merged PR 1146: #2962 Keycard Ipad Login Fix
#2962 Keycard Ipad Login Fix
2022-03-29 13:42:20 +00:00
Nino Righi
c6b584d637 Merged PR 1145: #2962 Fix Keycard
#2962 Fix Keycard
2022-03-29 13:26:54 +00:00
Nino Righi
d992ce87a9 Merged PR 1144: #2962 Keycard Login Fix
#2962 Keycard Login Fix
2022-03-29 13:11:25 +00:00
Nino Righi
a87072e542 Merged PR 1142: #2962 Zwischencommit
#2962 Zwischencommit
2022-03-29 12:32:54 +00:00
Nino Righi
eff0388b8f Merged PR 1141: #2962 2956 Keycard Login IPAD Fix
#2962 2956 Keycard Login IPAD Fix
2022-03-29 12:02:34 +00:00
Nino Righi
9c517810ba Merged PR 1140: #2963 Remission Close Filter Overlay After Hitting Apply Filter Settings
#2963 Remission Close Filter Overlay After Hitting Apply Filter Settings
2022-03-29 11:21:47 +00:00
Nino Righi
882cbddec0 Merged PR 1139: #2912 Autofocus on Input
#2912 Autofocus on Input
2022-03-28 16:03:54 +00:00
Nino Righi
d188272cbf Merged PR 1138: #2912 setTimeout on focus event
#2912 setTimeout on focus event
2022-03-28 15:46:44 +00:00
Nino Righi
72b3688365 Merged PR 1137: #2951 Fix Scanner PopUp
#2951 Fix Scanner PopUp
2022-03-28 14:48:35 +00:00
Andreas Schickinger
0f81914875 Merged PR 1135: #2774 AHF Cover Zusatz
#2774 AHF Cover Zusatz

Related work items: #2774
2022-03-28 13:04:37 +00:00
Nino Righi
e8e895d7b1 Merged PR 1136: #2951 Updated Test Config files and disabled login scanner popup
#2951 Updated Test Config files and disabled login scanner popup
2022-03-28 12:44:59 +00:00
Nino Righi
f0cc76f180 Merged PR 1134: Merge ISA 2.0 into Develop
Merge ISA 2.0 into Develop

Related work items: #1098, #2592, #2630, #2633, #2635, #2639, #2706, #2707, #2813, #2818, #2825, #2843, #2846, #2847, #2848, #2851, #2852, #2853, #2897, #2900
2022-03-28 10:12:12 +00:00
Lorenz Hilpert
91b3f44c1e Merge branch 'release/1.7' into develop 2022-03-28 09:14:12 +08:00
Lorenz Hilpert
43859599f0 Merge branch 'develop' into release/1.7 2022-03-28 09:13:33 +08:00
Andreas Schickinger
4dff0b1e6a Merged PR 1133: #2931 DIG Lieferzeitfenster Punkt fehlt
#2931 DIG Lieferzeitfenster Punkt fehlt

Related work items: #2931
2022-03-25 09:44:33 +00:00
Andreas Schickinger
f5fcee4e4a Merged PR 1132: Merge release in develop
Merge release in develop

Related work items: #2756, #2864, #2872, #2878, #2889, #2890, #2901, #2903, #2904, #2910
2022-03-24 13:21:09 +00:00
Andreas Schickinger
9ffb0b9a97 Merged PR 1131: Merge develop in release
Merge develop in release

Related work items: #2756, #2901, #2903, #2904, #2910
2022-03-24 13:06:34 +00:00
Andreas Schickinger
34698aca5e Merged PR 1130: #2910 Listenbestellung WK iPad Layout
#2910 Listenbestellung WK iPad Layout

Related work items: #2910
2022-03-23 12:57:13 +00:00
Lorenz Hilpert
4fcf9fabbf #2915 Trefferliste - klickbarer Bereich um Bullet-Checkbox 2022-03-23 20:09:47 +08:00
Nino Righi
fd907cf0cc Merged PR 1128: #2921 Listenbestellung B2B Orders Fix availabilities null for canAdd Request
#2921 Listenbestellung B2B Orders Fix availabilities null for canAdd Request
2022-03-23 11:49:34 +00:00
Nino Righi
cb543c7c98 Merged PR 1129: #2054 Checkout Summary Enable Print Button on B2B Order
#2054 Checkout Summary Enable Print Button on B2B Order
2022-03-23 11:44:09 +00:00
Andreas Schickinger
02ee730080 Merged PR 1126: #2901 Listenbestellung Sortierung
#2901 Listenbestellung Sortierung

Related work items: #2901
2022-03-22 14:25:55 +00:00
Nino Righi
5cccc5fedd Merged PR 1125: #2911 Bugfix Cart Font Size on Action CTAs
#2911 Bugfix Cart Font Size on Action CTAs
2022-03-21 17:33:55 +00:00
Andreas Schickinger
9b6f4d1ecf Merged PR 1124: #2756 Anmerkungsfeld mehrzeilig
#2756 Anmerkungsfeld mehrzeilig

Related work items: #2756
2022-03-21 16:57:15 +00:00
Nino Righi
022e8e9e73 Merged PR 1121: #2744 WA WE Order Item Details Display ssc and sscText
#2744 WA WE Order Item Details Display ssc and sscText
2022-03-21 14:43:07 +00:00
Andreas Schickinger
a81581e67f Merged PR 1119: #2904 Listenbestellung ipad 6 Mengen Dropdown Styling
#2904 Listenbestellung ipad 6 Mengen Dropdown Styling

Related work items: #2904
2022-03-21 12:58:25 +00:00
Andreas Schickinger
5823f57e03 Merged PR 1118: #2903 Listenbestellung Preis verschwindet bei Mengenänderung
#2903 Listenbestellung Preis verschwindet bei Mengenänderung

Related work items: #2903
2022-03-21 12:54:50 +00:00
Nico Hanus
96f2233421 add resource limits to helm files 2022-03-18 14:01:32 +01:00
Andreas Schickinger
ac1a772b21 Merged PR 1116: Merge develop into release/1.7
Merge develop into release

Related work items: #2864, #2872, #2878, #2889, #2890
2022-03-18 11:39:56 +00:00
Andreas Schickinger
04851dbe39 Merged PR 1114: #2890 Listenbestellung ipad Anpassungen
#2890 Listenbestellung ipad Anpassungen

Related work items: #2890
2022-03-17 14:44:56 +00:00
Andreas Schickinger
29181469db Merged PR 1113: #2872 Listenbestellung Warenkorb Abhol- und Lieferdatum anzeigen
#2872 Listenbestellung Warenkorb Abhol- und Lieferdatum anzeigen

Related work items: #2872
2022-03-17 13:13:39 +00:00
Andreas Schickinger
cb447e13f9 Merged PR 1112: #2889 Listenbestellung weitere ipad fixes
#2889 Listenbestellung weitere ipad fixes

Related work items: #2889
2022-03-17 12:27:52 +00:00
Andreas Schickinger
04a3b1767a Merged PR 1111: Listenbestellung ipad Modal Layout
Listenbestellung ipad Modal Layout
2022-03-17 10:59:51 +00:00
Andreas Schickinger
20f1a5c77e Merged PR 1110: #2878 Listenbestellung Bugfix endlos Loading, Spinner bei Übernehmen, Caching...
#2878 Listenbestellung Bugfix endlos Loading, Spinner bei Übernehmen, Caching für Logistician Request

Related work items: #2878
2022-03-16 16:36:37 +00:00
Andreas Schickinger
c979b48592 Merged PR 1108: #2846 Bei requestStatusCode 32 wird nun altAt angezeigt
#2846 Bei requestStatusCode 32 wird nun altAt angezeigt

Related work items: #2864
2022-03-15 17:09:45 +00:00
Michael Auer
88ebc39d65 Merge tag '1.6' into develop 2022-03-15 17:05:53 +01:00
Michael Auer
9aa3820e95 Merge branch 'release/1.6' 2022-03-15 17:05:46 +01:00
Andreas Schickinger
722ed6ade6 Merged PR 1105: #2773 Listenbestellung derzeit nicht bestellbar Farbe angepasst
#2773 Listenbestellung derzeit nicht Bestellbar Farbe angepasst

Related work items: #2773
2022-03-15 10:17:02 +00:00
Andreas Schickinger
8f7448a095 Merged PR 1102: #2643 DIG Lieferzeitfenster Kaufoptionen
#2643 DIG Lieferzeitfenster Kaufoptionen

Related work items: #2643
2022-03-15 10:12:22 +00:00
Andreas Schickinger
5e9b3b56d4 Merged PR 1101: #2843 Listenbestellung Autorenlink schwarz
#2843 Listenbestellung Autorenlink schwarz

Related work items: #2843
2022-03-15 09:48:19 +00:00
Andreas Schickinger
95656b20d7 Merged PR 1103: #2793 Listenbestellung QuantityDropdown für Rücklage begrenzen
#2793 Listenbestellung QuantityDropdown für Rücklage begrenzen

Related work items: #2793
2022-03-15 09:45:21 +00:00
Andreas Schickinger
12fe8b46c3 Merged PR 1104: #2662 Listenbestellung Download OLA im Warenkorb bei Bestellen Klick
#2662 Listenbestellung Download OLA im Warenkorb bei Bestellen Klick

Related work items: #2662
2022-03-15 09:00:12 +00:00
Nino Righi
92958f4b22 Merged PR 1097: #2782 TK Remove Color Indicator inside Calendar and List View for Items with...
#2782 TK Remove Color Indicator inside Calendar and List View for Items with an Updated Item
2022-03-10 14:39:50 +00:00
Andreas Schickinger
05a3bbef7a Merged PR 1096: #2843 Listenbestellung Warenkorb Verlinkung für Artikeldetails und Autorsuche
#2843 Listenbestellung Warenkorb Verlinkung für Artikeldetails und Autorsuche

Related work items: #2843
2022-03-10 14:33:15 +00:00
Nino Righi
03467fcb83 Merged PR 1091: #2841 TK removed hours and minutes inside task info taskDate range
#2841 TK removed hours and minutes inside task info taskDate range
2022-03-09 13:29:03 +00:00
Nino Righi
e96c98e344 Merged PR 1089: #2796 #2795 List Order Changed Order of displayed purchasing options and show...
#2796 #2795 List Order Changed Order of displayed purchasing options and show instock if take away is available
2022-03-08 14:08:49 +00:00
Michael Auer
c660c5626d ~ Version Bump: 1.7 2022-03-08 14:18:57 +01:00
Nino Righi
b443c7a5de Merged PR 1088: Merge Listenbestellung into Develop
Merge Listenbestellung into Develop

Related work items: #2560, #2655, #2656, #2699, #2745, #2746, #2747, #2749, #2752, #2760, #2789
2022-03-08 12:25:32 +00:00
Lorenz Hilpert
578f3fee7a Merge branch 'release/1.6' into develop 2022-03-01 10:45:56 +01:00
Lorenz Hilpert
caddcd0e2b Merge branch 'develop' into release/1.6 2022-03-01 10:45:02 +01:00
Michael Auer
cce810d4e3 ~ Version Bump: 1.6 2022-03-01 10:27:31 +01:00
Lorenz Hilpert
2f060e6209 #2783 Nullabfrage auf quantity 2022-02-28 17:25:06 +01:00
Lorenz Hilpert
df94c1ab59 #2783 Teilabholung - mehrere Exemplare von einem Artikel nicht nacheinander abholbar 2022-02-28 13:15:22 +01:00
Nino Righi
9c2ed96331 Merged PR 1075: #2729 Article Details Show Archiv Article Badge with matching text if article...
#2729 Article Details Show Archiv Article Badge with matching text if article is Available or not
2022-02-24 17:45:49 +00:00
Andreas Schickinger
b462e39a51 Merged PR 1073: #2628 HFI Lieferschein drucken
#2628 HFI Lieferschein drucken

Related work items: #2628
2022-02-23 14:01:36 +00:00
Nino Righi
8340649292 Merged PR 1059: #2733 Bugfix WK Notification, select checkbox SMS after input of mobilenumber
#2733 Bugfix WK Notification, select checkbox SMS after input of mobilenumber
2022-02-17 17:02:18 +00:00
Nino Righi
1d2df695d3 Merged PR 1056: #2696 Upgrade of p4mUser is now possible
#2696 Upgrade of p4mUser is now possible
2022-02-15 00:26:58 +00:00
Nino Righi
f46ef394d9 Merged PR 1054: #2671 Article Negative Price
#2671 Article Negative Price
2022-02-15 00:24:11 +00:00
Nino Righi
fcf016ea85 Merged PR 1053: #2689 Take RetailPrice if no catalog Price available for take-away option. Sh...
#2689 Take RetailPrice if no catalog Price available for take-away option. Show RetailPrice in Article Details View for the same case
2022-02-15 00:23:09 +00:00
Andreas Schickinger
e2ebba9f9f Merged PR 1051: #2668 Hotfix - Kubi leere Trefferliste bei Dialog
#2668 Hotfix - Kubi leere Trefferliste bei Dialog

Related work items: #2668
2022-02-09 16:20:57 +00:00
Nino Righi
d9460df0ca Merged PR 1045: #2688 WK Updated Show Custom Price Function
#2688 WK Updated Show Custom Price Function
2022-02-08 14:40:05 +00:00
Nino Righi
9d988e18be Merged PR 1044: #2604 ipad detection
#2604 ipad detection
2022-02-07 17:02:56 +00:00
Nino Righi
949ee7da6e Merged PR 1043: #2604 Hotfix ipad mini 6 detection
#2604 Hotfix ipad mini 6 detection
2022-02-07 16:53:19 +00:00
Nino Righi
736f402179 Merged PR 1042: #2604 Hotfix ipad mini 6 detection
#2604 Hotfix ipad mini 6 detection
2022-02-07 16:45:02 +00:00
Nino Righi
e5c4eb6a8e Merged PR 1041: #2604 Hotfix ipad 6
#2604 Hotfix ipad 6
2022-02-07 16:33:04 +00:00
Nino Righi
c93233674b Merged PR 1040: #2604 Hotfix Ipad 6 Remission Artikel Hinzufügen CTA
#2604 Hotfix Ipad 6 Remission Artikel Hinzufügen CTA
2022-02-07 16:29:29 +00:00
Nino Righi
02abf0852e Merged PR 1039: #2619 Customer Searchbox Placeholder Wording and Resizing
#2619 Customer Searchbox Placeholder Wording and Resizing
2022-02-07 14:58:57 +00:00
Nino Righi
cf3e5ce9a3 Merged PR 1038: #2604 Hotfix Ipad 6 Remission Artikel Hinzufügen CTA
#2604 Hotfix Ipad 6 Remission Artikel Hinzufügen CTA
2022-02-07 14:24:06 +00:00
Lorenz Hilpert
da71454070 Merged PR 1035: #2504 Remivorschau-ChangeStatus-removed
#2504 Remivorschau-ChangeStatus-removed
2022-02-02 15:52:23 +00:00
Lorenz Hilpert
08a8575025 Benamung von tags fue unit tests 2022-02-01 13:45:33 +01:00
Lorenz Hilpert
a99494b6ea Data Attribute für e2e tests hinzugefuegt 2022-01-28 11:00:33 +01:00
Andreas Schickinger
52ab4fccbd Merged PR 1024: #2504 Remissionsvorschau Navigation in Details
#2504 Remissionsvorschau Navigation in Details

Related work items: #2504
2022-01-19 09:39:09 +00:00
Nino Righi
2ab1599fa3 Merged PR 1021: #2504 AHF Remissionsvorschau
#2504 AHF Remissionsvorschau
2022-01-13 15:21:07 +00:00
Michael Auer
f220dde3b2 Merge tag '1.5' into develop 2021-12-22 11:26:38 +01:00
Michael Auer
d027df8856 Merge branch 'release/1.5' 2021-12-22 11:26:36 +01:00
Michael Auer
1dc979baaf merge branch 'release/1.5' 2021-12-22 11:24:24 +01:00
Michael Auer
f9908eaa57 merge branch 'release/1.5' 2021-12-22 11:23:09 +01:00
Michael Auer
8e8150e246 merge branch 'release/1.5' 2021-12-22 11:21:34 +01:00
Andreas Schickinger
19fe83ed25 Merged PR 1018: #2555 Hotfix Historie und Print Dialog doppelte Scrollbar
#2555 Hotfix Historie und Print Dialog doppelte Scrollbar

Related work items: #2555
2021-12-20 12:06:44 +00:00
Lorenz Hilpert
3c033a1f0d Merged PR 1017: #2556 Fehlermeldung - scrollPos
#2556 Fehlermeldung  - scrollPos

Related work items: #2556
2021-12-20 09:57:50 +00:00
Andreas Schickinger
be031fb702 Merged PR 1016: #2543 Hotfix PDP SSC Fehlermeldung
#2543 Hotfix PDP SSC Fehlermeldung

Related work items: #2543
2021-12-16 13:24:06 +00:00
Lorenz Hilpert
34a6c6d997 Merged PR 1015: #2542 bei leere Liste wirf klick auf Abholfach Fehler
#2542 bei leere Liste wirf klick auf Abholfach Fehler

Related work items: #2542
2021-12-15 16:33:34 +00:00
Lorenz Hilpert
fa2c0d101f Merged PR 1014: #2541 Load-Ansicht dauerhaft bei leere Liste
#2541 Load-Ansicht dauerhaft bei leere Liste

Related work items: #2541
2021-12-15 15:01:00 +00:00
Lorenz Hilpert
c3f6cef14e Merged PR 1013: #2533 Reservierungsliste - bei leerer Liste wirf klick auf Abholfach Fehler
#2533  Reservierungsliste - bei leerer Liste wirf klick auf Abholfach Fehler

Related work items: #2533
2021-12-15 14:56:41 +00:00
Andreas Schickinger
67bcb25106 Merged PR 1012: #2534 Hotfix - WA WE Meldenummern ändern Item wird vor Patch Aufruf aktualisiert
#2534 Hotfix - WA WE Meldenummern ändern Item wird vor Patch Aufruf aktualisiert

Related work items: #2534
2021-12-14 12:33:06 +00:00
Lorenz Hilpert
6cfbce4f16 Merge branch 'release/1.5' into develop 2021-12-10 10:50:36 +01:00
Lorenz Hilpert
cc65a29b05 Merge branch 'develop' into release/1.5 2021-12-10 10:50:10 +01:00
Nino Righi
79d1cb7e87 Merged PR 1008: #2503 Fix Undefined query Params
#2503 Fix Undefined query Params
2021-12-09 10:33:18 +00:00
Lorenz Hilpert
925b0e75db #2454 Suchfeld bei Remi abschluss auf IPad 6 ausblenden 2021-12-09 11:17:22 +01:00
Nino Righi
85b6439d5f Merged PR 1007: #2122 Patch Request - CTA Positioning
#2122 Patch Request - CTA Positioning
2021-12-08 13:11:27 +00:00
Nino Righi
8d75d2e9c9 Merged PR 1006: #2503 #2514 Kundenkarte Kundendaten Erfassen
#2503 #2514 Kundenkarte Kundendaten Erfassen
2021-12-08 09:54:41 +00:00
Lorenz Hilpert
af7e2298b5 #2475 - Button anzeige - bei Nullbestand CTA anzeigen 2021-12-08 10:49:20 +01:00
Andreas Schickinger
11fd851f21 Merged PR 1005: #2122 WA/WE Erneut senden Button Scroll Verhalten fix
#2122 WA/WE Erneut senden Button Scroll Verhalten fix

Related work items: #2122
2021-12-07 14:59:02 +00:00
Andreas Schickinger
2360a63fcb Merged PR 1004: #2501 WE WA Vormerker änderbar
#2501 WE WA Vormerker änderbar

Related work items: #2501
2021-12-07 13:39:52 +00:00
Andreas Schickinger
20e8433963 Merged PR 1003: #2507 Reservieren in andere Filiale Hotfix
#2507 Reservieren in andere Filiale Hotfix
Wenn für die zugeordnete Filiale die InStock und Store Availability nichts zurückliefern, wurden die Branches nicht geladen. Wird dem Purchasing Modal von außen als vorausgewählte Option "Take-Away" oder "Pick-Up" mitgegeben, werden die Branches nun auch geladen.

Related work items: #2507
2021-12-07 12:27:37 +00:00
Andreas Schickinger
c655c1b90b Merged PR 1002: #2502 Kundensuche Dialog
#2502 Kundensuche Dialog

Related work items: #2502
2021-12-07 12:27:03 +00:00
Nino Righi
215e542516 Merged PR 1001: #2122 WA/WE Kunde erneut benachrichtigen Implementierung
#2122 WA/WE Kunde erneut benachrichtigen Implementierung
2021-12-07 12:26:25 +00:00
Michael Auer
680ab2d92a Merge branch 'master' into develop 2021-12-07 11:11:33 +01:00
Lorenz Hilpert
e5dfc2484b Merge branch 'develop' of https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend into develop 2021-12-06 14:33:59 +01:00
Lorenz Hilpert
1962f47f81 #2475 - Abholfach - bei Nullbestand CTA anzeigen 2021-12-06 14:33:51 +01:00
Andreas Schickinger
6b9ea288d1 Merged PR 1000: #2069 WE WA Archiv Filter Detailseite anzeigen
#2069 WE WA Archiv Filter Detailseite anzeigen

Related work items: #2069
2021-12-06 13:29:18 +00:00
Nino Righi
baeaec54c5 Merged PR 999: #2484 AHF Routing to Details After Clicking an Item
#2484 AHF Routing to Details After Clicking an Item
2021-12-03 12:07:29 +00:00
Nino Righi
7272415ed4 Merged PR 998: #2496 PDP Fix Display No Supplier Available if No Ssc and SscText is available
#2496 PDP Fix Display No Supplier Available if No Ssc and SscText is available
2021-12-02 17:02:26 +00:00
Andreas Schickinger
c75d956c52 Merged PR 996: #2373 Scroll Position Handling
#2373 Wareneingang, Warenausgang, Reservierungen, Abholfachbereinigungsliste Scroll Position Handling

Related work items: #2373
2021-12-02 16:24:45 +00:00
Lorenz Hilpert
99d8084fe3 Merged PR 997: #2137 Übernahme der Email und des Nachnamens zu Kundendaten erfassen
#2137 Übernahme der Email und des Nachnamens zu Kundendaten erfassen
2021-12-02 16:24:01 +00:00
Lorenz Hilpert
e61601fca2 Merged PR 994: #2383 WE // Sortierung auf der Wareneingangsliste
Related work items: #2383
2021-12-02 16:20:44 +00:00
Andreas Schickinger
0cfeabb0f2 Merged PR 995: #2486 Erscheinungsdatum führende Null bei Format
#2486 Erscheinungsdatum führende Null bei Format

Related work items: #2486
2021-12-02 13:11:52 +00:00
Lorenz Hilpert
d059646ebe #2450 ui fehler - WE Titel Breite 2021-12-02 11:29:05 +01:00
Andreas Schickinger
553c5cd1c1 Merged PR 993: #2446 Erscheinungsdatum Bugfix wenn kein publicationDate existiert
#2446 Erscheinungsdatum Bugfix wenn kein publicationDate existiert

Related work items: #2446
2021-12-01 16:14:28 +00:00
Nino Righi
ba0a4f1bf9 Merged PR 991: #2383 Sortierung Wareneingangsliste
#2383 Sortierung Wareneingangsliste
2021-12-01 14:59:52 +00:00
Lorenz Hilpert
614e8f0ccc #2450 Lesealter breite ipad 6 2021-12-01 15:57:58 +01:00
Andreas Schickinger
428a905caa Merged PR 992: #2373 UiScrollContainer ScrollTo Methode
#2373 UiScrollContainer ScrollTo Methode

Related work items: #2373
2021-12-01 14:09:37 +00:00
Andreas Schickinger
6193306255 Merged PR 990: #2446 Artikelsuche Artikeldetails Erscheinungsdatum angepasst
#2446 Artikelsuche Artikeldetails Erscheinungsdatum angepasst

Related work items: #2446
2021-11-29 16:54:40 +00:00
Andreas Schickinger
b2913f9fa0 Merged PR 989: #2455 Warenkorb undefined wird nicht angezeigt
#2455 Warenkorb undefined wird nicht angezeigt

Related work items: #2455
2021-11-29 14:15:27 +00:00
Lorenz Hilpert
ce5ec1d961 #2454 ipad6 - Remission Scan Button wird nicht angezeigt 2021-11-29 14:23:04 +01:00
Lorenz Hilpert
96725d1730 #2290 Remi Screens am Desktop verschoben 2021-11-29 14:16:22 +01:00
Lorenz Hilpert
eec018de3c Merge branch 'develop' of https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend into develop 2021-11-29 14:06:55 +01:00
Lorenz Hilpert
f753d33731 #2328 AHF Status versendet und zugestellt leeres Blatt 2021-11-29 14:06:42 +01:00
Lorenz Hilpert
ffd69fbc75 Merged PR 933: Checkout Dummy Modal Implementation
Related work items: #2315
2021-11-29 12:56:51 +00:00
Lorenz Hilpert
fe60370c0a Merge branch 'release/1.5' into develop 2021-11-26 14:06:17 +01:00
1568 changed files with 95587 additions and 14514 deletions

3
.gitignore vendored
View File

@@ -34,6 +34,7 @@ speed-measure-plugin.json
/.sass-cache
/connect.lock
/coverage
/testresults
/libpeerconnection.log
npm-debug.log
yarn-error.log
@@ -45,4 +46,4 @@ testem.log
.DS_Store
Thumbs.db
libs/swagger/src/lib/*
libs/swagger/src/lib/*

View File

@@ -5,6 +5,7 @@
/helmvalues
/apps/swagger
/ng-swagger-gen
/apps/isa-app/src/assets
*.json
*.yml

View File

@@ -3,7 +3,6 @@
"johnpapa.angular2",
"esbenp.prettier-vscode",
"angular.ng-template",
"ms-vscode.vscode-typescript-tslint-plugin",
"eg2.vscode-npm-script"
]
}
}

View File

@@ -1,5 +1,5 @@
#stage 1
FROM node:14 as node
FROM node:14 as base
ARG IS_PRODUCTION=false
ARG SEMVERSION=1.0.0
ARG BuildUniqueID
@@ -11,9 +11,19 @@ RUN npm version ${SEMVERSION}
RUN npm install --always-auth=false
RUN if [ "${IS_PRODUCTION}" = "true" ] ; then npm run-script build-prod ; else npm run-script build ; fi
# stage 2
FROM nginx:alpine
# stage final
FROM nginx:alpine as publish
ARG BuildUniqueID
LABEL build.uniqueid="${BuildUniqueID:-1}"
COPY --from=node /app/dist/sales /usr/share/nginx/html
COPY --from=node /app/nginx.conf /etc/nginx/conf.d/default.conf
COPY --from=base /app/dist/isa-app /usr/share/nginx/html
COPY --from=base /app/nginx.conf /etc/nginx/conf.d/default.conf
# stage npm test
FROM base as test
ARG BuildUniqueID
LABEL build.uniqueid="${BuildUniqueID:-1}"
RUN wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb -q -O /tmp/chrome.deb && apt update && apt install -y /tmp/chrome.deb
# ignore exitcode, sonst gibts keinen container
RUN npm test || true
ENTRYPOINT [ "/bin/sleep", "60000" ]

View File

@@ -10,7 +10,7 @@
"prefix": "lib",
"architect": {
"build": {
"builder": "@angular-devkit/build-ng-packagr:build",
"builder": "@angular-devkit/build-angular:ng-packagr",
"options": {
"tsConfig": "libs/ui/tsconfig.lib.json",
"project": "libs/ui/ng-package.json"
@@ -55,7 +55,6 @@
"options": {
"aot": true,
"outputPath": "dist/sales",
"outputHashing": "all",
"index": "apps/sales/src/index.html",
"main": "apps/sales/src/main.ts",
"polyfills": "apps/sales/src/polyfills.ts",
@@ -123,10 +122,7 @@
"serve": {
"builder": "@angular-builders/custom-webpack:dev-server",
"options": {
"browserTarget": "sales:build",
"customWebpackConfig": {
"path": "apps/sales/webpack.config.js"
}
"browserTarget": "sales:build"
},
"configurations": {
"test": {
@@ -224,7 +220,7 @@
"prefix": "lib",
"architect": {
"build": {
"builder": "@angular-devkit/build-ng-packagr:build",
"builder": "@angular-devkit/build-angular:ng-packagr",
"options": {
"tsConfig": "libs/sso/tsconfig.lib.json",
"project": "libs/sso/ng-package.json"
@@ -264,7 +260,7 @@
"prefix": "lib",
"architect": {
"build": {
"builder": "@angular-devkit/build-ng-packagr:build",
"builder": "@angular-devkit/build-angular:ng-packagr",
"options": {
"tsConfig": "apps/swagger/availability/tsconfig.lib.json",
"project": "apps/swagger/availability/ng-package.json"
@@ -304,7 +300,7 @@
"prefix": "lib",
"architect": {
"build": {
"builder": "@angular-devkit/build-ng-packagr:build",
"builder": "@angular-devkit/build-angular:ng-packagr",
"options": {
"tsConfig": "apps/swagger/checkout/tsconfig.lib.json",
"project": "apps/swagger/checkout/ng-package.json"
@@ -344,7 +340,7 @@
"prefix": "lib",
"architect": {
"build": {
"builder": "@angular-devkit/build-ng-packagr:build",
"builder": "@angular-devkit/build-angular:ng-packagr",
"options": {
"tsConfig": "apps/swagger/crm/tsconfig.lib.json",
"project": "apps/swagger/crm/ng-package.json"
@@ -384,7 +380,7 @@
"prefix": "lib",
"architect": {
"build": {
"builder": "@angular-devkit/build-ng-packagr:build",
"builder": "@angular-devkit/build-angular:ng-packagr",
"options": {
"tsConfig": "apps/swagger/isa/tsconfig.lib.json",
"project": "apps/swagger/isa/ng-package.json"
@@ -424,7 +420,7 @@
"prefix": "lib",
"architect": {
"build": {
"builder": "@angular-devkit/build-ng-packagr:build",
"builder": "@angular-devkit/build-angular:ng-packagr",
"options": {
"tsConfig": "apps/swagger/oms/tsconfig.lib.json",
"project": "apps/swagger/oms/ng-package.json"
@@ -464,7 +460,7 @@
"prefix": "lib",
"architect": {
"build": {
"builder": "@angular-devkit/build-ng-packagr:build",
"builder": "@angular-devkit/build-angular:ng-packagr",
"options": {
"tsConfig": "apps/swagger/print/tsconfig.lib.json",
"project": "apps/swagger/print/ng-package.json"
@@ -504,7 +500,7 @@
"prefix": "lib",
"architect": {
"build": {
"builder": "@angular-devkit/build-ng-packagr:build",
"builder": "@angular-devkit/build-angular:ng-packagr",
"options": {
"tsConfig": "apps/swagger/cat/tsconfig.lib.json",
"project": "apps/swagger/cat/ng-package.json"
@@ -544,7 +540,7 @@
"prefix": "lib",
"architect": {
"build": {
"builder": "@angular-devkit/build-ng-packagr:build",
"builder": "@angular-devkit/build-angular:ng-packagr",
"options": {
"tsConfig": "apps/swagger/eis/tsconfig.lib.json",
"project": "apps/swagger/eis/ng-package.json"
@@ -584,7 +580,7 @@
"prefix": "lib",
"architect": {
"build": {
"builder": "@angular-devkit/build-ng-packagr:build",
"builder": "@angular-devkit/build-angular:ng-packagr",
"options": {
"tsConfig": "apps/native-container/tsconfig.lib.json",
"project": "apps/native-container/ng-package.json"
@@ -624,7 +620,7 @@
"prefix": "lib",
"architect": {
"build": {
"builder": "@angular-devkit/build-ng-packagr:build",
"builder": "@angular-devkit/build-angular:ng-packagr",
"options": {
"tsConfig": "apps/isa/remission/tsconfig.lib.json",
"project": "apps/isa/remission/ng-package.json"
@@ -664,7 +660,7 @@
"prefix": "crm",
"architect": {
"build": {
"builder": "@angular-devkit/build-ng-packagr:build",
"builder": "@angular-devkit/build-angular:ng-packagr",
"options": {
"tsConfig": "apps/domain/crm/tsconfig.lib.json",
"project": "apps/domain/crm/ng-package.json"
@@ -704,7 +700,7 @@
"prefix": "checkout",
"architect": {
"build": {
"builder": "@angular-devkit/build-ng-packagr:build",
"builder": "@angular-devkit/build-angular:ng-packagr",
"options": {
"tsConfig": "apps/domain/checkout/tsconfig.lib.json",
"project": "apps/domain/checkout/ng-package.json"
@@ -3296,7 +3292,559 @@
}
}
}
},
"isa-app": {
"projectType": "application",
"schematics": {
"@schematics/angular:component": {
"style": "scss"
},
"@schematics/angular:application": {
"strict": true
}
},
"root": "apps/isa-app",
"sourceRoot": "apps/isa-app/src",
"prefix": "app",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:browser",
"options": {
"outputPath": "dist/isa-app",
"index": "apps/isa-app/src/index.html",
"main": "apps/isa-app/src/main.ts",
"polyfills": "apps/isa-app/src/polyfills.ts",
"tsConfig": "apps/isa-app/tsconfig.app.json",
"inlineStyleLanguage": "scss",
"assets": [
"apps/isa-app/src/favicon.ico",
"apps/isa-app/src/assets",
"apps/isa-app/src/config",
"apps/isa-app/src/silent-refresh.html",
"apps/isa-app/src/manifest.webmanifest"
],
"styles": [
"apps/isa-app/src/styles.scss"
],
"scripts": [],
"serviceWorker": true,
"ngswConfigPath": "apps/isa-app/ngsw-config.json"
},
"configurations": {
"production": {
"budgets": [
{
"type": "initial",
"maximumWarning": "500kb",
"maximumError": "1mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "2kb",
"maximumError": "4kb"
}
],
"fileReplacements": [
{
"replace": "apps/isa-app/src/environments/environment.ts",
"with": "apps/isa-app/src/environments/environment.prod.ts"
}
],
"outputHashing": "all"
},
"development": {
"buildOptimizer": false,
"optimization": false,
"vendorChunk": true,
"extractLicenses": false,
"sourceMap": true,
"namedChunks": true
}
},
"defaultConfiguration": "production"
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"configurations": {
"production": {
"browserTarget": "isa-app:build:production"
},
"development": {
"browserTarget": "isa-app:build:development"
}
},
"defaultConfiguration": "development"
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"browserTarget": "isa-app:build"
}
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"main": "apps/isa-app/src/test.ts",
"polyfills": "apps/isa-app/src/polyfills.ts",
"tsConfig": "apps/isa-app/tsconfig.spec.json",
"karmaConfig": "apps/isa-app/karma.conf.js",
"inlineStyleLanguage": "scss",
"assets": [
"apps/isa-app/src/favicon.ico",
"apps/isa-app/src/assets",
"apps/isa-app/src/manifest.webmanifest"
],
"styles": [
"apps/isa-app/src/styles.scss"
],
"scripts": []
}
}
}
},
"@core/config": {
"projectType": "library",
"root": "apps/core/config",
"sourceRoot": "apps/core/config/src",
"prefix": "lib",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:ng-packagr",
"options": {
"project": "apps/core/config/ng-package.json"
},
"configurations": {
"production": {
"tsConfig": "apps/core/config/tsconfig.lib.prod.json"
},
"development": {
"tsConfig": "apps/core/config/tsconfig.lib.json"
}
},
"defaultConfiguration": "production"
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"main": "apps/core/config/src/test.ts",
"tsConfig": "apps/core/config/tsconfig.spec.json",
"karmaConfig": "apps/core/config/karma.conf.js"
}
}
}
},
"@core/auth": {
"projectType": "library",
"root": "apps/core/auth",
"sourceRoot": "apps/core/auth/src",
"prefix": "lib",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:ng-packagr",
"options": {
"project": "apps/core/auth/ng-package.json"
},
"configurations": {
"production": {
"tsConfig": "apps/core/auth/tsconfig.lib.prod.json"
},
"development": {
"tsConfig": "apps/core/auth/tsconfig.lib.json"
}
},
"defaultConfiguration": "production"
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"main": "apps/core/auth/src/test.ts",
"tsConfig": "apps/core/auth/tsconfig.spec.json",
"karmaConfig": "apps/core/auth/karma.conf.js"
}
}
}
},
"@shell/footer": {
"projectType": "library",
"root": "apps/shell/footer",
"sourceRoot": "apps/shell/footer/src",
"prefix": "lib",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:ng-packagr",
"options": {
"project": "apps/shell/footer/ng-package.json"
},
"configurations": {
"production": {
"tsConfig": "apps/shell/footer/tsconfig.lib.prod.json"
},
"development": {
"tsConfig": "apps/shell/footer/tsconfig.lib.json"
}
},
"defaultConfiguration": "production"
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"main": "apps/shell/footer/src/test.ts",
"tsConfig": "apps/shell/footer/tsconfig.spec.json",
"karmaConfig": "apps/shell/footer/karma.conf.js"
}
}
}
},
"@shell/process": {
"projectType": "library",
"root": "apps/shell/process",
"sourceRoot": "apps/shell/process/src",
"prefix": "lib",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:ng-packagr",
"options": {
"project": "apps/shell/process/ng-package.json"
},
"configurations": {
"production": {
"tsConfig": "apps/shell/process/tsconfig.lib.prod.json"
},
"development": {
"tsConfig": "apps/shell/process/tsconfig.lib.json"
}
},
"defaultConfiguration": "production"
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"main": "apps/shell/process/src/test.ts",
"tsConfig": "apps/shell/process/tsconfig.spec.json",
"karmaConfig": "apps/shell/process/karma.conf.js"
}
}
}
},
"@page/dashboard": {
"projectType": "library",
"root": "apps/page/dashboard",
"sourceRoot": "apps/page/dashboard/src",
"prefix": "page",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:ng-packagr",
"options": {
"project": "apps/page/dashboard/ng-package.json"
},
"configurations": {
"production": {
"tsConfig": "apps/page/dashboard/tsconfig.lib.prod.json"
},
"development": {
"tsConfig": "apps/page/dashboard/tsconfig.lib.json"
}
},
"defaultConfiguration": "production"
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"main": "apps/page/dashboard/src/test.ts",
"tsConfig": "apps/page/dashboard/tsconfig.spec.json",
"karmaConfig": "apps/page/dashboard/karma.conf.js"
}
}
}
},
"@domain/isa": {
"projectType": "library",
"root": "apps/domain/isa",
"sourceRoot": "apps/domain/isa/src",
"prefix": "lib",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:ng-packagr",
"options": {
"project": "apps/domain/isa/ng-package.json"
},
"configurations": {
"production": {
"tsConfig": "apps/domain/isa/tsconfig.lib.prod.json"
},
"development": {
"tsConfig": "apps/domain/isa/tsconfig.lib.json"
}
},
"defaultConfiguration": "production"
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"main": "apps/domain/isa/src/test.ts",
"tsConfig": "apps/domain/isa/tsconfig.spec.json",
"karmaConfig": "apps/domain/isa/karma.conf.js"
}
}
}
},
"@core/logger": {
"projectType": "library",
"root": "apps/core/logger",
"sourceRoot": "apps/core/logger/src",
"prefix": "lib",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:ng-packagr",
"options": {
"project": "apps/core/logger/ng-package.json"
},
"configurations": {
"production": {
"tsConfig": "apps/core/logger/tsconfig.lib.prod.json"
},
"development": {
"tsConfig": "apps/core/logger/tsconfig.lib.json"
}
},
"defaultConfiguration": "production"
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"main": "apps/core/logger/src/test.ts",
"tsConfig": "apps/core/logger/tsconfig.spec.json",
"karmaConfig": "apps/core/logger/karma.conf.js"
}
}
}
},
"@shell/filter-overlay": {
"projectType": "library",
"root": "apps/shell/filter-overlay",
"sourceRoot": "apps/shell/filter-overlay/src",
"prefix": "lib",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:ng-packagr",
"options": {
"project": "apps/shell/filter-overlay/ng-package.json"
},
"configurations": {
"production": {
"tsConfig": "apps/shell/filter-overlay/tsconfig.lib.prod.json"
},
"development": {
"tsConfig": "apps/shell/filter-overlay/tsconfig.lib.json"
}
},
"defaultConfiguration": "production"
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"main": "apps/shell/filter-overlay/src/test.ts",
"tsConfig": "apps/shell/filter-overlay/tsconfig.spec.json",
"karmaConfig": "apps/shell/filter-overlay/karma.conf.js"
}
}
}
},
"@store/search-component-store": {
"projectType": "library",
"root": "apps/store/search-component-store",
"sourceRoot": "apps/store/search-component-store/src",
"prefix": "lib",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:ng-packagr",
"options": {
"project": "apps/store/search-component-store/ng-package.json"
},
"configurations": {
"production": {
"tsConfig": "apps/store/search-component-store/tsconfig.lib.prod.json"
},
"development": {
"tsConfig": "apps/store/search-component-store/tsconfig.lib.json"
}
},
"defaultConfiguration": "production"
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"main": "apps/store/search-component-store/src/test.ts",
"tsConfig": "apps/store/search-component-store/tsconfig.spec.json",
"karmaConfig": "apps/store/search-component-store/karma.conf.js"
}
}
}
},
"@domain/remission": {
"projectType": "library",
"root": "apps/domain/remission",
"sourceRoot": "apps/domain/remission/src",
"prefix": "lib",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:ng-packagr",
"options": {
"project": "apps/domain/remission/ng-package.json"
},
"configurations": {
"production": {
"tsConfig": "apps/domain/remission/tsconfig.lib.prod.json"
},
"development": {
"tsConfig": "apps/domain/remission/tsconfig.lib.json"
}
},
"defaultConfiguration": "production"
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"main": "apps/domain/remission/src/test.ts",
"tsConfig": "apps/domain/remission/tsconfig.spec.json",
"karmaConfig": "apps/domain/remission/karma.conf.js"
}
}
}
},
"@page/remission": {
"projectType": "library",
"root": "apps/page/remission",
"sourceRoot": "apps/page/remission/src",
"prefix": "page",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:ng-packagr",
"options": {
"project": "apps/page/remission/ng-package.json"
},
"configurations": {
"production": {
"tsConfig": "apps/page/remission/tsconfig.lib.prod.json"
},
"development": {
"tsConfig": "apps/page/remission/tsconfig.lib.json"
}
},
"defaultConfiguration": "production"
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"main": "apps/page/remission/src/test.ts",
"tsConfig": "apps/page/remission/tsconfig.spec.json",
"karmaConfig": "apps/page/remission/karma.conf.js"
}
}
}
},
"@modal/notifications": {
"projectType": "library",
"root": "apps/modal/notifications",
"sourceRoot": "apps/modal/notifications/src",
"prefix": "lib",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:ng-packagr",
"options": {
"project": "apps/modal/notifications/ng-package.json"
},
"configurations": {
"production": {
"tsConfig": "apps/modal/notifications/tsconfig.lib.prod.json"
},
"development": {
"tsConfig": "apps/modal/notifications/tsconfig.lib.json"
}
},
"defaultConfiguration": "production"
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"main": "apps/modal/notifications/src/test.ts",
"tsConfig": "apps/modal/notifications/tsconfig.spec.json",
"karmaConfig": "apps/modal/notifications/karma.conf.js"
}
}
}
},
"@ui/branch-dropdown": {
"projectType": "library",
"root": "apps/ui/branch-dropdown",
"sourceRoot": "apps/ui/branch-dropdown/src",
"prefix": "lib",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:ng-packagr",
"options": {
"tsConfig": "apps/ui/branch-dropdown/tsconfig.lib.json",
"project": "apps/ui/branch-dropdown/ng-package.json"
},
"configurations": {
"production": {
"tsConfig": "apps/ui/branch-dropdown/tsconfig.lib.prod.json"
}
}
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"main": "apps/ui/branch-dropdown/src/test.ts",
"tsConfig": "apps/ui/branch-dropdown/tsconfig.spec.json",
"karmaConfig": "apps/ui/branch-dropdown/karma.conf.js"
}
},
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": [
"apps/ui/branch-dropdown/tsconfig.lib.json",
"apps/ui/branch-dropdown/tsconfig.spec.json"
],
"exclude": [
"**/node_modules/**"
]
}
}
}
},
"@adapter/scan": {
"projectType": "library",
"root": "apps/adapter/scan",
"sourceRoot": "apps/adapter/scan/src",
"prefix": "lib",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:ng-packagr",
"options": {
"project": "apps/adapter/scan/ng-package.json"
},
"configurations": {
"production": {
"tsConfig": "apps/adapter/scan/tsconfig.lib.prod.json"
},
"development": {
"tsConfig": "apps/adapter/scan/tsconfig.lib.json"
}
},
"defaultConfiguration": "production"
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"main": "apps/adapter/scan/src/test.ts",
"tsConfig": "apps/adapter/scan/tsconfig.spec.json",
"karmaConfig": "apps/adapter/scan/karma.conf.js"
}
}
}
}
},
"defaultProject": "sales"
"defaultProject": "isa-app"
}

View File

@@ -0,0 +1,25 @@
# Scan
This library was generated with [Angular CLI](https://github.com/angular/angular-cli) version 12.2.0.
## Code scaffolding
Run `ng generate component component-name --project scan` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module --project scan`.
> Note: Don't forget to add `--project scan` or else it will be added to the default project in your `angular.json` file.
## Build
Run `ng build scan` to build the project. The build artifacts will be stored in the `dist/` directory.
## Publishing
After building your library with `ng build scan`, go to the dist folder `cd dist/scan` and run `npm publish`.
## Running unit tests
Run `ng test scan` to execute the unit tests via [Karma](https://karma-runner.github.io).
## Further help
To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page.

View File

@@ -0,0 +1,41 @@
// Karma configuration file, see link for more information
// https://karma-runner.github.io/1.0/config/configuration-file.html
module.exports = function (config) {
config.set({
basePath: '',
frameworks: ['jasmine', '@angular-devkit/build-angular'],
plugins: [
require('karma-jasmine'),
require('karma-chrome-launcher'),
require('karma-jasmine-html-reporter'),
require('karma-coverage'),
require('@angular-devkit/build-angular/plugins/karma'),
],
client: {
jasmine: {
// you can add configuration options for Jasmine here
// the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html
// for example, you can disable the random execution with `random: false`
// or set a specific seed with `seed: 4321`
},
clearContext: false, // leave Jasmine Spec Runner output visible in browser
},
jasmineHtmlReporter: {
suppressAll: true, // removes the duplicated traces
},
coverageReporter: {
dir: require('path').join(__dirname, '../../../coverage/adapter/scan'),
subdir: '.',
reporters: [{ type: 'html' }, { type: 'text-summary' }],
},
reporters: ['progress', 'kjhtml'],
port: 9876,
colors: true,
logLevel: config.LOG_INFO,
autoWatch: true,
browsers: ['Chrome'],
singleRun: false,
restartOnFileChange: true,
});
};

View File

@@ -0,0 +1,7 @@
{
"$schema": "../../../node_modules/ng-packagr/ng-package.schema.json",
"dest": "../../../dist/adapter/scan",
"lib": {
"entryFile": "src/public-api.ts"
}
}

View File

@@ -0,0 +1,11 @@
{
"name": "@adapter/scan",
"version": "0.0.1",
"peerDependencies": {
"@angular/common": "^12.2.0",
"@angular/core": "^12.2.0"
},
"dependencies": {
"tslib": "^2.3.0"
}
}

View File

@@ -0,0 +1,46 @@
import { Injectable } from '@angular/core';
import { PromptModalData, UiModalService, UiPromptModalComponent } from '@ui/modal';
import { Observable } from 'rxjs';
import { ScanAdapter } from './scan-adapter';
@Injectable()
export class DevScanAdapter implements ScanAdapter {
constructor(private _modal: UiModalService) {}
getName(): string {
return 'Dev Scanner';
}
isPrimary(): boolean {
return true;
}
isReady(): boolean {
return true;
}
scan(): Observable<string> {
return new Observable((observer) => {
const modalRef = this._modal.open({
content: UiPromptModalComponent,
title: 'Scannen',
data: {
message: 'Diese Eingabemaske dient nur zu Entwicklungs und Testzwecken.',
placeholder: 'Scan Code',
confirmText: 'weiter',
cancelText: 'abbrechen',
} as PromptModalData,
});
const sub = modalRef.afterClosed$.subscribe((result) => {
observer.next(result.data);
observer.complete();
});
return () => {
modalRef.close();
sub.unsubscribe();
};
});
}
}

View File

@@ -0,0 +1,32 @@
import { Injectable } from '@angular/core';
import { NativeContainerService } from 'native-container';
import { Observable } from 'rxjs';
import { filter, map, take } from 'rxjs/operators';
import { ScanAdapter } from './scan-adapter';
@Injectable()
export class NativeScanAdapter implements ScanAdapter {
constructor(private readonly nativeContainerService: NativeContainerService) {}
getName(): string {
return 'Native Scanner';
}
isPrimary(): boolean {
return true;
}
isReady(): boolean {
// TODO: Fix Login Keycard Dauerschleife
return this.nativeContainerService.isUiWebview().isNative;
// return false;
}
scan(): Observable<string> {
return this.nativeContainerService.openScanner('scanBook').pipe(
filter((result) => result.status === 'SUCCESS'),
map((result) => result.data),
take(1)
);
}
}

View File

@@ -0,0 +1,11 @@
import { Observable } from 'rxjs';
export interface ScanAdapter {
getName(): string;
isPrimary(): boolean;
isReady(): boolean;
scan(): Observable<string>;
}

View File

@@ -0,0 +1,26 @@
import { NgModule } from '@angular/core';
import { DevScanAdapter } from './dev.scan-adapter';
import { NativeScanAdapter } from './native.scan-adapter';
import { ScanditModalModule } from './scandit-modal';
import { ScanditScanAdapter } from './scandit.scan-adapter';
import { SCAN_ADAPTER } from './tokens';
import { ZxingModalModule } from './zxing-modal';
import { ZxingScanAdapter } from './zxing.scan-adapter';
@NgModule({
imports: [ScanditModalModule, ZxingModalModule],
})
export class ScanAdapterModule {
static forRoot(dev?: boolean) {
return {
ngModule: ScanAdapterModule,
providers: [
{ provide: SCAN_ADAPTER, useClass: NativeScanAdapter, multi: true },
// { provide: SCAN_ADAPTER, useClass: ScanditScanAdapter, multi: true },
{ provide: SCAN_ADAPTER, useClass: ZxingScanAdapter, multi: true },
],
// Use for testing:
// providers: [{ provide: SCAN_ADAPTER, useClass: dev ? DevScanAdapter : NativeScanAdapter, multi: true }],
};
}
}

View File

@@ -0,0 +1,30 @@
import { Inject, Injectable } from '@angular/core';
import { ScanAdapter } from './scan-adapter';
import { SCAN_ADAPTER } from './tokens';
@Injectable({
providedIn: 'root',
})
export class ScanAdapterService {
constructor(@Inject(SCAN_ADAPTER) private readonly scanAdapters: ScanAdapter[]) {}
scanners() {
return this.scanAdapters.filter((adapter) => adapter.isReady());
}
scanner() {
return this.scanners().find((scanner) => scanner.isPrimary()) || this.scanners().find(() => true);
}
isReady() {
return this.scanAdapters.some((adapter) => adapter.isReady());
}
scan() {
const primaryScanner = this.scanner();
if (primaryScanner) {
return primaryScanner.scan();
}
return null;
}
}

View File

@@ -0,0 +1,2 @@
export * from './scandit-modal.component';
export * from './scandit-modal.module';

View File

@@ -0,0 +1 @@
<div class="scanner-container" #scanContainer></div>

View File

@@ -0,0 +1,4 @@
.scanner-container {
@apply mt-8;
max-height: calc(100vh - 10rem);
}

View File

@@ -0,0 +1,86 @@
import { Component, ChangeDetectionStrategy, ViewChild, ElementRef, AfterViewInit, NgZone, Inject, OnDestroy } from '@angular/core';
import { UiMessageModalComponent, UiModalRef, UiModalService } from '@ui/modal';
import { Barcode, BarcodePicker, ScanSettings } from 'scandit-sdk';
@Component({
selector: 'scandit-modal',
templateUrl: 'scandit-modal.component.html',
styleUrls: ['scandit-modal.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ScanditModalComponent implements AfterViewInit, OnDestroy {
private _barcodePicker: BarcodePicker;
@ViewChild('scanContainer', { read: ElementRef, static: true }) scanContainer: ElementRef;
constructor(private _modalRef: UiModalRef, private readonly _zone: NgZone, private readonly _modal: UiModalService) {}
ngAfterViewInit(): void {
this._zone.runOutsideAngular(() => {
BarcodePicker.create(this.scanContainer.nativeElement, {
playSoundOnScan: true,
vibrateOnScan: true,
})
.then(async (picker) => {
this._barcodePicker = picker;
var scanSettings = new ScanSettings({
blurryRecognition: false,
enabledSymbologies: [
Barcode.Symbology.EAN8,
Barcode.Symbology.EAN13,
Barcode.Symbology.UPCA,
Barcode.Symbology.UPCE,
Barcode.Symbology.CODE128,
Barcode.Symbology.CODE39,
Barcode.Symbology.CODE93,
Barcode.Symbology.INTERLEAVED_2_OF_5,
Barcode.Symbology.QR,
],
codeDuplicateFilter: 1000,
});
picker.applyScanSettings(scanSettings);
picker.on('scan', (barcode) => {
this._zone.run(() => {
if (barcode.barcodes.length) {
this._modalRef.close(barcode.barcodes[0].data);
} else if (barcode.texts.length) {
this._modalRef.close(barcode.texts[0].value);
}
});
});
})
.catch((err) => {
this._zone.run(() => {
this.cancel();
this._modal
.open({
content: UiMessageModalComponent,
data: {
message: `
Scanner kann nicht aktiviert werden.
Bitte stellen Sie sicher, dass der zugriff auf die Kamera erlaubt ist.
`,
},
})
.afterClosed$.subscribe(() => {
this.cancel();
});
});
});
});
}
cancel() {
this._modalRef.close();
}
ngOnDestroy(): void {
this._zone.runOutsideAngular(() => {
this._barcodePicker?.destroy(true);
});
}
}

View File

@@ -0,0 +1,11 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ScanditModalComponent } from './scandit-modal.component';
@NgModule({
imports: [CommonModule],
exports: [ScanditModalComponent],
declarations: [ScanditModalComponent],
})
export class ScanditModalModule {}

View File

@@ -0,0 +1,49 @@
import { Injectable } from '@angular/core';
import { UiModalService } from '@ui/modal';
import { Observable } from 'rxjs';
import { ScanAdapter } from './scan-adapter';
import { configure } from 'scandit-sdk';
import { ScanditModalComponent } from './scandit-modal';
import { filter, map } from 'rxjs/operators';
import { Config } from '@core/config';
@Injectable()
export class ScanditScanAdapter implements ScanAdapter {
private _isReady = false;
constructor(private readonly _modal: UiModalService, private readonly _config: Config) {
this.configure();
}
private async configure() {
await configure(this._config.get('licence.scandit'), {
engineLocation: '/assets/scandit/',
});
this._isReady = true;
}
getName(): string {
return 'Scandit';
}
isPrimary(): boolean {
return false;
}
isReady(): boolean {
return this._isReady;
}
scan(): Observable<string> {
return this._modal
.open({
content: ScanditModalComponent,
})
.afterClosed$.pipe(
map((result) => result.data),
filter((result) => !!result)
);
}
}

View File

@@ -0,0 +1,4 @@
import { InjectionToken } from '@angular/core';
import { ScanAdapter } from './scan-adapter';
export const SCAN_ADAPTER = new InjectionToken<ScanAdapter>('SCAN_ADAPTER');

View File

@@ -0,0 +1,2 @@
export * from './zxing-modal.component';
export * from './zxing-modal.module';

View File

@@ -0,0 +1,17 @@
<!-- <select>
<options [value]="dev.deviceId" *ngFor="let dev in devices$ | async">
{{ dev.label }}
</options>
</select> -->
<div class="device-container">
<button
[class.selected]="(selectedDevice$ | async) === dev.deviceId"
*ngFor="let dev of devices$ | async"
(click)="setSelectedDevice(dev.deviceId)"
>
{{ dev.label }}
</button>
</div>
<video class="scanner-container" #scanContainer></video>

View File

@@ -0,0 +1,17 @@
.scanner-container {
// @apply mt-8;
max-height: calc(100vh - 10rem);
width: 100%;
}
button {
@apply bg-white text-brand px-3 py-2;
&.selected {
@apply bg-brand text-white;
}
}
.device-container {
@apply flex flex-row items-center justify-center my-4;
}

View File

@@ -0,0 +1,104 @@
import { Component, ChangeDetectionStrategy, ViewChild, ElementRef, AfterViewInit, NgZone, Inject, OnDestroy, OnInit } from '@angular/core';
import { UiMessageModalComponent, UiModalRef, UiModalService } from '@ui/modal';
import { BarcodeFormat, DecodeHintType, BrowserMultiFormatReader } from '@zxing/library';
import { BehaviorSubject, from, ReplaySubject, Subject } from 'rxjs';
import { filter, takeUntil, tap } from 'rxjs/operators';
@Component({
selector: 'zxing-modal',
templateUrl: 'zxing-modal.component.html',
styleUrls: ['zxing-modal.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ZxingModalComponent implements OnInit, OnDestroy {
private _reader: BrowserMultiFormatReader;
@ViewChild('scanContainer', { read: ElementRef, static: true }) scanContainer: ElementRef;
devices$ = new ReplaySubject<MediaDeviceInfo[]>();
selectedDevice$ = new BehaviorSubject<string>(null);
get selectedDevice() {
return this.selectedDevice$.value;
}
private _onDestroy$ = new Subject<void>();
constructor(private _modalRef: UiModalRef, private readonly _zone: NgZone, private readonly _modal: UiModalService) {}
async ngOnInit() {
try {
const mediaStream: MediaStream = await navigator.mediaDevices.getUserMedia({ video: true, audio: false });
mediaStream.getVideoTracks().forEach((track) => track.stop());
const hints = new Map<DecodeHintType, any>();
const formats = [
BarcodeFormat.EAN_8,
BarcodeFormat.EAN_13,
BarcodeFormat.UPC_A,
BarcodeFormat.UPC_E,
BarcodeFormat.CODE_128,
BarcodeFormat.CODE_39,
BarcodeFormat.CODE_93,
BarcodeFormat.ITF,
BarcodeFormat.QR_CODE,
];
hints.set(DecodeHintType.POSSIBLE_FORMATS, formats);
this._reader = new BrowserMultiFormatReader(hints, 1000);
from(this._reader.listVideoInputDevices()).subscribe((devices) => {
this.devices$.next(devices);
this.selectedDevice$.next(devices[0].deviceId);
});
this.selectedDevice$
.pipe(
takeUntil(this._onDestroy$),
filter((v) => !!v),
tap(() => {
this._reader?.reset();
})
)
.subscribe((device) => {
this._reader.decodeFromVideoDevice(device, this.scanContainer.nativeElement, (result) => {
if (result) {
this._modalRef.close(result.getText());
}
});
});
} catch (error) {
this.cancel();
this._modal
.open({
content: UiMessageModalComponent,
data: {
message: `
Scanner kann nicht aktiviert werden.
Bitte stellen Sie sicher, dass der zugriff auf die Kamera erlaubt ist.
`,
},
})
.afterClosed$.subscribe(() => {
this.cancel();
});
}
}
cancel() {
this._modalRef.close();
}
ngOnDestroy(): void {
this._onDestroy$.next();
this._reader?.reset();
}
setSelectedDevice(id: string) {
this.selectedDevice$.next(id);
}
}

View File

@@ -0,0 +1,11 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ZxingModalComponent } from './zxing-modal.component';
@NgModule({
imports: [CommonModule],
exports: [ZxingModalComponent],
declarations: [ZxingModalComponent],
})
export class ZxingModalModule {}

View File

@@ -0,0 +1,38 @@
import { Injectable } from '@angular/core';
import { UiModalService } from '@ui/modal';
import { Observable } from 'rxjs';
import { ScanAdapter } from './scan-adapter';
import { filter, map } from 'rxjs/operators';
import { Config } from '@core/config';
import { ZxingModalComponent } from './zxing-modal/zxing-modal.component';
@Injectable()
export class ZxingScanAdapter implements ScanAdapter {
private _isReady = true;
constructor(private readonly _modal: UiModalService, private readonly _config: Config) {}
getName(): string {
return 'Zxing';
}
isPrimary(): boolean {
return false;
}
isReady(): boolean {
return this._isReady;
}
scan(): Observable<string> {
return this._modal
.open({
content: ZxingModalComponent,
})
.afterClosed$.pipe(
map((result) => result.data),
filter((result) => !!result)
);
}
}

View File

@@ -0,0 +1,6 @@
/*
* Public API Surface of scan
*/
export * from './lib/scan.service';
export * from './lib/scan.module';

View File

@@ -0,0 +1,25 @@
// This file is required by karma.conf.js and loads recursively all the .spec and framework files
import 'zone.js';
import 'zone.js/testing';
import { getTestBed } from '@angular/core/testing';
import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing';
declare const require: {
context(
path: string,
deep?: boolean,
filter?: RegExp
): {
keys(): string[];
<T>(id: string): T;
};
};
// First, initialize the Angular testing environment.
getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting(), { teardown: { destroyAfterEach: true } });
// Then we find all the tests.
const context = require.context('./', true, /\.spec\.ts$/);
// And load the modules.
context.keys().map(context);

View File

@@ -0,0 +1,20 @@
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
"extends": "../../../tsconfig.json",
"compilerOptions": {
"outDir": "../../../out-tsc/lib",
"target": "es2015",
"declaration": true,
"declarationMap": true,
"inlineSources": true,
"types": [],
"lib": [
"dom",
"es2018"
]
},
"exclude": [
"src/test.ts",
"**/*.spec.ts"
]
}

View File

@@ -0,0 +1,10 @@
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
"extends": "./tsconfig.lib.json",
"compilerOptions": {
"declarationMap": false
},
"angularCompilerOptions": {
"compilationMode": "partial"
}
}

View File

@@ -0,0 +1,17 @@
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
"extends": "../../../tsconfig.json",
"compilerOptions": {
"outDir": "../../../out-tsc/spec",
"types": [
"jasmine"
]
},
"files": [
"src/test.ts"
],
"include": [
"**/*.spec.ts",
"**/*.d.ts"
]
}

View File

@@ -1,11 +1,12 @@
import { Inject, Injectable } from '@angular/core';
import { Config } from '@core/config';
import { CDN_PRODUCT_IMAGE } from './tokens';
@Injectable({
providedIn: 'root',
})
export class ProductImageService {
constructor(@Inject(CDN_PRODUCT_IMAGE) private imageUrl: string) {}
constructor(private readonly _config: Config) {}
getImageUrl({
imageId,
@@ -18,6 +19,6 @@ export class ProductImageService {
height?: number;
showDummy?: boolean;
}): string {
return `${this.imageUrl}/${imageId}_${width}x${height}.jpg?showDummy=${showDummy}`;
return `${this._config.get('@cdn/product-image.url')}/${imageId}_${width}x${height}.jpg?showDummy=${showDummy}`;
}
}

View File

@@ -1,7 +1,7 @@
// This file is required by karma.conf.js and loads recursively all the .spec and framework files
import 'zone.js/dist/zone';
import 'zone.js/dist/zone-testing';
import 'zone.js';
import 'zone.js/testing';
import { getTestBed } from '@angular/core/testing';
import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing';

View File

@@ -1,5 +1,8 @@
// Karma configuration file, see link for more information
// https://karma-runner.github.io/1.0/config/configuration-file.html
const customLaunchers = require('../../../karma/custom-launchers');
const junitReporter = require('../../../karma/junit-reporter')('core-application');
const coverageReporter = require('../../../karma/coverage-reporter')('core-application');
module.exports = function (config) {
config.set({
@@ -9,23 +12,31 @@ module.exports = function (config) {
require('karma-jasmine'),
require('karma-chrome-launcher'),
require('karma-jasmine-html-reporter'),
require('karma-coverage-istanbul-reporter'),
require('karma-coverage'),
require('karma-junit-reporter'),
require('@angular-devkit/build-angular/plugins/karma'),
],
client: {
jasmine: {
// you can add configuration options for Jasmine here
// the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html
// for example, you can disable the random execution with `random: false`
// or set a specific seed with `seed: 4321`
},
clearContext: false, // leave Jasmine Spec Runner output visible in browser
},
coverageIstanbulReporter: {
dir: require('path').join(__dirname, '../../../coverage/core/application'),
reports: ['html', 'lcovonly', 'text-summary'],
fixWebpackSourcePaths: true,
jasmineHtmlReporter: {
suppressAll: true, // removes the duplicated traces
},
coverageReporter,
junitReporter,
reporters: ['progress', 'kjhtml'],
port: 9876,
colors: true,
logLevel: config.LOG_INFO,
autoWatch: true,
browsers: ['Chrome'],
customLaunchers,
singleRun: false,
restartOnFileChange: true,
});

View File

@@ -11,8 +11,13 @@ import { ApplicationService } from './application.service';
export class CoreApplicationModule {
static forRoot(): ModuleWithProviders<CoreApplicationModule> {
return {
ngModule: CoreApplicationModule,
providers: [ApplicationService, StoreModule.forFeature('core-application', applicationReducer).providers],
ngModule: RootCoreApplicationModule,
};
}
}
@NgModule({
imports: [StoreModule.forFeature('core-application', applicationReducer)],
providers: [ApplicationService],
})
export class RootCoreApplicationModule {}

View File

@@ -1,32 +1,232 @@
import { TestBed } from '@angular/core/testing';
import { isObservable } from 'rxjs';
import { createServiceFactory, SpectatorService, SpyObject } from '@ngneat/spectator';
import { Store } from '@ngrx/store';
import { Observable, of } from 'rxjs';
import { first } from 'rxjs/operators';
import { ApplicationProcess } from './defs';
import { ApplicationService } from './application.service';
import * as actions from './store/application.actions';
describe('ApplicationService', () => {
let service: ApplicationService;
let spectator: SpectatorService<ApplicationService>;
let store: SpyObject<Store>;
const createService = createServiceFactory({
service: ApplicationService,
mocks: [Store],
});
beforeEach(() => {
TestBed.configureTestingModule({
providers: [ApplicationService],
});
service = TestBed.inject(ApplicationService);
spectator = createService({});
store = spectator.inject(Store);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
describe('activatedProcessId', () => {
it('should return the processId', () => {
service.setActivatedProcessId(100);
expect(service.activatedProcessId).toEqual(100);
});
expect(spectator.service).toBeTruthy();
});
describe('activatedProcessId$', () => {
it('should return an observable', () => {
expect(isObservable(service.activatedProcessId$)).toBeTruthy();
expect(spectator.service.activatedProcessId$).toBeInstanceOf(Observable);
});
});
describe('activatedProcessId', () => {
it('should return the process id as a number', () => {
spyOnProperty(spectator.service['activatedProcessIdSubject'] as any, 'value').and.returnValue(2);
expect(spectator.service.activatedProcessId).toBe(2);
});
});
describe('getProcesses$()', () => {
it('should call select on store and return all selected processes', async () => {
const processes: ApplicationProcess[] = [
{ id: 1, name: 'Vorgang', type: 'cart', section: 'customer', data: { count: 1 } },
{ id: 2, name: 'Vorgang', type: 'task-calendar', section: 'branch' },
];
store.select.and.returnValue(of(processes));
const result = await spectator.service.getProcesses$().pipe(first()).toPromise();
expect(result).toEqual(processes);
expect(store.select).toHaveBeenCalled();
});
it('should call select on store and return all section customer processes', async () => {
const processes: ApplicationProcess[] = [
{ id: 1, name: 'Vorgang', type: 'cart', section: 'customer', data: { count: 1 } },
{ id: 2, name: 'Vorgang', type: 'task-calendar', section: 'branch' },
];
store.select.and.returnValue(of(processes));
const result = await spectator.service.getProcesses$('customer').pipe(first()).toPromise();
expect(result).toEqual([processes[0]]);
expect(store.select).toHaveBeenCalled();
});
it('should call select on store and return all section branch processes', async () => {
const processes: ApplicationProcess[] = [
{ id: 1, name: 'Vorgang', type: 'cart', section: 'customer', data: { count: 1 } },
{ id: 2, name: 'Vorgang', type: 'task-calendar', section: 'branch' },
];
store.select.and.returnValue(of(processes));
const result = await spectator.service.getProcesses$('branch').pipe(first()).toPromise();
expect(result).toEqual([processes[1]]);
expect(store.select).toHaveBeenCalled();
});
});
describe('getProcessById$()', () => {
it('should return the process by id', async () => {
const processes: ApplicationProcess[] = [
{ id: 1, name: 'Vorgang 1', section: 'customer' },
{ id: 2, name: 'Vorgang 2', section: 'customer' },
];
spyOn(spectator.service, 'getProcesses$').and.returnValue(of(processes));
const process = await spectator.service.getProcessById$(1).toPromise();
expect(process.id).toBe(1);
});
});
describe('getSection$()', () => {
it('should return the selected section branch', async () => {
const section = 'branch';
store.select.and.returnValue(of(section));
const result = await spectator.service.getSection$().pipe(first()).toPromise();
expect(result).toEqual(section);
expect(store.select).toHaveBeenCalled();
});
});
describe('getActivatedProcessId$', () => {
it('should return the current selected activated process id', async () => {
const activatedProcessId = 2;
store.select.and.returnValue(of({ id: activatedProcessId }));
const result = await spectator.service.getActivatedProcessId$().pipe(first()).toPromise();
expect(result).toEqual(activatedProcessId);
expect(store.select).toHaveBeenCalled();
});
});
describe('activateProcess()', () => {
it('should dispatch action setActivatedProcess with argument activatedProcessId and action type', () => {
const activatedProcessId = 2;
spectator.service.activateProcess(activatedProcessId);
expect(store.dispatch).toHaveBeenCalledWith({ activatedProcessId, type: actions.setActivatedProcess.type });
});
});
describe('removeProcess()', () => {
it('should dispatch action removeProcess with argument processId and action type', () => {
const processId = 2;
spectator.service.removeProcess(processId);
expect(store.dispatch).toHaveBeenCalledWith({ processId, type: actions.removeProcess.type });
});
});
describe('createProcess()', () => {
it('should dispatch action addProcess with process', async () => {
const process: ApplicationProcess = {
id: 1,
name: 'Vorgang 1',
section: 'customer',
type: 'cart',
};
const timestamp = 100;
spyOn(spectator.service as any, '_createTimestamp').and.returnValue(timestamp);
spyOn(spectator.service, 'getProcessById$').and.returnValue(of(undefined));
await spectator.service.createProcess(process);
expect(store.dispatch).toHaveBeenCalledWith({
type: actions.addProcess.type,
process: {
...process,
activated: 0,
created: timestamp,
},
});
});
it('should throw an error if the process id is already existing', async () => {
const process: ApplicationProcess = {
id: 1,
name: 'Vorgang 1',
section: 'customer',
type: 'cart',
};
spyOn(spectator.service, 'getProcessById$').and.returnValue(of(process));
await expectAsync(spectator.service.createProcess(process)).toBeRejectedWithError('Process Id existiert bereits');
});
it('should throw an error if the process id is not a number', async () => {
const process: ApplicationProcess = {
id: undefined,
name: 'Vorgang 1',
section: 'customer',
type: 'cart',
};
spyOn(spectator.service, 'getProcessById$').and.returnValue(of({ id: 5, name: 'Vorgang 2', section: 'customer' }));
await expectAsync(spectator.service.createProcess(process)).toBeRejectedWithError('Process Id nicht gesetzt');
});
});
describe('patchProcess', () => {
it('should dispatch action patchProcess with changes', async () => {
const process: ApplicationProcess = {
id: 1,
name: 'Vorgang 1',
section: 'customer',
type: 'cart',
};
await spectator.service.patchProcess(process.id, process);
expect(store.dispatch).toHaveBeenCalledWith({
type: actions.patchProcess.type,
process: {
...process,
},
});
});
});
describe('setSection()', () => {
it('should dispatch action setSection with argument section and action type', () => {
const section = 'customer';
spectator.service.setSection(section);
expect(store.dispatch).toHaveBeenCalledWith({ section, type: actions.setSection.type });
});
});
describe('getLastActivatedProcessWithSectionAndType()', () => {
it('should return the last activated process by section and type', async () => {
const processes: ApplicationProcess[] = [
{ id: 1, name: 'Vorgang 1', section: 'customer', type: 'cart', activated: 100 },
{ id: 2, name: 'Vorgang 2', section: 'customer', type: 'cart', activated: 200 },
{ id: 3, name: 'Vorgang 3', section: 'customer', type: 'goodsOut', activated: 300 },
];
spyOn(spectator.service, 'getProcesses$').and.returnValue(of(processes));
expect(await spectator.service.getLastActivatedProcessWithSectionAndType$('customer', 'cart').pipe(first()).toPromise()).toBe(
processes[1]
);
});
});
describe('getLastActivatedProcessWithSection()', () => {
it('should return the last activated process by section', async () => {
const processes: ApplicationProcess[] = [
{ id: 1, name: 'Vorgang 1', section: 'customer', activated: 100 },
{ id: 2, name: 'Vorgang 2', section: 'customer', activated: 200 },
{ id: 3, name: 'Vorgang 3', section: 'customer', activated: 300 },
];
spyOn(spectator.service, 'getProcesses$').and.returnValue(of(processes));
expect(await spectator.service.getLastActivatedProcessWithSection$('customer').pipe(first()).toPromise()).toBe(processes[2]);
});
});
describe('_createTimestamp', () => {
it('should return the current timestamp in ms', () => {
expect(spectator.service['_createTimestamp']()).toBeCloseTo(Date.now());
});
});
});

View File

@@ -1,37 +1,129 @@
import { Injectable } from '@angular/core';
import { Store } from '@ngrx/store';
import { BehaviorSubject } from 'rxjs';
import { processRemoved, selectSection, setSection } from './store';
import { isBoolean, isNumber } from '@utils/common';
import { BehaviorSubject, Observable } from 'rxjs';
import { first, map, switchMap } from 'rxjs/operators';
import { ApplicationProcess } from './defs';
import {
removeProcess,
selectSection,
selectProcesses,
setSection,
addProcess,
setActivatedProcess,
selectActivatedProcess,
patchProcess,
patchProcessData,
} from './store';
@Injectable()
export class ApplicationService {
/** @deprecated */
private activatedProcessIdSubject = new BehaviorSubject<number>(undefined);
/** @deprecated */
get activatedProcessId() {
return this.activatedProcessIdSubject.value;
}
/** @deprecated */
get activatedProcessId$() {
return this.activatedProcessIdSubject.asObservable();
}
readonly section$ = this.store.select(selectSection);
constructor(private store: Store) {}
setActivatedProcessId(processId: number) {
if (this.activatedProcessId !== processId) {
this.activatedProcessIdSubject.next(processId);
}
getProcesses$(section?: 'customer' | 'branch') {
const processes$ = this.store.select(selectProcesses);
return processes$.pipe(map((processes) => processes.filter((process) => (section ? process.section === section : true))));
}
getProcessById$(processId: number): Observable<ApplicationProcess> {
return this.getProcesses$().pipe(map((processes) => processes.find((process) => process.id === processId)));
}
getSection$() {
return this.store.select(selectSection);
}
/** @deprecated */
getActivatedProcessId$() {
return this.store.select(selectActivatedProcess).pipe(map((process) => process?.id));
}
activateProcess(activatedProcessId: number) {
this.store.dispatch(setActivatedProcess({ activatedProcessId }));
this.activatedProcessIdSubject.next(activatedProcessId);
}
removeProcess(processId: number) {
this.store.dispatch(processRemoved({ processId }));
this.store.dispatch(removeProcess({ processId }));
}
createProcess() {}
patchProcess(processId: number, changes: Partial<ApplicationProcess>) {
this.store.dispatch(patchProcess({ processId, changes }));
}
patchProcessData(processId: number, data: Record<string, any>) {
this.store.dispatch(patchProcessData({ processId, data }));
}
async createProcess(process: ApplicationProcess) {
const existingProcess = await this.getProcessById$(process?.id).pipe(first()).toPromise();
if (existingProcess?.id === process?.id) {
throw new Error('Process Id existiert bereits');
}
if (!isNumber(process.id)) {
throw new Error('Process Id nicht gesetzt');
}
if (!isBoolean(process.closeable)) {
process.closeable = true;
}
if (!isBoolean(process.confirmClosing)) {
process.confirmClosing = true;
}
process.created = this._createTimestamp();
process.activated = 0;
this.store.dispatch(addProcess({ process }));
}
setSection(section: 'customer' | 'branch') {
this.store.dispatch(setSection({ section }));
}
getLastActivatedProcessWithSectionAndType$(section: 'customer' | 'branch', type: string): Observable<ApplicationProcess> {
return this.getProcesses$(section).pipe(
map((processes) =>
processes
?.filter((process) => process.type === type)
?.reduce((latest, current) => {
if (!latest) {
return current;
}
return latest?.activated > current?.activated ? latest : current;
}, undefined)
)
);
}
getLastActivatedProcessWithSection$(section: 'customer' | 'branch'): Observable<ApplicationProcess> {
return this.getProcesses$(section).pipe(
map((processes) =>
processes?.reduce((latest, current) => {
if (!latest) {
return current;
}
return latest?.activated > current?.activated ? latest : current;
}, undefined)
)
);
}
private _createTimestamp() {
return Date.now();
}
}

View File

@@ -1,5 +1,11 @@
export interface ApplicationProcess {
id: number;
created?: number;
activated?: number;
name: string;
data: { [key: string]: any };
section: 'customer' | 'branch';
type?: string;
data?: { [key: string]: any };
closeable?: boolean;
confirmClosing?: boolean;
}

View File

@@ -1,7 +1,6 @@
// start:ng42.barrel
export * from './application.module';
export * from './application.service';
export * from './process.service';
export * from './defs';
export * from './store';
// end:ng42.barrel

View File

@@ -1,8 +0,0 @@
import { Injectable } from '@angular/core';
@Injectable({ providedIn: 'root' })
export class ProcessService {
constructor() {}
updateName(processId: number, name: string) {}
}

View File

@@ -1,10 +1,16 @@
import { createAction, props } from '@ngrx/store';
import { ApplicationProcess } from '..';
const prefix = '[CORE-APPLICATION]';
/**
* Action nach Entfernung eines Prozesses
*/
export const processRemoved = createAction(`${prefix} Process Removed`, props<{ processId: number }>());
export const setSection = createAction(`${prefix} Set Section`, props<{ section: 'customer' | 'branch' }>());
export const addProcess = createAction(`${prefix} Add Process`, props<{ process: ApplicationProcess }>());
export const removeProcess = createAction(`${prefix} Remove Process`, props<{ processId: number }>());
export const setActivatedProcess = createAction(`${prefix} Set Activated Process`, props<{ activatedProcessId: number }>());
export const patchProcess = createAction(`${prefix} Patch Process`, props<{ processId: number; changes: Partial<ApplicationProcess> }>());
export const patchProcessData = createAction(`${prefix} Patch Process Data`, props<{ processId: number; data: Record<string, any> }>());

View File

@@ -0,0 +1,200 @@
import { INITIAL_APPLICATION_STATE } from './application.state';
import * as actions from './application.actions';
import { applicationReducer } from './application.reducer';
import { ApplicationProcess } from '../defs';
import { ApplicationState } from './application.state';
describe('applicationReducer', () => {
describe('setSection()', () => {
it('should return modified state with section customer', () => {
const initialState = INITIAL_APPLICATION_STATE;
const action = actions.setSection({ section: 'customer' });
const state = applicationReducer(initialState, action);
expect(state).toEqual({
...initialState,
section: 'customer',
});
});
it('should return modified state with section branch', () => {
const initialState = INITIAL_APPLICATION_STATE;
const action = actions.setSection({ section: 'branch' });
const state = applicationReducer(initialState, action);
expect(state).toEqual({
...initialState,
section: 'branch',
});
});
});
describe('addProcess()', () => {
it('should return modified state with new process if no processes are registered in the state', () => {
const initialState = INITIAL_APPLICATION_STATE;
const process: ApplicationProcess = {
id: 1,
name: 'Vorgang',
section: 'customer',
type: 'cart',
data: {},
};
const action = actions.addProcess({ process });
const state = applicationReducer(initialState, action);
expect(state.processes[0]).toEqual(process);
});
});
describe('patchProcess()', () => {
it('should return modified state with updated process when id is found', () => {
const initialState = INITIAL_APPLICATION_STATE;
const process: ApplicationProcess = {
id: 1,
name: 'Vorgang',
section: 'customer',
type: 'cart',
};
const action = actions.patchProcess({ process: { ...process, name: 'Test' } });
const state = applicationReducer(
{
...initialState,
processes: [process],
},
action
);
expect(state.processes[0].name).toEqual('Test');
});
it('should return unmodified state when id is not existing', () => {
const initialState = INITIAL_APPLICATION_STATE;
const process: ApplicationProcess = {
id: 1,
name: 'Vorgang',
section: 'customer',
type: 'cart',
};
const action = actions.patchProcess({ process: { ...process, id: 2 } });
const state = applicationReducer(
{
...initialState,
processes: [process],
},
action
);
expect(state.processes).toEqual([process]);
});
});
describe('removeProcess()', () => {
it('should return initial state if no processes are registered in the state', () => {
const initialState = INITIAL_APPLICATION_STATE;
const action = actions.removeProcess({ processId: 2 });
const state = applicationReducer(initialState, action);
expect(state).toEqual(initialState);
});
it('should return the unmodified state if processId not found', () => {
const initialState = INITIAL_APPLICATION_STATE;
const modifiedState: ApplicationState = {
...initialState,
section: 'customer',
processes: [
{
id: 1,
name: 'Vorgang',
section: 'customer',
type: 'cart',
},
{
id: 4,
name: 'Vorgang',
section: 'customer',
type: 'goods-out',
},
] as ApplicationProcess[],
};
const action = actions.removeProcess({ processId: 2 });
const state = applicationReducer(modifiedState, action);
expect(state).toEqual(modifiedState);
});
it('should return modified state, after process gets removed', () => {
const initialState = INITIAL_APPLICATION_STATE;
const modifiedState: ApplicationState = {
...initialState,
section: 'customer',
processes: [
{
id: 1,
name: 'Vorgang',
section: 'customer',
type: 'cart',
},
{
id: 2,
name: 'Vorgang',
section: 'customer',
type: 'goods-out',
},
] as ApplicationProcess[],
};
const action = actions.removeProcess({ processId: 2 });
const state = applicationReducer(modifiedState, action);
expect(state.processes).toEqual([
{
id: 1,
name: 'Vorgang',
section: 'customer',
type: 'cart',
},
]);
});
});
describe('setActivatedProcess()', () => {
it('should return modified state with process.activated value', () => {
const process: ApplicationProcess = {
id: 3,
name: 'Vorgang 3',
section: 'customer',
};
const initialState: ApplicationState = {
...INITIAL_APPLICATION_STATE,
processes: [process],
};
const action = actions.setActivatedProcess({ activatedProcessId: 3 });
const state = applicationReducer(initialState, action);
expect(state.processes[0].activated).toBeDefined();
});
it('should return modified state without process.activated value when activatedProcessId doesnt exist', () => {
const process: ApplicationProcess = {
id: 1,
name: 'Vorgang 3',
section: 'customer',
};
const initialState: ApplicationState = {
...INITIAL_APPLICATION_STATE,
processes: [process],
};
const action = actions.setActivatedProcess({ activatedProcessId: 3 });
const state = applicationReducer(initialState, action);
expect(state.processes[0].activated).toBeUndefined();
});
});
});

View File

@@ -1,10 +1,45 @@
import { Action, createReducer, on } from '@ngrx/store';
import { setSection } from './application.actions';
import { setSection, addProcess, removeProcess, setActivatedProcess, patchProcess, patchProcessData } from './application.actions';
import { ApplicationState, INITIAL_APPLICATION_STATE } from './application.state';
const _applicationReducer = createReducer(
INITIAL_APPLICATION_STATE,
on(setSection, (state, { section }) => ({ ...state, section }))
on(setSection, (state, { section }) => ({ ...state, section })),
on(addProcess, (state, { process }) => ({ ...state, processes: [...state.processes, { data: {}, ...process }] })),
on(removeProcess, (state, { processId }) => {
const processes = state?.processes?.filter((process) => process.id !== processId) || [];
return { ...state, processes };
}),
on(setActivatedProcess, (state, { activatedProcessId }) => {
const processes = state.processes.map((process) => {
if (process.id === activatedProcessId) {
return { ...process, activated: Date.now() };
}
return process;
});
return { ...state, processes };
}),
on(patchProcess, (state, { processId, changes }) => {
const processes = state.processes.map((process) => {
if (process.id === processId) {
return { ...process, ...changes };
}
return process;
});
return { ...state, processes };
}),
on(patchProcessData, (state, { processId, data }) => {
const processes = state.processes.map((process) => {
if (process.id === processId) {
return { ...process, data: { ...(process.data || {}), ...data } };
}
return process;
});
return { ...state, processes };
})
);
export function applicationReducer(state: ApplicationState, action: Action) {

View File

@@ -0,0 +1,32 @@
import { ApplicationState } from './application.state';
import { ApplicationProcess } from '../defs';
import * as selectors from './application.selectors';
describe('applicationSelectors', () => {
it('should select the processes', () => {
const processes: ApplicationProcess[] = [{ id: 1, name: 'Vorgang 1', section: 'customer' }];
const state: Partial<ApplicationState> = {
processes,
};
expect(selectors.selectProcesses.projector(state)).toEqual(processes);
});
it('should select the section', () => {
const state: Partial<ApplicationState> = {
section: 'customer',
};
expect(selectors.selectSection.projector(state)).toEqual('customer');
});
it('should select the activatedProcess', () => {
const processes: ApplicationProcess[] = [
{ id: 1, name: 'Vorgang 1', section: 'customer', activated: 100 },
{ id: 2, name: 'Vorgang 2', section: 'customer', activated: 300 },
{ id: 3, name: 'Vorgang 3', section: 'customer', activated: 200 },
];
const state: Partial<ApplicationState> = {
processes,
};
expect(selectors.selectActivatedProcess.projector(state)).toEqual(processes[1]);
});
});

View File

@@ -3,3 +3,14 @@ import { ApplicationState } from './application.state';
export const selectApplicationState = createFeatureSelector<ApplicationState>('core-application');
export const selectSection = createSelector(selectApplicationState, (s) => s.section);
export const selectProcesses = createSelector(selectApplicationState, (s) => s.processes);
export const selectActivatedProcess = createSelector(selectApplicationState, (s) =>
s?.processes?.reduce((process, current) => {
if (!process) {
return current;
}
return process.activated > current.activated ? process : current;
}, undefined)
);

View File

@@ -1,7 +1,11 @@
import { ApplicationProcess } from '../defs';
export interface ApplicationState {
processes: ApplicationProcess[];
section: 'customer' | 'branch';
}
export const INITIAL_APPLICATION_STATE: ApplicationState = {
processes: [],
section: 'customer',
};

View File

@@ -1,7 +1,7 @@
// This file is required by karma.conf.js and loads recursively all the .spec and framework files
import 'zone.js/dist/zone';
import 'zone.js/dist/zone-testing';
import 'zone.js';
import 'zone.js/testing';
import { getTestBed } from '@angular/core/testing';
import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing';

25
apps/core/auth/README.md Normal file
View File

@@ -0,0 +1,25 @@
# Auth
This library was generated with [Angular CLI](https://github.com/angular/angular-cli) version 12.2.0.
## Code scaffolding
Run `ng generate component component-name --project auth` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module --project auth`.
> Note: Don't forget to add `--project auth` or else it will be added to the default project in your `angular.json` file.
## Build
Run `ng build auth` to build the project. The build artifacts will be stored in the `dist/` directory.
## Publishing
After building your library with `ng build auth`, go to the dist folder `cd dist/auth` and run `npm publish`.
## Running unit tests
Run `ng test auth` to execute the unit tests via [Karma](https://karma-runner.github.io).
## Further help
To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page.

View File

@@ -0,0 +1,43 @@
// Karma configuration file, see link for more information
// https://karma-runner.github.io/1.0/config/configuration-file.html
const customLaunchers = require('../../../karma/custom-launchers');
const junitReporter = require('../../../karma/junit-reporter')('core-auth');
const coverageReporter = require('../../../karma/coverage-reporter')('core-auth');
module.exports = function (config) {
config.set({
basePath: '',
frameworks: ['jasmine', '@angular-devkit/build-angular'],
plugins: [
require('karma-jasmine'),
require('karma-chrome-launcher'),
require('karma-jasmine-html-reporter'),
require('karma-coverage'),
require('karma-junit-reporter'),
require('@angular-devkit/build-angular/plugins/karma'),
],
client: {
jasmine: {
// you can add configuration options for Jasmine here
// the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html
// for example, you can disable the random execution with `random: false`
// or set a specific seed with `seed: 4321`
},
clearContext: false, // leave Jasmine Spec Runner output visible in browser
},
jasmineHtmlReporter: {
suppressAll: true, // removes the duplicated traces
},
coverageReporter,
junitReporter,
reporters: ['progress', 'kjhtml'],
port: 9876,
colors: true,
logLevel: config.LOG_INFO,
autoWatch: true,
browsers: ['Chrome'],
customLaunchers,
singleRun: false,
restartOnFileChange: true,
});
};

View File

@@ -0,0 +1,7 @@
{
"$schema": "../../../node_modules/ng-packagr/ng-package.schema.json",
"dest": "../../../dist/core/auth",
"lib": {
"entryFile": "src/public-api.ts"
}
}

View File

@@ -0,0 +1,11 @@
{
"name": "@core/auth",
"version": "0.0.1",
"peerDependencies": {
"@angular/common": "^12.2.0",
"@angular/core": "^12.2.0"
},
"dependencies": {
"tslib": "^2.3.0"
}
}

View File

@@ -0,0 +1,17 @@
import { ModuleWithProviders, NgModule } from '@angular/core';
import { AuthService } from './auth.service';
import { OAuthModule } from 'angular-oauth2-oidc';
@NgModule({})
export class AuthModule {
static forRoot(): ModuleWithProviders<AuthModule> {
return {
ngModule: AuthModule,
providers: [
AuthService,
OAuthModule.forRoot({
resourceServer: { sendAccessToken: true },
}).providers,
],
};
}
}

View File

@@ -0,0 +1,151 @@
import { Config } from '@core/config';
import { SpectatorService, createServiceFactory, SpyObject } from '@ngneat/spectator';
import { OAuthService } from 'angular-oauth2-oidc';
import { JwksValidationHandler } from 'angular-oauth2-oidc-jwks';
import { Observable } from 'rxjs';
import { AuthService } from './auth.service';
describe('AuthService', () => {
let spectator: SpectatorService<AuthService>;
const createService = createServiceFactory({
service: AuthService,
mocks: [Config, OAuthService],
});
let config: SpyObject<Config>;
let oAuthService: SpyObject<OAuthService>;
beforeEach(() => {
spectator = createService();
config = spectator.inject(Config);
oAuthService = spectator.inject(OAuthService);
});
it('should be created', () => {
expect(spectator.service).toBeTruthy();
});
describe('init()', () => {
it('should configure the oAuthService', () => {
config.get.and.returnValue({});
spectator.service.init();
expect(oAuthService.configure).toHaveBeenCalledWith({
redirectUri: window.location.origin,
silentRefreshRedirectUri: window.location.origin + '/silent-refresh.html',
useSilentRefresh: true,
});
expect(oAuthService.tokenValidationHandler).toBeInstanceOf(JwksValidationHandler);
expect(oAuthService.setupAutomaticSilentRefresh).toHaveBeenCalled();
});
it('should load the discovery document', async () => {
config.get.and.returnValue({});
oAuthService.loadDiscoveryDocumentAndTryLogin.and.returnValue(Promise.resolve(true));
spyOn(spectator.service['_initialized'], 'next');
await spectator.service.init();
expect(oAuthService.loadDiscoveryDocumentAndTryLogin).toHaveBeenCalled();
expect(spectator.service['_initialized'].next).toHaveBeenCalledWith(true);
});
it('should throw an error if its already initialized', async () => {
spyOn(spectator.service['_initialized'], 'getValue').and.returnValue(true);
await expectAsync(spectator.service.init()).toBeRejectedWithError('AuthService is already initialized');
});
});
describe('isAuthenticated()', () => {
it('should call hasValidIdToken() and return its value', () => {
oAuthService.hasValidIdToken.and.returnValue(true);
expect(spectator.service.isAuthenticated()).toBeTrue();
expect(oAuthService.hasValidIdToken).toHaveBeenCalled();
});
});
describe('getToken()', () => {
it('should call getAccessToken() and return its value', () => {
oAuthService.getAccessToken.and.returnValue('token');
expect(spectator.service.getToken()).toEqual('token');
expect(oAuthService.getAccessToken).toHaveBeenCalled();
});
});
describe('getClaims()', () => {
it('should call getAccessToken() and return its value', () => {
oAuthService.getAccessToken.and.returnValue('token');
const claims = {
claim1: 'value',
claim2: 'value2',
};
spyOn(spectator.service, 'parseJwt').and.returnValue(claims);
expect(spectator.service.getClaims()).toEqual(claims);
expect(spectator.service.parseJwt).toHaveBeenCalledWith('token');
expect(oAuthService.getAccessToken).toHaveBeenCalled();
});
});
describe('getClaimByKey()', () => {
it('should call getClaims() and return its key value', () => {
spyOn(spectator.service, 'getClaims').and.returnValue({
claim1: 'value',
claim2: 'value2',
});
expect(spectator.service.getClaimByKey('claim1')).toEqual('value');
expect(spectator.service.getClaims).toHaveBeenCalled();
});
it('should return null if getClaims() returns null or undefined', () => {
spyOn(spectator.service, 'getClaims').and.returnValue(null);
expect(spectator.service.getClaimByKey('claim1')).toBeNull();
expect(spectator.service.getClaims).toHaveBeenCalled();
});
});
describe('parseJwt()', () => {
it('should return null if the token is null or undefined', () => {
expect(spectator.service.parseJwt(null)).toBeNull();
expect(spectator.service.parseJwt(undefined)).toBeNull();
});
it('should return the value of the key', () => {
const token =
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ';
expect(spectator.service.parseJwt(token)).toEqual({
sub: '1234567890',
name: 'John Doe',
admin: true,
});
});
});
describe('login()', () => {
it('should call initLoginFlow()', () => {
spectator.service.login();
expect(oAuthService.initLoginFlow).toHaveBeenCalled();
});
});
describe('logout()', () => {
it('should call revokeTokenAndLogout()', async () => {
await spectator.service.logout();
expect(oAuthService.revokeTokenAndLogout).toHaveBeenCalled();
});
});
describe('getToken()', () => {
it('should return getAccessToken()', () => {
spectator.service.getToken();
expect(oAuthService.getAccessToken).toHaveBeenCalled();
});
});
describe('initialized', () => {
it('should return _initialized as Observable', () => {
spyOn(spectator.service['_initialized'], 'asObservable').and.callThrough();
expect(spectator.service.initialized$).toBeInstanceOf(Observable);
expect(spectator.service['_initialized'].asObservable).toHaveBeenCalled();
});
});
});

View File

@@ -0,0 +1,84 @@
import { Injectable } from '@angular/core';
import { Config } from '@core/config';
import { isNullOrUndefined } from '@utils/common';
import { AuthConfig, OAuthService } from 'angular-oauth2-oidc';
import { JwksValidationHandler } from 'angular-oauth2-oidc-jwks';
import { BehaviorSubject } from 'rxjs';
@Injectable({
providedIn: 'root',
})
export class AuthService {
private readonly _initialized = new BehaviorSubject<boolean>(false);
get initialized$() {
return this._initialized.asObservable();
}
constructor(private _config: Config, private readonly _oAuthService: OAuthService) {}
async init() {
if (this._initialized.getValue()) {
throw new Error('AuthService is already initialized');
}
const authConfig: AuthConfig = this._config.get('@core/auth');
authConfig.redirectUri = window.location.origin;
authConfig.silentRefreshRedirectUri = window.location.origin + '/silent-refresh.html';
authConfig.useSilentRefresh = true;
this._oAuthService.configure(authConfig);
this._oAuthService.tokenValidationHandler = new JwksValidationHandler();
this._oAuthService.setupAutomaticSilentRefresh();
await this._oAuthService.loadDiscoveryDocumentAndTryLogin();
this._initialized.next(true);
}
isAuthenticated() {
return this._oAuthService.hasValidIdToken();
}
getToken() {
return this._oAuthService.getAccessToken();
}
getClaims() {
const token = this._oAuthService.getAccessToken();
return this.parseJwt(token);
}
getClaimByKey(key: string) {
const claims = this.getClaims();
if (isNullOrUndefined(claims)) {
return null;
}
return claims[key];
}
parseJwt(token: string) {
if (isNullOrUndefined(token)) {
return null;
}
const base64Url = token.split('.')[1];
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
const encoded = window.atob(base64);
return JSON.parse(encoded);
}
login() {
this._oAuthService.initLoginFlow();
}
setKeyCardToken(token: string) {
this._oAuthService.customQueryParams = {
temp_token: token,
};
}
async logout() {
await this._oAuthService.revokeTokenAndLogout();
}
}

View File

@@ -0,0 +1,4 @@
// start:ng42.barrel
export * from './auth.module';
export * from './auth.service';
// end:ng42.barrel

View File

@@ -0,0 +1,5 @@
/*
* Public API Surface of auth
*/
export * from './lib';

View File

@@ -0,0 +1,25 @@
// This file is required by karma.conf.js and loads recursively all the .spec and framework files
import 'zone.js';
import 'zone.js/testing';
import { getTestBed } from '@angular/core/testing';
import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing';
declare const require: {
context(
path: string,
deep?: boolean,
filter?: RegExp
): {
keys(): string[];
<T>(id: string): T;
};
};
// First, initialize the Angular testing environment.
getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting(), { teardown: { destroyAfterEach: true } });
// Then we find all the tests.
const context = require.context('./', true, /\.spec\.ts$/);
// And load the modules.
context.keys().map(context);

View File

@@ -0,0 +1,20 @@
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
"extends": "../../../tsconfig.json",
"compilerOptions": {
"outDir": "../../../out-tsc/lib",
"target": "es2015",
"declaration": true,
"declarationMap": true,
"inlineSources": true,
"types": [],
"lib": [
"dom",
"es2018"
]
},
"exclude": [
"src/test.ts",
"**/*.spec.ts"
]
}

View File

@@ -0,0 +1,10 @@
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
"extends": "./tsconfig.lib.json",
"compilerOptions": {
"declarationMap": false
},
"angularCompilerOptions": {
"compilationMode": "partial"
}
}

View File

@@ -0,0 +1,17 @@
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
"extends": "../../../tsconfig.json",
"compilerOptions": {
"outDir": "../../../out-tsc/spec",
"types": [
"jasmine"
]
},
"files": [
"src/test.ts"
],
"include": [
"**/*.spec.ts",
"**/*.d.ts"
]
}

View File

@@ -3,7 +3,7 @@ import { Store } from '@ngrx/store';
import { getNumberId } from '@utils/id';
import { Observable } from 'rxjs';
import { map, take } from 'rxjs/operators';
import { first, map, take } from 'rxjs/operators';
import { Breadcrumb } from './defs';
import * as actions from './store/breadcrumb.actions';
@@ -29,6 +29,19 @@ export class BreadcrumbService {
return this.store.select(selectors.selectBreadcrumbsByKey, key);
}
getLastActivatedBreadcrumbByKey$(key: string | number): Observable<Breadcrumb> {
return this.getBreadcrumbByKey$(key).pipe(
map((crumbs) =>
crumbs.reduce((latest, current) => {
if (!latest) {
return current;
}
return latest.timestamp > current.timestamp ? latest : current;
}, undefined)
)
);
}
addBreadcrumb(breadcrumb: Breadcrumb): Breadcrumb {
const newBreadcrumb: Breadcrumb = { ...breadcrumb, id: getNumberId(), timestamp: Date.now(), changed: Date.now() };
this.store.dispatch(actions.addBreadcrumb({ breadcrumb: newBreadcrumb }));
@@ -39,6 +52,11 @@ export class BreadcrumbService {
this.store.dispatch(actions.updateBreadcrumb({ id: breadcrumbId, changes: { ...changes, changed: Date.now() } }));
}
async patchBreadcrumbByKeyAndTags(key: string | number, tags: string[], changes: Partial<Breadcrumb>) {
const crumbs = await this.getBreadcrumbsByKeyAndTags$(key, tags).pipe(first()).toPromise();
crumbs.forEach((crumb) => this.patchBreadcrumb(crumb.id, changes));
}
async addBreadcrumbIfNotExists(breadcrumb: Breadcrumb) {
const crumbs = await this.getBreadcrumbsByKeyAndTags$(breadcrumb.key, breadcrumb.tags).pipe(take(1)).toPromise();
if (crumbs.length === 0) {
@@ -112,6 +130,11 @@ export class BreadcrumbService {
this.store.dispatch(actions.removeManyBreadcrumb({ ids: breadcrumbsToRemove.map((crumb) => crumb.id) }));
}
async removeBreadcrumbsByKeyAndTags(key: number | string, tags: string[]) {
const crumbs = await this.getBreadcrumbsByKeyAndTags$(key, tags).pipe(first()).toPromise();
crumbs.forEach((crumb) => this.removeBreadcrumb(crumb.id));
}
getLatestBreadcrumbForSection(section: 'customer' | 'branch') {
return this.store
.select(selectors.selectBreadcrumbsBySection, { section })

View File

@@ -42,5 +42,5 @@ export interface Breadcrumb {
/**
* Applicatiuon Section
*/
section: 'branch' | 'customer';
section: string;
}

View File

@@ -1,5 +1,5 @@
import { Injectable } from '@angular/core';
import { processRemoved } from '@core/application';
import { removeProcess } from '@core/application';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import { NEVER } from 'rxjs';
import { mergeMap, tap, first, map } from 'rxjs/operators';
@@ -10,7 +10,7 @@ export class BreadcrumbEffects {
removeProcess$ = createEffect(
() =>
this.actions$.pipe(
ofType(processRemoved),
ofType(removeProcess),
mergeMap((action) =>
this.breadcrumb.getBreadcrumbByKey$(action.processId).pipe(
first(),

View File

@@ -1,7 +1,7 @@
// This file is required by karma.conf.js and loads recursively all the .spec and framework files
import 'zone.js/dist/zone';
import 'zone.js/dist/zone-testing';
import 'zone.js';
import 'zone.js/testing';
import { getTestBed } from '@angular/core/testing';
import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing';

View File

@@ -1,7 +1,7 @@
// This file is required by karma.conf.js and loads recursively all the .spec and framework files
import 'zone.js/dist/zone';
import 'zone.js/dist/zone-testing';
import 'zone.js';
import 'zone.js/testing';
import { getTestBed } from '@angular/core/testing';
import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing';

View File

@@ -1,7 +1,7 @@
// This file is required by karma.conf.js and loads recursively all the .spec and framework files
import 'zone.js/dist/zone';
import 'zone.js/dist/zone-testing';
import 'zone.js';
import 'zone.js/testing';
import { getTestBed } from '@angular/core/testing';
import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing';

View File

@@ -0,0 +1,25 @@
# Config
This library was generated with [Angular CLI](https://github.com/angular/angular-cli) version 12.2.0.
## Code scaffolding
Run `ng generate component component-name --project config` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module --project config`.
> Note: Don't forget to add `--project config` or else it will be added to the default project in your `angular.json` file.
## Build
Run `ng build config` to build the project. The build artifacts will be stored in the `dist/` directory.
## Publishing
After building your library with `ng build config`, go to the dist folder `cd dist/config` and run `npm publish`.
## Running unit tests
Run `ng test config` to execute the unit tests via [Karma](https://karma-runner.github.io).
## Further help
To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page.

View File

@@ -0,0 +1,43 @@
// Karma configuration file, see link for more information
// https://karma-runner.github.io/1.0/config/configuration-file.html
const customLaunchers = require('../../../karma/custom-launchers');
const junitReporter = require('../../../karma/junit-reporter')('core-config');
const coverageReporter = require('../../../karma/coverage-reporter')('core-config');
module.exports = function (config) {
config.set({
basePath: '',
frameworks: ['jasmine', '@angular-devkit/build-angular'],
plugins: [
require('karma-jasmine'),
require('karma-chrome-launcher'),
require('karma-jasmine-html-reporter'),
require('karma-coverage'),
require('karma-junit-reporter'),
require('@angular-devkit/build-angular/plugins/karma'),
],
client: {
jasmine: {
// you can add configuration options for Jasmine here
// the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html
// for example, you can disable the random execution with `random: false`
// or set a specific seed with `seed: 4321`
},
clearContext: false, // leave Jasmine Spec Runner output visible in browser
},
jasmineHtmlReporter: {
suppressAll: true, // removes the duplicated traces
},
coverageReporter,
junitReporter,
reporters: ['progress', 'kjhtml'],
port: 9876,
colors: true,
logLevel: config.LOG_INFO,
autoWatch: true,
browsers: ['Chrome'],
customLaunchers,
singleRun: false,
restartOnFileChange: true,
});
};

View File

@@ -0,0 +1,7 @@
{
"$schema": "../../../node_modules/ng-packagr/ng-package.schema.json",
"dest": "../../../dist/core/config",
"lib": {
"entryFile": "src/public-api.ts"
}
}

View File

@@ -0,0 +1,11 @@
{
"name": "@core/config",
"version": "0.0.1",
"peerDependencies": {
"@angular/common": "^12.2.0",
"@angular/core": "^12.2.0"
},
"dependencies": {
"tslib": "^2.3.0"
}
}

View File

@@ -0,0 +1,8 @@
import { Observable } from 'rxjs';
/**
* Config loader interface for loading configurations
*/
export interface ConfigLoader {
load(): Promise<any>;
}

View File

@@ -0,0 +1,4 @@
// start:ng42.barrel
export * from './config-loader';
export * from './json.config-loader';
// end:ng42.barrel

View File

@@ -0,0 +1,36 @@
// unit test JsonConfigLoader
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { createServiceFactory, SpectatorService } from '@ngneat/spectator';
import { CORE_JSON_CONFIG_LOADER_URL } from '../tokens';
import { JsonConfigLoader } from './json.config-loader';
describe('JsonConfigLoader', () => {
let spectator: SpectatorService<JsonConfigLoader>;
const createService = createServiceFactory({
imports: [HttpClientTestingModule],
service: JsonConfigLoader,
mocks: [],
providers: [{ provide: CORE_JSON_CONFIG_LOADER_URL, useValue: '/assets/config.json' }],
});
let httpTestingController: HttpTestingController;
beforeEach(() => {
spectator = createService();
httpTestingController = spectator.inject(HttpTestingController);
});
it('should create', () => {
expect(spectator.service).toBeTruthy();
});
describe('load', () => {
it('should call the provided url', async () => {
const reqPromise = spectator.service.load();
const req = httpTestingController.expectOne('/assets/config.json');
req.flush({ unit: 'test' });
const result = await reqPromise;
httpTestingController.verify();
expect(result).toEqual({ unit: 'test' });
});
});
});

View File

@@ -0,0 +1,13 @@
import { HttpClient } from '@angular/common/http';
import { Inject, Injectable } from '@angular/core';
import { ConfigLoader } from './config-loader';
import { CORE_JSON_CONFIG_LOADER_URL } from '../tokens';
@Injectable()
export class JsonConfigLoader implements ConfigLoader {
constructor(@Inject(CORE_JSON_CONFIG_LOADER_URL) private url: string, private http: HttpClient) {}
load(): Promise<any> {
return this.http.get(this.url).toPromise();
}
}

View File

@@ -0,0 +1,7 @@
import { Type } from '@angular/core';
import { ConfigLoader } from './config-loaders';
export interface ConfigModuleOptions {
useConfigLoader: Type<ConfigLoader>;
jsonConfigLoaderUrl?: string;
}

View File

@@ -0,0 +1,28 @@
import { APP_INITIALIZER, ModuleWithProviders, NgModule } from '@angular/core';
import { CORE_CONFIG_LOADER } from '@core/config';
import { Config } from './config';
import { ConfigModuleOptions } from './config-module-options';
import { CORE_JSON_CONFIG_LOADER_URL } from './tokens';
export function _initializeConfigFactory(config: Config) {
return () => config.init();
}
@NgModule({})
export class ConfigModule {
static forRoot(options: ConfigModuleOptions): ModuleWithProviders<ConfigModule> {
const configLoaderProvider = {
provide: CORE_CONFIG_LOADER,
useClass: options.useConfigLoader,
};
return {
ngModule: ConfigModule,
providers: [
Config,
configLoaderProvider,
options.jsonConfigLoaderUrl ? { provide: CORE_JSON_CONFIG_LOADER_URL, useValue: options.jsonConfigLoaderUrl } : null,
],
};
}
}

View File

@@ -0,0 +1,45 @@
import { createServiceFactory, SpectatorService } from '@ngneat/spectator';
import { Config } from './config';
import { ConfigLoader } from './config-loaders';
import { CORE_CONFIG_LOADER } from './tokens';
class TestConfigLoader implements ConfigLoader {
load() {
return Promise.resolve({});
}
}
// Unit test Config
describe('Config', () => {
let spectator: SpectatorService<Config>;
const createService = createServiceFactory({
service: Config,
providers: [{ provide: CORE_CONFIG_LOADER, useClass: TestConfigLoader }],
});
let configLoader: ConfigLoader;
beforeEach(() => {
spectator = createService();
configLoader = spectator.inject(CORE_CONFIG_LOADER);
});
it('should create', () => {
expect(spectator.service).toBeTruthy();
});
describe('init()', () => {
it('should load config and assigns it to _config', async () => {
const config = { unit: 'test' };
spyOn(configLoader, 'load').and.returnValue(Promise.resolve(config));
await spectator.service.init();
expect(spectator.service['_config']).toEqual(config);
});
});
describe('get()', () => {
it('should return config value', () => {
spectator.service['_config'] = { test: 'test' };
expect(spectator.service.get('test')).toEqual('test');
});
});
});

View File

@@ -0,0 +1,27 @@
import { Inject, Injectable } from '@angular/core';
import { ReplaySubject } from 'rxjs';
import { ConfigLoader } from './config-loaders';
import { CORE_CONFIG_LOADER } from './tokens';
import { pick } from './utils';
@Injectable()
export class Config {
private _config: any;
private readonly _initilized = new ReplaySubject<void>(1);
get initialized() {
return this._initilized.asObservable();
}
constructor(@Inject(CORE_CONFIG_LOADER) private readonly _configLoader: ConfigLoader) {}
// load config and assign it to this._config
async init() {
this._config = await this._configLoader.load();
this._initilized.next();
}
get(path: string) {
return pick(path, this._config);
}
}

View File

@@ -0,0 +1,8 @@
// start:ng42.barrel
export * from './config-module-options';
export * from './config.module';
export * from './config';
export * from './tokens';
export * from './config-loaders';
export * from './utils';
// end:ng42.barrel

View File

@@ -0,0 +1,6 @@
import { InjectionToken } from '@angular/core';
import { ConfigLoader } from './config-loaders';
export const CORE_CONFIG_LOADER = new InjectionToken<ConfigLoader>('core.config.loader');
export const CORE_JSON_CONFIG_LOADER_URL = new InjectionToken<ConfigLoader>('core.json.config.loader.url');

View File

@@ -0,0 +1,3 @@
// start:ng42.barrel
export * from './pick';
// end:ng42.barrel

View File

@@ -0,0 +1,41 @@
import { pick } from './pick';
describe('pick', () => {
it('should pick properties from the 1st level from the object', () => {
const obj = {
foo: 'bar',
};
expect(pick('foo', obj)).toEqual('bar');
});
it('should pick properties from the 2nd level from the object', () => {
const obj = {
foo: {
bar: 'baz',
},
};
expect(pick('foo.bar', obj)).toEqual('baz');
});
it('should pick properties from the 3rd level from the object', () => {
const obj = {
foo: {
bar: {
baz: 'qux',
},
},
};
expect(pick('foo.bar.baz', obj)).toEqual('qux');
});
it('should throw an error of obj is not an object', () => {
expect(() => pick('foo', 'bar')).toThrowError(`bar is not an object`);
});
it('should return undefined if the property is not found', () => {
const obj = {
foo: 'bar',
};
expect(pick('bar', obj)).toEqual(undefined);
});
});

View File

@@ -0,0 +1,33 @@
/**
* Pick a value from an object at a given path.
* @param path path of the value to pick
* @param obj object to pick from
* @returns the value at the path or undefined
* @throws if obj is not an object
*/
export function pick<T = any>(path: string, obj: Object): T {
const paths = path.split('.');
// check if obj is null or undefined
if (obj == null) {
return undefined;
}
// check if obj is of type object and not an array
// and throw an error if not
if (typeof obj !== 'object' || Array.isArray(obj)) {
throw new Error(`${obj} is not an object`);
}
let result = obj;
// loop through the path and pick the value
// early exit if the path is empty
for (const path of paths) {
result = result[path];
if (result == null) {
return undefined;
}
}
return result as T;
}

View File

@@ -0,0 +1,5 @@
/*
* Public API Surface of config
*/
export * from './lib';

View File

@@ -0,0 +1,25 @@
// This file is required by karma.conf.js and loads recursively all the .spec and framework files
import 'zone.js';
import 'zone.js/testing';
import { getTestBed } from '@angular/core/testing';
import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing';
declare const require: {
context(
path: string,
deep?: boolean,
filter?: RegExp
): {
keys(): string[];
<T>(id: string): T;
};
};
// First, initialize the Angular testing environment.
getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting(), { teardown: { destroyAfterEach: true } });
// Then we find all the tests.
const context = require.context('./', true, /\.spec\.ts$/);
// And load the modules.
context.keys().map(context);

View File

@@ -0,0 +1,20 @@
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
"extends": "../../../tsconfig.json",
"compilerOptions": {
"outDir": "../../../out-tsc/lib",
"target": "es2015",
"declaration": true,
"declarationMap": true,
"inlineSources": true,
"types": [],
"lib": [
"dom",
"es2018"
]
},
"exclude": [
"src/test.ts",
"**/*.spec.ts"
]
}

View File

@@ -0,0 +1,10 @@
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
"extends": "./tsconfig.lib.json",
"compilerOptions": {
"declarationMap": false
},
"angularCompilerOptions": {
"compilationMode": "partial"
}
}

View File

@@ -0,0 +1,17 @@
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
"extends": "../../../tsconfig.json",
"compilerOptions": {
"outDir": "../../../out-tsc/spec",
"types": [
"jasmine"
]
},
"files": [
"src/test.ts"
],
"include": [
"**/*.spec.ts",
"**/*.d.ts"
]
}

View File

@@ -1,7 +1,7 @@
// This file is required by karma.conf.js and loads recursively all the .spec and framework files
import 'zone.js/dist/zone';
import 'zone.js/dist/zone-testing';
import 'zone.js';
import 'zone.js/testing';
import { getTestBed } from '@angular/core/testing';
import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing';

View File

@@ -0,0 +1,25 @@
# Logger
This library was generated with [Angular CLI](https://github.com/angular/angular-cli) version 12.2.0.
## Code scaffolding
Run `ng generate component component-name --project logger` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module --project logger`.
> Note: Don't forget to add `--project logger` or else it will be added to the default project in your `angular.json` file.
## Build
Run `ng build logger` to build the project. The build artifacts will be stored in the `dist/` directory.
## Publishing
After building your library with `ng build logger`, go to the dist folder `cd dist/logger` and run `npm publish`.
## Running unit tests
Run `ng test logger` to execute the unit tests via [Karma](https://karma-runner.github.io).
## Further help
To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page.

View File

@@ -0,0 +1,43 @@
// Karma configuration file, see link for more information
// https://karma-runner.github.io/1.0/config/configuration-file.html
const customLaunchers = require('../../../karma/custom-launchers');
const junitReporter = require('../../../karma/junit-reporter')('core-logger');
const coverageReporter = require('../../../karma/coverage-reporter')('core-logger');
module.exports = function (config) {
config.set({
basePath: '',
frameworks: ['jasmine', '@angular-devkit/build-angular'],
plugins: [
require('karma-jasmine'),
require('karma-chrome-launcher'),
require('karma-jasmine-html-reporter'),
require('karma-coverage'),
require('karma-junit-reporter'),
require('@angular-devkit/build-angular/plugins/karma'),
],
client: {
jasmine: {
// you can add configuration options for Jasmine here
// the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html
// for example, you can disable the random execution with `random: false`
// or set a specific seed with `seed: 4321`
},
clearContext: false, // leave Jasmine Spec Runner output visible in browser
},
jasmineHtmlReporter: {
suppressAll: true, // removes the duplicated traces
},
junitReporter,
coverageReporter,
reporters: ['progress', 'kjhtml'],
port: 9876,
colors: true,
logLevel: config.LOG_INFO,
autoWatch: true,
browsers: ['Chrome'],
customLaunchers,
singleRun: false,
restartOnFileChange: true,
});
};

View File

@@ -0,0 +1,7 @@
{
"$schema": "../../../node_modules/ng-packagr/ng-package.schema.json",
"dest": "../../../dist/core/logger",
"lib": {
"entryFile": "src/public-api.ts"
}
}

View File

@@ -0,0 +1,11 @@
{
"name": "@core/logger",
"version": "0.0.1",
"peerDependencies": {
"@angular/common": "^12.2.0",
"@angular/core": "^12.2.0"
},
"dependencies": {
"tslib": "^2.3.0"
}
}

View File

@@ -0,0 +1,51 @@
import { createServiceFactory, SpectatorService } from '@ngneat/spectator';
import { ConsoleLogProvider } from './console-log.provider';
import { LogLevel } from './log-level';
describe('ConsoleLogProvider', () => {
let spectator: SpectatorService<ConsoleLogProvider>;
const createService = createServiceFactory({
service: ConsoleLogProvider,
});
beforeEach(() => {
spectator = createService();
});
it('should create', () => {
expect(spectator.service).toBeTruthy();
});
describe('log', () => {
it('should call console.debug', () => {
spyOn(console, 'debug');
spectator.service.log(LogLevel.DEBUG, 'test');
expect(console.debug).toHaveBeenCalledWith('test');
});
it('should call console.info', () => {
spyOn(console, 'info');
spectator.service.log(LogLevel.INFO, 'test');
expect(console.info).toHaveBeenCalledWith('test');
});
it('should call console.warn', () => {
spyOn(console, 'warn');
spectator.service.log(LogLevel.WARN, 'test');
expect(console.warn).toHaveBeenCalledWith('test');
});
it('should call console.error', () => {
spyOn(console, 'error');
spectator.service.log(LogLevel.ERROR, 'test');
expect(console.error).toHaveBeenCalledWith('test');
});
it('should not call console.log', () => {
spyOn(console, 'log');
spectator.service.log(LogLevel.OFF, 'test');
expect(console.log).not.toHaveBeenCalled();
});
});
});

View File

@@ -0,0 +1,25 @@
import { Injectable } from '@angular/core';
import { LogLevel } from './log-level';
import { LogProvider } from './log.provider';
@Injectable()
export class ConsoleLogProvider implements LogProvider {
log(logLevel: LogLevel, message: string, ...optionalParams: any[]): void {
switch (logLevel) {
case LogLevel.DEBUG:
console.debug(message, ...optionalParams);
break;
case LogLevel.INFO:
console.info(message, ...optionalParams);
break;
case LogLevel.WARN:
console.warn(message, ...optionalParams);
break;
case LogLevel.ERROR:
console.error(message, ...optionalParams);
break;
case LogLevel.OFF:
break;
}
}
}

View File

@@ -0,0 +1,8 @@
// start:ng42.barrel
export * from './console-log.provider';
export * from './log-level';
export * from './log.provider';
export * from './logger.module';
export * from './logger.service';
export * from './tokens';
// end:ng42.barrel

View File

@@ -0,0 +1,14 @@
export enum LogLevel {
DEBUG = 0,
INFO = 1,
WARN = 2,
ERROR = 3,
OFF = 4,
// aliases
debug = 0,
info = 1,
warn = 2,
error = 3,
off = 4,
}

View File

@@ -0,0 +1,6 @@
import { Injectable } from '@angular/core';
import { LogLevel } from './log-level';
export interface LogProvider {
log(logLevel: LogLevel, message: string, ...optionalParams: any[]): void;
}

View File

@@ -0,0 +1,31 @@
import { ModuleWithProviders, NgModule } from '@angular/core';
import { Config } from '@core/config';
import { ConsoleLogProvider } from './console-log.provider';
import { Logger } from './logger.service';
import { LOG_PROVIDER, LOG_LEVEL } from './tokens';
export function _logLevelProviderFactory(config: Config) {
return config.get('@core/logger.logLevel');
}
@NgModule({})
export class CoreLoggerModule {
static forRoot(): ModuleWithProviders<CoreLoggerModule> {
return {
ngModule: CoreLoggerModule,
providers: [
Logger,
{
provide: LOG_PROVIDER,
useClass: ConsoleLogProvider,
multi: true,
},
{
provide: LOG_LEVEL,
useFactory: _logLevelProviderFactory,
deps: [Config],
},
],
};
}
}

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