@cccteam/ccc-lib 0.0.13 → 0.0.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (226) hide show
  1. package/eslint.config.js +32 -0
  2. package/ng-package.json +11 -0
  3. package/package.json +7 -68
  4. package/src/auth-authentication-guard/authentication.guard.ts +40 -0
  5. package/src/auth-authentication-guard/ng-package.json +6 -0
  6. package/src/auth-authorization-guard/authorization.guard.ts +17 -0
  7. package/src/auth-authorization-guard/ng-package.json +6 -0
  8. package/src/auth-forms/ccc-field/ccc-field.component.html +1 -0
  9. package/src/auth-forms/ccc-field/ccc-field.component.scss +0 -0
  10. package/src/auth-forms/ccc-field/ccc-field.component.spec.ts +22 -0
  11. package/src/auth-forms/ccc-field/ccc-field.component.ts +74 -0
  12. package/src/auth-forms/form-helpers.ts +39 -0
  13. package/src/auth-forms/{public-api.d.ts → index.ts} +1 -0
  14. package/src/auth-forms/ng-package.json +6 -0
  15. package/src/auth-has-permission/has-permission.directive.ts +34 -0
  16. package/src/auth-has-permission/ng-package.json +6 -0
  17. package/src/auth-service/auth.service.ts +92 -0
  18. package/src/auth-service/ng-package.json +6 -0
  19. package/src/ccc-camel-case-to-title/camel-case-to-title.pipe.ts +23 -0
  20. package/src/ccc-camel-case-to-title/index.ts +1 -0
  21. package/src/ccc-camel-case-to-title/ng-package.json +6 -0
  22. package/src/ccc-grid/ccc-grid.component.ts +155 -0
  23. package/src/ccc-grid/index.ts +3 -0
  24. package/src/ccc-grid/ng-package.json +6 -0
  25. package/src/ccc-grid/table-button/table-button.component.html +16 -0
  26. package/src/ccc-grid/table-button/table-button.component.scss +5 -0
  27. package/src/ccc-grid/table-button/table-button.component.spec.ts +22 -0
  28. package/src/ccc-grid/table-button/table-button.component.ts +49 -0
  29. package/src/ccc-resource/can-deactivate.guard.ts +41 -0
  30. package/src/ccc-resource/compound-resource/compound-resource.component.html +57 -0
  31. package/src/ccc-resource/compound-resource/compound-resource.component.scss +86 -0
  32. package/src/ccc-resource/compound-resource/compound-resource.component.spec.ts +22 -0
  33. package/src/ccc-resource/compound-resource/compound-resource.component.ts +158 -0
  34. package/src/ccc-resource/concat-fns.ts +162 -0
  35. package/src/ccc-resource/empty-readonly-field/empty-readonly-field.component.html +12 -0
  36. package/src/ccc-resource/empty-readonly-field/empty-readonly-field.component.scss +0 -0
  37. package/src/ccc-resource/empty-readonly-field/empty-readonly-field.component.spec.ts +23 -0
  38. package/src/ccc-resource/empty-readonly-field/empty-readonly-field.component.ts +17 -0
  39. package/src/ccc-resource/form-state.service.ts +24 -0
  40. package/src/ccc-resource/format-fns.ts +49 -0
  41. package/src/ccc-resource/gui-constants.ts +88 -0
  42. package/src/ccc-resource/index.ts +23 -0
  43. package/src/ccc-resource/leave-page-confirmation-modal/leave-page-confirmation-modal.component.html +8 -0
  44. package/src/ccc-resource/leave-page-confirmation-modal/leave-page-confirmation-modal.component.scss +0 -0
  45. package/src/ccc-resource/leave-page-confirmation-modal/leave-page-confirmation-modal.component.spec.ts +22 -0
  46. package/src/ccc-resource/leave-page-confirmation-modal/leave-page-confirmation-modal.component.ts +12 -0
  47. package/src/ccc-resource/ng-package.json +6 -0
  48. package/src/ccc-resource/operation-types.ts +19 -0
  49. package/src/ccc-resource/padding-element/padding-element.component.html +1 -0
  50. package/src/ccc-resource/padding-element/padding-element.component.scss +3 -0
  51. package/src/ccc-resource/padding-element/padding-element.component.spec.ts +22 -0
  52. package/src/ccc-resource/padding-element/padding-element.component.ts +20 -0
  53. package/src/ccc-resource/resource-array-view/resource-array-view.component.html +81 -0
  54. package/src/ccc-resource/resource-array-view/resource-array-view.component.scss +21 -0
  55. package/src/ccc-resource/resource-array-view/resource-array-view.component.spec.ts +22 -0
  56. package/src/ccc-resource/resource-array-view/resource-array-view.component.ts +143 -0
  57. package/src/ccc-resource/resource-base/resource-base.component.spec.ts +22 -0
  58. package/src/ccc-resource/resource-base/resource-base.component.ts +11 -0
  59. package/src/ccc-resource/resource-cache.service.ts +232 -0
  60. package/src/ccc-resource/resource-create/resource-create.component.html +31 -0
  61. package/src/ccc-resource/resource-create/resource-create.component.scss +130 -0
  62. package/src/ccc-resource/resource-create/resource-create.component.spec.ts +22 -0
  63. package/src/ccc-resource/resource-create/resource-create.component.ts +303 -0
  64. package/src/ccc-resource/resource-field/base-field.directive.ts +102 -0
  65. package/src/ccc-resource/resource-field/fields/boolean-field/boolean-field.component.html +16 -0
  66. package/src/ccc-resource/resource-field/fields/boolean-field/boolean-field.component.scss +0 -0
  67. package/src/ccc-resource/resource-field/fields/boolean-field/boolean-field.component.spec.ts +22 -0
  68. package/src/ccc-resource/resource-field/fields/boolean-field/boolean-field.component.ts +15 -0
  69. package/src/ccc-resource/resource-field/fields/computed-field/computed-field.component.html +13 -0
  70. package/src/ccc-resource/resource-field/fields/computed-field/computed-field.component.scss +0 -0
  71. package/src/ccc-resource/resource-field/fields/computed-field/computed-field.component.spec.ts +23 -0
  72. package/src/ccc-resource/resource-field/fields/computed-field/computed-field.component.ts +50 -0
  73. package/src/ccc-resource/resource-field/fields/date-field/date-field.component.html +22 -0
  74. package/src/ccc-resource/resource-field/fields/date-field/date-field.component.scss +0 -0
  75. package/src/ccc-resource/resource-field/fields/date-field/date-field.component.spec.ts +22 -0
  76. package/src/ccc-resource/resource-field/fields/date-field/date-field.component.ts +14 -0
  77. package/src/ccc-resource/resource-field/fields/enumerated-field/enumerated-field.component.html +71 -0
  78. package/src/ccc-resource/resource-field/fields/enumerated-field/enumerated-field.component.scss +9 -0
  79. package/src/ccc-resource/resource-field/fields/enumerated-field/enumerated-field.component.spec.ts +22 -0
  80. package/src/ccc-resource/resource-field/fields/enumerated-field/enumerated-field.component.ts +207 -0
  81. package/src/ccc-resource/resource-field/fields/nullboolean-field/nullboolean-field.component.html +38 -0
  82. package/src/ccc-resource/resource-field/fields/nullboolean-field/nullboolean-field.component.scss +3 -0
  83. package/src/ccc-resource/resource-field/fields/nullboolean-field/nullboolean-field.component.spec.ts +22 -0
  84. package/src/ccc-resource/resource-field/fields/nullboolean-field/nullboolean-field.component.ts +87 -0
  85. package/src/ccc-resource/resource-field/fields/number-field/number-field.component.html +23 -0
  86. package/src/ccc-resource/resource-field/fields/number-field/number-field.component.scss +6 -0
  87. package/src/ccc-resource/resource-field/fields/number-field/number-field.component.spec.ts +22 -0
  88. package/src/ccc-resource/resource-field/fields/number-field/number-field.component.ts +14 -0
  89. package/src/ccc-resource/resource-field/fields/text-field/text-field.component.html +29 -0
  90. package/src/ccc-resource/resource-field/fields/text-field/text-field.component.scss +6 -0
  91. package/src/ccc-resource/resource-field/fields/text-field/text-field.component.spec.ts +22 -0
  92. package/src/ccc-resource/resource-field/fields/text-field/text-field.component.ts +23 -0
  93. package/src/ccc-resource/resource-field/resource-field.component.html +112 -0
  94. package/src/ccc-resource/resource-field/resource-field.component.scss +7 -0
  95. package/src/ccc-resource/resource-field/resource-field.component.spec.ts +22 -0
  96. package/src/ccc-resource/resource-field/resource-field.component.ts +214 -0
  97. package/src/ccc-resource/resource-layout/resource-layout.component.html +73 -0
  98. package/src/ccc-resource/resource-layout/resource-layout.component.scss +26 -0
  99. package/src/ccc-resource/resource-layout/resource-layout.component.spec.ts +22 -0
  100. package/src/ccc-resource/resource-layout/resource-layout.component.ts +176 -0
  101. package/src/ccc-resource/resource-list/ resource-list.component.spec.ts +22 -0
  102. package/src/ccc-resource/resource-list/resource-list.component.html +27 -0
  103. package/src/ccc-resource/resource-list/resource-list.component.scss +67 -0
  104. package/src/ccc-resource/resource-list/resource-list.component.ts +376 -0
  105. package/src/ccc-resource/resource-list-create/resource-list-create.component.html +71 -0
  106. package/src/ccc-resource/resource-list-create/resource-list-create.component.scss +9 -0
  107. package/src/ccc-resource/resource-list-create/resource-list-create.component.spec.ts +22 -0
  108. package/src/ccc-resource/resource-list-create/resource-list-create.component.ts +103 -0
  109. package/src/ccc-resource/resource-resolver/resource-resolver.component.html +1 -0
  110. package/src/ccc-resource/resource-resolver/resource-resolver.component.scss +0 -0
  111. package/src/ccc-resource/resource-resolver/resource-resolver.component.spec.ts +22 -0
  112. package/src/ccc-resource/resource-resolver/resource-resolver.component.ts +69 -0
  113. package/src/ccc-resource/resource-store.service.ts +93 -0
  114. package/src/ccc-resource/resource-view/resource-view.component.html +133 -0
  115. package/src/ccc-resource/resource-view/resource-view.component.scss +150 -0
  116. package/src/ccc-resource/resource-view/resource-view.component.spec.ts +22 -0
  117. package/src/ccc-resource/resource-view/resource-view.component.ts +354 -0
  118. package/src/ccc-resource/resources-helpers.ts +262 -0
  119. package/src/ccc-resource/utils/validator-utils.ts +6 -0
  120. package/{fesm2022/cccteam-ccc-lib.mjs → src/index.ts} +32 -11
  121. package/src/internal-types/index.ts +1 -0
  122. package/src/internal-types/ng-package.json +6 -0
  123. package/src/types/auth.actions.ts +46 -0
  124. package/src/types/configs.ts +952 -0
  125. package/src/types/constants.ts +1 -0
  126. package/src/types/core.actions.ts +33 -0
  127. package/src/types/{public-api.d.ts → index.ts} +3 -0
  128. package/src/types/ng-package.json +6 -0
  129. package/src/types/notification-message.ts +20 -0
  130. package/src/types/{permissions.d.ts → permissions.ts} +9 -9
  131. package/src/types/{session-info.d.ts → session-info.ts} +4 -3
  132. package/src/types/tokens.ts +20 -0
  133. package/src/ui-alert/alert.component.html +13 -0
  134. package/src/ui-alert/alert.component.scss +48 -0
  135. package/src/ui-alert/alert.component.spec.ts +22 -0
  136. package/src/ui-alert/alert.component.ts +35 -0
  137. package/src/ui-alert/ng-package.json +6 -0
  138. package/src/ui-core-service/index.ts +1 -0
  139. package/src/ui-core-service/ng-package.json +6 -0
  140. package/src/ui-core-service/ui-core.service.ts +34 -0
  141. package/src/ui-interceptor/api.interceptor.spec.ts +16 -0
  142. package/src/ui-interceptor/api.interceptor.ts +45 -0
  143. package/src/ui-interceptor/ng-package.json +6 -0
  144. package/src/ui-notification-service/ng-package.json +6 -0
  145. package/src/ui-notification-service/notification.service.ts +59 -0
  146. package/src/ui-sidenav/ng-package.json +6 -0
  147. package/src/ui-sidenav/sidenav.component.html +60 -0
  148. package/src/ui-sidenav/sidenav.component.scss +99 -0
  149. package/src/ui-sidenav/sidenav.component.spec.ts +22 -0
  150. package/src/ui-sidenav/sidenav.component.ts +64 -0
  151. package/src/util-request-options/ng-package.json +6 -0
  152. package/src/util-request-options/request-options.ts +17 -0
  153. package/tsconfig.lib.json +13 -0
  154. package/tsconfig.lib.prod.json +11 -0
  155. package/tsconfig.spec.json +15 -0
  156. package/cccteam-ccc-lib-0.0.13.tgz +0 -0
  157. package/fesm2022/cccteam-ccc-lib-src-auth-authentication-guard.mjs +0 -44
  158. package/fesm2022/cccteam-ccc-lib-src-auth-authentication-guard.mjs.map +0 -1
  159. package/fesm2022/cccteam-ccc-lib-src-auth-authorization-guard.mjs +0 -24
  160. package/fesm2022/cccteam-ccc-lib-src-auth-authorization-guard.mjs.map +0 -1
  161. package/fesm2022/cccteam-ccc-lib-src-auth-forms.mjs +0 -118
  162. package/fesm2022/cccteam-ccc-lib-src-auth-forms.mjs.map +0 -1
  163. package/fesm2022/cccteam-ccc-lib-src-auth-has-permission.mjs +0 -51
  164. package/fesm2022/cccteam-ccc-lib-src-auth-has-permission.mjs.map +0 -1
  165. package/fesm2022/cccteam-ccc-lib-src-auth-service.mjs +0 -56
  166. package/fesm2022/cccteam-ccc-lib-src-auth-service.mjs.map +0 -1
  167. package/fesm2022/cccteam-ccc-lib-src-auth-state.mjs +0 -109
  168. package/fesm2022/cccteam-ccc-lib-src-auth-state.mjs.map +0 -1
  169. package/fesm2022/cccteam-ccc-lib-src-types.mjs +0 -137
  170. package/fesm2022/cccteam-ccc-lib-src-types.mjs.map +0 -1
  171. package/fesm2022/cccteam-ccc-lib-src-ui-alert.mjs +0 -48
  172. package/fesm2022/cccteam-ccc-lib-src-ui-alert.mjs.map +0 -1
  173. package/fesm2022/cccteam-ccc-lib-src-ui-core-state.mjs +0 -100
  174. package/fesm2022/cccteam-ccc-lib-src-ui-core-state.mjs.map +0 -1
  175. package/fesm2022/cccteam-ccc-lib-src-ui-interceptor.mjs +0 -48
  176. package/fesm2022/cccteam-ccc-lib-src-ui-interceptor.mjs.map +0 -1
  177. package/fesm2022/cccteam-ccc-lib-src-ui-notification-service.mjs +0 -57
  178. package/fesm2022/cccteam-ccc-lib-src-ui-notification-service.mjs.map +0 -1
  179. package/fesm2022/cccteam-ccc-lib-src-ui-sidenav.mjs +0 -70
  180. package/fesm2022/cccteam-ccc-lib-src-ui-sidenav.mjs.map +0 -1
  181. package/fesm2022/cccteam-ccc-lib-src-util-request-options.mjs +0 -19
  182. package/fesm2022/cccteam-ccc-lib-src-util-request-options.mjs.map +0 -1
  183. package/fesm2022/cccteam-ccc-lib.mjs.map +0 -1
  184. package/index.d.ts +0 -5
  185. package/public-api.d.ts +0 -13
  186. package/src/auth-authentication-guard/authentication.guard.d.ts +0 -3
  187. package/src/auth-authentication-guard/index.d.ts +0 -5
  188. package/src/auth-authorization-guard/authorization.guard.d.ts +0 -3
  189. package/src/auth-authorization-guard/index.d.ts +0 -5
  190. package/src/auth-forms/ccc-field/ccc-field.component.d.ts +0 -25
  191. package/src/auth-forms/form-helpers.d.ts +0 -16
  192. package/src/auth-forms/index.d.ts +0 -5
  193. package/src/auth-has-permission/has-permission.directive.d.ts +0 -12
  194. package/src/auth-has-permission/index.d.ts +0 -5
  195. package/src/auth-service/auth.service.d.ts +0 -24
  196. package/src/auth-service/index.d.ts +0 -5
  197. package/src/auth-state/auth.state.d.ts +0 -27
  198. package/src/auth-state/index.d.ts +0 -5
  199. package/src/auth-state/public-api.d.ts +0 -1
  200. package/src/types/auth.actions.d.ts +0 -41
  201. package/src/types/core.actions.d.ts +0 -31
  202. package/src/types/index.d.ts +0 -5
  203. package/src/types/notification-message.d.ts +0 -18
  204. package/src/types/tokens.d.ts +0 -13
  205. package/src/ui-alert/alert.component.d.ts +0 -13
  206. package/src/ui-alert/index.d.ts +0 -5
  207. package/src/ui-core-state/core.state.d.ts +0 -28
  208. package/src/ui-core-state/index.d.ts +0 -5
  209. package/src/ui-core-state/public-api.d.ts +0 -1
  210. package/src/ui-interceptor/api.interceptor.d.ts +0 -12
  211. package/src/ui-interceptor/index.d.ts +0 -5
  212. package/src/ui-notification-service/index.d.ts +0 -5
  213. package/src/ui-notification-service/notification.service.d.ts +0 -30
  214. package/src/ui-sidenav/index.d.ts +0 -5
  215. package/src/ui-sidenav/sidenav.component.d.ts +0 -31
  216. package/src/util-request-options/index.d.ts +0 -5
  217. package/src/util-request-options/request-options.d.ts +0 -8
  218. /package/src/auth-authentication-guard/{public-api.d.ts → index.ts} +0 -0
  219. /package/src/auth-authorization-guard/{public-api.d.ts → index.ts} +0 -0
  220. /package/src/auth-has-permission/{public-api.d.ts → index.ts} +0 -0
  221. /package/src/auth-service/{public-api.d.ts → index.ts} +0 -0
  222. /package/src/ui-alert/{public-api.d.ts → index.ts} +0 -0
  223. /package/src/ui-interceptor/{public-api.d.ts → index.ts} +0 -0
  224. /package/src/ui-notification-service/{public-api.d.ts → index.ts} +0 -0
  225. /package/src/ui-sidenav/{public-api.d.ts → index.ts} +0 -0
  226. /package/src/util-request-options/{public-api.d.ts → index.ts} +0 -0
