@masterteam/form-builder 0.0.1 → 0.0.2

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.
@@ -3,6 +3,8 @@
3
3
  "save": "Save",
4
4
  "cancel": "Cancel",
5
5
  "delete": "Delete",
6
+ "name-ar": "الاسم بالعربية",
7
+ "name-en": "الاسم بالإنجليزية",
6
8
  "system": "System",
7
9
  "custom": "Custom",
8
10
  "layout": "Layout",
@@ -3,6 +3,8 @@
3
3
  "save": "Save",
4
4
  "cancel": "Cancel",
5
5
  "delete": "Delete",
6
+ "name-ar": "Name (Arabic)",
7
+ "name-en": "Name (English)",
6
8
  "system": "System",
7
9
  "custom": "Custom",
8
10
  "layout": "Layout",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@masterteam/form-builder",
3
- "version": "0.0.1",
3
+ "version": "0.0.2",
4
4
  "publishConfig": {
5
5
  "directory": ".",
6
6
  "linkDirectory": false,
@@ -21,8 +21,8 @@
21
21
  "tailwindcss-primeui": "^0.6.1",
22
22
  "tslib": "^2.8.1",
23
23
  "@masterteam/properties": "^0.0.26",
24
- "@masterteam/components": "^0.0.75",
25
24
  "@masterteam/forms": "^0.0.33",
25
+ "@masterteam/components": "^0.0.76",
26
26
  "@masterteam/icons": "^0.0.12"
27
27
  },
28
28
  "sideEffects": false,
@@ -21,9 +21,9 @@ import { ConfirmationService } from '@masterteam/components/confirmation';
21
21
  import type {
22
22
  EnrichedFormField,
23
23
  EnrichedFormSection,
24
- } from '../form-builder.model';
24
+ FieldWidth,
25
+ } from '../../store/form-builder/form-builder.model';
25
26
  import { FormBuilderFacade } from '../../store/form-builder';
26
- import type { FieldWidth } from '../../store/form-builder/form-builder.model';
27
27
  import {
28
28
  FBFieldConditions,
29
29
  ConditionFieldProperty,
@@ -208,7 +208,8 @@ export class FBFieldForm {
208
208
  header: this.transloco.translate('formBuilder.set-conditions'),
209
209
  styleClass: '!w-[calc(100%-25rem)] !absolute ',
210
210
  position: 'start',
211
- modal: false,
211
+ modal: true,
212
+ dismissible: true,
212
213
  appendTo: '#page-content',
213
214
  inputValues: {
214
215
  initialFormula: this.conditionalDisplayFormula(),
@@ -13,7 +13,7 @@ import { DynamicFormConfig, DynamicFieldConfig } from '@masterteam/components';
13
13
  import { Tabs } from '@masterteam/components/tabs';
14
14
  import { ModalService } from '@masterteam/components/modal';
15
15
  import { ModalRef } from '@masterteam/components/dialog';
16
- import type { EnrichedFormSection } from '../form-builder.model';
16
+ import type { EnrichedFormSection } from '../../store/form-builder/form-builder.model';
17
17
 
18
18
  type PreviewMode = 'create' | 'edit';
19
19
 
@@ -103,24 +103,29 @@ export class FBPreviewForm {
103
103
  */
104
104
  private mapFieldType(viewType: string): string {
105
105
  const typeMap: Record<string, string> = {
106
- text: 'text',
107
- number: 'number',
108
- date: 'date',
109
- select: 'select',
110
- multiselect: 'multiselect',
111
- toggle: 'toggle',
112
- checkbox: 'checkbox',
113
- textarea: 'textarea',
114
- editor: 'editor',
115
- color: 'color',
116
- slider: 'slider',
117
- radio: 'radio',
118
- 'radio-cards': 'radio-cards',
119
- upload: 'upload',
120
- 'user-search': 'user-search',
106
+ User: 'select',
107
+ Text: 'text',
108
+ LongText: 'editor-field',
109
+ Percentage: 'slider',
110
+ Date: 'date',
111
+ Currency: 'text',
112
+ Number: 'number',
113
+ Lookup: 'select',
114
+ LookupMultiSelect: 'select',
115
+ Checkbox: 'toggle',
116
+ InternalModule: 'select',
117
+ DynamicList: 'select',
118
+ API: 'select',
119
+ Time: 'date',
120
+ Status: 'select',
121
+ Attachment: 'attachment',
122
+ EditableListView: 'actionableTable',
123
+ LookupLog: 'actionableTable',
124
+ LookupMatrix: 'select',
125
+ Location: 'select',
121
126
  };
122
127
 
123
- return typeMap[viewType?.toLowerCase()] ?? 'text';
128
+ return typeMap[viewType] ?? 'text';
124
129
  }
125
130
 
126
131
  /**
@@ -7,7 +7,10 @@ import {
7
7
  signal,
8
8
  effect,
9
9
  } from '@angular/core';
10
- import { EnrichedFormField, EnrichedFormSection } from '../form-builder.model';
10
+ import type {
11
+ EnrichedFormField,
12
+ EnrichedFormSection,
13
+ } from '../../store/form-builder/form-builder.model';
11
14
  import { ConfirmationService } from '@masterteam/components/confirmation';
12
15
  import { ModalService } from '@masterteam/components/modal';
13
16
  import { TranslocoService } from '@jsverse/transloco';
@@ -127,7 +130,9 @@ export class FBSection {
127
130
  styleClass: '!w-100 !absolute ',
128
131
  position: 'end',
129
132
  appendTo: '#page-content',
130
- modal: false,
133
+ modal: true,
134
+ dismissible: true,
135
+
131
136
  inputValues: {
132
137
  sectionId: section.id,
133
138
  initialData: section.name,
@@ -152,7 +157,9 @@ export class FBSection {
152
157
  height: '20vw',
153
158
  styleClass: '!w-100 !absolute !shadow-none',
154
159
  position: 'end',
155
- modal: false,
160
+ modal: true,
161
+ dismissible: true,
162
+
156
163
  appendTo: '#page-content',
157
164
  inputValues: {
158
165
  initialData: field,
@@ -1,15 +1,33 @@
1
1
  <ng-container *transloco="let t; prefix: 'formBuilder'">
2
2
  <div class="flex gap-4 h-full w-full overflow-hidden" cdkDropListGroup>
3
- @if (properties().length > 0) {
4
- <mt-card
5
- class="z-1 w-1/5 min-w-xs shrink-0 h-full flex flex-col overflow-hidden"
6
- >
7
- <ng-template #headless>
8
- <!-- Header -->
9
- <div class="flex items-center justify-between px-4 pt-5">
10
- <h3 class="text-xl font-semibold">{{ t("form-elements") }}</h3>
11
- </div>
3
+ <!-- Properties Sidebar -->
4
+ <mt-card
5
+ class="z-1 w-1/5 min-w-xs shrink-0 h-full flex flex-col overflow-hidden"
6
+ >
7
+ <ng-template #headless>
8
+ <!-- Header -->
9
+ <div class="flex items-center justify-between px-4 pt-5">
10
+ <h3 class="text-xl font-semibold">{{ t("form-elements") }}</h3>
11
+ </div>
12
12
 
13
+ @if (properties().length === 0) {
14
+ @if (isLoading()) {
15
+ <!-- Properties Loading Skeleton -->
16
+ <div class="p-4 space-y-3">
17
+ <p-skeleton height="2rem" styleClass="mb-4" />
18
+ @for (i of [1, 2, 3, 4, 5, 6]; track i) {
19
+ <p-skeleton height="3rem" styleClass="rounded-lg" />
20
+ }
21
+ </div>
22
+ } @else {
23
+ <!-- No Properties State -->
24
+ <div class="flex-1 flex items-center justify-center p-4">
25
+ <div class="text-center text-muted-color">
26
+ <p class="text-sm">{{ t("no-properties") }}</p>
27
+ </div>
28
+ </div>
29
+ }
30
+ } @else {
13
31
  <!-- Tabs using PrimeNG -->
14
32
  <p-tabs
15
33
  [(value)]="activeTab"
@@ -61,9 +79,9 @@
61
79
  }
62
80
  </p-tabpanels>
63
81
  </p-tabs>
64
- </ng-template>
65
- </mt-card>
66
- }
82
+ }
83
+ </ng-template>
84
+ </mt-card>
67
85
 
68
86
  <!-- Main Canvas Area -->
69
87
  <div class="flex flex-col gap-4 flex-1 w-full h-full overflow-y-auto">
@@ -74,38 +92,74 @@
74
92
  icon="layout.layout-top"
75
93
  [label]="t('add-section')"
76
94
  (onClick)="addSection()"
95
+ [disabled]="isLoading()"
77
96
  ></mt-button>
78
97
  <mt-button
79
98
  icon="general.eye"
80
99
  [label]="t('preview')"
81
100
  (onClick)="openPreview()"
101
+ [disabled]="isLoading()"
82
102
  ></mt-button>
83
103
  <mt-button
84
104
  icon="finance.credit-card-plus"
85
105
  [label]="t('reset')"
86
106
  (onClick)="resetFormConfiguration()"
107
+ [disabled]="isLoading()"
87
108
  ></mt-button>
88
109
  </div>
89
110
  </ng-template>
90
111
  </mt-card>
91
- @for (section of enrichedSections(); track section.id) {
92
- <mt-fb-section
93
- [section]="section"
94
- [sectionsCount]="enrichedSections().length"
95
- [allSections]="enrichedSections()"
96
- (onFieldDrop)="drop($event)"
97
- >
98
- </mt-fb-section>
99
- } @empty {
100
- <mt-card>
101
- <div class="h-27 p-4">
102
- <div
103
- class="flex justify-center items-center gap-4 h-full border-2 border-primary rounded-xl bg-primary-200 text-primary"
104
- >
105
- <span>{{ t("no-section") }}</span>
112
+
113
+ @if (isLoading()) {
114
+ <!-- Form Loading Skeleton -->
115
+ @for (i of [1, 2]; track i) {
116
+ <mt-card>
117
+ <ng-template #headless>
118
+ <div class="p-4 space-y-4">
119
+ <!-- Section header skeleton -->
120
+ <div class="flex items-center justify-between">
121
+ <p-skeleton width="10rem" height="1.5rem" />
122
+ <div class="flex gap-2">
123
+ <p-skeleton width="2rem" height="2rem" shape="circle" />
124
+ <p-skeleton width="2rem" height="2rem" shape="circle" />
125
+ </div>
126
+ </div>
127
+ <!-- Fields skeleton -->
128
+ <div class="grid grid-cols-12 gap-4">
129
+ <div class="col-span-6">
130
+ <p-skeleton height="4rem" styleClass="rounded-lg" />
131
+ </div>
132
+ <div class="col-span-6">
133
+ <p-skeleton height="4rem" styleClass="rounded-lg" />
134
+ </div>
135
+ <div class="col-span-12">
136
+ <p-skeleton height="4rem" styleClass="rounded-lg" />
137
+ </div>
138
+ </div>
139
+ </div>
140
+ </ng-template>
141
+ </mt-card>
142
+ }
143
+ } @else {
144
+ @for (section of enrichedSections(); track section.id) {
145
+ <mt-fb-section
146
+ [section]="section"
147
+ [sectionsCount]="enrichedSections().length"
148
+ [allSections]="enrichedSections()"
149
+ (onFieldDrop)="drop($event)"
150
+ >
151
+ </mt-fb-section>
152
+ } @empty {
153
+ <mt-card>
154
+ <div class="h-27 p-4">
155
+ <div
156
+ class="flex justify-center items-center gap-4 h-full border-2 border-primary rounded-xl bg-primary-200 text-primary"
157
+ >
158
+ <span>{{ t("no-section") }}</span>
159
+ </div>
106
160
  </div>
107
- </div>
108
- </mt-card>
161
+ </mt-card>
162
+ }
109
163
  }
110
164
  </div>
111
165
  </div>
@@ -3,9 +3,10 @@ import type {
3
3
  FormField as BaseFormField,
4
4
  FormSection as BaseFormSection,
5
5
  FieldWidth,
6
+ PropertyItem,
6
7
  } from '../store/form-builder/form-builder.model';
7
8
 
8
- export type { FieldType, FieldWidth };
9
+ export type { FieldType, FieldWidth, PropertyItem };
9
10
 
10
11
  // Re-export base types
11
12
  export type FormField = BaseFormField;
@@ -24,37 +25,3 @@ export interface EnrichedFormField extends BaseFormField {
24
25
  export interface EnrichedFormSection extends Omit<BaseFormSection, 'fields'> {
25
26
  fields: EnrichedFormField[];
26
27
  }
27
- export interface PropertyItem {
28
- id: number;
29
- key: string;
30
- normalizedKey: string;
31
- viewType: string;
32
- viewTypeLabel: string;
33
- name: string | Record<string, string>;
34
- description?: string;
35
- defaultValue?: unknown;
36
- order?: number;
37
- canBeDeleted?: boolean;
38
- displayOverView?: boolean;
39
- enabled?: boolean;
40
- formula?: unknown[];
41
- isBasic?: boolean;
42
- isCalculated?: boolean;
43
- isConfigurable?: boolean;
44
- isHiddenInCreation?: boolean;
45
- isHiddenInEdition?: boolean;
46
- isLive?: boolean;
47
- isLog?: boolean;
48
- isRequired?: boolean;
49
- isSystem?: boolean;
50
- isTranslatable?: boolean;
51
- shownInTable?: boolean;
52
- includeInSummary?: boolean;
53
- dependsOn?: unknown[];
54
- isFilterFormula?: boolean;
55
- configuration?: Record<string, unknown>;
56
- category?: string;
57
- valueOnCreation?: boolean;
58
- isRelated?: boolean;
59
- [key: string]: unknown;
60
- }
@@ -1,26 +1,20 @@
1
- import {
2
- Component,
3
- computed,
4
- inject,
5
- input,
6
- signal,
7
- effect,
8
- } from '@angular/core';
1
+ import { Component, computed, inject, signal, effect } from '@angular/core';
9
2
  import { CommonModule } from '@angular/common';
10
3
  import { FormsModule } from '@angular/forms';
11
4
  import { TabsModule } from 'primeng/tabs';
5
+ import { SkeletonModule } from 'primeng/skeleton';
12
6
  import { Button } from '@masterteam/components/button';
13
7
  import { Card } from '@masterteam/components/card';
14
8
  import { ModalService } from '@masterteam/components/modal';
15
9
  import { ConfirmationService } from '@masterteam/components/confirmation';
16
10
  import { TranslocoDirective, TranslocoService } from '@jsverse/transloco';
17
11
  import { FBSection } from './fb-section/fb-section';
18
- import {
12
+ import type {
19
13
  EnrichedFormSection,
20
14
  EnrichedFormField,
15
+ FieldWidth,
21
16
  PropertyItem,
22
- } from './form-builder.model';
23
- import type { FieldWidth } from '../store/form-builder/form-builder.model';
17
+ } from '../store/form-builder/form-builder.model';
24
18
  import { CdkDragDrop, DragDropModule } from '@angular/cdk/drag-drop';
25
19
  import { FBSectionForm } from './fb-section-form/fb-section-form';
26
20
  import { FBPreviewForm } from './fb-preview-form/fb-preview-form';
@@ -33,6 +27,7 @@ import { FormBuilderFacade } from '../store/form-builder/form-builder.facade';
33
27
  CommonModule,
34
28
  FormsModule,
35
29
  TabsModule,
30
+ SkeletonModule,
36
31
  Button,
37
32
  Card,
38
33
  TranslocoDirective,
@@ -50,14 +45,12 @@ export class FormBuilder {
50
45
 
51
46
  private dialogRef: any;
52
47
 
53
- // Inputs - only properties needed from parent
54
- readonly properties = input<PropertyItem[]>([]);
55
-
56
48
  // Local UI state
57
49
  readonly activeTab = signal<string>('system');
58
50
 
59
51
  // State from facade
60
52
  readonly sections = this.facade.sections;
53
+ readonly properties = this.facade.properties;
61
54
  readonly isLoading = this.facade.isLoadingFormConfiguration;
62
55
  readonly error = this.facade.formConfigurationError;
63
56
 
@@ -113,8 +106,8 @@ export class FormBuilder {
113
106
  (p) => !usedPropertyIds.has(p.id),
114
107
  );
115
108
 
116
- const systemProps = availableProps.filter((p) => p.isSystem);
117
- const customProps = availableProps.filter((p) => !p.isSystem);
109
+ const systemProps = availableProps.filter((p) => p['isSystem']);
110
+ const customProps = availableProps.filter((p) => !p['isSystem']);
118
111
 
119
112
  if (systemProps.length > 0) {
120
113
  tabs.push({ id: 'system', title: 'System', properties: systemProps });
@@ -142,12 +135,17 @@ export class FormBuilder {
142
135
 
143
136
  if (event.previousContainer === event.container) {
144
137
  // Reordering within the same section
145
- const field = event.item.data as EnrichedFormField;
138
+ const fields = [...(event.container.data as EnrichedFormField[])];
139
+ const [movedField] = fields.splice(event.previousIndex, 1);
140
+ fields.splice(event.currentIndex, 0, movedField);
146
141
 
147
- // Update the field order via API
148
- this.facade.updateField(targetSectionId, field.id, {
149
- order: event.currentIndex,
150
- });
142
+ // Build bulk reorder payload with new order values
143
+ const reorderPayload = fields.map((field, index) => ({
144
+ id: field.id,
145
+ order: index,
146
+ }));
147
+
148
+ this.facade.reorderFields(targetSectionId, reorderPayload);
151
149
  } else if (event.previousContainer.id.startsWith('toolbox-')) {
152
150
  // Adding from toolbox
153
151
  const propertyItem = event.item.data as PropertyItem;
@@ -176,6 +174,7 @@ export class FormBuilder {
176
174
  position: 'end',
177
175
  appendTo: '#page-content',
178
176
  modal: true,
177
+ dismissible: true,
179
178
  inputValues: {
180
179
  sectionsCount: this.sections().length,
181
180
  },
@@ -188,6 +187,7 @@ export class FormBuilder {
188
187
  styleClass: '!w-[80vw] ',
189
188
  position: 'end',
190
189
  modal: true,
190
+ dismissible: true,
191
191
  inputValues: {
192
192
  sections: this.enrichedSections(),
193
193
  },
@@ -2,6 +2,8 @@ import type {
2
2
  AddFieldPayload,
3
3
  AddSectionPayload,
4
4
  MoveFieldPayload,
5
+ PropertyItem,
6
+ ReorderFieldPayload,
5
7
  UpdateFieldPayload,
6
8
  UpdateSectionPayload,
7
9
  } from './form-builder.model';
@@ -25,6 +27,11 @@ export class ResetFormBuilderState {
25
27
  static readonly type = '[FormBuilder] Reset State';
26
28
  }
27
29
 
30
+ export class SetProperties {
31
+ static readonly type = '[FormBuilder] Set Properties';
32
+ constructor(public properties: PropertyItem[]) {}
33
+ }
34
+
28
35
  // ============================================================================
29
36
  // Form Configuration Actions
30
37
  // ============================================================================
@@ -88,6 +95,14 @@ export class DeleteField {
88
95
  ) {}
89
96
  }
90
97
 
98
+ export class ReorderFields {
99
+ static readonly type = '[FormBuilder] Reorder Fields';
100
+ constructor(
101
+ public sectionId: string,
102
+ public payload: ReorderFieldPayload[],
103
+ ) {}
104
+ }
105
+
91
106
  export class MoveField {
92
107
  static readonly type = '[FormBuilder] Move Field';
93
108
  constructor(
@@ -6,6 +6,8 @@ import {
6
6
  AddSectionPayload,
7
7
  FormBuilderActionKey,
8
8
  MoveFieldPayload,
9
+ PropertyItem,
10
+ ReorderFieldPayload,
9
11
  UpdateFieldPayload,
10
12
  UpdateSectionPayload,
11
13
  } from './form-builder.model';
@@ -16,9 +18,11 @@ import {
16
18
  DeleteSection,
17
19
  GetFormConfiguration,
18
20
  MoveField,
21
+ ReorderFields,
19
22
  ResetFormBuilderState,
20
23
  ResetFormConfiguration,
21
24
  SetModuleInfo,
25
+ SetProperties,
22
26
  UpdateField,
23
27
  UpdateSection,
24
28
  } from './form-builder.actions';
@@ -36,6 +40,7 @@ export class FormBuilderFacade {
36
40
 
37
41
  readonly formConfiguration = select(FormBuilderState.getFormConfiguration);
38
42
  readonly sections = select(FormBuilderState.getSections);
43
+ readonly properties = select(FormBuilderState.getProperties);
39
44
  readonly moduleType = select(FormBuilderState.getModuleType);
40
45
  readonly moduleId = select(FormBuilderState.getModuleId);
41
46
 
@@ -144,6 +149,10 @@ export class FormBuilderFacade {
144
149
  return this.store.dispatch(new ResetFormBuilderState());
145
150
  }
146
151
 
152
+ setProperties(properties: PropertyItem[]) {
153
+ return this.store.dispatch(new SetProperties(properties));
154
+ }
155
+
147
156
  // ============================================================================
148
157
  // Form Configuration Dispatchers
149
158
  // ============================================================================
@@ -188,6 +197,10 @@ export class FormBuilderFacade {
188
197
  return this.store.dispatch(new DeleteField(sectionId, fieldId));
189
198
  }
190
199
 
200
+ reorderFields(sectionId: string, payload: ReorderFieldPayload[]) {
201
+ return this.store.dispatch(new ReorderFields(sectionId, payload));
202
+ }
203
+
191
204
  moveField(sectionId: string, fieldId: string, payload: MoveFieldPayload) {
192
205
  return this.store.dispatch(new MoveField(sectionId, fieldId, payload));
193
206
  }
@@ -18,6 +18,7 @@ export enum FormBuilderActionKey {
18
18
  UpdateField = 'updateField',
19
19
  DeleteField = 'deleteField',
20
20
  MoveField = 'moveField',
21
+ ReorderFields = 'reorderFields',
21
22
  }
22
23
 
23
24
  // ============================================================================
@@ -96,6 +97,37 @@ export interface MoveFieldPayload {
96
97
  order?: number;
97
98
  }
98
99
 
100
+ export interface ReorderFieldPayload {
101
+ id: string;
102
+ order: number;
103
+ }
104
+
105
+ // ============================================================================
106
+ // Property Item (for enrichment)
107
+ // ============================================================================
108
+ export interface PropertyItem {
109
+ id: number;
110
+ name: string | Record<string, string>;
111
+ viewType?: string;
112
+ [key: string]: any;
113
+ }
114
+
115
+ // ============================================================================
116
+ // Enriched Types (for UI display)
117
+ // ============================================================================
118
+
119
+ // Enriched field with UI properties (from PropertyItem)
120
+ export interface EnrichedFormField extends FormField {
121
+ name: string;
122
+ type: string;
123
+ data?: any;
124
+ }
125
+
126
+ // Enriched section for UI display
127
+ export interface EnrichedFormSection extends Omit<FormSection, 'fields'> {
128
+ fields: EnrichedFormField[];
129
+ }
130
+
99
131
  // ============================================================================
100
132
  // State Model
101
133
  // ============================================================================
@@ -109,4 +141,7 @@ export interface FormBuilderStateModel extends LoadingStateShape<FormBuilderActi
109
141
 
110
142
  // Form data
111
143
  formConfiguration: FormConfiguration | null;
144
+
145
+ // Properties for field enrichment
146
+ properties: PropertyItem[];
112
147
  }