@@ -0,0 +1,303 @@
1
+ import {
2
+ ChangeDetectionStrategy,
3
+ Component,
4
+ computed,
5
+ DestroyRef,
6
+ effect,
7
+ inject,
8
+ input,
9
+ OnInit,
10
+ output,
11
+ signal,
12
+ } from '@angular/core';
13
+ import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop';
14
+ import { FormControl, FormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms';
15
+ import { MatButtonModule } from '@angular/material/button';
16
+ import { MatDividerModule } from '@angular/material/divider';
17
+ import { MatIconModule } from '@angular/material/icon';
18
+ import { MatInputModule } from '@angular/material/input';
19
+ import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
20
+ import { ActivatedRoute, Data, Router, RouterModule } from '@angular/router';
21
+ import { CamelCaseToTitlePipe } from '@cccteam/ccc-lib/src/ccc-camel-case-to-title';
22
+ import {
23
+ ChildResourceConfig,
24
+ DataType,
25
+ FieldElement,
26
+ ListViewConfig,
27
+ RecordData,
28
+ Resource,
29
+ RESOURCE_META,
30
+ ResourceMeta,
31
+ RootConfig,
32
+ ViewConfig,
33
+ } from '@cccteam/ccc-lib/src/types';
34
+ import { NotificationService } from '@cccteam/ccc-lib/src/ui-notification-service';
35
+ import { tap } from 'rxjs';
36
+ import { FormStateService } from '../form-state.service';
37
+ import { flattenElements } from '../gui-constants';
38
+ import { CreateOperation } from '../operation-types';
39
+ import { ResourceCacheService } from '../resource-cache.service';
40
+ import { ResourceLayoutComponent } from '../resource-layout/resource-layout.component';
41
+ import { ResourceStore } from '../resource-store.service';
42
+ import { cleanStringForm, metadataTypeCoercion } from '../resources-helpers';
43
+
44
+ @Component({
45
+ selector: 'ccc-resource-create',
46
+ imports: [
47
+ FormsModule,
48
+ ReactiveFormsModule,
49
+ MatInputModule,
50
+ RouterModule,
51
+ MatButtonModule,
52
+ MatIconModule,
53
+ MatProgressSpinnerModule,
54
+ MatDividerModule,
55
+ ResourceLayoutComponent,
56
+ ],
57
+ templateUrl: './resource-create.component.html',
58
+ styleUrl: './resource-create.component.scss',
59
+ changeDetection: ChangeDetectionStrategy.OnPush,
60
+ providers: [ResourceStore],
61
+ })
62
+ export class ResourceCreateComponent implements OnInit {
63
+ activatedRoute = inject(ActivatedRoute);
64
+ notifications = inject(NotificationService);
65
+ store = inject(ResourceStore);
66
+ cache = inject(ResourceCacheService);
67
+ router = inject(Router);
68
+ destroyRef = inject(DestroyRef);
69
+ formState = inject(FormStateService);
70
+ resourceMeta = inject(RESOURCE_META);
71
+
72
+ routeData = toSignal<Data>(this.activatedRoute.data);
73
+
74
+ isDirty = signal(false);
75
+ submitted = signal<boolean>(false);
76
+ complete = output<boolean>();
77
+ resourceConfig = input<ChildResourceConfig>();
78
+ parentData = input<RecordData>({});
79
+ loadCreatedResource = input<boolean>(false);
80
+
81
+ rootConfig = computed(() => {
82
+ return this.activatedRoute.snapshot.data['config'] as RootConfig;
83
+ });
84
+
85
+ config = computed(() => {
86
+ const inputConfig = this.resourceConfig() as ViewConfig;
87
+ if (inputConfig !== undefined) {
88
+ return inputConfig;
89
+ }
90
+ return this.rootConfig().parentConfig as ListViewConfig;
91
+ });
92
+
93
+ indentTitle = computed(() => {
94
+ if (this.config().collapsible || this.resourceConfig() === undefined) {
95
+ return false;
96
+ }
97
+
98
+ return true;
99
+ });
100
+
101
+ form = computed(() => {
102
+ const meta = this.store.resourceMeta();
103
+ const fg = new FormGroup({});
104
+ const allElements = flattenElements(this.config().elements);
105
+
106
+ for (const field of meta.fields || []) {
107
+ if (fg.get(field.fieldName)) {
108
+ continue;
109
+ }
110
+
111
+ let control = new FormControl<DataType>('');
112
+ if (field.displayType === 'boolean') {
113
+ control = new FormControl<boolean>(false);
114
+ }
115
+
116
+ const findElement = allElements.find((element) => element.type === 'field' && element.name === field.fieldName);
117
+ const fieldConfig = findElement as FieldElement | undefined;
118
+ if (!fieldConfig) {
119
+ continue;
120
+ }
121
+ const fieldDefault = fieldConfig.default;
122
+ if (field.displayType === 'boolean') {
123
+ const booleanDefaultValue =
124
+ fieldDefault?.type == 'static' && typeof fieldDefault?.value === 'boolean' ? fieldDefault.value : null;
125
+ if (booleanDefaultValue === null) {
126
+ console.error(
127
+ `Default value for boolean field, ${field.fieldName}, is null, add a default value to the config`,
128
+ );
129
+ }
130
+ control = new FormControl<boolean | null>(booleanDefaultValue);
131
+ }
132
+
133
+ if (fieldDefault?.type === 'foreignKey' && this.parentData()) {
134
+ const parentValue = this.parentData()[fieldDefault.parentId];
135
+ if (parentValue !== undefined) {
136
+ control.setValue(parentValue);
137
+ }
138
+ } else if (fieldDefault?.type == 'static') {
139
+ const staticDefault = fieldDefault;
140
+ if (staticDefault.value) {
141
+ control.setValue(staticDefault.value);
142
+ }
143
+ }
144
+
145
+ if (fieldConfig.validators.length > 0) {
146
+ control.setValidators(fieldConfig.validators);
147
+ }
148
+
149
+ if (field.required && !control.hasValidator(Validators.required)) {
150
+ control.setValidators([Validators.required]);
151
+ }
152
+ fg.addControl(field.fieldName, control);
153
+ }
154
+ return fg;
155
+ });
156
+
157
+ route = computed(() => {
158
+ const meta = this.store.resourceMeta();
159
+ if (!meta) return '';
160
+ return meta.consolidatedRoute || meta.route;
161
+ });
162
+
163
+ primaryKeys = computed(() => {
164
+ const meta = this.store.resourceMeta() as ResourceMeta;
165
+ if (!meta) return [];
166
+ return meta.fields
167
+ .filter((field) => field.primaryKey)
168
+ .sort((a, b) => a.primaryKey!.ordinalPosition - b.primaryKey!.ordinalPosition);
169
+ });
170
+
171
+ hasRequiredPrimaryKey = computed(() => {
172
+ const meta = this.store.resourceMeta() as ResourceMeta;
173
+ if (!meta) {
174
+ return false;
175
+ }
176
+ return meta.fields.some((field) => field.primaryKey && field.required);
177
+ });
178
+
179
+ primaryKeyPath = computed(() => {
180
+ const meta = this.store.resourceMeta();
181
+ const isConsolidated = meta.consolidatedRoute !== undefined;
182
+ const pathPrefix = isConsolidated ? '/' + meta.route : '';
183
+ const keyPath = this.primaryKeys()
184
+ .map((field) => this.form().get(field.fieldName)?.value)
185
+ .join('/');
186
+
187
+ if (keyPath === '' && pathPrefix === '') {
188
+ return '/';
189
+ }
190
+
191
+ if (keyPath === '') {
192
+ return pathPrefix;
193
+ }
194
+
195
+ return pathPrefix + '/' + keyPath;
196
+ });
197
+
198
+ camelCaseToTitlePipe = new CamelCaseToTitlePipe();
199
+
200
+ ngOnInit(): void {
201
+ if (this.resourceMeta(this.config().primaryResource as Resource)) {
202
+ this.store.resourceName.set(this.config().primaryResource as Resource);
203
+ this.store.resourceMeta.set(this.resourceMeta(this.config().primaryResource as Resource));
204
+ }
205
+ }
206
+
207
+ saveForm(): void {
208
+ if (!this.form().valid) {
209
+ this.submitted.set(true);
210
+ this.form().markAllAsTouched();
211
+ const formElement = document.getElementById('resource-form');
212
+ const invalidField = formElement?.querySelector('.mdc-text-field--invalid');
213
+ if (invalidField) {
214
+ invalidField.scrollIntoView({ behavior: 'smooth' });
215
+ }
216
+ return;
217
+ }
218
+
219
+ const resourceMeta = this.store.resourceMeta();
220
+ if (!resourceMeta) return;
221
+
222
+ const cleanedForm = cleanStringForm<Record<string, string>>(this.form());
223
+ if (cleanedForm === undefined || cleanedForm === null) {
224
+ return;
225
+ }
226
+ const coercedCleanedData = metadataTypeCoercion(cleanedForm, this.store.resourceMeta());
227
+
228
+ const cleanedDataWithoutPrimaryKeys = Object.fromEntries(
229
+ Object.entries(coercedCleanedData).filter(
230
+ ([key]) => !this.primaryKeys().some((field) => field.fieldName === key),
231
+ ),
232
+ );
233
+
234
+ const createPatch: CreateOperation = {
235
+ op: 'add',
236
+ value: cleanedDataWithoutPrimaryKeys,
237
+ path: this.primaryKeyPath(),
238
+ };
239
+
240
+ this.cache
241
+ .createPatch(createPatch, this.route(), this.store.resourceName())
242
+ .pipe(
243
+ tap((response) => {
244
+ this.formState.decrementDirtyForms();
245
+
246
+ if (!response) {
247
+ this.complete.emit(true);
248
+ return;
249
+ }
250
+ const navigationRoutes = this.config().createNavigation;
251
+ const createIds = (response[this.store.resourceRoute()] as string[]) || [];
252
+ if (this.loadCreatedResource() && createIds.length === 1) {
253
+ let route = this.rootConfig().routeData.route;
254
+ if (this.parentData() || !route) {
255
+ route = resourceMeta.route;
256
+ }
257
+ if (navigationRoutes.length === 0) {
258
+ this.router.navigate([route, createIds[0]]);
259
+ } else {
260
+ if (createIds[0]) {
261
+ navigationRoutes.push(createIds[0]);
262
+ }
263
+ this.router.navigate(navigationRoutes);
264
+ }
265
+ } else {
266
+ this.complete.emit(true);
267
+ }
268
+ }),
269
+ )
270
+ .subscribe();
271
+ }
272
+
273
+ cancelForm(): void {
274
+ if (this.form().dirty) {
275
+ this.formState.decrementDirtyForms();
276
+ }
277
+ this.complete.emit(true);
278
+ }
279
+
280
+ constructor() {
281
+ effect(() => {
282
+ console.debug('USAGE | New FormGroup subscription for: ', this.config().title);
283
+
284
+ this.form()
285
+ .valueChanges.pipe(
286
+ tap(() => {
287
+ const dirty = this.form().dirty;
288
+
289
+ if (dirty !== this.isDirty()) {
290
+ this.isDirty.set(dirty);
291
+ if (dirty) {
292
+ this.formState.incrementDirtyForms();
293
+ } else {
294
+ this.formState.decrementDirtyForms();
295
+ }
296
+ }
297
+ }),
298
+ takeUntilDestroyed(this.destroyRef),
299
+ )
300
+ .subscribe();
301
+ });
302
+ }
303
+ }
@@ -0,0 +1,102 @@
1
+ import { computed, Directive, inject, Injector, input, signal } from '@angular/core';
2
+ import { FormGroup } from '@angular/forms';
3
+ import { DataType, FieldElement, FieldMeta, Meta, RecordData, RESOURCE_META, RPCFieldMeta } from '@cccteam/ccc-lib/src/types';
4
+ import { ResourceCacheService } from '../resource-cache.service';
5
+ import { isUUID } from '../resources-helpers';
6
+
7
+ /*
8
+ * Base class for all resource input components.
9
+ */
10
+ @Directive()
11
+ export abstract class BaseInputComponent {
12
+ injector = inject(Injector);
13
+ cache = inject(ResourceCacheService);
14
+ resourceMeta = inject(RESOURCE_META);
15
+
16
+ meta = input.required<Meta>();
17
+ pristineValue = input<DataType | null>();
18
+ editMode = input.required<'edit' | 'view'>();
19
+ showField = input<boolean>();
20
+ fieldConfig = input.required<FieldElement>();
21
+ fieldClass = input<string>();
22
+ fieldMeta = input.required<FieldMeta | RPCFieldMeta>();
23
+ form = input.required<FormGroup>();
24
+ relatedData = input<RecordData>();
25
+
26
+ /**
27
+ * Resets the field back to its pristine value.
28
+ */
29
+ reset(): void {
30
+ const control = this.form().get(this.fieldConfig().name as string);
31
+ if (control) {
32
+ control.setValue(this.pristineValue());
33
+ control.markAsPristine();
34
+ }
35
+ }
36
+
37
+ prefixString = computed(() => {
38
+ const relatedData = this.relatedData();
39
+
40
+ let builtPrefix = '';
41
+ this.fieldConfig().prefixes.forEach((prefix) => {
42
+ if (typeof prefix === 'string') {
43
+ builtPrefix += prefix;
44
+ } else if ('resource' in prefix) {
45
+ if (!relatedData || !relatedData[prefix.id]) return;
46
+ const id = relatedData[prefix.id];
47
+ if (typeof id !== 'string') return;
48
+ if (!id || !isUUID(id)) return;
49
+ const prefixResourceMeta = this.resourceMeta(prefix.resource);
50
+ const resource = this.cache
51
+ .registerView(signal(prefixResourceMeta.route), signal(prefix.resource), signal(id))
52
+ ?.value();
53
+ if (resource && prefix.field in resource) {
54
+ builtPrefix += resource[prefix.field];
55
+ }
56
+ } else if ('field' in prefix) {
57
+ if (relatedData) {
58
+ const fieldValue = relatedData[prefix.field];
59
+ builtPrefix += fieldValue ? fieldValue : '';
60
+ }
61
+ }
62
+ });
63
+ return builtPrefix;
64
+ });
65
+
66
+ suffixString = computed(() => {
67
+ const relatedData = this.relatedData();
68
+
69
+ let builtSuffix = '';
70
+ this.fieldConfig().suffixes.forEach((suffix) => {
71
+ if (typeof suffix === 'string') {
72
+ builtSuffix += suffix;
73
+ } else if ('resource' in suffix) {
74
+ if (!relatedData || !relatedData[suffix.id]) return;
75
+ const id = relatedData[suffix.id];
76
+ if (typeof id !== 'string') return;
77
+ if (!id || !isUUID(id)) return;
78
+ const suffixResourceMeta = this.resourceMeta(suffix.resource);
79
+ const resource = this.cache
80
+ .registerView(signal(suffixResourceMeta.route), signal(suffix.resource), signal(id))
81
+ ?.value();
82
+ if (resource && suffix.field in resource) {
83
+ builtSuffix += resource[suffix.field];
84
+ }
85
+ } else if ('field' in suffix) {
86
+ if (relatedData) {
87
+ const fieldValue = relatedData[suffix.field];
88
+ builtSuffix += fieldValue ? fieldValue : '';
89
+ }
90
+ }
91
+ });
92
+ return builtSuffix;
93
+ });
94
+
95
+ floatLabel = computed(() => {
96
+ if (this.fieldConfig().prefixes.length > 0 || this.fieldConfig().suffixes.length > 0) {
97
+ return 'always';
98
+ } else {
99
+ return 'auto';
100
+ }
101
+ });
102
+ }
@@ -0,0 +1,16 @@
1
+ @if (showField()) {
2
+ <ng-container [formGroup]="form()">
3
+ <div [class.readonly-field]="editMode() === 'view'">
4
+ <mat-checkbox
5
+ class="field checkbox {{ fieldClass() }}"
6
+ [formControlName]="fieldConfig().name"
7
+ (keydown.enter)="$event.preventDefault()">
8
+ {{ fieldConfig().label }}
9
+ </mat-checkbox>
10
+ </div>
11
+ </ng-container>
12
+ } @else {
13
+ <ng-container [formGroup]="form()">
14
+ <input type="hidden" [formControlName]="fieldConfig().name" />
15
+ </ng-container>
16
+ }
@@ -0,0 +1,22 @@
1
+ import { ComponentFixture, TestBed } from '@angular/core/testing';
2
+
3
+ import { BooleanFieldComponent } from './boolean-field.component';
4
+
5
+ xdescribe('BooleanFieldComponent', () => {
6
+ let component: BooleanFieldComponent;
7
+ let fixture: ComponentFixture<BooleanFieldComponent>;
8
+
9
+ beforeEach(async () => {
10
+ await TestBed.configureTestingModule({
11
+ imports: [BooleanFieldComponent],
12
+ }).compileComponents();
13
+
14
+ fixture = TestBed.createComponent(BooleanFieldComponent);
15
+ component = fixture.componentInstance;
16
+ fixture.detectChanges();
17
+ });
18
+
19
+ it('should create', () => {
20
+ expect(component).toBeTruthy();
21
+ });
22
+ });
@@ -0,0 +1,15 @@
1
+ import { Component } from '@angular/core';
2
+ import { ReactiveFormsModule } from '@angular/forms';
3
+ import { MatCheckboxModule } from '@angular/material/checkbox';
4
+ import { MatDatepickerModule } from '@angular/material/datepicker';
5
+ import { MatFormFieldModule } from '@angular/material/form-field';
6
+ import { MatInputModule } from '@angular/material/input';
7
+ import { BaseInputComponent } from '../../base-field.directive';
8
+
9
+ @Component({
10
+ selector: 'ccc-boolean-field',
11
+ imports: [MatFormFieldModule, MatInputModule, MatDatepickerModule, ReactiveFormsModule, MatCheckboxModule],
12
+ templateUrl: './boolean-field.component.html',
13
+ styleUrl: './boolean-field.component.scss',
14
+ })
15
+ export class BooleanFieldComponent extends BaseInputComponent {}
@@ -0,0 +1,13 @@
1
+ @if (showField()) {
2
+ <div class="readonly-field">
3
+ <mat-form-field class="field {{ fieldClass() }}" [subscriptSizing]="'dynamic'">
4
+ <mat-label>{{ fieldConfig().label }}</mat-label>
5
+ <input
6
+ matInput
7
+ (keydown.enter)="$event.preventDefault()"
8
+ [readonly]="true"
9
+ type="text"
10
+ [value]="computedValue()" />
11
+ </mat-form-field>
12
+ </div>
13
+ }
@@ -0,0 +1,23 @@
1
+ import { ComponentFixture, TestBed } from '@angular/core/testing';
2
+
3
+ import { ComputedFieldComponent } from './computed-field.component';
4
+
5
+ describe('ComputedFieldComponent', () => {
6
+ let component: ComputedFieldComponent;
7
+ let fixture: ComponentFixture<ComputedFieldComponent>;
8
+
9
+ beforeEach(async () => {
10
+ await TestBed.configureTestingModule({
11
+ imports: [ComputedFieldComponent]
12
+ })
13
+ .compileComponents();
14
+
15
+ fixture = TestBed.createComponent(ComputedFieldComponent);
16
+ component = fixture.componentInstance;
17
+ fixture.detectChanges();
18
+ });
19
+
20
+ it('should create', () => {
21
+ expect(component).toBeTruthy();
22
+ });
23
+ });
@@ -0,0 +1,50 @@
1
+ import { Component, computed, effect, HostBinding, input } from '@angular/core';
2
+ import { MatButtonModule } from '@angular/material/button';
3
+ import { MatFormFieldModule } from '@angular/material/form-field';
4
+ import { MatInputModule } from '@angular/material/input';
5
+ import { ComputedDisplayFieldElement, RecordData } from '@cccteam/ccc-lib/src/types';
6
+
7
+ @Component({
8
+ selector: 'ccc-computed-field',
9
+ imports: [MatFormFieldModule, MatButtonModule, MatInputModule],
10
+ templateUrl: './computed-field.component.html',
11
+ styleUrl: './computed-field.component.scss',
12
+ })
13
+ export class ComputedFieldComponent {
14
+ fieldConfig = input.required<ComputedDisplayFieldElement>();
15
+ fieldClass = input<string>();
16
+ formDataState = input<RecordData>();
17
+
18
+ computedValue = computed(() => {
19
+ try {
20
+ return this.fieldConfig().calculatedValue(this.formDataState());
21
+ } catch (e) {
22
+ console.error('Failed to calculate value for computed field: ', this.fieldConfig().label);
23
+ console.error(e);
24
+ return '';
25
+ }
26
+ });
27
+
28
+ showField = computed(() => {
29
+ const shouldRender = this.fieldConfig().shouldRender;
30
+ if (typeof shouldRender === 'boolean') {
31
+ return shouldRender;
32
+ }
33
+
34
+ try {
35
+ return shouldRender(this.formDataState());
36
+ } catch (e) {
37
+ console.error('Failed to calculate value for should Render function for field: ', this.fieldConfig().label);
38
+ console.error(e);
39
+ return true;
40
+ }
41
+ });
42
+
43
+ @HostBinding('class') class = '';
44
+
45
+ constructor() {
46
+ effect(() => {
47
+ this.class = this.showField() ? 'col-' + this.fieldConfig()?.cols : 'hidden-field';
48
+ });
49
+ }
50
+ }
@@ -0,0 +1,22 @@
1
+ @if (showField()) {
2
+ <div [class.readonly-field]="editMode() === 'view'">
3
+ <mat-form-field class="field {{ fieldClass() }}" [formGroup]="form()" [subscriptSizing]="'dynamic'">
4
+ <mat-label>{{ fieldConfig().label }}</mat-label>
5
+ <input
6
+ matInput
7
+ [readonly]="editMode() === 'view'"
8
+ [matDatepicker]="picker"
9
+ type="text"
10
+ (keydown.enter)="$event.preventDefault()"
11
+ [formControlName]="fieldConfig().name" />
12
+ @if (editMode() === 'edit') {
13
+ <mat-datepicker-toggle matSuffix [for]="picker"></mat-datepicker-toggle>
14
+ }
15
+ <mat-datepicker #picker></mat-datepicker>
16
+ </mat-form-field>
17
+ </div>
18
+ } @else {
19
+ <ng-container [formGroup]="form()">
20
+ <input type="hidden" [formControlName]="fieldConfig().name" />
21
+ </ng-container>
22
+ }
@@ -0,0 +1,22 @@
1
+ import { ComponentFixture, TestBed } from '@angular/core/testing';
2
+
3
+ import { DateFieldComponent } from './date-field.component';
4
+
5
+ xdescribe('DateFieldComponent', () => {
6
+ let component: DateFieldComponent;
7
+ let fixture: ComponentFixture<DateFieldComponent>;
8
+
9
+ beforeEach(async () => {
10
+ await TestBed.configureTestingModule({
11
+ imports: [DateFieldComponent],
12
+ }).compileComponents();
13
+
14
+ fixture = TestBed.createComponent(DateFieldComponent);
15
+ component = fixture.componentInstance;
16
+ fixture.detectChanges();
17
+ });
18
+
19
+ it('should create', () => {
20
+ expect(component).toBeTruthy();
21
+ });
22
+ });
@@ -0,0 +1,14 @@
1
+ import { Component } from '@angular/core';
2
+ import { ReactiveFormsModule } from '@angular/forms';
3
+ import { MatDatepickerModule } from '@angular/material/datepicker';
4
+ import { MatFormFieldModule } from '@angular/material/form-field';
5
+ import { MatInputModule } from '@angular/material/input';
6
+ import { BaseInputComponent } from '../../base-field.directive';
7
+
8
+ @Component({
9
+ selector: 'ccc-date-field',
10
+ imports: [MatFormFieldModule, MatInputModule, MatDatepickerModule, ReactiveFormsModule],
11
+ templateUrl: './date-field.component.html',
12
+ styleUrl: './date-field.component.scss',
13
+ })
14
+ export class DateFieldComponent extends BaseInputComponent {}